Fix merge conflicts

This commit is contained in:
Tuan Dang
2024-05-12 08:43:29 -07:00
355 changed files with 8632 additions and 14162 deletions
@@ -74,21 +74,21 @@ jobs:
uses: pr-mpt/actions-commit-hash@v2 uses: pr-mpt/actions-commit-hash@v2
- name: Download task definition - name: Download task definition
run: | run: |
aws ecs describe-task-definition --task-definition infisical-prod-platform --query taskDefinition > task-definition.json aws ecs describe-task-definition --task-definition infisical-core-platform --query taskDefinition > task-definition.json
- name: Render Amazon ECS task definition - name: Render Amazon ECS task definition
id: render-web-container id: render-web-container
uses: aws-actions/amazon-ecs-render-task-definition@v1 uses: aws-actions/amazon-ecs-render-task-definition@v1
with: with:
task-definition: task-definition.json task-definition: task-definition.json
container-name: infisical-prod-platform container-name: infisical-core-platform
image: infisical/staging_infisical:${{ steps.commit.outputs.short }} image: infisical/staging_infisical:${{ steps.commit.outputs.short }}
environment-variables: "LOG_LEVEL=info" environment-variables: "LOG_LEVEL=info"
- name: Deploy to Amazon ECS service - name: Deploy to Amazon ECS service
uses: aws-actions/amazon-ecs-deploy-task-definition@v1 uses: aws-actions/amazon-ecs-deploy-task-definition@v1
with: with:
task-definition: ${{ steps.render-web-container.outputs.task-definition }} task-definition: ${{ steps.render-web-container.outputs.task-definition }}
service: infisical-prod-platform service: infisical-core-platform
cluster: infisical-prod-platform cluster: infisical-core-platform
wait-for-service-stability: true wait-for-service-stability: true
production-postgres-deployment: production-postgres-deployment:
@@ -135,6 +135,6 @@ jobs:
uses: aws-actions/amazon-ecs-deploy-task-definition@v1 uses: aws-actions/amazon-ecs-deploy-task-definition@v1
with: with:
task-definition: ${{ steps.render-web-container.outputs.task-definition }} task-definition: ${{ steps.render-web-container.outputs.task-definition }}
service: infisical-prod-platform service: infisical-core-platform
cluster: infisical-prod-platform cluster: infisical-core-platform
wait-for-service-stability: true wait-for-service-stability: true
@@ -38,6 +38,16 @@ jobs:
rm added_files.txt rm added_files.txt
git commit -m "chore: renamed new migration files to latest timestamp (gh-action)" 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 - name: Create Pull Request
if: env.SKIP_RENAME != 'true' if: env.SKIP_RENAME != 'true'
uses: peter-evans/create-pull-request@v6 uses: peter-evans/create-pull-request@v6
@@ -46,3 +56,4 @@ jobs:
commit-message: 'chore: renamed new migration files to latest UTC (gh-action)' commit-message: 'chore: renamed new migration files to latest UTC (gh-action)'
title: 'GH Action: rename new migration file timestamp' title: 'GH Action: rename new migration file timestamp'
branch-suffix: timestamp branch-suffix: timestamp
reviewers: ${{ steps.pr_details.outputs.pr_merger }}
+3 -1
View File
@@ -2,4 +2,6 @@
frontend/src/views/Project/MembersPage/components/IdentityTab/components/IdentityRoleForm/IdentityRbacSection.tsx:generic-api-key:206 frontend/src/views/Project/MembersPage/components/IdentityTab/components/IdentityRoleForm/IdentityRbacSection.tsx:generic-api-key:206
frontend/src/views/Project/MembersPage/components/IdentityTab/components/IdentityRoleForm/SpecificPrivilegeSection.tsx:generic-api-key:304 frontend/src/views/Project/MembersPage/components/IdentityTab/components/IdentityRoleForm/SpecificPrivilegeSection.tsx:generic-api-key:304
frontend/src/views/Project/MembersPage/components/MemberListTab/MemberRoleForm/MemberRbacSection.tsx:generic-api-key:206 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 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
+410 -262
View File
@@ -49,7 +49,7 @@
"libsodium-wrappers": "^0.7.13", "libsodium-wrappers": "^0.7.13",
"lodash.isequal": "^4.5.0", "lodash.isequal": "^4.5.0",
"ms": "^2.1.3", "ms": "^2.1.3",
"mysql2": "^3.9.4", "mysql2": "^3.9.7",
"nanoid": "^5.0.4", "nanoid": "^5.0.4",
"nodemailer": "^6.9.9", "nodemailer": "^6.9.9",
"ora": "^7.0.1", "ora": "^7.0.1",
@@ -1207,6 +1207,58 @@
"node": ">=14.0.0" "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": { "node_modules/@aws-sdk/client-secrets-manager/node_modules/uuid": {
"version": "8.3.2", "version": "8.3.2",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
@@ -1314,7 +1366,7 @@
"@aws-sdk/credential-provider-node": "^3.504.0" "@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", "version": "3.504.0",
"resolved": "https://registry.npmjs.org/@aws-sdk/client-sts/-/client-sts-3.504.0.tgz", "resolved": "https://registry.npmjs.org/@aws-sdk/client-sts/-/client-sts-3.504.0.tgz",
"integrity": "sha512-IESs8FkL7B/uY+ml4wgoRkrr6xYo4PizcNw6JX17eveq1gRBCPKeGMjE6HTDOcIYZZ8rqz/UeuH3JD4UhrMOnA==", "integrity": "sha512-IESs8FkL7B/uY+ml4wgoRkrr6xYo4PizcNw6JX17eveq1gRBCPKeGMjE6HTDOcIYZZ8rqz/UeuH3JD4UhrMOnA==",
@@ -1436,6 +1488,58 @@
"node": ">=14.0.0" "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": { "node_modules/@aws-sdk/credential-provider-node": {
"version": "3.504.0", "version": "3.504.0",
"resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.504.0.tgz", "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": ">=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": { "node_modules/@aws-sdk/middleware-host-header": {
"version": "3.502.0", "version": "3.502.0",
"resolved": "https://registry.npmjs.org/@aws-sdk/middleware-host-header/-/middleware-host-header-3.502.0.tgz", "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": { "node_modules/@smithy/abort-controller": {
"version": "2.1.3", "version": "2.2.0",
"resolved": "https://registry.npmjs.org/@smithy/abort-controller/-/abort-controller-2.1.3.tgz", "resolved": "https://registry.npmjs.org/@smithy/abort-controller/-/abort-controller-2.2.0.tgz",
"integrity": "sha512-c2aYH2Wu1RVE3rLlVgg2kQOBJGM0WbjReQi5DnPTm2Zb7F0gk7J2aeQeaX2u/lQZoHl6gv8Oac7mt9alU3+f4A==", "integrity": "sha512-wRlta7GuLWpTqtFfGo+nZyOO1vEvewdNR1R4rTxpC8XU6vG/NDyrFBhwLZsqg1NUoR1noVaXJPC/7ZK47QCySw==",
"dependencies": { "dependencies": {
"@smithy/types": "^2.10.1", "@smithy/types": "^2.12.0",
"tslib": "^2.5.0" "tslib": "^2.6.2"
}, },
"engines": { "engines": {
"node": ">=14.0.0" "node": ">=14.0.0"
} }
}, },
"node_modules/@smithy/config-resolver": { "node_modules/@smithy/config-resolver": {
"version": "2.1.4", "version": "2.2.0",
"resolved": "https://registry.npmjs.org/@smithy/config-resolver/-/config-resolver-2.1.4.tgz", "resolved": "https://registry.npmjs.org/@smithy/config-resolver/-/config-resolver-2.2.0.tgz",
"integrity": "sha512-AW2WUZmBAzgO3V3ovKtsUbI3aBNMeQKFDumoqkNxaVDWF/xfnxAWqBKDr/NuG7c06N2Rm4xeZLPiJH/d+na0HA==", "integrity": "sha512-fsiMgd8toyUba6n1WRmr+qACzXltpdDkPTAaDqc8QqPBUzO+/JKwL6bUBseHVi8tu9l+3JOK+tSf7cay+4B3LA==",
"dependencies": { "dependencies": {
"@smithy/node-config-provider": "^2.2.4", "@smithy/node-config-provider": "^2.3.0",
"@smithy/types": "^2.10.1", "@smithy/types": "^2.12.0",
"@smithy/util-config-provider": "^2.2.1", "@smithy/util-config-provider": "^2.3.0",
"@smithy/util-middleware": "^2.1.3", "@smithy/util-middleware": "^2.2.0",
"tslib": "^2.5.0" "tslib": "^2.6.2"
}, },
"engines": { "engines": {
"node": ">=14.0.0" "node": ">=14.0.0"
} }
}, },
"node_modules/@smithy/core": { "node_modules/@smithy/core": {
"version": "1.3.5", "version": "1.4.2",
"resolved": "https://registry.npmjs.org/@smithy/core/-/core-1.3.5.tgz", "resolved": "https://registry.npmjs.org/@smithy/core/-/core-1.4.2.tgz",
"integrity": "sha512-Rrc+e2Jj6Gu7Xbn0jvrzZlSiP2CZocIOfZ9aNUA82+1sa6GBnxqL9+iZ9EKHeD9aqD1nU8EK4+oN2EiFpSv7Yw==", "integrity": "sha512-2fek3I0KZHWJlRLvRTqxTEri+qV0GRHrJIoLFuBMZB4EMg4WgeBGfF0X6abnrNYpq55KJ6R4D6x4f0vLnhzinA==",
"dependencies": { "dependencies": {
"@smithy/middleware-endpoint": "^2.4.4", "@smithy/middleware-endpoint": "^2.5.1",
"@smithy/middleware-retry": "^2.1.4", "@smithy/middleware-retry": "^2.3.1",
"@smithy/middleware-serde": "^2.1.3", "@smithy/middleware-serde": "^2.3.0",
"@smithy/protocol-http": "^3.2.1", "@smithy/protocol-http": "^3.3.0",
"@smithy/smithy-client": "^2.4.2", "@smithy/smithy-client": "^2.5.1",
"@smithy/types": "^2.10.1", "@smithy/types": "^2.12.0",
"@smithy/util-middleware": "^2.1.3", "@smithy/util-middleware": "^2.2.0",
"tslib": "^2.5.0" "tslib": "^2.6.2"
}, },
"engines": { "engines": {
"node": ">=14.0.0" "node": ">=14.0.0"
} }
}, },
"node_modules/@smithy/credential-provider-imds": { "node_modules/@smithy/credential-provider-imds": {
"version": "2.2.4", "version": "2.3.0",
"resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-2.2.4.tgz", "resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-2.3.0.tgz",
"integrity": "sha512-DdatjmBZQnhGe1FhI8gO98f7NmvQFSDiZTwC3WMvLTCKQUY+Y1SVkhJqIuLu50Eb7pTheoXQmK+hKYUgpUWsNA==", "integrity": "sha512-BWB9mIukO1wjEOo1Ojgl6LrG4avcaC7T/ZP6ptmAaW4xluhSIPZhY+/PI5YKzlk+jsm+4sQZB45Bt1OfMeQa3w==",
"dependencies": { "dependencies": {
"@smithy/node-config-provider": "^2.2.4", "@smithy/node-config-provider": "^2.3.0",
"@smithy/property-provider": "^2.1.3", "@smithy/property-provider": "^2.2.0",
"@smithy/types": "^2.10.1", "@smithy/types": "^2.12.0",
"@smithy/url-parser": "^2.1.3", "@smithy/url-parser": "^2.2.0",
"tslib": "^2.5.0" "tslib": "^2.6.2"
}, },
"engines": { "engines": {
"node": ">=14.0.0" "node": ">=14.0.0"
@@ -3779,459 +3935,451 @@
} }
}, },
"node_modules/@smithy/fetch-http-handler": { "node_modules/@smithy/fetch-http-handler": {
"version": "2.4.3", "version": "2.5.0",
"resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-2.4.3.tgz", "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-2.5.0.tgz",
"integrity": "sha512-Fn/KYJFo6L5I4YPG8WQb2hOmExgRmNpVH5IK2zU3JKrY5FKW7y9ar5e0BexiIC9DhSKqKX+HeWq/Y18fq7Dkpw==", "integrity": "sha512-BOWEBeppWhLn/no/JxUL/ghTfANTjT7kg3Ww2rPqTUY9R4yHPXxJ9JhMe3Z03LN3aPwiwlpDIUcVw1xDyHqEhw==",
"dependencies": { "dependencies": {
"@smithy/protocol-http": "^3.2.1", "@smithy/protocol-http": "^3.3.0",
"@smithy/querystring-builder": "^2.1.3", "@smithy/querystring-builder": "^2.2.0",
"@smithy/types": "^2.10.1", "@smithy/types": "^2.12.0",
"@smithy/util-base64": "^2.1.1", "@smithy/util-base64": "^2.3.0",
"tslib": "^2.5.0" "tslib": "^2.6.2"
} }
}, },
"node_modules/@smithy/hash-node": { "node_modules/@smithy/hash-node": {
"version": "2.1.3", "version": "2.2.0",
"resolved": "https://registry.npmjs.org/@smithy/hash-node/-/hash-node-2.1.3.tgz", "resolved": "https://registry.npmjs.org/@smithy/hash-node/-/hash-node-2.2.0.tgz",
"integrity": "sha512-FsAPCUj7VNJIdHbSxMd5uiZiF20G2zdSDgrgrDrHqIs/VMxK85Vqk5kMVNNDMCZmMezp6UKnac0B4nAyx7HJ9g==", "integrity": "sha512-zLWaC/5aWpMrHKpoDF6nqpNtBhlAYKF/7+9yMN7GpdR8CzohnWfGtMznPybnwSS8saaXBMxIGwJqR4HmRp6b3g==",
"dependencies": { "dependencies": {
"@smithy/types": "^2.10.1", "@smithy/types": "^2.12.0",
"@smithy/util-buffer-from": "^2.1.1", "@smithy/util-buffer-from": "^2.2.0",
"@smithy/util-utf8": "^2.1.1", "@smithy/util-utf8": "^2.3.0",
"tslib": "^2.5.0" "tslib": "^2.6.2"
}, },
"engines": { "engines": {
"node": ">=14.0.0" "node": ">=14.0.0"
} }
}, },
"node_modules/@smithy/invalid-dependency": { "node_modules/@smithy/invalid-dependency": {
"version": "2.1.3", "version": "2.2.0",
"resolved": "https://registry.npmjs.org/@smithy/invalid-dependency/-/invalid-dependency-2.1.3.tgz", "resolved": "https://registry.npmjs.org/@smithy/invalid-dependency/-/invalid-dependency-2.2.0.tgz",
"integrity": "sha512-wkra7d/G4CbngV4xsjYyAYOvdAhahQje/WymuQdVEnXFExJopEu7fbL5AEAlBPgWHXwu94VnCSG00gVzRfExyg==", "integrity": "sha512-nEDASdbKFKPXN2O6lOlTgrEEOO9NHIeO+HVvZnkqc8h5U9g3BIhWsvzFo+UcUbliMHvKNPD/zVxDrkP1Sbgp8Q==",
"dependencies": { "dependencies": {
"@smithy/types": "^2.10.1", "@smithy/types": "^2.12.0",
"tslib": "^2.5.0" "tslib": "^2.6.2"
} }
}, },
"node_modules/@smithy/is-array-buffer": { "node_modules/@smithy/is-array-buffer": {
"version": "2.1.1", "version": "2.2.0",
"resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-2.1.1.tgz", "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-2.2.0.tgz",
"integrity": "sha512-xozSQrcUinPpNPNPds4S7z/FakDTh1MZWtRP/2vQtYB/u3HYrX2UXuZs+VhaKBd6Vc7g2XPr2ZtwGBNDN6fNKQ==", "integrity": "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==",
"dependencies": { "dependencies": {
"tslib": "^2.5.0" "tslib": "^2.6.2"
}, },
"engines": { "engines": {
"node": ">=14.0.0" "node": ">=14.0.0"
} }
}, },
"node_modules/@smithy/middleware-content-length": { "node_modules/@smithy/middleware-content-length": {
"version": "2.1.3", "version": "2.2.0",
"resolved": "https://registry.npmjs.org/@smithy/middleware-content-length/-/middleware-content-length-2.1.3.tgz", "resolved": "https://registry.npmjs.org/@smithy/middleware-content-length/-/middleware-content-length-2.2.0.tgz",
"integrity": "sha512-aJduhkC+dcXxdnv5ZpM3uMmtGmVFKx412R1gbeykS5HXDmRU6oSsyy2SoHENCkfOGKAQOjVE2WVqDJibC0d21g==", "integrity": "sha512-5bl2LG1Ah/7E5cMSC+q+h3IpVHMeOkG0yLRyQT1p2aMJkSrZG7RlXHPuAgb7EyaFeidKEnnd/fNaLLaKlHGzDQ==",
"dependencies": { "dependencies": {
"@smithy/protocol-http": "^3.2.1", "@smithy/protocol-http": "^3.3.0",
"@smithy/types": "^2.10.1", "@smithy/types": "^2.12.0",
"tslib": "^2.5.0" "tslib": "^2.6.2"
}, },
"engines": { "engines": {
"node": ">=14.0.0" "node": ">=14.0.0"
} }
}, },
"node_modules/@smithy/middleware-endpoint": { "node_modules/@smithy/middleware-endpoint": {
"version": "2.4.4", "version": "2.5.1",
"resolved": "https://registry.npmjs.org/@smithy/middleware-endpoint/-/middleware-endpoint-2.4.4.tgz", "resolved": "https://registry.npmjs.org/@smithy/middleware-endpoint/-/middleware-endpoint-2.5.1.tgz",
"integrity": "sha512-4yjHyHK2Jul4JUDBo2sTsWY9UshYUnXeb/TAK/MTaPEb8XQvDmpwSFnfIRDU45RY1a6iC9LCnmJNg/yHyfxqkw==", "integrity": "sha512-1/8kFp6Fl4OsSIVTWHnNjLnTL8IqpIb/D3sTSczrKFnrE9VMNWxnrRKNvpUHOJ6zpGD5f62TPm7+17ilTJpiCQ==",
"dependencies": { "dependencies": {
"@smithy/middleware-serde": "^2.1.3", "@smithy/middleware-serde": "^2.3.0",
"@smithy/node-config-provider": "^2.2.4", "@smithy/node-config-provider": "^2.3.0",
"@smithy/shared-ini-file-loader": "^2.3.4", "@smithy/shared-ini-file-loader": "^2.4.0",
"@smithy/types": "^2.10.1", "@smithy/types": "^2.12.0",
"@smithy/url-parser": "^2.1.3", "@smithy/url-parser": "^2.2.0",
"@smithy/util-middleware": "^2.1.3", "@smithy/util-middleware": "^2.2.0",
"tslib": "^2.5.0" "tslib": "^2.6.2"
}, },
"engines": { "engines": {
"node": ">=14.0.0" "node": ">=14.0.0"
} }
}, },
"node_modules/@smithy/middleware-retry": { "node_modules/@smithy/middleware-retry": {
"version": "2.1.4", "version": "2.3.1",
"resolved": "https://registry.npmjs.org/@smithy/middleware-retry/-/middleware-retry-2.1.4.tgz", "resolved": "https://registry.npmjs.org/@smithy/middleware-retry/-/middleware-retry-2.3.1.tgz",
"integrity": "sha512-Cyolv9YckZTPli1EkkaS39UklonxMd08VskiuMhURDjC0HHa/AD6aK/YoD21CHv9s0QLg0WMLvk9YeLTKkXaFQ==", "integrity": "sha512-P2bGufFpFdYcWvqpyqqmalRtwFUNUA8vHjJR5iGqbfR6mp65qKOLcUd6lTr4S9Gn/enynSrSf3p3FVgVAf6bXA==",
"dependencies": { "dependencies": {
"@smithy/node-config-provider": "^2.2.4", "@smithy/node-config-provider": "^2.3.0",
"@smithy/protocol-http": "^3.2.1", "@smithy/protocol-http": "^3.3.0",
"@smithy/service-error-classification": "^2.1.3", "@smithy/service-error-classification": "^2.1.5",
"@smithy/smithy-client": "^2.4.2", "@smithy/smithy-client": "^2.5.1",
"@smithy/types": "^2.10.1", "@smithy/types": "^2.12.0",
"@smithy/util-middleware": "^2.1.3", "@smithy/util-middleware": "^2.2.0",
"@smithy/util-retry": "^2.1.3", "@smithy/util-retry": "^2.2.0",
"tslib": "^2.5.0", "tslib": "^2.6.2",
"uuid": "^8.3.2" "uuid": "^9.0.1"
}, },
"engines": { "engines": {
"node": ">=14.0.0" "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": { "node_modules/@smithy/middleware-serde": {
"version": "2.1.3", "version": "2.3.0",
"resolved": "https://registry.npmjs.org/@smithy/middleware-serde/-/middleware-serde-2.1.3.tgz", "resolved": "https://registry.npmjs.org/@smithy/middleware-serde/-/middleware-serde-2.3.0.tgz",
"integrity": "sha512-s76LId+TwASrHhUa9QS4k/zeXDUAuNuddKklQzRgumbzge5BftVXHXIqL4wQxKGLocPwfgAOXWx+HdWhQk9hTg==", "integrity": "sha512-sIADe7ojwqTyvEQBe1nc/GXB9wdHhi9UwyX0lTyttmUWDJLP655ZYE1WngnNyXREme8I27KCaUhyhZWRXL0q7Q==",
"dependencies": { "dependencies": {
"@smithy/types": "^2.10.1", "@smithy/types": "^2.12.0",
"tslib": "^2.5.0" "tslib": "^2.6.2"
}, },
"engines": { "engines": {
"node": ">=14.0.0" "node": ">=14.0.0"
} }
}, },
"node_modules/@smithy/middleware-stack": { "node_modules/@smithy/middleware-stack": {
"version": "2.1.3", "version": "2.2.0",
"resolved": "https://registry.npmjs.org/@smithy/middleware-stack/-/middleware-stack-2.1.3.tgz", "resolved": "https://registry.npmjs.org/@smithy/middleware-stack/-/middleware-stack-2.2.0.tgz",
"integrity": "sha512-opMFufVQgvBSld/b7mD7OOEBxF6STyraVr1xel1j0abVILM8ALJvRoFbqSWHGmaDlRGIiV9Q5cGbWi0sdiEaLQ==", "integrity": "sha512-Qntc3jrtwwrsAC+X8wms8zhrTr0sFXnyEGhZd9sLtsJ/6gGQKFzNB+wWbOcpJd7BR8ThNCoKt76BuQahfMvpeA==",
"dependencies": { "dependencies": {
"@smithy/types": "^2.10.1", "@smithy/types": "^2.12.0",
"tslib": "^2.5.0" "tslib": "^2.6.2"
}, },
"engines": { "engines": {
"node": ">=14.0.0" "node": ">=14.0.0"
} }
}, },
"node_modules/@smithy/node-config-provider": { "node_modules/@smithy/node-config-provider": {
"version": "2.2.4", "version": "2.3.0",
"resolved": "https://registry.npmjs.org/@smithy/node-config-provider/-/node-config-provider-2.2.4.tgz", "resolved": "https://registry.npmjs.org/@smithy/node-config-provider/-/node-config-provider-2.3.0.tgz",
"integrity": "sha512-nqazHCp8r4KHSFhRQ+T0VEkeqvA0U+RhehBSr1gunUuNW3X7j0uDrWBxB2gE9eutzy6kE3Y7L+Dov/UXT871vg==", "integrity": "sha512-0elK5/03a1JPWMDPaS726Iw6LpQg80gFut1tNpPfxFuChEEklo2yL823V94SpTZTxmKlXFtFgsP55uh3dErnIg==",
"dependencies": { "dependencies": {
"@smithy/property-provider": "^2.1.3", "@smithy/property-provider": "^2.2.0",
"@smithy/shared-ini-file-loader": "^2.3.4", "@smithy/shared-ini-file-loader": "^2.4.0",
"@smithy/types": "^2.10.1", "@smithy/types": "^2.12.0",
"tslib": "^2.5.0" "tslib": "^2.6.2"
}, },
"engines": { "engines": {
"node": ">=14.0.0" "node": ">=14.0.0"
} }
}, },
"node_modules/@smithy/node-http-handler": { "node_modules/@smithy/node-http-handler": {
"version": "2.4.1", "version": "2.5.0",
"resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-2.4.1.tgz", "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-2.5.0.tgz",
"integrity": "sha512-HCkb94soYhJMxPCa61wGKgmeKpJ3Gftx1XD6bcWEB2wMV1L9/SkQu/6/ysKBnbOzWRE01FGzwrTxucHypZ8rdg==", "integrity": "sha512-mVGyPBzkkGQsPoxQUbxlEfRjrj6FPyA3u3u2VXGr9hT8wilsoQdZdvKpMBFMB8Crfhv5dNkKHIW0Yyuc7eABqA==",
"dependencies": { "dependencies": {
"@smithy/abort-controller": "^2.1.3", "@smithy/abort-controller": "^2.2.0",
"@smithy/protocol-http": "^3.2.1", "@smithy/protocol-http": "^3.3.0",
"@smithy/querystring-builder": "^2.1.3", "@smithy/querystring-builder": "^2.2.0",
"@smithy/types": "^2.10.1", "@smithy/types": "^2.12.0",
"tslib": "^2.5.0" "tslib": "^2.6.2"
}, },
"engines": { "engines": {
"node": ">=14.0.0" "node": ">=14.0.0"
} }
}, },
"node_modules/@smithy/property-provider": { "node_modules/@smithy/property-provider": {
"version": "2.1.3", "version": "2.2.0",
"resolved": "https://registry.npmjs.org/@smithy/property-provider/-/property-provider-2.1.3.tgz", "resolved": "https://registry.npmjs.org/@smithy/property-provider/-/property-provider-2.2.0.tgz",
"integrity": "sha512-bMz3se+ySKWNrgm7eIiQMa2HO/0fl2D0HvLAdg9pTMcpgp4SqOAh6bz7Ik6y7uQqSrk4rLjIKgbQ6yzYgGehCQ==", "integrity": "sha512-+xiil2lFhtTRzXkx8F053AV46QnIw6e7MV8od5Mi68E1ICOjCeCHw2XfLnDEUHnT9WGUIkwcqavXjfwuJbGlpg==",
"dependencies": { "dependencies": {
"@smithy/types": "^2.10.1", "@smithy/types": "^2.12.0",
"tslib": "^2.5.0" "tslib": "^2.6.2"
}, },
"engines": { "engines": {
"node": ">=14.0.0" "node": ">=14.0.0"
} }
}, },
"node_modules/@smithy/protocol-http": { "node_modules/@smithy/protocol-http": {
"version": "3.2.1", "version": "3.3.0",
"resolved": "https://registry.npmjs.org/@smithy/protocol-http/-/protocol-http-3.2.1.tgz", "resolved": "https://registry.npmjs.org/@smithy/protocol-http/-/protocol-http-3.3.0.tgz",
"integrity": "sha512-KLrQkEw4yJCeAmAH7hctE8g9KwA7+H2nSJwxgwIxchbp/L0B5exTdOQi9D5HinPLlothoervGmhpYKelZ6AxIA==", "integrity": "sha512-Xy5XK1AFWW2nlY/biWZXu6/krgbaf2dg0q492D8M5qthsnU2H+UgFeZLbM76FnH7s6RO/xhQRkj+T6KBO3JzgQ==",
"dependencies": { "dependencies": {
"@smithy/types": "^2.10.1", "@smithy/types": "^2.12.0",
"tslib": "^2.5.0" "tslib": "^2.6.2"
}, },
"engines": { "engines": {
"node": ">=14.0.0" "node": ">=14.0.0"
} }
}, },
"node_modules/@smithy/querystring-builder": { "node_modules/@smithy/querystring-builder": {
"version": "2.1.3", "version": "2.2.0",
"resolved": "https://registry.npmjs.org/@smithy/querystring-builder/-/querystring-builder-2.1.3.tgz", "resolved": "https://registry.npmjs.org/@smithy/querystring-builder/-/querystring-builder-2.2.0.tgz",
"integrity": "sha512-kFD3PnNqKELe6m9GRHQw/ftFFSZpnSeQD4qvgDB6BQN6hREHELSosVFUMPN4M3MDKN2jAwk35vXHLoDrNfKu0A==", "integrity": "sha512-L1kSeviUWL+emq3CUVSgdogoM/D9QMFaqxL/dd0X7PCNWmPXqt+ExtrBjqT0V7HLN03Vs9SuiLrG3zy3JGnE5A==",
"dependencies": { "dependencies": {
"@smithy/types": "^2.10.1", "@smithy/types": "^2.12.0",
"@smithy/util-uri-escape": "^2.1.1", "@smithy/util-uri-escape": "^2.2.0",
"tslib": "^2.5.0" "tslib": "^2.6.2"
}, },
"engines": { "engines": {
"node": ">=14.0.0" "node": ">=14.0.0"
} }
}, },
"node_modules/@smithy/querystring-parser": { "node_modules/@smithy/querystring-parser": {
"version": "2.1.3", "version": "2.2.0",
"resolved": "https://registry.npmjs.org/@smithy/querystring-parser/-/querystring-parser-2.1.3.tgz", "resolved": "https://registry.npmjs.org/@smithy/querystring-parser/-/querystring-parser-2.2.0.tgz",
"integrity": "sha512-3+CWJoAqcBMR+yvz6D+Fc5VdoGFtfenW6wqSWATWajrRMGVwJGPT3Vy2eb2bnMktJc4HU4bpjeovFa566P3knQ==", "integrity": "sha512-BvHCDrKfbG5Yhbpj4vsbuPV2GgcpHiAkLeIlcA1LtfpMz3jrqizP1+OguSNSj1MwBHEiN+jwNisXLGdajGDQJA==",
"dependencies": { "dependencies": {
"@smithy/types": "^2.10.1", "@smithy/types": "^2.12.0",
"tslib": "^2.5.0" "tslib": "^2.6.2"
}, },
"engines": { "engines": {
"node": ">=14.0.0" "node": ">=14.0.0"
} }
}, },
"node_modules/@smithy/service-error-classification": { "node_modules/@smithy/service-error-classification": {
"version": "2.1.3", "version": "2.1.5",
"resolved": "https://registry.npmjs.org/@smithy/service-error-classification/-/service-error-classification-2.1.3.tgz", "resolved": "https://registry.npmjs.org/@smithy/service-error-classification/-/service-error-classification-2.1.5.tgz",
"integrity": "sha512-iUrpSsem97bbXHHT/v3s7vaq8IIeMo6P6cXdeYHrx0wOJpMeBGQF7CB0mbJSiTm3//iq3L55JiEm8rA7CTVI8A==", "integrity": "sha512-uBDTIBBEdAQryvHdc5W8sS5YX7RQzF683XrHePVdFmAgKiMofU15FLSM0/HU03hKTnazdNRFa0YHS7+ArwoUSQ==",
"dependencies": { "dependencies": {
"@smithy/types": "^2.10.1" "@smithy/types": "^2.12.0"
}, },
"engines": { "engines": {
"node": ">=14.0.0" "node": ">=14.0.0"
} }
}, },
"node_modules/@smithy/shared-ini-file-loader": { "node_modules/@smithy/shared-ini-file-loader": {
"version": "2.3.4", "version": "2.4.0",
"resolved": "https://registry.npmjs.org/@smithy/shared-ini-file-loader/-/shared-ini-file-loader-2.3.4.tgz", "resolved": "https://registry.npmjs.org/@smithy/shared-ini-file-loader/-/shared-ini-file-loader-2.4.0.tgz",
"integrity": "sha512-CiZmPg9GeDKbKmJGEFvJBsJcFnh0AQRzOtQAzj1XEa8N/0/uSN/v1LYzgO7ry8hhO8+9KB7+DhSW0weqBra4Aw==", "integrity": "sha512-WyujUJL8e1B6Z4PBfAqC/aGY1+C7T0w20Gih3yrvJSk97gpiVfB+y7c46T4Nunk+ZngLq0rOIdeVeIklk0R3OA==",
"dependencies": { "dependencies": {
"@smithy/types": "^2.10.1", "@smithy/types": "^2.12.0",
"tslib": "^2.5.0" "tslib": "^2.6.2"
}, },
"engines": { "engines": {
"node": ">=14.0.0" "node": ">=14.0.0"
} }
}, },
"node_modules/@smithy/signature-v4": { "node_modules/@smithy/signature-v4": {
"version": "2.1.3", "version": "2.3.0",
"resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-2.1.3.tgz", "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-2.3.0.tgz",
"integrity": "sha512-Jq4iPPdCmJojZTsPePn4r1ULShh6ONkokLuxp1Lnk4Sq7r7rJp4HlA1LbPBq4bD64TIzQezIpr1X+eh5NYkNxw==", "integrity": "sha512-ui/NlpILU+6HAQBfJX8BBsDXuKSNrjTSuOYArRblcrErwKFutjrCNb/OExfVRyj9+26F9J+ZmfWT+fKWuDrH3Q==",
"dependencies": { "dependencies": {
"@smithy/eventstream-codec": "^2.1.3", "@smithy/is-array-buffer": "^2.2.0",
"@smithy/is-array-buffer": "^2.1.1", "@smithy/types": "^2.12.0",
"@smithy/types": "^2.10.1", "@smithy/util-hex-encoding": "^2.2.0",
"@smithy/util-hex-encoding": "^2.1.1", "@smithy/util-middleware": "^2.2.0",
"@smithy/util-middleware": "^2.1.3", "@smithy/util-uri-escape": "^2.2.0",
"@smithy/util-uri-escape": "^2.1.1", "@smithy/util-utf8": "^2.3.0",
"@smithy/util-utf8": "^2.1.1", "tslib": "^2.6.2"
"tslib": "^2.5.0"
}, },
"engines": { "engines": {
"node": ">=14.0.0" "node": ">=14.0.0"
} }
}, },
"node_modules/@smithy/smithy-client": { "node_modules/@smithy/smithy-client": {
"version": "2.4.2", "version": "2.5.1",
"resolved": "https://registry.npmjs.org/@smithy/smithy-client/-/smithy-client-2.4.2.tgz", "resolved": "https://registry.npmjs.org/@smithy/smithy-client/-/smithy-client-2.5.1.tgz",
"integrity": "sha512-ntAFYN51zu3N3mCd95YFcFi/8rmvm//uX+HnK24CRbI6k5Rjackn0JhgKz5zOx/tbNvOpgQIwhSX+1EvEsBLbA==", "integrity": "sha512-jrbSQrYCho0yDaaf92qWgd+7nAeap5LtHTI51KXqmpIFCceKU3K9+vIVTUH72bOJngBMqa4kyu1VJhRcSrk/CQ==",
"dependencies": { "dependencies": {
"@smithy/middleware-endpoint": "^2.4.4", "@smithy/middleware-endpoint": "^2.5.1",
"@smithy/middleware-stack": "^2.1.3", "@smithy/middleware-stack": "^2.2.0",
"@smithy/protocol-http": "^3.2.1", "@smithy/protocol-http": "^3.3.0",
"@smithy/types": "^2.10.1", "@smithy/types": "^2.12.0",
"@smithy/util-stream": "^2.1.3", "@smithy/util-stream": "^2.2.0",
"tslib": "^2.5.0" "tslib": "^2.6.2"
}, },
"engines": { "engines": {
"node": ">=14.0.0" "node": ">=14.0.0"
} }
}, },
"node_modules/@smithy/types": { "node_modules/@smithy/types": {
"version": "2.10.1", "version": "2.12.0",
"resolved": "https://registry.npmjs.org/@smithy/types/-/types-2.10.1.tgz", "resolved": "https://registry.npmjs.org/@smithy/types/-/types-2.12.0.tgz",
"integrity": "sha512-hjQO+4ru4cQ58FluQvKKiyMsFg0A6iRpGm2kqdH8fniyNd2WyanoOsYJfMX/IFLuLxEoW6gnRkNZy1y6fUUhtA==", "integrity": "sha512-QwYgloJ0sVNBeBuBs65cIkTbfzV/Q6ZNPCJ99EICFEdJYG50nGIY/uYXp+TbsdJReIuPr0a0kXmCvren3MbRRw==",
"dependencies": { "dependencies": {
"tslib": "^2.5.0" "tslib": "^2.6.2"
}, },
"engines": { "engines": {
"node": ">=14.0.0" "node": ">=14.0.0"
} }
}, },
"node_modules/@smithy/url-parser": { "node_modules/@smithy/url-parser": {
"version": "2.1.3", "version": "2.2.0",
"resolved": "https://registry.npmjs.org/@smithy/url-parser/-/url-parser-2.1.3.tgz", "resolved": "https://registry.npmjs.org/@smithy/url-parser/-/url-parser-2.2.0.tgz",
"integrity": "sha512-X1NRA4WzK/ihgyzTpeGvI9Wn45y8HmqF4AZ/FazwAv8V203Ex+4lXqcYI70naX9ETqbqKVzFk88W6WJJzCggTQ==", "integrity": "sha512-hoA4zm61q1mNTpksiSWp2nEl1dt3j726HdRhiNgVJQMj7mLp7dprtF57mOB6JvEk/x9d2bsuL5hlqZbBuHQylQ==",
"dependencies": { "dependencies": {
"@smithy/querystring-parser": "^2.1.3", "@smithy/querystring-parser": "^2.2.0",
"@smithy/types": "^2.10.1", "@smithy/types": "^2.12.0",
"tslib": "^2.5.0" "tslib": "^2.6.2"
} }
}, },
"node_modules/@smithy/util-base64": { "node_modules/@smithy/util-base64": {
"version": "2.1.1", "version": "2.3.0",
"resolved": "https://registry.npmjs.org/@smithy/util-base64/-/util-base64-2.1.1.tgz", "resolved": "https://registry.npmjs.org/@smithy/util-base64/-/util-base64-2.3.0.tgz",
"integrity": "sha512-UfHVpY7qfF/MrgndI5PexSKVTxSZIdz9InghTFa49QOvuu9I52zLPLUHXvHpNuMb1iD2vmc6R+zbv/bdMipR/g==", "integrity": "sha512-s3+eVwNeJuXUwuMbusncZNViuhv2LjVJ1nMwTqSA0XAC7gjKhqqxRdJPhR8+YrkoZ9IiIbFk/yK6ACe/xlF+hw==",
"dependencies": { "dependencies": {
"@smithy/util-buffer-from": "^2.1.1", "@smithy/util-buffer-from": "^2.2.0",
"tslib": "^2.5.0" "@smithy/util-utf8": "^2.3.0",
"tslib": "^2.6.2"
}, },
"engines": { "engines": {
"node": ">=14.0.0" "node": ">=14.0.0"
} }
}, },
"node_modules/@smithy/util-body-length-browser": { "node_modules/@smithy/util-body-length-browser": {
"version": "2.1.1", "version": "2.2.0",
"resolved": "https://registry.npmjs.org/@smithy/util-body-length-browser/-/util-body-length-browser-2.1.1.tgz", "resolved": "https://registry.npmjs.org/@smithy/util-body-length-browser/-/util-body-length-browser-2.2.0.tgz",
"integrity": "sha512-ekOGBLvs1VS2d1zM2ER4JEeBWAvIOUKeaFch29UjjJsxmZ/f0L3K3x0dEETgh3Q9bkZNHgT+rkdl/J/VUqSRag==", "integrity": "sha512-dtpw9uQP7W+n3vOtx0CfBD5EWd7EPdIdsQnWTDoFf77e3VUf05uA7R7TGipIo8e4WL2kuPdnsr3hMQn9ziYj5w==",
"dependencies": { "dependencies": {
"tslib": "^2.5.0" "tslib": "^2.6.2"
} }
}, },
"node_modules/@smithy/util-body-length-node": { "node_modules/@smithy/util-body-length-node": {
"version": "2.2.1", "version": "2.3.0",
"resolved": "https://registry.npmjs.org/@smithy/util-body-length-node/-/util-body-length-node-2.2.1.tgz", "resolved": "https://registry.npmjs.org/@smithy/util-body-length-node/-/util-body-length-node-2.3.0.tgz",
"integrity": "sha512-/ggJG+ta3IDtpNVq4ktmEUtOkH1LW64RHB5B0hcr5ZaWBmo96UX2cIOVbjCqqDickTXqBWZ4ZO0APuaPrD7Abg==", "integrity": "sha512-ITWT1Wqjubf2CJthb0BuT9+bpzBfXeMokH/AAa5EJQgbv9aPMVfnM76iFIZVFf50hYXGbtiV71BHAthNWd6+dw==",
"dependencies": { "dependencies": {
"tslib": "^2.5.0" "tslib": "^2.6.2"
}, },
"engines": { "engines": {
"node": ">=14.0.0" "node": ">=14.0.0"
} }
}, },
"node_modules/@smithy/util-buffer-from": { "node_modules/@smithy/util-buffer-from": {
"version": "2.1.1", "version": "2.2.0",
"resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-2.1.1.tgz", "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-2.2.0.tgz",
"integrity": "sha512-clhNjbyfqIv9Md2Mg6FffGVrJxw7bgK7s3Iax36xnfVj6cg0fUG7I4RH0XgXJF8bxi+saY5HR21g2UPKSxVCXg==", "integrity": "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==",
"dependencies": { "dependencies": {
"@smithy/is-array-buffer": "^2.1.1", "@smithy/is-array-buffer": "^2.2.0",
"tslib": "^2.5.0" "tslib": "^2.6.2"
}, },
"engines": { "engines": {
"node": ">=14.0.0" "node": ">=14.0.0"
} }
}, },
"node_modules/@smithy/util-config-provider": { "node_modules/@smithy/util-config-provider": {
"version": "2.2.1", "version": "2.3.0",
"resolved": "https://registry.npmjs.org/@smithy/util-config-provider/-/util-config-provider-2.2.1.tgz", "resolved": "https://registry.npmjs.org/@smithy/util-config-provider/-/util-config-provider-2.3.0.tgz",
"integrity": "sha512-50VL/tx9oYYcjJn/qKqNy7sCtpD0+s8XEBamIFo4mFFTclKMNp+rsnymD796uybjiIquB7VCB/DeafduL0y2kw==", "integrity": "sha512-HZkzrRcuFN1k70RLqlNK4FnPXKOpkik1+4JaBoHNJn+RnJGYqaa3c5/+XtLOXhlKzlRgNvyaLieHTW2VwGN0VQ==",
"dependencies": { "dependencies": {
"tslib": "^2.5.0" "tslib": "^2.6.2"
}, },
"engines": { "engines": {
"node": ">=14.0.0" "node": ">=14.0.0"
} }
}, },
"node_modules/@smithy/util-defaults-mode-browser": { "node_modules/@smithy/util-defaults-mode-browser": {
"version": "2.1.4", "version": "2.2.1",
"resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-browser/-/util-defaults-mode-browser-2.1.4.tgz", "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-browser/-/util-defaults-mode-browser-2.2.1.tgz",
"integrity": "sha512-J6XAVY+/g7jf03QMnvqPyU+8jqGrrtXoKWFVOS+n1sz0Lg8HjHJ1ANqaDN+KTTKZRZlvG8nU5ZrJOUL6VdwgcQ==", "integrity": "sha512-RtKW+8j8skk17SYowucwRUjeh4mCtnm5odCL0Lm2NtHQBsYKrNW0od9Rhopu9wF1gHMfHeWF7i90NwBz/U22Kw==",
"dependencies": { "dependencies": {
"@smithy/property-provider": "^2.1.3", "@smithy/property-provider": "^2.2.0",
"@smithy/smithy-client": "^2.4.2", "@smithy/smithy-client": "^2.5.1",
"@smithy/types": "^2.10.1", "@smithy/types": "^2.12.0",
"bowser": "^2.11.0", "bowser": "^2.11.0",
"tslib": "^2.5.0" "tslib": "^2.6.2"
}, },
"engines": { "engines": {
"node": ">= 10.0.0" "node": ">= 10.0.0"
} }
}, },
"node_modules/@smithy/util-defaults-mode-node": { "node_modules/@smithy/util-defaults-mode-node": {
"version": "2.2.3", "version": "2.3.1",
"resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-node/-/util-defaults-mode-node-2.2.3.tgz", "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-node/-/util-defaults-mode-node-2.3.1.tgz",
"integrity": "sha512-ttUISrv1uVOjTlDa3nznX33f0pthoUlP+4grhTvOzcLhzArx8qHB94/untGACOG3nlf8vU20nI2iWImfzoLkYA==", "integrity": "sha512-vkMXHQ0BcLFysBMWgSBLSk3+leMpFSyyFj8zQtv5ZyUBx8/owVh1/pPEkzmW/DR/Gy/5c8vjLDD9gZjXNKbrpA==",
"dependencies": { "dependencies": {
"@smithy/config-resolver": "^2.1.4", "@smithy/config-resolver": "^2.2.0",
"@smithy/credential-provider-imds": "^2.2.4", "@smithy/credential-provider-imds": "^2.3.0",
"@smithy/node-config-provider": "^2.2.4", "@smithy/node-config-provider": "^2.3.0",
"@smithy/property-provider": "^2.1.3", "@smithy/property-provider": "^2.2.0",
"@smithy/smithy-client": "^2.4.2", "@smithy/smithy-client": "^2.5.1",
"@smithy/types": "^2.10.1", "@smithy/types": "^2.12.0",
"tslib": "^2.5.0" "tslib": "^2.6.2"
}, },
"engines": { "engines": {
"node": ">= 10.0.0" "node": ">= 10.0.0"
} }
}, },
"node_modules/@smithy/util-endpoints": { "node_modules/@smithy/util-endpoints": {
"version": "1.1.4", "version": "1.2.0",
"resolved": "https://registry.npmjs.org/@smithy/util-endpoints/-/util-endpoints-1.1.4.tgz", "resolved": "https://registry.npmjs.org/@smithy/util-endpoints/-/util-endpoints-1.2.0.tgz",
"integrity": "sha512-/qAeHmK5l4yQ4/bCIJ9p49wDe9rwWtOzhPHblu386fwPNT3pxmodgcs9jDCV52yK9b4rB8o9Sj31P/7Vzka1cg==", "integrity": "sha512-BuDHv8zRjsE5zXd3PxFXFknzBG3owCpjq8G3FcsXW3CykYXuEqM3nTSsmLzw5q+T12ZYuDlVUZKBdpNbhVtlrQ==",
"dependencies": { "dependencies": {
"@smithy/node-config-provider": "^2.2.4", "@smithy/node-config-provider": "^2.3.0",
"@smithy/types": "^2.10.1", "@smithy/types": "^2.12.0",
"tslib": "^2.5.0" "tslib": "^2.6.2"
}, },
"engines": { "engines": {
"node": ">= 14.0.0" "node": ">= 14.0.0"
} }
}, },
"node_modules/@smithy/util-hex-encoding": { "node_modules/@smithy/util-hex-encoding": {
"version": "2.1.1", "version": "2.2.0",
"resolved": "https://registry.npmjs.org/@smithy/util-hex-encoding/-/util-hex-encoding-2.1.1.tgz", "resolved": "https://registry.npmjs.org/@smithy/util-hex-encoding/-/util-hex-encoding-2.2.0.tgz",
"integrity": "sha512-3UNdP2pkYUUBGEXzQI9ODTDK+Tcu1BlCyDBaRHwyxhA+8xLP8agEKQq4MGmpjqb4VQAjq9TwlCQX0kP6XDKYLg==", "integrity": "sha512-7iKXR+/4TpLK194pVjKiasIyqMtTYJsgKgM242Y9uzt5dhHnUDvMNb+3xIhRJ9QhvqGii/5cRUt4fJn3dtXNHQ==",
"dependencies": { "dependencies": {
"tslib": "^2.5.0" "tslib": "^2.6.2"
}, },
"engines": { "engines": {
"node": ">=14.0.0" "node": ">=14.0.0"
} }
}, },
"node_modules/@smithy/util-middleware": { "node_modules/@smithy/util-middleware": {
"version": "2.1.3", "version": "2.2.0",
"resolved": "https://registry.npmjs.org/@smithy/util-middleware/-/util-middleware-2.1.3.tgz", "resolved": "https://registry.npmjs.org/@smithy/util-middleware/-/util-middleware-2.2.0.tgz",
"integrity": "sha512-/+2fm7AZ2ozl5h8wM++ZP0ovE9/tiUUAHIbCfGfb3Zd3+Dyk17WODPKXBeJ/TnK5U+x743QmA0xHzlSm8I/qhw==", "integrity": "sha512-L1qpleXf9QD6LwLCJ5jddGkgWyuSvWBkJwWAZ6kFkdifdso+sk3L3O1HdmPvCdnCK3IS4qWyPxev01QMnfHSBw==",
"dependencies": { "dependencies": {
"@smithy/types": "^2.10.1", "@smithy/types": "^2.12.0",
"tslib": "^2.5.0" "tslib": "^2.6.2"
}, },
"engines": { "engines": {
"node": ">=14.0.0" "node": ">=14.0.0"
} }
}, },
"node_modules/@smithy/util-retry": { "node_modules/@smithy/util-retry": {
"version": "2.1.3", "version": "2.2.0",
"resolved": "https://registry.npmjs.org/@smithy/util-retry/-/util-retry-2.1.3.tgz", "resolved": "https://registry.npmjs.org/@smithy/util-retry/-/util-retry-2.2.0.tgz",
"integrity": "sha512-Kbvd+GEMuozbNUU3B89mb99tbufwREcyx2BOX0X2+qHjq6Gvsah8xSDDgxISDwcOHoDqUWO425F0Uc/QIRhYkg==", "integrity": "sha512-q9+pAFPTfftHXRytmZ7GzLFFrEGavqapFc06XxzZFcSIGERXMerXxCitjOG1prVDR9QdjqotF40SWvbqcCpf8g==",
"dependencies": { "dependencies": {
"@smithy/service-error-classification": "^2.1.3", "@smithy/service-error-classification": "^2.1.5",
"@smithy/types": "^2.10.1", "@smithy/types": "^2.12.0",
"tslib": "^2.5.0" "tslib": "^2.6.2"
}, },
"engines": { "engines": {
"node": ">= 14.0.0" "node": ">= 14.0.0"
} }
}, },
"node_modules/@smithy/util-stream": { "node_modules/@smithy/util-stream": {
"version": "2.1.3", "version": "2.2.0",
"resolved": "https://registry.npmjs.org/@smithy/util-stream/-/util-stream-2.1.3.tgz", "resolved": "https://registry.npmjs.org/@smithy/util-stream/-/util-stream-2.2.0.tgz",
"integrity": "sha512-HvpEQbP8raTy9n86ZfXiAkf3ezp1c3qeeO//zGqwZdrfaoOpGKQgF2Sv1IqZp7wjhna7pvczWaGUHjcOPuQwKw==", "integrity": "sha512-17faEXbYWIRst1aU9SvPZyMdWmqIrduZjVOqCPMIsWFNxs5yQQgFrJL6b2SdiCzyW9mJoDjFtgi53xx7EH+BXA==",
"dependencies": { "dependencies": {
"@smithy/fetch-http-handler": "^2.4.3", "@smithy/fetch-http-handler": "^2.5.0",
"@smithy/node-http-handler": "^2.4.1", "@smithy/node-http-handler": "^2.5.0",
"@smithy/types": "^2.10.1", "@smithy/types": "^2.12.0",
"@smithy/util-base64": "^2.1.1", "@smithy/util-base64": "^2.3.0",
"@smithy/util-buffer-from": "^2.1.1", "@smithy/util-buffer-from": "^2.2.0",
"@smithy/util-hex-encoding": "^2.1.1", "@smithy/util-hex-encoding": "^2.2.0",
"@smithy/util-utf8": "^2.1.1", "@smithy/util-utf8": "^2.3.0",
"tslib": "^2.5.0" "tslib": "^2.6.2"
}, },
"engines": { "engines": {
"node": ">=14.0.0" "node": ">=14.0.0"
} }
}, },
"node_modules/@smithy/util-uri-escape": { "node_modules/@smithy/util-uri-escape": {
"version": "2.1.1", "version": "2.2.0",
"resolved": "https://registry.npmjs.org/@smithy/util-uri-escape/-/util-uri-escape-2.1.1.tgz", "resolved": "https://registry.npmjs.org/@smithy/util-uri-escape/-/util-uri-escape-2.2.0.tgz",
"integrity": "sha512-saVzI1h6iRBUVSqtnlOnc9ssU09ypo7n+shdQ8hBTZno/9rZ3AuRYvoHInV57VF7Qn7B+pFJG7qTzFiHxWlWBw==", "integrity": "sha512-jtmJMyt1xMD/d8OtbVJ2gFZOSKc+ueYJZPW20ULW1GOp/q/YIM0wNh+u8ZFao9UaIGz4WoPW8hC64qlWLIfoDA==",
"dependencies": { "dependencies": {
"tslib": "^2.5.0" "tslib": "^2.6.2"
}, },
"engines": { "engines": {
"node": ">=14.0.0" "node": ">=14.0.0"
} }
}, },
"node_modules/@smithy/util-utf8": { "node_modules/@smithy/util-utf8": {
"version": "2.1.1", "version": "2.3.0",
"resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.1.1.tgz", "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.3.0.tgz",
"integrity": "sha512-BqTpzYEcUMDwAKr7/mVRUtHDhs6ZoXDi9NypMvMfOr/+u1NW7JgqodPDECiiLboEm6bobcPcECxzjtQh865e9A==", "integrity": "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==",
"dependencies": { "dependencies": {
"@smithy/util-buffer-from": "^2.1.1", "@smithy/util-buffer-from": "^2.2.0",
"tslib": "^2.5.0" "tslib": "^2.6.2"
}, },
"engines": { "engines": {
"node": ">=14.0.0" "node": ">=14.0.0"
@@ -9954,9 +10102,9 @@
} }
}, },
"node_modules/mysql2": { "node_modules/mysql2": {
"version": "3.9.4", "version": "3.9.7",
"resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.9.4.tgz", "resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.9.7.tgz",
"integrity": "sha512-OEESQuwxMza803knC1YSt7NMuc1BrK9j7gZhCSs2WAyxr1vfiI7QLaLOKTh5c9SWGz98qVyQUbK8/WckevNQhg==", "integrity": "sha512-KnJT8vYRcNAZv73uf9zpXqNbvBG7DJrs+1nACsjZP1HMJ1TgXEy8wnNilXAn/5i57JizXKtrUtwDB7HxT9DDpw==",
"dependencies": { "dependencies": {
"denque": "^2.1.0", "denque": "^2.1.0",
"generate-function": "^2.3.1", "generate-function": "^2.3.1",
+1 -1
View File
@@ -110,7 +110,7 @@
"libsodium-wrappers": "^0.7.13", "libsodium-wrappers": "^0.7.13",
"lodash.isequal": "^4.5.0", "lodash.isequal": "^4.5.0",
"ms": "^2.1.3", "ms": "^2.1.3",
"mysql2": "^3.9.4", "mysql2": "^3.9.7",
"nanoid": "^5.0.4", "nanoid": "^5.0.4",
"nodemailer": "^6.9.9", "nodemailer": "^6.9.9",
"ora": "^7.0.1", "ora": "^7.0.1",
+6
View File
@@ -1,6 +1,8 @@
import "fastify"; import "fastify";
import { TUsers } from "@app/db/schemas"; 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 { TAuditLogServiceFactory } from "@app/ee/services/audit-log/audit-log-service";
import { TCreateAuditLogDTO } from "@app/ee/services/audit-log/audit-log-types"; import { TCreateAuditLogDTO } from "@app/ee/services/audit-log/audit-log-types";
import { TAuditLogStreamServiceFactory } from "@app/ee/services/audit-log-stream/audit-log-stream-service"; 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 { TGroupProjectServiceFactory } from "@app/services/group-project/group-project-service";
import { TIdentityServiceFactory } from "@app/services/identity/identity-service"; import { TIdentityServiceFactory } from "@app/services/identity/identity-service";
import { TIdentityAccessTokenServiceFactory } from "@app/services/identity-access-token/identity-access-token-service"; import { TIdentityAccessTokenServiceFactory } from "@app/services/identity-access-token/identity-access-token-service";
import { TIdentityAwsAuthServiceFactory } from "@app/services/identity-aws-auth/identity-aws-auth-service";
import { TIdentityKubernetesAuthServiceFactory } from "@app/services/identity-kubernetes-auth/identity-kubernetes-auth-service"; import { TIdentityKubernetesAuthServiceFactory } from "@app/services/identity-kubernetes-auth/identity-kubernetes-auth-service";
import { TIdentityProjectServiceFactory } from "@app/services/identity-project/identity-project-service"; import { TIdentityProjectServiceFactory } from "@app/services/identity-project/identity-project-service";
import { TIdentityUaServiceFactory } from "@app/services/identity-ua/identity-ua-service"; import { TIdentityUaServiceFactory } from "@app/services/identity-ua/identity-ua-service";
@@ -115,6 +118,9 @@ declare module "fastify" {
identityProject: TIdentityProjectServiceFactory; identityProject: TIdentityProjectServiceFactory;
identityUa: TIdentityUaServiceFactory; identityUa: TIdentityUaServiceFactory;
identityKubernetesAuth: TIdentityKubernetesAuthServiceFactory; identityKubernetesAuth: TIdentityKubernetesAuthServiceFactory;
identityAwsAuth: TIdentityAwsAuthServiceFactory;
accessApprovalPolicy: TAccessApprovalPolicyServiceFactory;
accessApprovalRequest: TAccessApprovalRequestServiceFactory;
secretApprovalPolicy: TSecretApprovalPolicyServiceFactory; secretApprovalPolicy: TSecretApprovalPolicyServiceFactory;
secretApprovalRequest: TSecretApprovalRequestServiceFactory; secretApprovalRequest: TSecretApprovalRequestServiceFactory;
secretRotation: TSecretRotationServiceFactory; secretRotation: TSecretRotationServiceFactory;
+45
View File
@@ -2,6 +2,18 @@ import { Knex } from "knex";
import { import {
TableName, TableName,
TAccessApprovalPolicies,
TAccessApprovalPoliciesApprovers,
TAccessApprovalPoliciesApproversInsert,
TAccessApprovalPoliciesApproversUpdate,
TAccessApprovalPoliciesInsert,
TAccessApprovalPoliciesUpdate,
TAccessApprovalRequests,
TAccessApprovalRequestsInsert,
TAccessApprovalRequestsReviewers,
TAccessApprovalRequestsReviewersInsert,
TAccessApprovalRequestsReviewersUpdate,
TAccessApprovalRequestsUpdate,
TApiKeys, TApiKeys,
TApiKeysInsert, TApiKeysInsert,
TApiKeysUpdate, TApiKeysUpdate,
@@ -47,6 +59,9 @@ import {
TIdentityAccessTokens, TIdentityAccessTokens,
TIdentityAccessTokensInsert, TIdentityAccessTokensInsert,
TIdentityAccessTokensUpdate, TIdentityAccessTokensUpdate,
TIdentityAwsAuths,
TIdentityAwsAuthsInsert,
TIdentityAwsAuthsUpdate,
TIdentityKubernetesAuths, TIdentityKubernetesAuths,
TIdentityKubernetesAuthsInsert, TIdentityKubernetesAuthsInsert,
TIdentityKubernetesAuthsUpdate, TIdentityKubernetesAuthsUpdate,
@@ -322,6 +337,11 @@ declare module "knex/types/tables" {
TIdentityKubernetesAuthsInsert, TIdentityKubernetesAuthsInsert,
TIdentityKubernetesAuthsUpdate TIdentityKubernetesAuthsUpdate
>; >;
[TableName.IdentityAwsAuth]: Knex.CompositeTableType<
TIdentityAwsAuths,
TIdentityAwsAuthsInsert,
TIdentityAwsAuthsUpdate
>;
[TableName.IdentityUaClientSecret]: Knex.CompositeTableType< [TableName.IdentityUaClientSecret]: Knex.CompositeTableType<
TIdentityUaClientSecrets, TIdentityUaClientSecrets,
TIdentityUaClientSecretsInsert, TIdentityUaClientSecretsInsert,
@@ -352,6 +372,31 @@ declare module "knex/types/tables" {
TIdentityProjectAdditionalPrivilegeInsert, TIdentityProjectAdditionalPrivilegeInsert,
TIdentityProjectAdditionalPrivilegeUpdate 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.ScimToken]: Knex.CompositeTableType<TScimTokens, TScimTokensInsert, TScimTokensUpdate>;
[TableName.SecretApprovalPolicy]: Knex.CompositeTableType< [TableName.SecretApprovalPolicy]: Knex.CompositeTableType<
TSecretApprovalPolicies, TSecretApprovalPolicies,
@@ -0,0 +1,54 @@
import { Knex } from "knex";
import { TableName } from "../schemas";
export async function up(knex: Knex): Promise<void> {
const isUsersTablePresent = await knex.schema.hasTable(TableName.Users);
if (isUsersTablePresent) {
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);
if (isUserAliasTablePresent) {
await knex.schema.alterTable(TableName.UserAliases, (t) => {
t.string("username").nullable().alter();
});
}
const isSuperAdminTablePresent = await knex.schema.hasTable(TableName.SuperAdmin);
if (isSuperAdminTablePresent) {
await knex.schema.alterTable(TableName.SuperAdmin, (t) => {
t.boolean("trustSamlEmails").defaultTo(false);
t.boolean("trustLdapEmails").defaultTo(false);
});
}
}
export async function down(knex: Knex): Promise<void> {
if (await knex.schema.hasColumn(TableName.Users, "isEmailVerified")) {
await knex.schema.alterTable(TableName.Users, (t) => {
t.dropColumn("isEmailVerified");
});
}
if (await knex.schema.hasColumn(TableName.SuperAdmin, "trustSamlEmails")) {
await knex.schema.alterTable(TableName.SuperAdmin, (t) => {
t.dropColumn("trustSamlEmails");
});
}
if (await knex.schema.hasColumn(TableName.SuperAdmin, "trustLdapEmails")) {
await knex.schema.alterTable(TableName.SuperAdmin, (t) => {
t.dropColumn("trustLdapEmails");
});
}
}
@@ -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);
}
@@ -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);
}
@@ -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>
>;
@@ -7,16 +7,18 @@ import { z } from "zod";
import { TImmutableDBKeys } from "./models"; import { TImmutableDBKeys } from "./models";
export const SecretApprovalPoliciesSchema = z.object({ export const AccessApprovalPoliciesSchema = z.object({
id: z.string().uuid(), id: z.string().uuid(),
name: z.string(), name: z.string(),
secretPath: z.string().nullable().optional(),
approvals: z.number().default(1), approvals: z.number().default(1),
secretPath: z.string().nullable().optional(),
envId: z.string().uuid(), envId: z.string().uuid(),
createdAt: z.date(), createdAt: z.date(),
updatedAt: z.date(), updatedAt: z.date()
}); });
export type TSecretApprovalPolicies = z.infer<typeof SecretApprovalPoliciesSchema>; export type TAccessApprovalPolicies = z.infer<typeof AccessApprovalPoliciesSchema>;
export type TSecretApprovalPoliciesInsert = Omit<TSecretApprovalPolicies, TImmutableDBKeys>; export type TAccessApprovalPoliciesInsert = Omit<z.input<typeof AccessApprovalPoliciesSchema>, TImmutableDBKeys>;
export type TSecretApprovalPoliciesUpdate = Partial<Omit<TSecretApprovalPolicies, TImmutableDBKeys>>; export type TAccessApprovalPoliciesUpdate = Partial<
Omit<z.input<typeof AccessApprovalPoliciesSchema>, TImmutableDBKeys>
>;
@@ -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>
>;
@@ -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>
>;
@@ -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>>;
+5
View File
@@ -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 "./api-keys";
export * from "./audit-log-streams"; export * from "./audit-log-streams";
export * from "./audit-logs"; export * from "./audit-logs";
@@ -13,6 +17,7 @@ export * from "./group-project-memberships";
export * from "./groups"; export * from "./groups";
export * from "./identities"; export * from "./identities";
export * from "./identity-access-tokens"; export * from "./identity-access-tokens";
export * from "./identity-aws-auths";
export * from "./identity-kubernetes-auths"; export * from "./identity-kubernetes-auths";
export * from "./identity-org-memberships"; export * from "./identity-org-memberships";
export * from "./identity-project-additional-privilege"; export * from "./identity-project-additional-privilege";
+7 -1
View File
@@ -46,11 +46,16 @@ export enum TableName {
IdentityUniversalAuth = "identity_universal_auths", IdentityUniversalAuth = "identity_universal_auths",
IdentityKubernetesAuth = "identity_kubernetes_auths", IdentityKubernetesAuth = "identity_kubernetes_auths",
IdentityUaClientSecret = "identity_ua_client_secrets", IdentityUaClientSecret = "identity_ua_client_secrets",
IdentityAwsAuth = "identity_aws_auths",
IdentityOrgMembership = "identity_org_memberships", IdentityOrgMembership = "identity_org_memberships",
IdentityProjectMembership = "identity_project_memberships", IdentityProjectMembership = "identity_project_memberships",
IdentityProjectMembershipRole = "identity_project_membership_role", IdentityProjectMembershipRole = "identity_project_membership_role",
IdentityProjectAdditionalPrivilege = "identity_project_additional_privilege", IdentityProjectAdditionalPrivilege = "identity_project_additional_privilege",
ScimToken = "scim_tokens", 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", SecretApprovalPolicy = "secret_approval_policies",
SecretApprovalPolicyApprover = "secret_approval_policies_approvers", SecretApprovalPolicyApprover = "secret_approval_policies_approvers",
SecretApprovalRequest = "secret_approval_requests", SecretApprovalRequest = "secret_approval_requests",
@@ -140,5 +145,6 @@ export enum ProjectUpgradeStatus {
export enum IdentityAuthMethod { export enum IdentityAuthMethod {
Univeral = "universal-auth", Univeral = "universal-auth",
Kubernetes_Auth = "kubernetes-auth" Kubernetes_Auth = "kubernetes-auth",
AWS_AUTH = "aws-auth"
} }
+3 -1
View File
@@ -14,7 +14,9 @@ export const SuperAdminSchema = z.object({
createdAt: z.date(), createdAt: z.date(),
updatedAt: z.date(), updatedAt: z.date(),
allowedSignUpDomain: z.string().nullable().optional(), allowedSignUpDomain: z.string().nullable().optional(),
instanceId: z.string().uuid().default("00000000-0000-0000-0000-000000000000") instanceId: z.string().uuid().default("00000000-0000-0000-0000-000000000000"),
trustSamlEmails: z.boolean().default(false).nullable().optional(),
trustLdapEmails: z.boolean().default(false).nullable().optional()
}); });
export type TSuperAdmin = z.infer<typeof SuperAdminSchema>; export type TSuperAdmin = z.infer<typeof SuperAdminSchema>;
+1 -1
View File
@@ -10,7 +10,7 @@ import { TImmutableDBKeys } from "./models";
export const UserAliasesSchema = z.object({ export const UserAliasesSchema = z.object({
id: z.string().uuid(), id: z.string().uuid(),
userId: z.string().uuid(), userId: z.string().uuid(),
username: z.string(), username: z.string().nullable().optional(),
aliasType: z.string(), aliasType: z.string(),
externalId: z.string(), externalId: z.string(),
emails: z.string().array().nullable().optional(), emails: z.string().array().nullable().optional(),
+2 -1
View File
@@ -21,7 +21,8 @@ export const UsersSchema = z.object({
createdAt: z.date(), createdAt: z.date(),
updatedAt: z.date(), updatedAt: z.date(),
isGhost: z.boolean().default(false), isGhost: z.boolean().default(false),
username: z.string() username: z.string(),
isEmailVerified: z.boolean().default(false).nullable().optional()
}); });
export type TUsers = z.infer<typeof UsersSchema>; export type TUsers = z.infer<typeof UsersSchema>;
@@ -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 };
}
});
};
@@ -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 { packRules } from "@casl/ability/extra";
import { PackRule, packRules, unpackRules } from "@casl/ability/extra";
import slugify from "@sindresorhus/slugify"; import slugify from "@sindresorhus/slugify";
import ms from "ms"; import ms from "ms";
import { z } from "zod"; 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 { 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 { IDENTITY_ADDITIONAL_PRIVILEGE } from "@app/lib/api-docs";
import { alphaNumericNanoId } from "@app/lib/nanoid"; import { alphaNumericNanoId } from "@app/lib/nanoid";
import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; 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"; import { AuthMode } from "@app/services/auth/auth-type";
export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: FastifyZodProvider) => { export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: FastifyZodProvider) => {
@@ -41,11 +39,11 @@ export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: F
}) })
.optional() .optional()
.describe(IDENTITY_ADDITIONAL_PRIVILEGE.CREATE.slug), .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: { response: {
200: z.object({ 200: z.object({
privilege: IdentityProjectAdditionalPrivilegeSchema privilege: SanitizedIdentityPrivilegeSchema
}) })
} }
}, },
@@ -92,7 +90,7 @@ export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: F
}) })
.optional() .optional()
.describe(IDENTITY_ADDITIONAL_PRIVILEGE.CREATE.slug), .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 temporaryMode: z
.nativeEnum(IdentityProjectAdditionalPrivilegeTemporaryMode) .nativeEnum(IdentityProjectAdditionalPrivilegeTemporaryMode)
.describe(IDENTITY_ADDITIONAL_PRIVILEGE.CREATE.temporaryMode), .describe(IDENTITY_ADDITIONAL_PRIVILEGE.CREATE.temporaryMode),
@@ -107,7 +105,7 @@ export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: F
}), }),
response: { response: {
200: z.object({ 200: z.object({
privilege: IdentityProjectAdditionalPrivilegeSchema privilege: SanitizedIdentityPrivilegeSchema
}) })
} }
}, },
@@ -157,7 +155,7 @@ export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: F
message: "Slug must be a valid slug" message: "Slug must be a valid slug"
}) })
.describe(IDENTITY_ADDITIONAL_PRIVILEGE.UPDATE.newSlug), .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), isTemporary: z.boolean().describe(IDENTITY_ADDITIONAL_PRIVILEGE.UPDATE.isTemporary),
temporaryMode: z temporaryMode: z
.nativeEnum(IdentityProjectAdditionalPrivilegeTemporaryMode) .nativeEnum(IdentityProjectAdditionalPrivilegeTemporaryMode)
@@ -175,7 +173,7 @@ export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: F
}), }),
response: { response: {
200: z.object({ 200: z.object({
privilege: IdentityProjectAdditionalPrivilegeSchema privilege: SanitizedIdentityPrivilegeSchema
}) })
} }
}, },
@@ -219,7 +217,7 @@ export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: F
}), }),
response: { response: {
200: z.object({ 200: z.object({
privilege: IdentityProjectAdditionalPrivilegeSchema privilege: SanitizedIdentityPrivilegeSchema
}) })
} }
}, },
@@ -260,7 +258,7 @@ export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: F
}), }),
response: { response: {
200: z.object({ 200: z.object({
privilege: IdentityProjectAdditionalPrivilegeSchema privilege: SanitizedIdentityPrivilegeSchema
}) })
} }
}, },
@@ -293,16 +291,11 @@ export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: F
], ],
querystring: z.object({ querystring: z.object({
identityId: z.string().min(1).describe(IDENTITY_ADDITIONAL_PRIVILEGE.LIST.identityId), identityId: z.string().min(1).describe(IDENTITY_ADDITIONAL_PRIVILEGE.LIST.identityId),
projectSlug: z.string().min(1).describe(IDENTITY_ADDITIONAL_PRIVILEGE.LIST.projectSlug), 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)
}), }),
response: { response: {
200: z.object({ 200: z.object({
privileges: IdentityProjectAdditionalPrivilegeSchema.array() privileges: SanitizedIdentityPrivilegeSchema.array()
}) })
} }
}, },
@@ -315,15 +308,9 @@ export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: F
actorOrgId: req.permission.orgId, actorOrgId: req.permission.orgId,
...req.query ...req.query
}); });
if (req.query.unpacked) { return {
return { privileges
privileges: privileges.map(({ permissions, ...el }) => ({ };
...el,
permissions: unpackRules(permissions as PackRule<RawRuleOf<MongoAbility<ProjectPermissionSet>>>[])
}))
};
}
return { privileges };
} }
}); });
}; };
+5
View File
@@ -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 { registerAuditLogStreamRouter } from "./audit-log-stream-router";
import { registerDynamicSecretLeaseRouter } from "./dynamic-secret-lease-router"; import { registerDynamicSecretLeaseRouter } from "./dynamic-secret-lease-router";
import { registerDynamicSecretRouter } from "./dynamic-secret-router"; import { registerDynamicSecretRouter } from "./dynamic-secret-router";
@@ -41,6 +43,9 @@ export const registerV1EERoutes = async (server: FastifyZodProvider) => {
prefix: "/secret-rotation-providers" prefix: "/secret-rotation-providers"
}); });
await server.register(registerAccessApprovalPolicyRouter, { prefix: "/access-approvals/policies" });
await server.register(registerAccessApprovalRequestRouter, { prefix: "/access-approvals/requests" });
await server.register( await server.register(
async (dynamicSecretRouter) => { async (dynamicSecretRouter) => {
await dynamicSecretRouter.register(registerDynamicSecretRouter); await dynamicSecretRouter.register(registerDynamicSecretRouter);
+3 -1
View File
@@ -18,6 +18,7 @@ import { LdapConfigsSchema, LdapGroupMapsSchema } from "@app/db/schemas";
import { TLDAPConfig } from "@app/ee/services/ldap-config/ldap-config-types"; import { TLDAPConfig } from "@app/ee/services/ldap-config/ldap-config-types";
import { isValidLdapFilter, searchGroups } from "@app/ee/services/ldap-config/ldap-fns"; import { isValidLdapFilter, searchGroups } from "@app/ee/services/ldap-config/ldap-fns";
import { getConfig } from "@app/lib/config/env"; import { getConfig } from "@app/lib/config/env";
import { BadRequestError } from "@app/lib/errors";
import { logger } from "@app/lib/logger"; import { logger } from "@app/lib/logger";
import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
@@ -52,6 +53,7 @@ export const registerLdapRouter = async (server: FastifyZodProvider) => {
// eslint-disable-next-line // eslint-disable-next-line
async (req: IncomingMessage, user, cb) => { async (req: IncomingMessage, user, cb) => {
try { try {
if (!user.email) throw new BadRequestError({ message: "Invalid request. Missing email." });
const ldapConfig = (req as unknown as FastifyRequest).ldapConfig as TLDAPConfig; const ldapConfig = (req as unknown as FastifyRequest).ldapConfig as TLDAPConfig;
let groups: { dn: string; cn: string }[] | undefined; let groups: { dn: string; cn: string }[] | undefined;
@@ -74,7 +76,7 @@ export const registerLdapRouter = async (server: FastifyZodProvider) => {
username: user.uid, username: user.uid,
firstName: user.givenName ?? user.cn ?? "", firstName: user.givenName ?? user.cn ?? "",
lastName: user.sn ?? "", lastName: user.sn ?? "",
emails: user.mail ? [user.mail] : [], email: user.mail,
groups, groups,
relayState: ((req as unknown as FastifyRequest).body as { RelayState?: string }).RelayState, relayState: ((req as unknown as FastifyRequest).body as { RelayState?: string }).RelayState,
orgId: (req as unknown as FastifyRequest).ldapConfig.organization orgId: (req as unknown as FastifyRequest).ldapConfig.organization
+2 -2
View File
@@ -102,12 +102,12 @@ export const registerSamlRouter = async (server: FastifyZodProvider) => {
if (!profile) throw new BadRequestError({ message: "Missing profile" }); if (!profile) throw new BadRequestError({ message: "Missing profile" });
const email = profile?.email ?? (profile?.emailAddress as string); // emailRippling is added because in Rippling the field `email` reserved const email = profile?.email ?? (profile?.emailAddress as string); // emailRippling is added because in Rippling the field `email` reserved
if (!profile.email || !profile.firstName) { if (!email || !profile.firstName) {
throw new BadRequestError({ message: "Invalid request. Missing email or first name" }); throw new BadRequestError({ message: "Invalid request. Missing email or first name" });
} }
const { isUserCompleted, providerAuthToken } = await server.services.saml.samlLogin({ const { isUserCompleted, providerAuthToken } = await server.services.saml.samlLogin({
username: profile.nameID ?? email, externalId: profile.nameID,
email, email,
firstName: profile.firstName as string, firstName: profile.firstName as string,
lastName: profile.lastName as string, lastName: profile.lastName as string,
+14 -14
View File
@@ -153,7 +153,7 @@ export const registerScimRouter = async (server: FastifyZodProvider) => {
onRequest: verifyAuth([AuthMode.SCIM_TOKEN]), onRequest: verifyAuth([AuthMode.SCIM_TOKEN]),
handler: async (req) => { handler: async (req) => {
const users = await req.server.services.scim.listScimUsers({ const users = await req.server.services.scim.listScimUsers({
offset: req.query.startIndex, startIndex: req.query.startIndex,
limit: req.query.count, limit: req.query.count,
filter: req.query.filter, filter: req.query.filter,
orgId: req.permission.orgId orgId: req.permission.orgId
@@ -163,11 +163,11 @@ export const registerScimRouter = async (server: FastifyZodProvider) => {
}); });
server.route({ server.route({
url: "/Users/:userId", url: "/Users/:orgMembershipId",
method: "GET", method: "GET",
schema: { schema: {
params: z.object({ params: z.object({
userId: z.string().trim() orgMembershipId: z.string().trim()
}), }),
response: { response: {
201: z.object({ 201: z.object({
@@ -193,7 +193,7 @@ export const registerScimRouter = async (server: FastifyZodProvider) => {
onRequest: verifyAuth([AuthMode.SCIM_TOKEN]), onRequest: verifyAuth([AuthMode.SCIM_TOKEN]),
handler: async (req) => { handler: async (req) => {
const user = await req.server.services.scim.getScimUser({ const user = await req.server.services.scim.getScimUser({
userId: req.params.userId, orgMembershipId: req.params.orgMembershipId,
orgId: req.permission.orgId orgId: req.permission.orgId
}); });
return user; return user;
@@ -249,7 +249,7 @@ export const registerScimRouter = async (server: FastifyZodProvider) => {
const primaryEmail = req.body.emails?.find((email) => email.primary)?.value; const primaryEmail = req.body.emails?.find((email) => email.primary)?.value;
const user = await req.server.services.scim.createScimUser({ const user = await req.server.services.scim.createScimUser({
username: req.body.userName, externalId: req.body.userName,
email: primaryEmail, email: primaryEmail,
firstName: req.body.name.givenName, firstName: req.body.name.givenName,
lastName: req.body.name.familyName, lastName: req.body.name.familyName,
@@ -261,11 +261,11 @@ export const registerScimRouter = async (server: FastifyZodProvider) => {
}); });
server.route({ server.route({
url: "/Users/:userId", url: "/Users/:orgMembershipId",
method: "DELETE", method: "DELETE",
schema: { schema: {
params: z.object({ params: z.object({
userId: z.string().trim() orgMembershipId: z.string().trim()
}), }),
response: { response: {
200: z.object({}) 200: z.object({})
@@ -274,7 +274,7 @@ export const registerScimRouter = async (server: FastifyZodProvider) => {
onRequest: verifyAuth([AuthMode.SCIM_TOKEN]), onRequest: verifyAuth([AuthMode.SCIM_TOKEN]),
handler: async (req) => { handler: async (req) => {
const user = await req.server.services.scim.deleteScimUser({ const user = await req.server.services.scim.deleteScimUser({
userId: req.params.userId, orgMembershipId: req.params.orgMembershipId,
orgId: req.permission.orgId orgId: req.permission.orgId
}); });
@@ -361,7 +361,7 @@ export const registerScimRouter = async (server: FastifyZodProvider) => {
handler: async (req) => { handler: async (req) => {
const groups = await req.server.services.scim.listScimGroups({ const groups = await req.server.services.scim.listScimGroups({
orgId: req.permission.orgId, orgId: req.permission.orgId,
offset: req.query.startIndex, startIndex: req.query.startIndex,
limit: req.query.count limit: req.query.count
}); });
@@ -416,10 +416,10 @@ export const registerScimRouter = async (server: FastifyZodProvider) => {
displayName: z.string().trim(), displayName: z.string().trim(),
members: z.array( members: z.array(
z.object({ z.object({
value: z.string(), // infisical userId value: z.string(), // infisical orgMembershipId
display: z.string() display: z.string()
}) })
) // note: is this where members are added to group? )
}), }),
response: { response: {
200: z.object({ 200: z.object({
@@ -534,11 +534,11 @@ export const registerScimRouter = async (server: FastifyZodProvider) => {
}); });
server.route({ server.route({
url: "/Users/:userId", url: "/Users/:orgMembershipId",
method: "PUT", method: "PUT",
schema: { schema: {
params: z.object({ params: z.object({
userId: z.string().trim() orgMembershipId: z.string().trim()
}), }),
body: z.object({ body: z.object({
schemas: z.array(z.string()), schemas: z.array(z.string()),
@@ -575,7 +575,7 @@ export const registerScimRouter = async (server: FastifyZodProvider) => {
onRequest: verifyAuth([AuthMode.SCIM_TOKEN]), onRequest: verifyAuth([AuthMode.SCIM_TOKEN]),
handler: async (req) => { handler: async (req) => {
const user = await req.server.services.scim.replaceScimUser({ const user = await req.server.services.scim.replaceScimUser({
userId: req.params.userId, orgMembershipId: req.params.orgMembershipId,
orgId: req.permission.orgId, orgId: req.permission.orgId,
active: req.body.active active: req.body.active
}); });
@@ -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)
};
};
@@ -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">;
@@ -70,6 +70,10 @@ export enum EventType {
CREATE_IDENTITY_UNIVERSAL_AUTH_CLIENT_SECRET = "create-identity-universal-auth-client-secret", CREATE_IDENTITY_UNIVERSAL_AUTH_CLIENT_SECRET = "create-identity-universal-auth-client-secret",
REVOKE_IDENTITY_UNIVERSAL_AUTH_CLIENT_SECRET = "revoke-identity-universal-auth-client-secret", REVOKE_IDENTITY_UNIVERSAL_AUTH_CLIENT_SECRET = "revoke-identity-universal-auth-client-secret",
GET_IDENTITY_UNIVERSAL_AUTH_CLIENT_SECRETS = "get-identity-universal-auth-client-secret", GET_IDENTITY_UNIVERSAL_AUTH_CLIENT_SECRETS = "get-identity-universal-auth-client-secret",
LOGIN_IDENTITY_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", CREATE_ENVIRONMENT = "create-environment",
UPDATE_ENVIRONMENT = "update-environment", UPDATE_ENVIRONMENT = "update-environment",
DELETE_ENVIRONMENT = "delete-environment", DELETE_ENVIRONMENT = "delete-environment",
@@ -454,6 +458,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 { interface CreateEnvironmentEvent {
type: EventType.CREATE_ENVIRONMENT; type: EventType.CREATE_ENVIRONMENT;
metadata: { metadata: {
@@ -712,6 +760,10 @@ export type Event =
| CreateIdentityUniversalAuthClientSecretEvent | CreateIdentityUniversalAuthClientSecretEvent
| GetIdentityUniversalAuthClientSecretsEvent | GetIdentityUniversalAuthClientSecretsEvent
| RevokeIdentityUniversalAuthClientSecretEvent | RevokeIdentityUniversalAuthClientSecretEvent
| LoginIdentityAwsAuthEvent
| AddIdentityAwsAuthEvent
| UpdateIdentityAwsAuthEvent
| GetIdentityAwsAuthEvent
| CreateEnvironmentEvent | CreateEnvironmentEvent
| UpdateEnvironmentEvent | UpdateEnvironmentEvent
| DeleteEnvironmentEvent | DeleteEnvironmentEvent
+3 -3
View File
@@ -1,6 +1,6 @@
import { Knex } from "knex"; import { Knex } from "knex";
import { SecretKeyEncoding, TUsers } from "@app/db/schemas"; import { SecretKeyEncoding, TableName, TUsers } from "@app/db/schemas";
import { decryptAsymmetric, encryptAsymmetric, infisicalSymmetricDecrypt } from "@app/lib/crypto/encryption"; import { decryptAsymmetric, encryptAsymmetric, infisicalSymmetricDecrypt } from "@app/lib/crypto/encryption";
import { BadRequestError, ScimRequestError } from "@app/lib/errors"; import { BadRequestError, ScimRequestError } from "@app/lib/errors";
@@ -188,9 +188,9 @@ export const addUsersToGroupByUserIds = async ({
// check if all user(s) are part of the organization // check if all user(s) are part of the organization
const existingUserOrgMemberships = await orgDAL.findMembership( const existingUserOrgMemberships = await orgDAL.findMembership(
{ {
orgId: group.orgId, [`${TableName.OrgMembership}.orgId` as "orgId"]: group.orgId,
$in: { $in: {
userId: userIds [`${TableName.OrgMembership}.userId` as "userId"]: userIds
} }
}, },
{ tx } { tx }
@@ -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 ms from "ms";
import { z } from "zod";
import { isAtLeastAsPrivileged } from "@app/lib/casl"; import { isAtLeastAsPrivileged } from "@app/lib/casl";
import { BadRequestError, ForbiddenRequestError } from "@app/lib/errors"; 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 { TProjectDALFactory } from "@app/services/project/project-dal";
import { TPermissionServiceFactory } from "../permission/permission-service"; 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 { TIdentityProjectAdditionalPrivilegeDALFactory } from "./identity-project-additional-privilege-dal";
import { import {
IdentityProjectAdditionalPrivilegeTemporaryMode, IdentityProjectAdditionalPrivilegeTemporaryMode,
@@ -30,6 +32,27 @@ export type TIdentityProjectAdditionalPrivilegeServiceFactory = ReturnType<
typeof identityProjectAdditionalPrivilegeServiceFactory 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 = ({ export const identityProjectAdditionalPrivilegeServiceFactory = ({
identityProjectAdditionalPrivilegeDAL, identityProjectAdditionalPrivilegeDAL,
identityProjectDAL, identityProjectDAL,
@@ -86,7 +109,10 @@ export const identityProjectAdditionalPrivilegeServiceFactory = ({
slug, slug,
permissions: customPermission permissions: customPermission
}); });
return additionalPrivilege; return {
...additionalPrivilege,
permissions: unpackPermissions(additionalPrivilege.permissions)
};
} }
const relativeTempAllocatedTimeInMs = ms(dto.temporaryRange); const relativeTempAllocatedTimeInMs = ms(dto.temporaryRange);
@@ -100,7 +126,10 @@ export const identityProjectAdditionalPrivilegeServiceFactory = ({
temporaryAccessStartTime: new Date(dto.temporaryAccessStartTime), temporaryAccessStartTime: new Date(dto.temporaryAccessStartTime),
temporaryAccessEndTime: new Date(new Date(dto.temporaryAccessStartTime).getTime() + relativeTempAllocatedTimeInMs) temporaryAccessEndTime: new Date(new Date(dto.temporaryAccessStartTime).getTime() + relativeTempAllocatedTimeInMs)
}); });
return additionalPrivilege; return {
...additionalPrivilege,
permissions: unpackPermissions(additionalPrivilege.permissions)
};
}; };
const updateBySlug = async ({ const updateBySlug = async ({
@@ -163,7 +192,11 @@ export const identityProjectAdditionalPrivilegeServiceFactory = ({
temporaryAccessStartTime: new Date(temporaryAccessStartTime || ""), temporaryAccessStartTime: new Date(temporaryAccessStartTime || ""),
temporaryAccessEndTime: new Date(new Date(temporaryAccessStartTime || "").getTime() + ms(temporaryRange || "")) temporaryAccessEndTime: new Date(new Date(temporaryAccessStartTime || "").getTime() + ms(temporaryRange || ""))
}); });
return additionalPrivilege; return {
...additionalPrivilege,
permissions: unpackPermissions(additionalPrivilege.permissions)
};
} }
const additionalPrivilege = await identityProjectAdditionalPrivilegeDAL.updateById(identityPrivilege.id, { const additionalPrivilege = await identityProjectAdditionalPrivilegeDAL.updateById(identityPrivilege.id, {
@@ -174,7 +207,11 @@ export const identityProjectAdditionalPrivilegeServiceFactory = ({
temporaryRange: null, temporaryRange: null,
temporaryMode: null temporaryMode: null
}); });
return additionalPrivilege; return {
...additionalPrivilege,
permissions: unpackPermissions(additionalPrivilege.permissions)
};
}; };
const deleteBySlug = async ({ const deleteBySlug = async ({
@@ -220,7 +257,11 @@ export const identityProjectAdditionalPrivilegeServiceFactory = ({
if (!identityPrivilege) throw new BadRequestError({ message: "Identity additional privilege not found" }); if (!identityPrivilege) throw new BadRequestError({ message: "Identity additional privilege not found" });
const deletedPrivilege = await identityProjectAdditionalPrivilegeDAL.deleteById(identityPrivilege.id); const deletedPrivilege = await identityProjectAdditionalPrivilegeDAL.deleteById(identityPrivilege.id);
return deletedPrivilege; return {
...deletedPrivilege,
permissions: unpackPermissions(deletedPrivilege.permissions)
};
}; };
const getPrivilegeDetailsBySlug = async ({ const getPrivilegeDetailsBySlug = async ({
@@ -254,7 +295,10 @@ export const identityProjectAdditionalPrivilegeServiceFactory = ({
}); });
if (!identityPrivilege) throw new BadRequestError({ message: "Identity additional privilege not found" }); if (!identityPrivilege) throw new BadRequestError({ message: "Identity additional privilege not found" });
return identityPrivilege; return {
...identityPrivilege,
permissions: unpackPermissions(identityPrivilege.permissions)
};
}; };
const listIdentityProjectPrivileges = async ({ const listIdentityProjectPrivileges = async ({
@@ -284,7 +328,11 @@ export const identityProjectAdditionalPrivilegeServiceFactory = ({
const identityPrivileges = await identityProjectAdditionalPrivilegeDAL.find({ const identityPrivileges = await identityProjectAdditionalPrivilegeDAL.find({
projectMembershipId: identityProjectMembership.id projectMembershipId: identityProjectMembership.id
}); });
return identityPrivileges; return identityPrivileges.map((el) => ({
...el,
permissions: unpackPermissions(el.permissions)
}));
}; };
return { return {
@@ -1,7 +1,14 @@
import { ForbiddenError } from "@casl/ability"; import { ForbiddenError } from "@casl/ability";
import jwt from "jsonwebtoken"; import jwt from "jsonwebtoken";
import { OrgMembershipRole, OrgMembershipStatus, SecretKeyEncoding, TLdapConfigsUpdate } from "@app/db/schemas"; import {
OrgMembershipRole,
OrgMembershipStatus,
SecretKeyEncoding,
TableName,
TLdapConfigsUpdate,
TUsers
} from "@app/db/schemas";
import { TGroupDALFactory } from "@app/ee/services/group/group-dal"; import { TGroupDALFactory } from "@app/ee/services/group/group-dal";
import { addUsersToGroupByUserIds, removeUsersFromGroupByUserIds } from "@app/ee/services/group/group-fns"; import { addUsersToGroupByUserIds, removeUsersFromGroupByUserIds } from "@app/ee/services/group/group-fns";
import { TUserGroupMembershipDALFactory } from "@app/ee/services/group/user-group-membership-dal"; import { TUserGroupMembershipDALFactory } from "@app/ee/services/group/user-group-membership-dal";
@@ -19,12 +26,15 @@ import { AuthMethod, AuthTokenType } from "@app/services/auth/auth-type";
import { TGroupProjectDALFactory } from "@app/services/group-project/group-project-dal"; import { TGroupProjectDALFactory } from "@app/services/group-project/group-project-dal";
import { TOrgBotDALFactory } from "@app/services/org/org-bot-dal"; import { TOrgBotDALFactory } from "@app/services/org/org-bot-dal";
import { TOrgDALFactory } from "@app/services/org/org-dal"; import { TOrgDALFactory } from "@app/services/org/org-dal";
import { TOrgMembershipDALFactory } from "@app/services/org-membership/org-membership-dal";
import { TProjectDALFactory } from "@app/services/project/project-dal"; import { TProjectDALFactory } from "@app/services/project/project-dal";
import { TProjectBotDALFactory } from "@app/services/project-bot/project-bot-dal"; import { TProjectBotDALFactory } from "@app/services/project-bot/project-bot-dal";
import { TProjectKeyDALFactory } from "@app/services/project-key/project-key-dal"; import { TProjectKeyDALFactory } from "@app/services/project-key/project-key-dal";
import { getServerCfg } from "@app/services/super-admin/super-admin-service";
import { TUserDALFactory } from "@app/services/user/user-dal"; import { TUserDALFactory } from "@app/services/user/user-dal";
import { normalizeUsername } from "@app/services/user/user-fns"; import { normalizeUsername } from "@app/services/user/user-fns";
import { TUserAliasDALFactory } from "@app/services/user-alias/user-alias-dal"; import { TUserAliasDALFactory } from "@app/services/user-alias/user-alias-dal";
import { UserAliasType } from "@app/services/user-alias/user-alias-types";
import { TLicenseServiceFactory } from "../license/license-service"; import { TLicenseServiceFactory } from "../license/license-service";
import { OrgPermissionActions, OrgPermissionSubjects } from "../permission/org-permission"; import { OrgPermissionActions, OrgPermissionSubjects } from "../permission/org-permission";
@@ -46,6 +56,7 @@ import { TLdapGroupMapDALFactory } from "./ldap-group-map-dal";
type TLdapConfigServiceFactoryDep = { type TLdapConfigServiceFactoryDep = {
ldapConfigDAL: Pick<TLdapConfigDALFactory, "create" | "update" | "findOne">; ldapConfigDAL: Pick<TLdapConfigDALFactory, "create" | "update" | "findOne">;
ldapGroupMapDAL: Pick<TLdapGroupMapDALFactory, "find" | "create" | "delete" | "findLdapGroupMapsByLdapConfigId">; ldapGroupMapDAL: Pick<TLdapGroupMapDALFactory, "find" | "create" | "delete" | "findLdapGroupMapsByLdapConfigId">;
orgMembershipDAL: Pick<TOrgMembershipDALFactory, "create">;
orgDAL: Pick< orgDAL: Pick<
TOrgDALFactory, TOrgDALFactory,
"createMembership" | "updateMembershipById" | "findMembership" | "findOrgById" | "findOne" | "updateById" "createMembership" | "updateMembershipById" | "findMembership" | "findOrgById" | "findOne" | "updateById"
@@ -75,6 +86,7 @@ export const ldapConfigServiceFactory = ({
ldapConfigDAL, ldapConfigDAL,
ldapGroupMapDAL, ldapGroupMapDAL,
orgDAL, orgDAL,
orgMembershipDAL,
orgBotDAL, orgBotDAL,
groupDAL, groupDAL,
groupProjectDAL, groupProjectDAL,
@@ -379,16 +391,17 @@ export const ldapConfigServiceFactory = ({
username, username,
firstName, firstName,
lastName, lastName,
emails, email,
groups, groups,
orgId, orgId,
relayState relayState
}: TLdapLoginDTO) => { }: TLdapLoginDTO) => {
const appCfg = getConfig(); const appCfg = getConfig();
const serverCfg = await getServerCfg();
let userAlias = await userAliasDAL.findOne({ let userAlias = await userAliasDAL.findOne({
externalId, externalId,
orgId, orgId,
aliasType: AuthMethod.LDAP aliasType: UserAliasType.LDAP
}); });
const organization = await orgDAL.findOrgById(orgId); const organization = await orgDAL.findOrgById(orgId);
@@ -396,7 +409,13 @@ export const ldapConfigServiceFactory = ({
if (userAlias) { if (userAlias) {
await userDAL.transaction(async (tx) => { await userDAL.transaction(async (tx) => {
const [orgMembership] = await orgDAL.findMembership({ userId: userAlias.userId }, { tx }); const [orgMembership] = await orgDAL.findMembership(
{
[`${TableName.OrgMembership}.userId` as "userId"]: userAlias.userId,
[`${TableName.OrgMembership}.orgId` as "id"]: orgId
},
{ tx }
);
if (!orgMembership) { if (!orgMembership) {
await orgDAL.createMembership( await orgDAL.createMembership(
{ {
@@ -419,40 +438,75 @@ export const ldapConfigServiceFactory = ({
}); });
} else { } else {
userAlias = await userDAL.transaction(async (tx) => { userAlias = await userDAL.transaction(async (tx) => {
const uniqueUsername = await normalizeUsername(username, userDAL); let newUser: TUsers | undefined;
const newUser = await userDAL.create( if (serverCfg.trustSamlEmails) {
{ newUser = await userDAL.findOne(
username: uniqueUsername, {
email: emails[0], email,
firstName, isEmailVerified: true
lastName, },
authMethods: [AuthMethod.LDAP], tx
isGhost: false );
}, }
tx
); if (!newUser) {
const uniqueUsername = await normalizeUsername(username, userDAL);
newUser = await userDAL.create(
{
username: serverCfg.trustLdapEmails ? email : uniqueUsername,
email,
isEmailVerified: serverCfg.trustLdapEmails,
firstName,
lastName,
authMethods: [],
isGhost: false
},
tx
);
}
const newUserAlias = await userAliasDAL.create( const newUserAlias = await userAliasDAL.create(
{ {
userId: newUser.id, userId: newUser.id,
username, username,
aliasType: AuthMethod.LDAP, aliasType: UserAliasType.LDAP,
externalId, externalId,
emails, emails: [email],
orgId orgId
}, },
tx tx
); );
await orgDAL.createMembership( const [orgMembership] = await orgDAL.findMembership(
{ {
userId: newUser.id, [`${TableName.OrgMembership}.userId` as "userId"]: newUser.id,
orgId, [`${TableName.OrgMembership}.orgId` as "id"]: orgId
role: OrgMembershipRole.Member,
status: OrgMembershipStatus.Invited
}, },
tx { tx }
); );
if (!orgMembership) {
await orgMembershipDAL.create(
{
userId: userAlias.userId,
inviteEmail: email,
orgId,
role: OrgMembershipRole.Member,
status: newUser.isAccepted ? OrgMembershipStatus.Accepted : OrgMembershipStatus.Invited // if user is fully completed, then set status to accepted, otherwise set it to invited so we can update it later
},
tx
);
// Only update the membership to Accepted if the user account is already completed.
} else if (orgMembership.status === OrgMembershipStatus.Invited && newUser.isAccepted) {
await orgDAL.updateMembershipById(
orgMembership.id,
{
status: OrgMembershipStatus.Accepted
},
tx
);
}
return newUserAlias; return newUserAlias;
}); });
} }
@@ -543,11 +597,14 @@ export const ldapConfigServiceFactory = ({
authTokenType: AuthTokenType.PROVIDER_TOKEN, authTokenType: AuthTokenType.PROVIDER_TOKEN,
userId: user.id, userId: user.id,
username: user.username, username: user.username,
...(user.email && { email: user.email, isEmailVerified: user.isEmailVerified }),
firstName, firstName,
lastName, lastName,
organizationName: organization.name, organizationName: organization.name,
organizationId: organization.id, organizationId: organization.id,
organizationSlug: organization.slug,
authMethod: AuthMethod.LDAP, authMethod: AuthMethod.LDAP,
authType: UserAliasType.LDAP,
isUserCompleted, isUserCompleted,
...(relayState ...(relayState
? { ? {
@@ -51,7 +51,7 @@ export type TLdapLoginDTO = {
username: string; username: string;
firstName: string; firstName: string;
lastName: string; lastName: string;
emails: string[]; email: string;
orgId: string; orgId: string;
groups?: { groups?: {
dn: string; dn: string;
@@ -121,8 +121,8 @@ export const licenseServiceFactory = ({
if (isValidOfflineLicense) { if (isValidOfflineLicense) {
onPremFeatures = contents.license.features; onPremFeatures = contents.license.features;
instanceType = InstanceType.EnterpriseOnPrem; instanceType = InstanceType.EnterpriseOnPremOffline;
logger.info(`Instance type: ${InstanceType.EnterpriseOnPrem}`); logger.info(`Instance type: ${InstanceType.EnterpriseOnPremOffline}`);
isValidLicense = true; isValidLicense = true;
return; return;
} }
@@ -3,6 +3,7 @@ import { TOrgPermission } from "@app/lib/types";
export enum InstanceType { export enum InstanceType {
OnPrem = "self-hosted", OnPrem = "self-hosted",
EnterpriseOnPrem = "enterprise-self-hosted", EnterpriseOnPrem = "enterprise-self-hosted",
EnterpriseOnPremOffline = "enterprise-self-hosted-offline",
Cloud = "cloud" Cloud = "cloud"
} }
@@ -7,7 +7,8 @@ import {
SecretKeyEncoding, SecretKeyEncoding,
TableName, TableName,
TSamlConfigs, TSamlConfigs,
TSamlConfigsUpdate TSamlConfigsUpdate,
TUsers
} from "@app/db/schemas"; } from "@app/db/schemas";
import { getConfig } from "@app/lib/config/env"; import { getConfig } from "@app/lib/config/env";
import { import {
@@ -19,10 +20,18 @@ import {
infisicalSymmetricEncypt infisicalSymmetricEncypt
} from "@app/lib/crypto/encryption"; } from "@app/lib/crypto/encryption";
import { BadRequestError } from "@app/lib/errors"; import { BadRequestError } from "@app/lib/errors";
import { AuthMethod, AuthTokenType } from "@app/services/auth/auth-type"; import { AuthTokenType } from "@app/services/auth/auth-type";
import { TAuthTokenServiceFactory } from "@app/services/auth-token/auth-token-service";
import { TokenType } from "@app/services/auth-token/auth-token-types";
import { TOrgBotDALFactory } from "@app/services/org/org-bot-dal"; import { TOrgBotDALFactory } from "@app/services/org/org-bot-dal";
import { TOrgDALFactory } from "@app/services/org/org-dal"; import { TOrgDALFactory } from "@app/services/org/org-dal";
import { TOrgMembershipDALFactory } from "@app/services/org-membership/org-membership-dal";
import { SmtpTemplates, TSmtpService } from "@app/services/smtp/smtp-service";
import { getServerCfg } from "@app/services/super-admin/super-admin-service";
import { TUserDALFactory } from "@app/services/user/user-dal"; import { TUserDALFactory } from "@app/services/user/user-dal";
import { normalizeUsername } from "@app/services/user/user-fns";
import { TUserAliasDALFactory } from "@app/services/user-alias/user-alias-dal";
import { UserAliasType } from "@app/services/user-alias/user-alias-types";
import { TLicenseServiceFactory } from "../license/license-service"; import { TLicenseServiceFactory } from "../license/license-service";
import { OrgPermissionActions, OrgPermissionSubjects } from "../permission/org-permission"; import { OrgPermissionActions, OrgPermissionSubjects } from "../permission/org-permission";
@@ -31,15 +40,19 @@ import { TSamlConfigDALFactory } from "./saml-config-dal";
import { TCreateSamlCfgDTO, TGetSamlCfgDTO, TSamlLoginDTO, TUpdateSamlCfgDTO } from "./saml-config-types"; import { TCreateSamlCfgDTO, TGetSamlCfgDTO, TSamlLoginDTO, TUpdateSamlCfgDTO } from "./saml-config-types";
type TSamlConfigServiceFactoryDep = { type TSamlConfigServiceFactoryDep = {
samlConfigDAL: TSamlConfigDALFactory; samlConfigDAL: Pick<TSamlConfigDALFactory, "create" | "findOne" | "update" | "findById">;
userDAL: Pick<TUserDALFactory, "create" | "findOne" | "transaction" | "updateById">; userDAL: Pick<TUserDALFactory, "create" | "findOne" | "transaction" | "updateById" | "findById">;
userAliasDAL: Pick<TUserAliasDALFactory, "create" | "findOne">;
orgDAL: Pick< orgDAL: Pick<
TOrgDALFactory, TOrgDALFactory,
"createMembership" | "updateMembershipById" | "findMembership" | "findOrgById" | "findOne" | "updateById" "createMembership" | "updateMembershipById" | "findMembership" | "findOrgById" | "findOne" | "updateById"
>; >;
orgMembershipDAL: Pick<TOrgMembershipDALFactory, "create">;
orgBotDAL: Pick<TOrgBotDALFactory, "findOne" | "create" | "transaction">; orgBotDAL: Pick<TOrgBotDALFactory, "findOne" | "create" | "transaction">;
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">; permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">;
licenseService: Pick<TLicenseServiceFactory, "getPlan">; licenseService: Pick<TLicenseServiceFactory, "getPlan">;
tokenService: Pick<TAuthTokenServiceFactory, "createTokenForUser">;
smtpService: Pick<TSmtpService, "sendMail">;
}; };
export type TSamlConfigServiceFactory = ReturnType<typeof samlConfigServiceFactory>; export type TSamlConfigServiceFactory = ReturnType<typeof samlConfigServiceFactory>;
@@ -48,9 +61,13 @@ export const samlConfigServiceFactory = ({
samlConfigDAL, samlConfigDAL,
orgBotDAL, orgBotDAL,
orgDAL, orgDAL,
orgMembershipDAL,
userDAL, userDAL,
userAliasDAL,
permissionService, permissionService,
licenseService licenseService,
tokenService,
smtpService
}: TSamlConfigServiceFactoryDep) => { }: TSamlConfigServiceFactoryDep) => {
const createSamlCfg = async ({ const createSamlCfg = async ({
cert, cert,
@@ -305,7 +322,7 @@ export const samlConfigServiceFactory = ({
}; };
const samlLogin = async ({ const samlLogin = async ({
username, externalId,
email, email,
firstName, firstName,
lastName, lastName,
@@ -314,38 +331,40 @@ export const samlConfigServiceFactory = ({
relayState relayState
}: TSamlLoginDTO) => { }: TSamlLoginDTO) => {
const appCfg = getConfig(); const appCfg = getConfig();
let user = await userDAL.findOne({ username }); const serverCfg = await getServerCfg();
const userAlias = await userAliasDAL.findOne({
externalId,
orgId,
aliasType: UserAliasType.SAML
});
const organization = await orgDAL.findOrgById(orgId); const organization = await orgDAL.findOrgById(orgId);
if (!organization) throw new BadRequestError({ message: "Org not found" }); if (!organization) throw new BadRequestError({ message: "Org not found" });
// TODO(dangtony98): remove this after aliases update let user: TUsers;
if (authProvider === AuthMethod.KEYCLOAK_SAML && appCfg.LICENSE_SERVER_KEY) { if (userAlias) {
throw new BadRequestError({ message: "Keycloak SAML is not yet available on Infisical Cloud" }); user = await userDAL.transaction(async (tx) => {
} const foundUser = await userDAL.findById(userAlias.userId, tx);
if (user) {
await userDAL.transaction(async (tx) => {
const [orgMembership] = await orgDAL.findMembership( const [orgMembership] = await orgDAL.findMembership(
{ {
userId: user.id, [`${TableName.OrgMembership}.userId` as "userId"]: foundUser.id,
[`${TableName.OrgMembership}.orgId` as "id"]: orgId [`${TableName.OrgMembership}.orgId` as "id"]: orgId
}, },
{ tx } { tx }
); );
if (!orgMembership) { if (!orgMembership) {
await orgDAL.createMembership( await orgMembershipDAL.create(
{ {
userId: user.id, userId: userAlias.userId,
orgId,
inviteEmail: email, inviteEmail: email,
orgId,
role: OrgMembershipRole.Member, role: OrgMembershipRole.Member,
status: user.isAccepted ? OrgMembershipStatus.Accepted : OrgMembershipStatus.Invited // if user is fully completed, then set status to accepted, otherwise set it to invited so we can update it later status: foundUser.isAccepted ? OrgMembershipStatus.Accepted : OrgMembershipStatus.Invited // if user is fully completed, then set status to accepted, otherwise set it to invited so we can update it later
}, },
tx tx
); );
// Only update the membership to Accepted if the user account is already completed. // Only update the membership to Accepted if the user account is already completed.
} else if (orgMembership.status === OrgMembershipStatus.Invited && user.isAccepted) { } else if (orgMembership.status === OrgMembershipStatus.Invited && foundUser.isAccepted) {
await orgDAL.updateMembershipById( await orgDAL.updateMembershipById(
orgMembership.id, orgMembership.id,
{ {
@@ -354,40 +373,97 @@ export const samlConfigServiceFactory = ({
tx tx
); );
} }
return foundUser;
}); });
} else { } else {
user = await userDAL.transaction(async (tx) => { user = await userDAL.transaction(async (tx) => {
const newUser = await userDAL.create( let newUser: TUsers | undefined;
if (serverCfg.trustSamlEmails) {
newUser = await userDAL.findOne(
{
email,
isEmailVerified: true
},
tx
);
}
if (!newUser) {
const uniqueUsername = await normalizeUsername(`${firstName ?? ""}-${lastName ?? ""}`, userDAL);
newUser = await userDAL.create(
{
username: serverCfg.trustSamlEmails ? email : uniqueUsername,
email,
isEmailVerified: serverCfg.trustSamlEmails,
firstName,
lastName,
authMethods: [],
isGhost: false
},
tx
);
}
await userAliasDAL.create(
{ {
username, userId: newUser.id,
email, aliasType: UserAliasType.SAML,
firstName, externalId,
lastName, emails: email ? [email] : [],
authMethods: [AuthMethod.EMAIL], orgId
isGhost: false
}, },
tx tx
); );
await orgDAL.createMembership({
inviteEmail: email, const [orgMembership] = await orgDAL.findMembership(
orgId, {
role: OrgMembershipRole.Member, [`${TableName.OrgMembership}.userId` as "userId"]: newUser.id,
status: OrgMembershipStatus.Invited [`${TableName.OrgMembership}.orgId` as "id"]: orgId
}); },
{ tx }
);
if (!orgMembership) {
await orgMembershipDAL.create(
{
userId: newUser.id,
inviteEmail: email,
orgId,
role: OrgMembershipRole.Member,
status: newUser.isAccepted ? OrgMembershipStatus.Accepted : OrgMembershipStatus.Invited // if user is fully completed, then set status to accepted, otherwise set it to invited so we can update it later
},
tx
);
// Only update the membership to Accepted if the user account is already completed.
} else if (orgMembership.status === OrgMembershipStatus.Invited && newUser.isAccepted) {
await orgDAL.updateMembershipById(
orgMembership.id,
{
status: OrgMembershipStatus.Accepted
},
tx
);
}
return newUser; return newUser;
}); });
} }
const isUserCompleted = Boolean(user.isAccepted); const isUserCompleted = Boolean(user.isAccepted);
const providerAuthToken = jwt.sign( const providerAuthToken = jwt.sign(
{ {
authTokenType: AuthTokenType.PROVIDER_TOKEN, authTokenType: AuthTokenType.PROVIDER_TOKEN,
userId: user.id, userId: user.id,
username: user.username, username: user.username,
...(user.email && { email: user.email, isEmailVerified: user.isEmailVerified }),
firstName, firstName,
lastName, lastName,
organizationName: organization.name, organizationName: organization.name,
organizationId: organization.id, organizationId: organization.id,
organizationSlug: organization.slug,
authMethod: authProvider, authMethod: authProvider,
authType: UserAliasType.SAML,
isUserCompleted, isUserCompleted,
...(relayState ...(relayState
? { ? {
@@ -403,6 +479,22 @@ export const samlConfigServiceFactory = ({
await samlConfigDAL.update({ orgId }, { lastUsed: new Date() }); await samlConfigDAL.update({ orgId }, { lastUsed: new Date() });
if (user.email && !user.isEmailVerified) {
const token = await tokenService.createTokenForUser({
type: TokenType.TOKEN_EMAIL_VERIFICATION,
userId: user.id
});
await smtpService.sendMail({
template: SmtpTemplates.EmailVerification,
subjectLine: "Infisical confirmation code",
recipients: [user.email],
substitutions: {
code: token
}
});
}
return { isUserCompleted, providerAuthToken }; return { isUserCompleted, providerAuthToken };
}; };
@@ -45,8 +45,8 @@ export type TGetSamlCfgDTO =
}; };
export type TSamlLoginDTO = { export type TSamlLoginDTO = {
username: string; externalId: string;
email?: string; email: string;
firstName: string; firstName: string;
lastName?: string; lastName?: string;
authProvider: string; authProvider: string;
+9 -9
View File
@@ -2,31 +2,31 @@ import { TListScimGroups, TListScimUsers, TScimGroup, TScimUser } from "./scim-t
export const buildScimUserList = ({ export const buildScimUserList = ({
scimUsers, scimUsers,
offset, startIndex,
limit limit
}: { }: {
scimUsers: TScimUser[]; scimUsers: TScimUser[];
offset: number; startIndex: number;
limit: number; limit: number;
}): TListScimUsers => { }): TListScimUsers => {
return { return {
Resources: scimUsers, Resources: scimUsers,
itemsPerPage: limit, itemsPerPage: limit,
schemas: ["urn:ietf:params:scim:api:messages:2.0:ListResponse"], schemas: ["urn:ietf:params:scim:api:messages:2.0:ListResponse"],
startIndex: offset, startIndex,
totalResults: scimUsers.length totalResults: scimUsers.length
}; };
}; };
export const buildScimUser = ({ export const buildScimUser = ({
userId, orgMembershipId,
username, username,
email, email,
firstName, firstName,
lastName, lastName,
active active
}: { }: {
userId: string; orgMembershipId: string;
username: string; username: string;
email?: string | null; email?: string | null;
firstName: string; firstName: string;
@@ -35,7 +35,7 @@ export const buildScimUser = ({
}): TScimUser => { }): TScimUser => {
const scimUser = { const scimUser = {
schemas: ["urn:ietf:params:scim:schemas:core:2.0:User"], schemas: ["urn:ietf:params:scim:schemas:core:2.0:User"],
id: userId, id: orgMembershipId,
userName: username, userName: username,
displayName: `${firstName} ${lastName}`, displayName: `${firstName} ${lastName}`,
name: { name: {
@@ -65,18 +65,18 @@ export const buildScimUser = ({
export const buildScimGroupList = ({ export const buildScimGroupList = ({
scimGroups, scimGroups,
offset, startIndex,
limit limit
}: { }: {
scimGroups: TScimGroup[]; scimGroups: TScimGroup[];
offset: number; startIndex: number;
limit: number; limit: number;
}): TListScimGroups => { }): TListScimGroups => {
return { return {
Resources: scimGroups, Resources: scimGroups,
itemsPerPage: limit, itemsPerPage: limit,
schemas: ["urn:ietf:params:scim:api:messages:2.0:ListResponse"], schemas: ["urn:ietf:params:scim:api:messages:2.0:ListResponse"],
startIndex: offset, startIndex,
totalResults: scimGroups.length totalResults: scimGroups.length
}; };
}; };
+216 -115
View File
@@ -2,7 +2,7 @@ import { ForbiddenError } from "@casl/ability";
import slugify from "@sindresorhus/slugify"; import slugify from "@sindresorhus/slugify";
import jwt from "jsonwebtoken"; import jwt from "jsonwebtoken";
import { OrgMembershipRole, OrgMembershipStatus, TableName, TGroups } from "@app/db/schemas"; import { OrgMembershipRole, OrgMembershipStatus, TableName, TGroups, TOrgMemberships, TUsers } from "@app/db/schemas";
import { TGroupDALFactory } from "@app/ee/services/group/group-dal"; import { TGroupDALFactory } from "@app/ee/services/group/group-dal";
import { addUsersToGroupByUserIds, removeUsersFromGroupByUserIds } from "@app/ee/services/group/group-fns"; import { addUsersToGroupByUserIds, removeUsersFromGroupByUserIds } from "@app/ee/services/group/group-fns";
import { TUserGroupMembershipDALFactory } from "@app/ee/services/group/user-group-membership-dal"; import { TUserGroupMembershipDALFactory } from "@app/ee/services/group/user-group-membership-dal";
@@ -11,16 +11,21 @@ import { getConfig } from "@app/lib/config/env";
import { BadRequestError, ScimRequestError, UnauthorizedError } from "@app/lib/errors"; import { BadRequestError, ScimRequestError, UnauthorizedError } from "@app/lib/errors";
import { alphaNumericNanoId } from "@app/lib/nanoid"; import { alphaNumericNanoId } from "@app/lib/nanoid";
import { TOrgPermission } from "@app/lib/types"; import { TOrgPermission } from "@app/lib/types";
import { AuthMethod, AuthTokenType } from "@app/services/auth/auth-type"; import { AuthTokenType } from "@app/services/auth/auth-type";
import { TGroupProjectDALFactory } from "@app/services/group-project/group-project-dal"; import { TGroupProjectDALFactory } from "@app/services/group-project/group-project-dal";
import { TOrgDALFactory } from "@app/services/org/org-dal"; import { TOrgDALFactory } from "@app/services/org/org-dal";
import { deleteOrgMembership } from "@app/services/org/org-fns"; import { deleteOrgMembershipFn } from "@app/services/org/org-fns";
import { TOrgMembershipDALFactory } from "@app/services/org-membership/org-membership-dal";
import { TProjectDALFactory } from "@app/services/project/project-dal"; import { TProjectDALFactory } from "@app/services/project/project-dal";
import { TProjectBotDALFactory } from "@app/services/project-bot/project-bot-dal"; import { TProjectBotDALFactory } from "@app/services/project-bot/project-bot-dal";
import { TProjectKeyDALFactory } from "@app/services/project-key/project-key-dal"; import { TProjectKeyDALFactory } from "@app/services/project-key/project-key-dal";
import { TProjectMembershipDALFactory } from "@app/services/project-membership/project-membership-dal"; import { TProjectMembershipDALFactory } from "@app/services/project-membership/project-membership-dal";
import { SmtpTemplates, TSmtpService } from "@app/services/smtp/smtp-service"; import { SmtpTemplates, TSmtpService } from "@app/services/smtp/smtp-service";
import { getServerCfg } from "@app/services/super-admin/super-admin-service";
import { TUserDALFactory } from "@app/services/user/user-dal"; import { TUserDALFactory } from "@app/services/user/user-dal";
import { normalizeUsername } from "@app/services/user/user-fns";
import { TUserAliasDALFactory } from "@app/services/user-alias/user-alias-dal";
import { UserAliasType } from "@app/services/user-alias/user-alias-types";
import { TLicenseServiceFactory } from "../license/license-service"; import { TLicenseServiceFactory } from "../license/license-service";
import { OrgPermissionActions, OrgPermissionSubjects } from "../permission/org-permission"; import { OrgPermissionActions, OrgPermissionSubjects } from "../permission/org-permission";
@@ -47,24 +52,32 @@ import {
type TScimServiceFactoryDep = { type TScimServiceFactoryDep = {
scimDAL: Pick<TScimDALFactory, "create" | "find" | "findById" | "deleteById">; scimDAL: Pick<TScimDALFactory, "create" | "find" | "findById" | "deleteById">;
userDAL: Pick<TUserDALFactory, "find" | "findOne" | "create" | "transaction" | "findUserEncKeyByUserIdsBatch">; userDAL: Pick<
TUserDALFactory,
"find" | "findOne" | "create" | "transaction" | "findUserEncKeyByUserIdsBatch" | "findById"
>;
userAliasDAL: Pick<TUserAliasDALFactory, "findOne" | "create" | "delete">;
orgDAL: Pick< orgDAL: Pick<
TOrgDALFactory, TOrgDALFactory,
"createMembership" | "findById" | "findMembership" | "deleteMembershipById" | "transaction" "createMembership" | "findById" | "findMembership" | "deleteMembershipById" | "transaction" | "updateMembershipById"
>; >;
orgMembershipDAL: Pick<TOrgMembershipDALFactory, "find" | "findOne" | "create" | "updateById">;
projectDAL: Pick<TProjectDALFactory, "find" | "findProjectGhostUser">; projectDAL: Pick<TProjectDALFactory, "find" | "findProjectGhostUser">;
projectMembershipDAL: Pick<TProjectMembershipDALFactory, "find" | "delete">; projectMembershipDAL: Pick<TProjectMembershipDALFactory, "find" | "delete" | "findProjectMembershipsByUserId">;
groupDAL: Pick< groupDAL: Pick<
TGroupDALFactory, TGroupDALFactory,
"create" | "findOne" | "findAllGroupMembers" | "update" | "delete" | "findGroups" | "transaction" "create" | "findOne" | "findAllGroupMembers" | "update" | "delete" | "findGroups" | "transaction"
>; >;
groupProjectDAL: Pick<TGroupProjectDALFactory, "find">; groupProjectDAL: Pick<TGroupProjectDALFactory, "find">;
userGroupMembershipDAL: TUserGroupMembershipDALFactory; // TODO: Pick userGroupMembershipDAL: Pick<
TUserGroupMembershipDALFactory,
"find" | "transaction" | "insertMany" | "filterProjectsByUserMembership" | "delete"
>;
projectKeyDAL: Pick<TProjectKeyDALFactory, "find" | "findLatestProjectKey" | "insertMany" | "delete">; projectKeyDAL: Pick<TProjectKeyDALFactory, "find" | "findLatestProjectKey" | "insertMany" | "delete">;
projectBotDAL: Pick<TProjectBotDALFactory, "findOne">; projectBotDAL: Pick<TProjectBotDALFactory, "findOne">;
licenseService: Pick<TLicenseServiceFactory, "getPlan">; licenseService: Pick<TLicenseServiceFactory, "getPlan" | "updateSubscriptionOrgMemberCount">;
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">; permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">;
smtpService: TSmtpService; smtpService: Pick<TSmtpService, "sendMail">;
}; };
export type TScimServiceFactory = ReturnType<typeof scimServiceFactory>; export type TScimServiceFactory = ReturnType<typeof scimServiceFactory>;
@@ -73,7 +86,9 @@ export const scimServiceFactory = ({
licenseService, licenseService,
scimDAL, scimDAL,
userDAL, userDAL,
userAliasDAL,
orgDAL, orgDAL,
orgMembershipDAL,
projectDAL, projectDAL,
projectMembershipDAL, projectMembershipDAL,
groupDAL, groupDAL,
@@ -160,7 +175,7 @@ export const scimServiceFactory = ({
}; };
// SCIM server endpoints // SCIM server endpoints
const listScimUsers = async ({ offset, limit, filter, orgId }: TListScimUsersDTO): Promise<TListScimUsers> => { const listScimUsers = async ({ startIndex, limit, filter, orgId }: TListScimUsersDTO): Promise<TListScimUsers> => {
const org = await orgDAL.findById(orgId); const org = await orgDAL.findById(orgId);
if (!org.scimEnabled) if (!org.scimEnabled)
@@ -178,11 +193,11 @@ export const scimServiceFactory = ({
attributeName = "email"; attributeName = "email";
} }
return { [attributeName]: parsedValue }; return { [attributeName]: parsedValue.replace(/"/g, "") };
}; };
const findOpts = { const findOpts = {
...(offset && { offset }), ...(startIndex && { offset: startIndex - 1 }),
...(limit && { limit }) ...(limit && { limit })
}; };
@@ -194,10 +209,10 @@ export const scimServiceFactory = ({
findOpts findOpts
); );
const scimUsers = users.map(({ userId, username, firstName, lastName, email }) => const scimUsers = users.map(({ id, externalId, username, firstName, lastName, email }) =>
buildScimUser({ buildScimUser({
userId: userId ?? "", orgMembershipId: id ?? "",
username, username: externalId ?? username,
firstName: firstName ?? "", firstName: firstName ?? "",
lastName: lastName ?? "", lastName: lastName ?? "",
email, email,
@@ -207,16 +222,16 @@ export const scimServiceFactory = ({
return buildScimUserList({ return buildScimUserList({
scimUsers, scimUsers,
offset, startIndex,
limit limit
}); });
}; };
const getScimUser = async ({ userId, orgId }: TGetScimUserDTO) => { const getScimUser = async ({ orgMembershipId, orgId }: TGetScimUserDTO) => {
const [membership] = await orgDAL const [membership] = await orgDAL
.findMembership({ .findMembership({
userId, [`${TableName.OrgMembership}.id` as "id"]: orgMembershipId,
[`${TableName.OrgMembership}.orgId` as "id"]: orgId [`${TableName.OrgMembership}.orgId` as "orgId"]: orgId
}) })
.catch(() => { .catch(() => {
throw new ScimRequestError({ throw new ScimRequestError({
@@ -238,8 +253,8 @@ export const scimServiceFactory = ({
}); });
return buildScimUser({ return buildScimUser({
userId: membership.userId as string, orgMembershipId: membership.id,
username: membership.username, username: membership.externalId ?? membership.username,
email: membership.email ?? "", email: membership.email ?? "",
firstName: membership.firstName as string, firstName: membership.firstName as string,
lastName: membership.lastName as string, lastName: membership.lastName as string,
@@ -247,7 +262,9 @@ export const scimServiceFactory = ({
}); });
}; };
const createScimUser = async ({ username, email, firstName, lastName, orgId }: TCreateScimUserDTO) => { const createScimUser = async ({ externalId, email, firstName, lastName, orgId }: TCreateScimUserDTO) => {
if (!email) throw new ScimRequestError({ detail: "Invalid request. Missing email.", status: 400 });
const org = await orgDAL.findById(orgId); const org = await orgDAL.findById(orgId);
if (!org) if (!org)
@@ -262,67 +279,121 @@ export const scimServiceFactory = ({
status: 403 status: 403
}); });
let user = await userDAL.findOne({ const appCfg = getConfig();
username const serverCfg = await getServerCfg();
const userAlias = await userAliasDAL.findOne({
externalId,
orgId,
aliasType: UserAliasType.SAML
}); });
if (user) { const { user: createdUser, orgMembership: createdOrgMembership } = await userDAL.transaction(async (tx) => {
await userDAL.transaction(async (tx) => { let user: TUsers | undefined;
const [orgMembership] = await orgDAL.findMembership( let orgMembership: TOrgMemberships;
if (userAlias) {
user = await userDAL.findById(userAlias.userId, tx);
orgMembership = await orgMembershipDAL.findOne(
{ {
userId: user.id, userId: user.id,
[`${TableName.OrgMembership}.orgId` as "id"]: orgId orgId
}, },
{ tx } tx
); );
if (orgMembership)
throw new ScimRequestError({
detail: "User already exists in the database",
status: 409
});
if (!orgMembership) { if (!orgMembership) {
await orgDAL.createMembership( orgMembership = await orgMembershipDAL.create(
{ {
userId: user.id, userId: userAlias.userId,
orgId,
inviteEmail: email, inviteEmail: email,
orgId,
role: OrgMembershipRole.Member, role: OrgMembershipRole.Member,
status: OrgMembershipStatus.Invited status: user.isAccepted ? OrgMembershipStatus.Accepted : OrgMembershipStatus.Invited // if user is fully completed, then set status to accepted, otherwise set it to invited so we can update it later
},
tx
);
} else if (orgMembership.status === OrgMembershipStatus.Invited && user.isAccepted) {
orgMembership = await orgMembershipDAL.updateById(
orgMembership.id,
{
status: OrgMembershipStatus.Accepted
}, },
tx tx
); );
} }
}); } else {
} else { if (serverCfg.trustSamlEmails) {
user = await userDAL.transaction(async (tx) => { user = await userDAL.findOne(
const newUser = await userDAL.create( {
email,
isEmailVerified: true
},
tx
);
}
if (!user) {
const uniqueUsername = await normalizeUsername(`${firstName}-${lastName}`, userDAL);
user = await userDAL.create(
{
username: serverCfg.trustSamlEmails ? email : uniqueUsername,
email,
isEmailVerified: serverCfg.trustSamlEmails,
firstName,
lastName,
authMethods: [],
isGhost: false
},
tx
);
}
await userAliasDAL.create(
{ {
username, userId: user.id,
email, aliasType: UserAliasType.SAML,
firstName, externalId,
lastName, emails: email ? [email] : [],
authMethods: [AuthMethod.EMAIL], orgId
isGhost: false
}, },
tx tx
); );
await orgDAL.createMembership( const [foundOrgMembership] = await orgDAL.findMembership(
{ {
inviteEmail: email, [`${TableName.OrgMembership}.userId` as "userId"]: user.id,
orgId, [`${TableName.OrgMembership}.orgId` as "id"]: orgId
userId: newUser.id,
role: OrgMembershipRole.Member,
status: OrgMembershipStatus.Invited
}, },
tx { tx }
); );
return newUser;
});
}
const appCfg = getConfig(); orgMembership = foundOrgMembership;
if (!orgMembership) {
orgMembership = await orgMembershipDAL.create(
{
userId: user.id,
inviteEmail: email,
orgId,
role: OrgMembershipRole.Member,
status: user.isAccepted ? OrgMembershipStatus.Accepted : OrgMembershipStatus.Invited // if user is fully completed, then set status to accepted, otherwise set it to invited so we can update it later
},
tx
);
// Only update the membership to Accepted if the user account is already completed.
} else if (orgMembership.status === OrgMembershipStatus.Invited && user.isAccepted) {
orgMembership = await orgDAL.updateMembershipById(
orgMembership.id,
{
status: OrgMembershipStatus.Accepted
},
tx
);
}
}
return { user, orgMembership };
});
if (email) { if (email) {
await smtpService.sendMail({ await smtpService.sendMail({
@@ -337,20 +408,20 @@ export const scimServiceFactory = ({
} }
return buildScimUser({ return buildScimUser({
userId: user.id, orgMembershipId: createdOrgMembership.id,
username: user.username, username: externalId,
firstName: user.firstName as string, firstName: createdUser.firstName as string,
lastName: user.lastName as string, lastName: createdUser.lastName as string,
email: user.email ?? "", email: createdUser.email ?? "",
active: true active: true
}); });
}; };
const updateScimUser = async ({ userId, orgId, operations }: TUpdateScimUserDTO) => { const updateScimUser = async ({ orgMembershipId, orgId, operations }: TUpdateScimUserDTO) => {
const [membership] = await orgDAL const [membership] = await orgDAL
.findMembership({ .findMembership({
userId, [`${TableName.OrgMembership}.id` as "id"]: orgMembershipId,
[`${TableName.OrgMembership}.orgId` as "id"]: orgId [`${TableName.OrgMembership}.orgId` as "orgId"]: orgId
}) })
.catch(() => { .catch(() => {
throw new ScimRequestError({ throw new ScimRequestError({
@@ -386,18 +457,20 @@ export const scimServiceFactory = ({
}); });
if (!active) { if (!active) {
await deleteOrgMembership({ await deleteOrgMembershipFn({
orgMembershipId: membership.id, orgMembershipId: membership.id,
orgId: membership.orgId, orgId: membership.orgId,
orgDAL, orgDAL,
projectDAL, projectMembershipDAL,
projectMembershipDAL projectKeyDAL,
userAliasDAL,
licenseService
}); });
} }
return buildScimUser({ return buildScimUser({
userId: membership.userId as string, orgMembershipId: membership.id,
username: membership.username, username: membership.externalId ?? membership.username,
email: membership.email, email: membership.email,
firstName: membership.firstName as string, firstName: membership.firstName as string,
lastName: membership.lastName as string, lastName: membership.lastName as string,
@@ -405,11 +478,11 @@ export const scimServiceFactory = ({
}); });
}; };
const replaceScimUser = async ({ userId, active, orgId }: TReplaceScimUserDTO) => { const replaceScimUser = async ({ orgMembershipId, active, orgId }: TReplaceScimUserDTO) => {
const [membership] = await orgDAL const [membership] = await orgDAL
.findMembership({ .findMembership({
userId, [`${TableName.OrgMembership}.id` as "id"]: orgMembershipId,
[`${TableName.OrgMembership}.orgId` as "id"]: orgId [`${TableName.OrgMembership}.orgId` as "orgId"]: orgId
}) })
.catch(() => { .catch(() => {
throw new ScimRequestError({ throw new ScimRequestError({
@@ -431,19 +504,20 @@ export const scimServiceFactory = ({
}); });
if (!active) { if (!active) {
// tx await deleteOrgMembershipFn({
await deleteOrgMembership({
orgMembershipId: membership.id, orgMembershipId: membership.id,
orgId: membership.orgId, orgId: membership.orgId,
orgDAL, orgDAL,
projectDAL, projectMembershipDAL,
projectMembershipDAL projectKeyDAL,
userAliasDAL,
licenseService
}); });
} }
return buildScimUser({ return buildScimUser({
userId: membership.userId as string, orgMembershipId: membership.id,
username: membership.username, username: membership.externalId ?? membership.username,
email: membership.email, email: membership.email,
firstName: membership.firstName as string, firstName: membership.firstName as string,
lastName: membership.lastName as string, lastName: membership.lastName as string,
@@ -451,18 +525,11 @@ export const scimServiceFactory = ({
}); });
}; };
const deleteScimUser = async ({ userId, orgId }: TDeleteScimUserDTO) => { const deleteScimUser = async ({ orgMembershipId, orgId }: TDeleteScimUserDTO) => {
const [membership] = await orgDAL const [membership] = await orgDAL.findMembership({
.findMembership({ [`${TableName.OrgMembership}.id` as "id"]: orgMembershipId,
userId, [`${TableName.OrgMembership}.orgId` as "orgId"]: orgId
[`${TableName.OrgMembership}.orgId` as "id"]: orgId });
})
.catch(() => {
throw new ScimRequestError({
detail: "User not found",
status: 404
});
});
if (!membership) if (!membership)
throw new ScimRequestError({ throw new ScimRequestError({
@@ -477,18 +544,20 @@ export const scimServiceFactory = ({
}); });
} }
await deleteOrgMembership({ await deleteOrgMembershipFn({
orgMembershipId: membership.id, orgMembershipId: membership.id,
orgId: membership.orgId, orgId: membership.orgId,
orgDAL, orgDAL,
projectDAL, projectMembershipDAL,
projectMembershipDAL projectKeyDAL,
userAliasDAL,
licenseService
}); });
return {}; // intentionally return empty object upon success return {}; // intentionally return empty object upon success
}; };
const listScimGroups = async ({ orgId, offset, limit }: TListScimGroupsDTO) => { const listScimGroups = async ({ orgId, startIndex, limit }: TListScimGroupsDTO) => {
const plan = await licenseService.getPlan(orgId); const plan = await licenseService.getPlan(orgId);
if (!plan.groups) if (!plan.groups)
throw new BadRequestError({ throw new BadRequestError({
@@ -509,21 +578,27 @@ export const scimServiceFactory = ({
status: 403 status: 403
}); });
const groups = await groupDAL.findGroups({ const groups = await groupDAL.findGroups(
orgId {
}); orgId
},
{
offset: startIndex - 1,
limit
}
);
const scimGroups = groups.map((group) => const scimGroups = groups.map((group) =>
buildScimGroup({ buildScimGroup({
groupId: group.id, groupId: group.id,
name: group.name, name: group.name,
members: [] members: [] // does this need to be populated?
}) })
); );
return buildScimGroupList({ return buildScimGroupList({
scimGroups, scimGroups,
offset, startIndex,
limit limit
}); });
}; };
@@ -562,9 +637,15 @@ export const scimServiceFactory = ({
); );
if (members && members.length) { if (members && members.length) {
const orgMemberships = await orgMembershipDAL.find({
$in: {
id: members.map((member) => member.value)
}
});
const newMembers = await addUsersToGroupByUserIds({ const newMembers = await addUsersToGroupByUserIds({
group, group,
userIds: members.map((member) => member.value), userIds: orgMemberships.map((membership) => membership.userId as string),
userDAL, userDAL,
userGroupMembershipDAL, userGroupMembershipDAL,
orgDAL, orgDAL,
@@ -581,12 +662,19 @@ export const scimServiceFactory = ({
return { group, newMembers: [] }; return { group, newMembers: [] };
}); });
const orgMemberships = await orgDAL.findMembership({
[`${TableName.OrgMembership}.orgId` as "orgId"]: orgId,
$in: {
[`${TableName.OrgMembership}.userId` as "userId"]: newGroup.newMembers.map((member) => member.id)
}
});
return buildScimGroup({ return buildScimGroup({
groupId: newGroup.group.id, groupId: newGroup.group.id,
name: newGroup.group.name, name: newGroup.group.name,
members: newGroup.newMembers.map((member) => ({ members: orgMemberships.map(({ id, firstName, lastName }) => ({
value: member.id, value: id,
display: `${member.firstName} ${member.lastName}` display: `${firstName} ${lastName}`
})) }))
}); });
}; };
@@ -615,15 +703,22 @@ export const scimServiceFactory = ({
groupId: group.id groupId: group.id
}); });
const orgMemberships = await orgDAL.findMembership({
[`${TableName.OrgMembership}.orgId` as "orgId"]: orgId,
$in: {
[`${TableName.OrgMembership}.userId` as "userId"]: users
.filter((user) => user.isPartOfGroup)
.map((user) => user.id)
}
});
return buildScimGroup({ return buildScimGroup({
groupId: group.id, groupId: group.id,
name: group.name, name: group.name,
members: users members: orgMemberships.map(({ id, firstName, lastName }) => ({
.filter((user) => user.isPartOfGroup) value: id,
.map((user) => ({ display: `${firstName} ${lastName}`
value: user.id, }))
display: `${user.firstName} ${user.lastName}`
}))
}); });
}; };
@@ -667,7 +762,13 @@ export const scimServiceFactory = ({
} }
if (members) { if (members) {
const membersIdsSet = new Set(members.map((member) => member.value)); const orgMemberships = await orgMembershipDAL.find({
$in: {
id: members.map((member) => member.value)
}
});
const membersIdsSet = new Set(orgMemberships.map((orgMembership) => orgMembership.userId));
const directMemberUserIds = ( const directMemberUserIds = (
await userGroupMembershipDAL.find({ await userGroupMembershipDAL.find({
@@ -686,13 +787,13 @@ export const scimServiceFactory = ({
const allMembersUserIds = directMemberUserIds.concat(pendingGroupAdditionsUserIds); const allMembersUserIds = directMemberUserIds.concat(pendingGroupAdditionsUserIds);
const allMembersUserIdsSet = new Set(allMembersUserIds); const allMembersUserIdsSet = new Set(allMembersUserIds);
const toAddUserIds = members.filter((member) => !allMembersUserIdsSet.has(member.value)); const toAddUserIds = orgMemberships.filter((member) => !allMembersUserIdsSet.has(member.userId as string));
const toRemoveUserIds = allMembersUserIds.filter((userId) => !membersIdsSet.has(userId)); const toRemoveUserIds = allMembersUserIds.filter((userId) => !membersIdsSet.has(userId));
if (toAddUserIds.length) { if (toAddUserIds.length) {
await addUsersToGroupByUserIds({ await addUsersToGroupByUserIds({
group, group,
userIds: toAddUserIds.map((member) => member.value), userIds: toAddUserIds.map((member) => member.userId as string),
userDAL, userDAL,
userGroupMembershipDAL, userGroupMembershipDAL,
orgDAL, orgDAL,
+7 -7
View File
@@ -12,7 +12,7 @@ export type TDeleteScimTokenDTO = {
// SCIM server endpoint types // SCIM server endpoint types
export type TListScimUsersDTO = { export type TListScimUsersDTO = {
offset: number; startIndex: number;
limit: number; limit: number;
filter?: string; filter?: string;
orgId: string; orgId: string;
@@ -27,12 +27,12 @@ export type TListScimUsers = {
}; };
export type TGetScimUserDTO = { export type TGetScimUserDTO = {
userId: string; orgMembershipId: string;
orgId: string; orgId: string;
}; };
export type TCreateScimUserDTO = { export type TCreateScimUserDTO = {
username: string; externalId: string;
email?: string; email?: string;
firstName: string; firstName: string;
lastName: string; lastName: string;
@@ -40,7 +40,7 @@ export type TCreateScimUserDTO = {
}; };
export type TUpdateScimUserDTO = { export type TUpdateScimUserDTO = {
userId: string; orgMembershipId: string;
orgId: string; orgId: string;
operations: { operations: {
op: string; op: string;
@@ -54,18 +54,18 @@ export type TUpdateScimUserDTO = {
}; };
export type TReplaceScimUserDTO = { export type TReplaceScimUserDTO = {
userId: string; orgMembershipId: string;
active: boolean; active: boolean;
orgId: string; orgId: string;
}; };
export type TDeleteScimUserDTO = { export type TDeleteScimUserDTO = {
userId: string; orgMembershipId: string;
orgId: string; orgId: string;
}; };
export type TListScimGroupsDTO = { export type TListScimGroupsDTO = {
offset: number; startIndex: number;
limit: number; limit: number;
orgId: string; orgId: string;
}; };
+43 -10
View File
@@ -92,6 +92,18 @@ export const UNIVERSAL_AUTH = {
} }
} as const; } 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 = { export const ORGANIZATIONS = {
LIST_USER_MEMBERSHIPS: { LIST_USER_MEMBERSHIPS: {
organizationId: "The ID of the organization to get memberships from." organizationId: "The ID of the organization to get memberships from."
@@ -240,6 +252,7 @@ export const FOLDERS = {
name: "The new name of the folder.", name: "The new name of the folder.",
path: "The path of the folder to update.", path: "The path of the folder to update.",
directory: "The new directory of the folder to update. (Deprecated in favor of path)", directory: "The new directory of the folder to update. (Deprecated in favor of path)",
projectSlug: "The slug of the project where the folder is located.",
workspaceId: "The ID of the project where the folder is located." workspaceId: "The ID of the project where the folder is located."
}, },
DELETE: { DELETE: {
@@ -276,7 +289,8 @@ export const RAW_SECRETS = {
recursive: 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.", "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.", 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.", environment: "The slug of the environment to list secrets from.",
secretPath: "The secret path to list secrets from.", secretPath: "The secret path to list secrets from.",
includeImports: "Weather to include imported secrets or not." includeImports: "Weather to include imported secrets or not."
@@ -295,6 +309,7 @@ export const RAW_SECRETS = {
GET: { GET: {
secretName: "The name of the secret to get.", secretName: "The name of the secret to get.",
workspaceId: "The ID of the project to get the secret from.", 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.", environment: "The slug of the environment to get the secret from.",
secretPath: "The path of the secret to get.", secretPath: "The path of the secret to get.",
version: "The version of the secret to get.", version: "The version of the secret to get.",
@@ -465,12 +480,21 @@ export const SECRET_TAGS = {
export const IDENTITY_ADDITIONAL_PRIVILEGE = { export const IDENTITY_ADDITIONAL_PRIVILEGE = {
CREATE: { CREATE: {
projectSlug: "The slug of the project of the identity in.", 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.", slug: "The slug of the privilege to create.",
permissions: `The permission object for the privilege. permissions: `The permission object for the privilege.
1. [["read", "secrets", {environment: "dev", secretPath: {$glob: "/"}}]] - Read secrets
2. [["read", "secrets", {environment: "dev"}], ["create", "secrets", {environment: "dev"}]] \`\`\`
2. [["read", "secrets", {environment: "dev"}]] { "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.", isPackPermission: "Whether the server should pack(compact) the permission object.",
isTemporary: "Whether the privilege is temporary.", isTemporary: "Whether the privilege is temporary.",
@@ -484,11 +508,19 @@ export const IDENTITY_ADDITIONAL_PRIVILEGE = {
slug: "The slug of the privilege to update.", slug: "The slug of the privilege to update.",
newSlug: "The new slug of the privilege to update.", newSlug: "The new slug of the privilege to update.",
permissions: `The permission object for the privilege. permissions: `The permission object for the privilege.
1. [["read", "secrets", {environment: "dev", secretPath: {$glob: "/"}}]] - Read secrets
2. [["read", "secrets", {environment: "dev"}], ["create", "secrets", {environment: "dev"}]] \`\`\`
2. [["read", "secrets", {environment: "dev"}]] { "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.", isTemporary: "Whether the privilege is temporary.",
temporaryMode: "Type of temporary access given. Types: relative", temporaryMode: "Type of temporary access given. Types: relative",
temporaryRange: "TTL for the temporay time. Eg: 1m, 1h, 1d", temporaryRange: "TTL for the temporay time. Eg: 1m, 1h, 1d",
@@ -596,7 +628,8 @@ export const INTEGRATION = {
shouldAutoRedeploy: "Used by Render to trigger auto deploy.", shouldAutoRedeploy: "Used by Render to trigger auto deploy.",
secretGCPLabel: "The label for GCP secrets.", secretGCPLabel: "The label for GCP secrets.",
secretAWSTag: "The tags for AWS 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: { UPDATE: {
@@ -108,6 +108,7 @@ export const injectIdentity = fp(async (server: FastifyZodProvider) => {
if (req.url.includes("/api/v3/auth/")) { if (req.url.includes("/api/v3/auth/")) {
return; return;
} }
if (!authMode) return; if (!authMode) return;
switch (authMode) { switch (authMode) {
+73 -4
View File
@@ -2,6 +2,12 @@ import { Knex } from "knex";
import { z } from "zod"; import { z } from "zod";
import { registerV1EERoutes } from "@app/ee/routes/v1"; 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 { auditLogDALFactory } from "@app/ee/services/audit-log/audit-log-dal";
import { auditLogQueueServiceFactory } from "@app/ee/services/audit-log/audit-log-queue"; import { auditLogQueueServiceFactory } from "@app/ee/services/audit-log/audit-log-queue";
import { auditLogServiceFactory } from "@app/ee/services/audit-log/audit-log-service"; 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 { identityServiceFactory } from "@app/services/identity/identity-service";
import { identityAccessTokenDALFactory } from "@app/services/identity-access-token/identity-access-token-dal"; 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 { 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 { identityKubernetesAuthDALFactory } from "@app/services/identity-kubernetes-auth/identity-kubernetes-auth-dal"; import { identityKubernetesAuthDALFactory } from "@app/services/identity-kubernetes-auth/identity-kubernetes-auth-dal";
import { identityKubernetesAuthServiceFactory } from "@app/services/identity-kubernetes-auth/identity-kubernetes-auth-service"; import { identityKubernetesAuthServiceFactory } from "@app/services/identity-kubernetes-auth/identity-kubernetes-auth-service";
import { identityProjectDALFactory } from "@app/services/identity-project/identity-project-dal"; import { identityProjectDALFactory } from "@app/services/identity-project/identity-project-dal";
@@ -90,6 +98,7 @@ import { orgDALFactory } from "@app/services/org/org-dal";
import { orgRoleDALFactory } from "@app/services/org/org-role-dal"; import { orgRoleDALFactory } from "@app/services/org/org-role-dal";
import { orgRoleServiceFactory } from "@app/services/org/org-role-service"; import { orgRoleServiceFactory } from "@app/services/org/org-role-service";
import { orgServiceFactory } from "@app/services/org/org-service"; import { orgServiceFactory } from "@app/services/org/org-service";
import { orgMembershipDALFactory } from "@app/services/org-membership/org-membership-dal";
import { projectDALFactory } from "@app/services/project/project-dal"; import { projectDALFactory } from "@app/services/project/project-dal";
import { projectQueueFactory } from "@app/services/project/project-queue"; import { projectQueueFactory } from "@app/services/project/project-queue";
import { projectServiceFactory } from "@app/services/project/project-service"; import { projectServiceFactory } from "@app/services/project/project-service";
@@ -157,6 +166,7 @@ export const registerRoutes = async (
const authDAL = authDALFactory(db); const authDAL = authDALFactory(db);
const authTokenDAL = tokenDALFactory(db); const authTokenDAL = tokenDALFactory(db);
const orgDAL = orgDALFactory(db); const orgDAL = orgDALFactory(db);
const orgMembershipDAL = orgMembershipDALFactory(db);
const orgBotDAL = orgBotDALFactory(db); const orgBotDAL = orgBotDALFactory(db);
const incidentContactDAL = incidentContactDALFactory(db); const incidentContactDAL = incidentContactDALFactory(db);
const orgRoleDAL = orgRoleDALFactory(db); const orgRoleDAL = orgRoleDALFactory(db);
@@ -196,6 +206,7 @@ export const registerRoutes = async (
const identityUaDAL = identityUaDALFactory(db); const identityUaDAL = identityUaDALFactory(db);
const identityKubernetesAuthDAL = identityKubernetesAuthDALFactory(db); const identityKubernetesAuthDAL = identityKubernetesAuthDALFactory(db);
const identityUaClientSecretDAL = identityUaClientSecretDALFactory(db); const identityUaClientSecretDAL = identityUaClientSecretDALFactory(db);
const identityAwsAuthDAL = identityAwsAuthDALFactory(db);
const auditLogDAL = auditLogDALFactory(db); const auditLogDAL = auditLogDALFactory(db);
const auditLogStreamDAL = auditLogStreamDALFactory(db); const auditLogStreamDAL = auditLogStreamDALFactory(db);
@@ -208,6 +219,12 @@ export const registerRoutes = async (
const scimDAL = scimDALFactory(db); const scimDAL = scimDALFactory(db);
const ldapConfigDAL = ldapConfigDALFactory(db); const ldapConfigDAL = ldapConfigDALFactory(db);
const ldapGroupMapDAL = ldapGroupMapDALFactory(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 sapApproverDAL = secretApprovalPolicyApproverDALFactory(db);
const secretApprovalPolicyDAL = secretApprovalPolicyDALFactory(db); const secretApprovalPolicyDAL = secretApprovalPolicyDALFactory(db);
const secretApprovalRequestDAL = secretApprovalRequestDALFactory(db); const secretApprovalRequestDAL = secretApprovalRequestDALFactory(db);
@@ -265,13 +282,19 @@ export const registerRoutes = async (
permissionService, permissionService,
secretApprovalPolicyDAL secretApprovalPolicyDAL
}); });
const tokenService = tokenServiceFactory({ tokenDAL: authTokenDAL, userDAL });
const samlService = samlConfigServiceFactory({ const samlService = samlConfigServiceFactory({
permissionService, permissionService,
orgBotDAL, orgBotDAL,
orgDAL, orgDAL,
orgMembershipDAL,
userDAL, userDAL,
userAliasDAL,
samlConfigDAL, samlConfigDAL,
licenseService licenseService,
tokenService,
smtpService
}); });
const groupService = groupServiceFactory({ const groupService = groupServiceFactory({
userDAL, userDAL,
@@ -300,7 +323,9 @@ export const registerRoutes = async (
licenseService, licenseService,
scimDAL, scimDAL,
userDAL, userDAL,
userAliasDAL,
orgDAL, orgDAL,
orgMembershipDAL,
projectDAL, projectDAL,
projectMembershipDAL, projectMembershipDAL,
groupDAL, groupDAL,
@@ -316,6 +341,7 @@ export const registerRoutes = async (
ldapConfigDAL, ldapConfigDAL,
ldapGroupMapDAL, ldapGroupMapDAL,
orgDAL, orgDAL,
orgMembershipDAL,
orgBotDAL, orgBotDAL,
groupDAL, groupDAL,
groupProjectDAL, groupProjectDAL,
@@ -339,8 +365,13 @@ export const registerRoutes = async (
queueService queueService
}); });
const tokenService = tokenServiceFactory({ tokenDAL: authTokenDAL, userDAL }); const userService = userServiceFactory({
const userService = userServiceFactory({ userDAL }); userDAL,
userAliasDAL,
orgMembershipDAL,
tokenService,
smtpService
});
const loginService = authLoginServiceFactory({ userDAL, smtpService, tokenService, orgDAL, tokenDAL: authTokenDAL }); const loginService = authLoginServiceFactory({ userDAL, smtpService, tokenService, orgDAL, tokenDAL: authTokenDAL });
const passwordService = authPaswordServiceFactory({ const passwordService = authPaswordServiceFactory({
tokenService, tokenService,
@@ -349,6 +380,7 @@ export const registerRoutes = async (
userDAL userDAL
}); });
const orgService = orgServiceFactory({ const orgService = orgServiceFactory({
userAliasDAL,
licenseService, licenseService,
samlConfigDAL, samlConfigDAL,
orgRoleDAL, orgRoleDAL,
@@ -509,8 +541,10 @@ export const registerRoutes = async (
folderDAL, folderDAL,
folderVersionDAL, folderVersionDAL,
projectEnvDAL, projectEnvDAL,
snapshotService snapshotService,
projectDAL
}); });
const integrationAuthService = integrationAuthServiceFactory({ const integrationAuthService = integrationAuthServiceFactory({
integrationAuthDAL, integrationAuthDAL,
integrationDAL, integrationDAL,
@@ -583,6 +617,30 @@ export const registerRoutes = async (
secretVersionTagDAL, secretVersionTagDAL,
secretQueueService 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({ const secretRotationQueue = secretRotationQueueFactory({
telemetryService, telemetryService,
secretRotationDAL, secretRotationDAL,
@@ -658,6 +716,14 @@ export const registerRoutes = async (
permissionService, permissionService,
licenseService licenseService
}); });
const identityAwsAuthService = identityAwsAuthServiceFactory({
identityAccessTokenDAL,
identityAwsAuthDAL,
identityOrgMembershipDAL,
identityDAL,
licenseService,
permissionService
});
const dynamicSecretProviders = buildDynamicSecretProviders(); const dynamicSecretProviders = buildDynamicSecretProviders();
const dynamicSecretQueueService = dynamicSecretLeaseQueueServiceFactory({ const dynamicSecretQueueService = dynamicSecretLeaseQueueServiceFactory({
@@ -728,7 +794,10 @@ export const registerRoutes = async (
identityProject: identityProjectService, identityProject: identityProjectService,
identityUa: identityUaService, identityUa: identityUaService,
identityKubernetesAuth: identityKubernetesAuthService, identityKubernetesAuth: identityKubernetesAuthService,
identityAwsAuth: identityAwsAuthService,
secretApprovalPolicy: sapService, secretApprovalPolicy: sapService,
accessApprovalPolicy: accessApprovalPolicyService,
accessApprovalRequest: accessApprovalRequestService,
secretApprovalRequest: sarService, secretApprovalRequest: sarService,
secretRotation: secretRotationService, secretRotation: secretRotationService,
dynamicSecret: dynamicSecretService, dynamicSecret: dynamicSecretService,
@@ -2,10 +2,12 @@ import { z } from "zod";
import { import {
DynamicSecretsSchema, DynamicSecretsSchema,
IdentityProjectAdditionalPrivilegeSchema,
IntegrationAuthsSchema, IntegrationAuthsSchema,
SecretApprovalPoliciesSchema, SecretApprovalPoliciesSchema,
UsersSchema UsersSchema
} from "@app/db/schemas"; } 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 // sometimes the return data must be santizied to avoid leaking important values
// always prefer pick over omit in zod // always prefer pick over omit in zod
@@ -62,6 +64,35 @@ export const secretRawSchema = z.object({
secretComment: z.string().optional() 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({ export const SanitizedDynamicSecretSchema = DynamicSecretsSchema.omit({
inputIV: true, inputIV: true,
inputTag: true, inputTag: true,
+3 -1
View File
@@ -42,7 +42,9 @@ export const registerAdminRouter = async (server: FastifyZodProvider) => {
schema: { schema: {
body: z.object({ body: z.object({
allowSignUp: z.boolean().optional(), allowSignUp: z.boolean().optional(),
allowedSignUpDomain: z.string().optional().nullable() allowedSignUpDomain: z.string().optional().nullable(),
trustSamlEmails: z.boolean().optional(),
trustLdapEmails: z.boolean().optional()
}), }),
response: { response: {
200: z.object({ 200: z.object({
@@ -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
View File
@@ -2,6 +2,7 @@ import { registerAdminRouter } from "./admin-router";
import { registerAuthRoutes } from "./auth-router"; import { registerAuthRoutes } from "./auth-router";
import { registerProjectBotRouter } from "./bot-router"; import { registerProjectBotRouter } from "./bot-router";
import { registerIdentityAccessTokenRouter } from "./identity-access-token-router"; import { registerIdentityAccessTokenRouter } from "./identity-access-token-router";
import { registerIdentityAwsAuthRouter } from "./identity-aws-iam-auth-router";
import { registerIdentityKubernetesRouter } from "./identity-kubernetes-auth-router"; import { registerIdentityKubernetesRouter } from "./identity-kubernetes-auth-router";
import { registerIdentityRouter } from "./identity-router"; import { registerIdentityRouter } from "./identity-router";
import { registerIdentityUaRouter } from "./identity-ua"; import { registerIdentityUaRouter } from "./identity-ua";
@@ -30,6 +31,7 @@ export const registerV1Routes = async (server: FastifyZodProvider) => {
await authRouter.register(registerIdentityUaRouter); await authRouter.register(registerIdentityUaRouter);
await authRouter.register(registerIdentityKubernetesRouter); await authRouter.register(registerIdentityKubernetesRouter);
await authRouter.register(registerIdentityAccessTokenRouter); await authRouter.register(registerIdentityAccessTokenRouter);
await authRouter.register(registerIdentityAwsAuthRouter);
}, },
{ prefix: "/auth" } { prefix: "/auth" }
); );
@@ -66,7 +66,8 @@ export const registerIntegrationRouter = async (server: FastifyZodProvider) => {
) )
.optional() .optional()
.describe(INTEGRATION.CREATE.metadata.secretAWSTag), .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({}) .default({})
}), }),
@@ -127,6 +127,70 @@ export const registerSecretFolderRouter = async (server: FastifyZodProvider) =>
} }
}); });
server.route({
url: "/batch",
method: "PATCH",
config: {
rateLimit: secretsLimit
},
schema: {
description: "Update folders by batch",
security: [
{
bearerAuth: []
}
],
body: z.object({
projectSlug: z.string().trim().describe(FOLDERS.UPDATE.projectSlug),
folders: z
.object({
id: z.string().describe(FOLDERS.UPDATE.folderId),
environment: z.string().trim().describe(FOLDERS.UPDATE.environment),
name: z.string().trim().describe(FOLDERS.UPDATE.name),
path: z.string().trim().default("/").transform(removeTrailingSlash).describe(FOLDERS.UPDATE.path)
})
.array()
.min(1)
}),
response: {
200: z.object({
folders: SecretFoldersSchema.array()
})
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const { newFolders, oldFolders, projectId } = await server.services.folder.updateManyFolders({
...req.body,
actorId: req.permission.id,
actor: req.permission.type,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId
});
await Promise.all(
req.body.folders.map(async (folder, index) => {
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
projectId,
event: {
type: EventType.UPDATE_FOLDER,
metadata: {
environment: oldFolders[index].envId,
folderId: oldFolders[index].id,
folderPath: folder.path,
newFolderName: newFolders[index].name,
oldFolderName: oldFolders[index].name
}
}
});
})
);
return { folders: newFolders };
}
});
// TODO(daniel): Expose this route in api reference and write docs for it. // TODO(daniel): Expose this route in api reference and write docs for it.
server.route({ server.route({
method: "DELETE", method: "DELETE",
+42 -1
View File
@@ -2,11 +2,52 @@ import { z } from "zod";
import { AuthTokenSessionsSchema, OrganizationsSchema, UserEncryptionKeysSchema, UsersSchema } from "@app/db/schemas"; import { AuthTokenSessionsSchema, OrganizationsSchema, UserEncryptionKeysSchema, UsersSchema } from "@app/db/schemas";
import { ApiKeysSchema } from "@app/db/schemas/api-keys"; import { ApiKeysSchema } from "@app/db/schemas/api-keys";
import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; import { authRateLimit, readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMethod, AuthMode } from "@app/services/auth/auth-type"; import { AuthMethod, AuthMode } from "@app/services/auth/auth-type";
export const registerUserRouter = async (server: FastifyZodProvider) => { export const registerUserRouter = async (server: FastifyZodProvider) => {
server.route({
method: "POST",
url: "/me/emails/code",
config: {
rateLimit: authRateLimit
},
schema: {
body: z.object({
username: z.string().trim()
}),
response: {
200: z.object({})
}
},
handler: async (req) => {
await server.services.user.sendEmailVerificationCode(req.body.username);
return {};
}
});
server.route({
method: "POST",
url: "/me/emails/verify",
config: {
rateLimit: authRateLimit
},
schema: {
body: z.object({
username: z.string().trim(),
code: z.string().trim()
}),
response: {
200: z.object({})
}
},
handler: async (req) => {
await server.services.user.verifyEmailVerificationCode(req.body.username, req.body.code);
return {};
}
});
server.route({ server.route({
method: "PATCH", method: "PATCH",
url: "/me/mfa", url: "/me/mfa",
@@ -293,6 +293,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
}), }),
querystring: z.object({ querystring: z.object({
workspaceId: z.string().trim().optional().describe(RAW_SECRETS.GET.workspaceId), 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), environment: z.string().trim().optional().describe(RAW_SECRETS.GET.environment),
secretPath: z.string().trim().default("/").transform(removeTrailingSlash).describe(RAW_SECRETS.GET.secretPath), secretPath: z.string().trim().default("/").transform(removeTrailingSlash).describe(RAW_SECRETS.GET.secretPath),
version: z.coerce.number().optional().describe(RAW_SECRETS.GET.version), 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]), onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => { handler: async (req) => {
const { workspaceSlug } = req.query;
let { secretPath, environment, workspaceId } = req.query; let { secretPath, environment, workspaceId } = req.query;
if (req.auth.actor === ActorType.SERVICE) { if (req.auth.actor === ActorType.SERVICE) {
const scope = ServiceTokenScopes.parse(req.auth.serviceToken.scopes); 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({ const secret = await server.services.secret.getSecretByNameRaw({
actorId: req.permission.id, actorId: req.permission.id,
@@ -331,6 +335,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
actorOrgId: req.permission.orgId, actorOrgId: req.permission.orgId,
environment, environment,
projectId: workspaceId, projectId: workspaceId,
projectSlug: workspaceSlug,
path: secretPath, path: secretPath,
secretName: req.params.secretName, secretName: req.params.secretName,
type: req.query.type, type: req.query.type,
@@ -339,7 +344,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
}); });
await server.services.auditLog.createAuditLog({ await server.services.auditLog.createAuditLog({
projectId: req.query.workspaceId, projectId: secret.workspace,
...req.auditLogInfo, ...req.auditLogInfo,
event: { event: {
type: EventType.GET_SECRET, type: EventType.GET_SECRET,
@@ -358,7 +363,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
distinctId: getTelemetryDistinctId(req), distinctId: getTelemetryDistinctId(req),
properties: { properties: {
numberOfSecrets: 1, numberOfSecrets: 1,
workspaceId, workspaceId: secret.workspace,
environment, environment,
secretPath: req.query.secretPath, secretPath: req.query.secretPath,
channel: getUserAgentType(req.headers["user-agent"]), channel: getUserAgentType(req.headers["user-agent"]),
@@ -27,10 +27,17 @@ export const getTokenConfig = (tokenType: TokenType) => {
const expiresAt = new Date(new Date().getTime() + 86400000); const expiresAt = new Date(new Date().getTime() + 86400000);
return { token, expiresAt }; return { token, expiresAt };
} }
case TokenType.TOKEN_EMAIL_VERIFICATION: {
// generate random 6-digit code
const token = String(crypto.randomInt(10 ** 5, 10 ** 6 - 1));
const triesLeft = 3;
const expiresAt = new Date(new Date().getTime() + 86400000);
return { token, triesLeft, expiresAt };
}
case TokenType.TOKEN_EMAIL_MFA: { case TokenType.TOKEN_EMAIL_MFA: {
// generate random 6-digit code // generate random 6-digit code
const token = String(crypto.randomInt(10 ** 5, 10 ** 6 - 1)); const token = String(crypto.randomInt(10 ** 5, 10 ** 6 - 1));
const triesLeft = 5; const triesLeft = 3;
const expiresAt = new Date(new Date().getTime() + 300000); const expiresAt = new Date(new Date().getTime() + 300000);
return { token, triesLeft, expiresAt }; return { token, triesLeft, expiresAt };
} }
@@ -1,5 +1,6 @@
export enum TokenType { export enum TokenType {
TOKEN_EMAIL_CONFIRMATION = "emailConfirmation", TOKEN_EMAIL_CONFIRMATION = "emailConfirmation",
TOKEN_EMAIL_VERIFICATION = "emailVerification", // unverified -> verified
TOKEN_EMAIL_MFA = "emailMfa", TOKEN_EMAIL_MFA = "emailMfa",
TOKEN_EMAIL_ORG_INVITATION = "organizationInvitation", TOKEN_EMAIL_ORG_INVITATION = "organizationInvitation",
TOKEN_EMAIL_PASSWORD_RESET = "passwordReset" TOKEN_EMAIL_PASSWORD_RESET = "passwordReset"
@@ -361,6 +361,7 @@ export const authLoginServiceFactory = ({
user = await userDAL.create({ user = await userDAL.create({
username: email, username: email,
email, email,
isEmailVerified: true,
firstName, firstName,
lastName, lastName,
authMethods: [authMethod], authMethods: [authMethod],
@@ -374,6 +375,8 @@ export const authLoginServiceFactory = ({
authTokenType: AuthTokenType.PROVIDER_TOKEN, authTokenType: AuthTokenType.PROVIDER_TOKEN,
userId: user.id, userId: user.id,
username: user.username, username: user.username,
email: user.email,
isEmailVerified: user.isEmailVerified,
firstName: user.firstName, firstName: user.firstName,
lastName: user.lastName, lastName: user.lastName,
authMethod, authMethod,
@@ -1,6 +1,6 @@
import jwt from "jsonwebtoken"; import jwt from "jsonwebtoken";
import { OrgMembershipStatus } from "@app/db/schemas"; import { OrgMembershipStatus, TableName } from "@app/db/schemas";
import { convertPendingGroupAdditionsToGroupMemberships } from "@app/ee/services/group/group-fns"; import { convertPendingGroupAdditionsToGroupMemberships } from "@app/ee/services/group/group-fns";
import { TUserGroupMembershipDALFactory } from "@app/ee/services/group/user-group-membership-dal"; import { TUserGroupMembershipDALFactory } from "@app/ee/services/group/user-group-membership-dal";
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
@@ -80,7 +80,7 @@ export const authSignupServiceFactory = ({
}); });
await smtpService.sendMail({ await smtpService.sendMail({
template: SmtpTemplates.EmailVerification, template: SmtpTemplates.SignupEmailVerification,
subjectLine: "Infisical confirmation code", subjectLine: "Infisical confirmation code",
recipients: [user.email as string], recipients: [user.email as string],
substitutions: { substitutions: {
@@ -102,6 +102,8 @@ export const authSignupServiceFactory = ({
code code
}); });
await userDAL.updateById(user.id, { isEmailVerified: true });
// generate jwt token this is a temporary token // generate jwt token this is a temporary token
const jwtToken = jwt.sign( const jwtToken = jwt.sign(
{ {
@@ -169,12 +171,11 @@ export const authSignupServiceFactory = ({
tx tx
); );
// If it's SAML Auth and the organization ID is present, we should check if the user has a pending invite for this org, and accept it // If it's SAML Auth and the organization ID is present, we should check if the user has a pending invite for this org, and accept it
if (isAuthMethodSaml(authMethod) && organizationId) { if ((isAuthMethodSaml(authMethod) || authMethod === AuthMethod.LDAP) && organizationId) {
const [pendingOrgMembership] = await orgDAL.findMembership({ const [pendingOrgMembership] = await orgDAL.findMembership({
inviteEmail: email, [`${TableName.OrgMembership}.userId` as "userId"]: user.id,
userId: user.id,
status: OrgMembershipStatus.Invited, status: OrgMembershipStatus.Invited,
orgId: organizationId [`${TableName.OrgMembership}.orgId` as "orgId"]: organizationId
}); });
if (pendingOrgMembership) { if (pendingOrgMembership) {
@@ -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) => { }: TIdentityUaServiceFactoryDep) => {
const login = async (clientId: string, clientSecret: string, ip: string) => { const login = async (clientId: string, clientSecret: string, ip: string) => {
const identityUa = await identityUaDAL.findOne({ clientId }); 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 }); const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId: identityUa.identityId });
@@ -68,7 +68,7 @@ export const identityUaServiceFactory = ({
const validClientSecretInfo = clientSecrtInfo.find(({ clientSecretHash }) => const validClientSecretInfo = clientSecrtInfo.find(({ clientSecretHash }) =>
bcrypt.compareSync(clientSecret, clientSecretHash) bcrypt.compareSync(clientSecret, clientSecretHash)
); );
if (!validClientSecretInfo) throw new UnauthorizedError(); if (!validClientSecretInfo) throw new UnauthorizedError({ message: "Invalid credentials" });
const { clientSecretTTL, clientSecretNumUses, clientSecretNumUsesLimit } = validClientSecretInfo; const { clientSecretTTL, clientSecretNumUses, clientSecretNumUsesLimit } = validClientSecretInfo;
if (Number(clientSecretTTL) > 0) { if (Number(clientSecretTTL) > 0) {
@@ -517,20 +517,22 @@ const syncSecretsAWSParameterStore = async ({
}) })
); );
// Identify secrets to delete if (!metadata.shouldDisableDelete) {
await Promise.all( // Identify secrets to delete
Object.keys(awsParameterStoreSecretsObj).map(async (key) => { await Promise.all(
if (!(key in secrets)) { Object.keys(awsParameterStoreSecretsObj).map(async (key) => {
// case: if (!(key in secrets)) {
// -> delete secret // case:
await ssm // -> delete secret
.deleteParameter({ await ssm
Name: awsParameterStoreSecretsObj[key].Name as string .deleteParameter({
}) Name: awsParameterStoreSecretsObj[key].Name as string
.promise(); })
} .promise();
}) }
); })
);
}
}; };
/** /**
@@ -27,6 +27,7 @@ export type TCreateIntegrationDTO = {
value: string; value: string;
}[]; }[];
kmsKeyId?: string; kmsKeyId?: string;
shouldDisableDelete?: boolean;
}; };
} & Omit<TProjectPermission, "projectId">; } & Omit<TProjectPermission, "projectId">;
@@ -0,0 +1,13 @@
import { TDbClient } from "@app/db";
import { TableName } from "@app/db/schemas";
import { ormify } from "@app/lib/knex";
export type TOrgMembershipDALFactory = ReturnType<typeof orgMembershipDALFactory>;
export const orgMembershipDALFactory = (db: TDbClient) => {
const orgMembershipOrm = ormify(db, TableName.OrgMembership);
return {
...orgMembershipOrm
};
};
+7 -1
View File
@@ -262,13 +262,19 @@ export const orgDALFactory = (db: TDbClient) => {
.where(buildFindFilter(filter)) .where(buildFindFilter(filter))
.join(TableName.Users, `${TableName.Users}.id`, `${TableName.OrgMembership}.userId`) .join(TableName.Users, `${TableName.Users}.id`, `${TableName.OrgMembership}.userId`)
.join(TableName.Organization, `${TableName.Organization}.id`, `${TableName.OrgMembership}.orgId`) .join(TableName.Organization, `${TableName.Organization}.id`, `${TableName.OrgMembership}.orgId`)
.leftJoin(TableName.UserAliases, function joinUserAlias() {
this.on(`${TableName.UserAliases}.userId`, "=", `${TableName.OrgMembership}.userId`)
.andOn(`${TableName.UserAliases}.orgId`, "=", `${TableName.OrgMembership}.orgId`)
.andOn(`${TableName.UserAliases}.aliasType`, "=", (tx || db).raw("?", ["saml"]));
})
.select( .select(
selectAllTableCols(TableName.OrgMembership), selectAllTableCols(TableName.OrgMembership),
db.ref("email").withSchema(TableName.Users), db.ref("email").withSchema(TableName.Users),
db.ref("username").withSchema(TableName.Users), db.ref("username").withSchema(TableName.Users),
db.ref("firstName").withSchema(TableName.Users), db.ref("firstName").withSchema(TableName.Users),
db.ref("lastName").withSchema(TableName.Users), db.ref("lastName").withSchema(TableName.Users),
db.ref("scimEnabled").withSchema(TableName.Organization) db.ref("scimEnabled").withSchema(TableName.Organization),
db.ref("externalId").withSchema(TableName.UserAliases)
) )
.where({ isGhost: false }); .where({ isGhost: false });
+53 -16
View File
@@ -1,41 +1,78 @@
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
import { TOrgDALFactory } from "@app/services/org/org-dal"; import { TOrgDALFactory } from "@app/services/org/org-dal";
import { TProjectDALFactory } from "@app/services/project/project-dal"; import { TProjectKeyDALFactory } from "@app/services/project-key/project-key-dal";
import { TProjectMembershipDALFactory } from "@app/services/project-membership/project-membership-dal"; import { TProjectMembershipDALFactory } from "@app/services/project-membership/project-membership-dal";
import { TUserAliasDALFactory } from "@app/services/user-alias/user-alias-dal";
type TDeleteOrgMembership = { type TDeleteOrgMembership = {
orgMembershipId: string; orgMembershipId: string;
orgId: string; orgId: string;
orgDAL: Pick<TOrgDALFactory, "findMembership" | "deleteMembershipById" | "transaction">; orgDAL: Pick<TOrgDALFactory, "findMembership" | "deleteMembershipById" | "transaction">;
projectDAL: Pick<TProjectDALFactory, "find">; projectMembershipDAL: Pick<TProjectMembershipDALFactory, "delete" | "findProjectMembershipsByUserId">;
projectMembershipDAL: Pick<TProjectMembershipDALFactory, "find" | "delete">; projectKeyDAL: Pick<TProjectKeyDALFactory, "find" | "delete">;
userAliasDAL: Pick<TUserAliasDALFactory, "delete">;
licenseService: Pick<TLicenseServiceFactory, "updateSubscriptionOrgMemberCount">;
}; };
export const deleteOrgMembership = async ({ export const deleteOrgMembershipFn = async ({
orgMembershipId, orgMembershipId,
orgId, orgId,
orgDAL, orgDAL,
projectDAL, projectMembershipDAL,
projectMembershipDAL projectKeyDAL,
userAliasDAL,
licenseService
}: TDeleteOrgMembership) => { }: TDeleteOrgMembership) => {
const membership = await orgDAL.transaction(async (tx) => { const deletedMembership = await orgDAL.transaction(async (tx) => {
// delete org membership
const orgMembership = await orgDAL.deleteMembershipById(orgMembershipId, orgId, tx); const orgMembership = await orgDAL.deleteMembershipById(orgMembershipId, orgId, tx);
const projects = await projectDAL.find({ orgId }, { tx }); if (!orgMembership.userId) {
await licenseService.updateSubscriptionOrgMemberCount(orgId);
return orgMembership;
}
// delete associated project memberships await userAliasDAL.delete(
await projectMembershipDAL.delete(
{ {
$in: { userId: orgMembership.userId,
projectId: projects.map((project) => project.id) orgId
},
userId: orgMembership.userId as string
}, },
tx tx
); );
// Get all the project memberships of the user in the organization
const projectMemberships = await projectMembershipDAL.findProjectMembershipsByUserId(orgId, orgMembership.userId);
// Delete all the project memberships of the user in the organization
await projectMembershipDAL.delete(
{
$in: {
id: projectMemberships.map((membership) => membership.id)
}
},
tx
);
// Get all the project keys of the user in the organization
const projectKeys = await projectKeyDAL.find({
$in: {
projectId: projectMemberships.map((membership) => membership.projectId)
},
receiverId: orgMembership.userId
});
// Delete all the project keys of the user in the organization
await projectKeyDAL.delete(
{
$in: {
id: projectKeys.map((key) => key.id)
}
},
tx
);
await licenseService.updateSubscriptionOrgMemberCount(orgId);
return orgMembership; return orgMembership;
}); });
return membership; return deletedMembership;
}; };
+26 -45
View File
@@ -4,7 +4,7 @@ import crypto from "crypto";
import jwt from "jsonwebtoken"; import jwt from "jsonwebtoken";
import { Knex } from "knex"; import { Knex } from "knex";
import { OrgMembershipRole, OrgMembershipStatus } from "@app/db/schemas"; import { OrgMembershipRole, OrgMembershipStatus, TableName } from "@app/db/schemas";
import { TProjects } from "@app/db/schemas/projects"; import { TProjects } from "@app/db/schemas/projects";
import { TGroupDALFactory } from "@app/ee/services/group/group-dal"; import { TGroupDALFactory } from "@app/ee/services/group/group-dal";
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
@@ -18,6 +18,7 @@ import { generateUserSrpKeys } from "@app/lib/crypto/srp";
import { BadRequestError, UnauthorizedError } from "@app/lib/errors"; import { BadRequestError, UnauthorizedError } from "@app/lib/errors";
import { alphaNumericNanoId } from "@app/lib/nanoid"; import { alphaNumericNanoId } from "@app/lib/nanoid";
import { isDisposableEmail } from "@app/lib/validator"; import { isDisposableEmail } from "@app/lib/validator";
import { TUserAliasDALFactory } from "@app/services/user-alias/user-alias-dal";
import { ActorAuthMethod, ActorType, AuthMethod, AuthTokenType } from "../auth/auth-type"; import { ActorAuthMethod, ActorType, AuthMethod, AuthTokenType } from "../auth/auth-type";
import { TAuthTokenServiceFactory } from "../auth-token/auth-token-service"; import { TAuthTokenServiceFactory } from "../auth-token/auth-token-service";
@@ -30,6 +31,7 @@ import { TUserDALFactory } from "../user/user-dal";
import { TIncidentContactsDALFactory } from "./incident-contacts-dal"; import { TIncidentContactsDALFactory } from "./incident-contacts-dal";
import { TOrgBotDALFactory } from "./org-bot-dal"; import { TOrgBotDALFactory } from "./org-bot-dal";
import { TOrgDALFactory } from "./org-dal"; import { TOrgDALFactory } from "./org-dal";
import { deleteOrgMembershipFn } from "./org-fns";
import { TOrgRoleDALFactory } from "./org-role-dal"; import { TOrgRoleDALFactory } from "./org-role-dal";
import { import {
TDeleteOrgMembershipDTO, TDeleteOrgMembershipDTO,
@@ -43,6 +45,7 @@ import {
} from "./org-types"; } from "./org-types";
type TOrgServiceFactoryDep = { type TOrgServiceFactoryDep = {
userAliasDAL: Pick<TUserAliasDALFactory, "delete">;
orgDAL: TOrgDALFactory; orgDAL: TOrgDALFactory;
orgBotDAL: TOrgBotDALFactory; orgBotDAL: TOrgBotDALFactory;
orgRoleDAL: TOrgRoleDALFactory; orgRoleDAL: TOrgRoleDALFactory;
@@ -65,6 +68,7 @@ type TOrgServiceFactoryDep = {
export type TOrgServiceFactory = ReturnType<typeof orgServiceFactory>; export type TOrgServiceFactory = ReturnType<typeof orgServiceFactory>;
export const orgServiceFactory = ({ export const orgServiceFactory = ({
userAliasDAL,
orgDAL, orgDAL,
userDAL, userDAL,
groupDAL, groupDAL,
@@ -427,7 +431,13 @@ export const orgServiceFactory = ({
if (inviteeUser) { if (inviteeUser) {
// if user already exist means its already part of infisical // if user already exist means its already part of infisical
// Thus the signup flow is not needed anymore // Thus the signup flow is not needed anymore
const [inviteeMembership] = await orgDAL.findMembership({ orgId, userId: inviteeUser.id }, { tx }); const [inviteeMembership] = await orgDAL.findMembership(
{
[`${TableName.OrgMembership}.orgId` as "orgId"]: orgId,
[`${TableName.OrgMembership}.userId` as "userId"]: inviteeUser.id
},
{ tx }
);
if (inviteeMembership && inviteeMembership.status === OrgMembershipStatus.Accepted) { if (inviteeMembership && inviteeMembership.status === OrgMembershipStatus.Accepted) {
throw new BadRequestError({ throw new BadRequestError({
message: "Failed to invite an existing member of org", message: "Failed to invite an existing member of org",
@@ -519,9 +529,9 @@ export const orgServiceFactory = ({
throw new BadRequestError({ message: "Invalid request", name: "Verify user to org" }); throw new BadRequestError({ message: "Invalid request", name: "Verify user to org" });
} }
const [orgMembership] = await orgDAL.findMembership({ const [orgMembership] = await orgDAL.findMembership({
userId: user.id, [`${TableName.OrgMembership}.userId` as "userId"]: user.id,
status: OrgMembershipStatus.Invited, status: OrgMembershipStatus.Invited,
orgId [`${TableName.OrgMembership}.orgId` as "orgId"]: orgId
}); });
if (!orgMembership) if (!orgMembership)
throw new BadRequestError({ throw new BadRequestError({
@@ -536,6 +546,10 @@ export const orgServiceFactory = ({
code code
}); });
await userDAL.updateById(user.id, {
isEmailVerified: true
});
if (user.isAccepted) { if (user.isAccepted) {
// this means user has already completed signup process // this means user has already completed signup process
// isAccepted is set true when keys are exchanged // isAccepted is set true when keys are exchanged
@@ -572,47 +586,14 @@ export const orgServiceFactory = ({
const { permission } = await permissionService.getUserOrgPermission(userId, orgId, actorAuthMethod, actorOrgId); const { permission } = await permissionService.getUserOrgPermission(userId, orgId, actorAuthMethod, actorOrgId);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Delete, OrgPermissionSubjects.Member); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Delete, OrgPermissionSubjects.Member);
const deletedMembership = await orgDAL.transaction(async (tx) => { const deletedMembership = await deleteOrgMembershipFn({
const orgMembership = await orgDAL.deleteMembershipById(membershipId, orgId, tx); orgMembershipId: membershipId,
orgId,
if (!orgMembership.userId) { orgDAL,
await licenseService.updateSubscriptionOrgMemberCount(orgId); projectMembershipDAL,
return orgMembership; projectKeyDAL,
} userAliasDAL,
licenseService
// Get all the project memberships of the user in the organization
const projectMemberships = await projectMembershipDAL.findProjectMembershipsByUserId(orgId, orgMembership.userId);
// Delete all the project memberships of the user in the organization
await projectMembershipDAL.delete(
{
$in: {
id: projectMemberships.map((membership) => membership.id)
}
},
tx
);
// Get all the project keys of the user in the organization
const projectKeys = await projectKeyDAL.find({
$in: {
projectId: projectMemberships.map((membership) => membership.projectId)
},
receiverId: orgMembership.userId
});
// Delete all the project keys of the user in the organization
await projectKeyDAL.delete(
{
$in: {
id: projectKeys.map((key) => key.id)
}
},
tx
);
await licenseService.updateSubscriptionOrgMemberCount(orgId);
return orgMembership;
}); });
return deletedMembership; return deletedMembership;
@@ -110,7 +110,7 @@ export const projectMembershipServiceFactory = ({
); );
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Create, ProjectPermissionSub.Member); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Create, ProjectPermissionSub.Member);
const orgMembers = await orgDAL.findMembership({ const orgMembers = await orgDAL.findMembership({
orgId: project.orgId, [`${TableName.OrgMembership}.orgId` as "orgId"]: project.orgId,
$in: { $in: {
[`${TableName.OrgMembership}.id` as "id"]: members.map(({ orgMembershipId }) => orgMembershipId) [`${TableName.OrgMembership}.id` as "id"]: members.map(({ orgMembershipId }) => orgMembershipId)
} }
@@ -119,7 +119,7 @@ export const projectMembershipServiceFactory = ({
const existingMembers = await projectMembershipDAL.find({ const existingMembers = await projectMembershipDAL.find({
projectId, projectId,
$in: { userId: orgMembers.map(({ userId }) => userId).filter(Boolean) as string[] } $in: { userId: orgMembers.map(({ userId }) => userId).filter(Boolean) }
}); });
if (existingMembers.length) throw new BadRequestError({ message: "Some users are already part of project" }); if (existingMembers.length) throw new BadRequestError({ message: "Some users are already part of project" });
@@ -134,7 +134,7 @@ export const projectMembershipServiceFactory = ({
const projectMemberships = await projectMembershipDAL.insertMany( const projectMemberships = await projectMembershipDAL.insertMany(
orgMembers.map(({ userId }) => ({ orgMembers.map(({ userId }) => ({
projectId, projectId,
userId: userId as string userId
})), })),
tx tx
); );
@@ -145,12 +145,12 @@ export const projectMembershipServiceFactory = ({
const encKeyGroupByOrgMembId = groupBy(members, (i) => i.orgMembershipId); const encKeyGroupByOrgMembId = groupBy(members, (i) => i.orgMembershipId);
await projectKeyDAL.insertMany( await projectKeyDAL.insertMany(
orgMembers orgMembers
.filter(({ userId }) => !userIdsToExcludeForProjectKeyAddition.has(userId as string)) .filter(({ userId }) => !userIdsToExcludeForProjectKeyAddition.has(userId))
.map(({ userId, id }) => ({ .map(({ userId, id }) => ({
encryptedKey: encKeyGroupByOrgMembId[id][0].workspaceEncryptedKey, encryptedKey: encKeyGroupByOrgMembId[id][0].workspaceEncryptedKey,
nonce: encKeyGroupByOrgMembId[id][0].workspaceEncryptedNonce, nonce: encKeyGroupByOrgMembId[id][0].workspaceEncryptedNonce,
senderId: actorId, senderId: actorId,
receiverId: userId as string, receiverId: userId,
projectId projectId
})), })),
tx tx
@@ -8,6 +8,7 @@ import {
SecretKeyEncoding, SecretKeyEncoding,
SecretsSchema, SecretsSchema,
SecretVersionsSchema, SecretVersionsSchema,
TableName,
TIntegrationAuths, TIntegrationAuths,
TSecretApprovalRequestsSecrets, TSecretApprovalRequestsSecrets,
TSecrets, TSecrets,
@@ -273,7 +274,10 @@ export const projectQueueFactory = ({
for (const key of existingProjectKeys) { for (const key of existingProjectKeys) {
const user = await userDAL.findUserEncKeyByUserId(key.receiverId); const user = await userDAL.findUserEncKeyByUserId(key.receiverId);
const [orgMembership] = await orgDAL.findMembership({ userId: key.receiverId, orgId: project.orgId }); const [orgMembership] = await orgDAL.findMembership({
[`${TableName.OrgMembership}.userId` as "userId"]: key.receiverId,
[`${TableName.OrgMembership}.orgId` as "orgId"]: project.orgId
});
if (!user) { if (!user) {
throw new Error(`User with ID ${key.receiverId} was not found during upgrade.`); throw new Error(`User with ID ${key.receiverId} was not found during upgrade.`);
@@ -8,9 +8,16 @@ import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services
import { TSecretSnapshotServiceFactory } from "@app/ee/services/secret-snapshot/secret-snapshot-service"; import { TSecretSnapshotServiceFactory } from "@app/ee/services/secret-snapshot/secret-snapshot-service";
import { BadRequestError } from "@app/lib/errors"; import { BadRequestError } from "@app/lib/errors";
import { TProjectDALFactory } from "../project/project-dal";
import { TProjectEnvDALFactory } from "../project-env/project-env-dal"; import { TProjectEnvDALFactory } from "../project-env/project-env-dal";
import { TSecretFolderDALFactory } from "./secret-folder-dal"; import { TSecretFolderDALFactory } from "./secret-folder-dal";
import { TCreateFolderDTO, TDeleteFolderDTO, TGetFolderDTO, TUpdateFolderDTO } from "./secret-folder-types"; import {
TCreateFolderDTO,
TDeleteFolderDTO,
TGetFolderDTO,
TUpdateFolderDTO,
TUpdateManyFoldersDTO
} from "./secret-folder-types";
import { TSecretFolderVersionDALFactory } from "./secret-folder-version-dal"; import { TSecretFolderVersionDALFactory } from "./secret-folder-version-dal";
type TSecretFolderServiceFactoryDep = { type TSecretFolderServiceFactoryDep = {
@@ -19,6 +26,7 @@ type TSecretFolderServiceFactoryDep = {
folderDAL: TSecretFolderDALFactory; folderDAL: TSecretFolderDALFactory;
projectEnvDAL: Pick<TProjectEnvDALFactory, "findOne">; projectEnvDAL: Pick<TProjectEnvDALFactory, "findOne">;
folderVersionDAL: TSecretFolderVersionDALFactory; folderVersionDAL: TSecretFolderVersionDALFactory;
projectDAL: Pick<TProjectDALFactory, "findProjectBySlug">;
}; };
export type TSecretFolderServiceFactory = ReturnType<typeof secretFolderServiceFactory>; export type TSecretFolderServiceFactory = ReturnType<typeof secretFolderServiceFactory>;
@@ -28,7 +36,8 @@ export const secretFolderServiceFactory = ({
snapshotService, snapshotService,
permissionService, permissionService,
projectEnvDAL, projectEnvDAL,
folderVersionDAL folderVersionDAL,
projectDAL
}: TSecretFolderServiceFactoryDep) => { }: TSecretFolderServiceFactoryDep) => {
const createFolder = async ({ const createFolder = async ({
projectId, projectId,
@@ -116,6 +125,105 @@ export const secretFolderServiceFactory = ({
return folder; return folder;
}; };
const updateManyFolders = async ({
actor,
actorId,
projectSlug,
actorAuthMethod,
actorOrgId,
folders
}: TUpdateManyFoldersDTO) => {
const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId);
if (!project) {
throw new BadRequestError({ message: "Project not found" });
}
const { permission } = await permissionService.getProjectPermission(
actor,
actorId,
project.id,
actorAuthMethod,
actorOrgId
);
folders.forEach(({ environment, path: secretPath }) => {
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Edit,
subject(ProjectPermissionSub.Secrets, { environment, secretPath })
);
});
const result = await folderDAL.transaction(async (tx) =>
Promise.all(
folders.map(async (newFolder) => {
const { environment, path: secretPath, id, name } = newFolder;
const parentFolder = await folderDAL.findBySecretPath(project.id, environment, secretPath);
if (!parentFolder) {
throw new BadRequestError({ message: "Secret path not found", name: "Batch update folder" });
}
const env = await projectEnvDAL.findOne({ projectId: project.id, slug: environment });
if (!env) {
throw new BadRequestError({ message: "Environment not found", name: "Batch update folder" });
}
const folder = await folderDAL
.findOne({ envId: env.id, id, parentId: parentFolder.id })
// now folder api accepts id based change
// this is for cli backward compatiability and when cli removes this, we will remove this logic
.catch(() => folderDAL.findOne({ envId: env.id, name: id, parentId: parentFolder.id }));
if (!folder) {
throw new BadRequestError({ message: "Folder not found" });
}
if (name !== folder.name) {
// ensure that new folder name is unique
const folderToCheck = await folderDAL.findOne({
name,
envId: env.id,
parentId: parentFolder.id
});
if (folderToCheck) {
throw new BadRequestError({
message: "Folder with specified name already exists",
name: "Batch update folder"
});
}
}
const [doc] = await folderDAL.update(
{ envId: env.id, id: folder.id, parentId: parentFolder.id },
{ name },
tx
);
await folderVersionDAL.create(
{
name: doc.name,
envId: doc.envId,
version: doc.version,
folderId: doc.id
},
tx
);
if (!doc) {
throw new BadRequestError({ message: "Folder not found", name: "Batch update folder" });
}
return { oldFolder: folder, newFolder: doc };
})
)
);
await Promise.all(result.map(async (res) => snapshotService.performSnapshot(res.newFolder.parentId as string)));
return {
projectId: project.id,
newFolders: result.map((res) => res.newFolder),
oldFolders: result.map((res) => res.oldFolder)
};
};
const updateFolder = async ({ const updateFolder = async ({
projectId, projectId,
actor, actor,
@@ -151,6 +259,21 @@ export const secretFolderServiceFactory = ({
.catch(() => folderDAL.findOne({ envId: env.id, name: id, parentId: parentFolder.id })); .catch(() => folderDAL.findOne({ envId: env.id, name: id, parentId: parentFolder.id }));
if (!folder) throw new BadRequestError({ message: "Folder not found" }); if (!folder) throw new BadRequestError({ message: "Folder not found" });
if (name !== folder.name) {
// ensure that new folder name is unique
const folderToCheck = await folderDAL.findOne({
name,
envId: env.id,
parentId: parentFolder.id
});
if (folderToCheck) {
throw new BadRequestError({
message: "Folder with specified name already exists",
name: "Update folder"
});
}
}
const newFolder = await folderDAL.transaction(async (tx) => { const newFolder = await folderDAL.transaction(async (tx) => {
const [doc] = await folderDAL.update({ envId: env.id, id: folder.id, parentId: parentFolder.id }, { name }, tx); const [doc] = await folderDAL.update({ envId: env.id, id: folder.id, parentId: parentFolder.id }, { name }, tx);
@@ -239,6 +362,7 @@ export const secretFolderServiceFactory = ({
return { return {
createFolder, createFolder,
updateFolder, updateFolder,
updateManyFolders,
deleteFolder, deleteFolder,
getFolders getFolders
}; };
@@ -13,6 +13,16 @@ export type TUpdateFolderDTO = {
name: string; name: string;
} & TProjectPermission; } & TProjectPermission;
export type TUpdateManyFoldersDTO = {
projectSlug: string;
folders: {
environment: string;
path: string;
id: string;
name: string;
}[];
} & Omit<TProjectPermission, "projectId">;
export type TDeleteFolderDTO = { export type TDeleteFolderDTO = {
environment: string; environment: string;
path: string; path: string;
@@ -972,7 +972,8 @@ export const secretServiceFactory = ({
path, path,
actor, actor,
environment, environment,
projectId, projectId: workspaceId,
projectSlug,
actorId, actorId,
actorOrgId, actorOrgId,
actorAuthMethod, actorAuthMethod,
@@ -980,6 +981,8 @@ export const secretServiceFactory = ({
includeImports, includeImports,
version version
}: TGetASecretRawDTO) => { }: TGetASecretRawDTO) => {
const projectId = workspaceId || (await projectDAL.findProjectBySlug(projectSlug as string, actorOrgId)).id;
const botKey = await projectBotService.getBotKey(projectId); const botKey = await projectBotService.getBotKey(projectId);
if (!botKey) throw new BadRequestError({ message: "Project bot not found", name: "bot_not_found_error" }); if (!botKey) throw new BadRequestError({ message: "Project bot not found", name: "bot_not_found_error" });
+3 -1
View File
@@ -152,7 +152,9 @@ export type TGetASecretRawDTO = {
type: "shared" | "personal"; type: "shared" | "personal";
includeImports?: boolean; includeImports?: boolean;
version?: number; version?: number;
} & TProjectPermission; projectSlug?: string;
projectId?: string;
} & Omit<TProjectPermission, "projectId">;
export type TCreateSecretRawDTO = TProjectPermission & { export type TCreateSecretRawDTO = TProjectPermission & {
secretPath: string; secretPath: string;
@@ -17,9 +17,11 @@ export type TSmtpSendMail = {
export type TSmtpService = ReturnType<typeof smtpServiceFactory>; export type TSmtpService = ReturnType<typeof smtpServiceFactory>;
export enum SmtpTemplates { export enum SmtpTemplates {
SignupEmailVerification = "signupEmailVerification.handlebars",
EmailVerification = "emailVerification.handlebars", EmailVerification = "emailVerification.handlebars",
SecretReminder = "secretReminder.handlebars", SecretReminder = "secretReminder.handlebars",
EmailMfa = "emailMfa.handlebars", EmailMfa = "emailMfa.handlebars",
AccessApprovalRequest = "accessApprovalRequest.handlebars",
HistoricalSecretList = "historicalSecretLeakIncident.handlebars", HistoricalSecretList = "historicalSecretLeakIncident.handlebars",
NewDeviceJoin = "newDevice.handlebars", NewDeviceJoin = "newDevice.handlebars",
OrgInvite = "organizationInvitation.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>
@@ -1,17 +1,15 @@
<!DOCTYPE html>
<html> <html>
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8" />
<meta http-equiv="x-ua-compatible" content="ie=edge"> <meta http-equiv="x-ua-compatible" content="ie=edge" />
<title>Code</title> <title>Code</title>
</head> </head>
<body> <body>
<h2>Confirm your email address</h2> <h2>Confirm your email address</h2>
<p>Your confirmation code is below — enter it in the browser window where you've started signing up for Infisical.</p> <p>Your confirmation code is below — enter it in the browser window where you've started confirming your email.</p>
<h1>{{code}}</h1> <h1>{{code}}</h1>
<p>Questions about setting up Infisical? Email us at [email protected]</p> </body>
</body>
</html> </html>
@@ -0,0 +1,17 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="x-ua-compatible" content="ie=edge">
<title>Code</title>
</head>
<body>
<h2>Confirm your email address</h2>
<p>Your confirmation code is below — enter it in the browser window where you've started signing up for Infisical.</p>
<h1>{{code}}</h1>
<p>Questions about setting up Infisical? Email us at [email protected]</p>
</body>
</html>
@@ -102,7 +102,8 @@ export const superAdminServiceFactory = ({
superAdmin: true, superAdmin: true,
isGhost: false, isGhost: false,
isAccepted: true, isAccepted: true,
authMethods: [AuthMethod.EMAIL] authMethods: [AuthMethod.EMAIL],
isEmailVerified: true
}, },
tx tx
); );
@@ -0,0 +1,4 @@
export enum UserAliasType {
LDAP = "ldap",
SAML = "saml"
}
+12
View File
@@ -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) => { const createUserEncryption = async (data: TUserEncryptionKeysInsert, tx?: Knex) => {
try { try {
const [userEnc] = await (tx || db)(TableName.UserEncryptionKey).insert(data).returning("*"); const [userEnc] = await (tx || db)(TableName.UserEncryptionKey).insert(data).returning("*");
@@ -140,6 +151,7 @@ export const userDALFactory = (db: TDbClient) => {
findUserEncKeyByUserId, findUserEncKeyByUserId,
updateUserEncryptionByUserId, updateUserEncryptionByUserId,
findUserByProjectMembershipId, findUserByProjectMembershipId,
findUsersByProjectMembershipIds,
upsertUserEncryptionKey, upsertUserEncryptionKey,
createUserEncryption, createUserEncryption,
findOneUserAction, findOneUserAction,
+1 -1
View File
@@ -4,7 +4,7 @@ import { alphaNumericNanoId } from "@app/lib/nanoid";
import { TUserDALFactory } from "@app/services/user/user-dal"; import { TUserDALFactory } from "@app/services/user/user-dal";
export const normalizeUsername = async (username: string, userDAL: Pick<TUserDALFactory, "findOne">) => { export const normalizeUsername = async (username: string, userDAL: Pick<TUserDALFactory, "findOne">) => {
let attempt = slugify(username); let attempt = slugify(`${username}-${alphaNumericNanoId(4)}`);
let user = await userDAL.findOne({ username: attempt }); let user = await userDAL.findOne({ username: attempt });
if (!user) return attempt; if (!user) return attempt;
+140 -2
View File
@@ -1,15 +1,151 @@
import { BadRequestError } from "@app/lib/errors"; import { BadRequestError } from "@app/lib/errors";
import { TAuthTokenServiceFactory } from "@app/services/auth-token/auth-token-service";
import { TokenType } from "@app/services/auth-token/auth-token-types";
import { TOrgMembershipDALFactory } from "@app/services/org-membership/org-membership-dal";
import { SmtpTemplates, TSmtpService } from "@app/services/smtp/smtp-service";
import { TUserAliasDALFactory } from "@app/services/user-alias/user-alias-dal";
import { AuthMethod } from "../auth/auth-type"; import { AuthMethod } from "../auth/auth-type";
import { TUserDALFactory } from "./user-dal"; import { TUserDALFactory } from "./user-dal";
type TUserServiceFactoryDep = { type TUserServiceFactoryDep = {
userDAL: TUserDALFactory; userDAL: Pick<
TUserDALFactory,
| "find"
| "findOne"
| "findById"
| "transaction"
| "updateById"
| "update"
| "deleteById"
| "findOneUserAction"
| "createUserAction"
| "findUserEncKeyByUserId"
>;
userAliasDAL: Pick<TUserAliasDALFactory, "find" | "insertMany">;
orgMembershipDAL: Pick<TOrgMembershipDALFactory, "find" | "insertMany">;
tokenService: Pick<TAuthTokenServiceFactory, "createTokenForUser" | "validateTokenForUser">;
smtpService: Pick<TSmtpService, "sendMail">;
}; };
export type TUserServiceFactory = ReturnType<typeof userServiceFactory>; export type TUserServiceFactory = ReturnType<typeof userServiceFactory>;
export const userServiceFactory = ({ userDAL }: TUserServiceFactoryDep) => { export const userServiceFactory = ({
userDAL,
userAliasDAL,
orgMembershipDAL,
tokenService,
smtpService
}: TUserServiceFactoryDep) => {
const sendEmailVerificationCode = async (username: string) => {
const user = await userDAL.findOne({ username });
if (!user) throw new BadRequestError({ name: "Failed to find user" });
if (!user.email)
throw new BadRequestError({ name: "Failed to send email verification code due to no email on user" });
if (user.isEmailVerified)
throw new BadRequestError({ name: "Failed to send email verification code due to email already verified" });
const token = await tokenService.createTokenForUser({
type: TokenType.TOKEN_EMAIL_VERIFICATION,
userId: user.id
});
await smtpService.sendMail({
template: SmtpTemplates.EmailVerification,
subjectLine: "Infisical confirmation code",
recipients: [user.email],
substitutions: {
code: token
}
});
};
const verifyEmailVerificationCode = async (username: string, code: string) => {
const user = await userDAL.findOne({ username });
if (!user) throw new BadRequestError({ name: "Failed to find user" });
if (!user.email)
throw new BadRequestError({ name: "Failed to verify email verification code due to no email on user" });
if (user.isEmailVerified)
throw new BadRequestError({ name: "Failed to verify email verification code due to email already verified" });
await tokenService.validateTokenForUser({
type: TokenType.TOKEN_EMAIL_VERIFICATION,
userId: user.id,
code
});
const { email } = user;
await userDAL.transaction(async (tx) => {
await userDAL.updateById(
user.id,
{
isEmailVerified: true
},
tx
);
// check if there are users with the same email.
const users = await userDAL.find(
{
email,
isEmailVerified: true
},
{ tx }
);
if (users.length > 1) {
// merge users
const mergeUser = users.find((u) => u.id !== user.id);
if (!mergeUser) throw new BadRequestError({ name: "Failed to find merge user" });
const mergeUserOrgMembershipSet = new Set(
(await orgMembershipDAL.find({ userId: mergeUser.id }, { tx })).map((m) => m.orgId)
);
const myOrgMemberships = (await orgMembershipDAL.find({ userId: user.id }, { tx })).filter(
(m) => !mergeUserOrgMembershipSet.has(m.orgId)
);
const userAliases = await userAliasDAL.find(
{
userId: user.id
},
{ tx }
);
await userDAL.deleteById(user.id, tx);
if (myOrgMemberships.length) {
await orgMembershipDAL.insertMany(
myOrgMemberships.map((orgMembership) => ({
...orgMembership,
userId: mergeUser.id
})),
tx
);
}
if (userAliases.length) {
await userAliasDAL.insertMany(
userAliases.map((userAlias) => ({
...userAlias,
userId: mergeUser.id
})),
tx
);
}
} else {
// update current user's username to [email]
await userDAL.updateById(
user.id,
{
username: email
},
tx
);
}
});
};
const toggleUserMfa = async (userId: string, isMfaEnabled: boolean) => { const toggleUserMfa = async (userId: string, isMfaEnabled: boolean) => {
const user = await userDAL.findById(userId); const user = await userDAL.findById(userId);
@@ -72,6 +208,8 @@ export const userServiceFactory = ({ userDAL }: TUserServiceFactoryDep) => {
}; };
return { return {
sendEmailVerificationCode,
verifyEmailVerificationCode,
toggleUserMfa, toggleUserMfa,
updateUserName, updateUserName,
updateAuthMethods, updateAuthMethods,
@@ -4,59 +4,66 @@ sidebarTitle: "What is Infisical?"
description: "An Introduction to the Infisical secret management platform." description: "An Introduction to the Infisical secret management platform."
--- ---
Infisical is an [open-source](https://github.com/infisical/infisical) secret management platform for developers. 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 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 credentials, and certificates across infrastructure. In addition, Infisical prevents secrets leaks to git and enables secure
sharing of secrets among engineers. 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. Start managing secrets securely with [Infisical Cloud](https://app.infisical.com) or learn how to [host Infisical](/self-hosting/overview) yourself.
<CardGroup cols={2}> <CardGroup cols={2}>
<Card <Card
title="Infisical Cloud" title="Infisical Cloud"
href="https://app.infisical.com/signup" href="https://app.infisical.com/signup"
icon="cloud" icon="cloud"
color="#000000" color="#000000"
> >
Get started with Infisical Cloud in just a few minutes. Get started with Infisical Cloud in just a few minutes.
</Card> </Card>
<Card <Card
href="/self-hosting/overview" href="/self-hosting/overview"
title="Self-hosting" title="Self-hosting"
icon="server" icon="server"
color="#000000" color="#000000"
> >
Self-host Infisical on your own infrastructure. Self-host Infisical on your own infrastructure.
</Card> </Card>
</CardGroup> </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). - 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). - **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. - Simple secret management inside **[CI/CD pipelines](/integrations/cicd/githubactions)** and staging environments.
- Secure and compliant secret management practices in **[production environments](/sdks/overview)**. - 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. - **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. - **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: 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.
- **[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)). - **[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. Depending on your use case, it might be helpful to look into some of the resources and guides provided below.
<CardGroup cols={2}> <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. Inject secrets into any application process/environment.
</Card> </Card>
<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. Fetch secrets with any programming language on demand.
</Card> </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. Inject secrets into Docker containers.
</Card> </Card>
<Card <Card
@@ -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**.
![identities organization](/images/platform/identities/identities-org.png)
When creating an identity, you specify an organization level [role](/documentation/platform/role-based-access-controls) for it to assume; you can configure roles in Organization Settings > Access Control > Organization Roles.
![identities organization create](/images/platform/identities/identities-org-create.png)
Now input a few details for your new identity. Here's some guidance for each field:
- Name (required): A friendly name for the identity.
- Role (required): A role from the **Organization Roles** tab for the identity to assume. The organization role assigned will determine what organization level resources this identity can have access to.
Once you've created an identity, you'll be prompted to configure the authentication method for it. Here, select **AWS Auth**.
![identities create aws auth method](/images/platform/identities/identities-org-create-aws-auth-method.png)
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.
![identities project](/images/platform/identities/identities-project.png)
![identities project create](/images/platform/identities/identities-project-create.png)
</Step>
<Step title="Accessing the Infisical API with the identity">
To access the Infisical API as the identity, you need to 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). 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.
![organization identities](/images/platform/organization/organization-machine-identities.png) ![organization identities](/images/platform/organization/organization-machine-identities.png)
@@ -21,7 +21,7 @@ Key Features:
A typical workflow for using identities consists of four steps: A typical workflow for using identities consists of four steps:
1. Creating the identity with a name and [role](/documentation/platform/role-based-access-controls) in Organization Access Control > Machine Identities. 1. Creating the identity with a name and [role](/documentation/platform/role-based-access-controls) in Organization Access Control > Machine Identities.
This step also involves configuring an authentication method for it such as [Universal Auth](/documentation/platform/identities/universal-auth). 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. 2. Adding the identity to the project(s) you want it to have access to.
3. Authenticating the identity with the Infisical API based on the configured authentication method on it and receiving a short-lived access token back. 3. Authenticating the identity with the Infisical API based on the configured authentication method on it and receiving a short-lived access token back.
4. Authenticating subsequent requests with the Infisical API using the short-lived access token. 4. Authenticating subsequent requests with the Infisical API using the short-lived access token.
@@ -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: 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 ## FAQ
@@ -3,19 +3,35 @@ title: Universal Auth
description: "Learn how to authenticate to Infisical from any platform or environment." 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. Note over Client,Infis: Step 1: Login Operation
- Support for expiration, so, if specified, the **Client Secret** of the identity will automatically be defunct after a period of time. Client->>Infis: Send Client ID and Client Secret
- 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.
## 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 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. using the Universal Auth authentication method.
@@ -27,18 +43,18 @@ using the Universal Auth authentication method.
![identities organization](/images/platform/identities/identities-org.png) ![identities organization](/images/platform/identities/identities-org.png)
When creating an identity, you specify an organization level [role](/documentation/platform/role-based-access-controls) for it to assume; you can configure roles in Organization Settings > Access Control > Organization Roles. When creating an identity, you specify an organization level [role](/documentation/platform/role-based-access-controls) for it to assume; you can configure roles in Organization Settings > Access Control > Organization Roles.
![identities organization create](/images/platform/identities/identities-org-create.png) ![identities organization create](/images/platform/identities/identities-org-create.png)
Now input a few details for your new identity. Here's some guidance for each field: Now input a few details for your new identity. Here's some guidance for each field:
- Name (required): A friendly name for the identity. - 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. - 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. Once you've created an identity, you'll be prompted to configure the **Universal Auth** authentication method for it.
![identities organization create auth method](/images/platform/identities/identities-org-create-auth-method.png) ![identities organization create auth method](/images/platform/identities/identities-org-create-auth-method.png)
Here's some more guidance on each field: 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. - 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. Next, select the identity you want to add to the project and the project level role you want to allow it to assume. The project role assigned will determine what project level resources this identity can have access to.
![identities project](/images/platform/identities/identities-project.png) ![identities project](/images/platform/identities/identities-project.png)
![identities project create](/images/platform/identities/identities-project-create.png) ![identities project create](/images/platform/identities/identities-project-create.png)
</Step> </Step>
<Step title="Accessing the Infisical API with the identity"> <Step title="Accessing the Infisical API with the identity">
To access the Infisical API as the identity, you should first perform a login operation 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 #### Sample request
``` ```bash Request
curl --location --request POST 'https://app.infisical.com/api/v1/auth/universal-auth/login' \ curl --location --request POST 'https://app.infisical.com/api/v1/auth/universal-auth/login' \
--header 'Content-Type: application/x-www-form-urlencoded' \ --header 'Content-Type: application/x-www-form-urlencoded' \
--data-urlencode 'clientSecret=...' \ --data-urlencode 'clientId=...' \
--data-urlencode 'clientId=...' --data-urlencode 'clientSecret=...'
``` ```
#### Sample response #### Sample response
``` ```bash Response
{ {
"accessToken": "...", "accessToken": "...",
"expiresIn": 7200, "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) Next, you can use the access token to authenticate with the [Infisical API](/api-reference/overview/introduction)
<Note> <Note>
Each identity access token has a time-to-live (TLL) which you can infer from the response of the login operation; 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. 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, 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. a new access token should be obtained by performing another login operation.
</Note> </Note>
</Step> </Step>
</Steps> </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. 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. 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 Regardless of how frequently an access token is renewed, its lifespan remains bound to the maximum TTL determined at its creation
</Accordion> </Accordion>
</AccordionGroup> </AccordionGroup>
@@ -12,6 +12,10 @@ description: "Learn how to log in to Infisical with LDAP."
You can configure your organization in Infisical to have members authenticate with the platform via [LDAP](https://en.wikipedia.org/wiki/Lightweight_Directory_Access_Protocol) You can configure your organization in Infisical to have members authenticate with the platform via [LDAP](https://en.wikipedia.org/wiki/Lightweight_Directory_Access_Protocol)
Prerequisites:
- You must have an email address to use LDAP, regardless of whether or not you use that email address to sign in.
<Steps> <Steps>
<Step title="Prepare the LDAP configuration in Infisical"> <Step title="Prepare the LDAP configuration in Infisical">
In Infisical, head to your Organization Settings > Security > LDAP and select **Manage**. In Infisical, head to your Organization Settings > Security > LDAP and select **Manage**.
@@ -10,6 +10,10 @@ description: "Learn how to configure JumpCloud LDAP for authenticating into Infi
it. it.
</Info> </Info>
Prerequisites:
- You must have an email address to use LDAP, regardless of whether or not you use that email address to sign in.
<Steps> <Steps>
<Step title="Prepare LDAP in JumpCloud"> <Step title="Prepare LDAP in JumpCloud">
In JumpCloud, head to USER MANAGEMENT > Users and create a new user via the **Manual user entry** option. This user In JumpCloud, head to USER MANAGEMENT > Users and create a new user via the **Manual user entry** option. This user
+19 -2
View File
@@ -3,11 +3,13 @@ title: "LDAP Overview"
sidebarTitle: "Overview" sidebarTitle: "Overview"
description: "Learn how to authenticate into Infisical with LDAP." description: "Learn how to authenticate into Infisical with LDAP."
--- ---
<Info> <Info>
LDAP is a paid feature. LDAP is a paid feature.
If you're using Infisical Cloud, then it is available under the **Enterprise Tier**. If you're self-hosting Infisical, If you're using Infisical Cloud, then it is available under the **Enterprise Tier**. If you're self-hosting Infisical,
then you should contact [email protected] to purchase an enterprise license to use it. then you should contact [email protected] to purchase an enterprise license to use it.
</Info> </Info>
You can configure your organization in Infisical to have members authenticate with the platform via [LDAP](https://en.wikipedia.org/wiki/Lightweight_Directory_Access_Protocol). You can configure your organization in Infisical to have members authenticate with the platform via [LDAP](https://en.wikipedia.org/wiki/Lightweight_Directory_Access_Protocol).
@@ -25,3 +27,18 @@ Read the general instructions for configuring LDAP [here](/documentation/platfor
If the documentation for your required identity provider is not shown in the list above, please reach out to [[email protected]](mailto:[email protected]) for assistance. If the documentation for your required identity provider is not shown in the list above, please reach out to [[email protected]](mailto:[email protected]) for assistance.
## FAQ
<AccordionGroup>
<Accordion title="Why does Infisical require additional email verification for users connected via LDAP?">
By default, Infisical Cloud is configured to not trust emails from external
identity providers to prevent any malicious account takeover attempts via
email spoofing. Accordingly, Infisical creates a new user for anyone provisioned
through an external identity provider and requires an additional email
verification step upon their first login.
If you're running a self-hosted instance of Infisical and would like it to trust emails from external identity providers,
you can configure this behavior in the admin panel.
</Accordion>
</AccordionGroup>
+23 -16
View File
@@ -4,10 +4,10 @@ description: "Learn how to configure Okta SAML 2.0 for Infisical SSO."
--- ---
<Info> <Info>
Okta SAML SSO is a paid feature. Okta SAML SSO is a paid feature. If you're using Infisical Cloud, then it is
available under the **Pro Tier**. If you're self-hosting Infisical, then you
If you're using Infisical Cloud, then it is available under the **Pro Tier**. If you're self-hosting Infisical, should contact [email protected] to purchase an enterprise license to use
then you should contact [email protected] to purchase an enterprise license to use it. it.
</Info> </Info>
<Steps> <Steps>
@@ -22,24 +22,24 @@ description: "Learn how to configure Okta SAML 2.0 for Infisical SSO."
button. button.
![SAML Okta create app integration](../../../images/sso/okta/create-app-integration.png) ![SAML Okta create app integration](../../../images/sso/okta/create-app-integration.png)
In the Create a New Application Integration dialog, select the **SAML 2.0** radio button: In the Create a New Application Integration dialog, select the **SAML 2.0** radio button:
![SAML Okta create SAML 2.0 integration](../../../images/sso/okta/create-saml-app.png) ![SAML Okta create SAML 2.0 integration](../../../images/sso/okta/create-saml-app.png)
On the General Settings screen, give the application a unique name like Infisical and select **Next**. On the General Settings screen, give the application a unique name like Infisical and select **Next**.
![SAML Okta create SAML 2.0 integration](../../../images/sso/okta/general-settings.png) ![SAML Okta create SAML 2.0 integration](../../../images/sso/okta/general-settings.png)
On the Configure SAML screen, set the **Single sign-on URL** and **Audience URI (SP Entity ID)** from step 1. On the Configure SAML screen, set the **Single sign-on URL** and **Audience URI (SP Entity ID)** from step 1.
![SAML Okta configure IdP fields](../../../images/sso/okta/configure-saml.png) ![SAML Okta configure IdP fields](../../../images/sso/okta/configure-saml.png)
<Note> <Note>
If you're self-hosting Infisical, then you will want to replace If you're self-hosting Infisical, then you will want to replace
`https://app.infisical.com` with your own domain. `https://app.infisical.com` with your own domain.
</Note> </Note>
Also on the Configure SAML screen, configure the **Attribute Statements** to map: Also on the Configure SAML screen, configure the **Attribute Statements** to map:
- `id -> user.id`, - `id -> user.id`,
@@ -50,6 +50,7 @@ description: "Learn how to configure Okta SAML 2.0 for Infisical SSO."
![SAML Okta attribute statements](../../../images/sso/okta/attribute-statements.png) ![SAML Okta attribute statements](../../../images/sso/okta/attribute-statements.png)
Once configured, select **Next** to proceed to the Feedback screen and select **Finish**. Once configured, select **Next** to proceed to the Feedback screen and select **Finish**.
</Step> </Step>
<Step title="Retrieve Identity Provider (IdP) Information from Okta"> <Step title="Retrieve Identity Provider (IdP) Information from Okta">
Once your application is created, select the **Sign On** tab for the app and select the **View Setup Instructions** button located on the right side of the screen: Once your application is created, select the **Sign On** tab for the app and select the **View Setup Instructions** button located on the right side of the screen:
@@ -59,12 +60,14 @@ description: "Learn how to configure Okta SAML 2.0 for Infisical SSO."
Copy the **Identity Provider Single Sign-On URL**, the **Identity Provider Issuer**, and the **X.509 Certificate** to use when finishing configuring Okta SAML in Infisical. Copy the **Identity Provider Single Sign-On URL**, the **Identity Provider Issuer**, and the **X.509 Certificate** to use when finishing configuring Okta SAML in Infisical.
![SAML Okta IdP values](../../../images/sso/okta/idp-values.png) ![SAML Okta IdP values](../../../images/sso/okta/idp-values.png)
</Step> </Step>
<Step title="Finish configuring SAML in Infisical"> <Step title="Finish configuring SAML in Infisical">
Back in Infisical, set **Identity Provider Single Sign-On URL**, **Identity Provider Issuer**, Back in Infisical, set **Identity Provider Single Sign-On URL**, **Identity Provider Issuer**,
and **Certificate** to **X.509 Certificate** from step 3. Once you've done that, press **Update** to complete the required configuration. and **Certificate** to **X.509 Certificate** from step 3. Once you've done that, press **Update** to complete the required configuration.
![SAML Okta paste values into Infisical](../../../images/sso/okta/idp-values-2.png) ![SAML Okta paste values into Infisical](../../../images/sso/okta/idp-values-2.png)
</Step> </Step>
<Step title="Assign users in Okta to the application"> <Step title="Assign users in Okta to the application">
Back in Okta, navigate to the **Assignments** tab and select **Assign**. You can assign access to the application on a user-by-user basis using the Assign to People option, or in-bulk using the Assign to Groups option. Back in Okta, navigate to the **Assignments** tab and select **Assign**. You can assign access to the application on a user-by-user basis using the Assign to People option, or in-bulk using the Assign to Groups option.
@@ -72,11 +75,13 @@ description: "Learn how to configure Okta SAML 2.0 for Infisical SSO."
![SAML Okta assignment](../../../images/sso/okta/assignment.png) ![SAML Okta assignment](../../../images/sso/okta/assignment.png)
At this point, you have configured everything you need within the context of the Okta Admin Portal. At this point, you have configured everything you need within the context of the Okta Admin Portal.
</Step> </Step>
<Step title="Enable SAML SSO in Infisical"> <Step title="Enable SAML SSO in Infisical">
Enabling SAML SSO allows members in your organization to log into Infisical via Okta. Enabling SAML SSO allows members in your organization to log into Infisical via Okta.
![SAML Okta enable SAML](../../../images/sso/okta/enable-saml.png) ![SAML Okta enable SAML](../../../images/sso/okta/enable-saml.png)
</Step> </Step>
<Step title="Enforce SAML SSO in Infisical"> <Step title="Enforce SAML SSO in Infisical">
Enforcing SAML SSO ensures that members in your organization can only access Infisical Enforcing SAML SSO ensures that members in your organization can only access Infisical
@@ -89,13 +94,15 @@ description: "Learn how to configure Okta SAML 2.0 for Infisical SSO."
We recommend ensuring that your account is provisioned the application in Okta We recommend ensuring that your account is provisioned the application in Okta
prior to enforcing SAML SSO to prevent any unintended issues. prior to enforcing SAML SSO to prevent any unintended issues.
</Warning> </Warning>
</Step> </Step>
</Steps> </Steps>
<Note> <Note>
If you're configuring SAML SSO on a self-hosted instance of Infisical, make sure to If you're configuring SAML SSO on a self-hosted instance of Infisical, make
set the `AUTH_SECRET` and `SITE_URL` environment variable for it to work: sure to set the `AUTH_SECRET` and `SITE_URL` environment variable for it to
work: - `AUTH_SECRET`: A secret key used for signing and verifying JWT. This
- `AUTH_SECRET`: A secret key used for signing and verifying JWT. This can be a random 32-byte base64 string generated with `openssl rand -base64 32`. can be a random 32-byte base64 string generated with `openssl rand -base64
- `SITE_URL`: The URL of your self-hosted instance of Infisical - should be an absolute URL including the protocol (e.g. https://app.infisical.com) 32`. - `SITE_URL`: The URL of your self-hosted instance of Infisical - should
</Note> be an absolute URL including the protocol (e.g. https://app.infisical.com)
</Note>

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