mirror of
https://github.com/Infisical/infisical.git
synced 2025-03-21 04:07:20 +00:00
Compare commits
245 Commits
groups-pha
...
maidul-pac
Author | SHA1 | Date | |
---|---|---|---|
7c77a4f049 | |||
9dfb587032 | |||
3952ad9a2e | |||
9c15cb407d | |||
cb17efa10b | |||
4adc2c4927 | |||
1a26b34ad8 | |||
21c339d27a | |||
1da4cf85f8 | |||
20f29c752d | |||
29ea12f8b1 | |||
b4f1cce587 | |||
5a92520ca3 | |||
42471b22bb | |||
79704e9c98 | |||
1165d11816 | |||
15ea96815c | |||
86d4d88b58 | |||
a12ad91e59 | |||
f74993e850 | |||
d0036a5656 | |||
e7f19421ef | |||
e18d830fe8 | |||
be2fc4fec4 | |||
829dbb9970 | |||
0b012c5dfb | |||
b0421ccad0 | |||
6b83326d00 | |||
1f6abc7f27 | |||
4a02520147 | |||
14f38eb961 | |||
37a59b2576 | |||
cebd22da8e | |||
d200405c6e | |||
3a1cdc4f44 | |||
2929d94f0a | |||
0383ae9e8b | |||
00faa6257f | |||
183bde55ca | |||
c96fc1f798 | |||
80f7ff1ea8 | |||
c87620109b | |||
02c158b4ed | |||
588f4bdb09 | |||
ddfa64eb33 | |||
7fdaa1543a | |||
c8433f39ed | |||
ba238a8f3b | |||
dd89a80449 | |||
a1585db76a | |||
f5f0bf3c83 | |||
3638645b8a | |||
f957b9d970 | |||
b461697fbf | |||
3ce91b8a20 | |||
8bab14a672 | |||
78922a80e2 | |||
0181007c66 | |||
306cf8733e | |||
6e829516db | |||
c08fcc6f5e | |||
9a585ad930 | |||
95c1fff7d3 | |||
9c2591f3a6 | |||
a579598b6d | |||
af0d31db2c | |||
fb6c4acf31 | |||
551ca0fa8c | |||
4a0ccbe69e | |||
f5a463ddea | |||
ce1ad6f32e | |||
56c8b4f5e5 | |||
29b26e3158 | |||
6e209bf099 | |||
949d210263 | |||
1a2d8e96f3 | |||
9198eb5fba | |||
0580f37c5e | |||
e53d40f0e5 | |||
801c0c5ada | |||
7b8af89bee | |||
ef7f5c9eac | |||
db0b4a5ad1 | |||
cb505d1525 | |||
c66476e2b4 | |||
60a06edd9b | |||
e8e1d46f0e | |||
038fe3508c | |||
7d1dff9e5a | |||
5117f5d3c1 | |||
350dd97b98 | |||
121902e51f | |||
923bf02046 | |||
27447ddc88 | |||
a3b4b650d1 | |||
3f0f45e853 | |||
3bb50b235d | |||
1afd120e8e | |||
ab3593af37 | |||
2c2afbea7a | |||
4eabbb3ac5 | |||
1ccd74e1a5 | |||
812cced9d5 | |||
cd6be68461 | |||
5c69bbf515 | |||
448f89fd1c | |||
3331699f56 | |||
810f670e64 | |||
5894df4370 | |||
2aacd54116 | |||
73d9fcc0de | |||
7ac3bb20df | |||
d659b5a624 | |||
0bbdf2a8f4 | |||
a8eba9cfbf | |||
a3d7c5f599 | |||
c325674da0 | |||
3637152a6b | |||
8ed3c0cd68 | |||
cdd836d58f | |||
3d3b1eb21a | |||
6aab28c4c7 | |||
f038b28c1c | |||
24a286e898 | |||
0c1103e778 | |||
2c1eecaf85 | |||
5884565de7 | |||
dd43268506 | |||
9d362b8597 | |||
972ecc3e92 | |||
dc3014409f | |||
4e449f62c0 | |||
c911a7cd81 | |||
44370d49e3 | |||
c7d2dfd351 | |||
1785548a40 | |||
2baf9e0739 | |||
01e7ed23ba | |||
1f789110e3 | |||
c874c943c1 | |||
dab69dcb51 | |||
8e82bfae86 | |||
bc810ea567 | |||
22470376d9 | |||
bb9503471f | |||
a687b1d0db | |||
0aa77f90c8 | |||
5a04371fb0 | |||
70c06c91c8 | |||
926d324ae3 | |||
e48377dea9 | |||
5e1484bd05 | |||
6d9de752d7 | |||
f9a9b1222e | |||
4326ce970a | |||
7a3a9ca9ea | |||
32a110e0ca | |||
da5278f6bf | |||
7e765681cb | |||
0990ce1f92 | |||
2369ff6813 | |||
478520f090 | |||
54313f9c08 | |||
cb8763bc9c | |||
c5d11eee7f | |||
8e1d19c041 | |||
608c7a4dee | |||
c7b60bcf0e | |||
6ae62675be | |||
fb2ab200b9 | |||
f1428d72c2 | |||
4cb51805f0 | |||
8c40918cef | |||
3a002b921a | |||
299653528c | |||
8c256bd9c8 | |||
f8e0e01bb8 | |||
b59413ded0 | |||
15c747e8e8 | |||
073a9ee6a4 | |||
d371c568f1 | |||
e6c086ab09 | |||
890c8b89be | |||
6f4b62cfbb | |||
076c70f6ff | |||
aedc1f2441 | |||
352d363bd4 | |||
ac92a916b4 | |||
17587ff1b8 | |||
7f1c8d9ff6 | |||
ac24c0f760 | |||
0e95c1bcee | |||
447630135b | |||
ddd6adf804 | |||
a4b6d2650a | |||
2f5d6b11da | |||
d380b7f788 | |||
7aee4fdfcd | |||
83bd3a0bf4 | |||
1f68730aa3 | |||
7fd1d72985 | |||
b298eec9db | |||
696479a2ef | |||
ad6e2aeb9e | |||
ad405109a0 | |||
992a82015a | |||
317956a038 | |||
5255c4075a | |||
eca36f1993 | |||
7e29a6a656 | |||
f458e34c37 | |||
99f5ed1f4b | |||
f981c59b5c | |||
a528d011c0 | |||
d337118803 | |||
68a11db1c6 | |||
91bf6a6dad | |||
12c655a152 | |||
1d2f10178f | |||
c5cd5047d7 | |||
06c103c10a | |||
b6a73459a8 | |||
536f51f6ba | |||
a9b72b2da3 | |||
e3c80309c3 | |||
ec3d6c20e8 | |||
5d7c0f30c8 | |||
a3552d00d1 | |||
c9f0ba08e1 | |||
308e605b6c | |||
4d8965eb82 | |||
0b089e6fa6 | |||
42383d5643 | |||
d198ba1a79 | |||
b3579cb271 | |||
fdd67c89b3 | |||
79e9b1b2ae | |||
86fd4d5fba | |||
4692aa12bd | |||
61a0997adc | |||
cbf8e041e9 | |||
5c4d35e30a | |||
d5c74d558a | |||
9c002ad645 | |||
c88923e0c6 |
.github/workflows
.infisicalignorebackend
package-lock.jsonpackage.json
src
@types
db
migrations
20240507032811_trusted-saml-ldap-emails.ts20240507162140_access-approval-policy.ts20240507162141_access.ts20240507210655_identity-aws-auth.ts
schemas
ee
routes/v1
access-approval-policy-router.tsaccess-approval-request-router.tsidentity-project-additional-privilege-router.tsindex.ts
services
access-approval-policy
access-approval-policy-approver-dal.tsaccess-approval-policy-dal.tsaccess-approval-policy-fns.tsaccess-approval-policy-service.tsaccess-approval-policy-types.ts
access-approval-request
access-approval-request-dal.tsaccess-approval-request-fns.tsaccess-approval-request-reviewer-dal.tsaccess-approval-request-service.tsaccess-approval-request-types.ts
audit-log
identity-project-additional-privilege
license
lib/api-docs
server
plugins/auth
routes
services
identity-aws-auth
identity-aws-auth-dal.tsidentity-aws-auth-fns.tsidentity-aws-auth-service.tsidentity-aws-auth-types.tsidentity-aws-auth-validators.ts
identity-ua
integration-auth
integration
org
secret
smtp
user
docs
documentation
getting-started
platform/identities
images/platform/identities
integrations/platforms
mint.jsonfrontend
package-lock.json
src
components
permissions
v2
Badge
Button
Divider
SecretPathInput
Select
hooks/api
accessApproval
identities
identityProjectAdditionalPrivilege
index.tsxintegrations
roles
secretFolders
types.tslayouts/AppLayout
lib/fn
pages
integrations/aws-parameter-store
login
project/[id]/secrets/v2
views
Org/MembersPage/components/OrgIdentityTab/components/IdentitySection
IdentityAuthMethodModal.tsxIdentityAwsAuthForm.tsxIdentityModal.tsxIdentityTable.tsxIdentityUniversalAuthForm.tsx
Project/MembersPage/components
IdentityTab/components/IdentityRoleForm
MemberListTab/MemberRoleForm
SecretApprovalPage
SecretApprovalPage.tsx
components
AccessApprovalPolicyList
AccessApprovalRequest
SecretApprovalPolicyList
SecretApprovalRequest
SecretMainPage/components
SecretOverviewPage
helm-charts/secrets-operator
k8-operator
api/v1alpha1
config
controllers
packages
pg-migrator/src/models/integration
@ -74,21 +74,21 @@ jobs:
|
||||
uses: pr-mpt/actions-commit-hash@v2
|
||||
- name: Download task definition
|
||||
run: |
|
||||
aws ecs describe-task-definition --task-definition infisical-prod-platform --query taskDefinition > task-definition.json
|
||||
aws ecs describe-task-definition --task-definition infisical-core-platform --query taskDefinition > task-definition.json
|
||||
- name: Render Amazon ECS task definition
|
||||
id: render-web-container
|
||||
uses: aws-actions/amazon-ecs-render-task-definition@v1
|
||||
with:
|
||||
task-definition: task-definition.json
|
||||
container-name: infisical-prod-platform
|
||||
container-name: infisical-core-platform
|
||||
image: infisical/staging_infisical:${{ steps.commit.outputs.short }}
|
||||
environment-variables: "LOG_LEVEL=info"
|
||||
- name: Deploy to Amazon ECS service
|
||||
uses: aws-actions/amazon-ecs-deploy-task-definition@v1
|
||||
with:
|
||||
task-definition: ${{ steps.render-web-container.outputs.task-definition }}
|
||||
service: infisical-prod-platform
|
||||
cluster: infisical-prod-platform
|
||||
service: infisical-core-platform
|
||||
cluster: infisical-core-platform
|
||||
wait-for-service-stability: true
|
||||
|
||||
production-postgres-deployment:
|
||||
@ -135,6 +135,6 @@ jobs:
|
||||
uses: aws-actions/amazon-ecs-deploy-task-definition@v1
|
||||
with:
|
||||
task-definition: ${{ steps.render-web-container.outputs.task-definition }}
|
||||
service: infisical-prod-platform
|
||||
cluster: infisical-prod-platform
|
||||
service: infisical-core-platform
|
||||
cluster: infisical-core-platform
|
||||
wait-for-service-stability: true
|
||||
|
@ -38,6 +38,16 @@ jobs:
|
||||
rm added_files.txt
|
||||
git commit -m "chore: renamed new migration files to latest timestamp (gh-action)"
|
||||
|
||||
- name: Get PR details
|
||||
id: pr_details
|
||||
run: |
|
||||
PR_NUMBER=${{ github.event.pull_request.number }}
|
||||
PR_MERGER=$(curl -s "https://api.github.com/repos/${{ github.repository }}/pulls/$PR_NUMBER" | jq -r '.merged_by.login')
|
||||
|
||||
echo "PR Number: $PR_NUMBER"
|
||||
echo "PR Merger: $PR_MERGER"
|
||||
echo "pr_merger=$PR_MERGER" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Create Pull Request
|
||||
if: env.SKIP_RENAME != 'true'
|
||||
uses: peter-evans/create-pull-request@v6
|
||||
@ -46,3 +56,4 @@ jobs:
|
||||
commit-message: 'chore: renamed new migration files to latest UTC (gh-action)'
|
||||
title: 'GH Action: rename new migration file timestamp'
|
||||
branch-suffix: timestamp
|
||||
reviewers: ${{ steps.pr_details.outputs.pr_merger }}
|
||||
|
@ -4,3 +4,4 @@ frontend/src/views/Project/MembersPage/components/IdentityTab/components/Identit
|
||||
frontend/src/views/Project/MembersPage/components/MemberListTab/MemberRoleForm/MemberRbacSection.tsx:generic-api-key:206
|
||||
frontend/src/views/Project/MembersPage/components/MemberListTab/MemberRoleForm/SpecificPrivilegeSection.tsx:generic-api-key:292
|
||||
docs/self-hosting/configuration/envars.mdx:generic-api-key:106
|
||||
frontend/src/views/Project/MembersPage/components/MemberListTab/MemberRoleForm/SpecificPrivilegeSection.tsx:generic-api-key:451
|
||||
|
672
backend/package-lock.json
generated
672
backend/package-lock.json
generated
@ -49,7 +49,7 @@
|
||||
"libsodium-wrappers": "^0.7.13",
|
||||
"lodash.isequal": "^4.5.0",
|
||||
"ms": "^2.1.3",
|
||||
"mysql2": "^3.9.4",
|
||||
"mysql2": "^3.9.7",
|
||||
"nanoid": "^5.0.4",
|
||||
"nodemailer": "^6.9.9",
|
||||
"ora": "^7.0.1",
|
||||
@ -1207,6 +1207,58 @@
|
||||
"node": ">=14.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@aws-sdk/client-secrets-manager/node_modules/@aws-sdk/client-sts": {
|
||||
"version": "3.504.0",
|
||||
"resolved": "https://registry.npmjs.org/@aws-sdk/client-sts/-/client-sts-3.504.0.tgz",
|
||||
"integrity": "sha512-IESs8FkL7B/uY+ml4wgoRkrr6xYo4PizcNw6JX17eveq1gRBCPKeGMjE6HTDOcIYZZ8rqz/UeuH3JD4UhrMOnA==",
|
||||
"dependencies": {
|
||||
"@aws-crypto/sha256-browser": "3.0.0",
|
||||
"@aws-crypto/sha256-js": "3.0.0",
|
||||
"@aws-sdk/core": "3.496.0",
|
||||
"@aws-sdk/middleware-host-header": "3.502.0",
|
||||
"@aws-sdk/middleware-logger": "3.502.0",
|
||||
"@aws-sdk/middleware-recursion-detection": "3.502.0",
|
||||
"@aws-sdk/middleware-user-agent": "3.502.0",
|
||||
"@aws-sdk/region-config-resolver": "3.502.0",
|
||||
"@aws-sdk/types": "3.502.0",
|
||||
"@aws-sdk/util-endpoints": "3.502.0",
|
||||
"@aws-sdk/util-user-agent-browser": "3.502.0",
|
||||
"@aws-sdk/util-user-agent-node": "3.502.0",
|
||||
"@smithy/config-resolver": "^2.1.1",
|
||||
"@smithy/core": "^1.3.1",
|
||||
"@smithy/fetch-http-handler": "^2.4.1",
|
||||
"@smithy/hash-node": "^2.1.1",
|
||||
"@smithy/invalid-dependency": "^2.1.1",
|
||||
"@smithy/middleware-content-length": "^2.1.1",
|
||||
"@smithy/middleware-endpoint": "^2.4.1",
|
||||
"@smithy/middleware-retry": "^2.1.1",
|
||||
"@smithy/middleware-serde": "^2.1.1",
|
||||
"@smithy/middleware-stack": "^2.1.1",
|
||||
"@smithy/node-config-provider": "^2.2.1",
|
||||
"@smithy/node-http-handler": "^2.3.1",
|
||||
"@smithy/protocol-http": "^3.1.1",
|
||||
"@smithy/smithy-client": "^2.3.1",
|
||||
"@smithy/types": "^2.9.1",
|
||||
"@smithy/url-parser": "^2.1.1",
|
||||
"@smithy/util-base64": "^2.1.1",
|
||||
"@smithy/util-body-length-browser": "^2.1.1",
|
||||
"@smithy/util-body-length-node": "^2.2.1",
|
||||
"@smithy/util-defaults-mode-browser": "^2.1.1",
|
||||
"@smithy/util-defaults-mode-node": "^2.1.1",
|
||||
"@smithy/util-endpoints": "^1.1.1",
|
||||
"@smithy/util-middleware": "^2.1.1",
|
||||
"@smithy/util-retry": "^2.1.1",
|
||||
"@smithy/util-utf8": "^2.1.1",
|
||||
"fast-xml-parser": "4.2.5",
|
||||
"tslib": "^2.5.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@aws-sdk/credential-provider-node": "^3.504.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@aws-sdk/client-secrets-manager/node_modules/uuid": {
|
||||
"version": "8.3.2",
|
||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
|
||||
@ -1314,7 +1366,7 @@
|
||||
"@aws-sdk/credential-provider-node": "^3.504.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@aws-sdk/client-sts": {
|
||||
"node_modules/@aws-sdk/client-sso-oidc/node_modules/@aws-sdk/client-sts": {
|
||||
"version": "3.504.0",
|
||||
"resolved": "https://registry.npmjs.org/@aws-sdk/client-sts/-/client-sts-3.504.0.tgz",
|
||||
"integrity": "sha512-IESs8FkL7B/uY+ml4wgoRkrr6xYo4PizcNw6JX17eveq1gRBCPKeGMjE6HTDOcIYZZ8rqz/UeuH3JD4UhrMOnA==",
|
||||
@ -1436,6 +1488,58 @@
|
||||
"node": ">=14.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@aws-sdk/credential-provider-ini/node_modules/@aws-sdk/client-sts": {
|
||||
"version": "3.504.0",
|
||||
"resolved": "https://registry.npmjs.org/@aws-sdk/client-sts/-/client-sts-3.504.0.tgz",
|
||||
"integrity": "sha512-IESs8FkL7B/uY+ml4wgoRkrr6xYo4PizcNw6JX17eveq1gRBCPKeGMjE6HTDOcIYZZ8rqz/UeuH3JD4UhrMOnA==",
|
||||
"dependencies": {
|
||||
"@aws-crypto/sha256-browser": "3.0.0",
|
||||
"@aws-crypto/sha256-js": "3.0.0",
|
||||
"@aws-sdk/core": "3.496.0",
|
||||
"@aws-sdk/middleware-host-header": "3.502.0",
|
||||
"@aws-sdk/middleware-logger": "3.502.0",
|
||||
"@aws-sdk/middleware-recursion-detection": "3.502.0",
|
||||
"@aws-sdk/middleware-user-agent": "3.502.0",
|
||||
"@aws-sdk/region-config-resolver": "3.502.0",
|
||||
"@aws-sdk/types": "3.502.0",
|
||||
"@aws-sdk/util-endpoints": "3.502.0",
|
||||
"@aws-sdk/util-user-agent-browser": "3.502.0",
|
||||
"@aws-sdk/util-user-agent-node": "3.502.0",
|
||||
"@smithy/config-resolver": "^2.1.1",
|
||||
"@smithy/core": "^1.3.1",
|
||||
"@smithy/fetch-http-handler": "^2.4.1",
|
||||
"@smithy/hash-node": "^2.1.1",
|
||||
"@smithy/invalid-dependency": "^2.1.1",
|
||||
"@smithy/middleware-content-length": "^2.1.1",
|
||||
"@smithy/middleware-endpoint": "^2.4.1",
|
||||
"@smithy/middleware-retry": "^2.1.1",
|
||||
"@smithy/middleware-serde": "^2.1.1",
|
||||
"@smithy/middleware-stack": "^2.1.1",
|
||||
"@smithy/node-config-provider": "^2.2.1",
|
||||
"@smithy/node-http-handler": "^2.3.1",
|
||||
"@smithy/protocol-http": "^3.1.1",
|
||||
"@smithy/smithy-client": "^2.3.1",
|
||||
"@smithy/types": "^2.9.1",
|
||||
"@smithy/url-parser": "^2.1.1",
|
||||
"@smithy/util-base64": "^2.1.1",
|
||||
"@smithy/util-body-length-browser": "^2.1.1",
|
||||
"@smithy/util-body-length-node": "^2.2.1",
|
||||
"@smithy/util-defaults-mode-browser": "^2.1.1",
|
||||
"@smithy/util-defaults-mode-node": "^2.1.1",
|
||||
"@smithy/util-endpoints": "^1.1.1",
|
||||
"@smithy/util-middleware": "^2.1.1",
|
||||
"@smithy/util-retry": "^2.1.1",
|
||||
"@smithy/util-utf8": "^2.1.1",
|
||||
"fast-xml-parser": "4.2.5",
|
||||
"tslib": "^2.5.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@aws-sdk/credential-provider-node": "^3.504.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@aws-sdk/credential-provider-node": {
|
||||
"version": "3.504.0",
|
||||
"resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.504.0.tgz",
|
||||
@ -1505,6 +1609,58 @@
|
||||
"node": ">=14.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@aws-sdk/credential-provider-web-identity/node_modules/@aws-sdk/client-sts": {
|
||||
"version": "3.504.0",
|
||||
"resolved": "https://registry.npmjs.org/@aws-sdk/client-sts/-/client-sts-3.504.0.tgz",
|
||||
"integrity": "sha512-IESs8FkL7B/uY+ml4wgoRkrr6xYo4PizcNw6JX17eveq1gRBCPKeGMjE6HTDOcIYZZ8rqz/UeuH3JD4UhrMOnA==",
|
||||
"dependencies": {
|
||||
"@aws-crypto/sha256-browser": "3.0.0",
|
||||
"@aws-crypto/sha256-js": "3.0.0",
|
||||
"@aws-sdk/core": "3.496.0",
|
||||
"@aws-sdk/middleware-host-header": "3.502.0",
|
||||
"@aws-sdk/middleware-logger": "3.502.0",
|
||||
"@aws-sdk/middleware-recursion-detection": "3.502.0",
|
||||
"@aws-sdk/middleware-user-agent": "3.502.0",
|
||||
"@aws-sdk/region-config-resolver": "3.502.0",
|
||||
"@aws-sdk/types": "3.502.0",
|
||||
"@aws-sdk/util-endpoints": "3.502.0",
|
||||
"@aws-sdk/util-user-agent-browser": "3.502.0",
|
||||
"@aws-sdk/util-user-agent-node": "3.502.0",
|
||||
"@smithy/config-resolver": "^2.1.1",
|
||||
"@smithy/core": "^1.3.1",
|
||||
"@smithy/fetch-http-handler": "^2.4.1",
|
||||
"@smithy/hash-node": "^2.1.1",
|
||||
"@smithy/invalid-dependency": "^2.1.1",
|
||||
"@smithy/middleware-content-length": "^2.1.1",
|
||||
"@smithy/middleware-endpoint": "^2.4.1",
|
||||
"@smithy/middleware-retry": "^2.1.1",
|
||||
"@smithy/middleware-serde": "^2.1.1",
|
||||
"@smithy/middleware-stack": "^2.1.1",
|
||||
"@smithy/node-config-provider": "^2.2.1",
|
||||
"@smithy/node-http-handler": "^2.3.1",
|
||||
"@smithy/protocol-http": "^3.1.1",
|
||||
"@smithy/smithy-client": "^2.3.1",
|
||||
"@smithy/types": "^2.9.1",
|
||||
"@smithy/url-parser": "^2.1.1",
|
||||
"@smithy/util-base64": "^2.1.1",
|
||||
"@smithy/util-body-length-browser": "^2.1.1",
|
||||
"@smithy/util-body-length-node": "^2.2.1",
|
||||
"@smithy/util-defaults-mode-browser": "^2.1.1",
|
||||
"@smithy/util-defaults-mode-node": "^2.1.1",
|
||||
"@smithy/util-endpoints": "^1.1.1",
|
||||
"@smithy/util-middleware": "^2.1.1",
|
||||
"@smithy/util-retry": "^2.1.1",
|
||||
"@smithy/util-utf8": "^2.1.1",
|
||||
"fast-xml-parser": "4.2.5",
|
||||
"tslib": "^2.5.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@aws-sdk/credential-provider-node": "^3.504.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@aws-sdk/middleware-host-header": {
|
||||
"version": "3.502.0",
|
||||
"resolved": "https://registry.npmjs.org/@aws-sdk/middleware-host-header/-/middleware-host-header-3.502.0.tgz",
|
||||
@ -3657,60 +3813,60 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@smithy/abort-controller": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/@smithy/abort-controller/-/abort-controller-2.1.3.tgz",
|
||||
"integrity": "sha512-c2aYH2Wu1RVE3rLlVgg2kQOBJGM0WbjReQi5DnPTm2Zb7F0gk7J2aeQeaX2u/lQZoHl6gv8Oac7mt9alU3+f4A==",
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@smithy/abort-controller/-/abort-controller-2.2.0.tgz",
|
||||
"integrity": "sha512-wRlta7GuLWpTqtFfGo+nZyOO1vEvewdNR1R4rTxpC8XU6vG/NDyrFBhwLZsqg1NUoR1noVaXJPC/7ZK47QCySw==",
|
||||
"dependencies": {
|
||||
"@smithy/types": "^2.10.1",
|
||||
"tslib": "^2.5.0"
|
||||
"@smithy/types": "^2.12.0",
|
||||
"tslib": "^2.6.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@smithy/config-resolver": {
|
||||
"version": "2.1.4",
|
||||
"resolved": "https://registry.npmjs.org/@smithy/config-resolver/-/config-resolver-2.1.4.tgz",
|
||||
"integrity": "sha512-AW2WUZmBAzgO3V3ovKtsUbI3aBNMeQKFDumoqkNxaVDWF/xfnxAWqBKDr/NuG7c06N2Rm4xeZLPiJH/d+na0HA==",
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@smithy/config-resolver/-/config-resolver-2.2.0.tgz",
|
||||
"integrity": "sha512-fsiMgd8toyUba6n1WRmr+qACzXltpdDkPTAaDqc8QqPBUzO+/JKwL6bUBseHVi8tu9l+3JOK+tSf7cay+4B3LA==",
|
||||
"dependencies": {
|
||||
"@smithy/node-config-provider": "^2.2.4",
|
||||
"@smithy/types": "^2.10.1",
|
||||
"@smithy/util-config-provider": "^2.2.1",
|
||||
"@smithy/util-middleware": "^2.1.3",
|
||||
"tslib": "^2.5.0"
|
||||
"@smithy/node-config-provider": "^2.3.0",
|
||||
"@smithy/types": "^2.12.0",
|
||||
"@smithy/util-config-provider": "^2.3.0",
|
||||
"@smithy/util-middleware": "^2.2.0",
|
||||
"tslib": "^2.6.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@smithy/core": {
|
||||
"version": "1.3.5",
|
||||
"resolved": "https://registry.npmjs.org/@smithy/core/-/core-1.3.5.tgz",
|
||||
"integrity": "sha512-Rrc+e2Jj6Gu7Xbn0jvrzZlSiP2CZocIOfZ9aNUA82+1sa6GBnxqL9+iZ9EKHeD9aqD1nU8EK4+oN2EiFpSv7Yw==",
|
||||
"version": "1.4.2",
|
||||
"resolved": "https://registry.npmjs.org/@smithy/core/-/core-1.4.2.tgz",
|
||||
"integrity": "sha512-2fek3I0KZHWJlRLvRTqxTEri+qV0GRHrJIoLFuBMZB4EMg4WgeBGfF0X6abnrNYpq55KJ6R4D6x4f0vLnhzinA==",
|
||||
"dependencies": {
|
||||
"@smithy/middleware-endpoint": "^2.4.4",
|
||||
"@smithy/middleware-retry": "^2.1.4",
|
||||
"@smithy/middleware-serde": "^2.1.3",
|
||||
"@smithy/protocol-http": "^3.2.1",
|
||||
"@smithy/smithy-client": "^2.4.2",
|
||||
"@smithy/types": "^2.10.1",
|
||||
"@smithy/util-middleware": "^2.1.3",
|
||||
"tslib": "^2.5.0"
|
||||
"@smithy/middleware-endpoint": "^2.5.1",
|
||||
"@smithy/middleware-retry": "^2.3.1",
|
||||
"@smithy/middleware-serde": "^2.3.0",
|
||||
"@smithy/protocol-http": "^3.3.0",
|
||||
"@smithy/smithy-client": "^2.5.1",
|
||||
"@smithy/types": "^2.12.0",
|
||||
"@smithy/util-middleware": "^2.2.0",
|
||||
"tslib": "^2.6.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@smithy/credential-provider-imds": {
|
||||
"version": "2.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-2.2.4.tgz",
|
||||
"integrity": "sha512-DdatjmBZQnhGe1FhI8gO98f7NmvQFSDiZTwC3WMvLTCKQUY+Y1SVkhJqIuLu50Eb7pTheoXQmK+hKYUgpUWsNA==",
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-2.3.0.tgz",
|
||||
"integrity": "sha512-BWB9mIukO1wjEOo1Ojgl6LrG4avcaC7T/ZP6ptmAaW4xluhSIPZhY+/PI5YKzlk+jsm+4sQZB45Bt1OfMeQa3w==",
|
||||
"dependencies": {
|
||||
"@smithy/node-config-provider": "^2.2.4",
|
||||
"@smithy/property-provider": "^2.1.3",
|
||||
"@smithy/types": "^2.10.1",
|
||||
"@smithy/url-parser": "^2.1.3",
|
||||
"tslib": "^2.5.0"
|
||||
"@smithy/node-config-provider": "^2.3.0",
|
||||
"@smithy/property-provider": "^2.2.0",
|
||||
"@smithy/types": "^2.12.0",
|
||||
"@smithy/url-parser": "^2.2.0",
|
||||
"tslib": "^2.6.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
@ -3779,459 +3935,451 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@smithy/fetch-http-handler": {
|
||||
"version": "2.4.3",
|
||||
"resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-2.4.3.tgz",
|
||||
"integrity": "sha512-Fn/KYJFo6L5I4YPG8WQb2hOmExgRmNpVH5IK2zU3JKrY5FKW7y9ar5e0BexiIC9DhSKqKX+HeWq/Y18fq7Dkpw==",
|
||||
"version": "2.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-2.5.0.tgz",
|
||||
"integrity": "sha512-BOWEBeppWhLn/no/JxUL/ghTfANTjT7kg3Ww2rPqTUY9R4yHPXxJ9JhMe3Z03LN3aPwiwlpDIUcVw1xDyHqEhw==",
|
||||
"dependencies": {
|
||||
"@smithy/protocol-http": "^3.2.1",
|
||||
"@smithy/querystring-builder": "^2.1.3",
|
||||
"@smithy/types": "^2.10.1",
|
||||
"@smithy/util-base64": "^2.1.1",
|
||||
"tslib": "^2.5.0"
|
||||
"@smithy/protocol-http": "^3.3.0",
|
||||
"@smithy/querystring-builder": "^2.2.0",
|
||||
"@smithy/types": "^2.12.0",
|
||||
"@smithy/util-base64": "^2.3.0",
|
||||
"tslib": "^2.6.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@smithy/hash-node": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/@smithy/hash-node/-/hash-node-2.1.3.tgz",
|
||||
"integrity": "sha512-FsAPCUj7VNJIdHbSxMd5uiZiF20G2zdSDgrgrDrHqIs/VMxK85Vqk5kMVNNDMCZmMezp6UKnac0B4nAyx7HJ9g==",
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@smithy/hash-node/-/hash-node-2.2.0.tgz",
|
||||
"integrity": "sha512-zLWaC/5aWpMrHKpoDF6nqpNtBhlAYKF/7+9yMN7GpdR8CzohnWfGtMznPybnwSS8saaXBMxIGwJqR4HmRp6b3g==",
|
||||
"dependencies": {
|
||||
"@smithy/types": "^2.10.1",
|
||||
"@smithy/util-buffer-from": "^2.1.1",
|
||||
"@smithy/util-utf8": "^2.1.1",
|
||||
"tslib": "^2.5.0"
|
||||
"@smithy/types": "^2.12.0",
|
||||
"@smithy/util-buffer-from": "^2.2.0",
|
||||
"@smithy/util-utf8": "^2.3.0",
|
||||
"tslib": "^2.6.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@smithy/invalid-dependency": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/@smithy/invalid-dependency/-/invalid-dependency-2.1.3.tgz",
|
||||
"integrity": "sha512-wkra7d/G4CbngV4xsjYyAYOvdAhahQje/WymuQdVEnXFExJopEu7fbL5AEAlBPgWHXwu94VnCSG00gVzRfExyg==",
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@smithy/invalid-dependency/-/invalid-dependency-2.2.0.tgz",
|
||||
"integrity": "sha512-nEDASdbKFKPXN2O6lOlTgrEEOO9NHIeO+HVvZnkqc8h5U9g3BIhWsvzFo+UcUbliMHvKNPD/zVxDrkP1Sbgp8Q==",
|
||||
"dependencies": {
|
||||
"@smithy/types": "^2.10.1",
|
||||
"tslib": "^2.5.0"
|
||||
"@smithy/types": "^2.12.0",
|
||||
"tslib": "^2.6.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@smithy/is-array-buffer": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-2.1.1.tgz",
|
||||
"integrity": "sha512-xozSQrcUinPpNPNPds4S7z/FakDTh1MZWtRP/2vQtYB/u3HYrX2UXuZs+VhaKBd6Vc7g2XPr2ZtwGBNDN6fNKQ==",
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-2.2.0.tgz",
|
||||
"integrity": "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==",
|
||||
"dependencies": {
|
||||
"tslib": "^2.5.0"
|
||||
"tslib": "^2.6.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@smithy/middleware-content-length": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/@smithy/middleware-content-length/-/middleware-content-length-2.1.3.tgz",
|
||||
"integrity": "sha512-aJduhkC+dcXxdnv5ZpM3uMmtGmVFKx412R1gbeykS5HXDmRU6oSsyy2SoHENCkfOGKAQOjVE2WVqDJibC0d21g==",
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@smithy/middleware-content-length/-/middleware-content-length-2.2.0.tgz",
|
||||
"integrity": "sha512-5bl2LG1Ah/7E5cMSC+q+h3IpVHMeOkG0yLRyQT1p2aMJkSrZG7RlXHPuAgb7EyaFeidKEnnd/fNaLLaKlHGzDQ==",
|
||||
"dependencies": {
|
||||
"@smithy/protocol-http": "^3.2.1",
|
||||
"@smithy/types": "^2.10.1",
|
||||
"tslib": "^2.5.0"
|
||||
"@smithy/protocol-http": "^3.3.0",
|
||||
"@smithy/types": "^2.12.0",
|
||||
"tslib": "^2.6.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@smithy/middleware-endpoint": {
|
||||
"version": "2.4.4",
|
||||
"resolved": "https://registry.npmjs.org/@smithy/middleware-endpoint/-/middleware-endpoint-2.4.4.tgz",
|
||||
"integrity": "sha512-4yjHyHK2Jul4JUDBo2sTsWY9UshYUnXeb/TAK/MTaPEb8XQvDmpwSFnfIRDU45RY1a6iC9LCnmJNg/yHyfxqkw==",
|
||||
"version": "2.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@smithy/middleware-endpoint/-/middleware-endpoint-2.5.1.tgz",
|
||||
"integrity": "sha512-1/8kFp6Fl4OsSIVTWHnNjLnTL8IqpIb/D3sTSczrKFnrE9VMNWxnrRKNvpUHOJ6zpGD5f62TPm7+17ilTJpiCQ==",
|
||||
"dependencies": {
|
||||
"@smithy/middleware-serde": "^2.1.3",
|
||||
"@smithy/node-config-provider": "^2.2.4",
|
||||
"@smithy/shared-ini-file-loader": "^2.3.4",
|
||||
"@smithy/types": "^2.10.1",
|
||||
"@smithy/url-parser": "^2.1.3",
|
||||
"@smithy/util-middleware": "^2.1.3",
|
||||
"tslib": "^2.5.0"
|
||||
"@smithy/middleware-serde": "^2.3.0",
|
||||
"@smithy/node-config-provider": "^2.3.0",
|
||||
"@smithy/shared-ini-file-loader": "^2.4.0",
|
||||
"@smithy/types": "^2.12.0",
|
||||
"@smithy/url-parser": "^2.2.0",
|
||||
"@smithy/util-middleware": "^2.2.0",
|
||||
"tslib": "^2.6.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@smithy/middleware-retry": {
|
||||
"version": "2.1.4",
|
||||
"resolved": "https://registry.npmjs.org/@smithy/middleware-retry/-/middleware-retry-2.1.4.tgz",
|
||||
"integrity": "sha512-Cyolv9YckZTPli1EkkaS39UklonxMd08VskiuMhURDjC0HHa/AD6aK/YoD21CHv9s0QLg0WMLvk9YeLTKkXaFQ==",
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@smithy/middleware-retry/-/middleware-retry-2.3.1.tgz",
|
||||
"integrity": "sha512-P2bGufFpFdYcWvqpyqqmalRtwFUNUA8vHjJR5iGqbfR6mp65qKOLcUd6lTr4S9Gn/enynSrSf3p3FVgVAf6bXA==",
|
||||
"dependencies": {
|
||||
"@smithy/node-config-provider": "^2.2.4",
|
||||
"@smithy/protocol-http": "^3.2.1",
|
||||
"@smithy/service-error-classification": "^2.1.3",
|
||||
"@smithy/smithy-client": "^2.4.2",
|
||||
"@smithy/types": "^2.10.1",
|
||||
"@smithy/util-middleware": "^2.1.3",
|
||||
"@smithy/util-retry": "^2.1.3",
|
||||
"tslib": "^2.5.0",
|
||||
"uuid": "^8.3.2"
|
||||
"@smithy/node-config-provider": "^2.3.0",
|
||||
"@smithy/protocol-http": "^3.3.0",
|
||||
"@smithy/service-error-classification": "^2.1.5",
|
||||
"@smithy/smithy-client": "^2.5.1",
|
||||
"@smithy/types": "^2.12.0",
|
||||
"@smithy/util-middleware": "^2.2.0",
|
||||
"@smithy/util-retry": "^2.2.0",
|
||||
"tslib": "^2.6.2",
|
||||
"uuid": "^9.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@smithy/middleware-retry/node_modules/uuid": {
|
||||
"version": "8.3.2",
|
||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
|
||||
"integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==",
|
||||
"bin": {
|
||||
"uuid": "dist/bin/uuid"
|
||||
}
|
||||
},
|
||||
"node_modules/@smithy/middleware-serde": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/@smithy/middleware-serde/-/middleware-serde-2.1.3.tgz",
|
||||
"integrity": "sha512-s76LId+TwASrHhUa9QS4k/zeXDUAuNuddKklQzRgumbzge5BftVXHXIqL4wQxKGLocPwfgAOXWx+HdWhQk9hTg==",
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@smithy/middleware-serde/-/middleware-serde-2.3.0.tgz",
|
||||
"integrity": "sha512-sIADe7ojwqTyvEQBe1nc/GXB9wdHhi9UwyX0lTyttmUWDJLP655ZYE1WngnNyXREme8I27KCaUhyhZWRXL0q7Q==",
|
||||
"dependencies": {
|
||||
"@smithy/types": "^2.10.1",
|
||||
"tslib": "^2.5.0"
|
||||
"@smithy/types": "^2.12.0",
|
||||
"tslib": "^2.6.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@smithy/middleware-stack": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/@smithy/middleware-stack/-/middleware-stack-2.1.3.tgz",
|
||||
"integrity": "sha512-opMFufVQgvBSld/b7mD7OOEBxF6STyraVr1xel1j0abVILM8ALJvRoFbqSWHGmaDlRGIiV9Q5cGbWi0sdiEaLQ==",
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@smithy/middleware-stack/-/middleware-stack-2.2.0.tgz",
|
||||
"integrity": "sha512-Qntc3jrtwwrsAC+X8wms8zhrTr0sFXnyEGhZd9sLtsJ/6gGQKFzNB+wWbOcpJd7BR8ThNCoKt76BuQahfMvpeA==",
|
||||
"dependencies": {
|
||||
"@smithy/types": "^2.10.1",
|
||||
"tslib": "^2.5.0"
|
||||
"@smithy/types": "^2.12.0",
|
||||
"tslib": "^2.6.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@smithy/node-config-provider": {
|
||||
"version": "2.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@smithy/node-config-provider/-/node-config-provider-2.2.4.tgz",
|
||||
"integrity": "sha512-nqazHCp8r4KHSFhRQ+T0VEkeqvA0U+RhehBSr1gunUuNW3X7j0uDrWBxB2gE9eutzy6kE3Y7L+Dov/UXT871vg==",
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@smithy/node-config-provider/-/node-config-provider-2.3.0.tgz",
|
||||
"integrity": "sha512-0elK5/03a1JPWMDPaS726Iw6LpQg80gFut1tNpPfxFuChEEklo2yL823V94SpTZTxmKlXFtFgsP55uh3dErnIg==",
|
||||
"dependencies": {
|
||||
"@smithy/property-provider": "^2.1.3",
|
||||
"@smithy/shared-ini-file-loader": "^2.3.4",
|
||||
"@smithy/types": "^2.10.1",
|
||||
"tslib": "^2.5.0"
|
||||
"@smithy/property-provider": "^2.2.0",
|
||||
"@smithy/shared-ini-file-loader": "^2.4.0",
|
||||
"@smithy/types": "^2.12.0",
|
||||
"tslib": "^2.6.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@smithy/node-http-handler": {
|
||||
"version": "2.4.1",
|
||||
"resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-2.4.1.tgz",
|
||||
"integrity": "sha512-HCkb94soYhJMxPCa61wGKgmeKpJ3Gftx1XD6bcWEB2wMV1L9/SkQu/6/ysKBnbOzWRE01FGzwrTxucHypZ8rdg==",
|
||||
"version": "2.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-2.5.0.tgz",
|
||||
"integrity": "sha512-mVGyPBzkkGQsPoxQUbxlEfRjrj6FPyA3u3u2VXGr9hT8wilsoQdZdvKpMBFMB8Crfhv5dNkKHIW0Yyuc7eABqA==",
|
||||
"dependencies": {
|
||||
"@smithy/abort-controller": "^2.1.3",
|
||||
"@smithy/protocol-http": "^3.2.1",
|
||||
"@smithy/querystring-builder": "^2.1.3",
|
||||
"@smithy/types": "^2.10.1",
|
||||
"tslib": "^2.5.0"
|
||||
"@smithy/abort-controller": "^2.2.0",
|
||||
"@smithy/protocol-http": "^3.3.0",
|
||||
"@smithy/querystring-builder": "^2.2.0",
|
||||
"@smithy/types": "^2.12.0",
|
||||
"tslib": "^2.6.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@smithy/property-provider": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/@smithy/property-provider/-/property-provider-2.1.3.tgz",
|
||||
"integrity": "sha512-bMz3se+ySKWNrgm7eIiQMa2HO/0fl2D0HvLAdg9pTMcpgp4SqOAh6bz7Ik6y7uQqSrk4rLjIKgbQ6yzYgGehCQ==",
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@smithy/property-provider/-/property-provider-2.2.0.tgz",
|
||||
"integrity": "sha512-+xiil2lFhtTRzXkx8F053AV46QnIw6e7MV8od5Mi68E1ICOjCeCHw2XfLnDEUHnT9WGUIkwcqavXjfwuJbGlpg==",
|
||||
"dependencies": {
|
||||
"@smithy/types": "^2.10.1",
|
||||
"tslib": "^2.5.0"
|
||||
"@smithy/types": "^2.12.0",
|
||||
"tslib": "^2.6.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@smithy/protocol-http": {
|
||||
"version": "3.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@smithy/protocol-http/-/protocol-http-3.2.1.tgz",
|
||||
"integrity": "sha512-KLrQkEw4yJCeAmAH7hctE8g9KwA7+H2nSJwxgwIxchbp/L0B5exTdOQi9D5HinPLlothoervGmhpYKelZ6AxIA==",
|
||||
"version": "3.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@smithy/protocol-http/-/protocol-http-3.3.0.tgz",
|
||||
"integrity": "sha512-Xy5XK1AFWW2nlY/biWZXu6/krgbaf2dg0q492D8M5qthsnU2H+UgFeZLbM76FnH7s6RO/xhQRkj+T6KBO3JzgQ==",
|
||||
"dependencies": {
|
||||
"@smithy/types": "^2.10.1",
|
||||
"tslib": "^2.5.0"
|
||||
"@smithy/types": "^2.12.0",
|
||||
"tslib": "^2.6.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@smithy/querystring-builder": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/@smithy/querystring-builder/-/querystring-builder-2.1.3.tgz",
|
||||
"integrity": "sha512-kFD3PnNqKELe6m9GRHQw/ftFFSZpnSeQD4qvgDB6BQN6hREHELSosVFUMPN4M3MDKN2jAwk35vXHLoDrNfKu0A==",
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@smithy/querystring-builder/-/querystring-builder-2.2.0.tgz",
|
||||
"integrity": "sha512-L1kSeviUWL+emq3CUVSgdogoM/D9QMFaqxL/dd0X7PCNWmPXqt+ExtrBjqT0V7HLN03Vs9SuiLrG3zy3JGnE5A==",
|
||||
"dependencies": {
|
||||
"@smithy/types": "^2.10.1",
|
||||
"@smithy/util-uri-escape": "^2.1.1",
|
||||
"tslib": "^2.5.0"
|
||||
"@smithy/types": "^2.12.0",
|
||||
"@smithy/util-uri-escape": "^2.2.0",
|
||||
"tslib": "^2.6.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@smithy/querystring-parser": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/@smithy/querystring-parser/-/querystring-parser-2.1.3.tgz",
|
||||
"integrity": "sha512-3+CWJoAqcBMR+yvz6D+Fc5VdoGFtfenW6wqSWATWajrRMGVwJGPT3Vy2eb2bnMktJc4HU4bpjeovFa566P3knQ==",
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@smithy/querystring-parser/-/querystring-parser-2.2.0.tgz",
|
||||
"integrity": "sha512-BvHCDrKfbG5Yhbpj4vsbuPV2GgcpHiAkLeIlcA1LtfpMz3jrqizP1+OguSNSj1MwBHEiN+jwNisXLGdajGDQJA==",
|
||||
"dependencies": {
|
||||
"@smithy/types": "^2.10.1",
|
||||
"tslib": "^2.5.0"
|
||||
"@smithy/types": "^2.12.0",
|
||||
"tslib": "^2.6.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@smithy/service-error-classification": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/@smithy/service-error-classification/-/service-error-classification-2.1.3.tgz",
|
||||
"integrity": "sha512-iUrpSsem97bbXHHT/v3s7vaq8IIeMo6P6cXdeYHrx0wOJpMeBGQF7CB0mbJSiTm3//iq3L55JiEm8rA7CTVI8A==",
|
||||
"version": "2.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@smithy/service-error-classification/-/service-error-classification-2.1.5.tgz",
|
||||
"integrity": "sha512-uBDTIBBEdAQryvHdc5W8sS5YX7RQzF683XrHePVdFmAgKiMofU15FLSM0/HU03hKTnazdNRFa0YHS7+ArwoUSQ==",
|
||||
"dependencies": {
|
||||
"@smithy/types": "^2.10.1"
|
||||
"@smithy/types": "^2.12.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@smithy/shared-ini-file-loader": {
|
||||
"version": "2.3.4",
|
||||
"resolved": "https://registry.npmjs.org/@smithy/shared-ini-file-loader/-/shared-ini-file-loader-2.3.4.tgz",
|
||||
"integrity": "sha512-CiZmPg9GeDKbKmJGEFvJBsJcFnh0AQRzOtQAzj1XEa8N/0/uSN/v1LYzgO7ry8hhO8+9KB7+DhSW0weqBra4Aw==",
|
||||
"version": "2.4.0",
|
||||
"resolved": "https://registry.npmjs.org/@smithy/shared-ini-file-loader/-/shared-ini-file-loader-2.4.0.tgz",
|
||||
"integrity": "sha512-WyujUJL8e1B6Z4PBfAqC/aGY1+C7T0w20Gih3yrvJSk97gpiVfB+y7c46T4Nunk+ZngLq0rOIdeVeIklk0R3OA==",
|
||||
"dependencies": {
|
||||
"@smithy/types": "^2.10.1",
|
||||
"tslib": "^2.5.0"
|
||||
"@smithy/types": "^2.12.0",
|
||||
"tslib": "^2.6.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@smithy/signature-v4": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-2.1.3.tgz",
|
||||
"integrity": "sha512-Jq4iPPdCmJojZTsPePn4r1ULShh6ONkokLuxp1Lnk4Sq7r7rJp4HlA1LbPBq4bD64TIzQezIpr1X+eh5NYkNxw==",
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-2.3.0.tgz",
|
||||
"integrity": "sha512-ui/NlpILU+6HAQBfJX8BBsDXuKSNrjTSuOYArRblcrErwKFutjrCNb/OExfVRyj9+26F9J+ZmfWT+fKWuDrH3Q==",
|
||||
"dependencies": {
|
||||
"@smithy/eventstream-codec": "^2.1.3",
|
||||
"@smithy/is-array-buffer": "^2.1.1",
|
||||
"@smithy/types": "^2.10.1",
|
||||
"@smithy/util-hex-encoding": "^2.1.1",
|
||||
"@smithy/util-middleware": "^2.1.3",
|
||||
"@smithy/util-uri-escape": "^2.1.1",
|
||||
"@smithy/util-utf8": "^2.1.1",
|
||||
"tslib": "^2.5.0"
|
||||
"@smithy/is-array-buffer": "^2.2.0",
|
||||
"@smithy/types": "^2.12.0",
|
||||
"@smithy/util-hex-encoding": "^2.2.0",
|
||||
"@smithy/util-middleware": "^2.2.0",
|
||||
"@smithy/util-uri-escape": "^2.2.0",
|
||||
"@smithy/util-utf8": "^2.3.0",
|
||||
"tslib": "^2.6.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@smithy/smithy-client": {
|
||||
"version": "2.4.2",
|
||||
"resolved": "https://registry.npmjs.org/@smithy/smithy-client/-/smithy-client-2.4.2.tgz",
|
||||
"integrity": "sha512-ntAFYN51zu3N3mCd95YFcFi/8rmvm//uX+HnK24CRbI6k5Rjackn0JhgKz5zOx/tbNvOpgQIwhSX+1EvEsBLbA==",
|
||||
"version": "2.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@smithy/smithy-client/-/smithy-client-2.5.1.tgz",
|
||||
"integrity": "sha512-jrbSQrYCho0yDaaf92qWgd+7nAeap5LtHTI51KXqmpIFCceKU3K9+vIVTUH72bOJngBMqa4kyu1VJhRcSrk/CQ==",
|
||||
"dependencies": {
|
||||
"@smithy/middleware-endpoint": "^2.4.4",
|
||||
"@smithy/middleware-stack": "^2.1.3",
|
||||
"@smithy/protocol-http": "^3.2.1",
|
||||
"@smithy/types": "^2.10.1",
|
||||
"@smithy/util-stream": "^2.1.3",
|
||||
"tslib": "^2.5.0"
|
||||
"@smithy/middleware-endpoint": "^2.5.1",
|
||||
"@smithy/middleware-stack": "^2.2.0",
|
||||
"@smithy/protocol-http": "^3.3.0",
|
||||
"@smithy/types": "^2.12.0",
|
||||
"@smithy/util-stream": "^2.2.0",
|
||||
"tslib": "^2.6.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@smithy/types": {
|
||||
"version": "2.10.1",
|
||||
"resolved": "https://registry.npmjs.org/@smithy/types/-/types-2.10.1.tgz",
|
||||
"integrity": "sha512-hjQO+4ru4cQ58FluQvKKiyMsFg0A6iRpGm2kqdH8fniyNd2WyanoOsYJfMX/IFLuLxEoW6gnRkNZy1y6fUUhtA==",
|
||||
"version": "2.12.0",
|
||||
"resolved": "https://registry.npmjs.org/@smithy/types/-/types-2.12.0.tgz",
|
||||
"integrity": "sha512-QwYgloJ0sVNBeBuBs65cIkTbfzV/Q6ZNPCJ99EICFEdJYG50nGIY/uYXp+TbsdJReIuPr0a0kXmCvren3MbRRw==",
|
||||
"dependencies": {
|
||||
"tslib": "^2.5.0"
|
||||
"tslib": "^2.6.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@smithy/url-parser": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/@smithy/url-parser/-/url-parser-2.1.3.tgz",
|
||||
"integrity": "sha512-X1NRA4WzK/ihgyzTpeGvI9Wn45y8HmqF4AZ/FazwAv8V203Ex+4lXqcYI70naX9ETqbqKVzFk88W6WJJzCggTQ==",
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@smithy/url-parser/-/url-parser-2.2.0.tgz",
|
||||
"integrity": "sha512-hoA4zm61q1mNTpksiSWp2nEl1dt3j726HdRhiNgVJQMj7mLp7dprtF57mOB6JvEk/x9d2bsuL5hlqZbBuHQylQ==",
|
||||
"dependencies": {
|
||||
"@smithy/querystring-parser": "^2.1.3",
|
||||
"@smithy/types": "^2.10.1",
|
||||
"tslib": "^2.5.0"
|
||||
"@smithy/querystring-parser": "^2.2.0",
|
||||
"@smithy/types": "^2.12.0",
|
||||
"tslib": "^2.6.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@smithy/util-base64": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@smithy/util-base64/-/util-base64-2.1.1.tgz",
|
||||
"integrity": "sha512-UfHVpY7qfF/MrgndI5PexSKVTxSZIdz9InghTFa49QOvuu9I52zLPLUHXvHpNuMb1iD2vmc6R+zbv/bdMipR/g==",
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@smithy/util-base64/-/util-base64-2.3.0.tgz",
|
||||
"integrity": "sha512-s3+eVwNeJuXUwuMbusncZNViuhv2LjVJ1nMwTqSA0XAC7gjKhqqxRdJPhR8+YrkoZ9IiIbFk/yK6ACe/xlF+hw==",
|
||||
"dependencies": {
|
||||
"@smithy/util-buffer-from": "^2.1.1",
|
||||
"tslib": "^2.5.0"
|
||||
"@smithy/util-buffer-from": "^2.2.0",
|
||||
"@smithy/util-utf8": "^2.3.0",
|
||||
"tslib": "^2.6.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@smithy/util-body-length-browser": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@smithy/util-body-length-browser/-/util-body-length-browser-2.1.1.tgz",
|
||||
"integrity": "sha512-ekOGBLvs1VS2d1zM2ER4JEeBWAvIOUKeaFch29UjjJsxmZ/f0L3K3x0dEETgh3Q9bkZNHgT+rkdl/J/VUqSRag==",
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@smithy/util-body-length-browser/-/util-body-length-browser-2.2.0.tgz",
|
||||
"integrity": "sha512-dtpw9uQP7W+n3vOtx0CfBD5EWd7EPdIdsQnWTDoFf77e3VUf05uA7R7TGipIo8e4WL2kuPdnsr3hMQn9ziYj5w==",
|
||||
"dependencies": {
|
||||
"tslib": "^2.5.0"
|
||||
"tslib": "^2.6.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@smithy/util-body-length-node": {
|
||||
"version": "2.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@smithy/util-body-length-node/-/util-body-length-node-2.2.1.tgz",
|
||||
"integrity": "sha512-/ggJG+ta3IDtpNVq4ktmEUtOkH1LW64RHB5B0hcr5ZaWBmo96UX2cIOVbjCqqDickTXqBWZ4ZO0APuaPrD7Abg==",
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@smithy/util-body-length-node/-/util-body-length-node-2.3.0.tgz",
|
||||
"integrity": "sha512-ITWT1Wqjubf2CJthb0BuT9+bpzBfXeMokH/AAa5EJQgbv9aPMVfnM76iFIZVFf50hYXGbtiV71BHAthNWd6+dw==",
|
||||
"dependencies": {
|
||||
"tslib": "^2.5.0"
|
||||
"tslib": "^2.6.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@smithy/util-buffer-from": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-2.1.1.tgz",
|
||||
"integrity": "sha512-clhNjbyfqIv9Md2Mg6FffGVrJxw7bgK7s3Iax36xnfVj6cg0fUG7I4RH0XgXJF8bxi+saY5HR21g2UPKSxVCXg==",
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-2.2.0.tgz",
|
||||
"integrity": "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==",
|
||||
"dependencies": {
|
||||
"@smithy/is-array-buffer": "^2.1.1",
|
||||
"tslib": "^2.5.0"
|
||||
"@smithy/is-array-buffer": "^2.2.0",
|
||||
"tslib": "^2.6.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@smithy/util-config-provider": {
|
||||
"version": "2.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@smithy/util-config-provider/-/util-config-provider-2.2.1.tgz",
|
||||
"integrity": "sha512-50VL/tx9oYYcjJn/qKqNy7sCtpD0+s8XEBamIFo4mFFTclKMNp+rsnymD796uybjiIquB7VCB/DeafduL0y2kw==",
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@smithy/util-config-provider/-/util-config-provider-2.3.0.tgz",
|
||||
"integrity": "sha512-HZkzrRcuFN1k70RLqlNK4FnPXKOpkik1+4JaBoHNJn+RnJGYqaa3c5/+XtLOXhlKzlRgNvyaLieHTW2VwGN0VQ==",
|
||||
"dependencies": {
|
||||
"tslib": "^2.5.0"
|
||||
"tslib": "^2.6.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@smithy/util-defaults-mode-browser": {
|
||||
"version": "2.1.4",
|
||||
"resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-browser/-/util-defaults-mode-browser-2.1.4.tgz",
|
||||
"integrity": "sha512-J6XAVY+/g7jf03QMnvqPyU+8jqGrrtXoKWFVOS+n1sz0Lg8HjHJ1ANqaDN+KTTKZRZlvG8nU5ZrJOUL6VdwgcQ==",
|
||||
"version": "2.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-browser/-/util-defaults-mode-browser-2.2.1.tgz",
|
||||
"integrity": "sha512-RtKW+8j8skk17SYowucwRUjeh4mCtnm5odCL0Lm2NtHQBsYKrNW0od9Rhopu9wF1gHMfHeWF7i90NwBz/U22Kw==",
|
||||
"dependencies": {
|
||||
"@smithy/property-provider": "^2.1.3",
|
||||
"@smithy/smithy-client": "^2.4.2",
|
||||
"@smithy/types": "^2.10.1",
|
||||
"@smithy/property-provider": "^2.2.0",
|
||||
"@smithy/smithy-client": "^2.5.1",
|
||||
"@smithy/types": "^2.12.0",
|
||||
"bowser": "^2.11.0",
|
||||
"tslib": "^2.5.0"
|
||||
"tslib": "^2.6.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 10.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@smithy/util-defaults-mode-node": {
|
||||
"version": "2.2.3",
|
||||
"resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-node/-/util-defaults-mode-node-2.2.3.tgz",
|
||||
"integrity": "sha512-ttUISrv1uVOjTlDa3nznX33f0pthoUlP+4grhTvOzcLhzArx8qHB94/untGACOG3nlf8vU20nI2iWImfzoLkYA==",
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-node/-/util-defaults-mode-node-2.3.1.tgz",
|
||||
"integrity": "sha512-vkMXHQ0BcLFysBMWgSBLSk3+leMpFSyyFj8zQtv5ZyUBx8/owVh1/pPEkzmW/DR/Gy/5c8vjLDD9gZjXNKbrpA==",
|
||||
"dependencies": {
|
||||
"@smithy/config-resolver": "^2.1.4",
|
||||
"@smithy/credential-provider-imds": "^2.2.4",
|
||||
"@smithy/node-config-provider": "^2.2.4",
|
||||
"@smithy/property-provider": "^2.1.3",
|
||||
"@smithy/smithy-client": "^2.4.2",
|
||||
"@smithy/types": "^2.10.1",
|
||||
"tslib": "^2.5.0"
|
||||
"@smithy/config-resolver": "^2.2.0",
|
||||
"@smithy/credential-provider-imds": "^2.3.0",
|
||||
"@smithy/node-config-provider": "^2.3.0",
|
||||
"@smithy/property-provider": "^2.2.0",
|
||||
"@smithy/smithy-client": "^2.5.1",
|
||||
"@smithy/types": "^2.12.0",
|
||||
"tslib": "^2.6.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 10.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@smithy/util-endpoints": {
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/@smithy/util-endpoints/-/util-endpoints-1.1.4.tgz",
|
||||
"integrity": "sha512-/qAeHmK5l4yQ4/bCIJ9p49wDe9rwWtOzhPHblu386fwPNT3pxmodgcs9jDCV52yK9b4rB8o9Sj31P/7Vzka1cg==",
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@smithy/util-endpoints/-/util-endpoints-1.2.0.tgz",
|
||||
"integrity": "sha512-BuDHv8zRjsE5zXd3PxFXFknzBG3owCpjq8G3FcsXW3CykYXuEqM3nTSsmLzw5q+T12ZYuDlVUZKBdpNbhVtlrQ==",
|
||||
"dependencies": {
|
||||
"@smithy/node-config-provider": "^2.2.4",
|
||||
"@smithy/types": "^2.10.1",
|
||||
"tslib": "^2.5.0"
|
||||
"@smithy/node-config-provider": "^2.3.0",
|
||||
"@smithy/types": "^2.12.0",
|
||||
"tslib": "^2.6.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 14.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@smithy/util-hex-encoding": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@smithy/util-hex-encoding/-/util-hex-encoding-2.1.1.tgz",
|
||||
"integrity": "sha512-3UNdP2pkYUUBGEXzQI9ODTDK+Tcu1BlCyDBaRHwyxhA+8xLP8agEKQq4MGmpjqb4VQAjq9TwlCQX0kP6XDKYLg==",
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@smithy/util-hex-encoding/-/util-hex-encoding-2.2.0.tgz",
|
||||
"integrity": "sha512-7iKXR+/4TpLK194pVjKiasIyqMtTYJsgKgM242Y9uzt5dhHnUDvMNb+3xIhRJ9QhvqGii/5cRUt4fJn3dtXNHQ==",
|
||||
"dependencies": {
|
||||
"tslib": "^2.5.0"
|
||||
"tslib": "^2.6.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@smithy/util-middleware": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/@smithy/util-middleware/-/util-middleware-2.1.3.tgz",
|
||||
"integrity": "sha512-/+2fm7AZ2ozl5h8wM++ZP0ovE9/tiUUAHIbCfGfb3Zd3+Dyk17WODPKXBeJ/TnK5U+x743QmA0xHzlSm8I/qhw==",
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@smithy/util-middleware/-/util-middleware-2.2.0.tgz",
|
||||
"integrity": "sha512-L1qpleXf9QD6LwLCJ5jddGkgWyuSvWBkJwWAZ6kFkdifdso+sk3L3O1HdmPvCdnCK3IS4qWyPxev01QMnfHSBw==",
|
||||
"dependencies": {
|
||||
"@smithy/types": "^2.10.1",
|
||||
"tslib": "^2.5.0"
|
||||
"@smithy/types": "^2.12.0",
|
||||
"tslib": "^2.6.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@smithy/util-retry": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/@smithy/util-retry/-/util-retry-2.1.3.tgz",
|
||||
"integrity": "sha512-Kbvd+GEMuozbNUU3B89mb99tbufwREcyx2BOX0X2+qHjq6Gvsah8xSDDgxISDwcOHoDqUWO425F0Uc/QIRhYkg==",
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@smithy/util-retry/-/util-retry-2.2.0.tgz",
|
||||
"integrity": "sha512-q9+pAFPTfftHXRytmZ7GzLFFrEGavqapFc06XxzZFcSIGERXMerXxCitjOG1prVDR9QdjqotF40SWvbqcCpf8g==",
|
||||
"dependencies": {
|
||||
"@smithy/service-error-classification": "^2.1.3",
|
||||
"@smithy/types": "^2.10.1",
|
||||
"tslib": "^2.5.0"
|
||||
"@smithy/service-error-classification": "^2.1.5",
|
||||
"@smithy/types": "^2.12.0",
|
||||
"tslib": "^2.6.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 14.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@smithy/util-stream": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/@smithy/util-stream/-/util-stream-2.1.3.tgz",
|
||||
"integrity": "sha512-HvpEQbP8raTy9n86ZfXiAkf3ezp1c3qeeO//zGqwZdrfaoOpGKQgF2Sv1IqZp7wjhna7pvczWaGUHjcOPuQwKw==",
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@smithy/util-stream/-/util-stream-2.2.0.tgz",
|
||||
"integrity": "sha512-17faEXbYWIRst1aU9SvPZyMdWmqIrduZjVOqCPMIsWFNxs5yQQgFrJL6b2SdiCzyW9mJoDjFtgi53xx7EH+BXA==",
|
||||
"dependencies": {
|
||||
"@smithy/fetch-http-handler": "^2.4.3",
|
||||
"@smithy/node-http-handler": "^2.4.1",
|
||||
"@smithy/types": "^2.10.1",
|
||||
"@smithy/util-base64": "^2.1.1",
|
||||
"@smithy/util-buffer-from": "^2.1.1",
|
||||
"@smithy/util-hex-encoding": "^2.1.1",
|
||||
"@smithy/util-utf8": "^2.1.1",
|
||||
"tslib": "^2.5.0"
|
||||
"@smithy/fetch-http-handler": "^2.5.0",
|
||||
"@smithy/node-http-handler": "^2.5.0",
|
||||
"@smithy/types": "^2.12.0",
|
||||
"@smithy/util-base64": "^2.3.0",
|
||||
"@smithy/util-buffer-from": "^2.2.0",
|
||||
"@smithy/util-hex-encoding": "^2.2.0",
|
||||
"@smithy/util-utf8": "^2.3.0",
|
||||
"tslib": "^2.6.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@smithy/util-uri-escape": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@smithy/util-uri-escape/-/util-uri-escape-2.1.1.tgz",
|
||||
"integrity": "sha512-saVzI1h6iRBUVSqtnlOnc9ssU09ypo7n+shdQ8hBTZno/9rZ3AuRYvoHInV57VF7Qn7B+pFJG7qTzFiHxWlWBw==",
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@smithy/util-uri-escape/-/util-uri-escape-2.2.0.tgz",
|
||||
"integrity": "sha512-jtmJMyt1xMD/d8OtbVJ2gFZOSKc+ueYJZPW20ULW1GOp/q/YIM0wNh+u8ZFao9UaIGz4WoPW8hC64qlWLIfoDA==",
|
||||
"dependencies": {
|
||||
"tslib": "^2.5.0"
|
||||
"tslib": "^2.6.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@smithy/util-utf8": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.1.1.tgz",
|
||||
"integrity": "sha512-BqTpzYEcUMDwAKr7/mVRUtHDhs6ZoXDi9NypMvMfOr/+u1NW7JgqodPDECiiLboEm6bobcPcECxzjtQh865e9A==",
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.3.0.tgz",
|
||||
"integrity": "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==",
|
||||
"dependencies": {
|
||||
"@smithy/util-buffer-from": "^2.1.1",
|
||||
"tslib": "^2.5.0"
|
||||
"@smithy/util-buffer-from": "^2.2.0",
|
||||
"tslib": "^2.6.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
@ -9954,9 +10102,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/mysql2": {
|
||||
"version": "3.9.4",
|
||||
"resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.9.4.tgz",
|
||||
"integrity": "sha512-OEESQuwxMza803knC1YSt7NMuc1BrK9j7gZhCSs2WAyxr1vfiI7QLaLOKTh5c9SWGz98qVyQUbK8/WckevNQhg==",
|
||||
"version": "3.9.7",
|
||||
"resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.9.7.tgz",
|
||||
"integrity": "sha512-KnJT8vYRcNAZv73uf9zpXqNbvBG7DJrs+1nACsjZP1HMJ1TgXEy8wnNilXAn/5i57JizXKtrUtwDB7HxT9DDpw==",
|
||||
"dependencies": {
|
||||
"denque": "^2.1.0",
|
||||
"generate-function": "^2.3.1",
|
||||
|
@ -110,7 +110,7 @@
|
||||
"libsodium-wrappers": "^0.7.13",
|
||||
"lodash.isequal": "^4.5.0",
|
||||
"ms": "^2.1.3",
|
||||
"mysql2": "^3.9.4",
|
||||
"mysql2": "^3.9.7",
|
||||
"nanoid": "^5.0.4",
|
||||
"nodemailer": "^6.9.9",
|
||||
"ora": "^7.0.1",
|
||||
|
6
backend/src/@types/fastify.d.ts
vendored
6
backend/src/@types/fastify.d.ts
vendored
@ -1,6 +1,8 @@
|
||||
import "fastify";
|
||||
|
||||
import { TUsers } from "@app/db/schemas";
|
||||
import { TAccessApprovalPolicyServiceFactory } from "@app/ee/services/access-approval-policy/access-approval-policy-service";
|
||||
import { TAccessApprovalRequestServiceFactory } from "@app/ee/services/access-approval-request/access-approval-request-service";
|
||||
import { TAuditLogServiceFactory } from "@app/ee/services/audit-log/audit-log-service";
|
||||
import { TCreateAuditLogDTO } from "@app/ee/services/audit-log/audit-log-types";
|
||||
import { TAuditLogStreamServiceFactory } from "@app/ee/services/audit-log-stream/audit-log-stream-service";
|
||||
@ -30,6 +32,7 @@ import { TAuthTokenServiceFactory } from "@app/services/auth-token/auth-token-se
|
||||
import { TGroupProjectServiceFactory } from "@app/services/group-project/group-project-service";
|
||||
import { TIdentityServiceFactory } from "@app/services/identity/identity-service";
|
||||
import { TIdentityAccessTokenServiceFactory } from "@app/services/identity-access-token/identity-access-token-service";
|
||||
import { TIdentityAwsAuthServiceFactory } from "@app/services/identity-aws-auth/identity-aws-auth-service";
|
||||
import { TIdentityProjectServiceFactory } from "@app/services/identity-project/identity-project-service";
|
||||
import { TIdentityUaServiceFactory } from "@app/services/identity-ua/identity-ua-service";
|
||||
import { TIntegrationServiceFactory } from "@app/services/integration/integration-service";
|
||||
@ -113,6 +116,9 @@ declare module "fastify" {
|
||||
identityAccessToken: TIdentityAccessTokenServiceFactory;
|
||||
identityProject: TIdentityProjectServiceFactory;
|
||||
identityUa: TIdentityUaServiceFactory;
|
||||
identityAwsAuth: TIdentityAwsAuthServiceFactory;
|
||||
accessApprovalPolicy: TAccessApprovalPolicyServiceFactory;
|
||||
accessApprovalRequest: TAccessApprovalRequestServiceFactory;
|
||||
secretApprovalPolicy: TSecretApprovalPolicyServiceFactory;
|
||||
secretApprovalRequest: TSecretApprovalRequestServiceFactory;
|
||||
secretRotation: TSecretRotationServiceFactory;
|
||||
|
45
backend/src/@types/knex.d.ts
vendored
45
backend/src/@types/knex.d.ts
vendored
@ -2,6 +2,18 @@ import { Knex } from "knex";
|
||||
|
||||
import {
|
||||
TableName,
|
||||
TAccessApprovalPolicies,
|
||||
TAccessApprovalPoliciesApprovers,
|
||||
TAccessApprovalPoliciesApproversInsert,
|
||||
TAccessApprovalPoliciesApproversUpdate,
|
||||
TAccessApprovalPoliciesInsert,
|
||||
TAccessApprovalPoliciesUpdate,
|
||||
TAccessApprovalRequests,
|
||||
TAccessApprovalRequestsInsert,
|
||||
TAccessApprovalRequestsReviewers,
|
||||
TAccessApprovalRequestsReviewersInsert,
|
||||
TAccessApprovalRequestsReviewersUpdate,
|
||||
TAccessApprovalRequestsUpdate,
|
||||
TApiKeys,
|
||||
TApiKeysInsert,
|
||||
TApiKeysUpdate,
|
||||
@ -47,6 +59,9 @@ import {
|
||||
TIdentityAccessTokens,
|
||||
TIdentityAccessTokensInsert,
|
||||
TIdentityAccessTokensUpdate,
|
||||
TIdentityAwsAuths,
|
||||
TIdentityAwsAuthsInsert,
|
||||
TIdentityAwsAuthsUpdate,
|
||||
TIdentityOrgMemberships,
|
||||
TIdentityOrgMembershipsInsert,
|
||||
TIdentityOrgMembershipsUpdate,
|
||||
@ -314,6 +329,11 @@ declare module "knex/types/tables" {
|
||||
TIdentityUniversalAuthsInsert,
|
||||
TIdentityUniversalAuthsUpdate
|
||||
>;
|
||||
[TableName.IdentityAwsAuth]: Knex.CompositeTableType<
|
||||
TIdentityAwsAuths,
|
||||
TIdentityAwsAuthsInsert,
|
||||
TIdentityAwsAuthsUpdate
|
||||
>;
|
||||
[TableName.IdentityUaClientSecret]: Knex.CompositeTableType<
|
||||
TIdentityUaClientSecrets,
|
||||
TIdentityUaClientSecretsInsert,
|
||||
@ -344,6 +364,31 @@ declare module "knex/types/tables" {
|
||||
TIdentityProjectAdditionalPrivilegeInsert,
|
||||
TIdentityProjectAdditionalPrivilegeUpdate
|
||||
>;
|
||||
|
||||
[TableName.AccessApprovalPolicy]: Knex.CompositeTableType<
|
||||
TAccessApprovalPolicies,
|
||||
TAccessApprovalPoliciesInsert,
|
||||
TAccessApprovalPoliciesUpdate
|
||||
>;
|
||||
|
||||
[TableName.AccessApprovalPolicyApprover]: Knex.CompositeTableType<
|
||||
TAccessApprovalPoliciesApprovers,
|
||||
TAccessApprovalPoliciesApproversInsert,
|
||||
TAccessApprovalPoliciesApproversUpdate
|
||||
>;
|
||||
|
||||
[TableName.AccessApprovalRequest]: Knex.CompositeTableType<
|
||||
TAccessApprovalRequests,
|
||||
TAccessApprovalRequestsInsert,
|
||||
TAccessApprovalRequestsUpdate
|
||||
>;
|
||||
|
||||
[TableName.AccessApprovalRequestReviewer]: Knex.CompositeTableType<
|
||||
TAccessApprovalRequestsReviewers,
|
||||
TAccessApprovalRequestsReviewersInsert,
|
||||
TAccessApprovalRequestsReviewersUpdate
|
||||
>;
|
||||
|
||||
[TableName.ScimToken]: Knex.CompositeTableType<TScimTokens, TScimTokensInsert, TScimTokensUpdate>;
|
||||
[TableName.SecretApprovalPolicy]: Knex.CompositeTableType<
|
||||
TSecretApprovalPolicies,
|
||||
|
@ -5,9 +5,16 @@ import { TableName } from "../schemas";
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
const isUsersTablePresent = await knex.schema.hasTable(TableName.Users);
|
||||
if (isUsersTablePresent) {
|
||||
await knex.schema.alterTable(TableName.Users, (t) => {
|
||||
t.boolean("isEmailVerified");
|
||||
});
|
||||
const hasIsEmailVerifiedColumn = await knex.schema.hasColumn(TableName.Users, "isEmailVerified");
|
||||
|
||||
if (!hasIsEmailVerifiedColumn) {
|
||||
await knex.schema.alterTable(TableName.Users, (t) => {
|
||||
t.boolean("isEmailVerified").defaultTo(false);
|
||||
});
|
||||
}
|
||||
|
||||
// Backfilling the isEmailVerified to true where isAccepted is true
|
||||
await knex(TableName.Users).update({ isEmailVerified: true }).where("isAccepted", true);
|
||||
}
|
||||
|
||||
const isUserAliasTablePresent = await knex.schema.hasTable(TableName.UserAliases);
|
@ -0,0 +1,41 @@
|
||||
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.AccessApprovalPolicy))) {
|
||||
await knex.schema.createTable(TableName.AccessApprovalPolicy, (t) => {
|
||||
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
|
||||
t.string("name").notNullable();
|
||||
t.integer("approvals").defaultTo(1).notNullable();
|
||||
t.string("secretPath");
|
||||
|
||||
t.uuid("envId").notNullable();
|
||||
t.foreign("envId").references("id").inTable(TableName.Environment).onDelete("CASCADE");
|
||||
t.timestamps(true, true, true);
|
||||
});
|
||||
await createOnUpdateTrigger(knex, TableName.AccessApprovalPolicy);
|
||||
}
|
||||
|
||||
if (!(await knex.schema.hasTable(TableName.AccessApprovalPolicyApprover))) {
|
||||
await knex.schema.createTable(TableName.AccessApprovalPolicyApprover, (t) => {
|
||||
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
|
||||
t.uuid("approverId").notNullable();
|
||||
t.foreign("approverId").references("id").inTable(TableName.ProjectMembership).onDelete("CASCADE");
|
||||
|
||||
t.uuid("policyId").notNullable();
|
||||
t.foreign("policyId").references("id").inTable(TableName.AccessApprovalPolicy).onDelete("CASCADE");
|
||||
t.timestamps(true, true, true);
|
||||
});
|
||||
await createOnUpdateTrigger(knex, TableName.AccessApprovalPolicyApprover);
|
||||
}
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
await knex.schema.dropTableIfExists(TableName.AccessApprovalPolicyApprover);
|
||||
await knex.schema.dropTableIfExists(TableName.AccessApprovalPolicy);
|
||||
|
||||
await dropOnUpdateTrigger(knex, TableName.AccessApprovalPolicyApprover);
|
||||
await dropOnUpdateTrigger(knex, TableName.AccessApprovalPolicy);
|
||||
}
|
51
backend/src/db/migrations/20240507162141_access.ts
Normal file
51
backend/src/db/migrations/20240507162141_access.ts
Normal file
@ -0,0 +1,51 @@
|
||||
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.AccessApprovalRequest))) {
|
||||
await knex.schema.createTable(TableName.AccessApprovalRequest, (t) => {
|
||||
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
|
||||
|
||||
t.uuid("policyId").notNullable();
|
||||
t.foreign("policyId").references("id").inTable(TableName.AccessApprovalPolicy).onDelete("CASCADE");
|
||||
|
||||
t.uuid("privilegeId").nullable();
|
||||
t.foreign("privilegeId").references("id").inTable(TableName.ProjectUserAdditionalPrivilege).onDelete("CASCADE");
|
||||
|
||||
t.uuid("requestedBy").notNullable();
|
||||
t.foreign("requestedBy").references("id").inTable(TableName.ProjectMembership).onDelete("CASCADE");
|
||||
|
||||
// We use these values to create the actual privilege at a later point in time.
|
||||
t.boolean("isTemporary").notNullable();
|
||||
t.string("temporaryRange").nullable();
|
||||
|
||||
t.jsonb("permissions").notNullable();
|
||||
|
||||
t.timestamps(true, true, true);
|
||||
});
|
||||
}
|
||||
await createOnUpdateTrigger(knex, TableName.AccessApprovalRequest);
|
||||
|
||||
if (!(await knex.schema.hasTable(TableName.AccessApprovalRequestReviewer))) {
|
||||
await knex.schema.createTable(TableName.AccessApprovalRequestReviewer, (t) => {
|
||||
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
|
||||
t.uuid("member").notNullable();
|
||||
t.foreign("member").references("id").inTable(TableName.ProjectMembership).onDelete("CASCADE");
|
||||
t.string("status").notNullable();
|
||||
t.uuid("requestId").notNullable();
|
||||
t.foreign("requestId").references("id").inTable(TableName.AccessApprovalRequest).onDelete("CASCADE");
|
||||
t.timestamps(true, true, true);
|
||||
});
|
||||
}
|
||||
await createOnUpdateTrigger(knex, TableName.AccessApprovalRequestReviewer);
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
await knex.schema.dropTableIfExists(TableName.AccessApprovalRequestReviewer);
|
||||
await knex.schema.dropTableIfExists(TableName.AccessApprovalRequest);
|
||||
|
||||
await dropOnUpdateTrigger(knex, TableName.AccessApprovalRequestReviewer);
|
||||
await dropOnUpdateTrigger(knex, TableName.AccessApprovalRequest);
|
||||
}
|
@ -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.IdentityAwsAuth))) {
|
||||
await knex.schema.createTable(TableName.IdentityAwsAuth, (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("stsEndpoint").notNullable();
|
||||
t.string("allowedPrincipalArns").notNullable();
|
||||
t.string("allowedAccountIds").notNullable();
|
||||
});
|
||||
}
|
||||
|
||||
await createOnUpdateTrigger(knex, TableName.IdentityAwsAuth);
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
await knex.schema.dropTableIfExists(TableName.IdentityAwsAuth);
|
||||
await dropOnUpdateTrigger(knex, TableName.IdentityAwsAuth);
|
||||
}
|
25
backend/src/db/schemas/access-approval-policies-approvers.ts
Normal file
25
backend/src/db/schemas/access-approval-policies-approvers.ts
Normal file
@ -0,0 +1,25 @@
|
||||
// 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 AccessApprovalPoliciesApproversSchema = z.object({
|
||||
id: z.string().uuid(),
|
||||
approverId: z.string().uuid(),
|
||||
policyId: z.string().uuid(),
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date()
|
||||
});
|
||||
|
||||
export type TAccessApprovalPoliciesApprovers = z.infer<typeof AccessApprovalPoliciesApproversSchema>;
|
||||
export type TAccessApprovalPoliciesApproversInsert = Omit<
|
||||
z.input<typeof AccessApprovalPoliciesApproversSchema>,
|
||||
TImmutableDBKeys
|
||||
>;
|
||||
export type TAccessApprovalPoliciesApproversUpdate = Partial<
|
||||
Omit<z.input<typeof AccessApprovalPoliciesApproversSchema>, TImmutableDBKeys>
|
||||
>;
|
24
backend/src/db/schemas/access-approval-policies.ts
Normal file
24
backend/src/db/schemas/access-approval-policies.ts
Normal file
@ -0,0 +1,24 @@
|
||||
// Code generated by automation script, DO NOT EDIT.
|
||||
// Automated by pulling database and generating zod schema
|
||||
// To update. Just run npm run generate:schema
|
||||
// Written by akhilmhdh.
|
||||
|
||||
import { z } from "zod";
|
||||
|
||||
import { TImmutableDBKeys } from "./models";
|
||||
|
||||
export const AccessApprovalPoliciesSchema = z.object({
|
||||
id: z.string().uuid(),
|
||||
name: z.string(),
|
||||
approvals: z.number().default(1),
|
||||
secretPath: z.string().nullable().optional(),
|
||||
envId: z.string().uuid(),
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date()
|
||||
});
|
||||
|
||||
export type TAccessApprovalPolicies = z.infer<typeof AccessApprovalPoliciesSchema>;
|
||||
export type TAccessApprovalPoliciesInsert = Omit<z.input<typeof AccessApprovalPoliciesSchema>, TImmutableDBKeys>;
|
||||
export type TAccessApprovalPoliciesUpdate = Partial<
|
||||
Omit<z.input<typeof AccessApprovalPoliciesSchema>, TImmutableDBKeys>
|
||||
>;
|
26
backend/src/db/schemas/access-approval-requests-reviewers.ts
Normal file
26
backend/src/db/schemas/access-approval-requests-reviewers.ts
Normal file
@ -0,0 +1,26 @@
|
||||
// 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 AccessApprovalRequestsReviewersSchema = z.object({
|
||||
id: z.string().uuid(),
|
||||
member: z.string().uuid(),
|
||||
status: z.string(),
|
||||
requestId: z.string().uuid(),
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date()
|
||||
});
|
||||
|
||||
export type TAccessApprovalRequestsReviewers = z.infer<typeof AccessApprovalRequestsReviewersSchema>;
|
||||
export type TAccessApprovalRequestsReviewersInsert = Omit<
|
||||
z.input<typeof AccessApprovalRequestsReviewersSchema>,
|
||||
TImmutableDBKeys
|
||||
>;
|
||||
export type TAccessApprovalRequestsReviewersUpdate = Partial<
|
||||
Omit<z.input<typeof AccessApprovalRequestsReviewersSchema>, TImmutableDBKeys>
|
||||
>;
|
26
backend/src/db/schemas/access-approval-requests.ts
Normal file
26
backend/src/db/schemas/access-approval-requests.ts
Normal file
@ -0,0 +1,26 @@
|
||||
// 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 AccessApprovalRequestsSchema = z.object({
|
||||
id: z.string().uuid(),
|
||||
policyId: z.string().uuid(),
|
||||
privilegeId: z.string().uuid().nullable().optional(),
|
||||
requestedBy: z.string().uuid(),
|
||||
isTemporary: z.boolean(),
|
||||
temporaryRange: z.string().nullable().optional(),
|
||||
permissions: z.unknown(),
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date()
|
||||
});
|
||||
|
||||
export type TAccessApprovalRequests = z.infer<typeof AccessApprovalRequestsSchema>;
|
||||
export type TAccessApprovalRequestsInsert = Omit<z.input<typeof AccessApprovalRequestsSchema>, TImmutableDBKeys>;
|
||||
export type TAccessApprovalRequestsUpdate = Partial<
|
||||
Omit<z.input<typeof AccessApprovalRequestsSchema>, TImmutableDBKeys>
|
||||
>;
|
27
backend/src/db/schemas/identity-aws-auths.ts
Normal file
27
backend/src/db/schemas/identity-aws-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 IdentityAwsAuthsSchema = 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(),
|
||||
stsEndpoint: z.string(),
|
||||
allowedPrincipalArns: z.string(),
|
||||
allowedAccountIds: z.string()
|
||||
});
|
||||
|
||||
export type TIdentityAwsAuths = z.infer<typeof IdentityAwsAuthsSchema>;
|
||||
export type TIdentityAwsAuthsInsert = Omit<z.input<typeof IdentityAwsAuthsSchema>, TImmutableDBKeys>;
|
||||
export type TIdentityAwsAuthsUpdate = Partial<Omit<z.input<typeof IdentityAwsAuthsSchema>, TImmutableDBKeys>>;
|
@ -1,3 +1,7 @@
|
||||
export * from "./access-approval-policies";
|
||||
export * from "./access-approval-policies-approvers";
|
||||
export * from "./access-approval-requests";
|
||||
export * from "./access-approval-requests-reviewers";
|
||||
export * from "./api-keys";
|
||||
export * from "./audit-log-streams";
|
||||
export * from "./audit-logs";
|
||||
@ -13,6 +17,7 @@ export * from "./group-project-memberships";
|
||||
export * from "./groups";
|
||||
export * from "./identities";
|
||||
export * from "./identity-access-tokens";
|
||||
export * from "./identity-aws-auths";
|
||||
export * from "./identity-org-memberships";
|
||||
export * from "./identity-project-additional-privilege";
|
||||
export * from "./identity-project-membership-role";
|
||||
|
@ -45,11 +45,16 @@ export enum TableName {
|
||||
IdentityAccessToken = "identity_access_tokens",
|
||||
IdentityUniversalAuth = "identity_universal_auths",
|
||||
IdentityUaClientSecret = "identity_ua_client_secrets",
|
||||
IdentityAwsAuth = "identity_aws_auths",
|
||||
IdentityOrgMembership = "identity_org_memberships",
|
||||
IdentityProjectMembership = "identity_project_memberships",
|
||||
IdentityProjectMembershipRole = "identity_project_membership_role",
|
||||
IdentityProjectAdditionalPrivilege = "identity_project_additional_privilege",
|
||||
ScimToken = "scim_tokens",
|
||||
AccessApprovalPolicy = "access_approval_policies",
|
||||
AccessApprovalPolicyApprover = "access_approval_policies_approvers",
|
||||
AccessApprovalRequest = "access_approval_requests",
|
||||
AccessApprovalRequestReviewer = "access_approval_requests_reviewers",
|
||||
SecretApprovalPolicy = "secret_approval_policies",
|
||||
SecretApprovalPolicyApprover = "secret_approval_policies_approvers",
|
||||
SecretApprovalRequest = "secret_approval_requests",
|
||||
@ -138,5 +143,6 @@ export enum ProjectUpgradeStatus {
|
||||
}
|
||||
|
||||
export enum IdentityAuthMethod {
|
||||
Univeral = "universal-auth"
|
||||
Univeral = "universal-auth",
|
||||
AWS_AUTH = "aws-auth"
|
||||
}
|
||||
|
@ -22,7 +22,7 @@ export const UsersSchema = z.object({
|
||||
updatedAt: z.date(),
|
||||
isGhost: z.boolean().default(false),
|
||||
username: z.string(),
|
||||
isEmailVerified: z.boolean().nullable().optional()
|
||||
isEmailVerified: z.boolean().default(false).nullable().optional()
|
||||
});
|
||||
|
||||
export type TUsers = z.infer<typeof UsersSchema>;
|
||||
|
168
backend/src/ee/routes/v1/access-approval-policy-router.ts
Normal file
168
backend/src/ee/routes/v1/access-approval-policy-router.ts
Normal file
@ -0,0 +1,168 @@
|
||||
import { nanoid } from "nanoid";
|
||||
import { z } from "zod";
|
||||
|
||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||
import { sapPubSchema } from "@app/server/routes/sanitizedSchemas";
|
||||
import { AuthMode } from "@app/services/auth/auth-type";
|
||||
|
||||
export const registerAccessApprovalPolicyRouter = async (server: FastifyZodProvider) => {
|
||||
server.route({
|
||||
url: "/",
|
||||
method: "POST",
|
||||
schema: {
|
||||
body: z
|
||||
.object({
|
||||
projectSlug: z.string().trim(),
|
||||
name: z.string().optional(),
|
||||
secretPath: z.string().trim().default("/"),
|
||||
environment: z.string(),
|
||||
approvers: z.string().array().min(1),
|
||||
approvals: z.number().min(1).default(1)
|
||||
})
|
||||
.refine((data) => data.approvals <= data.approvers.length, {
|
||||
path: ["approvals"],
|
||||
message: "The number of approvals should be lower than the number of approvers."
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
approval: sapPubSchema
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
handler: async (req) => {
|
||||
const approval = await server.services.accessApprovalPolicy.createAccessApprovalPolicy({
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId,
|
||||
...req.body,
|
||||
projectSlug: req.body.projectSlug,
|
||||
name: req.body.name ?? `${req.body.environment}-${nanoid(3)}`
|
||||
});
|
||||
return { approval };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
url: "/",
|
||||
method: "GET",
|
||||
schema: {
|
||||
querystring: z.object({
|
||||
projectSlug: z.string().trim()
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
approvals: sapPubSchema.extend({ approvers: z.string().array(), secretPath: z.string().optional() }).array()
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
handler: async (req) => {
|
||||
const approvals = await server.services.accessApprovalPolicy.getAccessApprovalPolicyByProjectSlug({
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId,
|
||||
projectSlug: req.query.projectSlug
|
||||
});
|
||||
return { approvals };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
url: "/count",
|
||||
method: "GET",
|
||||
schema: {
|
||||
querystring: z.object({
|
||||
projectSlug: z.string(),
|
||||
envSlug: z.string()
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
count: z.number()
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
handler: async (req) => {
|
||||
const { count } = await server.services.accessApprovalPolicy.getAccessPolicyCountByEnvSlug({
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
projectSlug: req.query.projectSlug,
|
||||
actorOrgId: req.permission.orgId,
|
||||
envSlug: req.query.envSlug
|
||||
});
|
||||
return { count };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
url: "/:policyId",
|
||||
method: "PATCH",
|
||||
schema: {
|
||||
params: z.object({
|
||||
policyId: z.string()
|
||||
}),
|
||||
body: z
|
||||
.object({
|
||||
name: z.string().optional(),
|
||||
secretPath: z
|
||||
.string()
|
||||
.trim()
|
||||
.optional()
|
||||
.transform((val) => (val === "" ? "/" : val)),
|
||||
approvers: z.string().array().min(1),
|
||||
approvals: z.number().min(1).default(1)
|
||||
})
|
||||
.refine((data) => data.approvals <= data.approvers.length, {
|
||||
path: ["approvals"],
|
||||
message: "The number of approvals should be lower than the number of approvers."
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
approval: sapPubSchema
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
handler: async (req) => {
|
||||
await server.services.accessApprovalPolicy.updateAccessApprovalPolicy({
|
||||
policyId: req.params.policyId,
|
||||
actor: req.permission.type,
|
||||
actorOrgId: req.permission.orgId,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
...req.body
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
url: "/:policyId",
|
||||
method: "DELETE",
|
||||
schema: {
|
||||
params: z.object({
|
||||
policyId: z.string()
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
approval: sapPubSchema
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
handler: async (req) => {
|
||||
const approval = await server.services.accessApprovalPolicy.deleteAccessApprovalPolicy({
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId,
|
||||
policyId: req.params.policyId
|
||||
});
|
||||
return { approval };
|
||||
}
|
||||
});
|
||||
};
|
160
backend/src/ee/routes/v1/access-approval-request-router.ts
Normal file
160
backend/src/ee/routes/v1/access-approval-request-router.ts
Normal file
@ -0,0 +1,160 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { AccessApprovalRequestsReviewersSchema, AccessApprovalRequestsSchema } from "@app/db/schemas";
|
||||
import { ApprovalStatus } from "@app/ee/services/access-approval-request/access-approval-request-types";
|
||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||
import { AuthMode } from "@app/services/auth/auth-type";
|
||||
|
||||
export const registerAccessApprovalRequestRouter = async (server: FastifyZodProvider) => {
|
||||
server.route({
|
||||
url: "/",
|
||||
method: "POST",
|
||||
schema: {
|
||||
body: z.object({
|
||||
permissions: z.any().array(),
|
||||
isTemporary: z.boolean(),
|
||||
temporaryRange: z.string().optional()
|
||||
}),
|
||||
querystring: z.object({
|
||||
projectSlug: z.string().trim()
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
approval: AccessApprovalRequestsSchema
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
handler: async (req) => {
|
||||
const { request } = await server.services.accessApprovalRequest.createAccessApprovalRequest({
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
permissions: req.body.permissions,
|
||||
actorOrgId: req.permission.orgId,
|
||||
projectSlug: req.query.projectSlug,
|
||||
temporaryRange: req.body.temporaryRange,
|
||||
isTemporary: req.body.isTemporary
|
||||
});
|
||||
return { approval: request };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
url: "/count",
|
||||
method: "GET",
|
||||
schema: {
|
||||
querystring: z.object({
|
||||
projectSlug: z.string().trim()
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
pendingCount: z.number(),
|
||||
finalizedCount: z.number()
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
handler: async (req) => {
|
||||
const { count } = await server.services.accessApprovalRequest.getCount({
|
||||
projectSlug: req.query.projectSlug,
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorOrgId: req.permission.orgId,
|
||||
actorAuthMethod: req.permission.authMethod
|
||||
});
|
||||
|
||||
return { ...count };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
url: "/",
|
||||
method: "GET",
|
||||
schema: {
|
||||
querystring: z.object({
|
||||
projectSlug: z.string().trim(),
|
||||
authorProjectMembershipId: z.string().trim().optional(),
|
||||
envSlug: z.string().trim().optional()
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
requests: AccessApprovalRequestsSchema.extend({
|
||||
environmentName: z.string(),
|
||||
isApproved: z.boolean(),
|
||||
privilege: z
|
||||
.object({
|
||||
membershipId: z.string(),
|
||||
isTemporary: z.boolean(),
|
||||
temporaryMode: z.string().nullish(),
|
||||
temporaryRange: z.string().nullish(),
|
||||
temporaryAccessStartTime: z.date().nullish(),
|
||||
temporaryAccessEndTime: z.date().nullish(),
|
||||
permissions: z.unknown()
|
||||
})
|
||||
.nullable(),
|
||||
policy: z.object({
|
||||
id: z.string(),
|
||||
name: z.string(),
|
||||
approvals: z.number(),
|
||||
approvers: z.string().array(),
|
||||
secretPath: z.string().nullish(),
|
||||
envId: z.string()
|
||||
}),
|
||||
reviewers: z
|
||||
.object({
|
||||
member: z.string(),
|
||||
status: z.string()
|
||||
})
|
||||
.array()
|
||||
}).array()
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
handler: async (req) => {
|
||||
const { requests } = await server.services.accessApprovalRequest.listApprovalRequests({
|
||||
projectSlug: req.query.projectSlug,
|
||||
authorProjectMembershipId: req.query.authorProjectMembershipId,
|
||||
envSlug: req.query.envSlug,
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorOrgId: req.permission.orgId,
|
||||
actorAuthMethod: req.permission.authMethod
|
||||
});
|
||||
|
||||
return { requests };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
url: "/:requestId/review",
|
||||
method: "POST",
|
||||
schema: {
|
||||
params: z.object({
|
||||
requestId: z.string().trim()
|
||||
}),
|
||||
body: z.object({
|
||||
status: z.enum([ApprovalStatus.APPROVED, ApprovalStatus.REJECTED])
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
review: AccessApprovalRequestsReviewersSchema
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
handler: async (req) => {
|
||||
const review = await server.services.accessApprovalRequest.reviewAccessRequest({
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorOrgId: req.permission.orgId,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
requestId: req.params.requestId,
|
||||
status: req.body.status
|
||||
});
|
||||
|
||||
return { review };
|
||||
}
|
||||
});
|
||||
};
|
@ -1,16 +1,14 @@
|
||||
import { MongoAbility, RawRuleOf } from "@casl/ability";
|
||||
import { PackRule, packRules, unpackRules } from "@casl/ability/extra";
|
||||
import { packRules } from "@casl/ability/extra";
|
||||
import slugify from "@sindresorhus/slugify";
|
||||
import ms from "ms";
|
||||
import { z } from "zod";
|
||||
|
||||
import { IdentityProjectAdditionalPrivilegeSchema } from "@app/db/schemas";
|
||||
import { IdentityProjectAdditionalPrivilegeTemporaryMode } from "@app/ee/services/identity-project-additional-privilege/identity-project-additional-privilege-types";
|
||||
import { ProjectPermissionSet } from "@app/ee/services/permission/project-permission";
|
||||
import { IDENTITY_ADDITIONAL_PRIVILEGE } from "@app/lib/api-docs";
|
||||
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
||||
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||
import { PermissionSchema, SanitizedIdentityPrivilegeSchema } from "@app/server/routes/sanitizedSchemas";
|
||||
import { AuthMode } from "@app/services/auth/auth-type";
|
||||
|
||||
export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: FastifyZodProvider) => {
|
||||
@ -41,11 +39,11 @@ export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: F
|
||||
})
|
||||
.optional()
|
||||
.describe(IDENTITY_ADDITIONAL_PRIVILEGE.CREATE.slug),
|
||||
permissions: z.any().array().describe(IDENTITY_ADDITIONAL_PRIVILEGE.CREATE.permissions)
|
||||
permissions: PermissionSchema.array().describe(IDENTITY_ADDITIONAL_PRIVILEGE.CREATE.permissions)
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
privilege: IdentityProjectAdditionalPrivilegeSchema
|
||||
privilege: SanitizedIdentityPrivilegeSchema
|
||||
})
|
||||
}
|
||||
},
|
||||
@ -92,7 +90,7 @@ export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: F
|
||||
})
|
||||
.optional()
|
||||
.describe(IDENTITY_ADDITIONAL_PRIVILEGE.CREATE.slug),
|
||||
permissions: z.any().array().describe(IDENTITY_ADDITIONAL_PRIVILEGE.CREATE.permissions),
|
||||
permissions: PermissionSchema.array().describe(IDENTITY_ADDITIONAL_PRIVILEGE.CREATE.permissions),
|
||||
temporaryMode: z
|
||||
.nativeEnum(IdentityProjectAdditionalPrivilegeTemporaryMode)
|
||||
.describe(IDENTITY_ADDITIONAL_PRIVILEGE.CREATE.temporaryMode),
|
||||
@ -107,7 +105,7 @@ export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: F
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
privilege: IdentityProjectAdditionalPrivilegeSchema
|
||||
privilege: SanitizedIdentityPrivilegeSchema
|
||||
})
|
||||
}
|
||||
},
|
||||
@ -157,7 +155,7 @@ export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: F
|
||||
message: "Slug must be a valid slug"
|
||||
})
|
||||
.describe(IDENTITY_ADDITIONAL_PRIVILEGE.UPDATE.newSlug),
|
||||
permissions: z.any().array().describe(IDENTITY_ADDITIONAL_PRIVILEGE.UPDATE.permissions),
|
||||
permissions: PermissionSchema.array().describe(IDENTITY_ADDITIONAL_PRIVILEGE.UPDATE.permissions),
|
||||
isTemporary: z.boolean().describe(IDENTITY_ADDITIONAL_PRIVILEGE.UPDATE.isTemporary),
|
||||
temporaryMode: z
|
||||
.nativeEnum(IdentityProjectAdditionalPrivilegeTemporaryMode)
|
||||
@ -175,7 +173,7 @@ export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: F
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
privilege: IdentityProjectAdditionalPrivilegeSchema
|
||||
privilege: SanitizedIdentityPrivilegeSchema
|
||||
})
|
||||
}
|
||||
},
|
||||
@ -219,7 +217,7 @@ export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: F
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
privilege: IdentityProjectAdditionalPrivilegeSchema
|
||||
privilege: SanitizedIdentityPrivilegeSchema
|
||||
})
|
||||
}
|
||||
},
|
||||
@ -260,7 +258,7 @@ export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: F
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
privilege: IdentityProjectAdditionalPrivilegeSchema
|
||||
privilege: SanitizedIdentityPrivilegeSchema
|
||||
})
|
||||
}
|
||||
},
|
||||
@ -293,16 +291,11 @@ export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: F
|
||||
],
|
||||
querystring: z.object({
|
||||
identityId: z.string().min(1).describe(IDENTITY_ADDITIONAL_PRIVILEGE.LIST.identityId),
|
||||
projectSlug: z.string().min(1).describe(IDENTITY_ADDITIONAL_PRIVILEGE.LIST.projectSlug),
|
||||
unpacked: z
|
||||
.enum(["false", "true"])
|
||||
.transform((el) => el === "true")
|
||||
.default("true")
|
||||
.describe(IDENTITY_ADDITIONAL_PRIVILEGE.LIST.unpacked)
|
||||
projectSlug: z.string().min(1).describe(IDENTITY_ADDITIONAL_PRIVILEGE.LIST.projectSlug)
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
privileges: IdentityProjectAdditionalPrivilegeSchema.array()
|
||||
privileges: SanitizedIdentityPrivilegeSchema.array()
|
||||
})
|
||||
}
|
||||
},
|
||||
@ -315,15 +308,9 @@ export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: F
|
||||
actorOrgId: req.permission.orgId,
|
||||
...req.query
|
||||
});
|
||||
if (req.query.unpacked) {
|
||||
return {
|
||||
privileges: privileges.map(({ permissions, ...el }) => ({
|
||||
...el,
|
||||
permissions: unpackRules(permissions as PackRule<RawRuleOf<MongoAbility<ProjectPermissionSet>>>[])
|
||||
}))
|
||||
};
|
||||
}
|
||||
return { privileges };
|
||||
return {
|
||||
privileges
|
||||
};
|
||||
}
|
||||
});
|
||||
};
|
||||
|
@ -1,3 +1,5 @@
|
||||
import { registerAccessApprovalPolicyRouter } from "./access-approval-policy-router";
|
||||
import { registerAccessApprovalRequestRouter } from "./access-approval-request-router";
|
||||
import { registerAuditLogStreamRouter } from "./audit-log-stream-router";
|
||||
import { registerDynamicSecretLeaseRouter } from "./dynamic-secret-lease-router";
|
||||
import { registerDynamicSecretRouter } from "./dynamic-secret-router";
|
||||
@ -41,6 +43,9 @@ export const registerV1EERoutes = async (server: FastifyZodProvider) => {
|
||||
prefix: "/secret-rotation-providers"
|
||||
});
|
||||
|
||||
await server.register(registerAccessApprovalPolicyRouter, { prefix: "/access-approvals/policies" });
|
||||
await server.register(registerAccessApprovalRequestRouter, { prefix: "/access-approvals/requests" });
|
||||
|
||||
await server.register(
|
||||
async (dynamicSecretRouter) => {
|
||||
await dynamicSecretRouter.register(registerDynamicSecretRouter);
|
||||
|
10
backend/src/ee/services/access-approval-policy/access-approval-policy-approver-dal.ts
Normal file
10
backend/src/ee/services/access-approval-policy/access-approval-policy-approver-dal.ts
Normal file
@ -0,0 +1,10 @@
|
||||
import { TDbClient } from "@app/db";
|
||||
import { TableName } from "@app/db/schemas";
|
||||
import { ormify } from "@app/lib/knex";
|
||||
|
||||
export type TAccessApprovalPolicyApproverDALFactory = ReturnType<typeof accessApprovalPolicyApproverDALFactory>;
|
||||
|
||||
export const accessApprovalPolicyApproverDALFactory = (db: TDbClient) => {
|
||||
const accessApprovalPolicyApproverOrm = ormify(db, TableName.AccessApprovalPolicyApprover);
|
||||
return { ...accessApprovalPolicyApproverOrm };
|
||||
};
|
@ -0,0 +1,76 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { TDbClient } from "@app/db";
|
||||
import { TableName, TAccessApprovalPolicies } from "@app/db/schemas";
|
||||
import { DatabaseError } from "@app/lib/errors";
|
||||
import { buildFindFilter, mergeOneToManyRelation, ormify, selectAllTableCols, TFindFilter } from "@app/lib/knex";
|
||||
|
||||
export type TAccessApprovalPolicyDALFactory = ReturnType<typeof accessApprovalPolicyDALFactory>;
|
||||
|
||||
export const accessApprovalPolicyDALFactory = (db: TDbClient) => {
|
||||
const accessApprovalPolicyOrm = ormify(db, TableName.AccessApprovalPolicy);
|
||||
|
||||
const accessApprovalPolicyFindQuery = async (tx: Knex, filter: TFindFilter<TAccessApprovalPolicies>) => {
|
||||
const result = await tx(TableName.AccessApprovalPolicy)
|
||||
// eslint-disable-next-line
|
||||
.where(buildFindFilter(filter))
|
||||
.join(TableName.Environment, `${TableName.AccessApprovalPolicy}.envId`, `${TableName.Environment}.id`)
|
||||
.join(
|
||||
TableName.AccessApprovalPolicyApprover,
|
||||
`${TableName.AccessApprovalPolicy}.id`,
|
||||
`${TableName.AccessApprovalPolicyApprover}.policyId`
|
||||
)
|
||||
.select(tx.ref("approverId").withSchema(TableName.AccessApprovalPolicyApprover))
|
||||
.select(tx.ref("name").withSchema(TableName.Environment).as("envName"))
|
||||
.select(tx.ref("slug").withSchema(TableName.Environment).as("envSlug"))
|
||||
.select(tx.ref("id").withSchema(TableName.Environment).as("envId"))
|
||||
.select(tx.ref("projectId").withSchema(TableName.Environment))
|
||||
.select(selectAllTableCols(TableName.AccessApprovalPolicy));
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
const findById = async (id: string, tx?: Knex) => {
|
||||
try {
|
||||
const doc = await accessApprovalPolicyFindQuery(tx || db, {
|
||||
[`${TableName.AccessApprovalPolicy}.id` as "id"]: id
|
||||
});
|
||||
const formatedDoc = mergeOneToManyRelation(
|
||||
doc,
|
||||
"id",
|
||||
({ approverId, envId, envName: name, envSlug: slug, ...el }) => ({
|
||||
...el,
|
||||
envId,
|
||||
environment: { id: envId, name, slug }
|
||||
}),
|
||||
({ approverId }) => approverId,
|
||||
"approvers"
|
||||
);
|
||||
return formatedDoc?.[0];
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: "FindById" });
|
||||
}
|
||||
};
|
||||
|
||||
const find = async (filter: TFindFilter<TAccessApprovalPolicies & { projectId: string }>, tx?: Knex) => {
|
||||
try {
|
||||
const docs = await accessApprovalPolicyFindQuery(tx || db, filter);
|
||||
const formatedDoc = mergeOneToManyRelation(
|
||||
docs,
|
||||
"id",
|
||||
({ approverId, envId, envName: name, envSlug: slug, ...el }) => ({
|
||||
...el,
|
||||
envId,
|
||||
environment: { id: envId, name, slug }
|
||||
}),
|
||||
({ approverId }) => approverId,
|
||||
"approvers"
|
||||
);
|
||||
return formatedDoc.map((policy) => ({ ...policy, secretPath: policy.secretPath || undefined }));
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: "Find" });
|
||||
}
|
||||
};
|
||||
|
||||
return { ...accessApprovalPolicyOrm, find, findById };
|
||||
};
|
@ -0,0 +1,36 @@
|
||||
import { ForbiddenError, subject } from "@casl/ability";
|
||||
|
||||
import { BadRequestError } from "@app/lib/errors";
|
||||
import { ActorType } from "@app/services/auth/auth-type";
|
||||
|
||||
import { ProjectPermissionActions, ProjectPermissionSub } from "../permission/project-permission";
|
||||
import { TVerifyApprovers } from "./access-approval-policy-types";
|
||||
|
||||
export const verifyApprovers = async ({
|
||||
userIds,
|
||||
projectId,
|
||||
orgId,
|
||||
envSlug,
|
||||
actorAuthMethod,
|
||||
secretPath,
|
||||
permissionService
|
||||
}: TVerifyApprovers) => {
|
||||
for await (const userId of userIds) {
|
||||
try {
|
||||
const { permission: approverPermission } = await permissionService.getProjectPermission(
|
||||
ActorType.USER,
|
||||
userId,
|
||||
projectId,
|
||||
actorAuthMethod,
|
||||
orgId
|
||||
);
|
||||
|
||||
ForbiddenError.from(approverPermission).throwUnlessCan(
|
||||
ProjectPermissionActions.Create,
|
||||
subject(ProjectPermissionSub.Secrets, { environment: envSlug, secretPath })
|
||||
);
|
||||
} catch (err) {
|
||||
throw new BadRequestError({ message: "One or more approvers doesn't have access to be specified secret path" });
|
||||
}
|
||||
}
|
||||
};
|
@ -0,0 +1,273 @@
|
||||
import { ForbiddenError } from "@casl/ability";
|
||||
|
||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
|
||||
import { BadRequestError } from "@app/lib/errors";
|
||||
import { TProjectDALFactory } from "@app/services/project/project-dal";
|
||||
import { TProjectEnvDALFactory } from "@app/services/project-env/project-env-dal";
|
||||
import { TProjectMembershipDALFactory } from "@app/services/project-membership/project-membership-dal";
|
||||
|
||||
import { TAccessApprovalPolicyApproverDALFactory } from "./access-approval-policy-approver-dal";
|
||||
import { TAccessApprovalPolicyDALFactory } from "./access-approval-policy-dal";
|
||||
import { verifyApprovers } from "./access-approval-policy-fns";
|
||||
import {
|
||||
TCreateAccessApprovalPolicy,
|
||||
TDeleteAccessApprovalPolicy,
|
||||
TGetAccessPolicyCountByEnvironmentDTO,
|
||||
TListAccessApprovalPoliciesDTO,
|
||||
TUpdateAccessApprovalPolicy
|
||||
} from "./access-approval-policy-types";
|
||||
|
||||
type TSecretApprovalPolicyServiceFactoryDep = {
|
||||
projectDAL: TProjectDALFactory;
|
||||
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
|
||||
accessApprovalPolicyDAL: TAccessApprovalPolicyDALFactory;
|
||||
projectEnvDAL: Pick<TProjectEnvDALFactory, "find" | "findOne">;
|
||||
accessApprovalPolicyApproverDAL: TAccessApprovalPolicyApproverDALFactory;
|
||||
projectMembershipDAL: Pick<TProjectMembershipDALFactory, "find">;
|
||||
};
|
||||
|
||||
export type TAccessApprovalPolicyServiceFactory = ReturnType<typeof accessApprovalPolicyServiceFactory>;
|
||||
|
||||
export const accessApprovalPolicyServiceFactory = ({
|
||||
accessApprovalPolicyDAL,
|
||||
accessApprovalPolicyApproverDAL,
|
||||
permissionService,
|
||||
projectEnvDAL,
|
||||
projectDAL,
|
||||
projectMembershipDAL
|
||||
}: TSecretApprovalPolicyServiceFactoryDep) => {
|
||||
const createAccessApprovalPolicy = async ({
|
||||
name,
|
||||
actor,
|
||||
actorId,
|
||||
actorOrgId,
|
||||
secretPath,
|
||||
actorAuthMethod,
|
||||
approvals,
|
||||
approvers,
|
||||
projectSlug,
|
||||
environment
|
||||
}: TCreateAccessApprovalPolicy) => {
|
||||
const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId);
|
||||
if (!project) throw new BadRequestError({ message: "Project not found" });
|
||||
|
||||
if (approvals > approvers.length)
|
||||
throw new BadRequestError({ message: "Approvals cannot be greater than approvers" });
|
||||
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
actorId,
|
||||
project.id,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Create,
|
||||
ProjectPermissionSub.SecretApproval
|
||||
);
|
||||
const env = await projectEnvDAL.findOne({ slug: environment, projectId: project.id });
|
||||
if (!env) throw new BadRequestError({ message: "Environment not found" });
|
||||
|
||||
const secretApprovers = await projectMembershipDAL.find({
|
||||
projectId: project.id,
|
||||
$in: { id: approvers }
|
||||
});
|
||||
|
||||
if (secretApprovers.length !== approvers.length) {
|
||||
throw new BadRequestError({ message: "Approver not found in project" });
|
||||
}
|
||||
|
||||
await verifyApprovers({
|
||||
projectId: project.id,
|
||||
orgId: actorOrgId,
|
||||
envSlug: environment,
|
||||
secretPath,
|
||||
actorAuthMethod,
|
||||
permissionService,
|
||||
userIds: secretApprovers.map((approver) => approver.userId)
|
||||
});
|
||||
|
||||
const accessApproval = await accessApprovalPolicyDAL.transaction(async (tx) => {
|
||||
const doc = await accessApprovalPolicyDAL.create(
|
||||
{
|
||||
envId: env.id,
|
||||
approvals,
|
||||
secretPath,
|
||||
name
|
||||
},
|
||||
tx
|
||||
);
|
||||
await accessApprovalPolicyApproverDAL.insertMany(
|
||||
secretApprovers.map(({ id }) => ({
|
||||
approverId: id,
|
||||
policyId: doc.id
|
||||
})),
|
||||
tx
|
||||
);
|
||||
return doc;
|
||||
});
|
||||
return { ...accessApproval, environment: env, projectId: project.id };
|
||||
};
|
||||
|
||||
const getAccessApprovalPolicyByProjectSlug = async ({
|
||||
actorId,
|
||||
actor,
|
||||
actorOrgId,
|
||||
actorAuthMethod,
|
||||
projectSlug
|
||||
}: TListAccessApprovalPoliciesDTO) => {
|
||||
const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId);
|
||||
if (!project) throw new BadRequestError({ message: "Project not found" });
|
||||
|
||||
// Anyone in the project should be able to get the policies.
|
||||
/* const { permission } = */ await permissionService.getProjectPermission(
|
||||
actor,
|
||||
actorId,
|
||||
project.id,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
// ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.SecretApproval);
|
||||
|
||||
const accessApprovalPolicies = await accessApprovalPolicyDAL.find({ projectId: project.id });
|
||||
return accessApprovalPolicies;
|
||||
};
|
||||
|
||||
const updateAccessApprovalPolicy = async ({
|
||||
policyId,
|
||||
approvers,
|
||||
secretPath,
|
||||
name,
|
||||
actorId,
|
||||
actor,
|
||||
actorOrgId,
|
||||
actorAuthMethod,
|
||||
approvals
|
||||
}: TUpdateAccessApprovalPolicy) => {
|
||||
const accessApprovalPolicy = await accessApprovalPolicyDAL.findById(policyId);
|
||||
if (!accessApprovalPolicy) throw new BadRequestError({ message: "Secret approval policy not found" });
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
actorId,
|
||||
accessApprovalPolicy.projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.SecretApproval);
|
||||
|
||||
const updatedPolicy = await accessApprovalPolicyDAL.transaction(async (tx) => {
|
||||
const doc = await accessApprovalPolicyDAL.updateById(
|
||||
accessApprovalPolicy.id,
|
||||
{
|
||||
approvals,
|
||||
secretPath,
|
||||
name
|
||||
},
|
||||
tx
|
||||
);
|
||||
if (approvers) {
|
||||
// Find the workspace project memberships of the users passed in the approvers array
|
||||
const secretApprovers = await projectMembershipDAL.find(
|
||||
{
|
||||
projectId: accessApprovalPolicy.projectId,
|
||||
$in: { id: approvers }
|
||||
},
|
||||
{ tx }
|
||||
);
|
||||
|
||||
await verifyApprovers({
|
||||
projectId: accessApprovalPolicy.projectId,
|
||||
orgId: actorOrgId,
|
||||
envSlug: accessApprovalPolicy.environment.slug,
|
||||
secretPath: doc.secretPath!,
|
||||
actorAuthMethod,
|
||||
permissionService,
|
||||
userIds: secretApprovers.map((approver) => approver.userId)
|
||||
});
|
||||
|
||||
if (secretApprovers.length !== approvers.length)
|
||||
throw new BadRequestError({ message: "Approvals cannot be greater than approvers" });
|
||||
await accessApprovalPolicyApproverDAL.delete({ policyId: doc.id }, tx);
|
||||
await accessApprovalPolicyApproverDAL.insertMany(
|
||||
secretApprovers.map(({ id }) => ({
|
||||
approverId: id,
|
||||
policyId: doc.id
|
||||
})),
|
||||
tx
|
||||
);
|
||||
}
|
||||
return doc;
|
||||
});
|
||||
return {
|
||||
...updatedPolicy,
|
||||
environment: accessApprovalPolicy.environment,
|
||||
projectId: accessApprovalPolicy.projectId
|
||||
};
|
||||
};
|
||||
|
||||
const deleteAccessApprovalPolicy = async ({
|
||||
policyId,
|
||||
actor,
|
||||
actorId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
}: TDeleteAccessApprovalPolicy) => {
|
||||
const policy = await accessApprovalPolicyDAL.findById(policyId);
|
||||
if (!policy) throw new BadRequestError({ message: "Secret approval policy not found" });
|
||||
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
actorId,
|
||||
policy.projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Delete,
|
||||
ProjectPermissionSub.SecretApproval
|
||||
);
|
||||
|
||||
await accessApprovalPolicyDAL.deleteById(policyId);
|
||||
return policy;
|
||||
};
|
||||
|
||||
const getAccessPolicyCountByEnvSlug = async ({
|
||||
actor,
|
||||
actorOrgId,
|
||||
actorAuthMethod,
|
||||
projectSlug,
|
||||
actorId,
|
||||
envSlug
|
||||
}: TGetAccessPolicyCountByEnvironmentDTO) => {
|
||||
const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId);
|
||||
|
||||
if (!project) throw new BadRequestError({ message: "Project not found" });
|
||||
|
||||
const { membership } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
actorId,
|
||||
project.id,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
if (!membership) throw new BadRequestError({ message: "User not found in project" });
|
||||
|
||||
const environment = await projectEnvDAL.findOne({ projectId: project.id, slug: envSlug });
|
||||
if (!environment) throw new BadRequestError({ message: "Environment not found" });
|
||||
|
||||
const policies = await accessApprovalPolicyDAL.find({ envId: environment.id, projectId: project.id });
|
||||
if (!policies) throw new BadRequestError({ message: "No policies found" });
|
||||
|
||||
return { count: policies.length };
|
||||
};
|
||||
|
||||
return {
|
||||
getAccessPolicyCountByEnvSlug,
|
||||
createAccessApprovalPolicy,
|
||||
deleteAccessApprovalPolicy,
|
||||
updateAccessApprovalPolicy,
|
||||
getAccessApprovalPolicyByProjectSlug
|
||||
};
|
||||
};
|
@ -0,0 +1,44 @@
|
||||
import { TProjectPermission } from "@app/lib/types";
|
||||
import { ActorAuthMethod } from "@app/services/auth/auth-type";
|
||||
|
||||
import { TPermissionServiceFactory } from "../permission/permission-service";
|
||||
|
||||
export type TVerifyApprovers = {
|
||||
userIds: string[];
|
||||
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
|
||||
envSlug: string;
|
||||
actorAuthMethod: ActorAuthMethod;
|
||||
secretPath: string;
|
||||
projectId: string;
|
||||
orgId: string;
|
||||
};
|
||||
|
||||
export type TCreateAccessApprovalPolicy = {
|
||||
approvals: number;
|
||||
secretPath: string;
|
||||
environment: string;
|
||||
approvers: string[];
|
||||
projectSlug: string;
|
||||
name: string;
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
export type TUpdateAccessApprovalPolicy = {
|
||||
policyId: string;
|
||||
approvals?: number;
|
||||
approvers?: string[];
|
||||
secretPath?: string;
|
||||
name?: string;
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
export type TDeleteAccessApprovalPolicy = {
|
||||
policyId: string;
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
export type TGetAccessPolicyCountByEnvironmentDTO = {
|
||||
envSlug: string;
|
||||
projectSlug: string;
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
export type TListAccessApprovalPoliciesDTO = {
|
||||
projectSlug: string;
|
||||
} & Omit<TProjectPermission, "projectId">;
|
@ -0,0 +1,266 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { TDbClient } from "@app/db";
|
||||
import { AccessApprovalRequestsSchema, TableName, TAccessApprovalRequests } from "@app/db/schemas";
|
||||
import { DatabaseError } from "@app/lib/errors";
|
||||
import { ormify, selectAllTableCols, sqlNestRelationships, TFindFilter } from "@app/lib/knex";
|
||||
|
||||
import { ApprovalStatus } from "./access-approval-request-types";
|
||||
|
||||
export type TAccessApprovalRequestDALFactory = ReturnType<typeof accessApprovalRequestDALFactory>;
|
||||
|
||||
export const accessApprovalRequestDALFactory = (db: TDbClient) => {
|
||||
const accessApprovalRequestOrm = ormify(db, TableName.AccessApprovalRequest);
|
||||
|
||||
const findRequestsWithPrivilegeByPolicyIds = async (policyIds: string[]) => {
|
||||
try {
|
||||
const docs = await db(TableName.AccessApprovalRequest)
|
||||
.whereIn(`${TableName.AccessApprovalRequest}.policyId`, policyIds)
|
||||
|
||||
.leftJoin(
|
||||
TableName.ProjectUserAdditionalPrivilege,
|
||||
`${TableName.AccessApprovalRequest}.privilegeId`,
|
||||
`${TableName.ProjectUserAdditionalPrivilege}.id`
|
||||
)
|
||||
.leftJoin(
|
||||
TableName.AccessApprovalPolicy,
|
||||
`${TableName.AccessApprovalRequest}.policyId`,
|
||||
`${TableName.AccessApprovalPolicy}.id`
|
||||
)
|
||||
|
||||
.leftJoin(
|
||||
TableName.AccessApprovalRequestReviewer,
|
||||
`${TableName.AccessApprovalRequest}.id`,
|
||||
`${TableName.AccessApprovalRequestReviewer}.requestId`
|
||||
)
|
||||
.leftJoin(
|
||||
TableName.AccessApprovalPolicyApprover,
|
||||
`${TableName.AccessApprovalPolicy}.id`,
|
||||
`${TableName.AccessApprovalPolicyApprover}.policyId`
|
||||
)
|
||||
|
||||
.leftJoin(TableName.Environment, `${TableName.AccessApprovalPolicy}.envId`, `${TableName.Environment}.id`)
|
||||
|
||||
.select(selectAllTableCols(TableName.AccessApprovalRequest))
|
||||
.select(
|
||||
db.ref("id").withSchema(TableName.AccessApprovalPolicy).as("policyId"),
|
||||
db.ref("name").withSchema(TableName.AccessApprovalPolicy).as("policyName"),
|
||||
db.ref("approvals").withSchema(TableName.AccessApprovalPolicy).as("policyApprovals"),
|
||||
db.ref("secretPath").withSchema(TableName.AccessApprovalPolicy).as("policySecretPath"),
|
||||
db.ref("envId").withSchema(TableName.AccessApprovalPolicy).as("policyEnvId")
|
||||
)
|
||||
|
||||
.select(db.ref("approverId").withSchema(TableName.AccessApprovalPolicyApprover))
|
||||
|
||||
.select(
|
||||
db.ref("projectId").withSchema(TableName.Environment),
|
||||
db.ref("slug").withSchema(TableName.Environment).as("envSlug"),
|
||||
db.ref("name").withSchema(TableName.Environment).as("envName")
|
||||
)
|
||||
|
||||
.select(
|
||||
db.ref("member").withSchema(TableName.AccessApprovalRequestReviewer).as("reviewerMemberId"),
|
||||
db.ref("status").withSchema(TableName.AccessApprovalRequestReviewer).as("reviewerStatus")
|
||||
)
|
||||
|
||||
.select(
|
||||
db
|
||||
.ref("projectMembershipId")
|
||||
.withSchema(TableName.ProjectUserAdditionalPrivilege)
|
||||
.as("privilegeMembershipId"),
|
||||
db.ref("isTemporary").withSchema(TableName.ProjectUserAdditionalPrivilege).as("privilegeIsTemporary"),
|
||||
db.ref("temporaryMode").withSchema(TableName.ProjectUserAdditionalPrivilege).as("privilegeTemporaryMode"),
|
||||
db.ref("temporaryRange").withSchema(TableName.ProjectUserAdditionalPrivilege).as("privilegeTemporaryRange"),
|
||||
db
|
||||
.ref("temporaryAccessStartTime")
|
||||
.withSchema(TableName.ProjectUserAdditionalPrivilege)
|
||||
.as("privilegeTemporaryAccessStartTime"),
|
||||
db
|
||||
.ref("temporaryAccessEndTime")
|
||||
.withSchema(TableName.ProjectUserAdditionalPrivilege)
|
||||
.as("privilegeTemporaryAccessEndTime"),
|
||||
|
||||
db.ref("permissions").withSchema(TableName.ProjectUserAdditionalPrivilege).as("privilegePermissions")
|
||||
)
|
||||
.orderBy(`${TableName.AccessApprovalRequest}.createdAt`, "desc");
|
||||
|
||||
const formattedDocs = sqlNestRelationships({
|
||||
data: docs,
|
||||
key: "id",
|
||||
parentMapper: (doc) => ({
|
||||
...AccessApprovalRequestsSchema.parse(doc),
|
||||
projectId: doc.projectId,
|
||||
environment: doc.envSlug,
|
||||
environmentName: doc.envName,
|
||||
policy: {
|
||||
id: doc.policyId,
|
||||
name: doc.policyName,
|
||||
approvals: doc.policyApprovals,
|
||||
secretPath: doc.policySecretPath,
|
||||
envId: doc.policyEnvId
|
||||
},
|
||||
privilege: doc.privilegeId
|
||||
? {
|
||||
membershipId: doc.privilegeMembershipId,
|
||||
isTemporary: doc.privilegeIsTemporary,
|
||||
temporaryMode: doc.privilegeTemporaryMode,
|
||||
temporaryRange: doc.privilegeTemporaryRange,
|
||||
temporaryAccessStartTime: doc.privilegeTemporaryAccessStartTime,
|
||||
temporaryAccessEndTime: doc.privilegeTemporaryAccessEndTime,
|
||||
permissions: doc.privilegePermissions
|
||||
}
|
||||
: null,
|
||||
|
||||
isApproved: !!doc.privilegeId
|
||||
}),
|
||||
childrenMapper: [
|
||||
{
|
||||
key: "reviewerMemberId",
|
||||
label: "reviewers" as const,
|
||||
mapper: ({ reviewerMemberId: member, reviewerStatus: status }) => (member ? { member, status } : undefined)
|
||||
},
|
||||
{ key: "approverId", label: "approvers" as const, mapper: ({ approverId }) => approverId }
|
||||
]
|
||||
});
|
||||
|
||||
if (!formattedDocs) return [];
|
||||
|
||||
return formattedDocs.map((doc) => ({
|
||||
...doc,
|
||||
policy: { ...doc.policy, approvers: doc.approvers }
|
||||
}));
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: "FindRequestsWithPrivilege" });
|
||||
}
|
||||
};
|
||||
|
||||
const findQuery = (filter: TFindFilter<TAccessApprovalRequests>, tx: Knex) =>
|
||||
tx(TableName.AccessApprovalRequest)
|
||||
.where(filter)
|
||||
.join(
|
||||
TableName.AccessApprovalPolicy,
|
||||
`${TableName.AccessApprovalRequest}.policyId`,
|
||||
`${TableName.AccessApprovalPolicy}.id`
|
||||
)
|
||||
|
||||
.join(
|
||||
TableName.AccessApprovalPolicyApprover,
|
||||
`${TableName.AccessApprovalPolicy}.id`,
|
||||
`${TableName.AccessApprovalPolicyApprover}.policyId`
|
||||
)
|
||||
.leftJoin(
|
||||
TableName.AccessApprovalRequestReviewer,
|
||||
`${TableName.AccessApprovalRequest}.id`,
|
||||
`${TableName.AccessApprovalRequestReviewer}.requestId`
|
||||
)
|
||||
|
||||
.leftJoin(TableName.Environment, `${TableName.AccessApprovalPolicy}.envId`, `${TableName.Environment}.id`)
|
||||
.select(selectAllTableCols(TableName.AccessApprovalRequest))
|
||||
.select(
|
||||
tx.ref("member").withSchema(TableName.AccessApprovalRequestReviewer).as("reviewerMemberId"),
|
||||
tx.ref("status").withSchema(TableName.AccessApprovalRequestReviewer).as("reviewerStatus"),
|
||||
tx.ref("id").withSchema(TableName.AccessApprovalPolicy).as("policyId"),
|
||||
tx.ref("name").withSchema(TableName.AccessApprovalPolicy).as("policyName"),
|
||||
tx.ref("projectId").withSchema(TableName.Environment),
|
||||
tx.ref("slug").withSchema(TableName.Environment).as("environment"),
|
||||
tx.ref("secretPath").withSchema(TableName.AccessApprovalPolicy).as("policySecretPath"),
|
||||
tx.ref("approvals").withSchema(TableName.AccessApprovalPolicy).as("policyApprovals"),
|
||||
tx.ref("approverId").withSchema(TableName.AccessApprovalPolicyApprover)
|
||||
);
|
||||
|
||||
const findById = async (id: string, tx?: Knex) => {
|
||||
try {
|
||||
const sql = findQuery({ [`${TableName.AccessApprovalRequest}.id` as "id"]: id }, tx || db);
|
||||
const docs = await sql;
|
||||
const formatedDoc = sqlNestRelationships({
|
||||
data: docs,
|
||||
key: "id",
|
||||
parentMapper: (el) => ({
|
||||
...AccessApprovalRequestsSchema.parse(el),
|
||||
projectId: el.projectId,
|
||||
environment: el.environment,
|
||||
policy: {
|
||||
id: el.policyId,
|
||||
name: el.policyName,
|
||||
approvals: el.policyApprovals,
|
||||
secretPath: el.policySecretPath
|
||||
}
|
||||
}),
|
||||
childrenMapper: [
|
||||
{
|
||||
key: "reviewerMemberId",
|
||||
label: "reviewers" as const,
|
||||
mapper: ({ reviewerMemberId: member, reviewerStatus: status }) => (member ? { member, status } : undefined)
|
||||
},
|
||||
{ key: "approverId", label: "approvers" as const, mapper: ({ approverId }) => approverId }
|
||||
]
|
||||
});
|
||||
if (!formatedDoc?.[0]) return;
|
||||
return {
|
||||
...formatedDoc[0],
|
||||
policy: { ...formatedDoc[0].policy, approvers: formatedDoc[0].approvers }
|
||||
};
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: "FindByIdAccessApprovalRequest" });
|
||||
}
|
||||
};
|
||||
|
||||
const getCount = async ({ projectId }: { projectId: string }) => {
|
||||
try {
|
||||
const accessRequests = await db(TableName.AccessApprovalRequest)
|
||||
.leftJoin(
|
||||
TableName.AccessApprovalPolicy,
|
||||
`${TableName.AccessApprovalRequest}.policyId`,
|
||||
`${TableName.AccessApprovalPolicy}.id`
|
||||
)
|
||||
.leftJoin(TableName.Environment, `${TableName.AccessApprovalPolicy}.envId`, `${TableName.Environment}.id`)
|
||||
.leftJoin(
|
||||
TableName.ProjectUserAdditionalPrivilege,
|
||||
`${TableName.AccessApprovalRequest}.privilegeId`,
|
||||
`${TableName.ProjectUserAdditionalPrivilege}.id`
|
||||
)
|
||||
|
||||
.leftJoin(
|
||||
TableName.AccessApprovalRequestReviewer,
|
||||
`${TableName.AccessApprovalRequest}.id`,
|
||||
`${TableName.AccessApprovalRequestReviewer}.requestId`
|
||||
)
|
||||
|
||||
.where(`${TableName.Environment}.projectId`, projectId)
|
||||
.select(selectAllTableCols(TableName.AccessApprovalRequest))
|
||||
.select(db.ref("status").withSchema(TableName.AccessApprovalRequestReviewer).as("reviewerStatus"))
|
||||
.select(db.ref("member").withSchema(TableName.AccessApprovalRequestReviewer).as("reviewerMemberId"));
|
||||
|
||||
const formattedRequests = sqlNestRelationships({
|
||||
data: accessRequests,
|
||||
key: "id",
|
||||
parentMapper: (doc) => ({
|
||||
...AccessApprovalRequestsSchema.parse(doc)
|
||||
}),
|
||||
childrenMapper: [
|
||||
{
|
||||
key: "reviewerMemberId",
|
||||
label: "reviewers" as const,
|
||||
mapper: ({ reviewerMemberId: member, reviewerStatus: status }) => (member ? { member, status } : undefined)
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
// an approval is pending if there is no reviewer rejections and no privilege ID is set
|
||||
const pendingApprovals = formattedRequests.filter(
|
||||
(req) => !req.privilegeId && !req.reviewers.some((r) => r.status === ApprovalStatus.REJECTED)
|
||||
);
|
||||
|
||||
// an approval is finalized if there are any rejections or a privilege ID is set
|
||||
const finalizedApprovals = formattedRequests.filter(
|
||||
(req) => req.privilegeId || req.reviewers.some((r) => r.status === ApprovalStatus.REJECTED)
|
||||
);
|
||||
|
||||
return { pendingCount: pendingApprovals.length, finalizedCount: finalizedApprovals.length };
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: "GetCountAccessApprovalRequest" });
|
||||
}
|
||||
};
|
||||
|
||||
return { ...accessApprovalRequestOrm, findById, findRequestsWithPrivilegeByPolicyIds, getCount };
|
||||
};
|
@ -0,0 +1,53 @@
|
||||
import { PackRule, unpackRules } from "@casl/ability/extra";
|
||||
|
||||
import { UnauthorizedError } from "@app/lib/errors";
|
||||
|
||||
import { TVerifyPermission } from "./access-approval-request-types";
|
||||
|
||||
function filterUnique(value: string, index: number, array: string[]) {
|
||||
return array.indexOf(value) === index;
|
||||
}
|
||||
|
||||
export const verifyRequestedPermissions = ({ permissions }: TVerifyPermission) => {
|
||||
const permission = unpackRules(
|
||||
permissions as PackRule<{
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
conditions?: Record<string, any>;
|
||||
action: string;
|
||||
subject: [string];
|
||||
}>[]
|
||||
);
|
||||
|
||||
if (!permission || !permission.length) {
|
||||
throw new UnauthorizedError({ message: "No permission provided" });
|
||||
}
|
||||
|
||||
const requestedPermissions: string[] = [];
|
||||
|
||||
for (const p of permission) {
|
||||
if (p.action[0] === "read") requestedPermissions.push("Read Access");
|
||||
if (p.action[0] === "create") requestedPermissions.push("Create Access");
|
||||
if (p.action[0] === "delete") requestedPermissions.push("Delete Access");
|
||||
if (p.action[0] === "edit") requestedPermissions.push("Edit Access");
|
||||
}
|
||||
|
||||
const firstPermission = permission[0];
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars, @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access
|
||||
const permissionSecretPath = firstPermission.conditions?.secretPath?.$glob;
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars, @typescript-eslint/no-unsafe-assignment
|
||||
const permissionEnv = firstPermission.conditions?.environment;
|
||||
|
||||
if (!permissionEnv || typeof permissionEnv !== "string") {
|
||||
throw new UnauthorizedError({ message: "Permission environment is not a string" });
|
||||
}
|
||||
if (!permissionSecretPath || typeof permissionSecretPath !== "string") {
|
||||
throw new UnauthorizedError({ message: "Permission path is not a string" });
|
||||
}
|
||||
|
||||
return {
|
||||
envSlug: permissionEnv,
|
||||
secretPath: permissionSecretPath,
|
||||
accessTypes: requestedPermissions.filter(filterUnique)
|
||||
};
|
||||
};
|
10
backend/src/ee/services/access-approval-request/access-approval-request-reviewer-dal.ts
Normal file
10
backend/src/ee/services/access-approval-request/access-approval-request-reviewer-dal.ts
Normal file
@ -0,0 +1,10 @@
|
||||
import { TDbClient } from "@app/db";
|
||||
import { TableName } from "@app/db/schemas";
|
||||
import { ormify } from "@app/lib/knex";
|
||||
|
||||
export type TAccessApprovalRequestReviewerDALFactory = ReturnType<typeof accessApprovalRequestReviewerDALFactory>;
|
||||
|
||||
export const accessApprovalRequestReviewerDALFactory = (db: TDbClient) => {
|
||||
const secretApprovalRequestReviewerOrm = ormify(db, TableName.AccessApprovalRequestReviewer);
|
||||
return secretApprovalRequestReviewerOrm;
|
||||
};
|
@ -0,0 +1,369 @@
|
||||
import slugify from "@sindresorhus/slugify";
|
||||
import ms from "ms";
|
||||
|
||||
import { ProjectMembershipRole } from "@app/db/schemas";
|
||||
import { getConfig } from "@app/lib/config/env";
|
||||
import { BadRequestError, UnauthorizedError } from "@app/lib/errors";
|
||||
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
||||
import { TProjectDALFactory } from "@app/services/project/project-dal";
|
||||
import { TProjectEnvDALFactory } from "@app/services/project-env/project-env-dal";
|
||||
import { TProjectMembershipDALFactory } from "@app/services/project-membership/project-membership-dal";
|
||||
import { SmtpTemplates, TSmtpService } from "@app/services/smtp/smtp-service";
|
||||
import { TUserDALFactory } from "@app/services/user/user-dal";
|
||||
|
||||
import { TAccessApprovalPolicyApproverDALFactory } from "../access-approval-policy/access-approval-policy-approver-dal";
|
||||
import { TAccessApprovalPolicyDALFactory } from "../access-approval-policy/access-approval-policy-dal";
|
||||
import { verifyApprovers } from "../access-approval-policy/access-approval-policy-fns";
|
||||
import { TPermissionServiceFactory } from "../permission/permission-service";
|
||||
import { TProjectUserAdditionalPrivilegeDALFactory } from "../project-user-additional-privilege/project-user-additional-privilege-dal";
|
||||
import { ProjectUserAdditionalPrivilegeTemporaryMode } from "../project-user-additional-privilege/project-user-additional-privilege-types";
|
||||
import { TAccessApprovalRequestDALFactory } from "./access-approval-request-dal";
|
||||
import { verifyRequestedPermissions } from "./access-approval-request-fns";
|
||||
import { TAccessApprovalRequestReviewerDALFactory } from "./access-approval-request-reviewer-dal";
|
||||
import {
|
||||
ApprovalStatus,
|
||||
TCreateAccessApprovalRequestDTO,
|
||||
TGetAccessRequestCountDTO,
|
||||
TListApprovalRequestsDTO,
|
||||
TReviewAccessRequestDTO
|
||||
} from "./access-approval-request-types";
|
||||
|
||||
type TSecretApprovalRequestServiceFactoryDep = {
|
||||
additionalPrivilegeDAL: Pick<TProjectUserAdditionalPrivilegeDALFactory, "create" | "findById">;
|
||||
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
|
||||
accessApprovalPolicyApproverDAL: Pick<TAccessApprovalPolicyApproverDALFactory, "find">;
|
||||
projectEnvDAL: Pick<TProjectEnvDALFactory, "findOne">;
|
||||
projectDAL: Pick<TProjectDALFactory, "checkProjectUpgradeStatus" | "findProjectBySlug">;
|
||||
accessApprovalRequestDAL: Pick<
|
||||
TAccessApprovalRequestDALFactory,
|
||||
| "create"
|
||||
| "find"
|
||||
| "findRequestsWithPrivilegeByPolicyIds"
|
||||
| "findById"
|
||||
| "transaction"
|
||||
| "updateById"
|
||||
| "findOne"
|
||||
| "getCount"
|
||||
>;
|
||||
accessApprovalPolicyDAL: Pick<TAccessApprovalPolicyDALFactory, "findOne" | "find">;
|
||||
accessApprovalRequestReviewerDAL: Pick<
|
||||
TAccessApprovalRequestReviewerDALFactory,
|
||||
"create" | "find" | "findOne" | "transaction"
|
||||
>;
|
||||
projectMembershipDAL: Pick<TProjectMembershipDALFactory, "findById">;
|
||||
smtpService: Pick<TSmtpService, "sendMail">;
|
||||
userDAL: Pick<TUserDALFactory, "findUserByProjectMembershipId" | "findUsersByProjectMembershipIds">;
|
||||
};
|
||||
|
||||
export type TAccessApprovalRequestServiceFactory = ReturnType<typeof accessApprovalRequestServiceFactory>;
|
||||
|
||||
export const accessApprovalRequestServiceFactory = ({
|
||||
projectDAL,
|
||||
projectEnvDAL,
|
||||
permissionService,
|
||||
accessApprovalRequestDAL,
|
||||
accessApprovalRequestReviewerDAL,
|
||||
projectMembershipDAL,
|
||||
accessApprovalPolicyDAL,
|
||||
accessApprovalPolicyApproverDAL,
|
||||
additionalPrivilegeDAL,
|
||||
smtpService,
|
||||
userDAL
|
||||
}: TSecretApprovalRequestServiceFactoryDep) => {
|
||||
const createAccessApprovalRequest = async ({
|
||||
isTemporary,
|
||||
temporaryRange,
|
||||
actorId,
|
||||
permissions: requestedPermissions,
|
||||
actor,
|
||||
actorOrgId,
|
||||
actorAuthMethod,
|
||||
projectSlug
|
||||
}: TCreateAccessApprovalRequestDTO) => {
|
||||
const cfg = getConfig();
|
||||
const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId);
|
||||
if (!project) throw new UnauthorizedError({ message: "Project not found" });
|
||||
|
||||
// Anyone can create an access approval request.
|
||||
const { membership } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
actorId,
|
||||
project.id,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
if (!membership) throw new UnauthorizedError({ message: "You are not a member of this project" });
|
||||
|
||||
const requestedByUser = await userDAL.findUserByProjectMembershipId(membership.id);
|
||||
if (!requestedByUser) throw new UnauthorizedError({ message: "User not found" });
|
||||
|
||||
await projectDAL.checkProjectUpgradeStatus(project.id);
|
||||
|
||||
const { envSlug, secretPath, accessTypes } = verifyRequestedPermissions({ permissions: requestedPermissions });
|
||||
const environment = await projectEnvDAL.findOne({ projectId: project.id, slug: envSlug });
|
||||
|
||||
if (!environment) throw new UnauthorizedError({ message: "Environment not found" });
|
||||
|
||||
const policy = await accessApprovalPolicyDAL.findOne({
|
||||
envId: environment.id,
|
||||
secretPath
|
||||
});
|
||||
if (!policy) throw new UnauthorizedError({ message: "No policy matching criteria was found." });
|
||||
|
||||
const approvers = await accessApprovalPolicyApproverDAL.find({
|
||||
policyId: policy.id
|
||||
});
|
||||
|
||||
const approverUsers = await userDAL.findUsersByProjectMembershipIds(
|
||||
approvers.map((approver) => approver.approverId)
|
||||
);
|
||||
|
||||
const duplicateRequests = await accessApprovalRequestDAL.find({
|
||||
policyId: policy.id,
|
||||
requestedBy: membership.id,
|
||||
permissions: JSON.stringify(requestedPermissions),
|
||||
isTemporary
|
||||
});
|
||||
|
||||
if (duplicateRequests?.length > 0) {
|
||||
for await (const duplicateRequest of duplicateRequests) {
|
||||
if (duplicateRequest.privilegeId) {
|
||||
const privilege = await additionalPrivilegeDAL.findById(duplicateRequest.privilegeId);
|
||||
|
||||
const isExpired = new Date() > new Date(privilege.temporaryAccessEndTime || ("" as string));
|
||||
|
||||
if (!isExpired || !privilege.isTemporary) {
|
||||
throw new BadRequestError({ message: "You already have an active privilege with the same criteria" });
|
||||
}
|
||||
} else {
|
||||
const reviewers = await accessApprovalRequestReviewerDAL.find({
|
||||
requestId: duplicateRequest.id
|
||||
});
|
||||
|
||||
const isRejected = reviewers.some((reviewer) => reviewer.status === ApprovalStatus.REJECTED);
|
||||
|
||||
if (!isRejected) {
|
||||
throw new BadRequestError({ message: "You already have a pending access request with the same criteria" });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const approval = await accessApprovalRequestDAL.transaction(async (tx) => {
|
||||
const approvalRequest = await accessApprovalRequestDAL.create(
|
||||
{
|
||||
policyId: policy.id,
|
||||
requestedBy: membership.id,
|
||||
temporaryRange: temporaryRange || null,
|
||||
permissions: JSON.stringify(requestedPermissions),
|
||||
isTemporary
|
||||
},
|
||||
tx
|
||||
);
|
||||
|
||||
await smtpService.sendMail({
|
||||
recipients: approverUsers.filter((approver) => approver.email).map((approver) => approver.email!),
|
||||
subjectLine: "Access Approval Request",
|
||||
|
||||
substitutions: {
|
||||
projectName: project.name,
|
||||
requesterFullName: `${requestedByUser.firstName} ${requestedByUser.lastName}`,
|
||||
requesterEmail: requestedByUser.email,
|
||||
isTemporary,
|
||||
...(isTemporary && {
|
||||
expiresIn: ms(ms(temporaryRange || ""), { long: true })
|
||||
}),
|
||||
secretPath,
|
||||
environment: envSlug,
|
||||
permissions: accessTypes,
|
||||
approvalUrl: `${cfg.SITE_URL}/project/${project.id}/approval`
|
||||
},
|
||||
template: SmtpTemplates.AccessApprovalRequest
|
||||
});
|
||||
|
||||
return approvalRequest;
|
||||
});
|
||||
|
||||
return { request: approval };
|
||||
};
|
||||
|
||||
const listApprovalRequests = async ({
|
||||
projectSlug,
|
||||
authorProjectMembershipId,
|
||||
envSlug,
|
||||
actor,
|
||||
actorOrgId,
|
||||
actorId,
|
||||
actorAuthMethod
|
||||
}: TListApprovalRequestsDTO) => {
|
||||
const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId);
|
||||
if (!project) throw new UnauthorizedError({ message: "Project not found" });
|
||||
|
||||
const { membership } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
actorId,
|
||||
project.id,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
if (!membership) throw new UnauthorizedError({ message: "You are not a member of this project" });
|
||||
|
||||
const policies = await accessApprovalPolicyDAL.find({ projectId: project.id });
|
||||
let requests = await accessApprovalRequestDAL.findRequestsWithPrivilegeByPolicyIds(policies.map((p) => p.id));
|
||||
|
||||
if (authorProjectMembershipId) {
|
||||
requests = requests.filter((request) => request.requestedBy === authorProjectMembershipId);
|
||||
}
|
||||
|
||||
if (envSlug) {
|
||||
requests = requests.filter((request) => request.environment === envSlug);
|
||||
}
|
||||
|
||||
return { requests };
|
||||
};
|
||||
|
||||
const reviewAccessRequest = async ({
|
||||
requestId,
|
||||
actor,
|
||||
status,
|
||||
actorId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
}: TReviewAccessRequestDTO) => {
|
||||
const accessApprovalRequest = await accessApprovalRequestDAL.findById(requestId);
|
||||
if (!accessApprovalRequest) throw new BadRequestError({ message: "Secret approval request not found" });
|
||||
|
||||
const { policy } = accessApprovalRequest;
|
||||
const { membership, hasRole } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
actorId,
|
||||
accessApprovalRequest.projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
|
||||
if (!membership) throw new UnauthorizedError({ message: "You are not a member of this project" });
|
||||
|
||||
if (
|
||||
!hasRole(ProjectMembershipRole.Admin) &&
|
||||
accessApprovalRequest.requestedBy !== membership.id && // The request wasn't made by the current user
|
||||
!policy.approvers.find((approverId) => approverId === membership.id) // The request isn't performed by an assigned approver
|
||||
) {
|
||||
throw new UnauthorizedError({ message: "You are not authorized to approve this request" });
|
||||
}
|
||||
|
||||
const reviewerProjectMembership = await projectMembershipDAL.findById(membership.id);
|
||||
|
||||
await verifyApprovers({
|
||||
projectId: accessApprovalRequest.projectId,
|
||||
orgId: actorOrgId,
|
||||
envSlug: accessApprovalRequest.environment,
|
||||
secretPath: accessApprovalRequest.policy.secretPath!,
|
||||
actorAuthMethod,
|
||||
permissionService,
|
||||
userIds: [reviewerProjectMembership.userId]
|
||||
});
|
||||
|
||||
const existingReviews = await accessApprovalRequestReviewerDAL.find({ requestId: accessApprovalRequest.id });
|
||||
if (existingReviews.some((review) => review.status === ApprovalStatus.REJECTED)) {
|
||||
throw new BadRequestError({ message: "The request has already been rejected by another reviewer" });
|
||||
}
|
||||
|
||||
const reviewStatus = await accessApprovalRequestReviewerDAL.transaction(async (tx) => {
|
||||
const review = await accessApprovalRequestReviewerDAL.findOne(
|
||||
{
|
||||
requestId: accessApprovalRequest.id,
|
||||
member: membership.id
|
||||
},
|
||||
tx
|
||||
);
|
||||
if (!review) {
|
||||
const newReview = await accessApprovalRequestReviewerDAL.create(
|
||||
{
|
||||
status,
|
||||
requestId: accessApprovalRequest.id,
|
||||
member: membership.id
|
||||
},
|
||||
tx
|
||||
);
|
||||
|
||||
const allReviews = [...existingReviews, newReview];
|
||||
|
||||
const approvedReviews = allReviews.filter((r) => r.status === ApprovalStatus.APPROVED);
|
||||
|
||||
// approvals is the required number of approvals. If the number of approved reviews is equal to the number of required approvals, then the request is approved.
|
||||
if (approvedReviews.length === policy.approvals) {
|
||||
if (accessApprovalRequest.isTemporary && !accessApprovalRequest.temporaryRange) {
|
||||
throw new BadRequestError({ message: "Temporary range is required for temporary access" });
|
||||
}
|
||||
|
||||
let privilegeId: string | null = null;
|
||||
|
||||
if (!accessApprovalRequest.isTemporary && !accessApprovalRequest.temporaryRange) {
|
||||
// Permanent access
|
||||
const privilege = await additionalPrivilegeDAL.create(
|
||||
{
|
||||
projectMembershipId: accessApprovalRequest.requestedBy,
|
||||
slug: `requested-privilege-${slugify(alphaNumericNanoId(12))}`,
|
||||
permissions: JSON.stringify(accessApprovalRequest.permissions)
|
||||
},
|
||||
tx
|
||||
);
|
||||
privilegeId = privilege.id;
|
||||
} else {
|
||||
// Temporary access
|
||||
const relativeTempAllocatedTimeInMs = ms(accessApprovalRequest.temporaryRange!);
|
||||
const startTime = new Date();
|
||||
|
||||
const privilege = await additionalPrivilegeDAL.create(
|
||||
{
|
||||
projectMembershipId: accessApprovalRequest.requestedBy,
|
||||
slug: `requested-privilege-${slugify(alphaNumericNanoId(12))}`,
|
||||
permissions: JSON.stringify(accessApprovalRequest.permissions),
|
||||
isTemporary: true,
|
||||
temporaryMode: ProjectUserAdditionalPrivilegeTemporaryMode.Relative,
|
||||
temporaryRange: accessApprovalRequest.temporaryRange!,
|
||||
temporaryAccessStartTime: startTime,
|
||||
temporaryAccessEndTime: new Date(new Date(startTime).getTime() + relativeTempAllocatedTimeInMs)
|
||||
},
|
||||
tx
|
||||
);
|
||||
privilegeId = privilege.id;
|
||||
}
|
||||
|
||||
await accessApprovalRequestDAL.updateById(accessApprovalRequest.id, { privilegeId }, tx);
|
||||
}
|
||||
|
||||
return newReview;
|
||||
}
|
||||
throw new BadRequestError({ message: "You have already reviewed this request" });
|
||||
});
|
||||
|
||||
return reviewStatus;
|
||||
};
|
||||
|
||||
const getCount = async ({ projectSlug, actor, actorAuthMethod, actorId, actorOrgId }: TGetAccessRequestCountDTO) => {
|
||||
const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId);
|
||||
if (!project) throw new UnauthorizedError({ message: "Project not found" });
|
||||
|
||||
const { membership } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
actorId,
|
||||
project.id,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
if (!membership) throw new BadRequestError({ message: "User not found in project" });
|
||||
|
||||
const count = await accessApprovalRequestDAL.getCount({ projectId: project.id });
|
||||
|
||||
return { count };
|
||||
};
|
||||
|
||||
return {
|
||||
createAccessApprovalRequest,
|
||||
listApprovalRequests,
|
||||
reviewAccessRequest,
|
||||
getCount
|
||||
};
|
||||
};
|
@ -0,0 +1,33 @@
|
||||
import { TProjectPermission } from "@app/lib/types";
|
||||
|
||||
export enum ApprovalStatus {
|
||||
PENDING = "pending",
|
||||
APPROVED = "approved",
|
||||
REJECTED = "rejected"
|
||||
}
|
||||
|
||||
export type TVerifyPermission = {
|
||||
permissions: unknown;
|
||||
};
|
||||
|
||||
export type TGetAccessRequestCountDTO = {
|
||||
projectSlug: string;
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
export type TReviewAccessRequestDTO = {
|
||||
requestId: string;
|
||||
status: ApprovalStatus;
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
export type TCreateAccessApprovalRequestDTO = {
|
||||
projectSlug: string;
|
||||
permissions: unknown;
|
||||
isTemporary: boolean;
|
||||
temporaryRange?: string;
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
export type TListApprovalRequestsDTO = {
|
||||
projectSlug: string;
|
||||
authorProjectMembershipId?: string;
|
||||
envSlug?: string;
|
||||
} & Omit<TProjectPermission, "projectId">;
|
@ -66,6 +66,10 @@ export enum EventType {
|
||||
CREATE_IDENTITY_UNIVERSAL_AUTH_CLIENT_SECRET = "create-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",
|
||||
LOGIN_IDENTITY_AWS_AUTH = "login-identity-aws-auth",
|
||||
ADD_IDENTITY_AWS_AUTH = "add-identity-aws-auth",
|
||||
UPDATE_IDENTITY_AWS_AUTH = "update-identity-aws-auth",
|
||||
GET_IDENTITY_AWS_AUTH = "get-identity-aws-auth",
|
||||
CREATE_ENVIRONMENT = "create-environment",
|
||||
UPDATE_ENVIRONMENT = "update-environment",
|
||||
DELETE_ENVIRONMENT = "delete-environment",
|
||||
@ -406,6 +410,50 @@ interface RevokeIdentityUniversalAuthClientSecretEvent {
|
||||
};
|
||||
}
|
||||
|
||||
interface LoginIdentityAwsAuthEvent {
|
||||
type: EventType.LOGIN_IDENTITY_AWS_AUTH;
|
||||
metadata: {
|
||||
identityId: string;
|
||||
identityAwsAuthId: string;
|
||||
identityAccessTokenId: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface AddIdentityAwsAuthEvent {
|
||||
type: EventType.ADD_IDENTITY_AWS_AUTH;
|
||||
metadata: {
|
||||
identityId: string;
|
||||
stsEndpoint: string;
|
||||
allowedPrincipalArns: string;
|
||||
allowedAccountIds: string;
|
||||
accessTokenTTL: number;
|
||||
accessTokenMaxTTL: number;
|
||||
accessTokenNumUsesLimit: number;
|
||||
accessTokenTrustedIps: Array<TIdentityTrustedIp>;
|
||||
};
|
||||
}
|
||||
|
||||
interface UpdateIdentityAwsAuthEvent {
|
||||
type: EventType.UPDATE_IDENTITY_AWS_AUTH;
|
||||
metadata: {
|
||||
identityId: string;
|
||||
stsEndpoint?: string;
|
||||
allowedPrincipalArns?: string;
|
||||
allowedAccountIds?: string;
|
||||
accessTokenTTL?: number;
|
||||
accessTokenMaxTTL?: number;
|
||||
accessTokenNumUsesLimit?: number;
|
||||
accessTokenTrustedIps?: Array<TIdentityTrustedIp>;
|
||||
};
|
||||
}
|
||||
|
||||
interface GetIdentityAwsAuthEvent {
|
||||
type: EventType.GET_IDENTITY_AWS_AUTH;
|
||||
metadata: {
|
||||
identityId: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface CreateEnvironmentEvent {
|
||||
type: EventType.CREATE_ENVIRONMENT;
|
||||
metadata: {
|
||||
@ -660,6 +708,10 @@ export type Event =
|
||||
| CreateIdentityUniversalAuthClientSecretEvent
|
||||
| GetIdentityUniversalAuthClientSecretsEvent
|
||||
| RevokeIdentityUniversalAuthClientSecretEvent
|
||||
| LoginIdentityAwsAuthEvent
|
||||
| AddIdentityAwsAuthEvent
|
||||
| UpdateIdentityAwsAuthEvent
|
||||
| GetIdentityAwsAuthEvent
|
||||
| CreateEnvironmentEvent
|
||||
| UpdateEnvironmentEvent
|
||||
| DeleteEnvironmentEvent
|
||||
|
@ -1,5 +1,7 @@
|
||||
import { ForbiddenError } from "@casl/ability";
|
||||
import { ForbiddenError, MongoAbility, RawRuleOf } from "@casl/ability";
|
||||
import { PackRule, unpackRules } from "@casl/ability/extra";
|
||||
import ms from "ms";
|
||||
import { z } from "zod";
|
||||
|
||||
import { isAtLeastAsPrivileged } from "@app/lib/casl";
|
||||
import { BadRequestError, ForbiddenRequestError } from "@app/lib/errors";
|
||||
@ -8,7 +10,7 @@ import { TIdentityProjectDALFactory } from "@app/services/identity-project/ident
|
||||
import { TProjectDALFactory } from "@app/services/project/project-dal";
|
||||
|
||||
import { TPermissionServiceFactory } from "../permission/permission-service";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub } from "../permission/project-permission";
|
||||
import { ProjectPermissionActions, ProjectPermissionSet, ProjectPermissionSub } from "../permission/project-permission";
|
||||
import { TIdentityProjectAdditionalPrivilegeDALFactory } from "./identity-project-additional-privilege-dal";
|
||||
import {
|
||||
IdentityProjectAdditionalPrivilegeTemporaryMode,
|
||||
@ -30,6 +32,27 @@ export type TIdentityProjectAdditionalPrivilegeServiceFactory = ReturnType<
|
||||
typeof identityProjectAdditionalPrivilegeServiceFactory
|
||||
>;
|
||||
|
||||
// TODO(akhilmhdh): move this to more centralized
|
||||
export const UnpackedPermissionSchema = z.object({
|
||||
subject: z.union([z.string().min(1), z.string().array()]).optional(),
|
||||
action: z.union([z.string().min(1), z.string().array()]),
|
||||
conditions: z
|
||||
.object({
|
||||
environment: z.string().optional(),
|
||||
secretPath: z
|
||||
.object({
|
||||
$glob: z.string().min(1)
|
||||
})
|
||||
.optional()
|
||||
})
|
||||
.optional()
|
||||
});
|
||||
|
||||
const unpackPermissions = (permissions: unknown) =>
|
||||
UnpackedPermissionSchema.array().parse(
|
||||
unpackRules((permissions || []) as PackRule<RawRuleOf<MongoAbility<ProjectPermissionSet>>>[])
|
||||
);
|
||||
|
||||
export const identityProjectAdditionalPrivilegeServiceFactory = ({
|
||||
identityProjectAdditionalPrivilegeDAL,
|
||||
identityProjectDAL,
|
||||
@ -86,7 +109,10 @@ export const identityProjectAdditionalPrivilegeServiceFactory = ({
|
||||
slug,
|
||||
permissions: customPermission
|
||||
});
|
||||
return additionalPrivilege;
|
||||
return {
|
||||
...additionalPrivilege,
|
||||
permissions: unpackPermissions(additionalPrivilege.permissions)
|
||||
};
|
||||
}
|
||||
|
||||
const relativeTempAllocatedTimeInMs = ms(dto.temporaryRange);
|
||||
@ -100,7 +126,10 @@ export const identityProjectAdditionalPrivilegeServiceFactory = ({
|
||||
temporaryAccessStartTime: new Date(dto.temporaryAccessStartTime),
|
||||
temporaryAccessEndTime: new Date(new Date(dto.temporaryAccessStartTime).getTime() + relativeTempAllocatedTimeInMs)
|
||||
});
|
||||
return additionalPrivilege;
|
||||
return {
|
||||
...additionalPrivilege,
|
||||
permissions: unpackPermissions(additionalPrivilege.permissions)
|
||||
};
|
||||
};
|
||||
|
||||
const updateBySlug = async ({
|
||||
@ -163,7 +192,11 @@ export const identityProjectAdditionalPrivilegeServiceFactory = ({
|
||||
temporaryAccessStartTime: new Date(temporaryAccessStartTime || ""),
|
||||
temporaryAccessEndTime: new Date(new Date(temporaryAccessStartTime || "").getTime() + ms(temporaryRange || ""))
|
||||
});
|
||||
return additionalPrivilege;
|
||||
return {
|
||||
...additionalPrivilege,
|
||||
|
||||
permissions: unpackPermissions(additionalPrivilege.permissions)
|
||||
};
|
||||
}
|
||||
|
||||
const additionalPrivilege = await identityProjectAdditionalPrivilegeDAL.updateById(identityPrivilege.id, {
|
||||
@ -174,7 +207,11 @@ export const identityProjectAdditionalPrivilegeServiceFactory = ({
|
||||
temporaryRange: null,
|
||||
temporaryMode: null
|
||||
});
|
||||
return additionalPrivilege;
|
||||
return {
|
||||
...additionalPrivilege,
|
||||
|
||||
permissions: unpackPermissions(additionalPrivilege.permissions)
|
||||
};
|
||||
};
|
||||
|
||||
const deleteBySlug = async ({
|
||||
@ -220,7 +257,11 @@ export const identityProjectAdditionalPrivilegeServiceFactory = ({
|
||||
if (!identityPrivilege) throw new BadRequestError({ message: "Identity additional privilege not found" });
|
||||
|
||||
const deletedPrivilege = await identityProjectAdditionalPrivilegeDAL.deleteById(identityPrivilege.id);
|
||||
return deletedPrivilege;
|
||||
return {
|
||||
...deletedPrivilege,
|
||||
|
||||
permissions: unpackPermissions(deletedPrivilege.permissions)
|
||||
};
|
||||
};
|
||||
|
||||
const getPrivilegeDetailsBySlug = async ({
|
||||
@ -254,7 +295,10 @@ export const identityProjectAdditionalPrivilegeServiceFactory = ({
|
||||
});
|
||||
if (!identityPrivilege) throw new BadRequestError({ message: "Identity additional privilege not found" });
|
||||
|
||||
return identityPrivilege;
|
||||
return {
|
||||
...identityPrivilege,
|
||||
permissions: unpackPermissions(identityPrivilege.permissions)
|
||||
};
|
||||
};
|
||||
|
||||
const listIdentityProjectPrivileges = async ({
|
||||
@ -284,7 +328,11 @@ export const identityProjectAdditionalPrivilegeServiceFactory = ({
|
||||
const identityPrivileges = await identityProjectAdditionalPrivilegeDAL.find({
|
||||
projectMembershipId: identityProjectMembership.id
|
||||
});
|
||||
return identityPrivileges;
|
||||
return identityPrivileges.map((el) => ({
|
||||
...el,
|
||||
|
||||
permissions: unpackPermissions(el.permissions)
|
||||
}));
|
||||
};
|
||||
|
||||
return {
|
||||
|
@ -121,8 +121,8 @@ export const licenseServiceFactory = ({
|
||||
|
||||
if (isValidOfflineLicense) {
|
||||
onPremFeatures = contents.license.features;
|
||||
instanceType = InstanceType.EnterpriseOnPrem;
|
||||
logger.info(`Instance type: ${InstanceType.EnterpriseOnPrem}`);
|
||||
instanceType = InstanceType.EnterpriseOnPremOffline;
|
||||
logger.info(`Instance type: ${InstanceType.EnterpriseOnPremOffline}`);
|
||||
isValidLicense = true;
|
||||
return;
|
||||
}
|
||||
|
@ -3,6 +3,7 @@ import { TOrgPermission } from "@app/lib/types";
|
||||
export enum InstanceType {
|
||||
OnPrem = "self-hosted",
|
||||
EnterpriseOnPrem = "enterprise-self-hosted",
|
||||
EnterpriseOnPremOffline = "enterprise-self-hosted-offline",
|
||||
Cloud = "cloud"
|
||||
}
|
||||
|
||||
|
@ -92,6 +92,18 @@ export const UNIVERSAL_AUTH = {
|
||||
}
|
||||
} as const;
|
||||
|
||||
export const AWS_AUTH = {
|
||||
LOGIN: {
|
||||
identityId: "The ID of the identity to login.",
|
||||
iamHttpRequestMethod: "The HTTP request method used in the signed request.",
|
||||
iamRequestUrl:
|
||||
"The base64-encoded HTTP URL used in the signed request. Most likely, the base64-encoding of https://sts.amazonaws.com/",
|
||||
iamRequestBody:
|
||||
"The base64-encoded body of the signed request. Most likely, the base64-encoding of Action=GetCallerIdentity&Version=2011-06-15.",
|
||||
iamRequestHeaders: "The base64-encoded headers of the sts:GetCallerIdentity signed request."
|
||||
}
|
||||
} as const;
|
||||
|
||||
export const ORGANIZATIONS = {
|
||||
LIST_USER_MEMBERSHIPS: {
|
||||
organizationId: "The ID of the organization to get memberships from."
|
||||
@ -276,7 +288,8 @@ export const RAW_SECRETS = {
|
||||
recursive:
|
||||
"Whether or not to fetch all secrets from the specified base path, and all of its subdirectories. Note, the max depth is 20 deep.",
|
||||
workspaceId: "The ID of the project to list secrets from.",
|
||||
workspaceSlug: "The slug of the project to list secrets from. This parameter is only usable by machine identities.",
|
||||
workspaceSlug:
|
||||
"The slug of the project to list secrets from. This parameter is only applicable by machine identities.",
|
||||
environment: "The slug of the environment to list secrets from.",
|
||||
secretPath: "The secret path to list secrets from.",
|
||||
includeImports: "Weather to include imported secrets or not."
|
||||
@ -295,6 +308,7 @@ export const RAW_SECRETS = {
|
||||
GET: {
|
||||
secretName: "The name of the secret to get.",
|
||||
workspaceId: "The ID of the project to get the secret from.",
|
||||
workspaceSlug: "The slug of the project to get the secret from.",
|
||||
environment: "The slug of the environment to get the secret from.",
|
||||
secretPath: "The path of the secret to get.",
|
||||
version: "The version of the secret to get.",
|
||||
@ -465,12 +479,21 @@ export const SECRET_TAGS = {
|
||||
export const IDENTITY_ADDITIONAL_PRIVILEGE = {
|
||||
CREATE: {
|
||||
projectSlug: "The slug of the project of the identity in.",
|
||||
identityId: "The ID of the identity to delete.",
|
||||
identityId: "The ID of the identity to create.",
|
||||
slug: "The slug of the privilege to create.",
|
||||
permissions: `The permission object for the privilege.
|
||||
1. [["read", "secrets", {environment: "dev", secretPath: {$glob: "/"}}]]
|
||||
2. [["read", "secrets", {environment: "dev"}], ["create", "secrets", {environment: "dev"}]]
|
||||
2. [["read", "secrets", {environment: "dev"}]]
|
||||
- Read secrets
|
||||
\`\`\`
|
||||
{ "permissions": [{"action": "read", "subject": "secrets"]}
|
||||
\`\`\`
|
||||
- Read and Write secrets
|
||||
\`\`\`
|
||||
{ "permissions": [{"action": "read", "subject": "secrets"], {"action": "write", "subject": "secrets"]}
|
||||
\`\`\`
|
||||
- Read secrets scoped to an environment and secret path
|
||||
\`\`\`
|
||||
- { "permissions": [{"action": "read", "subject": "secrets", "conditions": { "environment": "dev", "secretPath": { "$glob": "/" } }}] }
|
||||
\`\`\`
|
||||
`,
|
||||
isPackPermission: "Whether the server should pack(compact) the permission object.",
|
||||
isTemporary: "Whether the privilege is temporary.",
|
||||
@ -484,11 +507,19 @@ export const IDENTITY_ADDITIONAL_PRIVILEGE = {
|
||||
slug: "The slug of the privilege to update.",
|
||||
newSlug: "The new slug of the privilege to update.",
|
||||
permissions: `The permission object for the privilege.
|
||||
1. [["read", "secrets", {environment: "dev", secretPath: {$glob: "/"}}]]
|
||||
2. [["read", "secrets", {environment: "dev"}], ["create", "secrets", {environment: "dev"}]]
|
||||
2. [["read", "secrets", {environment: "dev"}]]
|
||||
- Read secrets
|
||||
\`\`\`
|
||||
{ "permissions": [{"action": "read", "subject": "secrets"]}
|
||||
\`\`\`
|
||||
- Read and Write secrets
|
||||
\`\`\`
|
||||
{ "permissions": [{"action": "read", "subject": "secrets"], {"action": "write", "subject": "secrets"]}
|
||||
\`\`\`
|
||||
- Read secrets scoped to an environment and secret path
|
||||
\`\`\`
|
||||
- { "permissions": [{"action": "read", "subject": "secrets", "conditions": { "environment": "dev", "secretPath": { "$glob": "/" } }}] }
|
||||
\`\`\`
|
||||
`,
|
||||
isPackPermission: "Whether the server should pack(compact) the permission object.",
|
||||
isTemporary: "Whether the privilege is temporary.",
|
||||
temporaryMode: "Type of temporary access given. Types: relative",
|
||||
temporaryRange: "TTL for the temporay time. Eg: 1m, 1h, 1d",
|
||||
@ -596,7 +627,8 @@ export const INTEGRATION = {
|
||||
shouldAutoRedeploy: "Used by Render to trigger auto deploy.",
|
||||
secretGCPLabel: "The label for GCP secrets.",
|
||||
secretAWSTag: "The tags for AWS secrets.",
|
||||
kmsKeyId: "The ID of the encryption key from AWS KMS."
|
||||
kmsKeyId: "The ID of the encryption key from AWS KMS.",
|
||||
shouldDisableDelete: "The flag to disable deletion of secrets in AWS Parameter Store."
|
||||
}
|
||||
},
|
||||
UPDATE: {
|
||||
|
@ -108,6 +108,7 @@ export const injectIdentity = fp(async (server: FastifyZodProvider) => {
|
||||
if (req.url.includes("/api/v3/auth/")) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!authMode) return;
|
||||
|
||||
switch (authMode) {
|
||||
|
@ -2,6 +2,12 @@ import { Knex } from "knex";
|
||||
import { z } from "zod";
|
||||
|
||||
import { registerV1EERoutes } from "@app/ee/routes/v1";
|
||||
import { accessApprovalPolicyApproverDALFactory } from "@app/ee/services/access-approval-policy/access-approval-policy-approver-dal";
|
||||
import { accessApprovalPolicyDALFactory } from "@app/ee/services/access-approval-policy/access-approval-policy-dal";
|
||||
import { accessApprovalPolicyServiceFactory } from "@app/ee/services/access-approval-policy/access-approval-policy-service";
|
||||
import { accessApprovalRequestDALFactory } from "@app/ee/services/access-approval-request/access-approval-request-dal";
|
||||
import { accessApprovalRequestReviewerDALFactory } from "@app/ee/services/access-approval-request/access-approval-request-reviewer-dal";
|
||||
import { accessApprovalRequestServiceFactory } from "@app/ee/services/access-approval-request/access-approval-request-service";
|
||||
import { auditLogDALFactory } from "@app/ee/services/audit-log/audit-log-dal";
|
||||
import { auditLogQueueServiceFactory } from "@app/ee/services/audit-log/audit-log-queue";
|
||||
import { auditLogServiceFactory } from "@app/ee/services/audit-log/audit-log-service";
|
||||
@ -72,6 +78,8 @@ import { identityOrgDALFactory } from "@app/services/identity/identity-org-dal";
|
||||
import { identityServiceFactory } from "@app/services/identity/identity-service";
|
||||
import { identityAccessTokenDALFactory } from "@app/services/identity-access-token/identity-access-token-dal";
|
||||
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 { identityAwsAuthServiceFactory } from "@app/services/identity-aws-auth/identity-aws-auth-service";
|
||||
import { identityProjectDALFactory } from "@app/services/identity-project/identity-project-dal";
|
||||
import { identityProjectMembershipRoleDALFactory } from "@app/services/identity-project/identity-project-membership-role-dal";
|
||||
import { identityProjectServiceFactory } from "@app/services/identity-project/identity-project-service";
|
||||
@ -195,6 +203,7 @@ export const registerRoutes = async (
|
||||
|
||||
const identityUaDAL = identityUaDALFactory(db);
|
||||
const identityUaClientSecretDAL = identityUaClientSecretDALFactory(db);
|
||||
const identityAwsAuthDAL = identityAwsAuthDALFactory(db);
|
||||
|
||||
const auditLogDAL = auditLogDALFactory(db);
|
||||
const auditLogStreamDAL = auditLogStreamDALFactory(db);
|
||||
@ -207,6 +216,12 @@ export const registerRoutes = async (
|
||||
const scimDAL = scimDALFactory(db);
|
||||
const ldapConfigDAL = ldapConfigDALFactory(db);
|
||||
const ldapGroupMapDAL = ldapGroupMapDALFactory(db);
|
||||
|
||||
const accessApprovalPolicyDAL = accessApprovalPolicyDALFactory(db);
|
||||
const accessApprovalRequestDAL = accessApprovalRequestDALFactory(db);
|
||||
const accessApprovalPolicyApproverDAL = accessApprovalPolicyApproverDALFactory(db);
|
||||
const accessApprovalRequestReviewerDAL = accessApprovalRequestReviewerDALFactory(db);
|
||||
|
||||
const sapApproverDAL = secretApprovalPolicyApproverDALFactory(db);
|
||||
const secretApprovalPolicyDAL = secretApprovalPolicyDALFactory(db);
|
||||
const secretApprovalRequestDAL = secretApprovalRequestDALFactory(db);
|
||||
@ -265,6 +280,7 @@ export const registerRoutes = async (
|
||||
secretApprovalPolicyDAL
|
||||
});
|
||||
const tokenService = tokenServiceFactory({ tokenDAL: authTokenDAL, userDAL });
|
||||
|
||||
const samlService = samlConfigServiceFactory({
|
||||
permissionService,
|
||||
orgBotDAL,
|
||||
@ -596,6 +612,30 @@ export const registerRoutes = async (
|
||||
secretVersionTagDAL,
|
||||
secretQueueService
|
||||
});
|
||||
|
||||
const accessApprovalPolicyService = accessApprovalPolicyServiceFactory({
|
||||
accessApprovalPolicyDAL,
|
||||
accessApprovalPolicyApproverDAL,
|
||||
permissionService,
|
||||
projectEnvDAL,
|
||||
projectMembershipDAL,
|
||||
projectDAL
|
||||
});
|
||||
|
||||
const accessApprovalRequestService = accessApprovalRequestServiceFactory({
|
||||
projectDAL,
|
||||
permissionService,
|
||||
accessApprovalRequestReviewerDAL,
|
||||
additionalPrivilegeDAL: projectUserAdditionalPrivilegeDAL,
|
||||
projectMembershipDAL,
|
||||
accessApprovalPolicyDAL,
|
||||
accessApprovalRequestDAL,
|
||||
projectEnvDAL,
|
||||
userDAL,
|
||||
smtpService,
|
||||
accessApprovalPolicyApproverDAL
|
||||
});
|
||||
|
||||
const secretRotationQueue = secretRotationQueueFactory({
|
||||
telemetryService,
|
||||
secretRotationDAL,
|
||||
@ -662,6 +702,14 @@ export const registerRoutes = async (
|
||||
identityUaDAL,
|
||||
licenseService
|
||||
});
|
||||
const identityAwsAuthService = identityAwsAuthServiceFactory({
|
||||
identityAccessTokenDAL,
|
||||
identityAwsAuthDAL,
|
||||
identityOrgMembershipDAL,
|
||||
identityDAL,
|
||||
licenseService,
|
||||
permissionService
|
||||
});
|
||||
|
||||
const dynamicSecretProviders = buildDynamicSecretProviders();
|
||||
const dynamicSecretQueueService = dynamicSecretLeaseQueueServiceFactory({
|
||||
@ -731,7 +779,10 @@ export const registerRoutes = async (
|
||||
identityAccessToken: identityAccessTokenService,
|
||||
identityProject: identityProjectService,
|
||||
identityUa: identityUaService,
|
||||
identityAwsAuth: identityAwsAuthService,
|
||||
secretApprovalPolicy: sapService,
|
||||
accessApprovalPolicy: accessApprovalPolicyService,
|
||||
accessApprovalRequest: accessApprovalRequestService,
|
||||
secretApprovalRequest: sarService,
|
||||
secretRotation: secretRotationService,
|
||||
dynamicSecret: dynamicSecretService,
|
||||
|
@ -2,10 +2,12 @@ import { z } from "zod";
|
||||
|
||||
import {
|
||||
DynamicSecretsSchema,
|
||||
IdentityProjectAdditionalPrivilegeSchema,
|
||||
IntegrationAuthsSchema,
|
||||
SecretApprovalPoliciesSchema,
|
||||
UsersSchema
|
||||
} from "@app/db/schemas";
|
||||
import { UnpackedPermissionSchema } from "@app/ee/services/identity-project-additional-privilege/identity-project-additional-privilege-service";
|
||||
|
||||
// sometimes the return data must be santizied to avoid leaking important values
|
||||
// always prefer pick over omit in zod
|
||||
@ -62,6 +64,35 @@ export const secretRawSchema = z.object({
|
||||
secretComment: z.string().optional()
|
||||
});
|
||||
|
||||
export const PermissionSchema = z.object({
|
||||
action: z
|
||||
.string()
|
||||
.min(1)
|
||||
.describe("Describe what action an entity can take. Possible actions: create, edit, delete, and read"),
|
||||
subject: z
|
||||
.string()
|
||||
.min(1)
|
||||
.describe("The entity this permission pertains to. Possible options: secrets, environments"),
|
||||
conditions: z
|
||||
.object({
|
||||
environment: z.string().describe("The environment slug this permission should allow.").optional(),
|
||||
secretPath: z
|
||||
.object({
|
||||
$glob: z
|
||||
.string()
|
||||
.min(1)
|
||||
.describe("The secret path this permission should allow. Can be a glob pattern such as /folder-name/*/** ")
|
||||
})
|
||||
.optional()
|
||||
})
|
||||
.describe("When specified, only matching conditions will be allowed to access given resource.")
|
||||
.optional()
|
||||
});
|
||||
|
||||
export const SanitizedIdentityPrivilegeSchema = IdentityProjectAdditionalPrivilegeSchema.extend({
|
||||
permissions: UnpackedPermissionSchema.array()
|
||||
});
|
||||
|
||||
export const SanitizedDynamicSecretSchema = DynamicSecretsSchema.omit({
|
||||
inputIV: true,
|
||||
inputTag: true,
|
||||
|
269
backend/src/server/routes/v1/identity-aws-iam-auth-router.ts
Normal file
269
backend/src/server/routes/v1/identity-aws-iam-auth-router.ts
Normal file
@ -0,0 +1,269 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { IdentityAwsAuthsSchema } from "@app/db/schemas";
|
||||
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
||||
import { AWS_AUTH } from "@app/lib/api-docs";
|
||||
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||
import { AuthMode } from "@app/services/auth/auth-type";
|
||||
import { TIdentityTrustedIp } from "@app/services/identity/identity-types";
|
||||
import {
|
||||
validateAccountIds,
|
||||
validatePrincipalArns
|
||||
} from "@app/services/identity-aws-auth/identity-aws-auth-validators";
|
||||
|
||||
export const registerIdentityAwsAuthRouter = async (server: FastifyZodProvider) => {
|
||||
server.route({
|
||||
method: "POST",
|
||||
url: "/aws-auth/login",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
schema: {
|
||||
description: "Login with AWS Auth",
|
||||
body: z.object({
|
||||
identityId: z.string().describe(AWS_AUTH.LOGIN.identityId),
|
||||
iamHttpRequestMethod: z.string().default("POST").describe(AWS_AUTH.LOGIN.iamHttpRequestMethod),
|
||||
iamRequestBody: z.string().describe(AWS_AUTH.LOGIN.iamRequestBody),
|
||||
iamRequestHeaders: z.string().describe(AWS_AUTH.LOGIN.iamRequestHeaders)
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
accessToken: z.string(),
|
||||
expiresIn: z.coerce.number(),
|
||||
accessTokenMaxTTL: z.coerce.number(),
|
||||
tokenType: z.literal("Bearer")
|
||||
})
|
||||
}
|
||||
},
|
||||
handler: async (req) => {
|
||||
const { identityAwsAuth, accessToken, identityAccessToken, identityMembershipOrg } =
|
||||
await server.services.identityAwsAuth.login(req.body);
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
orgId: identityMembershipOrg?.orgId,
|
||||
event: {
|
||||
type: EventType.LOGIN_IDENTITY_AWS_AUTH,
|
||||
metadata: {
|
||||
identityId: identityAwsAuth.identityId,
|
||||
identityAccessTokenId: identityAccessToken.id,
|
||||
identityAwsAuthId: identityAwsAuth.id
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
accessToken,
|
||||
tokenType: "Bearer" as const,
|
||||
expiresIn: identityAwsAuth.accessTokenTTL,
|
||||
accessTokenMaxTTL: identityAwsAuth.accessTokenMaxTTL
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "POST",
|
||||
url: "/aws-auth/identities/:identityId",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
schema: {
|
||||
description: "Attach AWS Auth configuration onto identity",
|
||||
security: [
|
||||
{
|
||||
bearerAuth: []
|
||||
}
|
||||
],
|
||||
params: z.object({
|
||||
identityId: z.string().trim()
|
||||
}),
|
||||
body: z.object({
|
||||
stsEndpoint: z.string().trim().min(1).default("https://sts.amazonaws.com/"),
|
||||
allowedPrincipalArns: validatePrincipalArns,
|
||||
allowedAccountIds: validateAccountIds,
|
||||
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({
|
||||
identityAwsAuth: IdentityAwsAuthsSchema
|
||||
})
|
||||
}
|
||||
},
|
||||
handler: async (req) => {
|
||||
const identityAwsAuth = await server.services.identityAwsAuth.attachAwsAuth({
|
||||
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: identityAwsAuth.orgId,
|
||||
event: {
|
||||
type: EventType.ADD_IDENTITY_AWS_AUTH,
|
||||
metadata: {
|
||||
identityId: identityAwsAuth.identityId,
|
||||
stsEndpoint: identityAwsAuth.stsEndpoint,
|
||||
allowedPrincipalArns: identityAwsAuth.allowedPrincipalArns,
|
||||
allowedAccountIds: identityAwsAuth.allowedAccountIds,
|
||||
accessTokenTTL: identityAwsAuth.accessTokenTTL,
|
||||
accessTokenMaxTTL: identityAwsAuth.accessTokenMaxTTL,
|
||||
accessTokenTrustedIps: identityAwsAuth.accessTokenTrustedIps as TIdentityTrustedIp[],
|
||||
accessTokenNumUsesLimit: identityAwsAuth.accessTokenNumUsesLimit
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return { identityAwsAuth };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "PATCH",
|
||||
url: "/aws-auth/identities/:identityId",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
schema: {
|
||||
description: "Update AWS Auth configuration on identity",
|
||||
security: [
|
||||
{
|
||||
bearerAuth: []
|
||||
}
|
||||
],
|
||||
params: z.object({
|
||||
identityId: z.string()
|
||||
}),
|
||||
body: z.object({
|
||||
stsEndpoint: z.string().trim().min(1).optional(),
|
||||
allowedPrincipalArns: validatePrincipalArns,
|
||||
allowedAccountIds: validateAccountIds,
|
||||
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({
|
||||
identityAwsAuth: IdentityAwsAuthsSchema
|
||||
})
|
||||
}
|
||||
},
|
||||
handler: async (req) => {
|
||||
const identityAwsAuth = await server.services.identityAwsAuth.updateAwsAuth({
|
||||
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: identityAwsAuth.orgId,
|
||||
event: {
|
||||
type: EventType.UPDATE_IDENTITY_AWS_AUTH,
|
||||
metadata: {
|
||||
identityId: identityAwsAuth.identityId,
|
||||
stsEndpoint: identityAwsAuth.stsEndpoint,
|
||||
allowedPrincipalArns: identityAwsAuth.allowedPrincipalArns,
|
||||
allowedAccountIds: identityAwsAuth.allowedAccountIds,
|
||||
accessTokenTTL: identityAwsAuth.accessTokenTTL,
|
||||
accessTokenMaxTTL: identityAwsAuth.accessTokenMaxTTL,
|
||||
accessTokenTrustedIps: identityAwsAuth.accessTokenTrustedIps as TIdentityTrustedIp[],
|
||||
accessTokenNumUsesLimit: identityAwsAuth.accessTokenNumUsesLimit
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return { identityAwsAuth };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: "/aws-auth/identities/:identityId",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
schema: {
|
||||
description: "Retrieve AWS Auth configuration on identity",
|
||||
security: [
|
||||
{
|
||||
bearerAuth: []
|
||||
}
|
||||
],
|
||||
params: z.object({
|
||||
identityId: z.string()
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
identityAwsAuth: IdentityAwsAuthsSchema
|
||||
})
|
||||
}
|
||||
},
|
||||
handler: async (req) => {
|
||||
const identityAwsAuth = await server.services.identityAwsAuth.getAwsAuth({
|
||||
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: identityAwsAuth.orgId,
|
||||
event: {
|
||||
type: EventType.GET_IDENTITY_AWS_AUTH,
|
||||
metadata: {
|
||||
identityId: identityAwsAuth.identityId
|
||||
}
|
||||
}
|
||||
});
|
||||
return { identityAwsAuth };
|
||||
}
|
||||
});
|
||||
};
|
@ -2,6 +2,7 @@ import { registerAdminRouter } from "./admin-router";
|
||||
import { registerAuthRoutes } from "./auth-router";
|
||||
import { registerProjectBotRouter } from "./bot-router";
|
||||
import { registerIdentityAccessTokenRouter } from "./identity-access-token-router";
|
||||
import { registerIdentityAwsAuthRouter } from "./identity-aws-iam-auth-router";
|
||||
import { registerIdentityRouter } from "./identity-router";
|
||||
import { registerIdentityUaRouter } from "./identity-ua";
|
||||
import { registerIntegrationAuthRouter } from "./integration-auth-router";
|
||||
@ -28,6 +29,7 @@ export const registerV1Routes = async (server: FastifyZodProvider) => {
|
||||
await authRouter.register(registerAuthRoutes);
|
||||
await authRouter.register(registerIdentityUaRouter);
|
||||
await authRouter.register(registerIdentityAccessTokenRouter);
|
||||
await authRouter.register(registerIdentityAwsAuthRouter);
|
||||
},
|
||||
{ prefix: "/auth" }
|
||||
);
|
||||
|
@ -66,7 +66,8 @@ export const registerIntegrationRouter = async (server: FastifyZodProvider) => {
|
||||
)
|
||||
.optional()
|
||||
.describe(INTEGRATION.CREATE.metadata.secretAWSTag),
|
||||
kmsKeyId: z.string().optional().describe(INTEGRATION.CREATE.metadata.kmsKeyId)
|
||||
kmsKeyId: z.string().optional().describe(INTEGRATION.CREATE.metadata.kmsKeyId),
|
||||
shouldDisableDelete: z.boolean().optional().describe(INTEGRATION.CREATE.metadata.shouldDisableDelete)
|
||||
})
|
||||
.default({})
|
||||
}),
|
||||
|
@ -293,6 +293,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
|
||||
}),
|
||||
querystring: z.object({
|
||||
workspaceId: z.string().trim().optional().describe(RAW_SECRETS.GET.workspaceId),
|
||||
workspaceSlug: z.string().trim().optional().describe(RAW_SECRETS.GET.workspaceSlug),
|
||||
environment: z.string().trim().optional().describe(RAW_SECRETS.GET.environment),
|
||||
secretPath: z.string().trim().default("/").transform(removeTrailingSlash).describe(RAW_SECRETS.GET.secretPath),
|
||||
version: z.coerce.number().optional().describe(RAW_SECRETS.GET.version),
|
||||
@ -311,6 +312,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const { workspaceSlug } = req.query;
|
||||
let { secretPath, environment, workspaceId } = req.query;
|
||||
if (req.auth.actor === ActorType.SERVICE) {
|
||||
const scope = ServiceTokenScopes.parse(req.auth.serviceToken.scopes);
|
||||
@ -322,7 +324,9 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
|
||||
}
|
||||
}
|
||||
|
||||
if (!workspaceId || !environment) throw new BadRequestError({ message: "Missing workspace id or environment" });
|
||||
if (!environment) throw new BadRequestError({ message: "Missing environment" });
|
||||
if (!workspaceId && !workspaceSlug)
|
||||
throw new BadRequestError({ message: "You must provide workspaceSlug or workspaceId" });
|
||||
|
||||
const secret = await server.services.secret.getSecretByNameRaw({
|
||||
actorId: req.permission.id,
|
||||
@ -331,6 +335,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
|
||||
actorOrgId: req.permission.orgId,
|
||||
environment,
|
||||
projectId: workspaceId,
|
||||
projectSlug: workspaceSlug,
|
||||
path: secretPath,
|
||||
secretName: req.params.secretName,
|
||||
type: req.query.type,
|
||||
@ -339,7 +344,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
|
||||
});
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
projectId: req.query.workspaceId,
|
||||
projectId: secret.workspace,
|
||||
...req.auditLogInfo,
|
||||
event: {
|
||||
type: EventType.GET_SECRET,
|
||||
@ -358,7 +363,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
|
||||
distinctId: getTelemetryDistinctId(req),
|
||||
properties: {
|
||||
numberOfSecrets: 1,
|
||||
workspaceId,
|
||||
workspaceId: secret.workspace,
|
||||
environment,
|
||||
secretPath: req.query.secretPath,
|
||||
channel: getUserAgentType(req.headers["user-agent"]),
|
||||
|
@ -0,0 +1,11 @@
|
||||
import { TDbClient } from "@app/db";
|
||||
import { TableName } from "@app/db/schemas";
|
||||
import { ormify } from "@app/lib/knex";
|
||||
|
||||
export type TIdentityAwsAuthDALFactory = ReturnType<typeof identityAwsAuthDALFactory>;
|
||||
|
||||
export const identityAwsAuthDALFactory = (db: TDbClient) => {
|
||||
const awsAuthOrm = ormify(db, TableName.IdentityAwsAuth);
|
||||
|
||||
return awsAuthOrm;
|
||||
};
|
@ -0,0 +1,67 @@
|
||||
/**
|
||||
* Extracts the identity ARN from the GetCallerIdentity response to one of the following formats:
|
||||
* - arn:aws:iam::123456789012:user/MyUserName
|
||||
* - arn:aws:iam::123456789012:role/MyRoleName
|
||||
*/
|
||||
export const extractPrincipalArn = (arn: string) => {
|
||||
// split the ARN into parts using ":" as the delimiter
|
||||
const fullParts = arn.split(":");
|
||||
if (fullParts.length !== 6) {
|
||||
throw new Error(`Unrecognized ARN: contains ${fullParts.length} colon-separated parts, expected 6`);
|
||||
}
|
||||
const [prefix, partition, service, , accountNumber, resource] = fullParts;
|
||||
if (prefix !== "arn") {
|
||||
throw new Error('Unrecognized ARN: does not begin with "arn:"');
|
||||
}
|
||||
|
||||
// structure to hold the parsed data
|
||||
const entity = {
|
||||
Partition: partition,
|
||||
Service: service,
|
||||
AccountNumber: accountNumber,
|
||||
Type: "",
|
||||
Path: "",
|
||||
FriendlyName: "",
|
||||
SessionInfo: ""
|
||||
};
|
||||
|
||||
// validate the service is either 'iam' or 'sts'
|
||||
if (entity.Service !== "iam" && entity.Service !== "sts") {
|
||||
throw new Error(`Unrecognized service: ${entity.Service}, not one of iam or sts`);
|
||||
}
|
||||
|
||||
// parse the last part of the ARN which describes the resource
|
||||
const parts = resource.split("/");
|
||||
if (parts.length < 2) {
|
||||
throw new Error(`Unrecognized ARN: "${resource}" contains fewer than 2 slash-separated parts`);
|
||||
}
|
||||
|
||||
const [type, ...rest] = parts;
|
||||
entity.Type = type;
|
||||
entity.FriendlyName = parts[parts.length - 1];
|
||||
|
||||
// handle different types of resources
|
||||
switch (entity.Type) {
|
||||
case "assumed-role": {
|
||||
if (rest.length < 2) {
|
||||
throw new Error(`Unrecognized ARN: "${resource}" contains fewer than 3 slash-separated parts`);
|
||||
}
|
||||
// assumed roles use a special format where the friendly name is the role name
|
||||
const [roleName, sessionId] = rest;
|
||||
entity.Type = "role"; // treat assumed role case as role
|
||||
entity.FriendlyName = roleName;
|
||||
entity.SessionInfo = sessionId;
|
||||
break;
|
||||
}
|
||||
case "user":
|
||||
case "role":
|
||||
case "instance-profile":
|
||||
// standard cases: just join back the path if there's any
|
||||
entity.Path = rest.slice(0, -1).join("/");
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Unrecognized principal type: "${entity.Type}"`);
|
||||
}
|
||||
|
||||
return `arn:aws:iam::${entity.AccountNumber}:${entity.Type}/${entity.FriendlyName}`;
|
||||
};
|
@ -0,0 +1,310 @@
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
||||
import { ForbiddenError } from "@casl/ability";
|
||||
import axios from "axios";
|
||||
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 { TIdentityAwsAuthDALFactory } from "./identity-aws-auth-dal";
|
||||
import { extractPrincipalArn } from "./identity-aws-auth-fns";
|
||||
import {
|
||||
TAttachAwsAuthDTO,
|
||||
TAwsGetCallerIdentityHeaders,
|
||||
TGetAwsAuthDTO,
|
||||
TGetCallerIdentityResponse,
|
||||
TLoginAwsAuthDTO,
|
||||
TUpdateAwsAuthDTO
|
||||
} from "./identity-aws-auth-types";
|
||||
|
||||
type TIdentityAwsAuthServiceFactoryDep = {
|
||||
identityAccessTokenDAL: Pick<TIdentityAccessTokenDALFactory, "create">;
|
||||
identityAwsAuthDAL: Pick<TIdentityAwsAuthDALFactory, "findOne" | "transaction" | "create" | "updateById">;
|
||||
identityOrgMembershipDAL: Pick<TIdentityOrgDALFactory, "findOne">;
|
||||
identityDAL: Pick<TIdentityDALFactory, "updateById">;
|
||||
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
|
||||
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">;
|
||||
};
|
||||
|
||||
export type TIdentityAwsAuthServiceFactory = ReturnType<typeof identityAwsAuthServiceFactory>;
|
||||
|
||||
export const identityAwsAuthServiceFactory = ({
|
||||
identityAccessTokenDAL,
|
||||
identityAwsAuthDAL,
|
||||
identityOrgMembershipDAL,
|
||||
identityDAL,
|
||||
licenseService,
|
||||
permissionService
|
||||
}: TIdentityAwsAuthServiceFactoryDep) => {
|
||||
const login = async ({ identityId, iamHttpRequestMethod, iamRequestBody, iamRequestHeaders }: TLoginAwsAuthDTO) => {
|
||||
const identityAwsAuth = await identityAwsAuthDAL.findOne({ identityId });
|
||||
if (!identityAwsAuth) throw new UnauthorizedError();
|
||||
|
||||
const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId: identityAwsAuth.identityId });
|
||||
|
||||
const headers: TAwsGetCallerIdentityHeaders = JSON.parse(Buffer.from(iamRequestHeaders, "base64").toString());
|
||||
const body: string = Buffer.from(iamRequestBody, "base64").toString();
|
||||
|
||||
const {
|
||||
data: {
|
||||
GetCallerIdentityResponse: {
|
||||
GetCallerIdentityResult: { Account, Arn }
|
||||
}
|
||||
}
|
||||
}: { data: TGetCallerIdentityResponse } = await axios({
|
||||
method: iamHttpRequestMethod,
|
||||
url: identityAwsAuth.stsEndpoint,
|
||||
headers,
|
||||
data: body
|
||||
});
|
||||
|
||||
if (identityAwsAuth.allowedAccountIds) {
|
||||
// validate if Account is in the list of allowed Account IDs
|
||||
|
||||
const isAccountAllowed = identityAwsAuth.allowedAccountIds
|
||||
.split(",")
|
||||
.map((accountId) => accountId.trim())
|
||||
.some((accountId) => accountId === Account);
|
||||
|
||||
if (!isAccountAllowed) throw new UnauthorizedError();
|
||||
}
|
||||
|
||||
if (identityAwsAuth.allowedPrincipalArns) {
|
||||
// validate if Arn is in the list of allowed Principal ARNs
|
||||
|
||||
const isArnAllowed = identityAwsAuth.allowedPrincipalArns
|
||||
.split(",")
|
||||
.map((principalArn) => principalArn.trim())
|
||||
.some((principalArn) => {
|
||||
// convert wildcard ARN to a regular expression: "arn:aws:iam::123456789012:*" -> "^arn:aws:iam::123456789012:.*$"
|
||||
// considers exact matches + wildcard matches
|
||||
const regex = new RegExp(`^${principalArn.replace(/\*/g, ".*")}$`);
|
||||
return regex.test(extractPrincipalArn(Arn));
|
||||
});
|
||||
|
||||
if (!isArnAllowed) throw new UnauthorizedError();
|
||||
}
|
||||
|
||||
const identityAccessToken = await identityAwsAuthDAL.transaction(async (tx) => {
|
||||
const newToken = await identityAccessTokenDAL.create(
|
||||
{
|
||||
identityId: identityAwsAuth.identityId,
|
||||
isAccessTokenRevoked: false,
|
||||
accessTokenTTL: identityAwsAuth.accessTokenTTL,
|
||||
accessTokenMaxTTL: identityAwsAuth.accessTokenMaxTTL,
|
||||
accessTokenNumUses: 0,
|
||||
accessTokenNumUsesLimit: identityAwsAuth.accessTokenNumUsesLimit
|
||||
},
|
||||
tx
|
||||
);
|
||||
return newToken;
|
||||
});
|
||||
|
||||
const appCfg = getConfig();
|
||||
const accessToken = jwt.sign(
|
||||
{
|
||||
identityId: identityAwsAuth.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, identityAwsAuth, identityAccessToken, identityMembershipOrg };
|
||||
};
|
||||
|
||||
const attachAwsAuth = async ({
|
||||
identityId,
|
||||
stsEndpoint,
|
||||
allowedPrincipalArns,
|
||||
allowedAccountIds,
|
||||
accessTokenTTL,
|
||||
accessTokenMaxTTL,
|
||||
accessTokenNumUsesLimit,
|
||||
accessTokenTrustedIps,
|
||||
actorId,
|
||||
actorAuthMethod,
|
||||
actor,
|
||||
actorOrgId
|
||||
}: TAttachAwsAuthDTO) => {
|
||||
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 AWS 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 identityAwsAuth = await identityAwsAuthDAL.transaction(async (tx) => {
|
||||
const doc = await identityAwsAuthDAL.create(
|
||||
{
|
||||
identityId: identityMembershipOrg.identityId,
|
||||
type: "iam",
|
||||
stsEndpoint,
|
||||
allowedPrincipalArns,
|
||||
allowedAccountIds,
|
||||
accessTokenMaxTTL,
|
||||
accessTokenTTL,
|
||||
accessTokenNumUsesLimit,
|
||||
accessTokenTrustedIps: JSON.stringify(reformattedAccessTokenTrustedIps)
|
||||
},
|
||||
tx
|
||||
);
|
||||
await identityDAL.updateById(
|
||||
identityMembershipOrg.identityId,
|
||||
{
|
||||
authMethod: IdentityAuthMethod.AWS_AUTH
|
||||
},
|
||||
tx
|
||||
);
|
||||
return doc;
|
||||
});
|
||||
return { ...identityAwsAuth, orgId: identityMembershipOrg.orgId };
|
||||
};
|
||||
|
||||
const updateAwsAuth = async ({
|
||||
identityId,
|
||||
stsEndpoint,
|
||||
allowedPrincipalArns,
|
||||
allowedAccountIds,
|
||||
accessTokenTTL,
|
||||
accessTokenMaxTTL,
|
||||
accessTokenNumUsesLimit,
|
||||
accessTokenTrustedIps,
|
||||
actorId,
|
||||
actorAuthMethod,
|
||||
actor,
|
||||
actorOrgId
|
||||
}: TUpdateAwsAuthDTO) => {
|
||||
const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId });
|
||||
if (!identityMembershipOrg) throw new BadRequestError({ message: "Failed to find identity" });
|
||||
if (identityMembershipOrg.identity?.authMethod !== IdentityAuthMethod.AWS_AUTH)
|
||||
throw new BadRequestError({
|
||||
message: "Failed to update AWS Auth"
|
||||
});
|
||||
|
||||
const identityAwsAuth = await identityAwsAuthDAL.findOne({ identityId });
|
||||
|
||||
if (
|
||||
(accessTokenMaxTTL || identityAwsAuth.accessTokenMaxTTL) > 0 &&
|
||||
(accessTokenTTL || identityAwsAuth.accessTokenMaxTTL) > (accessTokenMaxTTL || identityAwsAuth.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 updatedAwsAuth = await identityAwsAuthDAL.updateById(identityAwsAuth.id, {
|
||||
stsEndpoint,
|
||||
allowedPrincipalArns,
|
||||
allowedAccountIds,
|
||||
accessTokenMaxTTL,
|
||||
accessTokenTTL,
|
||||
accessTokenNumUsesLimit,
|
||||
accessTokenTrustedIps: reformattedAccessTokenTrustedIps
|
||||
? JSON.stringify(reformattedAccessTokenTrustedIps)
|
||||
: undefined
|
||||
});
|
||||
|
||||
return { ...updatedAwsAuth, orgId: identityMembershipOrg.orgId };
|
||||
};
|
||||
|
||||
const getAwsAuth = async ({ identityId, actorId, actor, actorAuthMethod, actorOrgId }: TGetAwsAuthDTO) => {
|
||||
const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId });
|
||||
if (!identityMembershipOrg) throw new BadRequestError({ message: "Failed to find identity" });
|
||||
if (identityMembershipOrg.identity?.authMethod !== IdentityAuthMethod.AWS_AUTH)
|
||||
throw new BadRequestError({
|
||||
message: "The identity does not have AWS Auth attached"
|
||||
});
|
||||
|
||||
const awsIdentityAuth = await identityAwsAuthDAL.findOne({ identityId });
|
||||
|
||||
const { permission } = await permissionService.getOrgPermission(
|
||||
actor,
|
||||
actorId,
|
||||
identityMembershipOrg.orgId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Identity);
|
||||
return { ...awsIdentityAuth, orgId: identityMembershipOrg.orgId };
|
||||
};
|
||||
|
||||
return {
|
||||
login,
|
||||
attachAwsAuth,
|
||||
updateAwsAuth,
|
||||
getAwsAuth
|
||||
};
|
||||
};
|
@ -0,0 +1,54 @@
|
||||
import { TProjectPermission } from "@app/lib/types";
|
||||
|
||||
export type TLoginAwsAuthDTO = {
|
||||
identityId: string;
|
||||
iamHttpRequestMethod: string;
|
||||
iamRequestBody: string;
|
||||
iamRequestHeaders: string;
|
||||
};
|
||||
|
||||
export type TAttachAwsAuthDTO = {
|
||||
identityId: string;
|
||||
stsEndpoint: string;
|
||||
allowedPrincipalArns: string;
|
||||
allowedAccountIds: string;
|
||||
accessTokenTTL: number;
|
||||
accessTokenMaxTTL: number;
|
||||
accessTokenNumUsesLimit: number;
|
||||
accessTokenTrustedIps: { ipAddress: string }[];
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
export type TUpdateAwsAuthDTO = {
|
||||
identityId: string;
|
||||
stsEndpoint?: string;
|
||||
allowedPrincipalArns?: string;
|
||||
allowedAccountIds?: string;
|
||||
accessTokenTTL?: number;
|
||||
accessTokenMaxTTL?: number;
|
||||
accessTokenNumUsesLimit?: number;
|
||||
accessTokenTrustedIps?: { ipAddress: string }[];
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
export type TGetAwsAuthDTO = {
|
||||
identityId: string;
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
export type TAwsGetCallerIdentityHeaders = {
|
||||
"Content-Type": string;
|
||||
Host: string;
|
||||
"X-Amz-Date": string;
|
||||
"Content-Length": number;
|
||||
"x-amz-security-token": string;
|
||||
Authorization: string;
|
||||
};
|
||||
|
||||
export type TGetCallerIdentityResponse = {
|
||||
GetCallerIdentityResponse: {
|
||||
GetCallerIdentityResult: {
|
||||
Account: string;
|
||||
Arn: string;
|
||||
UserId: string;
|
||||
};
|
||||
ResponseMetadata: { RequestId: string };
|
||||
};
|
||||
};
|
@ -0,0 +1,58 @@
|
||||
import { z } from "zod";
|
||||
|
||||
const twelveDigitRegex = /^\d{12}$/;
|
||||
const arnRegex = /^arn:aws:iam::\d{12}:(user\/[\w-]+|role\/[\w-]+|\*)$/;
|
||||
|
||||
export const validateAccountIds = z
|
||||
.string()
|
||||
.trim()
|
||||
.default("")
|
||||
// Custom validation to ensure each part is a 12-digit number
|
||||
.refine(
|
||||
(data) => {
|
||||
if (data === "") return true;
|
||||
// Split the string by commas to check each supposed number
|
||||
const accountIds = data.split(",").map((id) => id.trim());
|
||||
// Return true only if every item matches the 12-digit requirement
|
||||
return accountIds.every((id) => twelveDigitRegex.test(id));
|
||||
},
|
||||
{
|
||||
message: "Each account ID must be a 12-digit number."
|
||||
}
|
||||
)
|
||||
// Transform the string to normalize space after commas
|
||||
.transform((data) => {
|
||||
if (data === "") return "";
|
||||
// Trim each ID and join with ', ' to ensure formatting
|
||||
return data
|
||||
.split(",")
|
||||
.map((id) => id.trim())
|
||||
.join(", ");
|
||||
});
|
||||
|
||||
export const validatePrincipalArns = z
|
||||
.string()
|
||||
.trim()
|
||||
.default("")
|
||||
// Custom validation for ARN format
|
||||
.refine(
|
||||
(data) => {
|
||||
// Skip validation if the string is empty
|
||||
if (data === "") return true;
|
||||
// Split the string by commas to check each supposed ARN
|
||||
const arns = data.split(",");
|
||||
// Return true only if every item matches one of the allowed ARN formats
|
||||
return arns.every((arn) => arnRegex.test(arn.trim()));
|
||||
},
|
||||
{
|
||||
message:
|
||||
"Each ARN must be in the format of 'arn:aws:iam::123456789012:user/UserName', 'arn:aws:iam::123456789012:role/RoleName', or 'arn:aws:iam::123456789012:*'."
|
||||
}
|
||||
)
|
||||
// Transform to normalize the spaces around commas
|
||||
.transform((data) =>
|
||||
data
|
||||
.split(",")
|
||||
.map((arn) => arn.trim())
|
||||
.join(", ")
|
||||
);
|
@ -52,7 +52,7 @@ export const identityUaServiceFactory = ({
|
||||
}: TIdentityUaServiceFactoryDep) => {
|
||||
const login = async (clientId: string, clientSecret: string, ip: string) => {
|
||||
const identityUa = await identityUaDAL.findOne({ clientId });
|
||||
if (!identityUa) throw new UnauthorizedError();
|
||||
if (!identityUa) throw new UnauthorizedError({ message: "Invalid credentials" });
|
||||
|
||||
const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId: identityUa.identityId });
|
||||
|
||||
@ -68,7 +68,7 @@ export const identityUaServiceFactory = ({
|
||||
const validClientSecretInfo = clientSecrtInfo.find(({ clientSecretHash }) =>
|
||||
bcrypt.compareSync(clientSecret, clientSecretHash)
|
||||
);
|
||||
if (!validClientSecretInfo) throw new UnauthorizedError();
|
||||
if (!validClientSecretInfo) throw new UnauthorizedError({ message: "Invalid credentials" });
|
||||
|
||||
const { clientSecretTTL, clientSecretNumUses, clientSecretNumUsesLimit } = validClientSecretInfo;
|
||||
if (Number(clientSecretTTL) > 0) {
|
||||
|
@ -517,20 +517,22 @@ const syncSecretsAWSParameterStore = async ({
|
||||
})
|
||||
);
|
||||
|
||||
// Identify secrets to delete
|
||||
await Promise.all(
|
||||
Object.keys(awsParameterStoreSecretsObj).map(async (key) => {
|
||||
if (!(key in secrets)) {
|
||||
// case:
|
||||
// -> delete secret
|
||||
await ssm
|
||||
.deleteParameter({
|
||||
Name: awsParameterStoreSecretsObj[key].Name as string
|
||||
})
|
||||
.promise();
|
||||
}
|
||||
})
|
||||
);
|
||||
if (!metadata.shouldDisableDelete) {
|
||||
// Identify secrets to delete
|
||||
await Promise.all(
|
||||
Object.keys(awsParameterStoreSecretsObj).map(async (key) => {
|
||||
if (!(key in secrets)) {
|
||||
// case:
|
||||
// -> delete secret
|
||||
await ssm
|
||||
.deleteParameter({
|
||||
Name: awsParameterStoreSecretsObj[key].Name as string
|
||||
})
|
||||
.promise();
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -27,6 +27,7 @@ export type TCreateIntegrationDTO = {
|
||||
value: string;
|
||||
}[];
|
||||
kmsKeyId?: string;
|
||||
shouldDisableDelete?: boolean;
|
||||
};
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
|
@ -546,6 +546,10 @@ export const orgServiceFactory = ({
|
||||
code
|
||||
});
|
||||
|
||||
await userDAL.updateById(user.id, {
|
||||
isEmailVerified: true
|
||||
});
|
||||
|
||||
if (user.isAccepted) {
|
||||
// this means user has already completed signup process
|
||||
// isAccepted is set true when keys are exchanged
|
||||
|
@ -972,7 +972,8 @@ export const secretServiceFactory = ({
|
||||
path,
|
||||
actor,
|
||||
environment,
|
||||
projectId,
|
||||
projectId: workspaceId,
|
||||
projectSlug,
|
||||
actorId,
|
||||
actorOrgId,
|
||||
actorAuthMethod,
|
||||
@ -980,6 +981,8 @@ export const secretServiceFactory = ({
|
||||
includeImports,
|
||||
version
|
||||
}: TGetASecretRawDTO) => {
|
||||
const projectId = workspaceId || (await projectDAL.findProjectBySlug(projectSlug as string, actorOrgId)).id;
|
||||
|
||||
const botKey = await projectBotService.getBotKey(projectId);
|
||||
if (!botKey) throw new BadRequestError({ message: "Project bot not found", name: "bot_not_found_error" });
|
||||
|
||||
|
@ -152,7 +152,9 @@ export type TGetASecretRawDTO = {
|
||||
type: "shared" | "personal";
|
||||
includeImports?: boolean;
|
||||
version?: number;
|
||||
} & TProjectPermission;
|
||||
projectSlug?: string;
|
||||
projectId?: string;
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
export type TCreateSecretRawDTO = TProjectPermission & {
|
||||
secretPath: string;
|
||||
|
@ -21,6 +21,7 @@ export enum SmtpTemplates {
|
||||
EmailVerification = "emailVerification.handlebars",
|
||||
SecretReminder = "secretReminder.handlebars",
|
||||
EmailMfa = "emailMfa.handlebars",
|
||||
AccessApprovalRequest = "accessApprovalRequest.handlebars",
|
||||
HistoricalSecretList = "historicalSecretLeakIncident.handlebars",
|
||||
NewDeviceJoin = "newDevice.handlebars",
|
||||
OrgInvite = "organizationInvitation.handlebars",
|
||||
|
@ -0,0 +1,50 @@
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta http-equiv="x-ua-compatible" content="ie=edge" />
|
||||
<title>Access Approval Request</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<h2>Infisical</h2>
|
||||
<h2>New access approval request pending your review</h2>
|
||||
<p>You have a new access approval request pending review in project "{{projectName}}".</p>
|
||||
|
||||
<p>
|
||||
{{requesterFullName}}
|
||||
({{requesterEmail}}) has requested
|
||||
{{#if isTemporary}}
|
||||
temporary
|
||||
{{else}}
|
||||
permanent
|
||||
{{/if}}
|
||||
access to
|
||||
{{secretPath}}
|
||||
in the
|
||||
{{environment}}
|
||||
environment.
|
||||
|
||||
{{#if isTemporary}}
|
||||
<br />
|
||||
This access will expire
|
||||
{{expiresIn}}
|
||||
after it has been approved.
|
||||
{{/if}}
|
||||
</p>
|
||||
<p>
|
||||
The following permissions are requested:
|
||||
<ul>
|
||||
{{#each permissions}}
|
||||
<li>{{this}}</li>
|
||||
{{/each}}
|
||||
</ul>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
View the request and approve or deny it
|
||||
<a href="{{approvalUrl}}">here</a>.
|
||||
</p>
|
||||
</body>
|
||||
|
||||
</html>
|
@ -74,6 +74,17 @@ export const userDALFactory = (db: TDbClient) => {
|
||||
}
|
||||
};
|
||||
|
||||
const findUsersByProjectMembershipIds = async (projectMembershipIds: string[]) => {
|
||||
try {
|
||||
return await db(TableName.ProjectMembership)
|
||||
.whereIn(`${TableName.ProjectMembership}.id`, projectMembershipIds)
|
||||
.join(TableName.Users, `${TableName.ProjectMembership}.userId`, `${TableName.Users}.id`)
|
||||
.select("*");
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: "Find users by project membership ids" });
|
||||
}
|
||||
};
|
||||
|
||||
const createUserEncryption = async (data: TUserEncryptionKeysInsert, tx?: Knex) => {
|
||||
try {
|
||||
const [userEnc] = await (tx || db)(TableName.UserEncryptionKey).insert(data).returning("*");
|
||||
@ -140,6 +151,7 @@ export const userDALFactory = (db: TDbClient) => {
|
||||
findUserEncKeyByUserId,
|
||||
updateUserEncryptionByUserId,
|
||||
findUserByProjectMembershipId,
|
||||
findUsersByProjectMembershipIds,
|
||||
upsertUserEncryptionKey,
|
||||
createUserEncryption,
|
||||
findOneUserAction,
|
||||
|
@ -4,59 +4,66 @@ sidebarTitle: "What is Infisical?"
|
||||
description: "An Introduction to the Infisical secret management platform."
|
||||
---
|
||||
|
||||
Infisical is an [open-source](https://github.com/infisical/infisical) secret management platform for developers.
|
||||
It provides capabilities for storing, managing, and syncing application configuration and secrets like API keys, database
|
||||
credentials, and certificates across infrastructure. In addition, Infisical prevents secrets leaks to git and enables secure
|
||||
Infisical is an [open-source](https://github.com/infisical/infisical) secret management platform for developers.
|
||||
It provides capabilities for storing, managing, and syncing application configuration and secrets like API keys, database
|
||||
credentials, and certificates across infrastructure. In addition, Infisical prevents secrets leaks to git and enables secure
|
||||
sharing of secrets among engineers.
|
||||
|
||||
Start managing secrets securely with [Infisical Cloud](https://app.infisical.com) or learn how to [host Infisical](/self-hosting/overview) yourself.
|
||||
|
||||
<CardGroup cols={2}>
|
||||
<Card
|
||||
title="Infisical Cloud"
|
||||
href="https://app.infisical.com/signup"
|
||||
icon="cloud"
|
||||
color="#000000"
|
||||
>
|
||||
Get started with Infisical Cloud in just a few minutes.
|
||||
</Card>
|
||||
<Card
|
||||
href="/self-hosting/overview"
|
||||
title="Self-hosting"
|
||||
icon="server"
|
||||
color="#000000"
|
||||
>
|
||||
Self-host Infisical on your own infrastructure.
|
||||
</Card>
|
||||
<Card
|
||||
title="Infisical Cloud"
|
||||
href="https://app.infisical.com/signup"
|
||||
icon="cloud"
|
||||
color="#000000"
|
||||
>
|
||||
Get started with Infisical Cloud in just a few minutes.
|
||||
</Card>
|
||||
<Card
|
||||
href="/self-hosting/overview"
|
||||
title="Self-hosting"
|
||||
icon="server"
|
||||
color="#000000"
|
||||
>
|
||||
Self-host Infisical on your own infrastructure.
|
||||
</Card>
|
||||
</CardGroup>
|
||||
|
||||
## Why Infisical?
|
||||
## Why Infisical?
|
||||
|
||||
Infisical helps developers achieve secure centralized secret management and provides all the tools to easily manage secrets in various environments and infrastructure components. In particular, here are some of the most common points that developers mention after adopting Infisical:
|
||||
|
||||
Infisical helps developers achieve secure centralized secret management and provides all the tools to easily manage secrets in various environments and infrastructure components. In particular, here are some of the most common points that developers mention after adopting Infisical:
|
||||
- Streamlined **local development** processes (switching .env files to [Infisical CLI](/cli/commands/run) and removing secrets from developer machines).
|
||||
- **Best-in-class developer experience** with an easy-to-use [Web Dashboard](/documentation/platform/project).
|
||||
- Simple secret management inside **[CI/CD pipelines](/integrations/cicd/githubactions)** and staging environments.
|
||||
- Secure and compliant secret management practices in **[production environments](/sdks/overview)**.
|
||||
- **Best-in-class developer experience** with an easy-to-use [Web Dashboard](/documentation/platform/project).
|
||||
- Simple secret management inside **[CI/CD pipelines](/integrations/cicd/githubactions)** and staging environments.
|
||||
- Secure and compliant secret management practices in **[production environments](/sdks/overview)**.
|
||||
- **Facilitated workflows** around [secret change management](/documentation/platform/pr-workflows), [access requests](/documentation/platform/access-controls/access-requests), [temporary access provisioning](/documentation/platform/access-controls/temporary-access), and more.
|
||||
- **Improved security posture** thanks to [secret scanning](/cli/scanning-overview), [granular access control policies](/documentation/platform/access-controls/overview), [automated secret rotation](https://infisical.com/docs/documentation/platform/secret-rotation/overview), and [dynamic secrets](/documentation/platform/dynamic-secrets/overview) capabilities.
|
||||
|
||||
## How does Infisical work?
|
||||
## How does Infisical work?
|
||||
|
||||
To make secret management effortless and secure, Infisical follows a certain structure for enabling secret management workflows as defined below.
|
||||
To make secret management effortless and secure, Infisical follows a certain structure for enabling secret management workflows as defined below.
|
||||
|
||||
**Identities** in Infisical are users or machine which have a certain set of roles and permissions assigned to them. Such identities are able to manage secrets in various **Clients** throughout the entire infrastructure. To do that, identities have to verify themselves through one of the available **Authentication Methods**.
|
||||
**Identities** in Infisical are users or machine which have a certain set of roles and permissions assigned to them. Such identities are able to manage secrets in various **Clients** throughout the entire infrastructure. To do that, identities have to verify themselves through one of the available **Authentication Methods**.
|
||||
|
||||
As a result, the 3 main concepts that are important to understand are:
|
||||
- **[Identities](/documentation/platform/identities/overview)**: users or machines with a set permissions assigned to them.
|
||||
As a result, the 3 main concepts that are important to understand are:
|
||||
|
||||
- **[Identities](/documentation/platform/identities/overview)**: users or machines with a set permissions assigned to them.
|
||||
- **[Clients](/integrations/platforms/kubernetes)**: Infisical-developed tools for managing secrets in various infrastructure components (e.g., [Kubernetes Operator](/integrations/platforms/kubernetes), [Infisical Agent](/integrations/platforms/infisical-agent), [CLI](/cli/usage), [SDKs](/sdks/overview), [API](/api-reference/overview/introduction), [Web Dashboard](/documentation/platform/organization)).
|
||||
- **[Authentication Methods](/documentation/platform/identities/universal-auth)**: ways for Identities to authenticate inside different clients (e.g., SAML SSO for Web Dashboard, Universal Auth for Infisical Agent, etc.).
|
||||
- **[Authentication Methods](/documentation/platform/identities/universal-auth)**: ways for Identities to authenticate inside different clients (e.g., SAML SSO for Web Dashboard, Universal Auth for Infisical Agent, AWS Auth etc.).
|
||||
|
||||
## How to get started with Infisical?
|
||||
## How to get started with Infisical?
|
||||
|
||||
Depending on your use case, it might be helpful to look into some of the resources and guides provided below.
|
||||
|
||||
<CardGroup cols={2}>
|
||||
<Card href="../../cli/overview" title="Command Line Interface (CLI)" icon="square-terminal" color="#000000">
|
||||
<Card
|
||||
href="../../cli/overview"
|
||||
title="Command Line Interface (CLI)"
|
||||
icon="square-terminal"
|
||||
color="#000000"
|
||||
>
|
||||
Inject secrets into any application process/environment.
|
||||
</Card>
|
||||
<Card
|
||||
@ -67,7 +74,12 @@ Depending on your use case, it might be helpful to look into some of the resourc
|
||||
>
|
||||
Fetch secrets with any programming language on demand.
|
||||
</Card>
|
||||
<Card href="../../integrations/platforms/docker-intro" title="Docker" icon="docker" color="#000000">
|
||||
<Card
|
||||
href="../../integrations/platforms/docker-intro"
|
||||
title="Docker"
|
||||
icon="docker"
|
||||
color="#000000"
|
||||
>
|
||||
Inject secrets into Docker containers.
|
||||
</Card>
|
||||
<Card
|
||||
|
304
docs/documentation/platform/identities/aws-auth.mdx
Normal file
304
docs/documentation/platform/identities/aws-auth.mdx
Normal file
@ -0,0 +1,304 @@
|
||||
---
|
||||
title: AWS Auth
|
||||
description: "Learn how to authenticate with Infisical for EC2 instances, Lambda functions, and other IAM principals."
|
||||
---
|
||||
|
||||
**AWS Auth** is an AWS-native authentication method for IAM principals like EC2 instances or Lambda functions to access Infisical.
|
||||
|
||||
## Diagram
|
||||
|
||||
The following sequence digram illustrates the AWS Auth workflow for authenticating AWS IAM principals with Infisical.
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant Client as Client
|
||||
participant Infis as Infisical
|
||||
participant AWS as AWS STS
|
||||
|
||||
Note over Client,Client: Step 1: Sign GetCallerIdentityQuery
|
||||
|
||||
Note over Client,Infis: Step 2: Login Operation
|
||||
Client->>Infis: Send signed query details /api/v1/auth/aws-auth/login
|
||||
|
||||
Note over Infis,AWS: Step 3: Query verification
|
||||
Infis->>AWS: Forward signed GetCallerIdentity query
|
||||
AWS-->>Infis: Return IAM user/role details
|
||||
|
||||
Note over Infis: Step 4: Identity Property Validation
|
||||
Infis->>Client: Return short-lived access token
|
||||
|
||||
Note over Client,Infis: Step 4: Access Infisical API with Token
|
||||
Client->>Infis: Make authenticated requests using the short-lived access token
|
||||
```
|
||||
|
||||
## Concept
|
||||
|
||||
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.
|
||||
|
||||
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.
|
||||
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.
|
||||
|
||||
<Note>
|
||||
We recommend using one of Infisical's clients like SDKs or the Infisical Agent
|
||||
to authenticate with Infisical using AWS Auth as they handle the
|
||||
authentication process including the signed `GetCallerIdentity` query
|
||||
construction for you.
|
||||
|
||||
Also, note that Infisical needs network-level access to send requests to the AWS STS API
|
||||
as part of the AWS Auth workflow.
|
||||
|
||||
</Note>
|
||||
|
||||
## Workflow
|
||||
|
||||
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.
|
||||
|
||||
<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 **AWS Auth**.
|
||||
|
||||

|
||||
|
||||
Here's some more guidance on each field:
|
||||
|
||||
- Allowed Principal ARNs: A comma-separated list of trusted IAM principal ARNs that are allowed to authenticate with Infisical. The values should take one of three forms: `arn:aws:iam::123456789012:user/MyUserName`, `arn:aws:iam::123456789012:role/MyRoleName`, or `arn:aws:iam::123456789012:*`. Using a wildcard in this case allows any IAM principal in the account `123456789012` to authenticate with Infisical under the identity.
|
||||
- Allowed Account IDs: A comma-separated list of trusted AWS account IDs that are allowed to authenticate with Infisical.
|
||||
- STS Endpoint (default is `https://sts.amazonaws.com/`): The endpoint URL for the AWS STS API. This is useful for AWS GovCloud or other AWS regions that have different STS endpoints.
|
||||
- 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 construct a signed `GetCallerIdentity` query using the [AWS Signature v4 algorithm](https://docs.aws.amazon.com/IAM/latest/UserGuide/create-signed-request.html) and make a request to the `/api/v1/auth/aws-auth/login` endpoint containing the query data
|
||||
in exchange for an access token.
|
||||
|
||||
We provide a few code examples below of how you can authenticate with Infisical from inside a Lambda function, EC2 instance, etc. and obtain an access token to access the [Infisical API](/api-reference/overview/introduction).
|
||||
|
||||
<AccordionGroup>
|
||||
<Accordion
|
||||
title="Sample code for inside a Lambda function"
|
||||
>
|
||||
The following query construction is an example of how you can authenticate with Infisical from inside a Lambda function.
|
||||
|
||||
The shown example uses Node.js but you can use other languages supported by AWS Lambda.
|
||||
|
||||
```javascript
|
||||
import AWS from "aws-sdk";
|
||||
import axios from "axios";
|
||||
|
||||
export const handler = async (event, context) => {
|
||||
try {
|
||||
const region = process.env.AWS_REGION;
|
||||
AWS.config.update({ region });
|
||||
|
||||
const iamRequestURL = `https://sts.${region}.amazonaws.com/`;
|
||||
const iamRequestBody = "Action=GetCallerIdentity&Version=2011-06-15";
|
||||
const iamRequestHeaders = {
|
||||
"Content-Type": "application/x-www-form-urlencoded; charset=utf-8",
|
||||
Host: `sts.${region}.amazonaws.com`,
|
||||
};
|
||||
|
||||
// Create the request
|
||||
const request = new AWS.HttpRequest(iamRequestURL, region);
|
||||
request.method = "POST";
|
||||
request.headers = iamRequestHeaders;
|
||||
request.headers["X-Amz-Date"] = AWS.util.date
|
||||
.iso8601(new Date())
|
||||
.replace(/[:-]|\.\d{3}/g, "");
|
||||
request.body = iamRequestBody;
|
||||
request.headers["Content-Length"] =
|
||||
Buffer.byteLength(iamRequestBody).toString();
|
||||
|
||||
// Sign the request
|
||||
const signer = new AWS.Signers.V4(request, "sts");
|
||||
signer.addAuthorization(AWS.config.credentials, new Date());
|
||||
|
||||
const infisicalUrl = "https://app.infisical.com"; // or your self-hosted Infisical URL
|
||||
const identityId = "<your-identity-id>";
|
||||
|
||||
const { data } = await axios.post(
|
||||
`${infisicalUrl}/api/v1/auth/aws-auth/login`,
|
||||
{
|
||||
identityId,
|
||||
iamHttpRequestMethod: "POST",
|
||||
iamRequestUrl: Buffer.from(iamRequestURL).toString("base64"),
|
||||
iamRequestBody: Buffer.from(iamRequestBody).toString("base64"),
|
||||
iamRequestHeaders: Buffer.from(
|
||||
JSON.stringify(iamRequestHeaders)
|
||||
).toString("base64"),
|
||||
}
|
||||
);
|
||||
|
||||
console.log("result data: ", data); // access token here
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
};
|
||||
````
|
||||
</Accordion>
|
||||
<Accordion
|
||||
title="Sample code for inside an EC2 instance"
|
||||
>
|
||||
The following query construction is an example of how you can authenticate with Infisical from inside a EC2 instance.
|
||||
|
||||
The shown example uses Node.js but you can use other language you wish.
|
||||
|
||||
```javascript
|
||||
import AWS from "aws-sdk";
|
||||
import axios from "axios";
|
||||
|
||||
const main = async () => {
|
||||
try {
|
||||
// obtain region from EC2 instance metadata
|
||||
const tokenResponse = await axios.put("http://169.254.169.254/latest/api/token", null, {
|
||||
headers: {
|
||||
"X-aws-ec2-metadata-token-ttl-seconds": "21600"
|
||||
}
|
||||
});
|
||||
|
||||
const url = "http://169.254.169.254/latest/dynamic/instance-identity/document";
|
||||
const response = await axios.get(url, {
|
||||
headers: {
|
||||
"X-aws-ec2-metadata-token": tokenResponse.data
|
||||
}
|
||||
});
|
||||
|
||||
const region = response.data.region;
|
||||
|
||||
AWS.config.update({
|
||||
region
|
||||
});
|
||||
|
||||
const iamRequestURL = `https://sts.${region}.amazonaws.com/`;
|
||||
const iamRequestBody = "Action=GetCallerIdentity&Version=2011-06-15";
|
||||
const iamRequestHeaders = {
|
||||
"Content-Type": "application/x-www-form-urlencoded; charset=utf-8",
|
||||
Host: `sts.${region}.amazonaws.com`
|
||||
};
|
||||
|
||||
const request = new AWS.HttpRequest(new AWS.Endpoint(iamRequestURL), AWS.config.region);
|
||||
request.method = "POST";
|
||||
request.headers = iamRequestHeaders;
|
||||
request.headers["X-Amz-Date"] = AWS.util.date.iso8601(new Date()).replace(/[:-]|\.\d{3}/g, "");
|
||||
request.body = iamRequestBody;
|
||||
request.headers["Content-Length"] = Buffer.byteLength(iamRequestBody);
|
||||
|
||||
const signer = new AWS.Signers.V4(request, "sts");
|
||||
signer.addAuthorization(AWS.config.credentials, new Date());
|
||||
|
||||
const infisicalUrl = "https://app.infisical.com"; // or your self-hosted Infisical URL
|
||||
const identityId = "<your-identity-id>";
|
||||
|
||||
const { data } = await axios.post(`${infisicalUrl}/api/v1/auth/aws-auth/login`, {
|
||||
identityId,
|
||||
iamHttpRequestMethod: "POST",
|
||||
iamRequestUrl: Buffer.from(iamRequestURL).toString("base64"),
|
||||
iamRequestBody: Buffer.from(iamRequestBody).toString("base64"),
|
||||
iamRequestHeaders: Buffer.from(JSON.stringify(iamRequestHeaders)).toString("base64")
|
||||
});
|
||||
|
||||
console.log("result data: ", data); // access token here
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
}
|
||||
|
||||
main();
|
||||
````
|
||||
</Accordion>
|
||||
<Accordion
|
||||
title="Sample code for general query construction"
|
||||
>
|
||||
The following query construction provides a generic example of how you can construct a signed `GetCallerIdentity` query and obtain the required payload components.
|
||||
|
||||
The shown example uses Node.js but you can use any language you wish.
|
||||
|
||||
```javascript
|
||||
const AWS = require("aws-sdk");
|
||||
|
||||
const region = "<your-aws-region>";
|
||||
const infisicalUrl = "https://app.infisical.com"; // or your self-hosted Infisical URL
|
||||
|
||||
const iamRequestURL = `https://sts.${region}.amazonaws.com/`;
|
||||
const iamRequestBody = "Action=GetCallerIdentity&Version=2011-06-15";
|
||||
const iamRequestHeaders = {
|
||||
"Content-Type": "application/x-www-form-urlencoded; charset=utf-8",
|
||||
Host: `sts.${region}.amazonaws.com`
|
||||
};
|
||||
|
||||
const request = new AWS.HttpRequest(new AWS.Endpoint(iamRequestURL), region);
|
||||
request.method = "POST";
|
||||
request.headers = iamRequestHeaders;
|
||||
request.headers["X-Amz-Date"] = AWS.util.date.iso8601(new Date()).replace(/[:-]|\.\d{3}/g, "");
|
||||
request.body = iamRequestBody;
|
||||
request.headers["Content-Length"] = Buffer.byteLength(iamRequestBody);
|
||||
````
|
||||
|
||||
#### Sample request
|
||||
|
||||
```bash Request
|
||||
curl --location --request POST 'https://app.infisical.com/api/v1/auth/aws-auth/login' \
|
||||
--header 'Content-Type: application/x-www-form-urlencoded' \
|
||||
--data-urlencode 'identityId=...' \
|
||||
--data-urlencode 'iamHttpRequestMethod=...' \
|
||||
--data-urlencode 'iamRequestBody=...' \
|
||||
--data-urlencode 'iamRequestHeaders=...'
|
||||
```
|
||||
|
||||
#### 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 AWS Auth as they handle the authentication process including the signed `GetCallerIdentity` query construction for you.
|
||||
</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>
|
@ -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).
|
||||
|
||||
Each identity must authenticate with the API using a supported authentication method like [Universal Auth](/documentation/platform/identities/universal-auth) to get back a short-lived access token to be used in subsequent requests.
|
||||
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.
|
||||
|
||||

|
||||
|
||||
@ -21,7 +21,7 @@ Key Features:
|
||||
A typical workflow for using identities consists of four steps:
|
||||
|
||||
1. Creating the identity with a name and [role](/documentation/platform/role-based-access-controls) in Organization Access Control > Machine Identities.
|
||||
This step also involves configuring an authentication method for it such as [Universal Auth](/documentation/platform/identities/universal-auth).
|
||||
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).
|
||||
2. Adding the identity to the project(s) you want it to have access to.
|
||||
3. Authenticating the identity with the Infisical API based on the configured authentication method on it and receiving a short-lived access token back.
|
||||
4. Authenticating subsequent requests with the Infisical API using the short-lived access token.
|
||||
@ -37,7 +37,8 @@ Machine Identity support for the rest of the clients is planned to be released i
|
||||
|
||||
To interact with various resources in Infisical, Machine Identities are able to authenticate using:
|
||||
|
||||
- [Universal Auth](/documentation/platform/identities/universal-auth): the most versatile authentication method that can be configured on an identity from any platform/environment to access Infisical.
|
||||
- [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.
|
||||
|
||||
## FAQ
|
||||
|
||||
|
@ -3,19 +3,35 @@ title: Universal Auth
|
||||
description: "Learn how to authenticate to Infisical from any platform or environment."
|
||||
---
|
||||
|
||||
**Universal Auth** is the most versatile authentication method that can be configured for a [machine identity](/documentation/platform/identities/machine-identities) to access Infisical from any platform or environment.
|
||||
**Universal Auth** is a platform-agnostic authentication method that can be configured for a [machine identity](/documentation/platform/identities/machine-identities) suitable to authenticate from any platform/environment.
|
||||
|
||||
In this method, each identity is given a **Client ID** for which you can generate one or more **Client Secret(s)**. Together, a **Client ID** and **Client Secret** can be exchanged for an access token to authenticate with the Infisical API.
|
||||
## Diagram
|
||||
|
||||
## Properties
|
||||
The following sequence digram illustrates the Universal Auth workflow for authenticating clients with Infisical.
|
||||
|
||||
Universal Auth supports many settings that can be beneficial for tightening your workflow security configuration:
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant Client as Client
|
||||
participant Infis as Infisical
|
||||
|
||||
- Support for restrictions on the number of times that the **Client Secret(s)** and access token(s) can be used.
|
||||
- Support for expiration, so, if specified, the **Client Secret** of the identity will automatically be defunct after a period of time.
|
||||
- Support for IP allowlisting; this means you can restrict the usage of **Client Secret(s)** and access token to a specific IP or CIDR range.
|
||||
Note over Client,Infis: Step 1: Login Operation
|
||||
Client->>Infis: Send Client ID and Client Secret
|
||||
|
||||
## Workflow
|
||||
Note over Infis: Step 2: Client ID and Client Secret validation
|
||||
Infis->>Client: Return short-lived access token
|
||||
|
||||
Note over Client,Infis: Step 3: Access Infisical API with Token
|
||||
Client->>Infis: Make authenticated requests using the short-lived access token
|
||||
```
|
||||
|
||||
## 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,
|
||||
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.
|
||||
|
||||
## Guide
|
||||
|
||||
In the following steps, we explore how to create and use identities for your workloads and applications to access the Infisical API
|
||||
using the Universal Auth authentication method.
|
||||
@ -27,18 +43,18 @@ using the Universal Auth authentication method.
|
||||

|
||||
|
||||
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 **Universal Auth** authentication method for it.
|
||||
|
||||
|
||||

|
||||
|
||||
|
||||
Here's some more guidance on each field:
|
||||
|
||||
- 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.
|
||||
@ -78,8 +94,9 @@ using the Universal Auth authentication method.
|
||||
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 should first perform a login operation
|
||||
@ -88,16 +105,16 @@ using the Universal Auth authentication method.
|
||||
|
||||
#### Sample request
|
||||
|
||||
```
|
||||
```bash Request
|
||||
curl --location --request POST 'https://app.infisical.com/api/v1/auth/universal-auth/login' \
|
||||
--header 'Content-Type: application/x-www-form-urlencoded' \
|
||||
--data-urlencode 'clientSecret=...' \
|
||||
--data-urlencode 'clientId=...'
|
||||
--data-urlencode 'clientId=...' \
|
||||
--data-urlencode 'clientSecret=...'
|
||||
```
|
||||
|
||||
|
||||
#### Sample response
|
||||
|
||||
```
|
||||
|
||||
```bash Response
|
||||
{
|
||||
"accessToken": "...",
|
||||
"expiresIn": 7200,
|
||||
@ -107,7 +124,7 @@ using the Universal Auth authentication method.
|
||||
```
|
||||
|
||||
Next, you can use the access token to authenticate with the [Infisical API](/api-reference/overview/introduction)
|
||||
|
||||
|
||||
<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.
|
||||
@ -115,6 +132,7 @@ using the Universal Auth authentication method.
|
||||
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>
|
||||
|
||||
@ -134,7 +152,8 @@ using the Universal Auth authentication method.
|
||||
|
||||
In certain cases, you may want to extend the lifespan of an access token; to do so, you must set a max TTL parameter.
|
||||
|
||||
A token can be renewed any number of time and each call to renew it will extend the toke life by increments of access token TTL.
|
||||
Regardless of how frequently an access token is renewed, its lifespan remains bound to the maximum TTL determined at its creation
|
||||
A token can be renewed any number of time and each call to renew it will extend the toke life by increments of access token TTL.
|
||||
Regardless of how frequently an access token is renewed, its lifespan remains bound to the maximum TTL determined at its creation
|
||||
|
||||
</Accordion>
|
||||
</AccordionGroup>
|
||||
</AccordionGroup>
|
||||
|
Binary file not shown.
After ![]() (image error) Size: 538 KiB |
@ -77,6 +77,8 @@ spec:
|
||||
projectSlug: <project-slug>
|
||||
envSlug: <env-slug> # "dev", "staging", "prod", etc..
|
||||
secretsPath: "<secrets-path>" # Root is "/"
|
||||
recursive: true # Fetch all secrets from the specified path and all sub-directories. Default is false.
|
||||
|
||||
credentialsRef:
|
||||
secretName: universal-auth-credentials
|
||||
secretNamespace: default
|
||||
@ -89,6 +91,7 @@ spec:
|
||||
secretsScope:
|
||||
envSlug: <env-slug>
|
||||
secretsPath: <secrets-path> # Root is "/"
|
||||
recursive: true # Fetch all secrets from the specified path and all sub-directories. Default is false.
|
||||
|
||||
managedSecretReference:
|
||||
secretName: managed-secret
|
||||
|
@ -153,6 +153,7 @@
|
||||
"documentation/platform/auth-methods/email-password",
|
||||
"documentation/platform/token",
|
||||
"documentation/platform/identities/universal-auth",
|
||||
"documentation/platform/identities/aws-auth",
|
||||
"documentation/platform/mfa",
|
||||
{
|
||||
"group": "SSO",
|
||||
@ -211,9 +212,7 @@
|
||||
},
|
||||
{
|
||||
"group": "Reference architectures",
|
||||
"pages": [
|
||||
"self-hosting/reference-architectures/aws-ecs"
|
||||
]
|
||||
"pages": ["self-hosting/reference-architectures/aws-ecs"]
|
||||
},
|
||||
"self-hosting/ee",
|
||||
"self-hosting/faq"
|
||||
|
13
frontend/package-lock.json
generated
13
frontend/package-lock.json
generated
@ -4,6 +4,7 @@
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "frontend",
|
||||
"dependencies": {
|
||||
"@casl/ability": "^6.5.0",
|
||||
"@casl/react": "^3.1.0",
|
||||
@ -12165,9 +12166,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/ejs": {
|
||||
"version": "3.1.9",
|
||||
"resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.9.tgz",
|
||||
"integrity": "sha512-rC+QVNMJWv+MtPgkt0y+0rVEIdbtxVADApW9JXrUVlzHetgcyczP/E7DJmWJ4fJCZF2cPcBk0laWO9ZHMG3DmQ==",
|
||||
"version": "3.1.10",
|
||||
"resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz",
|
||||
"integrity": "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"jake": "^10.8.5"
|
||||
@ -22439,9 +22440,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/tar": {
|
||||
"version": "6.2.0",
|
||||
"resolved": "https://registry.npmjs.org/tar/-/tar-6.2.0.tgz",
|
||||
"integrity": "sha512-/Wo7DcT0u5HUV486xg675HtjNd3BXZ6xDbzsCUZPt5iw8bTQ63bP0Raut3mvro9u+CUyq7YQd8Cx55fsZXxqLQ==",
|
||||
"version": "6.2.1",
|
||||
"resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz",
|
||||
"integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"chownr": "^2.0.0",
|
||||
|
@ -17,23 +17,20 @@ export const PermissionDeniedBanner = ({ containerClassName, className, children
|
||||
containerClassName
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className={twMerge(
|
||||
"flex items-end space-x-12 rounded-md bg-mineshaft-800 p-16 text-bunker-300",
|
||||
className
|
||||
)}
|
||||
>
|
||||
<div>
|
||||
<FontAwesomeIcon icon={faLock} size="6x" />
|
||||
</div>
|
||||
<div>
|
||||
<div className="mb-2 text-4xl font-medium">Access Restricted</div>
|
||||
{children || (
|
||||
<div className="text-sm">
|
||||
Your role has limited permissions, please <br /> contact your administrator to gain
|
||||
access
|
||||
</div>
|
||||
)}
|
||||
<div className={twMerge("rounded-md bg-mineshaft-800 p-16 text-bunker-300", className)}>
|
||||
<div className="flex items-end space-x-12">
|
||||
<div>
|
||||
<FontAwesomeIcon icon={faLock} size="6x" />
|
||||
</div>
|
||||
<div>
|
||||
<div className="mb-2 text-4xl font-medium">Access Restricted</div>
|
||||
{children || (
|
||||
<div className="text-sm">
|
||||
Your role has limited permissions, please <br /> contact your administrator to gain
|
||||
access
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
32
frontend/src/components/v2/Badge/Badge.tsx
Normal file
32
frontend/src/components/v2/Badge/Badge.tsx
Normal file
@ -0,0 +1,32 @@
|
||||
import { cva, VariantProps } from "cva";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
interface IProps {
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const badgeVariants = cva(
|
||||
[
|
||||
"inline-block cursor-default rounded-md bg-yellow/20 px-1.5 pb-[0.03rem] pt-[0.04rem] text-xs text-yellow opacity-80 hover:opacity-100"
|
||||
],
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
primary: "bg-yellow/20 text-yellow",
|
||||
danger: "bg-red/20 text-red",
|
||||
success: "bg-green/20 text-green"
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
export type BadgeProps = VariantProps<typeof badgeVariants> & IProps;
|
||||
|
||||
export const Badge = ({ children, className, variant }: BadgeProps) => {
|
||||
return (
|
||||
<div className={twMerge(badgeVariants({ variant: variant || "primary" }), className)}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
};
|
1
frontend/src/components/v2/Badge/index.tsx
Normal file
1
frontend/src/components/v2/Badge/index.tsx
Normal file
@ -0,0 +1 @@
|
||||
export { Badge } from "./Badge";
|
@ -29,7 +29,7 @@ const buttonVariants = cva(
|
||||
colorSchema: {
|
||||
primary: ["bg-primary", "text-black", "border-primary bg-opacity-90 hover:bg-opacity-100"],
|
||||
secondary: ["bg-mineshaft", "text-gray-300", "border-mineshaft hover:bg-opacity-80"],
|
||||
danger: ["bg-red", "text-white", "border-red hover:bg-opacity-90"],
|
||||
danger: ["!bg-red", "!text-white", "!border-red hover:!bg-opacity-90"],
|
||||
gray: ["bg-bunker-500", "text-bunker-200"]
|
||||
},
|
||||
variant: {
|
||||
|
13
frontend/src/components/v2/Divider/Divider.tsx
Normal file
13
frontend/src/components/v2/Divider/Divider.tsx
Normal file
@ -0,0 +1,13 @@
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
interface IProps {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export const Divider = ({ className }: IProps): JSX.Element => {
|
||||
return (
|
||||
<div className={twMerge("flex items-center px-2 opacity-50", className)}>
|
||||
<div aria-hidden="true" className="h-5 w-full grow border border-t border-mineshaft-200" />
|
||||
</div>
|
||||
);
|
||||
};
|
1
frontend/src/components/v2/Divider/index.tsx
Normal file
1
frontend/src/components/v2/Divider/index.tsx
Normal file
@ -0,0 +1 @@
|
||||
export { Divider } from "./Divider";
|
@ -31,6 +31,7 @@ export const SecretPathInput = ({
|
||||
const [inputValue, setInputValue] = useState(propValue ?? "");
|
||||
const [secretPath, setSecretPath] = useState("/");
|
||||
const [suggestions, setSuggestions] = useState<string[]>([]);
|
||||
const [isInputFocused, setIsInputFocus] = useState(false);
|
||||
const [highlightedIndex, setHighlightedIndex] = useState(-1);
|
||||
const debouncedInputValue = useDebounce(inputValue, 200);
|
||||
|
||||
@ -46,14 +47,6 @@ export const SecretPathInput = ({
|
||||
setInputValue(propValue ?? "/");
|
||||
}, [propValue]);
|
||||
|
||||
useEffect(() => {
|
||||
if (environment) {
|
||||
setInputValue("/");
|
||||
setSecretPath("/");
|
||||
onChange?.("/");
|
||||
}
|
||||
}, [environment]);
|
||||
|
||||
useEffect(() => {
|
||||
// update secret path if input is valid
|
||||
if (
|
||||
@ -63,7 +56,9 @@ export const SecretPathInput = ({
|
||||
) {
|
||||
setSecretPath(debouncedInputValue);
|
||||
}
|
||||
}, [debouncedInputValue]);
|
||||
|
||||
useEffect(() => {
|
||||
// filter suggestions based on matching
|
||||
const searchFragment = debouncedInputValue.split("/").pop() || "";
|
||||
const filteredSuggestions = folders
|
||||
@ -73,7 +68,7 @@ export const SecretPathInput = ({
|
||||
.sort((a, b) => a.toLowerCase().localeCompare(b.toLowerCase()));
|
||||
|
||||
setSuggestions(filteredSuggestions);
|
||||
}, [debouncedInputValue]);
|
||||
}, [debouncedInputValue, folders]);
|
||||
|
||||
const handleSuggestionSelect = (selectedIndex: number) => {
|
||||
if (!suggestions[selectedIndex]) {
|
||||
@ -83,7 +78,7 @@ export const SecretPathInput = ({
|
||||
const validPaths = inputValue.split("/");
|
||||
validPaths.pop();
|
||||
|
||||
const newValue = `${validPaths.join("/")}/${suggestions[selectedIndex]}`;
|
||||
const newValue = `${validPaths.join("/")}/${suggestions[selectedIndex]}/`;
|
||||
onChange?.(newValue);
|
||||
setInputValue(newValue);
|
||||
setSecretPath(newValue);
|
||||
@ -116,7 +111,7 @@ export const SecretPathInput = ({
|
||||
|
||||
return (
|
||||
<Popover.Root
|
||||
open={suggestions.length > 0 && inputValue.length > 1}
|
||||
open={suggestions.length > 0 && isInputFocused}
|
||||
onOpenChange={() => {
|
||||
setHighlightedIndex(-1);
|
||||
}}
|
||||
@ -127,6 +122,8 @@ export const SecretPathInput = ({
|
||||
type="text"
|
||||
autoComplete="off"
|
||||
onKeyDown={handleKeyDown}
|
||||
onFocus={() => setIsInputFocus(true)}
|
||||
onBlur={() => setIsInputFocus(false)}
|
||||
value={inputValue}
|
||||
onChange={handleInputChange}
|
||||
className={containerClassName}
|
||||
|
@ -41,18 +41,22 @@ export const Select = forwardRef<HTMLButtonElement, SelectProps>(
|
||||
ref={ref}
|
||||
className={twMerge(
|
||||
`inline-flex items-center justify-between rounded-md
|
||||
bg-mineshaft-900 px-3 py-2 font-inter text-sm font-normal text-bunker-200 outline-none data-[placeholder]:text-mineshaft-200 focus:bg-mineshaft-700/80`,
|
||||
className
|
||||
bg-mineshaft-900 px-3 py-2 font-inter text-sm font-normal text-bunker-200 outline-none focus:bg-mineshaft-700/80 data-[placeholder]:text-mineshaft-200`,
|
||||
className,
|
||||
isDisabled && "cursor-not-allowed opacity-50"
|
||||
)}
|
||||
>
|
||||
<SelectPrimitive.Value placeholder={placeholder}>
|
||||
{props.icon ? <FontAwesomeIcon icon={props.icon} /> : placeholder}
|
||||
</SelectPrimitive.Value>
|
||||
{!isDisabled && (
|
||||
<SelectPrimitive.Icon className="ml-3">
|
||||
<FontAwesomeIcon icon={faCaretDown} size="sm" />
|
||||
</SelectPrimitive.Icon>
|
||||
)}
|
||||
|
||||
<SelectPrimitive.Icon className="ml-3">
|
||||
<FontAwesomeIcon
|
||||
icon={faCaretDown}
|
||||
size="sm"
|
||||
className={twMerge(isDisabled && "opacity-30")}
|
||||
/>
|
||||
</SelectPrimitive.Icon>
|
||||
</SelectPrimitive.Trigger>
|
||||
<SelectPrimitive.Portal>
|
||||
<SelectPrimitive.Content
|
||||
|
13
frontend/src/hooks/api/accessApproval/index.tsx
Normal file
13
frontend/src/hooks/api/accessApproval/index.tsx
Normal file
@ -0,0 +1,13 @@
|
||||
export {
|
||||
useCreateAccessApprovalPolicy,
|
||||
useCreateAccessRequest,
|
||||
useDeleteAccessApprovalPolicy,
|
||||
useReviewAccessRequest,
|
||||
useUpdateAccessApprovalPolicy
|
||||
} from "./mutation";
|
||||
export {
|
||||
useGetAccessApprovalPolicies,
|
||||
useGetAccessApprovalRequests,
|
||||
useGetAccessPolicyApprovalCount,
|
||||
useGetAccessRequestsCount
|
||||
} from "./queries";
|
123
frontend/src/hooks/api/accessApproval/mutation.tsx
Normal file
123
frontend/src/hooks/api/accessApproval/mutation.tsx
Normal file
@ -0,0 +1,123 @@
|
||||
import { packRules } from "@casl/ability/extra";
|
||||
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
|
||||
import { apiRequest } from "@app/config/request";
|
||||
|
||||
import { accessApprovalKeys } from "./queries";
|
||||
import {
|
||||
TAccessApproval,
|
||||
TCreateAccessPolicyDTO,
|
||||
TCreateAccessRequestDTO,
|
||||
TDeleteSecretPolicyDTO,
|
||||
TUpdateAccessPolicyDTO
|
||||
} from "./types";
|
||||
|
||||
export const useCreateAccessApprovalPolicy = () => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation<{}, {}, TCreateAccessPolicyDTO>({
|
||||
mutationFn: async ({ environment, projectSlug, approvals, approvers, name, secretPath }) => {
|
||||
const { data } = await apiRequest.post("/api/v1/access-approvals/policies", {
|
||||
environment,
|
||||
projectSlug,
|
||||
approvals,
|
||||
approvers,
|
||||
secretPath,
|
||||
name
|
||||
});
|
||||
return data;
|
||||
},
|
||||
onSuccess: (_, { projectSlug }) => {
|
||||
queryClient.invalidateQueries(accessApprovalKeys.getAccessApprovalPolicies(projectSlug));
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const useUpdateAccessApprovalPolicy = () => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation<{}, {}, TUpdateAccessPolicyDTO>({
|
||||
mutationFn: async ({ id, approvers, approvals, name, secretPath }) => {
|
||||
const { data } = await apiRequest.patch(`/api/v1/access-approvals/policies/${id}`, {
|
||||
approvals,
|
||||
approvers,
|
||||
secretPath,
|
||||
name
|
||||
});
|
||||
return data;
|
||||
},
|
||||
onSuccess: (_, { projectSlug }) => {
|
||||
queryClient.invalidateQueries(accessApprovalKeys.getAccessApprovalPolicies(projectSlug));
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const useDeleteAccessApprovalPolicy = () => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation<{}, {}, TDeleteSecretPolicyDTO>({
|
||||
mutationFn: async ({ id }) => {
|
||||
const { data } = await apiRequest.delete(`/api/v1/access-approvals/policies/${id}`);
|
||||
return data;
|
||||
},
|
||||
onSuccess: (_, { projectSlug }) => {
|
||||
queryClient.invalidateQueries(accessApprovalKeys.getAccessApprovalPolicies(projectSlug));
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const useCreateAccessRequest = () => {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation<{}, {}, TCreateAccessRequestDTO>({
|
||||
mutationFn: async ({ projectSlug, ...request }) => {
|
||||
const { data } = await apiRequest.post<TAccessApproval>(
|
||||
"/api/v1/access-approvals/requests",
|
||||
{
|
||||
...request,
|
||||
permissions: request.permissions ? packRules(request.permissions) : undefined
|
||||
},
|
||||
{
|
||||
params: {
|
||||
projectSlug
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
return data;
|
||||
},
|
||||
onSuccess: (_, { projectSlug }) => {
|
||||
queryClient.invalidateQueries(accessApprovalKeys.getAccessApprovalRequestCount(projectSlug));
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const useReviewAccessRequest = () => {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation<
|
||||
{},
|
||||
{},
|
||||
{
|
||||
requestId: string;
|
||||
status: "approved" | "rejected";
|
||||
projectSlug: string;
|
||||
envSlug?: string;
|
||||
requestedBy?: string;
|
||||
}
|
||||
>({
|
||||
mutationFn: async ({ requestId, status }) => {
|
||||
const { data } = await apiRequest.post(
|
||||
`/api/v1/access-approvals/requests/${requestId}/review`,
|
||||
{
|
||||
status
|
||||
}
|
||||
);
|
||||
return data;
|
||||
},
|
||||
onSuccess: (_, { projectSlug, envSlug, requestedBy }) => {
|
||||
queryClient.invalidateQueries(
|
||||
accessApprovalKeys.getAccessApprovalRequests(projectSlug, envSlug, requestedBy)
|
||||
);
|
||||
queryClient.invalidateQueries(accessApprovalKeys.getAccessApprovalRequestCount(projectSlug));
|
||||
}
|
||||
});
|
||||
};
|
159
frontend/src/hooks/api/accessApproval/queries.tsx
Normal file
159
frontend/src/hooks/api/accessApproval/queries.tsx
Normal file
@ -0,0 +1,159 @@
|
||||
import { PackRule, unpackRules } from "@casl/ability/extra";
|
||||
import { useQuery, UseQueryOptions } from "@tanstack/react-query";
|
||||
|
||||
import { apiRequest } from "@app/config/request";
|
||||
|
||||
import { TProjectPermission } from "../roles/types";
|
||||
import {
|
||||
TAccessApprovalPolicy,
|
||||
TAccessApprovalRequest,
|
||||
TAccessRequestCount,
|
||||
TGetAccessApprovalRequestsDTO,
|
||||
TGetAccessPolicyApprovalCountDTO
|
||||
} from "./types";
|
||||
|
||||
export const accessApprovalKeys = {
|
||||
getAccessApprovalPolicies: (projectSlug: string) =>
|
||||
[{ projectSlug }, "access-approval-policies"] as const,
|
||||
getAccessApprovalPolicyOfABoard: (workspaceId: string, environment: string) =>
|
||||
[{ workspaceId, environment }, "access-approval-policy"] as const,
|
||||
|
||||
getAccessApprovalRequests: (projectSlug: string, envSlug?: string, requestedBy?: string) =>
|
||||
[{ projectSlug, envSlug, requestedBy }, "access-approvals-requests"] as const,
|
||||
getAccessApprovalRequestCount: (projectSlug: string) =>
|
||||
[{ projectSlug }, "access-approval-request-count"] as const
|
||||
};
|
||||
|
||||
export const fetchPolicyApprovalCount = async ({
|
||||
projectSlug,
|
||||
envSlug
|
||||
}: TGetAccessPolicyApprovalCountDTO) => {
|
||||
const { data } = await apiRequest.get<{ count: number }>(
|
||||
"/api/v1/access-approvals/policies/count",
|
||||
{
|
||||
params: { projectSlug, envSlug }
|
||||
}
|
||||
);
|
||||
return data.count;
|
||||
};
|
||||
|
||||
export const useGetAccessPolicyApprovalCount = ({
|
||||
projectSlug,
|
||||
envSlug,
|
||||
options = {}
|
||||
}: TGetAccessPolicyApprovalCountDTO & {
|
||||
options?: UseQueryOptions<
|
||||
number,
|
||||
unknown,
|
||||
number,
|
||||
ReturnType<typeof accessApprovalKeys.getAccessApprovalPolicies>
|
||||
>;
|
||||
}) =>
|
||||
useQuery({
|
||||
queryFn: () => fetchPolicyApprovalCount({ projectSlug, envSlug }),
|
||||
...options,
|
||||
enabled: Boolean(projectSlug) && (options?.enabled ?? true)
|
||||
});
|
||||
|
||||
const fetchApprovalPolicies = async ({ projectSlug }: TGetAccessApprovalRequestsDTO) => {
|
||||
const { data } = await apiRequest.get<{ approvals: TAccessApprovalPolicy[] }>(
|
||||
"/api/v1/access-approvals/policies",
|
||||
{ params: { projectSlug } }
|
||||
);
|
||||
return data.approvals;
|
||||
};
|
||||
|
||||
const fetchApprovalRequests = async ({
|
||||
projectSlug,
|
||||
envSlug,
|
||||
authorProjectMembershipId
|
||||
}: TGetAccessApprovalRequestsDTO) => {
|
||||
const { data } = await apiRequest.get<{ requests: TAccessApprovalRequest[] }>(
|
||||
"/api/v1/access-approvals/requests",
|
||||
{ params: { projectSlug, envSlug, authorProjectMembershipId } }
|
||||
);
|
||||
|
||||
return data.requests.map((request) => ({
|
||||
...request,
|
||||
|
||||
privilege: request.privilege
|
||||
? {
|
||||
...request.privilege,
|
||||
permissions: unpackRules(
|
||||
request.privilege.permissions as unknown as PackRule<TProjectPermission>[]
|
||||
)
|
||||
}
|
||||
: null,
|
||||
permissions: unpackRules(request.permissions as unknown as PackRule<TProjectPermission>[])
|
||||
}));
|
||||
};
|
||||
|
||||
const fetchAccessRequestsCount = async (projectSlug: string) => {
|
||||
const { data } = await apiRequest.get<TAccessRequestCount>(
|
||||
"/api/v1/access-approvals/requests/count",
|
||||
{ params: { projectSlug } }
|
||||
);
|
||||
return data;
|
||||
};
|
||||
|
||||
export const useGetAccessRequestsCount = ({
|
||||
projectSlug,
|
||||
options = {}
|
||||
}: TGetAccessApprovalRequestsDTO & {
|
||||
options?: UseQueryOptions<
|
||||
TAccessRequestCount,
|
||||
unknown,
|
||||
{ pendingCount: number; finalizedCount: number },
|
||||
ReturnType<typeof accessApprovalKeys.getAccessApprovalRequestCount>
|
||||
>;
|
||||
}) =>
|
||||
useQuery({
|
||||
queryKey: accessApprovalKeys.getAccessApprovalRequestCount(projectSlug),
|
||||
queryFn: () => fetchAccessRequestsCount(projectSlug),
|
||||
...options,
|
||||
enabled: Boolean(projectSlug) && (options?.enabled ?? true)
|
||||
});
|
||||
|
||||
export const useGetAccessApprovalPolicies = ({
|
||||
projectSlug,
|
||||
envSlug,
|
||||
authorProjectMembershipId,
|
||||
options = {}
|
||||
}: TGetAccessApprovalRequestsDTO & {
|
||||
options?: UseQueryOptions<
|
||||
TAccessApprovalPolicy[],
|
||||
unknown,
|
||||
TAccessApprovalPolicy[],
|
||||
ReturnType<typeof accessApprovalKeys.getAccessApprovalPolicies>
|
||||
>;
|
||||
}) =>
|
||||
useQuery({
|
||||
queryKey: accessApprovalKeys.getAccessApprovalPolicies(projectSlug),
|
||||
queryFn: () => fetchApprovalPolicies({ projectSlug, envSlug, authorProjectMembershipId }),
|
||||
...options,
|
||||
enabled: Boolean(projectSlug) && (options?.enabled ?? true)
|
||||
});
|
||||
|
||||
export const useGetAccessApprovalRequests = ({
|
||||
projectSlug,
|
||||
envSlug,
|
||||
authorProjectMembershipId,
|
||||
options = {}
|
||||
}: TGetAccessApprovalRequestsDTO & {
|
||||
options?: UseQueryOptions<
|
||||
TAccessApprovalRequest[],
|
||||
unknown,
|
||||
TAccessApprovalRequest[],
|
||||
ReturnType<typeof accessApprovalKeys.getAccessApprovalRequests>
|
||||
>;
|
||||
}) =>
|
||||
useQuery({
|
||||
queryKey: accessApprovalKeys.getAccessApprovalRequests(
|
||||
projectSlug,
|
||||
envSlug,
|
||||
authorProjectMembershipId
|
||||
),
|
||||
queryFn: () => fetchApprovalRequests({ projectSlug, envSlug, authorProjectMembershipId }),
|
||||
...options,
|
||||
enabled: Boolean(projectSlug) && (options?.enabled ?? true)
|
||||
});
|
139
frontend/src/hooks/api/accessApproval/types.ts
Normal file
139
frontend/src/hooks/api/accessApproval/types.ts
Normal file
@ -0,0 +1,139 @@
|
||||
import { TProjectPermission } from "../roles/types";
|
||||
import { WorkspaceEnv } from "../workspace/types";
|
||||
|
||||
export type TAccessApprovalPolicy = {
|
||||
id: string;
|
||||
name: string;
|
||||
approvals: number;
|
||||
secretPath: string;
|
||||
envId: string;
|
||||
workspace: string;
|
||||
environment: WorkspaceEnv;
|
||||
projectId: string;
|
||||
approvers: string[];
|
||||
};
|
||||
|
||||
export type TAccessApprovalRequest = {
|
||||
id: string;
|
||||
policyId: string;
|
||||
privilegeId: string | null;
|
||||
requestedBy: string;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
isTemporary: boolean;
|
||||
temporaryRange: string | null | undefined;
|
||||
|
||||
permissions: TProjectPermission[] | null;
|
||||
|
||||
// Computed
|
||||
environmentName: string;
|
||||
isApproved: boolean;
|
||||
|
||||
privilege: {
|
||||
membershipId: string;
|
||||
isTemporary: boolean;
|
||||
temporaryMode?: string | null;
|
||||
temporaryRange?: string | null;
|
||||
temporaryAccessStartTime?: Date | null;
|
||||
temporaryAccessEndTime?: Date | null;
|
||||
permissions: TProjectPermission[];
|
||||
isApproved: boolean;
|
||||
} | null;
|
||||
|
||||
policy: {
|
||||
id: string;
|
||||
name: string;
|
||||
approvals: number;
|
||||
approvers: string[];
|
||||
secretPath?: string | null;
|
||||
envId: string;
|
||||
};
|
||||
|
||||
reviewers: {
|
||||
member: string;
|
||||
status: string;
|
||||
}[];
|
||||
};
|
||||
|
||||
export type TAccessApproval = {
|
||||
id: string;
|
||||
policyId: string;
|
||||
privilegeId: string;
|
||||
requestedBy: string;
|
||||
};
|
||||
|
||||
export type TAccessRequestCount = {
|
||||
pendingCount: number;
|
||||
finalizedCount: number;
|
||||
};
|
||||
|
||||
export type TProjectUserPrivilege = {
|
||||
projectMembershipId: string;
|
||||
slug: string;
|
||||
id: string;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
permissions?: TProjectPermission[];
|
||||
} & (
|
||||
| {
|
||||
isTemporary: true;
|
||||
temporaryMode: string;
|
||||
temporaryRange: string;
|
||||
temporaryAccessStartTime: string;
|
||||
temporaryAccessEndTime?: string;
|
||||
}
|
||||
| {
|
||||
isTemporary: false;
|
||||
temporaryMode?: null;
|
||||
temporaryRange?: null;
|
||||
temporaryAccessStartTime?: null;
|
||||
temporaryAccessEndTime?: null;
|
||||
}
|
||||
);
|
||||
|
||||
export type TCreateAccessRequestDTO = {
|
||||
projectSlug: string;
|
||||
} & Omit<TProjectUserPrivilege, "id" | "createdAt" | "updatedAt" | "slug" | "projectMembershipId">;
|
||||
|
||||
export type TGetAccessApprovalRequestsDTO = {
|
||||
projectSlug: string;
|
||||
envSlug?: string;
|
||||
authorProjectMembershipId?: string;
|
||||
};
|
||||
|
||||
export type TGetAccessPolicyApprovalCountDTO = {
|
||||
projectSlug: string;
|
||||
envSlug: string;
|
||||
};
|
||||
|
||||
export type TGetSecretApprovalPolicyOfBoardDTO = {
|
||||
workspaceId: string;
|
||||
environment: string;
|
||||
secretPath: string;
|
||||
};
|
||||
|
||||
export type TCreateAccessPolicyDTO = {
|
||||
projectSlug: string;
|
||||
name?: string;
|
||||
environment: string;
|
||||
approvers?: string[];
|
||||
approvals?: number;
|
||||
secretPath?: string;
|
||||
};
|
||||
|
||||
export type TUpdateAccessPolicyDTO = {
|
||||
id: string;
|
||||
name?: string;
|
||||
approvers?: string[];
|
||||
secretPath?: string;
|
||||
environment?: string;
|
||||
approvals?: number;
|
||||
// for invalidating list
|
||||
projectSlug: string;
|
||||
};
|
||||
|
||||
export type TDeleteSecretPolicyDTO = {
|
||||
id: string;
|
||||
// for invalidating list
|
||||
projectSlug: string;
|
||||
};
|
@ -1,5 +1,6 @@
|
||||
import { IdentityAuthMethod } from "./enums";
|
||||
|
||||
export const identityAuthToNameMap: { [I in IdentityAuthMethod]: string } = {
|
||||
[IdentityAuthMethod.UNIVERSAL_AUTH]: "Universal Auth"
|
||||
[IdentityAuthMethod.UNIVERSAL_AUTH]: "Universal Auth",
|
||||
[IdentityAuthMethod.AWS_AUTH]: "AWS Auth"
|
||||
};
|
||||
|
@ -1,3 +1,4 @@
|
||||
export enum IdentityAuthMethod {
|
||||
UNIVERSAL_AUTH = "universal-auth"
|
||||
UNIVERSAL_AUTH = "universal-auth",
|
||||
AWS_AUTH = "aws-auth"
|
||||
}
|
||||
|
@ -1,12 +1,18 @@
|
||||
export { identityAuthToNameMap } from "./constants";
|
||||
export { IdentityAuthMethod } from "./enums";
|
||||
export {
|
||||
useAddIdentityAwsAuth,
|
||||
useAddIdentityUniversalAuth,
|
||||
useCreateIdentity,
|
||||
useCreateIdentityUniversalAuthClientSecret,
|
||||
useDeleteIdentity,
|
||||
useRevokeIdentityUniversalAuthClientSecret,
|
||||
useUpdateIdentity,
|
||||
useUpdateIdentityAwsAuth,
|
||||
useUpdateIdentityUniversalAuth
|
||||
} from "./mutations";
|
||||
export { useGetIdentityUniversalAuth, useGetIdentityUniversalAuthClientSecrets } from "./queries";
|
||||
export {
|
||||
useGetIdentityAwsAuth,
|
||||
useGetIdentityUniversalAuth,
|
||||
useGetIdentityUniversalAuthClientSecrets
|
||||
} from "./queries";
|
||||
|
@ -5,6 +5,7 @@ import { apiRequest } from "@app/config/request";
|
||||
import { organizationKeys } from "../organization/queries";
|
||||
import { identitiesKeys } from "./queries";
|
||||
import {
|
||||
AddIdentityAwsAuthDTO,
|
||||
AddIdentityUniversalAuthDTO,
|
||||
ClientSecretData,
|
||||
CreateIdentityDTO,
|
||||
@ -13,7 +14,9 @@ import {
|
||||
DeleteIdentityDTO,
|
||||
DeleteIdentityUniversalAuthClientSecretDTO,
|
||||
Identity,
|
||||
IdentityAwsAuth,
|
||||
IdentityUniversalAuth,
|
||||
UpdateIdentityAwsAuthDTO,
|
||||
UpdateIdentityDTO,
|
||||
UpdateIdentityUniversalAuthDTO
|
||||
} from "./types";
|
||||
@ -169,3 +172,74 @@ export const useRevokeIdentityUniversalAuthClientSecret = () => {
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const useAddIdentityAwsAuth = () => {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation<IdentityAwsAuth, {}, AddIdentityAwsAuthDTO>({
|
||||
mutationFn: async ({
|
||||
identityId,
|
||||
stsEndpoint,
|
||||
allowedPrincipalArns,
|
||||
allowedAccountIds,
|
||||
accessTokenTTL,
|
||||
accessTokenMaxTTL,
|
||||
accessTokenNumUsesLimit,
|
||||
accessTokenTrustedIps
|
||||
}) => {
|
||||
const {
|
||||
data: { identityAwsAuth }
|
||||
} = await apiRequest.post<{ identityAwsAuth: IdentityAwsAuth }>(
|
||||
`/api/v1/auth/aws-auth/identities/${identityId}`,
|
||||
{
|
||||
stsEndpoint,
|
||||
allowedPrincipalArns,
|
||||
allowedAccountIds,
|
||||
accessTokenTTL,
|
||||
accessTokenMaxTTL,
|
||||
accessTokenNumUsesLimit,
|
||||
accessTokenTrustedIps
|
||||
}
|
||||
);
|
||||
|
||||
return identityAwsAuth;
|
||||
},
|
||||
onSuccess: (_, { organizationId }) => {
|
||||
queryClient.invalidateQueries(organizationKeys.getOrgIdentityMemberships(organizationId));
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const useUpdateIdentityAwsAuth = () => {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation<IdentityAwsAuth, {}, UpdateIdentityAwsAuthDTO>({
|
||||
mutationFn: async ({
|
||||
identityId,
|
||||
stsEndpoint,
|
||||
allowedPrincipalArns,
|
||||
allowedAccountIds,
|
||||
accessTokenTTL,
|
||||
accessTokenMaxTTL,
|
||||
accessTokenNumUsesLimit,
|
||||
accessTokenTrustedIps
|
||||
}) => {
|
||||
const {
|
||||
data: { identityAwsAuth }
|
||||
} = await apiRequest.patch<{ identityAwsAuth: IdentityAwsAuth }>(
|
||||
`/api/v1/auth/aws-auth/identities/${identityId}`,
|
||||
{
|
||||
stsEndpoint,
|
||||
allowedPrincipalArns,
|
||||
allowedAccountIds,
|
||||
accessTokenTTL,
|
||||
accessTokenMaxTTL,
|
||||
accessTokenNumUsesLimit,
|
||||
accessTokenTrustedIps
|
||||
}
|
||||
);
|
||||
return identityAwsAuth;
|
||||
},
|
||||
onSuccess: (_, { organizationId }) => {
|
||||
queryClient.invalidateQueries(organizationKeys.getOrgIdentityMemberships(organizationId));
|
||||
}
|
||||
});
|
||||
};
|
||||
|
@ -2,27 +2,26 @@ import { useQuery } from "@tanstack/react-query";
|
||||
|
||||
import { apiRequest } from "@app/config/request";
|
||||
|
||||
import { ClientSecretData, IdentityUniversalAuth } from "./types";
|
||||
import { ClientSecretData, IdentityAwsAuth, IdentityUniversalAuth } from "./types";
|
||||
|
||||
export const identitiesKeys = {
|
||||
getIdentityUniversalAuth: (identityId: string) =>
|
||||
[{ identityId }, "identity-universal-auth"] as const,
|
||||
getIdentityUniversalAuthClientSecrets: (identityId: string) =>
|
||||
[{ identityId }, "identity-universal-auth-client-secrets"] as const
|
||||
[{ identityId }, "identity-universal-auth-client-secrets"] as const,
|
||||
getIdentityAwsAuth: (identityId: string) => [{ identityId }, "identity-aws-auth"] as const
|
||||
};
|
||||
|
||||
export const useGetIdentityUniversalAuth = (identityId: string) => {
|
||||
return useQuery({
|
||||
enabled: Boolean(identityId),
|
||||
queryKey: identitiesKeys.getIdentityUniversalAuth(identityId),
|
||||
queryFn: async () => {
|
||||
if (identityId === "") throw new Error("Identity ID is required");
|
||||
|
||||
const {
|
||||
data: { identityUniversalAuth }
|
||||
} = await apiRequest.get<{ identityUniversalAuth: IdentityUniversalAuth }>(
|
||||
`/api/v1/auth/universal-auth/identities/${identityId}`
|
||||
);
|
||||
|
||||
return identityUniversalAuth;
|
||||
}
|
||||
});
|
||||
@ -30,17 +29,30 @@ export const useGetIdentityUniversalAuth = (identityId: string) => {
|
||||
|
||||
export const useGetIdentityUniversalAuthClientSecrets = (identityId: string) => {
|
||||
return useQuery({
|
||||
enabled: Boolean(identityId),
|
||||
queryKey: identitiesKeys.getIdentityUniversalAuthClientSecrets(identityId),
|
||||
queryFn: async () => {
|
||||
if (identityId === "") return [];
|
||||
|
||||
const {
|
||||
data: { clientSecretData }
|
||||
} = await apiRequest.get<{ clientSecretData: ClientSecretData[] }>(
|
||||
`/api/v1/auth/universal-auth/identities/${identityId}/client-secrets`
|
||||
);
|
||||
|
||||
return clientSecretData;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const useGetIdentityAwsAuth = (identityId: string) => {
|
||||
return useQuery({
|
||||
enabled: Boolean(identityId),
|
||||
queryKey: identitiesKeys.getIdentityAwsAuth(identityId),
|
||||
queryFn: async () => {
|
||||
const {
|
||||
data: { identityAwsAuth }
|
||||
} = await apiRequest.get<{ identityAwsAuth: IdentityAwsAuth }>(
|
||||
`/api/v1/auth/aws-auth/identities/${identityId}`
|
||||
);
|
||||
return identityAwsAuth;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
@ -38,19 +38,19 @@ export type IdentityMembership = {
|
||||
customRoleSlug: string;
|
||||
} & (
|
||||
| {
|
||||
isTemporary: false;
|
||||
temporaryRange: null;
|
||||
temporaryMode: null;
|
||||
temporaryAccessEndTime: null;
|
||||
temporaryAccessStartTime: null;
|
||||
}
|
||||
isTemporary: false;
|
||||
temporaryRange: null;
|
||||
temporaryMode: null;
|
||||
temporaryAccessEndTime: null;
|
||||
temporaryAccessStartTime: null;
|
||||
}
|
||||
| {
|
||||
isTemporary: true;
|
||||
temporaryRange: string;
|
||||
temporaryMode: string;
|
||||
temporaryAccessEndTime: string;
|
||||
temporaryAccessStartTime: string;
|
||||
}
|
||||
isTemporary: true;
|
||||
temporaryRange: string;
|
||||
temporaryMode: string;
|
||||
temporaryAccessEndTime: string;
|
||||
temporaryAccessStartTime: string;
|
||||
}
|
||||
)
|
||||
>;
|
||||
createdAt: string;
|
||||
@ -113,6 +113,46 @@ export type UpdateIdentityUniversalAuthDTO = {
|
||||
}[];
|
||||
};
|
||||
|
||||
export type IdentityAwsAuth = {
|
||||
identityId: string;
|
||||
type: "iam";
|
||||
stsEndpoint: string;
|
||||
allowedPrincipalArns: string;
|
||||
allowedAccountIds: string;
|
||||
accessTokenTTL: number;
|
||||
accessTokenMaxTTL: number;
|
||||
accessTokenNumUsesLimit: number;
|
||||
accessTokenTrustedIps: IdentityTrustedIp[];
|
||||
};
|
||||
|
||||
export type AddIdentityAwsAuthDTO = {
|
||||
organizationId: string;
|
||||
identityId: string;
|
||||
stsEndpoint: string;
|
||||
allowedPrincipalArns: string;
|
||||
allowedAccountIds: string;
|
||||
accessTokenTTL: number;
|
||||
accessTokenMaxTTL: number;
|
||||
accessTokenNumUsesLimit: number;
|
||||
accessTokenTrustedIps: {
|
||||
ipAddress: string;
|
||||
}[];
|
||||
};
|
||||
|
||||
export type UpdateIdentityAwsAuthDTO = {
|
||||
organizationId: string;
|
||||
identityId: string;
|
||||
stsEndpoint?: string;
|
||||
allowedPrincipalArns?: string;
|
||||
allowedAccountIds?: string;
|
||||
accessTokenTTL?: number;
|
||||
accessTokenMaxTTL?: number;
|
||||
accessTokenNumUsesLimit?: number;
|
||||
accessTokenTrustedIps?: {
|
||||
ipAddress: string;
|
||||
}[];
|
||||
};
|
||||
|
||||
export type CreateIdentityUniversalAuthClientSecretDTO = {
|
||||
identityId: string;
|
||||
description?: string;
|
||||
|
@ -1,9 +1,7 @@
|
||||
import { PackRule, unpackRules } from "@casl/ability/extra";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
|
||||
import { apiRequest } from "@app/config/request";
|
||||
|
||||
import { TProjectPermission } from "../roles/types";
|
||||
import {
|
||||
TGetIdentityProejctPrivilegeDetails as TGetIdentityProjectPrivilegeDetails,
|
||||
TIdentityProjectPrivilege,
|
||||
@ -36,17 +34,14 @@ export const useGetIdentityProjectPrivilegeDetails = ({
|
||||
const {
|
||||
data: { privilege }
|
||||
} = await apiRequest.get<{
|
||||
privilege: Omit<TIdentityProjectPrivilege, "permissions"> & { permissions: unknown };
|
||||
privilege: TIdentityProjectPrivilege;
|
||||
}>(`/api/v1/additional-privilege/identity/${privilegeSlug}`, {
|
||||
params: {
|
||||
identityId,
|
||||
projectSlug
|
||||
}
|
||||
});
|
||||
return {
|
||||
...privilege,
|
||||
permissions: unpackRules(privilege.permissions as PackRule<TProjectPermission>[])
|
||||
};
|
||||
return privilege;
|
||||
}
|
||||
});
|
||||
};
|
||||
@ -62,16 +57,11 @@ export const useListIdentityProjectPrivileges = ({
|
||||
const {
|
||||
data: { privileges }
|
||||
} = await apiRequest.get<{
|
||||
privileges: Array<
|
||||
Omit<TIdentityProjectPrivilege, "permissions"> & { permissions: unknown }
|
||||
>;
|
||||
privileges: Array<TIdentityProjectPrivilege>;
|
||||
}>("/api/v1/additional-privilege/identity", {
|
||||
params: { identityId, projectSlug, unpacked: false }
|
||||
params: { identityId, projectSlug }
|
||||
});
|
||||
return privileges.map((el) => ({
|
||||
...el,
|
||||
permissions: unpackRules(el.permissions as PackRule<TProjectPermission>[])
|
||||
}));
|
||||
return privileges;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
@ -1,3 +1,4 @@
|
||||
export * from "./accessApproval";
|
||||
export * from "./admin";
|
||||
export * from "./apiKeys";
|
||||
export * from "./auditLogs";
|
||||
|
@ -68,6 +68,7 @@ export const useCreateIntegration = () => {
|
||||
value: string;
|
||||
}[];
|
||||
kmsKeyId?: string;
|
||||
shouldDisableDelete?: boolean;
|
||||
};
|
||||
}) => {
|
||||
const {
|
||||
|
@ -41,7 +41,7 @@ export type TPermission = {
|
||||
export type TProjectPermission = {
|
||||
conditions?: Record<string, any>;
|
||||
action: string;
|
||||
subject: [string];
|
||||
subject: string | string[];
|
||||
};
|
||||
|
||||
export type TGetUserOrgPermissionsDTO = {
|
||||
|
@ -79,7 +79,7 @@ export const useGetFoldersByEnv = ({
|
||||
});
|
||||
});
|
||||
return [...names];
|
||||
}, [(folders || []).map((folder) => folder.data)]);
|
||||
}, [...(folders || []).map((folder) => folder.data)]);
|
||||
|
||||
const isFolderPresentInEnv = useCallback(
|
||||
(name: string, env: string) => {
|
||||
@ -91,10 +91,24 @@ export const useGetFoldersByEnv = ({
|
||||
}
|
||||
return false;
|
||||
},
|
||||
[...(folders || []).map((folder) => folder.data)]
|
||||
);
|
||||
|
||||
const getFolderByNameAndEnv = useCallback(
|
||||
(name: string, env: string) => {
|
||||
const selectedEnvIndex = environments.indexOf(env);
|
||||
if (selectedEnvIndex !== -1) {
|
||||
return folders?.[selectedEnvIndex]?.data?.find(
|
||||
({ name: folderName }) => folderName === name
|
||||
);
|
||||
}
|
||||
|
||||
return undefined;
|
||||
},
|
||||
[(folders || []).map((folder) => folder.data)]
|
||||
);
|
||||
|
||||
return { folders, folderNames, isFolderPresentInEnv };
|
||||
return { folders, folderNames, isFolderPresentInEnv, getFolderByNameAndEnv };
|
||||
};
|
||||
|
||||
export const useCreateFolder = () => {
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { ZodIssue } from "zod";
|
||||
|
||||
export type { TAccessApprovalPolicy } from "./accessApproval/types";
|
||||
export type { TAuditLogStream } from "./auditLogStreams/types";
|
||||
export type { GetAuthTokenAPI } from "./auth/types";
|
||||
export type { IncidentContact } from "./incidentContacts/types";
|
||||
@ -49,13 +50,13 @@ export enum ApiErrorTypes {
|
||||
|
||||
export type TApiErrors =
|
||||
| {
|
||||
error: ApiErrorTypes.ValidationError;
|
||||
message: ZodIssue[];
|
||||
statusCode: 403;
|
||||
}
|
||||
error: ApiErrorTypes.ValidationError;
|
||||
message: ZodIssue[];
|
||||
statusCode: 403;
|
||||
}
|
||||
| { error: ApiErrorTypes.ForbiddenError; message: string; statusCode: 401 }
|
||||
| {
|
||||
statusCode: 400;
|
||||
message: string;
|
||||
error: ApiErrorTypes.BadRequestError;
|
||||
};
|
||||
statusCode: 400;
|
||||
message: string;
|
||||
error: ApiErrorTypes.BadRequestError;
|
||||
};
|
||||
|
@ -5,7 +5,7 @@
|
||||
/* eslint-disable no-var */
|
||||
/* eslint-disable func-names */
|
||||
|
||||
import { useEffect } from "react";
|
||||
import { useEffect, useMemo } from "react";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import Image from "next/image";
|
||||
@ -64,6 +64,7 @@ import {
|
||||
fetchOrgUsers,
|
||||
useAddUserToWsNonE2EE,
|
||||
useCreateWorkspace,
|
||||
useGetAccessRequestsCount,
|
||||
useGetOrgTrialUrl,
|
||||
useGetSecretApprovalRequestCount,
|
||||
useGetUserAction,
|
||||
@ -115,7 +116,7 @@ type TAddProjectFormData = yup.InferType<typeof formSchema>;
|
||||
|
||||
export const AppLayout = ({ children }: LayoutProps) => {
|
||||
const router = useRouter();
|
||||
|
||||
|
||||
const { mutateAsync } = useGetOrgTrialUrl();
|
||||
|
||||
const { workspaces, currentWorkspace } = useWorkspace();
|
||||
@ -124,9 +125,15 @@ export const AppLayout = ({ children }: LayoutProps) => {
|
||||
const { user } = useUser();
|
||||
const { subscription } = useSubscription();
|
||||
const workspaceId = currentWorkspace?.id || "";
|
||||
const projectSlug = currentWorkspace?.slug || "";
|
||||
const { data: updateClosed } = useGetUserAction("december_update_closed");
|
||||
|
||||
const { data: secretApprovalReqCount } = useGetSecretApprovalRequestCount({ workspaceId });
|
||||
const { data: accessApprovalRequestCount } = useGetAccessRequestsCount({ projectSlug });
|
||||
|
||||
const pendingRequestsCount = useMemo(() => {
|
||||
return (secretApprovalReqCount?.open || 0) + (accessApprovalRequestCount?.pendingCount || 0);
|
||||
}, [secretApprovalReqCount, accessApprovalRequestCount]);
|
||||
|
||||
const isAddingProjectsAllowed = subscription?.workspaceLimit
|
||||
? subscription.workspacesUsed < subscription.workspaceLimit
|
||||
@ -554,10 +561,13 @@ export const AppLayout = ({ children }: LayoutProps) => {
|
||||
}
|
||||
icon="system-outline-189-domain-verification"
|
||||
>
|
||||
Secret Approvals
|
||||
{Boolean(secretApprovalReqCount?.open) && (
|
||||
Approvals
|
||||
{Boolean(
|
||||
secretApprovalReqCount?.open ||
|
||||
accessApprovalRequestCount?.pendingCount
|
||||
) && (
|
||||
<span className="ml-2 rounded border border-primary-400 bg-primary-600 py-0.5 px-1 text-xs font-semibold text-black">
|
||||
{secretApprovalReqCount?.open}
|
||||
{pendingRequestsCount}
|
||||
</span>
|
||||
)}
|
||||
</MenuItem>
|
||||
|
15
frontend/src/lib/fn/debounce.ts
Normal file
15
frontend/src/lib/fn/debounce.ts
Normal file
@ -0,0 +1,15 @@
|
||||
export const debounce = <F extends (...args: any[]) => any>(
|
||||
func: F,
|
||||
delay: number
|
||||
): ((...args: Parameters<F>) => void) => {
|
||||
let timeoutId: ReturnType<typeof setTimeout> | null;
|
||||
return function debounced(...args: Parameters<F>) {
|
||||
if (timeoutId) {
|
||||
clearTimeout(timeoutId);
|
||||
}
|
||||
timeoutId = setTimeout(() => {
|
||||
func(...args);
|
||||
timeoutId = null;
|
||||
}, delay);
|
||||
};
|
||||
};
|
@ -89,6 +89,7 @@ export default function AWSParameterStoreCreateIntegrationPage() {
|
||||
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [shouldTag, setShouldTag] = useState(false);
|
||||
const [shouldDisableDelete, setShouldDisableDelete] = useState(false);
|
||||
const [tagKey, setTagKey] = useState("");
|
||||
const [tagValue, setTagValue] = useState("");
|
||||
const [kmsKeyId, setKmsKeyId] = useState("");
|
||||
@ -144,7 +145,8 @@ export default function AWSParameterStoreCreateIntegrationPage() {
|
||||
]
|
||||
}
|
||||
: {}),
|
||||
...(kmsKeyId && { kmsKeyId })
|
||||
...(kmsKeyId && { kmsKeyId }),
|
||||
...(shouldDisableDelete && { shouldDisableDelete })
|
||||
}
|
||||
});
|
||||
|
||||
@ -273,6 +275,15 @@ export default function AWSParameterStoreCreateIntegrationPage() {
|
||||
exit={{ opacity: 0, translateX: 30 }}
|
||||
>
|
||||
<div className="mt-2 ml-1">
|
||||
<Switch
|
||||
id="delete-aws"
|
||||
onCheckedChange={() => setShouldDisableDelete(!shouldDisableDelete)}
|
||||
isChecked={shouldDisableDelete}
|
||||
>
|
||||
Disable deleting secrets in AWS Parameter Store
|
||||
</Switch>
|
||||
</div>
|
||||
<div className="mt-4 ml-1">
|
||||
<Switch
|
||||
id="tag-aws"
|
||||
onCheckedChange={() => setShouldTag(!shouldTag)}
|
||||
|
@ -35,8 +35,6 @@ export default function LoginPage() {
|
||||
const selectOrg = useSelectOrganization();
|
||||
const { user, isLoading: userLoading } = useUser();
|
||||
|
||||
|
||||
|
||||
const queryParams = new URLSearchParams(window.location.search);
|
||||
|
||||
const logout = useLogoutUser(true);
|
||||
@ -153,7 +151,7 @@ export default function LoginPage() {
|
||||
|
||||
<div className="space-y-1">
|
||||
<p className="text-md text-center text-gray-500">
|
||||
You‘re currently logged in as <strong>{user.email}</strong>
|
||||
You‘re currently logged in as <strong>{user.username}</strong>
|
||||
</p>
|
||||
<p className="text-md text-center text-gray-500">
|
||||
Not you?{" "}
|
||||
|
@ -1,27 +0,0 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import Head from "next/head";
|
||||
|
||||
import { SecretMainPage } from "@app/views/SecretMainPage";
|
||||
|
||||
const Dashboard = () => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
<title>{t("common.head-title", { title: t("dashboard.title") })}</title>
|
||||
<link rel="icon" href="/infisical.ico" />
|
||||
<meta property="og:image" content="/images/message.png" />
|
||||
<meta property="og:title" content={String(t("dashboard.og-title"))} />
|
||||
<meta name="og:description" content={String(t("dashboard.og-description"))} />
|
||||
</Head>
|
||||
<div className="h-full">
|
||||
<SecretMainPage />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Dashboard;
|
||||
|
||||
Dashboard.requireAuth = true;
|
@ -1,3 +1,4 @@
|
||||
import { useEffect } from "react";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
import { yupResolver } from "@hookform/resolvers/yup";
|
||||
import * as yup from "yup";
|
||||
@ -13,6 +14,7 @@ import {
|
||||
import { IdentityAuthMethod } from "@app/hooks/api/identities";
|
||||
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||
|
||||
import { IdentityAwsAuthForm } from "./IdentityAwsAuthForm";
|
||||
import { IdentityUniversalAuthForm } from "./IdentityUniversalAuthForm";
|
||||
|
||||
type Props = {
|
||||
@ -24,22 +26,25 @@ type Props = {
|
||||
) => void;
|
||||
};
|
||||
|
||||
const identityAuthMethods = [{ label: "Universal Auth", value: IdentityAuthMethod.UNIVERSAL_AUTH }];
|
||||
const identityAuthMethods = [
|
||||
{ label: "Universal Auth", value: IdentityAuthMethod.UNIVERSAL_AUTH },
|
||||
{ label: "AWS Auth", value: IdentityAuthMethod.AWS_AUTH }
|
||||
];
|
||||
|
||||
const schema = yup
|
||||
.object({
|
||||
authMethod: yup.string().required("Auth method is required") // TODO: better enforcement here
|
||||
authMethod: yup.string().required("Auth method is required")
|
||||
})
|
||||
.required();
|
||||
|
||||
export type FormData = yup.InferType<typeof schema>;
|
||||
|
||||
export const IdentityAuthMethodModal = ({ popUp, handlePopUpOpen, handlePopUpToggle }: Props) => {
|
||||
const {
|
||||
control
|
||||
// watch,
|
||||
} = useForm<FormData>({
|
||||
resolver: yupResolver(schema)
|
||||
const { control, watch, setValue } = useForm<FormData>({
|
||||
resolver: yupResolver(schema),
|
||||
defaultValues: {
|
||||
authMethod: IdentityAuthMethod.UNIVERSAL_AUTH
|
||||
}
|
||||
});
|
||||
|
||||
const identityAuthMethodData = popUp?.identityAuthMethod?.data as {
|
||||
@ -48,16 +53,41 @@ export const IdentityAuthMethodModal = ({ popUp, handlePopUpOpen, handlePopUpTog
|
||||
authMethod?: IdentityAuthMethod;
|
||||
};
|
||||
|
||||
// const authMethod = watch("authMethod");
|
||||
useEffect(() => {
|
||||
if (identityAuthMethodData?.authMethod) {
|
||||
setValue("authMethod", identityAuthMethodData.authMethod);
|
||||
return;
|
||||
}
|
||||
|
||||
setValue("authMethod", IdentityAuthMethod.UNIVERSAL_AUTH);
|
||||
}, [identityAuthMethodData?.authMethod]);
|
||||
|
||||
const authMethod = watch("authMethod");
|
||||
|
||||
const renderIdentityAuthForm = () => {
|
||||
return (
|
||||
<IdentityUniversalAuthForm
|
||||
handlePopUpOpen={handlePopUpOpen}
|
||||
handlePopUpToggle={handlePopUpToggle}
|
||||
identityAuthMethodData={identityAuthMethodData}
|
||||
/>
|
||||
);
|
||||
switch (identityAuthMethodData?.authMethod ?? authMethod) {
|
||||
case IdentityAuthMethod.AWS_AUTH: {
|
||||
return (
|
||||
<IdentityAwsAuthForm
|
||||
handlePopUpOpen={handlePopUpOpen}
|
||||
handlePopUpToggle={handlePopUpToggle}
|
||||
identityAuthMethodData={identityAuthMethodData}
|
||||
/>
|
||||
);
|
||||
}
|
||||
case IdentityAuthMethod.UNIVERSAL_AUTH: {
|
||||
return (
|
||||
<IdentityUniversalAuthForm
|
||||
handlePopUpOpen={handlePopUpOpen}
|
||||
handlePopUpToggle={handlePopUpToggle}
|
||||
identityAuthMethodData={identityAuthMethodData}
|
||||
/>
|
||||
);
|
||||
}
|
||||
default: {
|
||||
return <div />;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
@ -83,6 +113,7 @@ export const IdentityAuthMethodModal = ({ popUp, handlePopUpOpen, handlePopUpTog
|
||||
{...field}
|
||||
onValueChange={(e) => onChange(e)}
|
||||
className="w-full"
|
||||
isDisabled={!!identityAuthMethodData?.authMethod}
|
||||
>
|
||||
{identityAuthMethods.map(({ label, value }) => (
|
||||
<SelectItem value={String(value || "")} key={label}>
|
||||
|
352
frontend/src/views/Org/MembersPage/components/OrgIdentityTab/components/IdentitySection/IdentityAwsAuthForm.tsx
Normal file
352
frontend/src/views/Org/MembersPage/components/OrgIdentityTab/components/IdentitySection/IdentityAwsAuthForm.tsx
Normal file
@ -0,0 +1,352 @@
|
||||
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 { yupResolver } from "@hookform/resolvers/yup";
|
||||
import * as yup from "yup";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { Button, FormControl, IconButton, Input } from "@app/components/v2";
|
||||
import { useOrganization, useSubscription } from "@app/context";
|
||||
import {
|
||||
useAddIdentityAwsAuth,
|
||||
useGetIdentityAwsAuth,
|
||||
useUpdateIdentityAwsAuth
|
||||
} 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 = yup
|
||||
.object({
|
||||
stsEndpoint: yup.string(),
|
||||
allowedPrincipalArns: yup.string(),
|
||||
allowedAccountIds: yup.string(),
|
||||
accessTokenTTL: yup.string().required("Access Token TTL is required"),
|
||||
accessTokenMaxTTL: yup.string().required("Access Max Token TTL is required"),
|
||||
accessTokenNumUsesLimit: yup.string().required("Access Token Max Number of Uses is required"),
|
||||
accessTokenTrustedIps: yup
|
||||
.array(
|
||||
yup.object({
|
||||
ipAddress: yup.string().max(50).required().label("IP Address")
|
||||
})
|
||||
)
|
||||
.min(1)
|
||||
.required()
|
||||
.label("Access Token Trusted IP")
|
||||
})
|
||||
.required();
|
||||
|
||||
export type FormData = yup.InferType<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 IdentityAwsAuthForm = ({
|
||||
handlePopUpOpen,
|
||||
handlePopUpToggle,
|
||||
identityAuthMethodData
|
||||
}: Props) => {
|
||||
const { currentOrg } = useOrganization();
|
||||
const orgId = currentOrg?.id || "";
|
||||
const { subscription } = useSubscription();
|
||||
|
||||
const { mutateAsync: addMutateAsync } = useAddIdentityAwsAuth();
|
||||
const { mutateAsync: updateMutateAsync } = useUpdateIdentityAwsAuth();
|
||||
|
||||
const { data } = useGetIdentityAwsAuth(identityAuthMethodData?.identityId ?? "");
|
||||
|
||||
const {
|
||||
control,
|
||||
handleSubmit,
|
||||
reset,
|
||||
formState: { isSubmitting }
|
||||
} = useForm<FormData>({
|
||||
resolver: yupResolver(schema),
|
||||
defaultValues: {
|
||||
stsEndpoint: "https://sts.amazonaws.com/",
|
||||
allowedPrincipalArns: "",
|
||||
allowedAccountIds: "",
|
||||
accessTokenTTL: "2592000",
|
||||
accessTokenMaxTTL: "2592000",
|
||||
accessTokenNumUsesLimit: "0",
|
||||
accessTokenTrustedIps: [{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }]
|
||||
}
|
||||
});
|
||||
|
||||
const {
|
||||
fields: accessTokenTrustedIpsFields,
|
||||
append: appendAccessTokenTrustedIp,
|
||||
remove: removeAccessTokenTrustedIp
|
||||
} = useFieldArray({ control, name: "accessTokenTrustedIps" });
|
||||
|
||||
useEffect(() => {
|
||||
if (data) {
|
||||
reset({
|
||||
stsEndpoint: data.stsEndpoint,
|
||||
allowedPrincipalArns: data.allowedPrincipalArns,
|
||||
allowedAccountIds: data.allowedAccountIds,
|
||||
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({
|
||||
stsEndpoint: "https://sts.amazonaws.com/",
|
||||
allowedPrincipalArns: "",
|
||||
allowedAccountIds: "",
|
||||
accessTokenTTL: "2592000",
|
||||
accessTokenMaxTTL: "2592000",
|
||||
accessTokenNumUsesLimit: "0",
|
||||
accessTokenTrustedIps: [{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }]
|
||||
});
|
||||
}
|
||||
}, [data]);
|
||||
|
||||
const onFormSubmit = async ({
|
||||
allowedPrincipalArns,
|
||||
allowedAccountIds,
|
||||
stsEndpoint,
|
||||
accessTokenTTL,
|
||||
accessTokenMaxTTL,
|
||||
accessTokenNumUsesLimit,
|
||||
accessTokenTrustedIps
|
||||
}: FormData) => {
|
||||
try {
|
||||
if (!identityAuthMethodData) return;
|
||||
|
||||
if (data) {
|
||||
await updateMutateAsync({
|
||||
organizationId: orgId,
|
||||
stsEndpoint,
|
||||
allowedPrincipalArns,
|
||||
allowedAccountIds,
|
||||
identityId: identityAuthMethodData.identityId,
|
||||
accessTokenTTL: Number(accessTokenTTL),
|
||||
accessTokenMaxTTL: Number(accessTokenMaxTTL),
|
||||
accessTokenNumUsesLimit: Number(accessTokenNumUsesLimit),
|
||||
accessTokenTrustedIps
|
||||
});
|
||||
} else {
|
||||
await addMutateAsync({
|
||||
organizationId: orgId,
|
||||
identityId: identityAuthMethodData.identityId,
|
||||
stsEndpoint: stsEndpoint || "",
|
||||
allowedPrincipalArns: allowedPrincipalArns || "",
|
||||
allowedAccountIds: allowedAccountIds || "",
|
||||
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}
|
||||
defaultValue="2592000"
|
||||
name="allowedPrincipalArns"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Allowed Principal ARNs"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input
|
||||
{...field}
|
||||
placeholder="arn:aws:iam::123456789012:role/MyRoleName, arn:aws:iam::123456789012:user/MyUserName..."
|
||||
type="text"
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="allowedAccountIds"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Allowed Account IDs"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input {...field} placeholder="123456789012, ..." />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
defaultValue="https://sts.amazonaws.com/"
|
||||
name="stsEndpoint"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl label="STS Endpoint" isError={Boolean(error)} errorText={error?.message}>
|
||||
<Input {...field} placeholder="https://sts.amazonaws.com/" type="text" />
|
||||
</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>
|
||||
);
|
||||
};
|
@ -15,7 +15,10 @@ import {
|
||||
} from "@app/components/v2";
|
||||
import { useOrganization } from "@app/context";
|
||||
import { useCreateIdentity, useGetOrgRoles, useUpdateIdentity } from "@app/hooks/api";
|
||||
import { IdentityAuthMethod, useAddIdentityUniversalAuth } from "@app/hooks/api/identities";
|
||||
import {
|
||||
IdentityAuthMethod
|
||||
// useAddIdentityUniversalAuth
|
||||
} from "@app/hooks/api/identities";
|
||||
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||
|
||||
const schema = yup
|
||||
@ -40,9 +43,7 @@ type Props = {
|
||||
handlePopUpToggle: (popUpName: keyof UsePopUpState<["identity"]>, state?: boolean) => void;
|
||||
};
|
||||
|
||||
export const IdentityModal = ({ popUp, /* handlePopUpOpen, */ handlePopUpToggle }: Props) => {
|
||||
|
||||
|
||||
export const IdentityModal = ({ popUp, handlePopUpOpen, handlePopUpToggle }: Props) => {
|
||||
const { currentOrg } = useOrganization();
|
||||
const orgId = currentOrg?.id || "";
|
||||
|
||||
@ -50,7 +51,7 @@ export const IdentityModal = ({ popUp, /* handlePopUpOpen, */ handlePopUpToggle
|
||||
|
||||
const { mutateAsync: createMutateAsync } = useCreateIdentity();
|
||||
const { mutateAsync: updateMutateAsync } = useUpdateIdentity();
|
||||
const { mutateAsync: addMutateAsync } = useAddIdentityUniversalAuth();
|
||||
// const { mutateAsync: addMutateAsync } = useAddIdentityUniversalAuth();
|
||||
|
||||
const {
|
||||
control,
|
||||
@ -113,31 +114,31 @@ export const IdentityModal = ({ popUp, /* handlePopUpOpen, */ handlePopUpToggle
|
||||
// create
|
||||
|
||||
const {
|
||||
id: createdId
|
||||
// name: createdName,
|
||||
// authMethod
|
||||
id: createdId,
|
||||
name: createdName,
|
||||
authMethod
|
||||
} = await createMutateAsync({
|
||||
name,
|
||||
role: role || undefined,
|
||||
organizationId: orgId
|
||||
});
|
||||
|
||||
await addMutateAsync({
|
||||
organizationId: orgId,
|
||||
identityId: createdId,
|
||||
clientSecretTrustedIps: [{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }],
|
||||
accessTokenTrustedIps: [{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }],
|
||||
accessTokenTTL: 2592000,
|
||||
accessTokenMaxTTL: 2592000,
|
||||
accessTokenNumUsesLimit: 0
|
||||
});
|
||||
// await addMutateAsync({
|
||||
// organizationId: orgId,
|
||||
// identityId: createdId,
|
||||
// clientSecretTrustedIps: [{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }],
|
||||
// accessTokenTrustedIps: [{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }],
|
||||
// accessTokenTTL: 2592000,
|
||||
// accessTokenMaxTTL: 2592000,
|
||||
// accessTokenNumUsesLimit: 0
|
||||
// });
|
||||
|
||||
handlePopUpToggle("identity", false);
|
||||
// handlePopUpOpen("identityAuthMethod", {
|
||||
// identityId: createdId,
|
||||
// name: createdName,
|
||||
// authMethod
|
||||
// });
|
||||
handlePopUpOpen("identityAuthMethod", {
|
||||
identityId: createdId,
|
||||
name: createdName,
|
||||
authMethod
|
||||
});
|
||||
}
|
||||
|
||||
createNotification({
|
||||
|
@ -23,8 +23,6 @@ import { useGetIdentityMembershipOrgs, useGetOrgRoles, useUpdateIdentity } from
|
||||
import { IdentityAuthMethod, identityAuthToNameMap } from "@app/hooks/api/identities";
|
||||
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||
|
||||
// TODO: some kind of map
|
||||
|
||||
type Props = {
|
||||
handlePopUpOpen: (
|
||||
popUpName: keyof UsePopUpState<
|
||||
@ -44,7 +42,6 @@ type Props = {
|
||||
};
|
||||
|
||||
export const IdentityTable = ({ handlePopUpOpen }: Props) => {
|
||||
|
||||
const { currentOrg } = useOrganization();
|
||||
const orgId = currentOrg?.id || "";
|
||||
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user