diff --git a/backend/package-lock.json b/backend/package-lock.json index b51573688..5928f64a5 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -1207,6 +1207,58 @@ "node": ">=14.0.0" } }, + "node_modules/@aws-sdk/client-secrets-manager/node_modules/@aws-sdk/client-sts": { + "version": "3.504.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sts/-/client-sts-3.504.0.tgz", + "integrity": "sha512-IESs8FkL7B/uY+ml4wgoRkrr6xYo4PizcNw6JX17eveq1gRBCPKeGMjE6HTDOcIYZZ8rqz/UeuH3JD4UhrMOnA==", + "dependencies": { + "@aws-crypto/sha256-browser": "3.0.0", + "@aws-crypto/sha256-js": "3.0.0", + "@aws-sdk/core": "3.496.0", + "@aws-sdk/middleware-host-header": "3.502.0", + "@aws-sdk/middleware-logger": "3.502.0", + "@aws-sdk/middleware-recursion-detection": "3.502.0", + "@aws-sdk/middleware-user-agent": "3.502.0", + "@aws-sdk/region-config-resolver": "3.502.0", + "@aws-sdk/types": "3.502.0", + "@aws-sdk/util-endpoints": "3.502.0", + "@aws-sdk/util-user-agent-browser": "3.502.0", + "@aws-sdk/util-user-agent-node": "3.502.0", + "@smithy/config-resolver": "^2.1.1", + "@smithy/core": "^1.3.1", + "@smithy/fetch-http-handler": "^2.4.1", + "@smithy/hash-node": "^2.1.1", + "@smithy/invalid-dependency": "^2.1.1", + "@smithy/middleware-content-length": "^2.1.1", + "@smithy/middleware-endpoint": "^2.4.1", + "@smithy/middleware-retry": "^2.1.1", + "@smithy/middleware-serde": "^2.1.1", + "@smithy/middleware-stack": "^2.1.1", + "@smithy/node-config-provider": "^2.2.1", + "@smithy/node-http-handler": "^2.3.1", + "@smithy/protocol-http": "^3.1.1", + "@smithy/smithy-client": "^2.3.1", + "@smithy/types": "^2.9.1", + "@smithy/url-parser": "^2.1.1", + "@smithy/util-base64": "^2.1.1", + "@smithy/util-body-length-browser": "^2.1.1", + "@smithy/util-body-length-node": "^2.2.1", + "@smithy/util-defaults-mode-browser": "^2.1.1", + "@smithy/util-defaults-mode-node": "^2.1.1", + "@smithy/util-endpoints": "^1.1.1", + "@smithy/util-middleware": "^2.1.1", + "@smithy/util-retry": "^2.1.1", + "@smithy/util-utf8": "^2.1.1", + "fast-xml-parser": "4.2.5", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "@aws-sdk/credential-provider-node": "^3.504.0" + } + }, "node_modules/@aws-sdk/client-secrets-manager/node_modules/uuid": { "version": "8.3.2", "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", @@ -1314,7 +1366,7 @@ "@aws-sdk/credential-provider-node": "^3.504.0" } }, - "node_modules/@aws-sdk/client-sts": { + "node_modules/@aws-sdk/client-sso-oidc/node_modules/@aws-sdk/client-sts": { "version": "3.504.0", "resolved": "https://registry.npmjs.org/@aws-sdk/client-sts/-/client-sts-3.504.0.tgz", "integrity": "sha512-IESs8FkL7B/uY+ml4wgoRkrr6xYo4PizcNw6JX17eveq1gRBCPKeGMjE6HTDOcIYZZ8rqz/UeuH3JD4UhrMOnA==", @@ -1436,6 +1488,58 @@ "node": ">=14.0.0" } }, + "node_modules/@aws-sdk/credential-provider-ini/node_modules/@aws-sdk/client-sts": { + "version": "3.504.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sts/-/client-sts-3.504.0.tgz", + "integrity": "sha512-IESs8FkL7B/uY+ml4wgoRkrr6xYo4PizcNw6JX17eveq1gRBCPKeGMjE6HTDOcIYZZ8rqz/UeuH3JD4UhrMOnA==", + "dependencies": { + "@aws-crypto/sha256-browser": "3.0.0", + "@aws-crypto/sha256-js": "3.0.0", + "@aws-sdk/core": "3.496.0", + "@aws-sdk/middleware-host-header": "3.502.0", + "@aws-sdk/middleware-logger": "3.502.0", + "@aws-sdk/middleware-recursion-detection": "3.502.0", + "@aws-sdk/middleware-user-agent": "3.502.0", + "@aws-sdk/region-config-resolver": "3.502.0", + "@aws-sdk/types": "3.502.0", + "@aws-sdk/util-endpoints": "3.502.0", + "@aws-sdk/util-user-agent-browser": "3.502.0", + "@aws-sdk/util-user-agent-node": "3.502.0", + "@smithy/config-resolver": "^2.1.1", + "@smithy/core": "^1.3.1", + "@smithy/fetch-http-handler": "^2.4.1", + "@smithy/hash-node": "^2.1.1", + "@smithy/invalid-dependency": "^2.1.1", + "@smithy/middleware-content-length": "^2.1.1", + "@smithy/middleware-endpoint": "^2.4.1", + "@smithy/middleware-retry": "^2.1.1", + "@smithy/middleware-serde": "^2.1.1", + "@smithy/middleware-stack": "^2.1.1", + "@smithy/node-config-provider": "^2.2.1", + "@smithy/node-http-handler": "^2.3.1", + "@smithy/protocol-http": "^3.1.1", + "@smithy/smithy-client": "^2.3.1", + "@smithy/types": "^2.9.1", + "@smithy/url-parser": "^2.1.1", + "@smithy/util-base64": "^2.1.1", + "@smithy/util-body-length-browser": "^2.1.1", + "@smithy/util-body-length-node": "^2.2.1", + "@smithy/util-defaults-mode-browser": "^2.1.1", + "@smithy/util-defaults-mode-node": "^2.1.1", + "@smithy/util-endpoints": "^1.1.1", + "@smithy/util-middleware": "^2.1.1", + "@smithy/util-retry": "^2.1.1", + "@smithy/util-utf8": "^2.1.1", + "fast-xml-parser": "4.2.5", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "@aws-sdk/credential-provider-node": "^3.504.0" + } + }, "node_modules/@aws-sdk/credential-provider-node": { "version": "3.504.0", "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.504.0.tgz", @@ -1505,6 +1609,58 @@ "node": ">=14.0.0" } }, + "node_modules/@aws-sdk/credential-provider-web-identity/node_modules/@aws-sdk/client-sts": { + "version": "3.504.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sts/-/client-sts-3.504.0.tgz", + "integrity": "sha512-IESs8FkL7B/uY+ml4wgoRkrr6xYo4PizcNw6JX17eveq1gRBCPKeGMjE6HTDOcIYZZ8rqz/UeuH3JD4UhrMOnA==", + "dependencies": { + "@aws-crypto/sha256-browser": "3.0.0", + "@aws-crypto/sha256-js": "3.0.0", + "@aws-sdk/core": "3.496.0", + "@aws-sdk/middleware-host-header": "3.502.0", + "@aws-sdk/middleware-logger": "3.502.0", + "@aws-sdk/middleware-recursion-detection": "3.502.0", + "@aws-sdk/middleware-user-agent": "3.502.0", + "@aws-sdk/region-config-resolver": "3.502.0", + "@aws-sdk/types": "3.502.0", + "@aws-sdk/util-endpoints": "3.502.0", + "@aws-sdk/util-user-agent-browser": "3.502.0", + "@aws-sdk/util-user-agent-node": "3.502.0", + "@smithy/config-resolver": "^2.1.1", + "@smithy/core": "^1.3.1", + "@smithy/fetch-http-handler": "^2.4.1", + "@smithy/hash-node": "^2.1.1", + "@smithy/invalid-dependency": "^2.1.1", + "@smithy/middleware-content-length": "^2.1.1", + "@smithy/middleware-endpoint": "^2.4.1", + "@smithy/middleware-retry": "^2.1.1", + "@smithy/middleware-serde": "^2.1.1", + "@smithy/middleware-stack": "^2.1.1", + "@smithy/node-config-provider": "^2.2.1", + "@smithy/node-http-handler": "^2.3.1", + "@smithy/protocol-http": "^3.1.1", + "@smithy/smithy-client": "^2.3.1", + "@smithy/types": "^2.9.1", + "@smithy/url-parser": "^2.1.1", + "@smithy/util-base64": "^2.1.1", + "@smithy/util-body-length-browser": "^2.1.1", + "@smithy/util-body-length-node": "^2.2.1", + "@smithy/util-defaults-mode-browser": "^2.1.1", + "@smithy/util-defaults-mode-node": "^2.1.1", + "@smithy/util-endpoints": "^1.1.1", + "@smithy/util-middleware": "^2.1.1", + "@smithy/util-retry": "^2.1.1", + "@smithy/util-utf8": "^2.1.1", + "fast-xml-parser": "4.2.5", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "@aws-sdk/credential-provider-node": "^3.504.0" + } + }, "node_modules/@aws-sdk/middleware-host-header": { "version": "3.502.0", "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-host-header/-/middleware-host-header-3.502.0.tgz", @@ -3657,60 +3813,60 @@ } }, "node_modules/@smithy/abort-controller": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/@smithy/abort-controller/-/abort-controller-2.1.3.tgz", - "integrity": "sha512-c2aYH2Wu1RVE3rLlVgg2kQOBJGM0WbjReQi5DnPTm2Zb7F0gk7J2aeQeaX2u/lQZoHl6gv8Oac7mt9alU3+f4A==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/abort-controller/-/abort-controller-2.2.0.tgz", + "integrity": "sha512-wRlta7GuLWpTqtFfGo+nZyOO1vEvewdNR1R4rTxpC8XU6vG/NDyrFBhwLZsqg1NUoR1noVaXJPC/7ZK47QCySw==", "dependencies": { - "@smithy/types": "^2.10.1", - "tslib": "^2.5.0" + "@smithy/types": "^2.12.0", + "tslib": "^2.6.2" }, "engines": { "node": ">=14.0.0" } }, "node_modules/@smithy/config-resolver": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/@smithy/config-resolver/-/config-resolver-2.1.4.tgz", - "integrity": "sha512-AW2WUZmBAzgO3V3ovKtsUbI3aBNMeQKFDumoqkNxaVDWF/xfnxAWqBKDr/NuG7c06N2Rm4xeZLPiJH/d+na0HA==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/config-resolver/-/config-resolver-2.2.0.tgz", + "integrity": "sha512-fsiMgd8toyUba6n1WRmr+qACzXltpdDkPTAaDqc8QqPBUzO+/JKwL6bUBseHVi8tu9l+3JOK+tSf7cay+4B3LA==", "dependencies": { - "@smithy/node-config-provider": "^2.2.4", - "@smithy/types": "^2.10.1", - "@smithy/util-config-provider": "^2.2.1", - "@smithy/util-middleware": "^2.1.3", - "tslib": "^2.5.0" + "@smithy/node-config-provider": "^2.3.0", + "@smithy/types": "^2.12.0", + "@smithy/util-config-provider": "^2.3.0", + "@smithy/util-middleware": "^2.2.0", + "tslib": "^2.6.2" }, "engines": { "node": ">=14.0.0" } }, "node_modules/@smithy/core": { - "version": "1.3.5", - "resolved": "https://registry.npmjs.org/@smithy/core/-/core-1.3.5.tgz", - "integrity": "sha512-Rrc+e2Jj6Gu7Xbn0jvrzZlSiP2CZocIOfZ9aNUA82+1sa6GBnxqL9+iZ9EKHeD9aqD1nU8EK4+oN2EiFpSv7Yw==", + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/@smithy/core/-/core-1.4.2.tgz", + "integrity": "sha512-2fek3I0KZHWJlRLvRTqxTEri+qV0GRHrJIoLFuBMZB4EMg4WgeBGfF0X6abnrNYpq55KJ6R4D6x4f0vLnhzinA==", "dependencies": { - "@smithy/middleware-endpoint": "^2.4.4", - "@smithy/middleware-retry": "^2.1.4", - "@smithy/middleware-serde": "^2.1.3", - "@smithy/protocol-http": "^3.2.1", - "@smithy/smithy-client": "^2.4.2", - "@smithy/types": "^2.10.1", - "@smithy/util-middleware": "^2.1.3", - "tslib": "^2.5.0" + "@smithy/middleware-endpoint": "^2.5.1", + "@smithy/middleware-retry": "^2.3.1", + "@smithy/middleware-serde": "^2.3.0", + "@smithy/protocol-http": "^3.3.0", + "@smithy/smithy-client": "^2.5.1", + "@smithy/types": "^2.12.0", + "@smithy/util-middleware": "^2.2.0", + "tslib": "^2.6.2" }, "engines": { "node": ">=14.0.0" } }, "node_modules/@smithy/credential-provider-imds": { - "version": "2.2.4", - "resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-2.2.4.tgz", - "integrity": "sha512-DdatjmBZQnhGe1FhI8gO98f7NmvQFSDiZTwC3WMvLTCKQUY+Y1SVkhJqIuLu50Eb7pTheoXQmK+hKYUgpUWsNA==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-2.3.0.tgz", + "integrity": "sha512-BWB9mIukO1wjEOo1Ojgl6LrG4avcaC7T/ZP6ptmAaW4xluhSIPZhY+/PI5YKzlk+jsm+4sQZB45Bt1OfMeQa3w==", "dependencies": { - "@smithy/node-config-provider": "^2.2.4", - "@smithy/property-provider": "^2.1.3", - "@smithy/types": "^2.10.1", - "@smithy/url-parser": "^2.1.3", - "tslib": "^2.5.0" + "@smithy/node-config-provider": "^2.3.0", + "@smithy/property-provider": "^2.2.0", + "@smithy/types": "^2.12.0", + "@smithy/url-parser": "^2.2.0", + "tslib": "^2.6.2" }, "engines": { "node": ">=14.0.0" @@ -3779,459 +3935,451 @@ } }, "node_modules/@smithy/fetch-http-handler": { - "version": "2.4.3", - "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-2.4.3.tgz", - "integrity": "sha512-Fn/KYJFo6L5I4YPG8WQb2hOmExgRmNpVH5IK2zU3JKrY5FKW7y9ar5e0BexiIC9DhSKqKX+HeWq/Y18fq7Dkpw==", + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-2.5.0.tgz", + "integrity": "sha512-BOWEBeppWhLn/no/JxUL/ghTfANTjT7kg3Ww2rPqTUY9R4yHPXxJ9JhMe3Z03LN3aPwiwlpDIUcVw1xDyHqEhw==", "dependencies": { - "@smithy/protocol-http": "^3.2.1", - "@smithy/querystring-builder": "^2.1.3", - "@smithy/types": "^2.10.1", - "@smithy/util-base64": "^2.1.1", - "tslib": "^2.5.0" + "@smithy/protocol-http": "^3.3.0", + "@smithy/querystring-builder": "^2.2.0", + "@smithy/types": "^2.12.0", + "@smithy/util-base64": "^2.3.0", + "tslib": "^2.6.2" } }, "node_modules/@smithy/hash-node": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/@smithy/hash-node/-/hash-node-2.1.3.tgz", - "integrity": "sha512-FsAPCUj7VNJIdHbSxMd5uiZiF20G2zdSDgrgrDrHqIs/VMxK85Vqk5kMVNNDMCZmMezp6UKnac0B4nAyx7HJ9g==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/hash-node/-/hash-node-2.2.0.tgz", + "integrity": "sha512-zLWaC/5aWpMrHKpoDF6nqpNtBhlAYKF/7+9yMN7GpdR8CzohnWfGtMznPybnwSS8saaXBMxIGwJqR4HmRp6b3g==", "dependencies": { - "@smithy/types": "^2.10.1", - "@smithy/util-buffer-from": "^2.1.1", - "@smithy/util-utf8": "^2.1.1", - "tslib": "^2.5.0" + "@smithy/types": "^2.12.0", + "@smithy/util-buffer-from": "^2.2.0", + "@smithy/util-utf8": "^2.3.0", + "tslib": "^2.6.2" }, "engines": { "node": ">=14.0.0" } }, "node_modules/@smithy/invalid-dependency": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/@smithy/invalid-dependency/-/invalid-dependency-2.1.3.tgz", - "integrity": "sha512-wkra7d/G4CbngV4xsjYyAYOvdAhahQje/WymuQdVEnXFExJopEu7fbL5AEAlBPgWHXwu94VnCSG00gVzRfExyg==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/invalid-dependency/-/invalid-dependency-2.2.0.tgz", + "integrity": "sha512-nEDASdbKFKPXN2O6lOlTgrEEOO9NHIeO+HVvZnkqc8h5U9g3BIhWsvzFo+UcUbliMHvKNPD/zVxDrkP1Sbgp8Q==", "dependencies": { - "@smithy/types": "^2.10.1", - "tslib": "^2.5.0" + "@smithy/types": "^2.12.0", + "tslib": "^2.6.2" } }, "node_modules/@smithy/is-array-buffer": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-2.1.1.tgz", - "integrity": "sha512-xozSQrcUinPpNPNPds4S7z/FakDTh1MZWtRP/2vQtYB/u3HYrX2UXuZs+VhaKBd6Vc7g2XPr2ZtwGBNDN6fNKQ==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-2.2.0.tgz", + "integrity": "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==", "dependencies": { - "tslib": "^2.5.0" + "tslib": "^2.6.2" }, "engines": { "node": ">=14.0.0" } }, "node_modules/@smithy/middleware-content-length": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/@smithy/middleware-content-length/-/middleware-content-length-2.1.3.tgz", - "integrity": "sha512-aJduhkC+dcXxdnv5ZpM3uMmtGmVFKx412R1gbeykS5HXDmRU6oSsyy2SoHENCkfOGKAQOjVE2WVqDJibC0d21g==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/middleware-content-length/-/middleware-content-length-2.2.0.tgz", + "integrity": "sha512-5bl2LG1Ah/7E5cMSC+q+h3IpVHMeOkG0yLRyQT1p2aMJkSrZG7RlXHPuAgb7EyaFeidKEnnd/fNaLLaKlHGzDQ==", "dependencies": { - "@smithy/protocol-http": "^3.2.1", - "@smithy/types": "^2.10.1", - "tslib": "^2.5.0" + "@smithy/protocol-http": "^3.3.0", + "@smithy/types": "^2.12.0", + "tslib": "^2.6.2" }, "engines": { "node": ">=14.0.0" } }, "node_modules/@smithy/middleware-endpoint": { - "version": "2.4.4", - "resolved": "https://registry.npmjs.org/@smithy/middleware-endpoint/-/middleware-endpoint-2.4.4.tgz", - "integrity": "sha512-4yjHyHK2Jul4JUDBo2sTsWY9UshYUnXeb/TAK/MTaPEb8XQvDmpwSFnfIRDU45RY1a6iC9LCnmJNg/yHyfxqkw==", + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@smithy/middleware-endpoint/-/middleware-endpoint-2.5.1.tgz", + "integrity": "sha512-1/8kFp6Fl4OsSIVTWHnNjLnTL8IqpIb/D3sTSczrKFnrE9VMNWxnrRKNvpUHOJ6zpGD5f62TPm7+17ilTJpiCQ==", "dependencies": { - "@smithy/middleware-serde": "^2.1.3", - "@smithy/node-config-provider": "^2.2.4", - "@smithy/shared-ini-file-loader": "^2.3.4", - "@smithy/types": "^2.10.1", - "@smithy/url-parser": "^2.1.3", - "@smithy/util-middleware": "^2.1.3", - "tslib": "^2.5.0" + "@smithy/middleware-serde": "^2.3.0", + "@smithy/node-config-provider": "^2.3.0", + "@smithy/shared-ini-file-loader": "^2.4.0", + "@smithy/types": "^2.12.0", + "@smithy/url-parser": "^2.2.0", + "@smithy/util-middleware": "^2.2.0", + "tslib": "^2.6.2" }, "engines": { "node": ">=14.0.0" } }, "node_modules/@smithy/middleware-retry": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/@smithy/middleware-retry/-/middleware-retry-2.1.4.tgz", - "integrity": "sha512-Cyolv9YckZTPli1EkkaS39UklonxMd08VskiuMhURDjC0HHa/AD6aK/YoD21CHv9s0QLg0WMLvk9YeLTKkXaFQ==", + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/@smithy/middleware-retry/-/middleware-retry-2.3.1.tgz", + "integrity": "sha512-P2bGufFpFdYcWvqpyqqmalRtwFUNUA8vHjJR5iGqbfR6mp65qKOLcUd6lTr4S9Gn/enynSrSf3p3FVgVAf6bXA==", "dependencies": { - "@smithy/node-config-provider": "^2.2.4", - "@smithy/protocol-http": "^3.2.1", - "@smithy/service-error-classification": "^2.1.3", - "@smithy/smithy-client": "^2.4.2", - "@smithy/types": "^2.10.1", - "@smithy/util-middleware": "^2.1.3", - "@smithy/util-retry": "^2.1.3", - "tslib": "^2.5.0", - "uuid": "^8.3.2" + "@smithy/node-config-provider": "^2.3.0", + "@smithy/protocol-http": "^3.3.0", + "@smithy/service-error-classification": "^2.1.5", + "@smithy/smithy-client": "^2.5.1", + "@smithy/types": "^2.12.0", + "@smithy/util-middleware": "^2.2.0", + "@smithy/util-retry": "^2.2.0", + "tslib": "^2.6.2", + "uuid": "^9.0.1" }, "engines": { "node": ">=14.0.0" } }, - "node_modules/@smithy/middleware-retry/node_modules/uuid": { - "version": "8.3.2", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", - "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", - "bin": { - "uuid": "dist/bin/uuid" - } - }, "node_modules/@smithy/middleware-serde": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/@smithy/middleware-serde/-/middleware-serde-2.1.3.tgz", - "integrity": "sha512-s76LId+TwASrHhUa9QS4k/zeXDUAuNuddKklQzRgumbzge5BftVXHXIqL4wQxKGLocPwfgAOXWx+HdWhQk9hTg==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@smithy/middleware-serde/-/middleware-serde-2.3.0.tgz", + "integrity": "sha512-sIADe7ojwqTyvEQBe1nc/GXB9wdHhi9UwyX0lTyttmUWDJLP655ZYE1WngnNyXREme8I27KCaUhyhZWRXL0q7Q==", "dependencies": { - "@smithy/types": "^2.10.1", - "tslib": "^2.5.0" + "@smithy/types": "^2.12.0", + "tslib": "^2.6.2" }, "engines": { "node": ">=14.0.0" } }, "node_modules/@smithy/middleware-stack": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/@smithy/middleware-stack/-/middleware-stack-2.1.3.tgz", - "integrity": "sha512-opMFufVQgvBSld/b7mD7OOEBxF6STyraVr1xel1j0abVILM8ALJvRoFbqSWHGmaDlRGIiV9Q5cGbWi0sdiEaLQ==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/middleware-stack/-/middleware-stack-2.2.0.tgz", + "integrity": "sha512-Qntc3jrtwwrsAC+X8wms8zhrTr0sFXnyEGhZd9sLtsJ/6gGQKFzNB+wWbOcpJd7BR8ThNCoKt76BuQahfMvpeA==", "dependencies": { - "@smithy/types": "^2.10.1", - "tslib": "^2.5.0" + "@smithy/types": "^2.12.0", + "tslib": "^2.6.2" }, "engines": { "node": ">=14.0.0" } }, "node_modules/@smithy/node-config-provider": { - "version": "2.2.4", - "resolved": "https://registry.npmjs.org/@smithy/node-config-provider/-/node-config-provider-2.2.4.tgz", - "integrity": "sha512-nqazHCp8r4KHSFhRQ+T0VEkeqvA0U+RhehBSr1gunUuNW3X7j0uDrWBxB2gE9eutzy6kE3Y7L+Dov/UXT871vg==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@smithy/node-config-provider/-/node-config-provider-2.3.0.tgz", + "integrity": "sha512-0elK5/03a1JPWMDPaS726Iw6LpQg80gFut1tNpPfxFuChEEklo2yL823V94SpTZTxmKlXFtFgsP55uh3dErnIg==", "dependencies": { - "@smithy/property-provider": "^2.1.3", - "@smithy/shared-ini-file-loader": "^2.3.4", - "@smithy/types": "^2.10.1", - "tslib": "^2.5.0" + "@smithy/property-provider": "^2.2.0", + "@smithy/shared-ini-file-loader": "^2.4.0", + "@smithy/types": "^2.12.0", + "tslib": "^2.6.2" }, "engines": { "node": ">=14.0.0" } }, "node_modules/@smithy/node-http-handler": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-2.4.1.tgz", - "integrity": "sha512-HCkb94soYhJMxPCa61wGKgmeKpJ3Gftx1XD6bcWEB2wMV1L9/SkQu/6/ysKBnbOzWRE01FGzwrTxucHypZ8rdg==", + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-2.5.0.tgz", + "integrity": "sha512-mVGyPBzkkGQsPoxQUbxlEfRjrj6FPyA3u3u2VXGr9hT8wilsoQdZdvKpMBFMB8Crfhv5dNkKHIW0Yyuc7eABqA==", "dependencies": { - "@smithy/abort-controller": "^2.1.3", - "@smithy/protocol-http": "^3.2.1", - "@smithy/querystring-builder": "^2.1.3", - "@smithy/types": "^2.10.1", - "tslib": "^2.5.0" + "@smithy/abort-controller": "^2.2.0", + "@smithy/protocol-http": "^3.3.0", + "@smithy/querystring-builder": "^2.2.0", + "@smithy/types": "^2.12.0", + "tslib": "^2.6.2" }, "engines": { "node": ">=14.0.0" } }, "node_modules/@smithy/property-provider": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/@smithy/property-provider/-/property-provider-2.1.3.tgz", - "integrity": "sha512-bMz3se+ySKWNrgm7eIiQMa2HO/0fl2D0HvLAdg9pTMcpgp4SqOAh6bz7Ik6y7uQqSrk4rLjIKgbQ6yzYgGehCQ==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/property-provider/-/property-provider-2.2.0.tgz", + "integrity": "sha512-+xiil2lFhtTRzXkx8F053AV46QnIw6e7MV8od5Mi68E1ICOjCeCHw2XfLnDEUHnT9WGUIkwcqavXjfwuJbGlpg==", "dependencies": { - "@smithy/types": "^2.10.1", - "tslib": "^2.5.0" + "@smithy/types": "^2.12.0", + "tslib": "^2.6.2" }, "engines": { "node": ">=14.0.0" } }, "node_modules/@smithy/protocol-http": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/@smithy/protocol-http/-/protocol-http-3.2.1.tgz", - "integrity": "sha512-KLrQkEw4yJCeAmAH7hctE8g9KwA7+H2nSJwxgwIxchbp/L0B5exTdOQi9D5HinPLlothoervGmhpYKelZ6AxIA==", + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/@smithy/protocol-http/-/protocol-http-3.3.0.tgz", + "integrity": "sha512-Xy5XK1AFWW2nlY/biWZXu6/krgbaf2dg0q492D8M5qthsnU2H+UgFeZLbM76FnH7s6RO/xhQRkj+T6KBO3JzgQ==", "dependencies": { - "@smithy/types": "^2.10.1", - "tslib": "^2.5.0" + "@smithy/types": "^2.12.0", + "tslib": "^2.6.2" }, "engines": { "node": ">=14.0.0" } }, "node_modules/@smithy/querystring-builder": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/@smithy/querystring-builder/-/querystring-builder-2.1.3.tgz", - "integrity": "sha512-kFD3PnNqKELe6m9GRHQw/ftFFSZpnSeQD4qvgDB6BQN6hREHELSosVFUMPN4M3MDKN2jAwk35vXHLoDrNfKu0A==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/querystring-builder/-/querystring-builder-2.2.0.tgz", + "integrity": "sha512-L1kSeviUWL+emq3CUVSgdogoM/D9QMFaqxL/dd0X7PCNWmPXqt+ExtrBjqT0V7HLN03Vs9SuiLrG3zy3JGnE5A==", "dependencies": { - "@smithy/types": "^2.10.1", - "@smithy/util-uri-escape": "^2.1.1", - "tslib": "^2.5.0" + "@smithy/types": "^2.12.0", + "@smithy/util-uri-escape": "^2.2.0", + "tslib": "^2.6.2" }, "engines": { "node": ">=14.0.0" } }, "node_modules/@smithy/querystring-parser": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/@smithy/querystring-parser/-/querystring-parser-2.1.3.tgz", - "integrity": "sha512-3+CWJoAqcBMR+yvz6D+Fc5VdoGFtfenW6wqSWATWajrRMGVwJGPT3Vy2eb2bnMktJc4HU4bpjeovFa566P3knQ==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/querystring-parser/-/querystring-parser-2.2.0.tgz", + "integrity": "sha512-BvHCDrKfbG5Yhbpj4vsbuPV2GgcpHiAkLeIlcA1LtfpMz3jrqizP1+OguSNSj1MwBHEiN+jwNisXLGdajGDQJA==", "dependencies": { - "@smithy/types": "^2.10.1", - "tslib": "^2.5.0" + "@smithy/types": "^2.12.0", + "tslib": "^2.6.2" }, "engines": { "node": ">=14.0.0" } }, "node_modules/@smithy/service-error-classification": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/@smithy/service-error-classification/-/service-error-classification-2.1.3.tgz", - "integrity": "sha512-iUrpSsem97bbXHHT/v3s7vaq8IIeMo6P6cXdeYHrx0wOJpMeBGQF7CB0mbJSiTm3//iq3L55JiEm8rA7CTVI8A==", + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@smithy/service-error-classification/-/service-error-classification-2.1.5.tgz", + "integrity": "sha512-uBDTIBBEdAQryvHdc5W8sS5YX7RQzF683XrHePVdFmAgKiMofU15FLSM0/HU03hKTnazdNRFa0YHS7+ArwoUSQ==", "dependencies": { - "@smithy/types": "^2.10.1" + "@smithy/types": "^2.12.0" }, "engines": { "node": ">=14.0.0" } }, "node_modules/@smithy/shared-ini-file-loader": { - "version": "2.3.4", - "resolved": "https://registry.npmjs.org/@smithy/shared-ini-file-loader/-/shared-ini-file-loader-2.3.4.tgz", - "integrity": "sha512-CiZmPg9GeDKbKmJGEFvJBsJcFnh0AQRzOtQAzj1XEa8N/0/uSN/v1LYzgO7ry8hhO8+9KB7+DhSW0weqBra4Aw==", + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/@smithy/shared-ini-file-loader/-/shared-ini-file-loader-2.4.0.tgz", + "integrity": "sha512-WyujUJL8e1B6Z4PBfAqC/aGY1+C7T0w20Gih3yrvJSk97gpiVfB+y7c46T4Nunk+ZngLq0rOIdeVeIklk0R3OA==", "dependencies": { - "@smithy/types": "^2.10.1", - "tslib": "^2.5.0" + "@smithy/types": "^2.12.0", + "tslib": "^2.6.2" }, "engines": { "node": ">=14.0.0" } }, "node_modules/@smithy/signature-v4": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-2.1.3.tgz", - "integrity": "sha512-Jq4iPPdCmJojZTsPePn4r1ULShh6ONkokLuxp1Lnk4Sq7r7rJp4HlA1LbPBq4bD64TIzQezIpr1X+eh5NYkNxw==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-2.3.0.tgz", + "integrity": "sha512-ui/NlpILU+6HAQBfJX8BBsDXuKSNrjTSuOYArRblcrErwKFutjrCNb/OExfVRyj9+26F9J+ZmfWT+fKWuDrH3Q==", "dependencies": { - "@smithy/eventstream-codec": "^2.1.3", - "@smithy/is-array-buffer": "^2.1.1", - "@smithy/types": "^2.10.1", - "@smithy/util-hex-encoding": "^2.1.1", - "@smithy/util-middleware": "^2.1.3", - "@smithy/util-uri-escape": "^2.1.1", - "@smithy/util-utf8": "^2.1.1", - "tslib": "^2.5.0" + "@smithy/is-array-buffer": "^2.2.0", + "@smithy/types": "^2.12.0", + "@smithy/util-hex-encoding": "^2.2.0", + "@smithy/util-middleware": "^2.2.0", + "@smithy/util-uri-escape": "^2.2.0", + "@smithy/util-utf8": "^2.3.0", + "tslib": "^2.6.2" }, "engines": { "node": ">=14.0.0" } }, "node_modules/@smithy/smithy-client": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/@smithy/smithy-client/-/smithy-client-2.4.2.tgz", - "integrity": "sha512-ntAFYN51zu3N3mCd95YFcFi/8rmvm//uX+HnK24CRbI6k5Rjackn0JhgKz5zOx/tbNvOpgQIwhSX+1EvEsBLbA==", + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@smithy/smithy-client/-/smithy-client-2.5.1.tgz", + "integrity": "sha512-jrbSQrYCho0yDaaf92qWgd+7nAeap5LtHTI51KXqmpIFCceKU3K9+vIVTUH72bOJngBMqa4kyu1VJhRcSrk/CQ==", "dependencies": { - "@smithy/middleware-endpoint": "^2.4.4", - "@smithy/middleware-stack": "^2.1.3", - "@smithy/protocol-http": "^3.2.1", - "@smithy/types": "^2.10.1", - "@smithy/util-stream": "^2.1.3", - "tslib": "^2.5.0" + "@smithy/middleware-endpoint": "^2.5.1", + "@smithy/middleware-stack": "^2.2.0", + "@smithy/protocol-http": "^3.3.0", + "@smithy/types": "^2.12.0", + "@smithy/util-stream": "^2.2.0", + "tslib": "^2.6.2" }, "engines": { "node": ">=14.0.0" } }, "node_modules/@smithy/types": { - "version": "2.10.1", - "resolved": "https://registry.npmjs.org/@smithy/types/-/types-2.10.1.tgz", - "integrity": "sha512-hjQO+4ru4cQ58FluQvKKiyMsFg0A6iRpGm2kqdH8fniyNd2WyanoOsYJfMX/IFLuLxEoW6gnRkNZy1y6fUUhtA==", + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-2.12.0.tgz", + "integrity": "sha512-QwYgloJ0sVNBeBuBs65cIkTbfzV/Q6ZNPCJ99EICFEdJYG50nGIY/uYXp+TbsdJReIuPr0a0kXmCvren3MbRRw==", "dependencies": { - "tslib": "^2.5.0" + "tslib": "^2.6.2" }, "engines": { "node": ">=14.0.0" } }, "node_modules/@smithy/url-parser": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/@smithy/url-parser/-/url-parser-2.1.3.tgz", - "integrity": "sha512-X1NRA4WzK/ihgyzTpeGvI9Wn45y8HmqF4AZ/FazwAv8V203Ex+4lXqcYI70naX9ETqbqKVzFk88W6WJJzCggTQ==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/url-parser/-/url-parser-2.2.0.tgz", + "integrity": "sha512-hoA4zm61q1mNTpksiSWp2nEl1dt3j726HdRhiNgVJQMj7mLp7dprtF57mOB6JvEk/x9d2bsuL5hlqZbBuHQylQ==", "dependencies": { - "@smithy/querystring-parser": "^2.1.3", - "@smithy/types": "^2.10.1", - "tslib": "^2.5.0" + "@smithy/querystring-parser": "^2.2.0", + "@smithy/types": "^2.12.0", + "tslib": "^2.6.2" } }, "node_modules/@smithy/util-base64": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/@smithy/util-base64/-/util-base64-2.1.1.tgz", - "integrity": "sha512-UfHVpY7qfF/MrgndI5PexSKVTxSZIdz9InghTFa49QOvuu9I52zLPLUHXvHpNuMb1iD2vmc6R+zbv/bdMipR/g==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@smithy/util-base64/-/util-base64-2.3.0.tgz", + "integrity": "sha512-s3+eVwNeJuXUwuMbusncZNViuhv2LjVJ1nMwTqSA0XAC7gjKhqqxRdJPhR8+YrkoZ9IiIbFk/yK6ACe/xlF+hw==", "dependencies": { - "@smithy/util-buffer-from": "^2.1.1", - "tslib": "^2.5.0" + "@smithy/util-buffer-from": "^2.2.0", + "@smithy/util-utf8": "^2.3.0", + "tslib": "^2.6.2" }, "engines": { "node": ">=14.0.0" } }, "node_modules/@smithy/util-body-length-browser": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/@smithy/util-body-length-browser/-/util-body-length-browser-2.1.1.tgz", - "integrity": "sha512-ekOGBLvs1VS2d1zM2ER4JEeBWAvIOUKeaFch29UjjJsxmZ/f0L3K3x0dEETgh3Q9bkZNHgT+rkdl/J/VUqSRag==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-body-length-browser/-/util-body-length-browser-2.2.0.tgz", + "integrity": "sha512-dtpw9uQP7W+n3vOtx0CfBD5EWd7EPdIdsQnWTDoFf77e3VUf05uA7R7TGipIo8e4WL2kuPdnsr3hMQn9ziYj5w==", "dependencies": { - "tslib": "^2.5.0" + "tslib": "^2.6.2" } }, "node_modules/@smithy/util-body-length-node": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/@smithy/util-body-length-node/-/util-body-length-node-2.2.1.tgz", - "integrity": "sha512-/ggJG+ta3IDtpNVq4ktmEUtOkH1LW64RHB5B0hcr5ZaWBmo96UX2cIOVbjCqqDickTXqBWZ4ZO0APuaPrD7Abg==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@smithy/util-body-length-node/-/util-body-length-node-2.3.0.tgz", + "integrity": "sha512-ITWT1Wqjubf2CJthb0BuT9+bpzBfXeMokH/AAa5EJQgbv9aPMVfnM76iFIZVFf50hYXGbtiV71BHAthNWd6+dw==", "dependencies": { - "tslib": "^2.5.0" + "tslib": "^2.6.2" }, "engines": { "node": ">=14.0.0" } }, "node_modules/@smithy/util-buffer-from": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-2.1.1.tgz", - "integrity": "sha512-clhNjbyfqIv9Md2Mg6FffGVrJxw7bgK7s3Iax36xnfVj6cg0fUG7I4RH0XgXJF8bxi+saY5HR21g2UPKSxVCXg==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-2.2.0.tgz", + "integrity": "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==", "dependencies": { - "@smithy/is-array-buffer": "^2.1.1", - "tslib": "^2.5.0" + "@smithy/is-array-buffer": "^2.2.0", + "tslib": "^2.6.2" }, "engines": { "node": ">=14.0.0" } }, "node_modules/@smithy/util-config-provider": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/@smithy/util-config-provider/-/util-config-provider-2.2.1.tgz", - "integrity": "sha512-50VL/tx9oYYcjJn/qKqNy7sCtpD0+s8XEBamIFo4mFFTclKMNp+rsnymD796uybjiIquB7VCB/DeafduL0y2kw==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@smithy/util-config-provider/-/util-config-provider-2.3.0.tgz", + "integrity": "sha512-HZkzrRcuFN1k70RLqlNK4FnPXKOpkik1+4JaBoHNJn+RnJGYqaa3c5/+XtLOXhlKzlRgNvyaLieHTW2VwGN0VQ==", "dependencies": { - "tslib": "^2.5.0" + "tslib": "^2.6.2" }, "engines": { "node": ">=14.0.0" } }, "node_modules/@smithy/util-defaults-mode-browser": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-browser/-/util-defaults-mode-browser-2.1.4.tgz", - "integrity": "sha512-J6XAVY+/g7jf03QMnvqPyU+8jqGrrtXoKWFVOS+n1sz0Lg8HjHJ1ANqaDN+KTTKZRZlvG8nU5ZrJOUL6VdwgcQ==", + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-browser/-/util-defaults-mode-browser-2.2.1.tgz", + "integrity": "sha512-RtKW+8j8skk17SYowucwRUjeh4mCtnm5odCL0Lm2NtHQBsYKrNW0od9Rhopu9wF1gHMfHeWF7i90NwBz/U22Kw==", "dependencies": { - "@smithy/property-provider": "^2.1.3", - "@smithy/smithy-client": "^2.4.2", - "@smithy/types": "^2.10.1", + "@smithy/property-provider": "^2.2.0", + "@smithy/smithy-client": "^2.5.1", + "@smithy/types": "^2.12.0", "bowser": "^2.11.0", - "tslib": "^2.5.0" + "tslib": "^2.6.2" }, "engines": { "node": ">= 10.0.0" } }, "node_modules/@smithy/util-defaults-mode-node": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-node/-/util-defaults-mode-node-2.2.3.tgz", - "integrity": "sha512-ttUISrv1uVOjTlDa3nznX33f0pthoUlP+4grhTvOzcLhzArx8qHB94/untGACOG3nlf8vU20nI2iWImfzoLkYA==", + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-node/-/util-defaults-mode-node-2.3.1.tgz", + "integrity": "sha512-vkMXHQ0BcLFysBMWgSBLSk3+leMpFSyyFj8zQtv5ZyUBx8/owVh1/pPEkzmW/DR/Gy/5c8vjLDD9gZjXNKbrpA==", "dependencies": { - "@smithy/config-resolver": "^2.1.4", - "@smithy/credential-provider-imds": "^2.2.4", - "@smithy/node-config-provider": "^2.2.4", - "@smithy/property-provider": "^2.1.3", - "@smithy/smithy-client": "^2.4.2", - "@smithy/types": "^2.10.1", - "tslib": "^2.5.0" + "@smithy/config-resolver": "^2.2.0", + "@smithy/credential-provider-imds": "^2.3.0", + "@smithy/node-config-provider": "^2.3.0", + "@smithy/property-provider": "^2.2.0", + "@smithy/smithy-client": "^2.5.1", + "@smithy/types": "^2.12.0", + "tslib": "^2.6.2" }, "engines": { "node": ">= 10.0.0" } }, "node_modules/@smithy/util-endpoints": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/@smithy/util-endpoints/-/util-endpoints-1.1.4.tgz", - "integrity": "sha512-/qAeHmK5l4yQ4/bCIJ9p49wDe9rwWtOzhPHblu386fwPNT3pxmodgcs9jDCV52yK9b4rB8o9Sj31P/7Vzka1cg==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-endpoints/-/util-endpoints-1.2.0.tgz", + "integrity": "sha512-BuDHv8zRjsE5zXd3PxFXFknzBG3owCpjq8G3FcsXW3CykYXuEqM3nTSsmLzw5q+T12ZYuDlVUZKBdpNbhVtlrQ==", "dependencies": { - "@smithy/node-config-provider": "^2.2.4", - "@smithy/types": "^2.10.1", - "tslib": "^2.5.0" + "@smithy/node-config-provider": "^2.3.0", + "@smithy/types": "^2.12.0", + "tslib": "^2.6.2" }, "engines": { "node": ">= 14.0.0" } }, "node_modules/@smithy/util-hex-encoding": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/@smithy/util-hex-encoding/-/util-hex-encoding-2.1.1.tgz", - "integrity": "sha512-3UNdP2pkYUUBGEXzQI9ODTDK+Tcu1BlCyDBaRHwyxhA+8xLP8agEKQq4MGmpjqb4VQAjq9TwlCQX0kP6XDKYLg==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-hex-encoding/-/util-hex-encoding-2.2.0.tgz", + "integrity": "sha512-7iKXR+/4TpLK194pVjKiasIyqMtTYJsgKgM242Y9uzt5dhHnUDvMNb+3xIhRJ9QhvqGii/5cRUt4fJn3dtXNHQ==", "dependencies": { - "tslib": "^2.5.0" + "tslib": "^2.6.2" }, "engines": { "node": ">=14.0.0" } }, "node_modules/@smithy/util-middleware": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/@smithy/util-middleware/-/util-middleware-2.1.3.tgz", - "integrity": "sha512-/+2fm7AZ2ozl5h8wM++ZP0ovE9/tiUUAHIbCfGfb3Zd3+Dyk17WODPKXBeJ/TnK5U+x743QmA0xHzlSm8I/qhw==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-middleware/-/util-middleware-2.2.0.tgz", + "integrity": "sha512-L1qpleXf9QD6LwLCJ5jddGkgWyuSvWBkJwWAZ6kFkdifdso+sk3L3O1HdmPvCdnCK3IS4qWyPxev01QMnfHSBw==", "dependencies": { - "@smithy/types": "^2.10.1", - "tslib": "^2.5.0" + "@smithy/types": "^2.12.0", + "tslib": "^2.6.2" }, "engines": { "node": ">=14.0.0" } }, "node_modules/@smithy/util-retry": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/@smithy/util-retry/-/util-retry-2.1.3.tgz", - "integrity": "sha512-Kbvd+GEMuozbNUU3B89mb99tbufwREcyx2BOX0X2+qHjq6Gvsah8xSDDgxISDwcOHoDqUWO425F0Uc/QIRhYkg==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-retry/-/util-retry-2.2.0.tgz", + "integrity": "sha512-q9+pAFPTfftHXRytmZ7GzLFFrEGavqapFc06XxzZFcSIGERXMerXxCitjOG1prVDR9QdjqotF40SWvbqcCpf8g==", "dependencies": { - "@smithy/service-error-classification": "^2.1.3", - "@smithy/types": "^2.10.1", - "tslib": "^2.5.0" + "@smithy/service-error-classification": "^2.1.5", + "@smithy/types": "^2.12.0", + "tslib": "^2.6.2" }, "engines": { "node": ">= 14.0.0" } }, "node_modules/@smithy/util-stream": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/@smithy/util-stream/-/util-stream-2.1.3.tgz", - "integrity": "sha512-HvpEQbP8raTy9n86ZfXiAkf3ezp1c3qeeO//zGqwZdrfaoOpGKQgF2Sv1IqZp7wjhna7pvczWaGUHjcOPuQwKw==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-stream/-/util-stream-2.2.0.tgz", + "integrity": "sha512-17faEXbYWIRst1aU9SvPZyMdWmqIrduZjVOqCPMIsWFNxs5yQQgFrJL6b2SdiCzyW9mJoDjFtgi53xx7EH+BXA==", "dependencies": { - "@smithy/fetch-http-handler": "^2.4.3", - "@smithy/node-http-handler": "^2.4.1", - "@smithy/types": "^2.10.1", - "@smithy/util-base64": "^2.1.1", - "@smithy/util-buffer-from": "^2.1.1", - "@smithy/util-hex-encoding": "^2.1.1", - "@smithy/util-utf8": "^2.1.1", - "tslib": "^2.5.0" + "@smithy/fetch-http-handler": "^2.5.0", + "@smithy/node-http-handler": "^2.5.0", + "@smithy/types": "^2.12.0", + "@smithy/util-base64": "^2.3.0", + "@smithy/util-buffer-from": "^2.2.0", + "@smithy/util-hex-encoding": "^2.2.0", + "@smithy/util-utf8": "^2.3.0", + "tslib": "^2.6.2" }, "engines": { "node": ">=14.0.0" } }, "node_modules/@smithy/util-uri-escape": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/@smithy/util-uri-escape/-/util-uri-escape-2.1.1.tgz", - "integrity": "sha512-saVzI1h6iRBUVSqtnlOnc9ssU09ypo7n+shdQ8hBTZno/9rZ3AuRYvoHInV57VF7Qn7B+pFJG7qTzFiHxWlWBw==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-uri-escape/-/util-uri-escape-2.2.0.tgz", + "integrity": "sha512-jtmJMyt1xMD/d8OtbVJ2gFZOSKc+ueYJZPW20ULW1GOp/q/YIM0wNh+u8ZFao9UaIGz4WoPW8hC64qlWLIfoDA==", "dependencies": { - "tslib": "^2.5.0" + "tslib": "^2.6.2" }, "engines": { "node": ">=14.0.0" } }, "node_modules/@smithy/util-utf8": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.1.1.tgz", - "integrity": "sha512-BqTpzYEcUMDwAKr7/mVRUtHDhs6ZoXDi9NypMvMfOr/+u1NW7JgqodPDECiiLboEm6bobcPcECxzjtQh865e9A==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.3.0.tgz", + "integrity": "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==", "dependencies": { - "@smithy/util-buffer-from": "^2.1.1", - "tslib": "^2.5.0" + "@smithy/util-buffer-from": "^2.2.0", + "tslib": "^2.6.2" }, "engines": { "node": ">=14.0.0" diff --git a/backend/src/@types/fastify.d.ts b/backend/src/@types/fastify.d.ts index 71a7c887c..da966218f 100644 --- a/backend/src/@types/fastify.d.ts +++ b/backend/src/@types/fastify.d.ts @@ -32,6 +32,7 @@ import { TAuthTokenServiceFactory } from "@app/services/auth-token/auth-token-se import { TGroupProjectServiceFactory } from "@app/services/group-project/group-project-service"; import { TIdentityServiceFactory } from "@app/services/identity/identity-service"; import { TIdentityAccessTokenServiceFactory } from "@app/services/identity-access-token/identity-access-token-service"; +import { TIdentityAwsIamAuthServiceFactory } from "@app/services/identity-aws-iam-auth/identity-aws-iam-auth-service"; import { TIdentityProjectServiceFactory } from "@app/services/identity-project/identity-project-service"; import { TIdentityUaServiceFactory } from "@app/services/identity-ua/identity-ua-service"; import { TIntegrationServiceFactory } from "@app/services/integration/integration-service"; @@ -115,6 +116,7 @@ declare module "fastify" { identityAccessToken: TIdentityAccessTokenServiceFactory; identityProject: TIdentityProjectServiceFactory; identityUa: TIdentityUaServiceFactory; + identityAwsIamAuth: TIdentityAwsIamAuthServiceFactory; accessApprovalPolicy: TAccessApprovalPolicyServiceFactory; accessApprovalRequest: TAccessApprovalRequestServiceFactory; secretApprovalPolicy: TSecretApprovalPolicyServiceFactory; diff --git a/backend/src/@types/knex.d.ts b/backend/src/@types/knex.d.ts index 14ebd4295..b597bd250 100644 --- a/backend/src/@types/knex.d.ts +++ b/backend/src/@types/knex.d.ts @@ -59,6 +59,9 @@ import { TIdentityAccessTokens, TIdentityAccessTokensInsert, TIdentityAccessTokensUpdate, + TIdentityAwsIamAuths, + TIdentityAwsIamAuthsInsert, + TIdentityAwsIamAuthsUpdate, TIdentityOrgMemberships, TIdentityOrgMembershipsInsert, TIdentityOrgMembershipsUpdate, @@ -326,6 +329,11 @@ declare module "knex/types/tables" { TIdentityUniversalAuthsInsert, TIdentityUniversalAuthsUpdate >; + [TableName.IdentityAwsIamAuth]: Knex.CompositeTableType< + TIdentityAwsIamAuths, + TIdentityAwsIamAuthsInsert, + TIdentityAwsIamAuthsUpdate + >; [TableName.IdentityUaClientSecret]: Knex.CompositeTableType< TIdentityUaClientSecrets, TIdentityUaClientSecretsInsert, diff --git a/backend/src/db/migrations/20240507055915_identity-aws-iam-auth.ts b/backend/src/db/migrations/20240507055915_identity-aws-iam-auth.ts new file mode 100644 index 000000000..0728d4f28 --- /dev/null +++ b/backend/src/db/migrations/20240507055915_identity-aws-iam-auth.ts @@ -0,0 +1,29 @@ +import { Knex } from "knex"; + +import { TableName } from "../schemas"; +import { createOnUpdateTrigger, dropOnUpdateTrigger } from "../utils"; + +export async function up(knex: Knex): Promise { + if (!(await knex.schema.hasTable(TableName.IdentityAwsIamAuth))) { + await knex.schema.createTable(TableName.IdentityAwsIamAuth, (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("stsEndpoint").notNullable(); + t.string("allowedPrincipalArns").notNullable(); + t.string("allowedAccountIds").notNullable(); + }); + } + + await createOnUpdateTrigger(knex, TableName.IdentityAwsIamAuth); +} + +export async function down(knex: Knex): Promise { + await knex.schema.dropTableIfExists(TableName.IdentityAwsIamAuth); + await dropOnUpdateTrigger(knex, TableName.IdentityAwsIamAuth); +} diff --git a/backend/src/db/schemas/identity-aws-iam-auths.ts b/backend/src/db/schemas/identity-aws-iam-auths.ts new file mode 100644 index 000000000..8912c71b3 --- /dev/null +++ b/backend/src/db/schemas/identity-aws-iam-auths.ts @@ -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 IdentityAwsIamAuthsSchema = 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(), + stsEndpoint: z.string(), + allowedPrincipalArns: z.string(), + allowedAccountIds: z.string() +}); + +export type TIdentityAwsIamAuths = z.infer; +export type TIdentityAwsIamAuthsInsert = Omit, TImmutableDBKeys>; +export type TIdentityAwsIamAuthsUpdate = Partial, TImmutableDBKeys>>; diff --git a/backend/src/db/schemas/index.ts b/backend/src/db/schemas/index.ts index 0fd7c5271..e5c9a9ceb 100644 --- a/backend/src/db/schemas/index.ts +++ b/backend/src/db/schemas/index.ts @@ -17,6 +17,7 @@ export * from "./group-project-memberships"; export * from "./groups"; export * from "./identities"; export * from "./identity-access-tokens"; +export * from "./identity-aws-iam-auths"; export * from "./identity-org-memberships"; export * from "./identity-project-additional-privilege"; export * from "./identity-project-membership-role"; diff --git a/backend/src/db/schemas/models.ts b/backend/src/db/schemas/models.ts index 24c6b8914..4c080535d 100644 --- a/backend/src/db/schemas/models.ts +++ b/backend/src/db/schemas/models.ts @@ -45,6 +45,7 @@ export enum TableName { IdentityAccessToken = "identity_access_tokens", IdentityUniversalAuth = "identity_universal_auths", IdentityUaClientSecret = "identity_ua_client_secrets", + IdentityAwsIamAuth = "identity_aws_iam_auths", IdentityOrgMembership = "identity_org_memberships", IdentityProjectMembership = "identity_project_memberships", IdentityProjectMembershipRole = "identity_project_membership_role", @@ -142,5 +143,6 @@ export enum ProjectUpgradeStatus { } export enum IdentityAuthMethod { - Univeral = "universal-auth" + Univeral = "universal-auth", + AWS_IAM_AUTH = "aws-iam-auth" } diff --git a/backend/src/ee/services/audit-log/audit-log-types.ts b/backend/src/ee/services/audit-log/audit-log-types.ts index 220c25002..1e79984e4 100644 --- a/backend/src/ee/services/audit-log/audit-log-types.ts +++ b/backend/src/ee/services/audit-log/audit-log-types.ts @@ -66,6 +66,10 @@ export enum EventType { CREATE_IDENTITY_UNIVERSAL_AUTH_CLIENT_SECRET = "create-identity-universal-auth-client-secret", REVOKE_IDENTITY_UNIVERSAL_AUTH_CLIENT_SECRET = "revoke-identity-universal-auth-client-secret", GET_IDENTITY_UNIVERSAL_AUTH_CLIENT_SECRETS = "get-identity-universal-auth-client-secret", + LOGIN_IDENTITY_AWS_IAM_AUTH = "login-identity-aws-iam-auth", + ADD_IDENTITY_AWS_IAM_AUTH = "add-identity-aws-iam-auth", + UPDATE_IDENTITY_AWS_IAM_AUTH = "update-identity-aws-iam-auth", + GET_IDENTITY_AWS_IAM_AUTH = "get-identity-aws-iam-auth", CREATE_ENVIRONMENT = "create-environment", UPDATE_ENVIRONMENT = "update-environment", DELETE_ENVIRONMENT = "delete-environment", @@ -406,6 +410,50 @@ interface RevokeIdentityUniversalAuthClientSecretEvent { }; } +interface LoginIdentityAwsIamAuthEvent { + type: EventType.LOGIN_IDENTITY_AWS_IAM_AUTH; + metadata: { + identityId: string; + identityAwsIamAuthId: string; + identityAccessTokenId: string; + }; +} + +interface AddIdentityAwsIamAuthEvent { + type: EventType.ADD_IDENTITY_AWS_IAM_AUTH; + metadata: { + identityId: string; + stsEndpoint: string; + allowedPrincipalArns: string; + allowedAccountIds: string; + accessTokenTTL: number; + accessTokenMaxTTL: number; + accessTokenNumUsesLimit: number; + accessTokenTrustedIps: Array; + }; +} + +interface UpdateIdentityAwsIamAuthEvent { + type: EventType.UPDATE_IDENTITY_AWS_IAM_AUTH; + metadata: { + identityId: string; + stsEndpoint?: string; + allowedPrincipalArns?: string; + allowedAccountIds?: string; + accessTokenTTL?: number; + accessTokenMaxTTL?: number; + accessTokenNumUsesLimit?: number; + accessTokenTrustedIps?: Array; + }; +} + +interface GetIdentityAwsIamAuthEvent { + type: EventType.GET_IDENTITY_AWS_IAM_AUTH; + metadata: { + identityId: string; + }; +} + interface CreateEnvironmentEvent { type: EventType.CREATE_ENVIRONMENT; metadata: { @@ -660,6 +708,10 @@ export type Event = | CreateIdentityUniversalAuthClientSecretEvent | GetIdentityUniversalAuthClientSecretsEvent | RevokeIdentityUniversalAuthClientSecretEvent + | LoginIdentityAwsIamAuthEvent + | AddIdentityAwsIamAuthEvent + | UpdateIdentityAwsIamAuthEvent + | GetIdentityAwsIamAuthEvent | CreateEnvironmentEvent | UpdateEnvironmentEvent | DeleteEnvironmentEvent diff --git a/backend/src/lib/api-docs/constants.ts b/backend/src/lib/api-docs/constants.ts index 61df46d92..336385472 100644 --- a/backend/src/lib/api-docs/constants.ts +++ b/backend/src/lib/api-docs/constants.ts @@ -92,6 +92,18 @@ export const UNIVERSAL_AUTH = { } } as const; +export const AWS_IAM_AUTH = { + LOGIN: { + identityId: "The ID of the identity to login.", + iamHttpRequestMethod: "The HTTP request method used in the signed request.", + iamRequestUrl: + "The base64-encoded HTTP URL used in the signed request. Most likely, the base64-encoding of https://sts.amazonaws.com/", + iamRequestBody: + "The base64-encoded body of the signed request. Most likely, the base64-encoding of Action=GetCallerIdentity&Version=2011-06-15.", + iamRequestHeaders: "The base64-encoded headers of the sts:GetCallerIdentity signed request." + } +} as const; + export const ORGANIZATIONS = { LIST_USER_MEMBERSHIPS: { organizationId: "The ID of the organization to get memberships from." diff --git a/backend/src/server/routes/index.ts b/backend/src/server/routes/index.ts index 269a97c49..62dad3cf4 100644 --- a/backend/src/server/routes/index.ts +++ b/backend/src/server/routes/index.ts @@ -78,6 +78,8 @@ import { identityOrgDALFactory } from "@app/services/identity/identity-org-dal"; import { identityServiceFactory } from "@app/services/identity/identity-service"; import { identityAccessTokenDALFactory } from "@app/services/identity-access-token/identity-access-token-dal"; import { identityAccessTokenServiceFactory } from "@app/services/identity-access-token/identity-access-token-service"; +import { identityAwsIamAuthDALFactory } from "@app/services/identity-aws-iam-auth/identity-aws-iam-auth-dal"; +import { identityAwsIamAuthServiceFactory } from "@app/services/identity-aws-iam-auth/identity-aws-iam-auth-service"; import { identityProjectDALFactory } from "@app/services/identity-project/identity-project-dal"; import { identityProjectMembershipRoleDALFactory } from "@app/services/identity-project/identity-project-membership-role-dal"; import { identityProjectServiceFactory } from "@app/services/identity-project/identity-project-service"; @@ -201,6 +203,7 @@ export const registerRoutes = async ( const identityUaDAL = identityUaDALFactory(db); const identityUaClientSecretDAL = identityUaClientSecretDALFactory(db); + const identityAwsIamAuthDAL = identityAwsIamAuthDALFactory(db); const auditLogDAL = auditLogDALFactory(db); const auditLogStreamDAL = auditLogStreamDALFactory(db); @@ -699,6 +702,14 @@ export const registerRoutes = async ( identityUaDAL, licenseService }); + const identityAWSIAMAuthService = identityAwsIamAuthServiceFactory({ + identityAccessTokenDAL, + identityAwsIamAuthDAL, + identityOrgMembershipDAL, + identityDAL, + licenseService, + permissionService + }); const dynamicSecretProviders = buildDynamicSecretProviders(); const dynamicSecretQueueService = dynamicSecretLeaseQueueServiceFactory({ @@ -768,6 +779,7 @@ export const registerRoutes = async ( identityAccessToken: identityAccessTokenService, identityProject: identityProjectService, identityUa: identityUaService, + identityAwsIamAuth: identityAWSIAMAuthService, secretApprovalPolicy: sapService, accessApprovalPolicy: accessApprovalPolicyService, accessApprovalRequest: accessApprovalRequestService, diff --git a/backend/src/server/routes/v1/identity-aws-iam-auth-router.ts b/backend/src/server/routes/v1/identity-aws-iam-auth-router.ts new file mode 100644 index 000000000..12003a158 --- /dev/null +++ b/backend/src/server/routes/v1/identity-aws-iam-auth-router.ts @@ -0,0 +1,269 @@ +import { z } from "zod"; + +import { IdentityAwsIamAuthsSchema } from "@app/db/schemas"; +import { EventType } from "@app/ee/services/audit-log/audit-log-types"; +import { AWS_IAM_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-iam-auth/identity-aws-iam-auth-validators"; + +export const registerIdentityAwsIamAuthRouter = async (server: FastifyZodProvider) => { + server.route({ + method: "POST", + url: "/aws-iam-auth/login", + config: { + rateLimit: writeLimit + }, + schema: { + description: "Login with AWS IAM Auth", + body: z.object({ + identityId: z.string().describe(AWS_IAM_AUTH.LOGIN.identityId), + iamHttpRequestMethod: z.string().default("POST").describe(AWS_IAM_AUTH.LOGIN.iamHttpRequestMethod), + iamRequestBody: z.string().describe(AWS_IAM_AUTH.LOGIN.iamRequestBody), + iamRequestHeaders: z.string().describe(AWS_IAM_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 { identityAwsIamAuth, accessToken, identityAccessToken, identityMembershipOrg } = + await server.services.identityAwsIamAuth.login(req.body); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + orgId: identityMembershipOrg?.orgId, + event: { + type: EventType.LOGIN_IDENTITY_AWS_IAM_AUTH, + metadata: { + identityId: identityAwsIamAuth.identityId, + identityAccessTokenId: identityAccessToken.id, + identityAwsIamAuthId: identityAwsIamAuth.id + } + } + }); + + return { + accessToken, + tokenType: "Bearer" as const, + expiresIn: identityAwsIamAuth.accessTokenTTL, + accessTokenMaxTTL: identityAwsIamAuth.accessTokenMaxTTL + }; + } + }); + + server.route({ + method: "POST", + url: "/aws-iam-auth/identities/:identityId", + config: { + rateLimit: writeLimit + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + schema: { + description: "Attach AWS IAM 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({ + identityAwsIamAuth: IdentityAwsIamAuthsSchema + }) + } + }, + handler: async (req) => { + const identityAwsIamAuth = await server.services.identityAwsIamAuth.attachAwsIamAuth({ + 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: identityAwsIamAuth.orgId, + event: { + type: EventType.ADD_IDENTITY_AWS_IAM_AUTH, + metadata: { + identityId: identityAwsIamAuth.identityId, + stsEndpoint: identityAwsIamAuth.stsEndpoint, + allowedPrincipalArns: identityAwsIamAuth.allowedPrincipalArns, + allowedAccountIds: identityAwsIamAuth.allowedAccountIds, + accessTokenTTL: identityAwsIamAuth.accessTokenTTL, + accessTokenMaxTTL: identityAwsIamAuth.accessTokenMaxTTL, + accessTokenTrustedIps: identityAwsIamAuth.accessTokenTrustedIps as TIdentityTrustedIp[], + accessTokenNumUsesLimit: identityAwsIamAuth.accessTokenNumUsesLimit + } + } + }); + + return { identityAwsIamAuth }; + } + }); + + server.route({ + method: "PATCH", + url: "/aws-iam-auth/identities/:identityId", + config: { + rateLimit: writeLimit + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + schema: { + description: "Update AWS IAM 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({ + identityAwsIamAuth: IdentityAwsIamAuthsSchema + }) + } + }, + handler: async (req) => { + const identityAwsIamAuth = await server.services.identityAwsIamAuth.updateAwsIamAuth({ + 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: identityAwsIamAuth.orgId, + event: { + type: EventType.UPDATE_IDENTITY_AWS_IAM_AUTH, + metadata: { + identityId: identityAwsIamAuth.identityId, + stsEndpoint: identityAwsIamAuth.stsEndpoint, + allowedPrincipalArns: identityAwsIamAuth.allowedPrincipalArns, + allowedAccountIds: identityAwsIamAuth.allowedAccountIds, + accessTokenTTL: identityAwsIamAuth.accessTokenTTL, + accessTokenMaxTTL: identityAwsIamAuth.accessTokenMaxTTL, + accessTokenTrustedIps: identityAwsIamAuth.accessTokenTrustedIps as TIdentityTrustedIp[], + accessTokenNumUsesLimit: identityAwsIamAuth.accessTokenNumUsesLimit + } + } + }); + + return { identityAwsIamAuth }; + } + }); + + server.route({ + method: "GET", + url: "/aws-iam-auth/identities/:identityId", + config: { + rateLimit: readLimit + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + schema: { + description: "Retrieve AWS IAM Auth configuration on identity", + security: [ + { + bearerAuth: [] + } + ], + params: z.object({ + identityId: z.string() + }), + response: { + 200: z.object({ + identityAwsIamAuth: IdentityAwsIamAuthsSchema + }) + } + }, + handler: async (req) => { + const identityAwsIamAuth = await server.services.identityAwsIamAuth.getAwsIamAuth({ + 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: identityAwsIamAuth.orgId, + event: { + type: EventType.GET_IDENTITY_AWS_IAM_AUTH, + metadata: { + identityId: identityAwsIamAuth.identityId + } + } + }); + return { identityAwsIamAuth }; + } + }); +}; diff --git a/backend/src/server/routes/v1/index.ts b/backend/src/server/routes/v1/index.ts index fbc68d974..4fbaabdfc 100644 --- a/backend/src/server/routes/v1/index.ts +++ b/backend/src/server/routes/v1/index.ts @@ -2,6 +2,7 @@ import { registerAdminRouter } from "./admin-router"; import { registerAuthRoutes } from "./auth-router"; import { registerProjectBotRouter } from "./bot-router"; import { registerIdentityAccessTokenRouter } from "./identity-access-token-router"; +import { registerIdentityAwsIamAuthRouter } from "./identity-aws-iam-auth-router"; import { registerIdentityRouter } from "./identity-router"; import { registerIdentityUaRouter } from "./identity-ua"; import { registerIntegrationAuthRouter } from "./integration-auth-router"; @@ -28,6 +29,7 @@ export const registerV1Routes = async (server: FastifyZodProvider) => { await authRouter.register(registerAuthRoutes); await authRouter.register(registerIdentityUaRouter); await authRouter.register(registerIdentityAccessTokenRouter); + await authRouter.register(registerIdentityAwsIamAuthRouter); }, { prefix: "/auth" } ); diff --git a/backend/src/services/identity-aws-iam-auth/identity-aws-iam-auth-dal.ts b/backend/src/services/identity-aws-iam-auth/identity-aws-iam-auth-dal.ts new file mode 100644 index 000000000..584ac775f --- /dev/null +++ b/backend/src/services/identity-aws-iam-auth/identity-aws-iam-auth-dal.ts @@ -0,0 +1,11 @@ +import { TDbClient } from "@app/db"; +import { TableName } from "@app/db/schemas"; +import { ormify } from "@app/lib/knex"; + +export type TIdentityAwsIamAuthDALFactory = ReturnType; + +export const identityAwsIamAuthDALFactory = (db: TDbClient) => { + const awsIamAuthOrm = ormify(db, TableName.IdentityAwsIamAuth); + + return awsIamAuthOrm; +}; diff --git a/backend/src/services/identity-aws-iam-auth/identity-aws-iam-auth-fns.ts b/backend/src/services/identity-aws-iam-auth/identity-aws-iam-auth-fns.ts new file mode 100644 index 000000000..517e9f613 --- /dev/null +++ b/backend/src/services/identity-aws-iam-auth/identity-aws-iam-auth-fns.ts @@ -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}`; +}; diff --git a/backend/src/services/identity-aws-iam-auth/identity-aws-iam-auth-service.ts b/backend/src/services/identity-aws-iam-auth/identity-aws-iam-auth-service.ts new file mode 100644 index 000000000..95deba2d8 --- /dev/null +++ b/backend/src/services/identity-aws-iam-auth/identity-aws-iam-auth-service.ts @@ -0,0 +1,315 @@ +/* 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 { TIdentityAwsIamAuthDALFactory } from "./identity-aws-iam-auth-dal"; +import { extractPrincipalArn } from "./identity-aws-iam-auth-fns"; +import { + TAttachAWSIAMAuthDTO, + TAWSGetCallerIdentityHeaders, + TGetAWSIAMAuthDTO, + TGetCallerIdentityResponse, + TLoginAWSIAMAuthDTO, + TUpdateAWSIAMAuthDTO +} from "./identity-aws-iam-auth-types"; + +type TIdentityAwsIamAuthServiceFactoryDep = { + identityAccessTokenDAL: Pick; + identityAwsIamAuthDAL: Pick; + identityOrgMembershipDAL: Pick; + identityDAL: Pick; + licenseService: Pick; + permissionService: Pick; +}; + +export type TIdentityAwsIamAuthServiceFactory = ReturnType; + +export const identityAwsIamAuthServiceFactory = ({ + identityAccessTokenDAL, + identityAwsIamAuthDAL, + identityOrgMembershipDAL, + identityDAL, + licenseService, + permissionService +}: TIdentityAwsIamAuthServiceFactoryDep) => { + const login = async ({ + identityId, + iamHttpRequestMethod, + iamRequestBody, + iamRequestHeaders + }: TLoginAWSIAMAuthDTO) => { + const identityAwsIamAuth = await identityAwsIamAuthDAL.findOne({ identityId }); + if (!identityAwsIamAuth) throw new UnauthorizedError(); + + const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId: identityAwsIamAuth.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: identityAwsIamAuth.stsEndpoint, + headers, + data: body + }); + + if (identityAwsIamAuth.allowedAccountIds) { + // validate if Account is in the list of allowed Account IDs + + const isAccountAllowed = identityAwsIamAuth.allowedAccountIds + .split(",") + .map((accountId) => accountId.trim()) + .some((accountId) => accountId === Account); + + if (!isAccountAllowed) throw new UnauthorizedError(); + } + + if (identityAwsIamAuth.allowedPrincipalArns) { + // validate if Arn is in the list of allowed Principal ARNs + + const isArnAllowed = identityAwsIamAuth.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 identityAwsIamAuthDAL.transaction(async (tx) => { + const newToken = await identityAccessTokenDAL.create( + { + identityId: identityAwsIamAuth.identityId, + isAccessTokenRevoked: false, + accessTokenTTL: identityAwsIamAuth.accessTokenTTL, + accessTokenMaxTTL: identityAwsIamAuth.accessTokenMaxTTL, + accessTokenNumUses: 0, + accessTokenNumUsesLimit: identityAwsIamAuth.accessTokenNumUsesLimit + }, + tx + ); + return newToken; + }); + + const appCfg = getConfig(); + const accessToken = jwt.sign( + { + identityId: identityAwsIamAuth.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, identityAwsIamAuth, identityAccessToken, identityMembershipOrg }; + }; + + const attachAwsIamAuth = async ({ + identityId, + stsEndpoint, + allowedPrincipalArns, + allowedAccountIds, + accessTokenTTL, + accessTokenMaxTTL, + accessTokenNumUsesLimit, + accessTokenTrustedIps, + actorId, + actorAuthMethod, + actor, + actorOrgId + }: TAttachAWSIAMAuthDTO) => { + 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 IAM 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 identityAwsIamAuth = await identityAwsIamAuthDAL.transaction(async (tx) => { + const doc = await identityAwsIamAuthDAL.create( + { + identityId: identityMembershipOrg.identityId, + stsEndpoint, + allowedPrincipalArns, + allowedAccountIds, + accessTokenMaxTTL, + accessTokenTTL, + accessTokenNumUsesLimit, + accessTokenTrustedIps: JSON.stringify(reformattedAccessTokenTrustedIps) + }, + tx + ); + await identityDAL.updateById( + identityMembershipOrg.identityId, + { + authMethod: IdentityAuthMethod.AWS_IAM_AUTH + }, + tx + ); + return doc; + }); + return { ...identityAwsIamAuth, orgId: identityMembershipOrg.orgId }; + }; + + const updateAwsIamAuth = async ({ + identityId, + stsEndpoint, + allowedPrincipalArns, + allowedAccountIds, + accessTokenTTL, + accessTokenMaxTTL, + accessTokenNumUsesLimit, + accessTokenTrustedIps, + actorId, + actorAuthMethod, + actor, + actorOrgId + }: TUpdateAWSIAMAuthDTO) => { + const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId }); + if (!identityMembershipOrg) throw new BadRequestError({ message: "Failed to find identity" }); + if (identityMembershipOrg.identity?.authMethod !== IdentityAuthMethod.AWS_IAM_AUTH) + throw new BadRequestError({ + message: "Failed to update AWS IAM Auth" + }); + + const identityAwsIamAuth = await identityAwsIamAuthDAL.findOne({ identityId }); + + if ( + (accessTokenMaxTTL || identityAwsIamAuth.accessTokenMaxTTL) > 0 && + (accessTokenTTL || identityAwsIamAuth.accessTokenMaxTTL) > + (accessTokenMaxTTL || identityAwsIamAuth.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 updatedAwsIamAuth = await identityAwsIamAuthDAL.updateById(identityAwsIamAuth.id, { + stsEndpoint, + allowedPrincipalArns, + allowedAccountIds, + accessTokenMaxTTL, + accessTokenTTL, + accessTokenNumUsesLimit, + accessTokenTrustedIps: reformattedAccessTokenTrustedIps + ? JSON.stringify(reformattedAccessTokenTrustedIps) + : undefined + }); + + return { ...updatedAwsIamAuth, orgId: identityMembershipOrg.orgId }; + }; + + const getAwsIamAuth = async ({ identityId, actorId, actor, actorAuthMethod, actorOrgId }: TGetAWSIAMAuthDTO) => { + const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId }); + if (!identityMembershipOrg) throw new BadRequestError({ message: "Failed to find identity" }); + if (identityMembershipOrg.identity?.authMethod !== IdentityAuthMethod.AWS_IAM_AUTH) + throw new BadRequestError({ + message: "The identity does not have AWS IAM Auth attached" + }); + + const awsIamIdentityAuth = await identityAwsIamAuthDAL.findOne({ identityId }); + + const { permission } = await permissionService.getOrgPermission( + actor, + actorId, + identityMembershipOrg.orgId, + actorAuthMethod, + actorOrgId + ); + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Identity); + return { ...awsIamIdentityAuth, orgId: identityMembershipOrg.orgId }; + }; + + return { + login, + attachAwsIamAuth, + updateAwsIamAuth, + getAwsIamAuth + }; +}; diff --git a/backend/src/services/identity-aws-iam-auth/identity-aws-iam-auth-types.ts b/backend/src/services/identity-aws-iam-auth/identity-aws-iam-auth-types.ts new file mode 100644 index 000000000..19f27f430 --- /dev/null +++ b/backend/src/services/identity-aws-iam-auth/identity-aws-iam-auth-types.ts @@ -0,0 +1,54 @@ +import { TProjectPermission } from "@app/lib/types"; + +export type TLoginAWSIAMAuthDTO = { + identityId: string; + iamHttpRequestMethod: string; + iamRequestBody: string; + iamRequestHeaders: string; +}; + +export type TAttachAWSIAMAuthDTO = { + identityId: string; + stsEndpoint: string; + allowedPrincipalArns: string; + allowedAccountIds: string; + accessTokenTTL: number; + accessTokenMaxTTL: number; + accessTokenNumUsesLimit: number; + accessTokenTrustedIps: { ipAddress: string }[]; +} & Omit; + +export type TUpdateAWSIAMAuthDTO = { + identityId: string; + stsEndpoint?: string; + allowedPrincipalArns?: string; + allowedAccountIds?: string; + accessTokenTTL?: number; + accessTokenMaxTTL?: number; + accessTokenNumUsesLimit?: number; + accessTokenTrustedIps?: { ipAddress: string }[]; +} & Omit; + +export type TGetAWSIAMAuthDTO = { + identityId: string; +} & Omit; + +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 }; + }; +}; diff --git a/backend/src/services/identity-aws-iam-auth/identity-aws-iam-auth-validators.ts b/backend/src/services/identity-aws-iam-auth/identity-aws-iam-auth-validators.ts new file mode 100644 index 000000000..2cb7b4ea4 --- /dev/null +++ b/backend/src/services/identity-aws-iam-auth/identity-aws-iam-auth-validators.ts @@ -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(", ") + ); diff --git a/docs/documentation/getting-started/introduction.mdx b/docs/documentation/getting-started/introduction.mdx index 0f414c62a..144691ac4 100644 --- a/docs/documentation/getting-started/introduction.mdx +++ b/docs/documentation/getting-started/introduction.mdx @@ -4,59 +4,66 @@ sidebarTitle: "What is Infisical?" description: "An Introduction to the Infisical secret management platform." --- -Infisical is an [open-source](https://github.com/infisical/infisical) secret management platform for developers. -It provides capabilities for storing, managing, and syncing application configuration and secrets like API keys, database -credentials, and certificates across infrastructure. In addition, Infisical prevents secrets leaks to git and enables secure +Infisical is an [open-source](https://github.com/infisical/infisical) secret management platform for developers. +It provides capabilities for storing, managing, and syncing application configuration and secrets like API keys, database +credentials, and certificates across infrastructure. In addition, Infisical prevents secrets leaks to git and enables secure sharing of secrets among engineers. Start managing secrets securely with [Infisical Cloud](https://app.infisical.com) or learn how to [host Infisical](/self-hosting/overview) yourself. - - Get started with Infisical Cloud in just a few minutes. - - - Self-host Infisical on your own infrastructure. - + + Get started with Infisical Cloud in just a few minutes. + + + Self-host Infisical on your own infrastructure. + -## Why Infisical? +## Why Infisical? + +Infisical helps developers achieve secure centralized secret management and provides all the tools to easily manage secrets in various environments and infrastructure components. In particular, here are some of the most common points that developers mention after adopting Infisical: -Infisical helps developers achieve secure centralized secret management and provides all the tools to easily manage secrets in various environments and infrastructure components. In particular, here are some of the most common points that developers mention after adopting Infisical: - Streamlined **local development** processes (switching .env files to [Infisical CLI](/cli/commands/run) and removing secrets from developer machines). -- **Best-in-class developer experience** with an easy-to-use [Web Dashboard](/documentation/platform/project). -- Simple secret management inside **[CI/CD pipelines](/integrations/cicd/githubactions)** and staging environments. -- Secure and compliant secret management practices in **[production environments](/sdks/overview)**. +- **Best-in-class developer experience** with an easy-to-use [Web Dashboard](/documentation/platform/project). +- Simple secret management inside **[CI/CD pipelines](/integrations/cicd/githubactions)** and staging environments. +- Secure and compliant secret management practices in **[production environments](/sdks/overview)**. - **Facilitated workflows** around [secret change management](/documentation/platform/pr-workflows), [access requests](/documentation/platform/access-controls/access-requests), [temporary access provisioning](/documentation/platform/access-controls/temporary-access), and more. - **Improved security posture** thanks to [secret scanning](/cli/scanning-overview), [granular access control policies](/documentation/platform/access-controls/overview), [automated secret rotation](https://infisical.com/docs/documentation/platform/secret-rotation/overview), and [dynamic secrets](/documentation/platform/dynamic-secrets/overview) capabilities. -## How does Infisical work? +## How does Infisical work? -To make secret management effortless and secure, Infisical follows a certain structure for enabling secret management workflows as defined below. +To make secret management effortless and secure, Infisical follows a certain structure for enabling secret management workflows as defined below. -**Identities** in Infisical are users or machine which have a certain set of roles and permissions assigned to them. Such identities are able to manage secrets in various **Clients** throughout the entire infrastructure. To do that, identities have to verify themselves through one of the available **Authentication Methods**. +**Identities** in Infisical are users or machine which have a certain set of roles and permissions assigned to them. Such identities are able to manage secrets in various **Clients** throughout the entire infrastructure. To do that, identities have to verify themselves through one of the available **Authentication Methods**. -As a result, the 3 main concepts that are important to understand are: -- **[Identities](/documentation/platform/identities/overview)**: users or machines with a set permissions assigned to them. +As a result, the 3 main concepts that are important to understand are: + +- **[Identities](/documentation/platform/identities/overview)**: users or machines with a set permissions assigned to them. - **[Clients](/integrations/platforms/kubernetes)**: Infisical-developed tools for managing secrets in various infrastructure components (e.g., [Kubernetes Operator](/integrations/platforms/kubernetes), [Infisical Agent](/integrations/platforms/infisical-agent), [CLI](/cli/usage), [SDKs](/sdks/overview), [API](/api-reference/overview/introduction), [Web Dashboard](/documentation/platform/organization)). -- **[Authentication Methods](/documentation/platform/identities/universal-auth)**: ways for Identities to authenticate inside different clients (e.g., SAML SSO for Web Dashboard, Universal Auth for Infisical Agent, etc.). +- **[Authentication Methods](/documentation/platform/identities/universal-auth)**: ways for Identities to authenticate inside different clients (e.g., SAML SSO for Web Dashboard, Universal Auth for Infisical Agent, AWS IAM 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. - + Inject secrets into any application process/environment. Fetch secrets with any programming language on demand. - + Inject secrets into Docker containers. +We recommend using one of Infisical's clients like SDKs or the Infisical Agent +to authenticate with Infisical using AWS IAM 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 IAM Auth workflow. + + + +## 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 IAM authentication method. + + + + 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 IAM Auth**. + + ![identities create iam auth method](/images/platform/identities/identities-org-create-aws-iam-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. + + + 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) + + + 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-iam-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). + + + + 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 = ""; + + const { data } = await axios.post( + `${infisicalUrl}/api/v1/auth/aws-iam-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); + } + }; + ```` + + + 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 = ""; + + const { data } = await axios.post(`${infisicalUrl}/api/v1/auth/aws-iam-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(); + ```` + + + 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 = ""; + 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-iam-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) + + + + + We recommend using one of Infisical's clients like SDKs or the Infisical Agent to authenticate with Infisical using AWS IAM Auth as they handle the authentication process including the signed `GetCallerIdentity` query construction for you. + + + + 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. + + + + diff --git a/docs/documentation/platform/identities/machine-identities.mdx b/docs/documentation/platform/identities/machine-identities.mdx index daa5db7ed..9168cb230 100644 --- a/docs/documentation/platform/identities/machine-identities.mdx +++ b/docs/documentation/platform/identities/machine-identities.mdx @@ -7,7 +7,7 @@ description: "Learn how to use Machine Identities to programmatically interact w An Infisical machine identity is an entity that represents a workload or application that require access to various resources in Infisical. This is conceptually similar to an IAM user in AWS or service account in Google Cloud Platform (GCP). -Each identity must authenticate with the API using a supported authentication method like [Universal Auth](/documentation/platform/identities/universal-auth) to get back a short-lived access token to be used in subsequent requests. +Each identity must authenticate using a supported authentication method like [Universal Auth](/documentation/platform/identities/universal-auth) or [AWS IAM Auth](/documentation/platform/identities/aws-iam-auth) to get back a short-lived access token to be used in subsequent requests. ![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: 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 IAM Auth](/documentation/platform/identities/aws-iam-auth). 2. Adding the identity to the project(s) you want it to have access to. 3. Authenticating the identity with the Infisical API based on the configured authentication method on it and receiving a short-lived access token back. 4. Authenticating subsequent requests with the Infisical API using the short-lived access token. @@ -37,7 +37,8 @@ Machine Identity support for the rest of the clients is planned to be released i To interact with various resources in Infisical, Machine Identities are able to authenticate using: -- [Universal Auth](/documentation/platform/identities/universal-auth): the most versatile authentication method that can be configured on an identity from any platform/environment to access Infisical. +- [Universal Auth](/documentation/platform/identities/universal-auth): A platform-agnostic authentication method that can be configured on an identity suitable to authenticate from any platform/environment. +- [AWS IAM Auth](/documentation/platform/identities/aws-iam-auth): An AWS-native authentication method for IAM principals like EC2 instances or Lambda functions to authenticate with Infisical. ## FAQ diff --git a/docs/documentation/platform/identities/universal-auth.mdx b/docs/documentation/platform/identities/universal-auth.mdx index a9f4dffae..cd40a9e64 100644 --- a/docs/documentation/platform/identities/universal-auth.mdx +++ b/docs/documentation/platform/identities/universal-auth.mdx @@ -3,17 +3,14 @@ title: Universal Auth description: "Learn how to authenticate to Infisical from any platform or environment." --- -**Universal Auth** is the most versatile authentication method that can be configured for a [machine identity](/documentation/platform/identities/machine-identities) to access Infisical from any platform or environment. +**Universal Auth** is a platform-agnostic authentication method that can be configured for a [machine identity](/documentation/platform/identities/machine-identities) suitable to authenticate from any platform/environment. -In this method, each identity is given a **Client ID** for which you can generate one or more **Client Secret(s)**. Together, a **Client ID** and **Client Secret** can be exchanged for an access token to authenticate with the Infisical API. +## Concept -## Properties +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. -Universal Auth supports many settings that can be beneficial for tightening your workflow security configuration: - -- Support for restrictions on the number of times that the **Client Secret(s)** and access token(s) can be used. -- Support for expiration, so, if specified, the **Client Secret** of the identity will automatically be defunct after a period of time. -- Support for IP allowlisting; this means you can restrict the usage of **Client Secret(s)** and access token to a specific IP or CIDR range. +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. ## Workflow @@ -27,18 +24,18 @@ using the Universal Auth authentication method. ![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 **Universal Auth** authentication method for it. - + ![identities organization create auth method](/images/platform/identities/identities-org-create-auth-method.png) - + Here's some more guidance on each field: - Access Token TTL (default is `2592000` equivalent to 30 days): The lifetime for an acccess token in seconds. This value will be referenced at renewal time. @@ -78,8 +75,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. ![identities project](/images/platform/identities/identities-project.png) - + ![identities project create](/images/platform/identities/identities-project-create.png) + To access the Infisical API as the identity, you should first perform a login operation @@ -88,16 +86,16 @@ using the Universal Auth authentication method. #### Sample request - ``` + ```bash Request curl --location --request POST 'https://app.infisical.com/api/v1/auth/universal-auth/login' \ --header 'Content-Type: application/x-www-form-urlencoded' \ - --data-urlencode 'clientSecret=...' \ - --data-urlencode 'clientId=...' + --data-urlencode 'clientId=...' \ + --data-urlencode 'clientSecret=...' ``` - + #### Sample response - - ``` + + ```bash Response { "accessToken": "...", "expiresIn": 7200, @@ -107,7 +105,7 @@ using the Universal Auth authentication method. ``` Next, you can use the access token to authenticate with the [Infisical API](/api-reference/overview/introduction) - + Each identity access token has a time-to-live (TLL) which you can infer from the response of the login operation; the default TTL is `7200` seconds which can be adjusted. @@ -115,6 +113,7 @@ using the Universal Auth authentication method. If an identity access token expires, it can no longer authenticate with the Infisical API. In this case, a new access token should be obtained by performing another login operation. + @@ -134,7 +133,8 @@ using the Universal Auth authentication method. In certain cases, you may want to extend the lifespan of an access token; to do so, you must set a max TTL parameter. - A token can be renewed any number of time and each call to renew it will extend the toke life by increments of access token TTL. - Regardless of how frequently an access token is renewed, its lifespan remains bound to the maximum TTL determined at its creation +A token can be renewed any number of time and each call to renew it will extend the toke life by increments of access token TTL. +Regardless of how frequently an access token is renewed, its lifespan remains bound to the maximum TTL determined at its creation + - \ No newline at end of file + diff --git a/docs/images/platform/identities/identities-org-create-aws-iam-auth-method.png b/docs/images/platform/identities/identities-org-create-aws-iam-auth-method.png new file mode 100644 index 000000000..4b902c048 Binary files /dev/null and b/docs/images/platform/identities/identities-org-create-aws-iam-auth-method.png differ diff --git a/docs/mint.json b/docs/mint.json index 0bb1ae9bf..71fe02a1f 100644 --- a/docs/mint.json +++ b/docs/mint.json @@ -153,6 +153,7 @@ "documentation/platform/auth-methods/email-password", "documentation/platform/token", "documentation/platform/identities/universal-auth", + "documentation/platform/identities/aws-iam-auth", "documentation/platform/mfa", { "group": "SSO", @@ -211,9 +212,7 @@ }, { "group": "Reference architectures", - "pages": [ - "self-hosting/reference-architectures/aws-ecs" - ] + "pages": ["self-hosting/reference-architectures/aws-ecs"] }, "self-hosting/ee", "self-hosting/faq" diff --git a/frontend/src/hooks/api/identities/constants.tsx b/frontend/src/hooks/api/identities/constants.tsx index 53fceb7d5..5e8ae069f 100644 --- a/frontend/src/hooks/api/identities/constants.tsx +++ b/frontend/src/hooks/api/identities/constants.tsx @@ -1,5 +1,6 @@ import { IdentityAuthMethod } from "./enums"; export const identityAuthToNameMap: { [I in IdentityAuthMethod]: string } = { - [IdentityAuthMethod.UNIVERSAL_AUTH]: "Universal Auth" + [IdentityAuthMethod.UNIVERSAL_AUTH]: "Universal Auth", + [IdentityAuthMethod.AWS_IAM_AUTH]: "AWS IAM Auth" }; diff --git a/frontend/src/hooks/api/identities/enums.tsx b/frontend/src/hooks/api/identities/enums.tsx index 524c3a20c..614549da3 100644 --- a/frontend/src/hooks/api/identities/enums.tsx +++ b/frontend/src/hooks/api/identities/enums.tsx @@ -1,3 +1,4 @@ export enum IdentityAuthMethod { - UNIVERSAL_AUTH = "universal-auth" + UNIVERSAL_AUTH = "universal-auth", + AWS_IAM_AUTH = "aws-iam-auth" } diff --git a/frontend/src/hooks/api/identities/index.tsx b/frontend/src/hooks/api/identities/index.tsx index 684a00b6f..16fc56782 100644 --- a/frontend/src/hooks/api/identities/index.tsx +++ b/frontend/src/hooks/api/identities/index.tsx @@ -1,12 +1,16 @@ export { identityAuthToNameMap } from "./constants"; export { IdentityAuthMethod } from "./enums"; export { + useAddIdentityAwsIamAuth, useAddIdentityUniversalAuth, useCreateIdentity, useCreateIdentityUniversalAuthClientSecret, useDeleteIdentity, useRevokeIdentityUniversalAuthClientSecret, useUpdateIdentity, - useUpdateIdentityUniversalAuth -} from "./mutations"; -export { useGetIdentityUniversalAuth, useGetIdentityUniversalAuthClientSecrets } from "./queries"; + useUpdateIdentityAwsIamAuth, + useUpdateIdentityUniversalAuth} from "./mutations"; +export { + useGetIdentityAwsIamAuth, + useGetIdentityUniversalAuth, + useGetIdentityUniversalAuthClientSecrets} from "./queries"; diff --git a/frontend/src/hooks/api/identities/mutations.tsx b/frontend/src/hooks/api/identities/mutations.tsx index ec418659d..fa956af13 100644 --- a/frontend/src/hooks/api/identities/mutations.tsx +++ b/frontend/src/hooks/api/identities/mutations.tsx @@ -5,6 +5,7 @@ import { apiRequest } from "@app/config/request"; import { organizationKeys } from "../organization/queries"; import { identitiesKeys } from "./queries"; import { + AddIdentityAwsIamAuthDTO, AddIdentityUniversalAuthDTO, ClientSecretData, CreateIdentityDTO, @@ -13,10 +14,11 @@ import { DeleteIdentityDTO, DeleteIdentityUniversalAuthClientSecretDTO, Identity, + IdentityAwsIamAuth, IdentityUniversalAuth, + UpdateIdentityAwsIamAuthDTO, UpdateIdentityDTO, - UpdateIdentityUniversalAuthDTO -} from "./types"; + UpdateIdentityUniversalAuthDTO} from "./types"; export const useCreateIdentity = () => { const queryClient = useQueryClient(); @@ -169,3 +171,74 @@ export const useRevokeIdentityUniversalAuthClientSecret = () => { } }); }; + +export const useAddIdentityAwsIamAuth = () => { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: async ({ + identityId, + stsEndpoint, + allowedPrincipalArns, + allowedAccountIds, + accessTokenTTL, + accessTokenMaxTTL, + accessTokenNumUsesLimit, + accessTokenTrustedIps + }) => { + const { + data: { identityAwsIamAuth } + } = await apiRequest.post<{ identityAwsIamAuth: IdentityAwsIamAuth }>( + `/api/v1/auth/aws-iam-auth/identities/${identityId}`, + { + stsEndpoint, + allowedPrincipalArns, + allowedAccountIds, + accessTokenTTL, + accessTokenMaxTTL, + accessTokenNumUsesLimit, + accessTokenTrustedIps + } + ); + + return identityAwsIamAuth; + }, + onSuccess: (_, { organizationId }) => { + queryClient.invalidateQueries(organizationKeys.getOrgIdentityMemberships(organizationId)); + } + }); +}; + +export const useUpdateIdentityAwsIamAuth = () => { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: async ({ + identityId, + stsEndpoint, + allowedPrincipalArns, + allowedAccountIds, + accessTokenTTL, + accessTokenMaxTTL, + accessTokenNumUsesLimit, + accessTokenTrustedIps + }) => { + const { + data: { identityAwsIamAuth } + } = await apiRequest.patch<{ identityAwsIamAuth: IdentityAwsIamAuth }>( + `/api/v1/auth/aws-iam-auth/identities/${identityId}`, + { + stsEndpoint, + allowedPrincipalArns, + allowedAccountIds, + accessTokenTTL, + accessTokenMaxTTL, + accessTokenNumUsesLimit, + accessTokenTrustedIps + } + ); + return identityAwsIamAuth; + }, + onSuccess: (_, { organizationId }) => { + queryClient.invalidateQueries(organizationKeys.getOrgIdentityMemberships(organizationId)); + } + }); +}; diff --git a/frontend/src/hooks/api/identities/queries.tsx b/frontend/src/hooks/api/identities/queries.tsx index 92d2a432f..5d4a4aeef 100644 --- a/frontend/src/hooks/api/identities/queries.tsx +++ b/frontend/src/hooks/api/identities/queries.tsx @@ -2,27 +2,26 @@ import { useQuery } from "@tanstack/react-query"; import { apiRequest } from "@app/config/request"; -import { ClientSecretData, IdentityUniversalAuth } from "./types"; +import { ClientSecretData, IdentityAwsIamAuth,IdentityUniversalAuth } from "./types"; export const identitiesKeys = { getIdentityUniversalAuth: (identityId: string) => [{ identityId }, "identity-universal-auth"] as const, getIdentityUniversalAuthClientSecrets: (identityId: string) => - [{ identityId }, "identity-universal-auth-client-secrets"] as const + [{ identityId }, "identity-universal-auth-client-secrets"] as const, + getIdentityAwsIamAuth: (identityId: string) => [{ identityId }, "identity-aws-iam-auth"] as const }; export const useGetIdentityUniversalAuth = (identityId: string) => { return useQuery({ + enabled: Boolean(identityId), queryKey: identitiesKeys.getIdentityUniversalAuth(identityId), queryFn: async () => { - if (identityId === "") throw new Error("Identity ID is required"); - const { data: { identityUniversalAuth } } = await apiRequest.get<{ identityUniversalAuth: IdentityUniversalAuth }>( `/api/v1/auth/universal-auth/identities/${identityId}` ); - return identityUniversalAuth; } }); @@ -30,17 +29,30 @@ export const useGetIdentityUniversalAuth = (identityId: string) => { export const useGetIdentityUniversalAuthClientSecrets = (identityId: string) => { return useQuery({ + enabled: Boolean(identityId), queryKey: identitiesKeys.getIdentityUniversalAuthClientSecrets(identityId), queryFn: async () => { - if (identityId === "") return []; - const { data: { clientSecretData } } = await apiRequest.get<{ clientSecretData: ClientSecretData[] }>( `/api/v1/auth/universal-auth/identities/${identityId}/client-secrets` ); - return clientSecretData; } }); }; + +export const useGetIdentityAwsIamAuth = (identityId: string) => { + return useQuery({ + enabled: Boolean(identityId), + queryKey: identitiesKeys.getIdentityAwsIamAuth(identityId), + queryFn: async () => { + const { + data: { identityAwsIamAuth } + } = await apiRequest.get<{ identityAwsIamAuth: IdentityAwsIamAuth }>( + `/api/v1/auth/aws-iam-auth/identities/${identityId}` + ); + return identityAwsIamAuth; + } + }); +}; diff --git a/frontend/src/hooks/api/identities/types.ts b/frontend/src/hooks/api/identities/types.ts index 4ac19c351..02b0e9f53 100644 --- a/frontend/src/hooks/api/identities/types.ts +++ b/frontend/src/hooks/api/identities/types.ts @@ -38,19 +38,19 @@ export type IdentityMembership = { customRoleSlug: string; } & ( | { - isTemporary: false; - temporaryRange: null; - temporaryMode: null; - temporaryAccessEndTime: null; - temporaryAccessStartTime: null; - } + isTemporary: false; + temporaryRange: null; + temporaryMode: null; + temporaryAccessEndTime: null; + temporaryAccessStartTime: null; + } | { - isTemporary: true; - temporaryRange: string; - temporaryMode: string; - temporaryAccessEndTime: string; - temporaryAccessStartTime: string; - } + isTemporary: true; + temporaryRange: string; + temporaryMode: string; + temporaryAccessEndTime: string; + temporaryAccessStartTime: string; + } ) >; createdAt: string; @@ -113,6 +113,45 @@ export type UpdateIdentityUniversalAuthDTO = { }[]; }; +export type IdentityAwsIamAuth = { + identityId: string; + stsEndpoint: string; + allowedPrincipalArns: string; + allowedAccountIds: string; + accessTokenTTL: number; + accessTokenMaxTTL: number; + accessTokenNumUsesLimit: number; + accessTokenTrustedIps: IdentityTrustedIp[]; +}; + +export type AddIdentityAwsIamAuthDTO = { + organizationId: string; + identityId: string; + stsEndpoint: string; + allowedPrincipalArns: string; + allowedAccountIds: string; + accessTokenTTL: number; + accessTokenMaxTTL: number; + accessTokenNumUsesLimit: number; + accessTokenTrustedIps: { + ipAddress: string; + }[]; +}; + +export type UpdateIdentityAwsIamAuthDTO = { + organizationId: string; + identityId: string; + stsEndpoint?: string; + allowedPrincipalArns?: string; + allowedAccountIds?: string; + accessTokenTTL?: number; + accessTokenMaxTTL?: number; + accessTokenNumUsesLimit?: number; + accessTokenTrustedIps?: { + ipAddress: string; + }[]; +}; + export type CreateIdentityUniversalAuthClientSecretDTO = { identityId: string; description?: string; diff --git a/frontend/src/views/Org/MembersPage/components/OrgIdentityTab/components/IdentitySection/IdentityAuthMethodModal.tsx b/frontend/src/views/Org/MembersPage/components/OrgIdentityTab/components/IdentitySection/IdentityAuthMethodModal.tsx index 7ec17dd31..2cd4cc334 100644 --- a/frontend/src/views/Org/MembersPage/components/OrgIdentityTab/components/IdentitySection/IdentityAuthMethodModal.tsx +++ b/frontend/src/views/Org/MembersPage/components/OrgIdentityTab/components/IdentitySection/IdentityAuthMethodModal.tsx @@ -1,3 +1,4 @@ +import { useEffect } from "react"; import { Controller, useForm } from "react-hook-form"; import { yupResolver } from "@hookform/resolvers/yup"; import * as yup from "yup"; @@ -13,6 +14,7 @@ import { import { IdentityAuthMethod } from "@app/hooks/api/identities"; import { UsePopUpState } from "@app/hooks/usePopUp"; +import { IdentityAwsIamAuthForm } from "./IdentityAwsIamAuthForm"; import { IdentityUniversalAuthForm } from "./IdentityUniversalAuthForm"; type Props = { @@ -24,22 +26,25 @@ type Props = { ) => void; }; -const identityAuthMethods = [{ label: "Universal Auth", value: IdentityAuthMethod.UNIVERSAL_AUTH }]; +const identityAuthMethods = [ + { label: "Universal Auth", value: IdentityAuthMethod.UNIVERSAL_AUTH }, + { label: "AWS IAM Auth", value: IdentityAuthMethod.AWS_IAM_AUTH } +]; const schema = yup .object({ - authMethod: yup.string().required("Auth method is required") // TODO: better enforcement here + authMethod: yup.string().required("Auth method is required") }) .required(); export type FormData = yup.InferType; export const IdentityAuthMethodModal = ({ popUp, handlePopUpOpen, handlePopUpToggle }: Props) => { - const { - control - // watch, - } = useForm({ - resolver: yupResolver(schema) + const { control, watch, setValue } = useForm({ + resolver: yupResolver(schema), + defaultValues: { + authMethod: IdentityAuthMethod.UNIVERSAL_AUTH + } }); const identityAuthMethodData = popUp?.identityAuthMethod?.data as { @@ -48,16 +53,41 @@ export const IdentityAuthMethodModal = ({ popUp, handlePopUpOpen, handlePopUpTog authMethod?: IdentityAuthMethod; }; - // const authMethod = watch("authMethod"); + useEffect(() => { + if (identityAuthMethodData?.authMethod) { + setValue("authMethod", identityAuthMethodData.authMethod); + return; + } + + setValue("authMethod", IdentityAuthMethod.UNIVERSAL_AUTH); + }, [identityAuthMethodData?.authMethod]); + + const authMethod = watch("authMethod"); const renderIdentityAuthForm = () => { - return ( - - ); + switch (identityAuthMethodData?.authMethod ?? authMethod) { + case IdentityAuthMethod.AWS_IAM_AUTH: { + return ( + + ); + } + case IdentityAuthMethod.UNIVERSAL_AUTH: { + return ( + + ); + } + default: { + return
; + } + } }; return ( @@ -83,6 +113,7 @@ export const IdentityAuthMethodModal = ({ popUp, handlePopUpOpen, handlePopUpTog {...field} onValueChange={(e) => onChange(e)} className="w-full" + isDisabled={!!identityAuthMethodData?.authMethod} > {identityAuthMethods.map(({ label, value }) => ( diff --git a/frontend/src/views/Org/MembersPage/components/OrgIdentityTab/components/IdentitySection/IdentityAwsIamAuthForm.tsx b/frontend/src/views/Org/MembersPage/components/OrgIdentityTab/components/IdentitySection/IdentityAwsIamAuthForm.tsx new file mode 100644 index 000000000..8528a5c30 --- /dev/null +++ b/frontend/src/views/Org/MembersPage/components/OrgIdentityTab/components/IdentitySection/IdentityAwsIamAuthForm.tsx @@ -0,0 +1,352 @@ +import { useEffect } from "react"; +import { Controller, useFieldArray, useForm } from "react-hook-form"; +import { faPlus, faXmark } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { yupResolver } from "@hookform/resolvers/yup"; +import * as yup from "yup"; + +import { createNotification } from "@app/components/notifications"; +import { Button, FormControl, IconButton, Input } from "@app/components/v2"; +import { useOrganization, useSubscription } from "@app/context"; +import { + useAddIdentityAwsIamAuth, + useGetIdentityAwsIamAuth, + useUpdateIdentityAwsIamAuth +} from "@app/hooks/api"; +import { IdentityAuthMethod } from "@app/hooks/api/identities"; +import { IdentityTrustedIp } from "@app/hooks/api/identities/types"; +import { UsePopUpState } from "@app/hooks/usePopUp"; + +const schema = yup + .object({ + stsEndpoint: yup.string(), + allowedPrincipalArns: yup.string(), + allowedAccountIds: yup.string(), + accessTokenTTL: yup.string().required("Access Token TTL is required"), + accessTokenMaxTTL: yup.string().required("Access Max Token TTL is required"), + accessTokenNumUsesLimit: yup.string().required("Access Token Max Number of Uses is required"), + accessTokenTrustedIps: yup + .array( + yup.object({ + ipAddress: yup.string().max(50).required().label("IP Address") + }) + ) + .min(1) + .required() + .label("Access Token Trusted IP") + }) + .required(); + +export type FormData = yup.InferType; + +type Props = { + handlePopUpOpen: (popUpName: keyof UsePopUpState<["upgradePlan"]>) => void; + handlePopUpToggle: ( + popUpName: keyof UsePopUpState<["identityAuthMethod"]>, + state?: boolean + ) => void; + identityAuthMethodData: { + identityId: string; + name: string; + authMethod?: IdentityAuthMethod; + }; +}; + +export const IdentityAwsIamAuthForm = ({ + handlePopUpOpen, + handlePopUpToggle, + identityAuthMethodData +}: Props) => { + const { currentOrg } = useOrganization(); + const orgId = currentOrg?.id || ""; + const { subscription } = useSubscription(); + + const { mutateAsync: addMutateAsync } = useAddIdentityAwsIamAuth(); + const { mutateAsync: updateMutateAsync } = useUpdateIdentityAwsIamAuth(); + + const { data } = useGetIdentityAwsIamAuth(identityAuthMethodData?.identityId ?? ""); + + const { + control, + handleSubmit, + reset, + formState: { isSubmitting } + } = useForm({ + resolver: yupResolver(schema), + defaultValues: { + stsEndpoint: "https://sts.amazonaws.com/", + allowedPrincipalArns: "", + allowedAccountIds: "", + accessTokenTTL: "2592000", + accessTokenMaxTTL: "2592000", + accessTokenNumUsesLimit: "0", + accessTokenTrustedIps: [{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }] + } + }); + + const { + fields: accessTokenTrustedIpsFields, + append: appendAccessTokenTrustedIp, + remove: removeAccessTokenTrustedIp + } = useFieldArray({ control, name: "accessTokenTrustedIps" }); + + useEffect(() => { + if (data) { + reset({ + stsEndpoint: data.stsEndpoint, + allowedPrincipalArns: data.allowedPrincipalArns, + allowedAccountIds: data.allowedAccountIds, + accessTokenTTL: String(data.accessTokenTTL), + accessTokenMaxTTL: String(data.accessTokenMaxTTL), + accessTokenNumUsesLimit: String(data.accessTokenNumUsesLimit), + accessTokenTrustedIps: data.accessTokenTrustedIps.map( + ({ ipAddress, prefix }: IdentityTrustedIp) => { + return { + ipAddress: `${ipAddress}${prefix !== undefined ? `/${prefix}` : ""}` + }; + } + ) + }); + } else { + reset({ + stsEndpoint: "https://sts.amazonaws.com/", + allowedPrincipalArns: "", + allowedAccountIds: "", + accessTokenTTL: "2592000", + accessTokenMaxTTL: "2592000", + accessTokenNumUsesLimit: "0", + accessTokenTrustedIps: [{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }] + }); + } + }, [data]); + + const onFormSubmit = async ({ + allowedPrincipalArns, + allowedAccountIds, + stsEndpoint, + accessTokenTTL, + accessTokenMaxTTL, + accessTokenNumUsesLimit, + accessTokenTrustedIps + }: FormData) => { + try { + if (!identityAuthMethodData) return; + + if (data) { + await updateMutateAsync({ + organizationId: orgId, + stsEndpoint, + allowedPrincipalArns, + allowedAccountIds, + identityId: identityAuthMethodData.identityId, + accessTokenTTL: Number(accessTokenTTL), + accessTokenMaxTTL: Number(accessTokenMaxTTL), + accessTokenNumUsesLimit: Number(accessTokenNumUsesLimit), + accessTokenTrustedIps + }); + } else { + await addMutateAsync({ + organizationId: orgId, + identityId: identityAuthMethodData.identityId, + stsEndpoint: stsEndpoint || "", + allowedPrincipalArns: allowedPrincipalArns || "", + allowedAccountIds: allowedAccountIds || "", + accessTokenTTL: Number(accessTokenTTL), + accessTokenMaxTTL: Number(accessTokenMaxTTL), + accessTokenNumUsesLimit: Number(accessTokenNumUsesLimit), + accessTokenTrustedIps + }); + } + + handlePopUpToggle("identityAuthMethod", false); + + createNotification({ + text: `Successfully ${ + identityAuthMethodData?.authMethod ? "updated" : "configured" + } auth method`, + type: "success" + }); + + reset(); + } catch (err) { + createNotification({ + text: `Failed to ${identityAuthMethodData?.authMethod ? "update" : "configure"} identity`, + type: "error" + }); + } + }; + + return ( +
+ ( + + + + )} + /> + ( + + + + )} + /> + ( + + + + )} + /> + ( + + + + )} + /> + ( + + + + )} + /> + ( + + + + )} + /> + {accessTokenTrustedIpsFields.map(({ id }, index) => ( +
+ { + return ( + + { + if (subscription?.ipAllowlisting) { + field.onChange(e); + return; + } + + handlePopUpOpen("upgradePlan"); + }} + placeholder="123.456.789.0" + /> + + ); + }} + /> + { + if (subscription?.ipAllowlisting) { + removeAccessTokenTrustedIp(index); + return; + } + + handlePopUpOpen("upgradePlan"); + }} + size="lg" + colorSchema="danger" + variant="plain" + ariaLabel="update" + className="p-3" + > + + +
+ ))} +
+ +
+
+ + +
+ + ); +}; diff --git a/frontend/src/views/Org/MembersPage/components/OrgIdentityTab/components/IdentitySection/IdentityModal.tsx b/frontend/src/views/Org/MembersPage/components/OrgIdentityTab/components/IdentitySection/IdentityModal.tsx index 4ab44e947..e8b16cb5d 100644 --- a/frontend/src/views/Org/MembersPage/components/OrgIdentityTab/components/IdentitySection/IdentityModal.tsx +++ b/frontend/src/views/Org/MembersPage/components/OrgIdentityTab/components/IdentitySection/IdentityModal.tsx @@ -15,7 +15,10 @@ import { } from "@app/components/v2"; import { useOrganization } from "@app/context"; import { useCreateIdentity, useGetOrgRoles, useUpdateIdentity } from "@app/hooks/api"; -import { IdentityAuthMethod, useAddIdentityUniversalAuth } from "@app/hooks/api/identities"; +import { + IdentityAuthMethod + // useAddIdentityUniversalAuth +} from "@app/hooks/api/identities"; import { UsePopUpState } from "@app/hooks/usePopUp"; const schema = yup @@ -40,9 +43,7 @@ type Props = { handlePopUpToggle: (popUpName: keyof UsePopUpState<["identity"]>, state?: boolean) => void; }; -export const IdentityModal = ({ popUp, /* handlePopUpOpen, */ handlePopUpToggle }: Props) => { - - +export const IdentityModal = ({ popUp, handlePopUpOpen, handlePopUpToggle }: Props) => { const { currentOrg } = useOrganization(); const orgId = currentOrg?.id || ""; @@ -50,7 +51,7 @@ export const IdentityModal = ({ popUp, /* handlePopUpOpen, */ handlePopUpToggle const { mutateAsync: createMutateAsync } = useCreateIdentity(); const { mutateAsync: updateMutateAsync } = useUpdateIdentity(); - const { mutateAsync: addMutateAsync } = useAddIdentityUniversalAuth(); + // const { mutateAsync: addMutateAsync } = useAddIdentityUniversalAuth(); const { control, @@ -113,31 +114,31 @@ export const IdentityModal = ({ popUp, /* handlePopUpOpen, */ handlePopUpToggle // create const { - id: createdId - // name: createdName, - // authMethod + id: createdId, + name: createdName, + authMethod } = await createMutateAsync({ name, role: role || undefined, organizationId: orgId }); - await addMutateAsync({ - organizationId: orgId, - identityId: createdId, - clientSecretTrustedIps: [{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }], - accessTokenTrustedIps: [{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }], - accessTokenTTL: 2592000, - accessTokenMaxTTL: 2592000, - accessTokenNumUsesLimit: 0 - }); + // await addMutateAsync({ + // organizationId: orgId, + // identityId: createdId, + // clientSecretTrustedIps: [{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }], + // accessTokenTrustedIps: [{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }], + // accessTokenTTL: 2592000, + // accessTokenMaxTTL: 2592000, + // accessTokenNumUsesLimit: 0 + // }); handlePopUpToggle("identity", false); - // handlePopUpOpen("identityAuthMethod", { - // identityId: createdId, - // name: createdName, - // authMethod - // }); + handlePopUpOpen("identityAuthMethod", { + identityId: createdId, + name: createdName, + authMethod + }); } createNotification({ diff --git a/frontend/src/views/Org/MembersPage/components/OrgIdentityTab/components/IdentitySection/IdentityTable.tsx b/frontend/src/views/Org/MembersPage/components/OrgIdentityTab/components/IdentitySection/IdentityTable.tsx index 804c2d054..0ed9cd9a4 100644 --- a/frontend/src/views/Org/MembersPage/components/OrgIdentityTab/components/IdentitySection/IdentityTable.tsx +++ b/frontend/src/views/Org/MembersPage/components/OrgIdentityTab/components/IdentitySection/IdentityTable.tsx @@ -23,8 +23,6 @@ import { useGetIdentityMembershipOrgs, useGetOrgRoles, useUpdateIdentity } from import { IdentityAuthMethod, identityAuthToNameMap } from "@app/hooks/api/identities"; import { UsePopUpState } from "@app/hooks/usePopUp"; -// TODO: some kind of map - type Props = { handlePopUpOpen: ( popUpName: keyof UsePopUpState< @@ -44,7 +42,6 @@ type Props = { }; export const IdentityTable = ({ handlePopUpOpen }: Props) => { - const { currentOrg } = useOrganization(); const orgId = currentOrg?.id || ""; diff --git a/frontend/src/views/Org/MembersPage/components/OrgIdentityTab/components/IdentitySection/IdentityUniversalAuthForm.tsx b/frontend/src/views/Org/MembersPage/components/OrgIdentityTab/components/IdentitySection/IdentityUniversalAuthForm.tsx index d8b4ac042..af82082a5 100644 --- a/frontend/src/views/Org/MembersPage/components/OrgIdentityTab/components/IdentitySection/IdentityUniversalAuthForm.tsx +++ b/frontend/src/views/Org/MembersPage/components/OrgIdentityTab/components/IdentitySection/IdentityUniversalAuthForm.tsx @@ -63,7 +63,6 @@ export const IdentityUniversalAuthForm = ({ handlePopUpToggle, identityAuthMethodData }: Props) => { - const { currentOrg } = useOrganization(); const orgId = currentOrg?.id || ""; const { subscription } = useSubscription(); @@ -384,7 +383,7 @@ export const IdentityUniversalAuthForm = ({ variant="plain" onClick={() => handlePopUpToggle("identityAuthMethod", false)} > - Cancel + {identityAuthMethodData?.authMethod ? "Cancel" : "Skip"}