diff --git a/backend/package-lock.json b/backend/package-lock.json index ad341d15c..ffe9cfcb0 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -10,6 +10,7 @@ "license": "ISC", "dependencies": { "@aws-sdk/client-iam": "^3.525.0", + "@aws-sdk/client-kms": "^3.609.0", "@aws-sdk/client-secrets-manager": "^3.504.0", "@aws-sdk/client-sts": "^3.600.0", "@casl/ability": "^6.5.0", @@ -52,6 +53,7 @@ "jmespath": "^0.16.0", "jsonwebtoken": "^9.0.2", "jsrp": "^0.2.4", + "jwks-rsa": "^3.1.0", "knex": "^3.0.1", "ldapjs": "^3.0.7", "libsodium-wrappers": "^0.7.13", @@ -1199,6 +1201,1067 @@ } } }, + "node_modules/@aws-sdk/client-kms": { + "version": "3.609.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-kms/-/client-kms-3.609.0.tgz", + "integrity": "sha512-tKXOHnmwdN7a7i213xDWtjlpkpRfRzdPjHXz1dl0ofUe/DYFph14O9TFWqCXQe25ilcjPuR8cmVJqHYy0kS/4A==", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/client-sso-oidc": "3.609.0", + "@aws-sdk/client-sts": "3.609.0", + "@aws-sdk/core": "3.609.0", + "@aws-sdk/credential-provider-node": "3.609.0", + "@aws-sdk/middleware-host-header": "3.609.0", + "@aws-sdk/middleware-logger": "3.609.0", + "@aws-sdk/middleware-recursion-detection": "3.609.0", + "@aws-sdk/middleware-user-agent": "3.609.0", + "@aws-sdk/region-config-resolver": "3.609.0", + "@aws-sdk/types": "3.609.0", + "@aws-sdk/util-endpoints": "3.609.0", + "@aws-sdk/util-user-agent-browser": "3.609.0", + "@aws-sdk/util-user-agent-node": "3.609.0", + "@smithy/config-resolver": "^3.0.4", + "@smithy/core": "^2.2.4", + "@smithy/fetch-http-handler": "^3.2.0", + "@smithy/hash-node": "^3.0.3", + "@smithy/invalid-dependency": "^3.0.3", + "@smithy/middleware-content-length": "^3.0.3", + "@smithy/middleware-endpoint": "^3.0.4", + "@smithy/middleware-retry": "^3.0.7", + "@smithy/middleware-serde": "^3.0.3", + "@smithy/middleware-stack": "^3.0.3", + "@smithy/node-config-provider": "^3.1.3", + "@smithy/node-http-handler": "^3.1.1", + "@smithy/protocol-http": "^4.0.3", + "@smithy/smithy-client": "^3.1.5", + "@smithy/types": "^3.3.0", + "@smithy/url-parser": "^3.0.3", + "@smithy/util-base64": "^3.0.0", + "@smithy/util-body-length-browser": "^3.0.0", + "@smithy/util-body-length-node": "^3.0.0", + "@smithy/util-defaults-mode-browser": "^3.0.7", + "@smithy/util-defaults-mode-node": "^3.0.7", + "@smithy/util-endpoints": "^2.0.4", + "@smithy/util-middleware": "^3.0.3", + "@smithy/util-retry": "^3.0.3", + "@smithy/util-utf8": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-kms/node_modules/@aws-crypto/sha256-browser": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-browser/-/sha256-browser-5.2.0.tgz", + "integrity": "sha512-AXfN/lGotSQwu6HNcEsIASo7kWXZ5HYWvfOmSNKDsEqC4OashTp8alTmaz+F7TC2L083SFv5RdB+qU3Vs1kZqw==", + "dependencies": { + "@aws-crypto/sha256-js": "^5.2.0", + "@aws-crypto/supports-web-crypto": "^5.2.0", + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "@aws-sdk/util-locate-window": "^3.0.0", + "@smithy/util-utf8": "^2.0.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-sdk/client-kms/node_modules/@aws-crypto/sha256-browser/node_modules/@smithy/util-utf8": { + "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.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/client-kms/node_modules/@aws-crypto/sha256-js": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-js/-/sha256-js-5.2.0.tgz", + "integrity": "sha512-FFQQyu7edu4ufvIZ+OadFpHHOt+eSTBaYaki44c+akjg7qZg9oOQeLlk77F6tSYqjDAFClrHJk9tMf0HdVyOvA==", + "dependencies": { + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-kms/node_modules/@aws-crypto/supports-web-crypto": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/supports-web-crypto/-/supports-web-crypto-5.2.0.tgz", + "integrity": "sha512-iAvUotm021kM33eCdNfwIN//F77/IADDSs58i+MDaOqFrVjZo9bAal0NK7HurRuWLLpF1iLX7gbWrjHjeo+YFg==", + "dependencies": { + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-sdk/client-kms/node_modules/@aws-crypto/util": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/util/-/util-5.2.0.tgz", + "integrity": "sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ==", + "dependencies": { + "@aws-sdk/types": "^3.222.0", + "@smithy/util-utf8": "^2.0.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-sdk/client-kms/node_modules/@aws-crypto/util/node_modules/@smithy/util-utf8": { + "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.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/client-kms/node_modules/@aws-sdk/client-sso": { + "version": "3.609.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.609.0.tgz", + "integrity": "sha512-gqXGFDkIpKHCKAbeJK4aIDt3tiwJ26Rf5Tqw9JS6BYXsdMeOB8FTzqD9R+Yc1epHd8s5L94sdqXT5PapgxFZrg==", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "3.609.0", + "@aws-sdk/middleware-host-header": "3.609.0", + "@aws-sdk/middleware-logger": "3.609.0", + "@aws-sdk/middleware-recursion-detection": "3.609.0", + "@aws-sdk/middleware-user-agent": "3.609.0", + "@aws-sdk/region-config-resolver": "3.609.0", + "@aws-sdk/types": "3.609.0", + "@aws-sdk/util-endpoints": "3.609.0", + "@aws-sdk/util-user-agent-browser": "3.609.0", + "@aws-sdk/util-user-agent-node": "3.609.0", + "@smithy/config-resolver": "^3.0.4", + "@smithy/core": "^2.2.4", + "@smithy/fetch-http-handler": "^3.2.0", + "@smithy/hash-node": "^3.0.3", + "@smithy/invalid-dependency": "^3.0.3", + "@smithy/middleware-content-length": "^3.0.3", + "@smithy/middleware-endpoint": "^3.0.4", + "@smithy/middleware-retry": "^3.0.7", + "@smithy/middleware-serde": "^3.0.3", + "@smithy/middleware-stack": "^3.0.3", + "@smithy/node-config-provider": "^3.1.3", + "@smithy/node-http-handler": "^3.1.1", + "@smithy/protocol-http": "^4.0.3", + "@smithy/smithy-client": "^3.1.5", + "@smithy/types": "^3.3.0", + "@smithy/url-parser": "^3.0.3", + "@smithy/util-base64": "^3.0.0", + "@smithy/util-body-length-browser": "^3.0.0", + "@smithy/util-body-length-node": "^3.0.0", + "@smithy/util-defaults-mode-browser": "^3.0.7", + "@smithy/util-defaults-mode-node": "^3.0.7", + "@smithy/util-endpoints": "^2.0.4", + "@smithy/util-middleware": "^3.0.3", + "@smithy/util-retry": "^3.0.3", + "@smithy/util-utf8": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-kms/node_modules/@aws-sdk/client-sso-oidc": { + "version": "3.609.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso-oidc/-/client-sso-oidc-3.609.0.tgz", + "integrity": "sha512-0bNPAyPdkWkS9EGB2A9BZDkBNrnVCBzk5lYRezoT4K3/gi9w1DTYH5tuRdwaTZdxW19U1mq7CV0YJJARKO1L9Q==", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "3.609.0", + "@aws-sdk/credential-provider-node": "3.609.0", + "@aws-sdk/middleware-host-header": "3.609.0", + "@aws-sdk/middleware-logger": "3.609.0", + "@aws-sdk/middleware-recursion-detection": "3.609.0", + "@aws-sdk/middleware-user-agent": "3.609.0", + "@aws-sdk/region-config-resolver": "3.609.0", + "@aws-sdk/types": "3.609.0", + "@aws-sdk/util-endpoints": "3.609.0", + "@aws-sdk/util-user-agent-browser": "3.609.0", + "@aws-sdk/util-user-agent-node": "3.609.0", + "@smithy/config-resolver": "^3.0.4", + "@smithy/core": "^2.2.4", + "@smithy/fetch-http-handler": "^3.2.0", + "@smithy/hash-node": "^3.0.3", + "@smithy/invalid-dependency": "^3.0.3", + "@smithy/middleware-content-length": "^3.0.3", + "@smithy/middleware-endpoint": "^3.0.4", + "@smithy/middleware-retry": "^3.0.7", + "@smithy/middleware-serde": "^3.0.3", + "@smithy/middleware-stack": "^3.0.3", + "@smithy/node-config-provider": "^3.1.3", + "@smithy/node-http-handler": "^3.1.1", + "@smithy/protocol-http": "^4.0.3", + "@smithy/smithy-client": "^3.1.5", + "@smithy/types": "^3.3.0", + "@smithy/url-parser": "^3.0.3", + "@smithy/util-base64": "^3.0.0", + "@smithy/util-body-length-browser": "^3.0.0", + "@smithy/util-body-length-node": "^3.0.0", + "@smithy/util-defaults-mode-browser": "^3.0.7", + "@smithy/util-defaults-mode-node": "^3.0.7", + "@smithy/util-endpoints": "^2.0.4", + "@smithy/util-middleware": "^3.0.3", + "@smithy/util-retry": "^3.0.3", + "@smithy/util-utf8": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "@aws-sdk/client-sts": "^3.609.0" + } + }, + "node_modules/@aws-sdk/client-kms/node_modules/@aws-sdk/core": { + "version": "3.609.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.609.0.tgz", + "integrity": "sha512-ptqw+DTxLr01+pKjDUuo53SEDzI+7nFM3WfQaEo0yhDg8vWw8PER4sWj1Ysx67ksctnZesPUjqxd5SHbtdBxiA==", + "dependencies": { + "@smithy/core": "^2.2.4", + "@smithy/protocol-http": "^4.0.3", + "@smithy/signature-v4": "^3.1.2", + "@smithy/smithy-client": "^3.1.5", + "@smithy/types": "^3.3.0", + "fast-xml-parser": "4.2.5", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-kms/node_modules/@aws-sdk/credential-provider-env": { + "version": "3.609.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.609.0.tgz", + "integrity": "sha512-v69ZCWcec2iuV9vLVJMa6fAb5xwkzN4jYIT8yjo2c4Ia/j976Q+TPf35Pnz5My48Xr94EFcaBazrWedF+kwfuQ==", + "dependencies": { + "@aws-sdk/types": "3.609.0", + "@smithy/property-provider": "^3.1.3", + "@smithy/types": "^3.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-kms/node_modules/@aws-sdk/credential-provider-http": { + "version": "3.609.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.609.0.tgz", + "integrity": "sha512-GQQfB9Mk4XUZwaPsk4V3w8MqleS6ApkZKVQn3vTLAKa8Y7B2Imcpe5zWbKYjDd8MPpMWjHcBGFTVlDRFP4zwSQ==", + "dependencies": { + "@aws-sdk/types": "3.609.0", + "@smithy/fetch-http-handler": "^3.2.0", + "@smithy/node-http-handler": "^3.1.1", + "@smithy/property-provider": "^3.1.3", + "@smithy/protocol-http": "^4.0.3", + "@smithy/smithy-client": "^3.1.5", + "@smithy/types": "^3.3.0", + "@smithy/util-stream": "^3.0.5", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-kms/node_modules/@aws-sdk/credential-provider-ini": { + "version": "3.609.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.609.0.tgz", + "integrity": "sha512-hwaBfXuBTv6/eAdEsDfGcteYUW6Km7lvvubbxEdxIuJNF3vswR7RMGIXaEC37hhPkTTgd3H0TONammhwZIfkog==", + "dependencies": { + "@aws-sdk/credential-provider-env": "3.609.0", + "@aws-sdk/credential-provider-http": "3.609.0", + "@aws-sdk/credential-provider-process": "3.609.0", + "@aws-sdk/credential-provider-sso": "3.609.0", + "@aws-sdk/credential-provider-web-identity": "3.609.0", + "@aws-sdk/types": "3.609.0", + "@smithy/credential-provider-imds": "^3.1.3", + "@smithy/property-provider": "^3.1.3", + "@smithy/shared-ini-file-loader": "^3.1.3", + "@smithy/types": "^3.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "@aws-sdk/client-sts": "^3.609.0" + } + }, + "node_modules/@aws-sdk/client-kms/node_modules/@aws-sdk/credential-provider-node": { + "version": "3.609.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.609.0.tgz", + "integrity": "sha512-4J8/JRuqfxJDGD9jTHVCBxCvYt7/Vgj2Stlhj930mrjFPO/yRw8ilAAZxBWe0JHPX3QwepCmh4ErZe53F5ysxQ==", + "dependencies": { + "@aws-sdk/credential-provider-env": "3.609.0", + "@aws-sdk/credential-provider-http": "3.609.0", + "@aws-sdk/credential-provider-ini": "3.609.0", + "@aws-sdk/credential-provider-process": "3.609.0", + "@aws-sdk/credential-provider-sso": "3.609.0", + "@aws-sdk/credential-provider-web-identity": "3.609.0", + "@aws-sdk/types": "3.609.0", + "@smithy/credential-provider-imds": "^3.1.3", + "@smithy/property-provider": "^3.1.3", + "@smithy/shared-ini-file-loader": "^3.1.3", + "@smithy/types": "^3.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-kms/node_modules/@aws-sdk/credential-provider-process": { + "version": "3.609.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.609.0.tgz", + "integrity": "sha512-Ux35nGOSJKZWUIM3Ny0ROZ8cqPRUEkh+tR3X2o9ydEbFiLq3eMMyEnHJqx4EeUjLRchidlm4CCid9GxMe5/gdw==", + "dependencies": { + "@aws-sdk/types": "3.609.0", + "@smithy/property-provider": "^3.1.3", + "@smithy/shared-ini-file-loader": "^3.1.3", + "@smithy/types": "^3.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-kms/node_modules/@aws-sdk/credential-provider-sso": { + "version": "3.609.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.609.0.tgz", + "integrity": "sha512-oQPGDKMMIxjvTcm86g07RPYeC7mCNk+29dPpY15ZAPRpAF7F0tircsC3wT9fHzNaKShEyK5LuI5Kg/uxsdy+Iw==", + "dependencies": { + "@aws-sdk/client-sso": "3.609.0", + "@aws-sdk/token-providers": "3.609.0", + "@aws-sdk/types": "3.609.0", + "@smithy/property-provider": "^3.1.3", + "@smithy/shared-ini-file-loader": "^3.1.3", + "@smithy/types": "^3.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-kms/node_modules/@aws-sdk/credential-provider-web-identity": { + "version": "3.609.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.609.0.tgz", + "integrity": "sha512-U+PG8NhlYYF45zbr1km3ROtBMYqyyj/oK8NRp++UHHeuavgrP+4wJ4wQnlEaKvJBjevfo3+dlIBcaeQ7NYejWg==", + "dependencies": { + "@aws-sdk/types": "3.609.0", + "@smithy/property-provider": "^3.1.3", + "@smithy/types": "^3.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "@aws-sdk/client-sts": "^3.609.0" + } + }, + "node_modules/@aws-sdk/client-kms/node_modules/@aws-sdk/middleware-host-header": { + "version": "3.609.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-host-header/-/middleware-host-header-3.609.0.tgz", + "integrity": "sha512-iTKfo158lc4jLDfYeZmYMIBHsn8m6zX+XB6birCSNZ/rrlzAkPbGE43CNdKfvjyWdqgLMRXF+B+OcZRvqhMXPQ==", + "dependencies": { + "@aws-sdk/types": "3.609.0", + "@smithy/protocol-http": "^4.0.3", + "@smithy/types": "^3.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-kms/node_modules/@aws-sdk/middleware-logger": { + "version": "3.609.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-logger/-/middleware-logger-3.609.0.tgz", + "integrity": "sha512-S62U2dy4jMDhDFDK5gZ4VxFdWzCtLzwbYyFZx2uvPYTECkepLUfzLic2BHg2Qvtu4QjX+oGE3P/7fwaGIsGNuQ==", + "dependencies": { + "@aws-sdk/types": "3.609.0", + "@smithy/types": "^3.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-kms/node_modules/@aws-sdk/middleware-recursion-detection": { + "version": "3.609.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.609.0.tgz", + "integrity": "sha512-6sewsYB7/o/nbUfA99Aa/LokM+a/u4Wpm/X2o0RxOsDtSB795ObebLJe2BxY5UssbGaWkn7LswyfvrdZNXNj1w==", + "dependencies": { + "@aws-sdk/types": "3.609.0", + "@smithy/protocol-http": "^4.0.3", + "@smithy/types": "^3.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-kms/node_modules/@aws-sdk/middleware-user-agent": { + "version": "3.609.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.609.0.tgz", + "integrity": "sha512-nbq7MXRmeXm4IDqh+sJRAxGPAq0OfGmGIwKvJcw66hLoG8CmhhVMZmIAEBDFr57S+YajGwnLLRt+eMI05MMeVA==", + "dependencies": { + "@aws-sdk/types": "3.609.0", + "@aws-sdk/util-endpoints": "3.609.0", + "@smithy/protocol-http": "^4.0.3", + "@smithy/types": "^3.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-kms/node_modules/@aws-sdk/region-config-resolver": { + "version": "3.609.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/region-config-resolver/-/region-config-resolver-3.609.0.tgz", + "integrity": "sha512-lMHBG8zg9GWYBc9/XVPKyuAUd7iKqfPP7z04zGta2kGNOKbUTeqmAdc1gJGku75p4kglIPlGBorOxti8DhRmKw==", + "dependencies": { + "@aws-sdk/types": "3.609.0", + "@smithy/node-config-provider": "^3.1.3", + "@smithy/types": "^3.3.0", + "@smithy/util-config-provider": "^3.0.0", + "@smithy/util-middleware": "^3.0.3", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-kms/node_modules/@aws-sdk/token-providers": { + "version": "3.609.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.609.0.tgz", + "integrity": "sha512-WvhW/7XSf+H7YmtiIigQxfDVZVZI7mbKikQ09YpzN7FeN3TmYib1+0tB+EE9TbICkwssjiFc71FEBEh4K9grKQ==", + "dependencies": { + "@aws-sdk/types": "3.609.0", + "@smithy/property-provider": "^3.1.3", + "@smithy/shared-ini-file-loader": "^3.1.3", + "@smithy/types": "^3.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "@aws-sdk/client-sso-oidc": "^3.609.0" + } + }, + "node_modules/@aws-sdk/client-kms/node_modules/@aws-sdk/types": { + "version": "3.609.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.609.0.tgz", + "integrity": "sha512-+Tqnh9w0h2LcrUsdXyT1F8mNhXz+tVYBtP19LpeEGntmvHwa2XzvLUCWpoIAIVsHp5+HdB2X9Sn0KAtmbFXc2Q==", + "dependencies": { + "@smithy/types": "^3.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-kms/node_modules/@aws-sdk/util-endpoints": { + "version": "3.609.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.609.0.tgz", + "integrity": "sha512-Rh+3V8dOvEeE1aQmUy904DYWtLUEJ7Vf5XBPlQ6At3pBhp+zpXbsnpZzVL33c8lW1xfj6YPwtO6gOeEsl1juCQ==", + "dependencies": { + "@aws-sdk/types": "3.609.0", + "@smithy/types": "^3.3.0", + "@smithy/util-endpoints": "^2.0.4", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-kms/node_modules/@aws-sdk/util-user-agent-browser": { + "version": "3.609.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.609.0.tgz", + "integrity": "sha512-fojPU+mNahzQ0YHYBsx0ZIhmMA96H+ZIZ665ObU9tl+SGdbLneVZVikGve+NmHTQwHzwkFsZYYnVKAkreJLAtA==", + "dependencies": { + "@aws-sdk/types": "3.609.0", + "@smithy/types": "^3.3.0", + "bowser": "^2.11.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-sdk/client-kms/node_modules/@aws-sdk/util-user-agent-node": { + "version": "3.609.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.609.0.tgz", + "integrity": "sha512-DlZBwQ/HkZyf3pOWc7+wjJRk5R7x9YxHhs2szHwtv1IW30KMabjjjX0GMlGJ9LLkBHkbaaEY/w9Tkj12XRLhRg==", + "dependencies": { + "@aws-sdk/types": "3.609.0", + "@smithy/node-config-provider": "^3.1.3", + "@smithy/types": "^3.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "aws-crt": ">=1.0.0" + }, + "peerDependenciesMeta": { + "aws-crt": { + "optional": true + } + } + }, + "node_modules/@aws-sdk/client-kms/node_modules/@smithy/abort-controller": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@smithy/abort-controller/-/abort-controller-3.1.1.tgz", + "integrity": "sha512-MBJBiidoe+0cTFhyxT8g+9g7CeVccLM0IOKKUMCNQ1CNMJ/eIfoo0RTfVrXOONEI1UCN1W+zkiHSbzUNE9dZtQ==", + "dependencies": { + "@smithy/types": "^3.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-kms/node_modules/@smithy/config-resolver": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@smithy/config-resolver/-/config-resolver-3.0.4.tgz", + "integrity": "sha512-VwiOk7TwXoE7NlNguV/aPq1hFH72tqkHCw8eWXbr2xHspRyyv9DLpLXhq+Ieje+NwoqXrY0xyQjPXdOE6cGcHA==", + "dependencies": { + "@smithy/node-config-provider": "^3.1.3", + "@smithy/types": "^3.3.0", + "@smithy/util-config-provider": "^3.0.0", + "@smithy/util-middleware": "^3.0.3", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-kms/node_modules/@smithy/core": { + "version": "2.2.5", + "resolved": "https://registry.npmjs.org/@smithy/core/-/core-2.2.5.tgz", + "integrity": "sha512-0kqyj93/Aa30TEXnnWRBetN8fDGjFF+u8cdIiMI8YS6CrUF2dLTavRfHKfWh5cL5d6s2ZNyEnLjBitdcKmkETQ==", + "dependencies": { + "@smithy/middleware-endpoint": "^3.0.4", + "@smithy/middleware-retry": "^3.0.8", + "@smithy/middleware-serde": "^3.0.3", + "@smithy/protocol-http": "^4.0.3", + "@smithy/smithy-client": "^3.1.6", + "@smithy/types": "^3.3.0", + "@smithy/util-middleware": "^3.0.3", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-kms/node_modules/@smithy/credential-provider-imds": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-3.1.3.tgz", + "integrity": "sha512-U1Yrv6hx/mRK6k8AncuI6jLUx9rn0VVSd9NPEX6pyYFBfkSkChOc/n4zUb8alHUVg83TbI4OdZVo1X0Zfj3ijA==", + "dependencies": { + "@smithy/node-config-provider": "^3.1.3", + "@smithy/property-provider": "^3.1.3", + "@smithy/types": "^3.3.0", + "@smithy/url-parser": "^3.0.3", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-kms/node_modules/@smithy/fetch-http-handler": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-3.2.1.tgz", + "integrity": "sha512-0w0bgUvZmfa0vHN8a+moByhCJT07WN6AHKEhFSOLsDpnszm+5dLVv5utGaqbhOrZ/aF5x3xuPMs/oMCd+4O5xg==", + "dependencies": { + "@smithy/protocol-http": "^4.0.3", + "@smithy/querystring-builder": "^3.0.3", + "@smithy/types": "^3.3.0", + "@smithy/util-base64": "^3.0.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-sdk/client-kms/node_modules/@smithy/hash-node": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@smithy/hash-node/-/hash-node-3.0.3.tgz", + "integrity": "sha512-2ctBXpPMG+B3BtWSGNnKELJ7SH9e4TNefJS0cd2eSkOOROeBnnVBnAy9LtJ8tY4vUEoe55N4CNPxzbWvR39iBw==", + "dependencies": { + "@smithy/types": "^3.3.0", + "@smithy/util-buffer-from": "^3.0.0", + "@smithy/util-utf8": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-kms/node_modules/@smithy/hash-node/node_modules/@smithy/util-buffer-from": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-3.0.0.tgz", + "integrity": "sha512-aEOHCgq5RWFbP+UDPvPot26EJHjOC+bRgse5A8V3FSShqd5E5UN4qc7zkwsvJPPAVsf73QwYcHN1/gt/rtLwQA==", + "dependencies": { + "@smithy/is-array-buffer": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-kms/node_modules/@smithy/invalid-dependency": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@smithy/invalid-dependency/-/invalid-dependency-3.0.3.tgz", + "integrity": "sha512-ID1eL/zpDULmHJbflb864k72/SNOZCADRc9i7Exq3RUNJw6raWUSlFEQ+3PX3EYs++bTxZB2dE9mEHTQLv61tw==", + "dependencies": { + "@smithy/types": "^3.3.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-sdk/client-kms/node_modules/@smithy/is-array-buffer": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-3.0.0.tgz", + "integrity": "sha512-+Fsu6Q6C4RSJiy81Y8eApjEB5gVtM+oFKTffg+jSuwtvomJJrhUJBu2zS8wjXSgH/g1MKEWrzyChTBe6clb5FQ==", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-kms/node_modules/@smithy/middleware-content-length": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@smithy/middleware-content-length/-/middleware-content-length-3.0.3.tgz", + "integrity": "sha512-Dbz2bzexReYIQDWMr+gZhpwBetNXzbhnEMhYKA6urqmojO14CsXjnsoPYO8UL/xxcawn8ZsuVU61ElkLSltIUQ==", + "dependencies": { + "@smithy/protocol-http": "^4.0.3", + "@smithy/types": "^3.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-kms/node_modules/@smithy/middleware-endpoint": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@smithy/middleware-endpoint/-/middleware-endpoint-3.0.4.tgz", + "integrity": "sha512-whUJMEPwl3ANIbXjBXZVdJNgfV2ZU8ayln7xUM47rXL2txuenI7jQ/VFFwCzy5lCmXScjp6zYtptW5Evud8e9g==", + "dependencies": { + "@smithy/middleware-serde": "^3.0.3", + "@smithy/node-config-provider": "^3.1.3", + "@smithy/shared-ini-file-loader": "^3.1.3", + "@smithy/types": "^3.3.0", + "@smithy/url-parser": "^3.0.3", + "@smithy/util-middleware": "^3.0.3", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-kms/node_modules/@smithy/middleware-retry": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/@smithy/middleware-retry/-/middleware-retry-3.0.8.tgz", + "integrity": "sha512-wmIw3t6ZbeqstUFdXtStzSSltoYrcfc28ndnr0mDSMmtMSRNduNbmneA7xiE224fVFXzbf24+0oREks1u2X7Mw==", + "dependencies": { + "@smithy/node-config-provider": "^3.1.3", + "@smithy/protocol-http": "^4.0.3", + "@smithy/service-error-classification": "^3.0.3", + "@smithy/smithy-client": "^3.1.6", + "@smithy/types": "^3.3.0", + "@smithy/util-middleware": "^3.0.3", + "@smithy/util-retry": "^3.0.3", + "tslib": "^2.6.2", + "uuid": "^9.0.1" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-kms/node_modules/@smithy/middleware-serde": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@smithy/middleware-serde/-/middleware-serde-3.0.3.tgz", + "integrity": "sha512-puUbyJQBcg9eSErFXjKNiGILJGtiqmuuNKEYNYfUD57fUl4i9+mfmThtQhvFXU0hCVG0iEJhvQUipUf+/SsFdA==", + "dependencies": { + "@smithy/types": "^3.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-kms/node_modules/@smithy/middleware-stack": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@smithy/middleware-stack/-/middleware-stack-3.0.3.tgz", + "integrity": "sha512-r4klY9nFudB0r9UdSMaGSyjyQK5adUyPnQN/ZM6M75phTxOdnc/AhpvGD1fQUvgmqjQEBGCwpnPbDm8pH5PapA==", + "dependencies": { + "@smithy/types": "^3.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-kms/node_modules/@smithy/node-config-provider": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@smithy/node-config-provider/-/node-config-provider-3.1.3.tgz", + "integrity": "sha512-rxdpAZczzholz6CYZxtqDu/aKTxATD5DAUDVj7HoEulq+pDSQVWzbg0btZDlxeFfa6bb2b5tUvgdX5+k8jUqcg==", + "dependencies": { + "@smithy/property-provider": "^3.1.3", + "@smithy/shared-ini-file-loader": "^3.1.3", + "@smithy/types": "^3.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-kms/node_modules/@smithy/node-http-handler": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-3.1.2.tgz", + "integrity": "sha512-Td3rUNI7qqtoSLTsJBtsyfoG4cF/XMFmJr6Z2dX8QNzIi6tIW6YmuyFml8mJ2cNpyWNqITKbROMOFrvQjmsOvw==", + "dependencies": { + "@smithy/abort-controller": "^3.1.1", + "@smithy/protocol-http": "^4.0.3", + "@smithy/querystring-builder": "^3.0.3", + "@smithy/types": "^3.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-kms/node_modules/@smithy/property-provider": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@smithy/property-provider/-/property-provider-3.1.3.tgz", + "integrity": "sha512-zahyOVR9Q4PEoguJ/NrFP4O7SMAfYO1HLhB18M+q+Z4KFd4V2obiMnlVoUFzFLSPeVt1POyNWneHHrZaTMoc/g==", + "dependencies": { + "@smithy/types": "^3.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-kms/node_modules/@smithy/protocol-http": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/@smithy/protocol-http/-/protocol-http-4.0.3.tgz", + "integrity": "sha512-x5jmrCWwQlx+Zv4jAtc33ijJ+vqqYN+c/ZkrnpvEe/uDas7AT7A/4Rc2CdfxgWv4WFGmEqODIrrUToPN6DDkGw==", + "dependencies": { + "@smithy/types": "^3.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-kms/node_modules/@smithy/querystring-builder": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@smithy/querystring-builder/-/querystring-builder-3.0.3.tgz", + "integrity": "sha512-vyWckeUeesFKzCDaRwWLUA1Xym9McaA6XpFfAK5qI9DKJ4M33ooQGqvM4J+LalH4u/Dq9nFiC8U6Qn1qi0+9zw==", + "dependencies": { + "@smithy/types": "^3.3.0", + "@smithy/util-uri-escape": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-kms/node_modules/@smithy/querystring-parser": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@smithy/querystring-parser/-/querystring-parser-3.0.3.tgz", + "integrity": "sha512-zahM1lQv2YjmznnfQsWbYojFe55l0SLG/988brlLv1i8z3dubloLF+75ATRsqPBboUXsW6I9CPGE5rQgLfY0vQ==", + "dependencies": { + "@smithy/types": "^3.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-kms/node_modules/@smithy/service-error-classification": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@smithy/service-error-classification/-/service-error-classification-3.0.3.tgz", + "integrity": "sha512-Jn39sSl8cim/VlkLsUhRFq/dKDnRUFlfRkvhOJaUbLBXUsLRLNf9WaxDv/z9BjuQ3A6k/qE8af1lsqcwm7+DaQ==", + "dependencies": { + "@smithy/types": "^3.3.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-kms/node_modules/@smithy/shared-ini-file-loader": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@smithy/shared-ini-file-loader/-/shared-ini-file-loader-3.1.3.tgz", + "integrity": "sha512-Z8Y3+08vgoDgl4HENqNnnzSISAaGrF2RoKupoC47u2wiMp+Z8P/8mDh1CL8+8ujfi2U5naNvopSBmP/BUj8b5w==", + "dependencies": { + "@smithy/types": "^3.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-kms/node_modules/@smithy/signature-v4": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-3.1.2.tgz", + "integrity": "sha512-3BcPylEsYtD0esM4Hoyml/+s7WP2LFhcM3J2AGdcL2vx9O60TtfpDOL72gjb4lU8NeRPeKAwR77YNyyGvMbuEA==", + "dependencies": { + "@smithy/is-array-buffer": "^3.0.0", + "@smithy/types": "^3.3.0", + "@smithy/util-hex-encoding": "^3.0.0", + "@smithy/util-middleware": "^3.0.3", + "@smithy/util-uri-escape": "^3.0.0", + "@smithy/util-utf8": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-kms/node_modules/@smithy/smithy-client": { + "version": "3.1.6", + "resolved": "https://registry.npmjs.org/@smithy/smithy-client/-/smithy-client-3.1.6.tgz", + "integrity": "sha512-w9oboI661hfptr26houZ5mdKc//DMxkuOMXSaIiALqGn4bHYT9S4U69BBS6tHX4TZHgShmhcz0d6aXk7FY5soA==", + "dependencies": { + "@smithy/middleware-endpoint": "^3.0.4", + "@smithy/middleware-stack": "^3.0.3", + "@smithy/protocol-http": "^4.0.3", + "@smithy/types": "^3.3.0", + "@smithy/util-stream": "^3.0.6", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-kms/node_modules/@smithy/types": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-3.3.0.tgz", + "integrity": "sha512-IxvBBCTFDHbVoK7zIxqA1ZOdc4QfM5HM7rGleCuHi7L1wnKv5Pn69xXJQ9hgxH60ZVygH9/JG0jRgtUncE3QUA==", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-kms/node_modules/@smithy/url-parser": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@smithy/url-parser/-/url-parser-3.0.3.tgz", + "integrity": "sha512-pw3VtZtX2rg+s6HMs6/+u9+hu6oY6U7IohGhVNnjbgKy86wcIsSZwgHrFR+t67Uyxvp4Xz3p3kGXXIpTNisq8A==", + "dependencies": { + "@smithy/querystring-parser": "^3.0.3", + "@smithy/types": "^3.3.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-sdk/client-kms/node_modules/@smithy/util-base64": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-base64/-/util-base64-3.0.0.tgz", + "integrity": "sha512-Kxvoh5Qtt0CDsfajiZOCpJxgtPHXOKwmM+Zy4waD43UoEMA+qPxxa98aE/7ZhdnBFZFXMOiBR5xbcaMhLtznQQ==", + "dependencies": { + "@smithy/util-buffer-from": "^3.0.0", + "@smithy/util-utf8": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-kms/node_modules/@smithy/util-base64/node_modules/@smithy/util-buffer-from": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-3.0.0.tgz", + "integrity": "sha512-aEOHCgq5RWFbP+UDPvPot26EJHjOC+bRgse5A8V3FSShqd5E5UN4qc7zkwsvJPPAVsf73QwYcHN1/gt/rtLwQA==", + "dependencies": { + "@smithy/is-array-buffer": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-kms/node_modules/@smithy/util-body-length-browser": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-body-length-browser/-/util-body-length-browser-3.0.0.tgz", + "integrity": "sha512-cbjJs2A1mLYmqmyVl80uoLTJhAcfzMOyPgjwAYusWKMdLeNtzmMz9YxNl3/jRLoxSS3wkqkf0jwNdtXWtyEBaQ==", + "dependencies": { + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-sdk/client-kms/node_modules/@smithy/util-body-length-node": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-body-length-node/-/util-body-length-node-3.0.0.tgz", + "integrity": "sha512-Tj7pZ4bUloNUP6PzwhN7K386tmSmEET9QtQg0TgdNOnxhZvCssHji+oZTUIuzxECRfG8rdm2PMw2WCFs6eIYkA==", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-kms/node_modules/@smithy/util-config-provider": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-config-provider/-/util-config-provider-3.0.0.tgz", + "integrity": "sha512-pbjk4s0fwq3Di/ANL+rCvJMKM5bzAQdE5S/6RL5NXgMExFAi6UgQMPOm5yPaIWPpr+EOXKXRonJ3FoxKf4mCJQ==", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-kms/node_modules/@smithy/util-defaults-mode-browser": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-browser/-/util-defaults-mode-browser-3.0.8.tgz", + "integrity": "sha512-eLRHCvM1w3ZJkYcd60yKqM3d70dPB+071EDpf9ZGYqFed3xcm/+pWwNS/xM0JXRrjm0yAA19dWcdFN2IE/66pQ==", + "dependencies": { + "@smithy/property-provider": "^3.1.3", + "@smithy/smithy-client": "^3.1.6", + "@smithy/types": "^3.3.0", + "bowser": "^2.11.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/@aws-sdk/client-kms/node_modules/@smithy/util-defaults-mode-node": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-node/-/util-defaults-mode-node-3.0.8.tgz", + "integrity": "sha512-Tajvdyg5+k77j6AOrwSCZgi7KdBizqPNs3HCnFGRoxDjzh+CjPLaLrXbIRB0lsAmqYmRHIU34IogByaqvDrkBQ==", + "dependencies": { + "@smithy/config-resolver": "^3.0.4", + "@smithy/credential-provider-imds": "^3.1.3", + "@smithy/node-config-provider": "^3.1.3", + "@smithy/property-provider": "^3.1.3", + "@smithy/smithy-client": "^3.1.6", + "@smithy/types": "^3.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/@aws-sdk/client-kms/node_modules/@smithy/util-endpoints": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@smithy/util-endpoints/-/util-endpoints-2.0.4.tgz", + "integrity": "sha512-ZAtNf+vXAsgzgRutDDiklU09ZzZiiV/nATyqde4Um4priTmasDH+eLpp3tspL0hS2dEootyFMhu1Y6Y+tzpWBQ==", + "dependencies": { + "@smithy/node-config-provider": "^3.1.3", + "@smithy/types": "^3.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-kms/node_modules/@smithy/util-hex-encoding": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-hex-encoding/-/util-hex-encoding-3.0.0.tgz", + "integrity": "sha512-eFndh1WEK5YMUYvy3lPlVmYY/fZcQE1D8oSf41Id2vCeIkKJXPcYDCZD+4+xViI6b1XSd7tE+s5AmXzz5ilabQ==", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-kms/node_modules/@smithy/util-middleware": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@smithy/util-middleware/-/util-middleware-3.0.3.tgz", + "integrity": "sha512-l+StyYYK/eO3DlVPbU+4Bi06Jjal+PFLSMmlWM1BEwyLxZ3aKkf1ROnoIakfaA7mC6uw3ny7JBkau4Yc+5zfWw==", + "dependencies": { + "@smithy/types": "^3.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-kms/node_modules/@smithy/util-retry": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@smithy/util-retry/-/util-retry-3.0.3.tgz", + "integrity": "sha512-AFw+hjpbtVApzpNDhbjNG5NA3kyoMs7vx0gsgmlJF4s+yz1Zlepde7J58zpIRIsdjc+emhpAITxA88qLkPF26w==", + "dependencies": { + "@smithy/service-error-classification": "^3.0.3", + "@smithy/types": "^3.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-kms/node_modules/@smithy/util-stream": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@smithy/util-stream/-/util-stream-3.0.6.tgz", + "integrity": "sha512-w9i//7egejAIvplX821rPWWgaiY1dxsQUw0hXX7qwa/uZ9U3zplqTQ871jWadkcVB9gFDhkPWYVZf4yfFbZ0xA==", + "dependencies": { + "@smithy/fetch-http-handler": "^3.2.1", + "@smithy/node-http-handler": "^3.1.2", + "@smithy/types": "^3.3.0", + "@smithy/util-base64": "^3.0.0", + "@smithy/util-buffer-from": "^3.0.0", + "@smithy/util-hex-encoding": "^3.0.0", + "@smithy/util-utf8": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-kms/node_modules/@smithy/util-stream/node_modules/@smithy/util-buffer-from": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-3.0.0.tgz", + "integrity": "sha512-aEOHCgq5RWFbP+UDPvPot26EJHjOC+bRgse5A8V3FSShqd5E5UN4qc7zkwsvJPPAVsf73QwYcHN1/gt/rtLwQA==", + "dependencies": { + "@smithy/is-array-buffer": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-kms/node_modules/@smithy/util-uri-escape": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-uri-escape/-/util-uri-escape-3.0.0.tgz", + "integrity": "sha512-LqR7qYLgZTD7nWLBecUi4aqolw8Mhza9ArpNEQ881MJJIU2sE5iHCK6TdyqqzcDLy0OPe10IY4T8ctVdtynubg==", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-kms/node_modules/@smithy/util-utf8": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-3.0.0.tgz", + "integrity": "sha512-rUeT12bxFnplYDe815GXbq/oixEGHfRFFtcTF3YdDi/JaENIM6aSYYLJydG83UNzLXeRI5K8abYd/8Sp/QM0kA==", + "dependencies": { + "@smithy/util-buffer-from": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-kms/node_modules/@smithy/util-utf8/node_modules/@smithy/util-buffer-from": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-3.0.0.tgz", + "integrity": "sha512-aEOHCgq5RWFbP+UDPvPot26EJHjOC+bRgse5A8V3FSShqd5E5UN4qc7zkwsvJPPAVsf73QwYcHN1/gt/rtLwQA==", + "dependencies": { + "@smithy/is-array-buffer": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, "node_modules/@aws-sdk/client-secrets-manager": { "version": "3.504.0", "resolved": "https://registry.npmjs.org/@aws-sdk/client-secrets-manager/-/client-secrets-manager-3.504.0.tgz", @@ -14094,6 +15157,43 @@ "safe-buffer": "^5.0.1" } }, + "node_modules/jwks-rsa": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jwks-rsa/-/jwks-rsa-3.1.0.tgz", + "integrity": "sha512-v7nqlfezb9YfHHzYII3ef2a2j1XnGeSE/bK3WfumaYCqONAIstJbrEGapz4kadScZzEt7zYCN7bucj8C0Mv/Rg==", + "dependencies": { + "@types/express": "^4.17.17", + "@types/jsonwebtoken": "^9.0.2", + "debug": "^4.3.4", + "jose": "^4.14.6", + "limiter": "^1.1.5", + "lru-memoizer": "^2.2.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/jwks-rsa/node_modules/debug": { + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.5.tgz", + "integrity": "sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/jwks-rsa/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, "node_modules/jws": { "version": "3.2.2", "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", @@ -14321,6 +15421,11 @@ "node": ">=14" } }, + "node_modules/limiter": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/limiter/-/limiter-1.1.5.tgz", + "integrity": "sha512-FWWMIEOxz3GwUI4Ts/IvgVy6LPvoMPgjMdQ185nN6psJyBJ4yOpzqm695/h5umdLJg2vW3GR5iG11MAkR2AzJA==" + }, "node_modules/lines-and-columns": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", @@ -14395,6 +15500,11 @@ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" }, + "node_modules/lodash.clonedeep": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz", + "integrity": "sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ==" + }, "node_modules/lodash.debounce": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", @@ -14513,6 +15623,15 @@ "node": ">=10" } }, + "node_modules/lru-memoizer": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/lru-memoizer/-/lru-memoizer-2.3.0.tgz", + "integrity": "sha512-GXn7gyHAMhO13WSKrIiNfztwxodVsP8IoZ3XfrJV4yH2x0/OeTO/FIaAHTY5YekdGgW94njfuKmyyt1E0mR6Ug==", + "dependencies": { + "lodash.clonedeep": "^4.5.0", + "lru-cache": "6.0.0" + } + }, "node_modules/luxon": { "version": "3.4.4", "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.4.4.tgz", diff --git a/backend/package.json b/backend/package.json index b2f3cba0c..dd66d4cd6 100644 --- a/backend/package.json +++ b/backend/package.json @@ -106,6 +106,7 @@ }, "dependencies": { "@aws-sdk/client-iam": "^3.525.0", + "@aws-sdk/client-kms": "^3.609.0", "@aws-sdk/client-secrets-manager": "^3.504.0", "@aws-sdk/client-sts": "^3.600.0", "@casl/ability": "^6.5.0", @@ -125,8 +126,8 @@ "@peculiar/asn1-schema": "^2.3.8", "@peculiar/x509": "^1.10.0", "@serdnam/pino-cloudwatch-transport": "^1.0.4", - "@team-plain/typescript-sdk": "^4.6.1", "@sindresorhus/slugify": "1.1.0", + "@team-plain/typescript-sdk": "^4.6.1", "@ucast/mongo2js": "^1.3.4", "ajv": "^8.12.0", "argon2": "^0.31.2", @@ -148,6 +149,7 @@ "jmespath": "^0.16.0", "jsonwebtoken": "^9.0.2", "jsrp": "^0.2.4", + "jwks-rsa": "^3.1.0", "knex": "^3.0.1", "ldapjs": "^3.0.7", "libsodium-wrappers": "^0.7.13", diff --git a/backend/src/@types/fastify.d.ts b/backend/src/@types/fastify.d.ts index 7a5682b30..8ae892560 100644 --- a/backend/src/@types/fastify.d.ts +++ b/backend/src/@types/fastify.d.ts @@ -9,6 +9,7 @@ import { TAuditLogStreamServiceFactory } from "@app/ee/services/audit-log-stream import { TCertificateAuthorityCrlServiceFactory } from "@app/ee/services/certificate-authority-crl/certificate-authority-crl-service"; import { TDynamicSecretServiceFactory } from "@app/ee/services/dynamic-secret/dynamic-secret-service"; import { TDynamicSecretLeaseServiceFactory } from "@app/ee/services/dynamic-secret-lease/dynamic-secret-lease-service"; +import { TExternalKmsServiceFactory } from "@app/ee/services/external-kms/external-kms-service"; import { TGroupServiceFactory } from "@app/ee/services/group/group-service"; import { TIdentityProjectAdditionalPrivilegeServiceFactory } from "@app/ee/services/identity-project-additional-privilege/identity-project-additional-privilege-service"; import { TLdapConfigServiceFactory } from "@app/ee/services/ldap-config/ldap-config-service"; @@ -41,6 +42,7 @@ import { TIdentityAwsAuthServiceFactory } from "@app/services/identity-aws-auth/ import { TIdentityAzureAuthServiceFactory } from "@app/services/identity-azure-auth/identity-azure-auth-service"; import { TIdentityGcpAuthServiceFactory } from "@app/services/identity-gcp-auth/identity-gcp-auth-service"; import { TIdentityKubernetesAuthServiceFactory } from "@app/services/identity-kubernetes-auth/identity-kubernetes-auth-service"; +import { TIdentityOidcAuthServiceFactory } from "@app/services/identity-oidc-auth/identity-oidc-auth-service"; import { TIdentityProjectServiceFactory } from "@app/services/identity-project/identity-project-service"; import { TIdentityTokenAuthServiceFactory } from "@app/services/identity-token-auth/identity-token-auth-service"; import { TIdentityUaServiceFactory } from "@app/services/identity-ua/identity-ua-service"; @@ -135,6 +137,7 @@ declare module "fastify" { identityGcpAuth: TIdentityGcpAuthServiceFactory; identityAwsAuth: TIdentityAwsAuthServiceFactory; identityAzureAuth: TIdentityAzureAuthServiceFactory; + identityOidcAuth: TIdentityOidcAuthServiceFactory; accessApprovalPolicy: TAccessApprovalPolicyServiceFactory; accessApprovalRequest: TAccessApprovalRequestServiceFactory; secretApprovalPolicy: TSecretApprovalPolicyServiceFactory; @@ -161,6 +164,7 @@ declare module "fastify" { secretSharing: TSecretSharingServiceFactory; rateLimit: TRateLimitServiceFactory; userEngagement: TUserEngagementServiceFactory; + externalKms: TExternalKmsServiceFactory; }; // this is exclusive use for middlewares in which we need to inject data // everywhere else access using service layer diff --git a/backend/src/@types/knex.d.ts b/backend/src/@types/knex.d.ts index 9d54335bb..8aa00ec79 100644 --- a/backend/src/@types/knex.d.ts +++ b/backend/src/@types/knex.d.ts @@ -59,6 +59,9 @@ import { TDynamicSecrets, TDynamicSecretsInsert, TDynamicSecretsUpdate, + TExternalKms, + TExternalKmsInsert, + TExternalKmsUpdate, TGitAppInstallSessions, TGitAppInstallSessionsInsert, TGitAppInstallSessionsUpdate, @@ -92,6 +95,9 @@ import { TIdentityKubernetesAuths, TIdentityKubernetesAuthsInsert, TIdentityKubernetesAuthsUpdate, + TIdentityOidcAuths, + TIdentityOidcAuthsInsert, + TIdentityOidcAuthsUpdate, TIdentityOrgMemberships, TIdentityOrgMembershipsInsert, TIdentityOrgMembershipsUpdate, @@ -122,6 +128,9 @@ import { TIntegrations, TIntegrationsInsert, TIntegrationsUpdate, + TInternalKms, + TInternalKmsInsert, + TInternalKmsUpdate, TKmsKeys, TKmsKeysInsert, TKmsKeysUpdate, @@ -483,6 +492,11 @@ declare module "knex/types/tables" { TIdentityAzureAuthsInsert, TIdentityAzureAuthsUpdate >; + [TableName.IdentityOidcAuth]: KnexOriginal.CompositeTableType< + TIdentityOidcAuths, + TIdentityOidcAuthsInsert, + TIdentityOidcAuthsUpdate + >; [TableName.IdentityUaClientSecret]: KnexOriginal.CompositeTableType< TIdentityUaClientSecrets, TIdentityUaClientSecretsInsert, @@ -648,6 +662,8 @@ declare module "knex/types/tables" { TKmsRootConfigInsert, TKmsRootConfigUpdate >; + [TableName.InternalKms]: KnexOriginal.CompositeTableType; + [TableName.ExternalKms]: KnexOriginal.CompositeTableType; [TableName.KmsKey]: KnexOriginal.CompositeTableType; [TableName.KmsKeyVersion]: KnexOriginal.CompositeTableType< TKmsKeyVersions, diff --git a/backend/src/db/migrations/20240708100026_external-kms.ts b/backend/src/db/migrations/20240708100026_external-kms.ts new file mode 100644 index 000000000..63390a635 --- /dev/null +++ b/backend/src/db/migrations/20240708100026_external-kms.ts @@ -0,0 +1,256 @@ +import slugify from "@sindresorhus/slugify"; +import { Knex } from "knex"; + +import { alphaNumericNanoId } from "@app/lib/nanoid"; + +import { TableName } from "../schemas"; + +const createInternalKmsTableAndBackfillData = async (knex: Knex) => { + const doesOldKmsKeyTableExist = await knex.schema.hasTable(TableName.KmsKey); + const doesInternalKmsTableExist = await knex.schema.hasTable(TableName.InternalKms); + + // building the internal kms table by filling from old kms table + if (doesOldKmsKeyTableExist && !doesInternalKmsTableExist) { + await knex.schema.createTable(TableName.InternalKms, (tb) => { + tb.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid()); + tb.binary("encryptedKey").notNullable(); + tb.string("encryptionAlgorithm").notNullable(); + tb.integer("version").defaultTo(1).notNullable(); + tb.uuid("kmsKeyId").unique().notNullable(); + tb.foreign("kmsKeyId").references("id").inTable(TableName.KmsKey).onDelete("CASCADE"); + }); + + // copy the old kms and backfill + const oldKmsKey = await knex(TableName.KmsKey).select("version", "encryptedKey", "encryptionAlgorithm", "id"); + if (oldKmsKey.length) { + await knex(TableName.InternalKms).insert( + oldKmsKey.map((el) => ({ + encryptionAlgorithm: el.encryptionAlgorithm, + encryptedKey: el.encryptedKey, + kmsKeyId: el.id, + version: el.version + })) + ); + } + } +}; + +const renameKmsKeyVersionTableAsInternalKmsKeyVersion = async (knex: Knex) => { + const doesOldKmsKeyVersionTableExist = await knex.schema.hasTable(TableName.KmsKeyVersion); + const doesNewKmsKeyVersionTableExist = await knex.schema.hasTable(TableName.InternalKmsKeyVersion); + + if (doesOldKmsKeyVersionTableExist && !doesNewKmsKeyVersionTableExist) { + // because we haven't started using versioning for kms thus no data exist + await knex.schema.renameTable(TableName.KmsKeyVersion, TableName.InternalKmsKeyVersion); + const hasKmsKeyIdColumn = await knex.schema.hasColumn(TableName.InternalKmsKeyVersion, "kmsKeyId"); + const hasInternalKmsIdColumn = await knex.schema.hasColumn(TableName.InternalKmsKeyVersion, "internalKmsId"); + + await knex.schema.alterTable(TableName.InternalKmsKeyVersion, (tb) => { + if (hasKmsKeyIdColumn) tb.dropColumn("kmsKeyId"); + if (!hasInternalKmsIdColumn) { + tb.uuid("internalKmsId").notNullable(); + tb.foreign("internalKmsId").references("id").inTable(TableName.InternalKms).onDelete("CASCADE"); + } + }); + } +}; + +const createExternalKmsKeyTable = async (knex: Knex) => { + const doesExternalKmsServiceExist = await knex.schema.hasTable(TableName.ExternalKms); + if (!doesExternalKmsServiceExist) { + await knex.schema.createTable(TableName.ExternalKms, (tb) => { + tb.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid()); + tb.string("provider").notNullable(); + tb.binary("encryptedProviderInputs").notNullable(); + tb.string("status"); + tb.string("statusDetails"); + tb.uuid("kmsKeyId").unique().notNullable(); + tb.foreign("kmsKeyId").references("id").inTable(TableName.KmsKey).onDelete("CASCADE"); + }); + } +}; + +const removeNonRequiredFieldsFromKmsKeyTableAndBackfillRequiredData = async (knex: Knex) => { + const doesOldKmsKeyTableExist = await knex.schema.hasTable(TableName.KmsKey); + + // building the internal kms table by filling from old kms table + if (doesOldKmsKeyTableExist) { + const hasSlugColumn = await knex.schema.hasColumn(TableName.KmsKey, "slug"); + const hasEncryptedKeyColumn = await knex.schema.hasColumn(TableName.KmsKey, "encryptedKey"); + const hasEncryptionAlgorithmColumn = await knex.schema.hasColumn(TableName.KmsKey, "encryptionAlgorithm"); + const hasVersionColumn = await knex.schema.hasColumn(TableName.KmsKey, "version"); + const hasTimestamps = await knex.schema.hasColumn(TableName.KmsKey, "createdAt"); + const hasProjectId = await knex.schema.hasColumn(TableName.KmsKey, "projectId"); + const hasOrgId = await knex.schema.hasColumn(TableName.KmsKey, "orgId"); + + await knex.schema.alterTable(TableName.KmsKey, (tb) => { + if (!hasSlugColumn) tb.string("slug", 32); + if (hasEncryptedKeyColumn) tb.dropColumn("encryptedKey"); + if (hasEncryptionAlgorithmColumn) tb.dropColumn("encryptionAlgorithm"); + if (hasVersionColumn) tb.dropColumn("version"); + if (!hasTimestamps) tb.timestamps(true, true, true); + }); + + // backfill all org id in kms key because its gonna be changed to non nullable + if (hasProjectId && hasOrgId) { + await knex(TableName.KmsKey) + .whereNull("orgId") + .update({ + // eslint-disable-next-line + // @ts-ignore because generate schema happens after this + orgId: knex(TableName.Project) + .select("orgId") + .where("id", knex.raw("??", [`${TableName.KmsKey}.projectId`])) + }); + } + + // backfill slugs in kms + const missingSlugs = await knex(TableName.KmsKey).whereNull("slug").select("id"); + if (missingSlugs.length) { + await knex(TableName.KmsKey) + // eslint-disable-next-line + // @ts-ignore because generate schema happens after this + .insert(missingSlugs.map(({ id }) => ({ id, slug: slugify(alphaNumericNanoId(8).toLowerCase()) }))) + .onConflict("id") + .merge(); + } + + await knex.schema.alterTable(TableName.KmsKey, (tb) => { + if (hasOrgId) tb.uuid("orgId").notNullable().alter(); + tb.string("slug", 32).notNullable().alter(); + if (hasProjectId) tb.dropColumn("projectId"); + if (hasOrgId) tb.unique(["orgId", "slug"]); + }); + } +}; + +/* + * The goal for this migration is split the existing kms key into three table + * the kms-key table would be a container table that contains + * the internal kms key table and external kms table + */ +export async function up(knex: Knex): Promise { + await createInternalKmsTableAndBackfillData(knex); + await renameKmsKeyVersionTableAsInternalKmsKeyVersion(knex); + await removeNonRequiredFieldsFromKmsKeyTableAndBackfillRequiredData(knex); + await createExternalKmsKeyTable(knex); + + const doesOrgKmsKeyExist = await knex.schema.hasColumn(TableName.Organization, "kmsDefaultKeyId"); + if (!doesOrgKmsKeyExist) { + await knex.schema.alterTable(TableName.Organization, (tb) => { + tb.uuid("kmsDefaultKeyId").nullable(); + tb.foreign("kmsDefaultKeyId").references("id").inTable(TableName.KmsKey); + }); + } + + const doesProjectKmsSecretManagerKeyExist = await knex.schema.hasColumn(TableName.Project, "kmsSecretManagerKeyId"); + if (!doesProjectKmsSecretManagerKeyExist) { + await knex.schema.alterTable(TableName.Project, (tb) => { + tb.uuid("kmsSecretManagerKeyId").nullable(); + tb.foreign("kmsSecretManagerKeyId").references("id").inTable(TableName.KmsKey); + }); + } +} + +const renameInternalKmsKeyVersionBackToKmsKeyVersion = async (knex: Knex) => { + const doesInternalKmsKeyVersionTableExist = await knex.schema.hasTable(TableName.InternalKmsKeyVersion); + const doesKmsKeyVersionTableExist = await knex.schema.hasTable(TableName.KmsKeyVersion); + if (doesInternalKmsKeyVersionTableExist && !doesKmsKeyVersionTableExist) { + // because we haven't started using versioning for kms thus no data exist + await knex.schema.renameTable(TableName.InternalKmsKeyVersion, TableName.KmsKeyVersion); + const hasInternalKmsIdColumn = await knex.schema.hasColumn(TableName.KmsKeyVersion, "internalKmsId"); + const hasKmsKeyIdColumn = await knex.schema.hasColumn(TableName.KmsKeyVersion, "kmsKeyId"); + + await knex.schema.alterTable(TableName.KmsKeyVersion, (tb) => { + if (hasInternalKmsIdColumn) tb.dropColumn("internalKmsId"); + if (!hasKmsKeyIdColumn) { + tb.uuid("kmsKeyId").notNullable(); + tb.foreign("kmsKeyId").references("id").inTable(TableName.KmsKey).onDelete("CASCADE"); + } + }); + } +}; + +const bringBackKmsKeyFields = async (knex: Knex) => { + const doesOldKmsKeyTableExist = await knex.schema.hasTable(TableName.KmsKey); + const doesInternalKmsTableExist = await knex.schema.hasTable(TableName.InternalKms); + if (doesOldKmsKeyTableExist && doesInternalKmsTableExist) { + const hasSlug = await knex.schema.hasColumn(TableName.KmsKey, "slug"); + const hasEncryptedKeyColumn = await knex.schema.hasColumn(TableName.KmsKey, "encryptedKey"); + const hasEncryptionAlgorithmColumn = await knex.schema.hasColumn(TableName.KmsKey, "encryptionAlgorithm"); + const hasVersionColumn = await knex.schema.hasColumn(TableName.KmsKey, "version"); + const hasNullableOrgId = await knex.schema.hasColumn(TableName.KmsKey, "orgId"); + const hasProjectIdColumn = await knex.schema.hasColumn(TableName.KmsKey, "projectId"); + + await knex.schema.alterTable(TableName.KmsKey, (tb) => { + if (!hasEncryptedKeyColumn) tb.binary("encryptedKey"); + if (!hasEncryptionAlgorithmColumn) tb.string("encryptionAlgorithm"); + if (!hasVersionColumn) tb.integer("version").defaultTo(1); + if (hasNullableOrgId) tb.uuid("orgId").nullable().alter(); + if (!hasProjectIdColumn) { + tb.string("projectId"); + tb.foreign("projectId").references("id").inTable(TableName.Project).onDelete("CASCADE"); + } + if (hasSlug) tb.dropColumn("slug"); + }); + } +}; + +const backfillKmsKeyFromInternalKmsTable = async (knex: Knex) => { + const doesOldKmsKeyTableExist = await knex.schema.hasTable(TableName.KmsKey); + const doesInternalKmsTableExist = await knex.schema.hasTable(TableName.InternalKms); + if (doesInternalKmsTableExist && doesOldKmsKeyTableExist) { + // backfill kms key with internal kms data + await knex(TableName.KmsKey).update({ + // eslint-disable-next-line + // @ts-ignore because generate schema happens after this + encryptedKey: knex(TableName.InternalKms) + .select("encryptedKey") + .where("kmsKeyId", knex.raw("??", [`${TableName.KmsKey}.id`])), + // eslint-disable-next-line + // @ts-ignore because generate schema happens after this + encryptionAlgorithm: knex(TableName.InternalKms) + .select("encryptionAlgorithm") + .where("kmsKeyId", knex.raw("??", [`${TableName.KmsKey}.id`])), + // eslint-disable-next-line + // @ts-ignore because generate schema happens after this + projectId: knex(TableName.Project) + .select("id") + .where("kmsCertificateKeyId", knex.raw("??", [`${TableName.KmsKey}.id`])) + }); + } +}; + +export async function down(knex: Knex): Promise { + const doesOrgKmsKeyExist = await knex.schema.hasColumn(TableName.Organization, "kmsDefaultKeyId"); + if (doesOrgKmsKeyExist) { + await knex.schema.alterTable(TableName.Organization, (tb) => { + tb.dropColumn("kmsDefaultKeyId"); + }); + } + + const doesProjectKmsSecretManagerKeyExist = await knex.schema.hasColumn(TableName.Project, "kmsSecretManagerKeyId"); + if (doesProjectKmsSecretManagerKeyExist) { + await knex.schema.alterTable(TableName.Project, (tb) => { + tb.dropColumn("kmsSecretManagerKeyId"); + }); + } + + await renameInternalKmsKeyVersionBackToKmsKeyVersion(knex); + await bringBackKmsKeyFields(knex); + await backfillKmsKeyFromInternalKmsTable(knex); + + const doesOldKmsKeyTableExist = await knex.schema.hasTable(TableName.KmsKey); + if (doesOldKmsKeyTableExist) { + await knex.schema.alterTable(TableName.KmsKey, (tb) => { + tb.binary("encryptedKey").notNullable().alter(); + tb.string("encryptionAlgorithm").notNullable().alter(); + }); + } + + const doesInternalKmsTableExist = await knex.schema.hasTable(TableName.InternalKms); + if (doesInternalKmsTableExist) await knex.schema.dropTable(TableName.InternalKms); + + const doesExternalKmsServiceExist = await knex.schema.hasTable(TableName.ExternalKms); + if (doesExternalKmsServiceExist) await knex.schema.dropTable(TableName.ExternalKms); +} diff --git a/backend/src/db/migrations/20240710045107_identity-oidc-auth.ts b/backend/src/db/migrations/20240710045107_identity-oidc-auth.ts new file mode 100644 index 000000000..fbf5db2a0 --- /dev/null +++ b/backend/src/db/migrations/20240710045107_identity-oidc-auth.ts @@ -0,0 +1,34 @@ +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.IdentityOidcAuth))) { + await knex.schema.createTable(TableName.IdentityOidcAuth, (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.uuid("identityId").notNullable().unique(); + t.foreign("identityId").references("id").inTable(TableName.Identity).onDelete("CASCADE"); + t.string("oidcDiscoveryUrl").notNullable(); + t.text("encryptedCaCert").notNullable(); + t.string("caCertIV").notNullable(); + t.string("caCertTag").notNullable(); + t.string("boundIssuer").notNullable(); + t.string("boundAudiences").notNullable(); + t.jsonb("boundClaims").notNullable(); + t.string("boundSubject"); + t.timestamps(true, true, true); + }); + + await createOnUpdateTrigger(knex, TableName.IdentityOidcAuth); + } +} + +export async function down(knex: Knex): Promise { + await knex.schema.dropTableIfExists(TableName.IdentityOidcAuth); + await dropOnUpdateTrigger(knex, TableName.IdentityOidcAuth); +} diff --git a/backend/src/db/migrations/20240715113110_org-membership-active-status.ts b/backend/src/db/migrations/20240715113110_org-membership-active-status.ts new file mode 100644 index 000000000..ddb8794db --- /dev/null +++ b/backend/src/db/migrations/20240715113110_org-membership-active-status.ts @@ -0,0 +1,25 @@ +import { Knex } from "knex"; + +import { TableName } from "../schemas"; + +export async function up(knex: Knex): Promise { + if (await knex.schema.hasTable(TableName.OrgMembership)) { + const doesUserIdExist = await knex.schema.hasColumn(TableName.OrgMembership, "userId"); + const doesOrgIdExist = await knex.schema.hasColumn(TableName.OrgMembership, "orgId"); + await knex.schema.alterTable(TableName.OrgMembership, (t) => { + t.boolean("isActive").notNullable().defaultTo(true); + if (doesUserIdExist && doesOrgIdExist) t.index(["userId", "orgId"]); + }); + } +} + +export async function down(knex: Knex): Promise { + if (await knex.schema.hasTable(TableName.OrgMembership)) { + const doesUserIdExist = await knex.schema.hasColumn(TableName.OrgMembership, "userId"); + const doesOrgIdExist = await knex.schema.hasColumn(TableName.OrgMembership, "orgId"); + await knex.schema.alterTable(TableName.OrgMembership, (t) => { + t.dropColumn("isActive"); + if (doesUserIdExist && doesOrgIdExist) t.dropIndex(["userId", "orgId"]); + }); + } +} diff --git a/backend/src/db/migrations/20240717184929_add-enforcement-level-secrets-policies.ts b/backend/src/db/migrations/20240717184929_add-enforcement-level-secrets-policies.ts new file mode 100644 index 000000000..ee668d145 --- /dev/null +++ b/backend/src/db/migrations/20240717184929_add-enforcement-level-secrets-policies.ts @@ -0,0 +1,23 @@ +import { Knex } from "knex"; + +import { EnforcementLevel } from "@app/lib/types"; + +import { TableName } from "../schemas"; + +export async function up(knex: Knex): Promise { + const hasColumn = await knex.schema.hasColumn(TableName.SecretApprovalPolicy, "enforcementLevel"); + if (!hasColumn) { + await knex.schema.table(TableName.SecretApprovalPolicy, (table) => { + table.string("enforcementLevel", 10).notNullable().defaultTo(EnforcementLevel.Hard); + }); + } +} + +export async function down(knex: Knex): Promise { + const hasColumn = await knex.schema.hasColumn(TableName.SecretApprovalPolicy, "enforcementLevel"); + if (hasColumn) { + await knex.schema.table(TableName.SecretApprovalPolicy, (table) => { + table.dropColumn("enforcementLevel"); + }); + } +} diff --git a/backend/src/db/migrations/20240717194958_add-enforcement-level-access-policies.ts b/backend/src/db/migrations/20240717194958_add-enforcement-level-access-policies.ts new file mode 100644 index 000000000..83ef07201 --- /dev/null +++ b/backend/src/db/migrations/20240717194958_add-enforcement-level-access-policies.ts @@ -0,0 +1,23 @@ +import { Knex } from "knex"; + +import { EnforcementLevel } from "@app/lib/types"; + +import { TableName } from "../schemas"; + +export async function up(knex: Knex): Promise { + const hasColumn = await knex.schema.hasColumn(TableName.AccessApprovalPolicy, "enforcementLevel"); + if (!hasColumn) { + await knex.schema.table(TableName.AccessApprovalPolicy, (table) => { + table.string("enforcementLevel", 10).notNullable().defaultTo(EnforcementLevel.Hard); + }); + } +} + +export async function down(knex: Knex): Promise { + const hasColumn = await knex.schema.hasColumn(TableName.AccessApprovalPolicy, "enforcementLevel"); + if (hasColumn) { + await knex.schema.table(TableName.AccessApprovalPolicy, (table) => { + table.dropColumn("enforcementLevel"); + }); + } +} diff --git a/backend/src/db/migrations/20240718170955_add-access-secret-sharing.ts b/backend/src/db/migrations/20240718170955_add-access-secret-sharing.ts new file mode 100644 index 000000000..705c8d986 --- /dev/null +++ b/backend/src/db/migrations/20240718170955_add-access-secret-sharing.ts @@ -0,0 +1,23 @@ +import { Knex } from "knex"; + +import { SecretSharingAccessType } from "@app/lib/types"; + +import { TableName } from "../schemas"; + +export async function up(knex: Knex): Promise { + const hasColumn = await knex.schema.hasColumn(TableName.SecretSharing, "accessType"); + if (!hasColumn) { + await knex.schema.table(TableName.SecretSharing, (table) => { + table.string("accessType").notNullable().defaultTo(SecretSharingAccessType.Anyone); + }); + } +} + +export async function down(knex: Knex): Promise { + const hasColumn = await knex.schema.hasColumn(TableName.SecretSharing, "accessType"); + if (hasColumn) { + await knex.schema.table(TableName.SecretSharing, (table) => { + table.dropColumn("accessType"); + }); + } +} diff --git a/backend/src/db/migrations/20240719182539_add-bypass-reason-secret-approval-requets.ts b/backend/src/db/migrations/20240719182539_add-bypass-reason-secret-approval-requets.ts new file mode 100644 index 000000000..6b688dbc3 --- /dev/null +++ b/backend/src/db/migrations/20240719182539_add-bypass-reason-secret-approval-requets.ts @@ -0,0 +1,21 @@ +import { Knex } from "knex"; + +import { TableName } from "../schemas"; + +export async function up(knex: Knex): Promise { + const hasColumn = await knex.schema.hasColumn(TableName.SecretApprovalRequest, "bypassReason"); + if (!hasColumn) { + await knex.schema.table(TableName.SecretApprovalRequest, (table) => { + table.string("bypassReason").nullable(); + }); + } +} + +export async function down(knex: Knex): Promise { + const hasColumn = await knex.schema.hasColumn(TableName.SecretApprovalRequest, "bypassReason"); + if (hasColumn) { + await knex.schema.table(TableName.SecretApprovalRequest, (table) => { + table.dropColumn("bypassReason"); + }); + } +} diff --git a/backend/src/db/schemas/access-approval-policies.ts b/backend/src/db/schemas/access-approval-policies.ts index 69068d23b..c05f22b31 100644 --- a/backend/src/db/schemas/access-approval-policies.ts +++ b/backend/src/db/schemas/access-approval-policies.ts @@ -5,6 +5,8 @@ import { z } from "zod"; +import { EnforcementLevel } from "@app/lib/types"; + import { TImmutableDBKeys } from "./models"; export const AccessApprovalPoliciesSchema = z.object({ @@ -14,7 +16,8 @@ export const AccessApprovalPoliciesSchema = z.object({ secretPath: z.string().nullable().optional(), envId: z.string().uuid(), createdAt: z.date(), - updatedAt: z.date() + updatedAt: z.date(), + enforcementLevel: z.nativeEnum(EnforcementLevel).default(EnforcementLevel.Hard) }); export type TAccessApprovalPolicies = z.infer; diff --git a/backend/src/db/schemas/external-kms.ts b/backend/src/db/schemas/external-kms.ts new file mode 100644 index 000000000..810c3f70f --- /dev/null +++ b/backend/src/db/schemas/external-kms.ts @@ -0,0 +1,23 @@ +// 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 { zodBuffer } from "@app/lib/zod"; + +import { TImmutableDBKeys } from "./models"; + +export const ExternalKmsSchema = z.object({ + id: z.string().uuid(), + provider: z.string(), + encryptedProviderInputs: zodBuffer, + status: z.string().nullable().optional(), + statusDetails: z.string().nullable().optional(), + kmsKeyId: z.string().uuid() +}); + +export type TExternalKms = z.infer; +export type TExternalKmsInsert = Omit, TImmutableDBKeys>; +export type TExternalKmsUpdate = Partial, TImmutableDBKeys>>; diff --git a/backend/src/db/schemas/identity-oidc-auths.ts b/backend/src/db/schemas/identity-oidc-auths.ts new file mode 100644 index 000000000..3d7d38c41 --- /dev/null +++ b/backend/src/db/schemas/identity-oidc-auths.ts @@ -0,0 +1,31 @@ +// 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 IdentityOidcAuthsSchema = 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(), + identityId: z.string().uuid(), + oidcDiscoveryUrl: z.string(), + encryptedCaCert: z.string(), + caCertIV: z.string(), + caCertTag: z.string(), + boundIssuer: z.string(), + boundAudiences: z.string(), + boundClaims: z.unknown(), + boundSubject: z.string().nullable().optional(), + createdAt: z.date(), + updatedAt: z.date() +}); + +export type TIdentityOidcAuths = z.infer; +export type TIdentityOidcAuthsInsert = Omit, TImmutableDBKeys>; +export type TIdentityOidcAuthsUpdate = Partial, TImmutableDBKeys>>; diff --git a/backend/src/db/schemas/index.ts b/backend/src/db/schemas/index.ts index bce99dfea..06c735cee 100644 --- a/backend/src/db/schemas/index.ts +++ b/backend/src/db/schemas/index.ts @@ -17,6 +17,7 @@ export * from "./certificate-secrets"; export * from "./certificates"; export * from "./dynamic-secret-leases"; export * from "./dynamic-secrets"; +export * from "./external-kms"; export * from "./git-app-install-sessions"; export * from "./git-app-org"; export * from "./group-project-membership-roles"; @@ -28,6 +29,7 @@ export * from "./identity-aws-auths"; export * from "./identity-azure-auths"; export * from "./identity-gcp-auths"; export * from "./identity-kubernetes-auths"; +export * from "./identity-oidc-auths"; export * from "./identity-org-memberships"; export * from "./identity-project-additional-privilege"; export * from "./identity-project-membership-role"; @@ -38,6 +40,7 @@ export * from "./identity-universal-auths"; export * from "./incident-contacts"; export * from "./integration-auths"; export * from "./integrations"; +export * from "./internal-kms"; export * from "./kms-key-versions"; export * from "./kms-keys"; export * from "./kms-root-config"; diff --git a/backend/src/db/schemas/internal-kms-key-version.ts b/backend/src/db/schemas/internal-kms-key-version.ts new file mode 100644 index 000000000..fc1e3c3db --- /dev/null +++ b/backend/src/db/schemas/internal-kms-key-version.ts @@ -0,0 +1,21 @@ +// Code generated by automation script, DO NOT EDIT. +// Automated by pulling database and generating zod schema +// To update. Just run npm run generate:schema +// Written by akhilmhdh. + +import { z } from "zod"; + +import { zodBuffer } from "@app/lib/zod"; + +import { TImmutableDBKeys } from "./models"; + +export const InternalKmsKeyVersionSchema = z.object({ + id: z.string().uuid(), + encryptedKey: zodBuffer, + version: z.number(), + internalKmsId: z.string().uuid() +}); + +export type TInternalKmsKeyVersion = z.infer; +export type TInternalKmsKeyVersionInsert = Omit, TImmutableDBKeys>; +export type TInternalKmsKeyVersionUpdate = Partial, TImmutableDBKeys>>; diff --git a/backend/src/db/schemas/internal-kms.ts b/backend/src/db/schemas/internal-kms.ts new file mode 100644 index 000000000..38e64dc5b --- /dev/null +++ b/backend/src/db/schemas/internal-kms.ts @@ -0,0 +1,22 @@ +// 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 { zodBuffer } from "@app/lib/zod"; + +import { TImmutableDBKeys } from "./models"; + +export const InternalKmsSchema = z.object({ + id: z.string().uuid(), + encryptedKey: zodBuffer, + encryptionAlgorithm: z.string(), + version: z.number().default(1), + kmsKeyId: z.string().uuid() +}); + +export type TInternalKms = z.infer; +export type TInternalKmsInsert = Omit, TImmutableDBKeys>; +export type TInternalKmsUpdate = Partial, TImmutableDBKeys>>; diff --git a/backend/src/db/schemas/kms-keys.ts b/backend/src/db/schemas/kms-keys.ts index 503c270d9..99df71f8d 100644 --- a/backend/src/db/schemas/kms-keys.ts +++ b/backend/src/db/schemas/kms-keys.ts @@ -5,20 +5,17 @@ import { z } from "zod"; -import { zodBuffer } from "@app/lib/zod"; - import { TImmutableDBKeys } from "./models"; export const KmsKeysSchema = z.object({ id: z.string().uuid(), - encryptedKey: zodBuffer, - encryptionAlgorithm: z.string(), - version: z.number().default(1), description: z.string().nullable().optional(), isDisabled: z.boolean().default(false).nullable().optional(), isReserved: z.boolean().default(true).nullable().optional(), - projectId: z.string().nullable().optional(), - orgId: z.string().uuid().nullable().optional() + orgId: z.string().uuid(), + createdAt: z.date(), + updatedAt: z.date(), + slug: z.string() }); export type TKmsKeys = z.infer; diff --git a/backend/src/db/schemas/models.ts b/backend/src/db/schemas/models.ts index 1dba71209..5747fb381 100644 --- a/backend/src/db/schemas/models.ts +++ b/backend/src/db/schemas/models.ts @@ -60,6 +60,7 @@ export enum TableName { IdentityAzureAuth = "identity_azure_auths", IdentityUaClientSecret = "identity_ua_client_secrets", IdentityAwsAuth = "identity_aws_auths", + IdentityOidcAuth = "identity_oidc_auths", IdentityOrgMembership = "identity_org_memberships", IdentityProjectMembership = "identity_project_memberships", IdentityProjectMembershipRole = "identity_project_membership_role", @@ -95,6 +96,10 @@ export enum TableName { // KMS Service KmsServerRootConfig = "kms_root_config", KmsKey = "kms_keys", + ExternalKms = "external_kms", + InternalKms = "internal_kms", + InternalKmsKeyVersion = "internal_kms_key_version", + // @depreciated KmsKeyVersion = "kms_key_versions" } @@ -167,5 +172,6 @@ export enum IdentityAuthMethod { KUBERNETES_AUTH = "kubernetes-auth", GCP_AUTH = "gcp-auth", AWS_AUTH = "aws-auth", - AZURE_AUTH = "azure-auth" + AZURE_AUTH = "azure-auth", + OIDC_AUTH = "oidc-auth" } diff --git a/backend/src/db/schemas/org-memberships.ts b/backend/src/db/schemas/org-memberships.ts index b1858e5be..7fc6f46eb 100644 --- a/backend/src/db/schemas/org-memberships.ts +++ b/backend/src/db/schemas/org-memberships.ts @@ -17,7 +17,8 @@ export const OrgMembershipsSchema = z.object({ userId: z.string().uuid().nullable().optional(), orgId: z.string().uuid(), roleId: z.string().uuid().nullable().optional(), - projectFavorites: z.string().array().nullable().optional() + projectFavorites: z.string().array().nullable().optional(), + isActive: z.boolean() }); export type TOrgMemberships = z.infer; diff --git a/backend/src/db/schemas/organizations.ts b/backend/src/db/schemas/organizations.ts index f2933af86..7b7a004fc 100644 --- a/backend/src/db/schemas/organizations.ts +++ b/backend/src/db/schemas/organizations.ts @@ -15,7 +15,8 @@ export const OrganizationsSchema = z.object({ createdAt: z.date(), updatedAt: z.date(), authEnforced: z.boolean().default(false).nullable().optional(), - scimEnabled: z.boolean().default(false).nullable().optional() + scimEnabled: z.boolean().default(false).nullable().optional(), + kmsDefaultKeyId: z.string().uuid().nullable().optional() }); export type TOrganizations = z.infer; diff --git a/backend/src/db/schemas/projects.ts b/backend/src/db/schemas/projects.ts index f776e864c..19597c9df 100644 --- a/backend/src/db/schemas/projects.ts +++ b/backend/src/db/schemas/projects.ts @@ -19,7 +19,8 @@ export const ProjectsSchema = z.object({ upgradeStatus: z.string().nullable().optional(), pitVersionLimit: z.number().default(10), kmsCertificateKeyId: z.string().uuid().nullable().optional(), - auditLogsRetentionDays: z.number().nullable().optional() + auditLogsRetentionDays: z.number().nullable().optional(), + kmsSecretManagerKeyId: z.string().uuid().nullable().optional() }); export type TProjects = z.infer; diff --git a/backend/src/db/schemas/secret-approval-policies.ts b/backend/src/db/schemas/secret-approval-policies.ts index d907ef1e0..94aeba050 100644 --- a/backend/src/db/schemas/secret-approval-policies.ts +++ b/backend/src/db/schemas/secret-approval-policies.ts @@ -14,7 +14,8 @@ export const SecretApprovalPoliciesSchema = z.object({ approvals: z.number().default(1), envId: z.string().uuid(), createdAt: z.date(), - updatedAt: z.date() + updatedAt: z.date(), + enforcementLevel: z.string().default("hard") }); export type TSecretApprovalPolicies = z.infer; diff --git a/backend/src/db/schemas/secret-approval-requests.ts b/backend/src/db/schemas/secret-approval-requests.ts index aa6896d10..7ca0b71d9 100644 --- a/backend/src/db/schemas/secret-approval-requests.ts +++ b/backend/src/db/schemas/secret-approval-requests.ts @@ -15,6 +15,7 @@ export const SecretApprovalRequestsSchema = z.object({ conflicts: z.unknown().nullable().optional(), slug: z.string(), folderId: z.string().uuid(), + bypassReason: z.string().nullable().optional(), createdAt: z.date(), updatedAt: z.date(), isReplicated: z.boolean().nullable().optional(), diff --git a/backend/src/db/schemas/secret-sharing.ts b/backend/src/db/schemas/secret-sharing.ts index c8d938861..4406ad493 100644 --- a/backend/src/db/schemas/secret-sharing.ts +++ b/backend/src/db/schemas/secret-sharing.ts @@ -5,6 +5,8 @@ import { z } from "zod"; +import { SecretSharingAccessType } from "@app/lib/types"; + import { TImmutableDBKeys } from "./models"; export const SecretSharingSchema = z.object({ @@ -16,6 +18,7 @@ export const SecretSharingSchema = z.object({ expiresAt: z.date(), userId: z.string().uuid().nullable().optional(), orgId: z.string().uuid().nullable().optional(), + accessType: z.nativeEnum(SecretSharingAccessType).default(SecretSharingAccessType.Organization), createdAt: z.date(), updatedAt: z.date(), expiresAfterViews: z.number().nullable().optional() diff --git a/backend/src/db/seeds/2-org.ts b/backend/src/db/seeds/2-org.ts index ba2f65a36..a02224dbc 100644 --- a/backend/src/db/seeds/2-org.ts +++ b/backend/src/db/seeds/2-org.ts @@ -29,7 +29,8 @@ export async function seed(knex: Knex): Promise { role: OrgMembershipRole.Admin, orgId: org.id, status: OrgMembershipStatus.Accepted, - userId: user.id + userId: user.id, + isActive: true } ]); } diff --git a/backend/src/ee/routes/v1/access-approval-policy-router.ts b/backend/src/ee/routes/v1/access-approval-policy-router.ts index 3b8949d3b..8a3f93109 100644 --- a/backend/src/ee/routes/v1/access-approval-policy-router.ts +++ b/backend/src/ee/routes/v1/access-approval-policy-router.ts @@ -1,6 +1,7 @@ import { nanoid } from "nanoid"; import { z } from "zod"; +import { EnforcementLevel } from "@app/lib/types"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { sapPubSchema } from "@app/server/routes/sanitizedSchemas"; import { AuthMode } from "@app/services/auth/auth-type"; @@ -17,7 +18,8 @@ export const registerAccessApprovalPolicyRouter = async (server: FastifyZodProvi secretPath: z.string().trim().default("/"), environment: z.string(), approvers: z.string().array().min(1), - approvals: z.number().min(1).default(1) + approvals: z.number().min(1).default(1), + enforcementLevel: z.nativeEnum(EnforcementLevel).default(EnforcementLevel.Hard) }) .refine((data) => data.approvals <= data.approvers.length, { path: ["approvals"], @@ -38,7 +40,8 @@ export const registerAccessApprovalPolicyRouter = async (server: FastifyZodProvi actorOrgId: req.permission.orgId, ...req.body, projectSlug: req.body.projectSlug, - name: req.body.name ?? `${req.body.environment}-${nanoid(3)}` + name: req.body.name ?? `${req.body.environment}-${nanoid(3)}`, + enforcementLevel: req.body.enforcementLevel }); return { approval }; } @@ -115,7 +118,8 @@ export const registerAccessApprovalPolicyRouter = async (server: FastifyZodProvi .optional() .transform((val) => (val === "" ? "/" : val)), approvers: z.string().array().min(1), - approvals: z.number().min(1).default(1) + approvals: z.number().min(1).default(1), + enforcementLevel: z.nativeEnum(EnforcementLevel).default(EnforcementLevel.Hard) }) .refine((data) => data.approvals <= data.approvers.length, { path: ["approvals"], diff --git a/backend/src/ee/routes/v1/access-approval-request-router.ts b/backend/src/ee/routes/v1/access-approval-request-router.ts index 4b173cfa7..972289b04 100644 --- a/backend/src/ee/routes/v1/access-approval-request-router.ts +++ b/backend/src/ee/routes/v1/access-approval-request-router.ts @@ -99,7 +99,8 @@ export const registerAccessApprovalRequestRouter = async (server: FastifyZodProv approvals: z.number(), approvers: z.string().array(), secretPath: z.string().nullish(), - envId: z.string() + envId: z.string(), + enforcementLevel: z.string() }), reviewers: z .object({ diff --git a/backend/src/ee/routes/v1/external-kms-router.ts b/backend/src/ee/routes/v1/external-kms-router.ts new file mode 100644 index 000000000..712b8b456 --- /dev/null +++ b/backend/src/ee/routes/v1/external-kms-router.ts @@ -0,0 +1,190 @@ +import { z } from "zod"; + +import { ExternalKmsSchema, KmsKeysSchema } from "@app/db/schemas"; +import { + ExternalKmsAwsSchema, + ExternalKmsInputSchema, + ExternalKmsInputUpdateSchema +} from "@app/ee/services/external-kms/providers/model"; +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"; + +const sanitizedExternalSchema = KmsKeysSchema.extend({ + external: ExternalKmsSchema.pick({ + id: true, + status: true, + statusDetails: true, + provider: true + }) +}); + +const sanitizedExternalSchemaForGetById = KmsKeysSchema.extend({ + external: ExternalKmsSchema.pick({ + id: true, + status: true, + statusDetails: true, + provider: true + }).extend({ + providerInput: ExternalKmsAwsSchema + }) +}); + +export const registerExternalKmsRouter = async (server: FastifyZodProvider) => { + server.route({ + method: "POST", + url: "/", + config: { + rateLimit: writeLimit + }, + schema: { + body: z.object({ + slug: z.string().min(1).trim().toLowerCase().optional(), + description: z.string().min(1).trim().optional(), + provider: ExternalKmsInputSchema + }), + response: { + 200: z.object({ + externalKms: sanitizedExternalSchema + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const externalKms = await server.services.externalKms.create({ + actor: req.permission.type, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + slug: req.body.slug, + provider: req.body.provider, + description: req.body.description + }); + return { externalKms }; + } + }); + + server.route({ + method: "PATCH", + url: "/:id", + config: { + rateLimit: writeLimit + }, + schema: { + params: z.object({ + id: z.string().trim().min(1) + }), + body: z.object({ + slug: z.string().min(1).trim().toLowerCase().optional(), + description: z.string().min(1).trim().optional(), + provider: ExternalKmsInputUpdateSchema + }), + response: { + 200: z.object({ + externalKms: sanitizedExternalSchema + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const externalKms = await server.services.externalKms.updateById({ + actor: req.permission.type, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + slug: req.body.slug, + provider: req.body.provider, + description: req.body.description, + id: req.params.id + }); + return { externalKms }; + } + }); + + server.route({ + method: "DELETE", + url: "/:id", + config: { + rateLimit: writeLimit + }, + schema: { + params: z.object({ + id: z.string().trim().min(1) + }), + response: { + 200: z.object({ + externalKms: sanitizedExternalSchema + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const externalKms = await server.services.externalKms.deleteById({ + actor: req.permission.type, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + id: req.params.id + }); + return { externalKms }; + } + }); + + server.route({ + method: "GET", + url: "/:id", + config: { + rateLimit: readLimit + }, + schema: { + params: z.object({ + id: z.string().trim().min(1) + }), + response: { + 200: z.object({ + externalKms: sanitizedExternalSchemaForGetById + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const externalKms = await server.services.externalKms.findById({ + actor: req.permission.type, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + id: req.params.id + }); + return { externalKms }; + } + }); + + server.route({ + method: "GET", + url: "/slug/:slug", + config: { + rateLimit: readLimit + }, + schema: { + params: z.object({ + slug: z.string().trim().min(1) + }), + response: { + 200: z.object({ + externalKms: sanitizedExternalSchemaForGetById + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const externalKms = await server.services.externalKms.findBySlug({ + actor: req.permission.type, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + slug: req.params.slug + }); + return { externalKms }; + } + }); +}; diff --git a/backend/src/ee/routes/v1/scim-router.ts b/backend/src/ee/routes/v1/scim-router.ts index 0a45486ef..e3456c70a 100644 --- a/backend/src/ee/routes/v1/scim-router.ts +++ b/backend/src/ee/routes/v1/scim-router.ts @@ -186,7 +186,13 @@ export const registerScimRouter = async (server: FastifyZodProvider) => { }) ), displayName: z.string().trim(), - active: z.boolean() + active: z.boolean(), + groups: z.array( + z.object({ + value: z.string().trim(), + display: z.string().trim() + }) + ) }) } }, @@ -344,7 +350,12 @@ export const registerScimRouter = async (server: FastifyZodProvider) => { schemas: z.array(z.string()), id: z.string().trim(), displayName: z.string().trim(), - members: z.array(z.any()).length(0), + members: z.array( + z.object({ + value: z.string(), + display: z.string() + }) + ), meta: z.object({ resourceType: z.string().trim() }) @@ -417,7 +428,7 @@ export const registerScimRouter = async (server: FastifyZodProvider) => { displayName: z.string().trim(), members: z.array( z.object({ - value: z.string(), // infisical orgMembershipId + value: z.string(), display: z.string() }) ) @@ -475,10 +486,13 @@ export const registerScimRouter = async (server: FastifyZodProvider) => { }), z.object({ op: z.literal("add"), - value: z.object({ - value: z.string().trim(), - display: z.string().trim().optional() - }) + path: z.string().trim(), + value: z.array( + z.object({ + value: z.string().trim(), + display: z.string().trim().optional() + }) + ) }) ]) ) @@ -569,7 +583,13 @@ export const registerScimRouter = async (server: FastifyZodProvider) => { }) ), displayName: z.string().trim(), - active: z.boolean() + active: z.boolean(), + groups: z.array( + z.object({ + value: z.string().trim(), + display: z.string().trim() + }) + ) }) } }, diff --git a/backend/src/ee/routes/v1/secret-approval-policy-router.ts b/backend/src/ee/routes/v1/secret-approval-policy-router.ts index ee10131dd..25c1bb0b5 100644 --- a/backend/src/ee/routes/v1/secret-approval-policy-router.ts +++ b/backend/src/ee/routes/v1/secret-approval-policy-router.ts @@ -2,6 +2,7 @@ import { nanoid } from "nanoid"; import { z } from "zod"; import { removeTrailingSlash } from "@app/lib/fn"; +import { EnforcementLevel } from "@app/lib/types"; import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { sapPubSchema } from "@app/server/routes/sanitizedSchemas"; @@ -24,11 +25,13 @@ export const registerSecretApprovalPolicyRouter = async (server: FastifyZodProvi .string() .optional() .nullable() + .default("/") .transform((val) => (val ? removeTrailingSlash(val) : val)), - approverUserIds: z.string().array().min(1), - approvals: z.number().min(1).default(1) + approvers: z.string().array().min(1), + approvals: z.number().min(1).default(1), + enforcementLevel: z.nativeEnum(EnforcementLevel).default(EnforcementLevel.Hard) }) - .refine((data) => data.approvals <= data.approverUserIds.length, { + .refine((data) => data.approvals <= data.approvers.length, { path: ["approvals"], message: "The number of approvals should be lower than the number of approvers." }), @@ -47,7 +50,8 @@ export const registerSecretApprovalPolicyRouter = async (server: FastifyZodProvi actorOrgId: req.permission.orgId, projectId: req.body.workspaceId, ...req.body, - name: req.body.name ?? `${req.body.environment}-${nanoid(3)}` + name: req.body.name ?? `${req.body.environment}-${nanoid(3)}`, + enforcementLevel: req.body.enforcementLevel }); return { approval }; } @@ -66,15 +70,17 @@ export const registerSecretApprovalPolicyRouter = async (server: FastifyZodProvi body: z .object({ name: z.string().optional(), - approverUserIds: z.string().array().min(1), + approvers: z.string().array().min(1), approvals: z.number().min(1).default(1), secretPath: z .string() .optional() .nullable() .transform((val) => (val ? removeTrailingSlash(val) : val)) + .transform((val) => (val === "" ? "/" : val)), + enforcementLevel: z.nativeEnum(EnforcementLevel).optional() }) - .refine((data) => data.approvals <= data.approverUserIds.length, { + .refine((data) => data.approvals <= data.approvers.length, { path: ["approvals"], message: "The number of approvals should be lower than the number of approvers." }), diff --git a/backend/src/ee/routes/v1/secret-approval-request-router.ts b/backend/src/ee/routes/v1/secret-approval-request-router.ts index 8e72597bd..0ba7099a3 100644 --- a/backend/src/ee/routes/v1/secret-approval-request-router.ts +++ b/backend/src/ee/routes/v1/secret-approval-request-router.ts @@ -49,7 +49,8 @@ export const registerSecretApprovalRequestRouter = async (server: FastifyZodProv name: z.string(), approvals: z.number(), approvers: z.string().array(), - secretPath: z.string().optional().nullable() + secretPath: z.string().optional().nullable(), + enforcementLevel: z.string() }), committerUser: approvalRequestUser, commits: z.object({ op: z.string(), secretId: z.string().nullable().optional() }).array(), @@ -116,6 +117,9 @@ export const registerSecretApprovalRequestRouter = async (server: FastifyZodProv params: z.object({ id: z.string() }), + body: z.object({ + bypassReason: z.string().optional() + }), response: { 200: z.object({ approval: SecretApprovalRequestsSchema @@ -129,7 +133,8 @@ export const registerSecretApprovalRequestRouter = async (server: FastifyZodProv actor: req.permission.type, actorAuthMethod: req.permission.authMethod, actorOrgId: req.permission.orgId, - approvalId: req.params.id + approvalId: req.params.id, + bypassReason: req.body.bypassReason }); return { approval }; } @@ -248,7 +253,8 @@ export const registerSecretApprovalRequestRouter = async (server: FastifyZodProv name: z.string(), approvals: z.number(), approvers: approvalRequestUser.array(), - secretPath: z.string().optional().nullable() + secretPath: z.string().optional().nullable(), + enforcementLevel: z.string() }), environment: z.string(), statusChangedByUser: approvalRequestUser.optional(), diff --git a/backend/src/ee/services/access-approval-policy/access-approval-policy-service.ts b/backend/src/ee/services/access-approval-policy/access-approval-policy-service.ts index 51a51abb5..c91fbf970 100644 --- a/backend/src/ee/services/access-approval-policy/access-approval-policy-service.ts +++ b/backend/src/ee/services/access-approval-policy/access-approval-policy-service.ts @@ -47,7 +47,8 @@ export const accessApprovalPolicyServiceFactory = ({ approvals, approvers, projectSlug, - environment + environment, + enforcementLevel }: TCreateAccessApprovalPolicy) => { const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId); if (!project) throw new BadRequestError({ message: "Project not found" }); @@ -94,7 +95,8 @@ export const accessApprovalPolicyServiceFactory = ({ envId: env.id, approvals, secretPath, - name + name, + enforcementLevel }, tx ); @@ -143,7 +145,8 @@ export const accessApprovalPolicyServiceFactory = ({ actor, actorOrgId, actorAuthMethod, - approvals + approvals, + enforcementLevel }: TUpdateAccessApprovalPolicy) => { const accessApprovalPolicy = await accessApprovalPolicyDAL.findById(policyId); if (!accessApprovalPolicy) throw new BadRequestError({ message: "Secret approval policy not found" }); @@ -163,7 +166,8 @@ export const accessApprovalPolicyServiceFactory = ({ { approvals, secretPath, - name + name, + enforcementLevel }, tx ); diff --git a/backend/src/ee/services/access-approval-policy/access-approval-policy-types.ts b/backend/src/ee/services/access-approval-policy/access-approval-policy-types.ts index 601561b68..fdb6fc8bb 100644 --- a/backend/src/ee/services/access-approval-policy/access-approval-policy-types.ts +++ b/backend/src/ee/services/access-approval-policy/access-approval-policy-types.ts @@ -1,4 +1,4 @@ -import { TProjectPermission } from "@app/lib/types"; +import { EnforcementLevel, TProjectPermission } from "@app/lib/types"; import { ActorAuthMethod } from "@app/services/auth/auth-type"; import { TPermissionServiceFactory } from "../permission/permission-service"; @@ -20,6 +20,7 @@ export type TCreateAccessApprovalPolicy = { approvers: string[]; projectSlug: string; name: string; + enforcementLevel: EnforcementLevel; } & Omit; export type TUpdateAccessApprovalPolicy = { @@ -28,6 +29,7 @@ export type TUpdateAccessApprovalPolicy = { approvers?: string[]; secretPath?: string; name?: string; + enforcementLevel?: EnforcementLevel; } & Omit; export type TDeleteAccessApprovalPolicy = { diff --git a/backend/src/ee/services/access-approval-request/access-approval-request-dal.ts b/backend/src/ee/services/access-approval-request/access-approval-request-dal.ts index c3c0d24d0..b293a38d6 100644 --- a/backend/src/ee/services/access-approval-request/access-approval-request-dal.ts +++ b/backend/src/ee/services/access-approval-request/access-approval-request-dal.ts @@ -48,6 +48,7 @@ export const accessApprovalRequestDALFactory = (db: TDbClient) => { db.ref("name").withSchema(TableName.AccessApprovalPolicy).as("policyName"), db.ref("approvals").withSchema(TableName.AccessApprovalPolicy).as("policyApprovals"), db.ref("secretPath").withSchema(TableName.AccessApprovalPolicy).as("policySecretPath"), + db.ref("enforcementLevel").withSchema(TableName.AccessApprovalPolicy).as("policyEnforcementLevel"), db.ref("envId").withSchema(TableName.AccessApprovalPolicy).as("policyEnvId") ) @@ -98,6 +99,7 @@ export const accessApprovalRequestDALFactory = (db: TDbClient) => { name: doc.policyName, approvals: doc.policyApprovals, secretPath: doc.policySecretPath, + enforcementLevel: doc.policyEnforcementLevel, envId: doc.policyEnvId }, privilege: doc.privilegeId @@ -165,6 +167,7 @@ export const accessApprovalRequestDALFactory = (db: TDbClient) => { tx.ref("projectId").withSchema(TableName.Environment), tx.ref("slug").withSchema(TableName.Environment).as("environment"), tx.ref("secretPath").withSchema(TableName.AccessApprovalPolicy).as("policySecretPath"), + tx.ref("enforcementLevel").withSchema(TableName.AccessApprovalPolicy).as("policyEnforcementLevel"), tx.ref("approvals").withSchema(TableName.AccessApprovalPolicy).as("policyApprovals"), tx.ref("approverId").withSchema(TableName.AccessApprovalPolicyApprover) ); @@ -184,7 +187,8 @@ export const accessApprovalRequestDALFactory = (db: TDbClient) => { id: el.policyId, name: el.policyName, approvals: el.policyApprovals, - secretPath: el.policySecretPath + secretPath: el.policySecretPath, + enforcementLevel: el.policyEnforcementLevel } }), childrenMapper: [ 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 8d872afa5..f7577b24e 100644 --- a/backend/src/ee/services/audit-log/audit-log-types.ts +++ b/backend/src/ee/services/audit-log/audit-log-types.ts @@ -45,6 +45,7 @@ export enum EventType { CREATE_SECRETS = "create-secrets", UPDATE_SECRET = "update-secret", UPDATE_SECRETS = "update-secrets", + MOVE_SECRETS = "move-secrets", DELETE_SECRET = "delete-secret", DELETE_SECRETS = "delete-secrets", GET_WORKSPACE_KEY = "get-workspace-key", @@ -78,6 +79,11 @@ export enum EventType { UPDATE_IDENTITY_KUBENETES_AUTH = "update-identity-kubernetes-auth", GET_IDENTITY_KUBERNETES_AUTH = "get-identity-kubernetes-auth", REVOKE_IDENTITY_KUBERNETES_AUTH = "revoke-identity-kubernetes-auth", + LOGIN_IDENTITY_OIDC_AUTH = "login-identity-oidc-auth", + ADD_IDENTITY_OIDC_AUTH = "add-identity-oidc-auth", + UPDATE_IDENTITY_OIDC_AUTH = "update-identity-oidc-auth", + GET_IDENTITY_OIDC_AUTH = "get-identity-oidc-auth", + REVOKE_IDENTITY_OIDC_AUTH = "revoke-identity-oidc-auth", 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", @@ -235,6 +241,17 @@ interface UpdateSecretBatchEvent { }; } +interface MoveSecretsEvent { + type: EventType.MOVE_SECRETS; + metadata: { + sourceEnvironment: string; + sourceSecretPath: string; + destinationEnvironment: string; + destinationSecretPath: string; + secretIds: string[]; + }; +} + interface DeleteSecretEvent { type: EventType.DELETE_SECRET; metadata: { @@ -749,6 +766,63 @@ interface GetIdentityAzureAuthEvent { }; } +interface LoginIdentityOidcAuthEvent { + type: EventType.LOGIN_IDENTITY_OIDC_AUTH; + metadata: { + identityId: string; + identityOidcAuthId: string; + identityAccessTokenId: string; + }; +} + +interface AddIdentityOidcAuthEvent { + type: EventType.ADD_IDENTITY_OIDC_AUTH; + metadata: { + identityId: string; + oidcDiscoveryUrl: string; + caCert: string; + boundIssuer: string; + boundAudiences: string; + boundClaims: Record; + boundSubject: string; + accessTokenTTL: number; + accessTokenMaxTTL: number; + accessTokenNumUsesLimit: number; + accessTokenTrustedIps: Array; + }; +} + +interface DeleteIdentityOidcAuthEvent { + type: EventType.REVOKE_IDENTITY_OIDC_AUTH; + metadata: { + identityId: string; + }; +} + +interface UpdateIdentityOidcAuthEvent { + type: EventType.UPDATE_IDENTITY_OIDC_AUTH; + metadata: { + identityId: string; + oidcDiscoveryUrl?: string; + caCert?: string; + boundIssuer?: string; + boundAudiences?: string; + boundClaims?: Record; + boundSubject?: string; + accessTokenTTL?: number; + accessTokenMaxTTL?: number; + accessTokenNumUsesLimit?: number; + accessTokenTrustedIps?: Array; + }; +} + +interface GetIdentityOidcAuthEvent { + type: EventType.GET_IDENTITY_OIDC_AUTH; + metadata: { + identityId: string; + }; +} + interface CreateEnvironmentEvent { type: EventType.CREATE_ENVIRONMENT; metadata: { @@ -1097,6 +1171,7 @@ export type Event = | CreateSecretBatchEvent | UpdateSecretEvent | UpdateSecretBatchEvent + | MoveSecretsEvent | DeleteSecretEvent | DeleteSecretBatchEvent | GetWorkspaceKeyEvent @@ -1149,6 +1224,11 @@ export type Event = | DeleteIdentityAzureAuthEvent | UpdateIdentityAzureAuthEvent | GetIdentityAzureAuthEvent + | LoginIdentityOidcAuthEvent + | AddIdentityOidcAuthEvent + | DeleteIdentityOidcAuthEvent + | UpdateIdentityOidcAuthEvent + | GetIdentityOidcAuthEvent | CreateEnvironmentEvent | UpdateEnvironmentEvent | DeleteEnvironmentEvent diff --git a/backend/src/ee/services/certificate-authority-crl/certificate-authority-crl-service.ts b/backend/src/ee/services/certificate-authority-crl/certificate-authority-crl-service.ts index c8b56561e..917c55a0f 100644 --- a/backend/src/ee/services/certificate-authority-crl/certificate-authority-crl-service.ts +++ b/backend/src/ee/services/certificate-authority-crl/certificate-authority-crl-service.ts @@ -17,7 +17,7 @@ type TCertificateAuthorityCrlServiceFactoryDep = { certificateAuthorityDAL: Pick; certificateAuthorityCrlDAL: Pick; projectDAL: Pick; - kmsService: Pick; + kmsService: Pick; permissionService: Pick; licenseService: Pick; }; @@ -68,11 +68,11 @@ export const certificateAuthorityCrlServiceFactory = ({ kmsService }); - const decryptedCrl = await kmsService.decrypt({ - kmsId: keyId, - cipherTextBlob: caCrl.encryptedCrl + const kmsDecryptor = await kmsService.decryptWithKmsKey({ + kmsId: keyId }); + const decryptedCrl = kmsDecryptor({ cipherTextBlob: caCrl.encryptedCrl }); const crl = new x509.X509Crl(decryptedCrl); const base64crl = crl.toString("base64"); diff --git a/backend/src/ee/services/external-kms/external-kms-dal.ts b/backend/src/ee/services/external-kms/external-kms-dal.ts new file mode 100644 index 000000000..bb9a6ce8d --- /dev/null +++ b/backend/src/ee/services/external-kms/external-kms-dal.ts @@ -0,0 +1,47 @@ +import { Knex } from "knex"; + +import { TDbClient } from "@app/db"; +import { TableName, TKmsKeys } from "@app/db/schemas"; +import { DatabaseError } from "@app/lib/errors"; +import { ormify, selectAllTableCols } from "@app/lib/knex"; + +export type TExternalKmsDALFactory = ReturnType; + +export const externalKmsDALFactory = (db: TDbClient) => { + const externalKmsOrm = ormify(db, TableName.ExternalKms); + + const find = async (filter: Partial, tx?: Knex) => { + try { + const result = await (tx || db.replicaNode())(TableName.ExternalKms) + .join(TableName.KmsKey, `${TableName.KmsKey}.id`, `${TableName.ExternalKms}.kmsKeyId`) + .where(filter) + .select(selectAllTableCols(TableName.KmsKey)) + .select( + db.ref("id").withSchema(TableName.ExternalKms).as("externalKmsId"), + db.ref("provider").withSchema(TableName.ExternalKms).as("externalKmsProvider"), + db.ref("encryptedProviderInputs").withSchema(TableName.ExternalKms).as("externalKmsEncryptedProviderInput"), + db.ref("status").withSchema(TableName.ExternalKms).as("externalKmsStatus"), + db.ref("statusDetails").withSchema(TableName.ExternalKms).as("externalKmsStatusDetails") + ); + + return result.map((el) => ({ + id: el.id, + description: el.description, + isDisabled: el.isDisabled, + isReserved: el.isReserved, + orgId: el.orgId, + slug: el.slug, + externalKms: { + id: el.externalKmsId, + provider: el.externalKmsProvider, + status: el.externalKmsStatus, + statusDetails: el.externalKmsStatusDetails + } + })); + } catch (error) { + throw new DatabaseError({ error, name: "Find" }); + } + }; + + return { ...externalKmsOrm, find }; +}; diff --git a/backend/src/ee/services/external-kms/external-kms-service.ts b/backend/src/ee/services/external-kms/external-kms-service.ts new file mode 100644 index 000000000..a2d314844 --- /dev/null +++ b/backend/src/ee/services/external-kms/external-kms-service.ts @@ -0,0 +1,309 @@ +import { ForbiddenError } from "@casl/ability"; +import slugify from "@sindresorhus/slugify"; + +import { BadRequestError } from "@app/lib/errors"; +import { alphaNumericNanoId } from "@app/lib/nanoid"; +import { TKmsKeyDALFactory } from "@app/services/kms/kms-key-dal"; +import { TKmsServiceFactory } from "@app/services/kms/kms-service"; + +import { OrgPermissionActions, OrgPermissionSubjects } from "../permission/org-permission"; +import { TPermissionServiceFactory } from "../permission/permission-service"; +import { TExternalKmsDALFactory } from "./external-kms-dal"; +import { + TCreateExternalKmsDTO, + TDeleteExternalKmsDTO, + TGetExternalKmsByIdDTO, + TGetExternalKmsBySlugDTO, + TListExternalKmsDTO, + TUpdateExternalKmsDTO +} from "./external-kms-types"; +import { AwsKmsProviderFactory } from "./providers/aws-kms"; +import { ExternalKmsAwsSchema, KmsProviders } from "./providers/model"; + +type TExternalKmsServiceFactoryDep = { + externalKmsDAL: TExternalKmsDALFactory; + kmsService: Pick; + kmsDAL: Pick; + permissionService: Pick; +}; + +export type TExternalKmsServiceFactory = ReturnType; + +export const externalKmsServiceFactory = ({ + externalKmsDAL, + permissionService, + kmsService, + kmsDAL +}: TExternalKmsServiceFactoryDep) => { + const create = async ({ + provider, + description, + actor, + slug, + actorId, + actorOrgId, + actorAuthMethod + }: TCreateExternalKmsDTO) => { + const { permission } = await permissionService.getOrgPermission( + actor, + actorId, + actorOrgId, + actorAuthMethod, + actorOrgId + ); + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Settings); + const kmsSlug = slug ? slugify(slug) : slugify(alphaNumericNanoId(8).toLowerCase()); + + let sanitizedProviderInput = ""; + switch (provider.type) { + case KmsProviders.Aws: + { + const externalKms = await AwsKmsProviderFactory({ inputs: provider.inputs }); + await externalKms.validateConnection(); + // if missing kms key this generate a new kms key id and returns new provider input + const newProviderInput = await externalKms.generateInputKmsKey(); + sanitizedProviderInput = JSON.stringify(newProviderInput); + } + break; + default: + throw new BadRequestError({ message: "external kms provided is invalid" }); + } + + const orgKmsKeyId = await kmsService.getOrgKmsKeyId(actorOrgId); + const kmsEncryptor = await kmsService.encryptWithKmsKey({ + kmsId: orgKmsKeyId + }); + const { cipherTextBlob: encryptedProviderInputs } = kmsEncryptor({ + plainText: Buffer.from(sanitizedProviderInput, "utf8") + }); + + const externalKms = await externalKmsDAL.transaction(async (tx) => { + const kms = await kmsDAL.create( + { + isReserved: false, + description, + slug: kmsSlug, + orgId: actorOrgId + }, + tx + ); + const externalKmsCfg = await externalKmsDAL.create( + { + provider: provider.type, + encryptedProviderInputs, + kmsKeyId: kms.id + }, + tx + ); + return { ...kms, external: externalKmsCfg }; + }); + + return externalKms; + }; + + const updateById = async ({ + provider, + description, + actor, + id: kmsId, + slug, + actorId, + actorOrgId, + actorAuthMethod + }: TUpdateExternalKmsDTO) => { + const kmsDoc = await kmsDAL.findById(kmsId); + const { permission } = await permissionService.getOrgPermission( + actor, + actorId, + kmsDoc.orgId, + actorAuthMethod, + actorOrgId + ); + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Settings); + const kmsSlug = slug ? slugify(slug) : undefined; + + const externalKmsDoc = await externalKmsDAL.findOne({ kmsKeyId: kmsDoc.id }); + if (!externalKmsDoc) throw new BadRequestError({ message: "External kms not found" }); + + const orgDefaultKmsId = await kmsService.getOrgKmsKeyId(kmsDoc.orgId); + let sanitizedProviderInput = ""; + if (provider) { + const kmsDecryptor = await kmsService.decryptWithKmsKey({ + kmsId: orgDefaultKmsId + }); + const decryptedProviderInputBlob = kmsDecryptor({ + cipherTextBlob: externalKmsDoc.encryptedProviderInputs + }); + + switch (provider.type) { + case KmsProviders.Aws: + { + const decryptedProviderInput = await ExternalKmsAwsSchema.parseAsync( + JSON.parse(decryptedProviderInputBlob.toString("utf8")) + ); + const updatedProviderInput = { ...decryptedProviderInput, ...provider.inputs }; + const externalKms = await AwsKmsProviderFactory({ inputs: updatedProviderInput }); + await externalKms.validateConnection(); + sanitizedProviderInput = JSON.stringify(updatedProviderInput); + } + break; + default: + throw new BadRequestError({ message: "external kms provided is invalid" }); + } + } + + let encryptedProviderInputs: Buffer | undefined; + if (sanitizedProviderInput) { + const kmsEncryptor = await kmsService.encryptWithKmsKey({ + kmsId: orgDefaultKmsId + }); + const { cipherTextBlob } = kmsEncryptor({ + plainText: Buffer.from(sanitizedProviderInput, "utf8") + }); + encryptedProviderInputs = cipherTextBlob; + } + + const externalKms = await externalKmsDAL.transaction(async (tx) => { + const kms = await kmsDAL.updateById( + kmsDoc.id, + { + description, + slug: kmsSlug + }, + tx + ); + if (encryptedProviderInputs) { + const externalKmsCfg = await externalKmsDAL.updateById( + externalKmsDoc.id, + { + encryptedProviderInputs + }, + tx + ); + return { ...kms, external: externalKmsCfg }; + } + return { ...kms, external: externalKmsDoc }; + }); + + return externalKms; + }; + + const deleteById = async ({ actor, id: kmsId, actorId, actorOrgId, actorAuthMethod }: TDeleteExternalKmsDTO) => { + const kmsDoc = await kmsDAL.findById(kmsId); + const { permission } = await permissionService.getOrgPermission( + actor, + actorId, + kmsDoc.orgId, + actorAuthMethod, + actorOrgId + ); + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Settings); + + const externalKmsDoc = await externalKmsDAL.findOne({ kmsKeyId: kmsDoc.id }); + if (!externalKmsDoc) throw new BadRequestError({ message: "External kms not found" }); + + const externalKms = await externalKmsDAL.transaction(async (tx) => { + const kms = await kmsDAL.deleteById(kmsDoc.id, tx); + return { ...kms, external: externalKmsDoc }; + }); + + return externalKms; + }; + + const list = async ({ actor, actorId, actorOrgId, actorAuthMethod }: TListExternalKmsDTO) => { + const { permission } = await permissionService.getOrgPermission( + actor, + actorId, + actorOrgId, + actorAuthMethod, + actorOrgId + ); + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Settings); + + const externalKmsDocs = await externalKmsDAL.find({ orgId: actorOrgId }); + + return externalKmsDocs; + }; + + const findById = async ({ actor, actorId, actorOrgId, actorAuthMethod, id: kmsId }: TGetExternalKmsByIdDTO) => { + const kmsDoc = await kmsDAL.findById(kmsId); + const { permission } = await permissionService.getOrgPermission( + actor, + actorId, + kmsDoc.orgId, + actorAuthMethod, + actorOrgId + ); + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Settings); + + const externalKmsDoc = await externalKmsDAL.findOne({ kmsKeyId: kmsDoc.id }); + if (!externalKmsDoc) throw new BadRequestError({ message: "External kms not found" }); + + const orgDefaultKmsId = await kmsService.getOrgKmsKeyId(kmsDoc.orgId); + const kmsDecryptor = await kmsService.decryptWithKmsKey({ + kmsId: orgDefaultKmsId + }); + const decryptedProviderInputBlob = kmsDecryptor({ + cipherTextBlob: externalKmsDoc.encryptedProviderInputs + }); + switch (externalKmsDoc.provider) { + case KmsProviders.Aws: { + const decryptedProviderInput = await ExternalKmsAwsSchema.parseAsync( + JSON.parse(decryptedProviderInputBlob.toString("utf8")) + ); + return { ...kmsDoc, external: { ...externalKmsDoc, providerInput: decryptedProviderInput } }; + } + default: + throw new BadRequestError({ message: "external kms provided is invalid" }); + } + }; + + const findBySlug = async ({ + actor, + actorId, + actorOrgId, + actorAuthMethod, + slug: kmsSlug + }: TGetExternalKmsBySlugDTO) => { + const kmsDoc = await kmsDAL.findOne({ slug: kmsSlug, orgId: actorOrgId }); + const { permission } = await permissionService.getOrgPermission( + actor, + actorId, + kmsDoc.orgId, + actorAuthMethod, + actorOrgId + ); + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Settings); + + const externalKmsDoc = await externalKmsDAL.findOne({ kmsKeyId: kmsDoc.id }); + if (!externalKmsDoc) throw new BadRequestError({ message: "External kms not found" }); + + const orgDefaultKmsId = await kmsService.getOrgKmsKeyId(kmsDoc.orgId); + const kmsDecryptor = await kmsService.decryptWithKmsKey({ + kmsId: orgDefaultKmsId + }); + const decryptedProviderInputBlob = kmsDecryptor({ + cipherTextBlob: externalKmsDoc.encryptedProviderInputs + }); + + switch (externalKmsDoc.provider) { + case KmsProviders.Aws: { + const decryptedProviderInput = await ExternalKmsAwsSchema.parseAsync( + JSON.parse(decryptedProviderInputBlob.toString("utf8")) + ); + return { ...kmsDoc, external: { ...externalKmsDoc, providerInput: decryptedProviderInput } }; + } + default: + throw new BadRequestError({ message: "external kms provided is invalid" }); + } + }; + + return { + create, + updateById, + deleteById, + list, + findById, + findBySlug + }; +}; diff --git a/backend/src/ee/services/external-kms/external-kms-types.ts b/backend/src/ee/services/external-kms/external-kms-types.ts new file mode 100644 index 000000000..6254a80ef --- /dev/null +++ b/backend/src/ee/services/external-kms/external-kms-types.ts @@ -0,0 +1,30 @@ +import { TOrgPermission } from "@app/lib/types"; + +import { TExternalKmsInputSchema, TExternalKmsInputUpdateSchema } from "./providers/model"; + +export type TCreateExternalKmsDTO = { + slug?: string; + description?: string; + provider: TExternalKmsInputSchema; +} & Omit; + +export type TUpdateExternalKmsDTO = { + id: string; + slug?: string; + description?: string; + provider?: TExternalKmsInputUpdateSchema; +} & Omit; + +export type TDeleteExternalKmsDTO = { + id: string; +} & Omit; + +export type TListExternalKmsDTO = Omit; + +export type TGetExternalKmsByIdDTO = { + id: string; +} & Omit; + +export type TGetExternalKmsBySlugDTO = { + slug: string; +} & Omit; diff --git a/backend/src/ee/services/external-kms/providers/aws-kms.ts b/backend/src/ee/services/external-kms/providers/aws-kms.ts new file mode 100644 index 000000000..5a437fd2d --- /dev/null +++ b/backend/src/ee/services/external-kms/providers/aws-kms.ts @@ -0,0 +1,102 @@ +import { CreateKeyCommand, DecryptCommand, DescribeKeyCommand, EncryptCommand, KMSClient } from "@aws-sdk/client-kms"; +import { AssumeRoleCommand, STSClient } from "@aws-sdk/client-sts"; +import { randomUUID } from "crypto"; + +import { ExternalKmsAwsSchema, KmsAwsCredentialType, TExternalKmsAwsSchema, TExternalKmsProviderFns } from "./model"; + +const getAwsKmsClient = async (providerInputs: TExternalKmsAwsSchema) => { + if (providerInputs.credential.type === KmsAwsCredentialType.AssumeRole) { + const awsCredential = providerInputs.credential.data; + const stsClient = new STSClient({ + region: providerInputs.awsRegion + }); + const command = new AssumeRoleCommand({ + RoleArn: awsCredential.assumeRoleArn, + RoleSessionName: `infisical-kms-${randomUUID()}`, + DurationSeconds: 900, // 15mins + ExternalId: awsCredential.externalId + }); + const response = await stsClient.send(command); + if (!response.Credentials?.AccessKeyId || !response.Credentials?.SecretAccessKey) + throw new Error("Failed to assume role"); + + const kmsClient = new KMSClient({ + region: providerInputs.awsRegion, + credentials: { + accessKeyId: response.Credentials.AccessKeyId, + secretAccessKey: response.Credentials.SecretAccessKey, + sessionToken: response.Credentials.SessionToken, + expiration: response.Credentials.Expiration + } + }); + return kmsClient; + } + const awsCredential = providerInputs.credential.data; + const kmsClient = new KMSClient({ + region: providerInputs.awsRegion, + credentials: { + accessKeyId: awsCredential.accessKey, + secretAccessKey: awsCredential.secretKey + } + }); + return kmsClient; +}; + +type AwsKmsProviderArgs = { + inputs: unknown; +}; +type TAwsKmsProviderFactoryReturn = TExternalKmsProviderFns & { + generateInputKmsKey: () => Promise; +}; + +export const AwsKmsProviderFactory = async ({ inputs }: AwsKmsProviderArgs): Promise => { + const providerInputs = await ExternalKmsAwsSchema.parseAsync(inputs); + const awsClient = await getAwsKmsClient(providerInputs); + + const generateInputKmsKey = async () => { + if (providerInputs.kmsKeyId) return providerInputs; + + const command = new CreateKeyCommand({ Tags: [{ TagKey: "author", TagValue: "infisical" }] }); + const kmsKey = await awsClient.send(command); + if (!kmsKey.KeyMetadata?.KeyId) throw new Error("Failed to generate kms key"); + + return { ...providerInputs, kmsKeyId: kmsKey.KeyMetadata?.KeyId }; + }; + + const validateConnection = async () => { + const command = new DescribeKeyCommand({ + KeyId: providerInputs.kmsKeyId + }); + const isConnected = await awsClient.send(command).then(() => true); + return isConnected; + }; + + const encrypt = async (data: Buffer) => { + const command = new EncryptCommand({ + KeyId: providerInputs.kmsKeyId, + Plaintext: data + }); + const encryptionCommand = await awsClient.send(command); + if (!encryptionCommand.CiphertextBlob) throw new Error("encryption failed"); + + return { encryptedBlob: Buffer.from(encryptionCommand.CiphertextBlob) }; + }; + + const decrypt = async (encryptedBlob: Buffer) => { + const command = new DecryptCommand({ + KeyId: providerInputs.kmsKeyId, + CiphertextBlob: encryptedBlob + }); + const decryptionCommand = await awsClient.send(command); + if (!decryptionCommand.Plaintext) throw new Error("decryption failed"); + + return { data: Buffer.from(decryptionCommand.Plaintext) }; + }; + + return { + generateInputKmsKey, + validateConnection, + encrypt, + decrypt + }; +}; diff --git a/backend/src/ee/services/external-kms/providers/model.ts b/backend/src/ee/services/external-kms/providers/model.ts new file mode 100644 index 000000000..5a87e0c98 --- /dev/null +++ b/backend/src/ee/services/external-kms/providers/model.ts @@ -0,0 +1,61 @@ +import { z } from "zod"; + +export enum KmsProviders { + Aws = "aws" +} + +export enum KmsAwsCredentialType { + AssumeRole = "assume-role", + AccessKey = "access-key" +} + +export const ExternalKmsAwsSchema = z.object({ + credential: z + .discriminatedUnion("type", [ + z.object({ + type: z.literal(KmsAwsCredentialType.AccessKey), + data: z.object({ + accessKey: z.string().trim().min(1).describe("AWS user account access key"), + secretKey: z.string().trim().min(1).describe("AWS user account secret key") + }) + }), + z.object({ + type: z.literal(KmsAwsCredentialType.AssumeRole), + data: z.object({ + assumeRoleArn: z.string().trim().min(1).describe("AWS user role to be assumed by infisical"), + externalId: z + .string() + .trim() + .min(1) + .optional() + .describe("AWS assume role external id for furthur security in authentication") + }) + }) + ]) + .describe("AWS credential information to connect"), + awsRegion: z.string().min(1).trim().describe("AWS region to connect"), + kmsKeyId: z + .string() + .trim() + .optional() + .describe("A pre existing AWS KMS key id to be used for encryption. If not provided a kms key will be generated.") +}); +export type TExternalKmsAwsSchema = z.infer; + +// The root schema of the JSON +export const ExternalKmsInputSchema = z.discriminatedUnion("type", [ + z.object({ type: z.literal(KmsProviders.Aws), inputs: ExternalKmsAwsSchema }) +]); +export type TExternalKmsInputSchema = z.infer; + +export const ExternalKmsInputUpdateSchema = z.discriminatedUnion("type", [ + z.object({ type: z.literal(KmsProviders.Aws), inputs: ExternalKmsAwsSchema.partial() }) +]); +export type TExternalKmsInputUpdateSchema = z.infer; + +// generic function shared by all provider +export type TExternalKmsProviderFns = { + validateConnection: () => Promise; + encrypt: (data: Buffer) => Promise<{ encryptedBlob: Buffer }>; + decrypt: (encryptedBlob: Buffer) => Promise<{ data: Buffer }>; +}; diff --git a/backend/src/ee/services/group/user-group-membership-dal.ts b/backend/src/ee/services/group/user-group-membership-dal.ts index e20cf317b..cadbbdc36 100644 --- a/backend/src/ee/services/group/user-group-membership-dal.ts +++ b/backend/src/ee/services/group/user-group-membership-dal.ts @@ -162,11 +162,60 @@ export const userGroupMembershipDALFactory = (db: TDbClient) => { } }; + const findGroupMembershipsByUserIdInOrg = async (userId: string, orgId: string) => { + try { + const docs = await db + .replicaNode()(TableName.UserGroupMembership) + .join(TableName.Groups, `${TableName.UserGroupMembership}.groupId`, `${TableName.Groups}.id`) + .join(TableName.OrgMembership, `${TableName.UserGroupMembership}.userId`, `${TableName.OrgMembership}.userId`) + .join(TableName.Users, `${TableName.UserGroupMembership}.userId`, `${TableName.Users}.id`) + .where(`${TableName.UserGroupMembership}.userId`, userId) + .where(`${TableName.Groups}.orgId`, orgId) + .select( + db.ref("id").withSchema(TableName.UserGroupMembership), + db.ref("groupId").withSchema(TableName.UserGroupMembership), + db.ref("name").withSchema(TableName.Groups).as("groupName"), + db.ref("id").withSchema(TableName.OrgMembership).as("orgMembershipId"), + db.ref("firstName").withSchema(TableName.Users).as("firstName"), + db.ref("lastName").withSchema(TableName.Users).as("lastName") + ); + + return docs; + } catch (error) { + throw new DatabaseError({ error, name: "Find group memberships by user id in org" }); + } + }; + + const findGroupMembershipsByGroupIdInOrg = async (groupId: string, orgId: string) => { + try { + const docs = await db + .replicaNode()(TableName.UserGroupMembership) + .join(TableName.Groups, `${TableName.UserGroupMembership}.groupId`, `${TableName.Groups}.id`) + .join(TableName.OrgMembership, `${TableName.UserGroupMembership}.userId`, `${TableName.OrgMembership}.userId`) + .join(TableName.Users, `${TableName.UserGroupMembership}.userId`, `${TableName.Users}.id`) + .where(`${TableName.Groups}.id`, groupId) + .where(`${TableName.Groups}.orgId`, orgId) + .select( + db.ref("id").withSchema(TableName.UserGroupMembership), + db.ref("groupId").withSchema(TableName.UserGroupMembership), + db.ref("name").withSchema(TableName.Groups).as("groupName"), + db.ref("id").withSchema(TableName.OrgMembership).as("orgMembershipId"), + db.ref("firstName").withSchema(TableName.Users).as("firstName"), + db.ref("lastName").withSchema(TableName.Users).as("lastName") + ); + return docs; + } catch (error) { + throw new DatabaseError({ error, name: "Find group memberships by group id in org" }); + } + }; + return { ...userGroupMembershipOrm, filterProjectsByUserMembership, findUserGroupMembershipsInProject, findGroupMembersNotInProject, - deletePendingUserGroupMembershipsByUserIds + deletePendingUserGroupMembershipsByUserIds, + findGroupMembershipsByUserIdInOrg, + findGroupMembershipsByGroupIdInOrg }; }; diff --git a/backend/src/ee/services/ldap-config/ldap-config-service.ts b/backend/src/ee/services/ldap-config/ldap-config-service.ts index e1b2e011a..6d09d5025 100644 --- a/backend/src/ee/services/ldap-config/ldap-config-service.ts +++ b/backend/src/ee/services/ldap-config/ldap-config-service.ts @@ -449,7 +449,8 @@ export const ldapConfigServiceFactory = ({ userId: userAlias.userId, orgId, role: OrgMembershipRole.Member, - status: OrgMembershipStatus.Accepted + status: OrgMembershipStatus.Accepted, + isActive: true }, tx ); @@ -534,7 +535,8 @@ export const ldapConfigServiceFactory = ({ inviteEmail: email, orgId, role: OrgMembershipRole.Member, - status: newUser.isAccepted ? OrgMembershipStatus.Accepted : OrgMembershipStatus.Invited // if user is fully completed, then set status to accepted, otherwise set it to invited so we can update it later + status: newUser.isAccepted ? OrgMembershipStatus.Accepted : OrgMembershipStatus.Invited, // if user is fully completed, then set status to accepted, otherwise set it to invited so we can update it later + isActive: true }, tx ); diff --git a/backend/src/ee/services/license/licence-fns.ts b/backend/src/ee/services/license/licence-fns.ts index d69f7bf95..3e30276cb 100644 --- a/backend/src/ee/services/license/licence-fns.ts +++ b/backend/src/ee/services/license/licence-fns.ts @@ -38,7 +38,8 @@ export const getDefaultOnPremFeatures = (): TFeatureSet => ({ has_used_trial: true, secretApproval: false, secretRotation: true, - caCrl: false + caCrl: false, + instanceUserManagement: false }); export const setupLicenceRequestWithStore = (baseURL: string, refreshUrl: string, licenseKey: string) => { diff --git a/backend/src/ee/services/license/license-service.ts b/backend/src/ee/services/license/license-service.ts index f0b568535..f5360bbdf 100644 --- a/backend/src/ee/services/license/license-service.ts +++ b/backend/src/ee/services/license/license-service.ts @@ -218,6 +218,8 @@ export const licenseServiceFactory = ({ } else if (instanceType === InstanceType.EnterpriseOnPrem) { const usedSeats = await licenseDAL.countOfOrgMembers(null, tx); const usedIdentitySeats = await licenseDAL.countOrgUsersAndIdentities(null, tx); + onPremFeatures.membersUsed = usedSeats; + onPremFeatures.identitiesUsed = usedIdentitySeats; await licenseServerOnPremApi.request.patch(`/api/license/v1/license`, { usedSeats, usedIdentitySeats diff --git a/backend/src/ee/services/license/license-types.ts b/backend/src/ee/services/license/license-types.ts index 36b03ff80..1cdc9fa51 100644 --- a/backend/src/ee/services/license/license-types.ts +++ b/backend/src/ee/services/license/license-types.ts @@ -30,9 +30,9 @@ export type TFeatureSet = { workspacesUsed: 0; dynamicSecret: false; memberLimit: null; - membersUsed: 0; + membersUsed: number; identityLimit: null; - identitiesUsed: 0; + identitiesUsed: number; environmentLimit: null; environmentsUsed: 0; secretVersioning: true; @@ -56,6 +56,7 @@ export type TFeatureSet = { secretApproval: false; secretRotation: true; caCrl: false; + instanceUserManagement: false; }; export type TOrgPlansTableDTO = { diff --git a/backend/src/ee/services/oidc/oidc-config-service.ts b/backend/src/ee/services/oidc/oidc-config-service.ts index 55c929b9a..f86f29646 100644 --- a/backend/src/ee/services/oidc/oidc-config-service.ts +++ b/backend/src/ee/services/oidc/oidc-config-service.ts @@ -193,7 +193,8 @@ export const oidcConfigServiceFactory = ({ inviteEmail: email, orgId, role: OrgMembershipRole.Member, - status: foundUser.isAccepted ? OrgMembershipStatus.Accepted : OrgMembershipStatus.Invited // if user is fully completed, then set status to accepted, otherwise set it to invited so we can update it later + status: foundUser.isAccepted ? OrgMembershipStatus.Accepted : OrgMembershipStatus.Invited, // if user is fully completed, then set status to accepted, otherwise set it to invited so we can update it later + isActive: true }, tx ); @@ -266,7 +267,8 @@ export const oidcConfigServiceFactory = ({ inviteEmail: email, orgId, role: OrgMembershipRole.Member, - status: newUser.isAccepted ? OrgMembershipStatus.Accepted : OrgMembershipStatus.Invited // if user is fully completed, then set status to accepted, otherwise set it to invited so we can update it later + status: newUser.isAccepted ? OrgMembershipStatus.Accepted : OrgMembershipStatus.Invited, // if user is fully completed, then set status to accepted, otherwise set it to invited so we can update it later + isActive: true }, tx ); diff --git a/backend/src/ee/services/permission/permission-service.ts b/backend/src/ee/services/permission/permission-service.ts index f4e423797..72a35a326 100644 --- a/backend/src/ee/services/permission/permission-service.ts +++ b/backend/src/ee/services/permission/permission-service.ts @@ -109,6 +109,9 @@ export const permissionServiceFactory = ({ authMethod: ActorAuthMethod, userOrgId?: string ) => { + // when token is scoped, ensure the passed org id is same as user org id + if (userOrgId && userOrgId !== orgId) + throw new BadRequestError({ message: "Invalid user token. Scoped to different organization." }); const membership = await permissionDAL.getOrgPermission(userId, orgId); if (!membership) throw new UnauthorizedError({ name: "User not in org" }); if (membership.role === OrgMembershipRole.Custom && !membership.permissions) { diff --git a/backend/src/ee/services/saml-config/saml-config-service.ts b/backend/src/ee/services/saml-config/saml-config-service.ts index c147b7e27..5779b73f6 100644 --- a/backend/src/ee/services/saml-config/saml-config-service.ts +++ b/backend/src/ee/services/saml-config/saml-config-service.ts @@ -370,7 +370,8 @@ export const samlConfigServiceFactory = ({ inviteEmail: email, orgId, role: OrgMembershipRole.Member, - status: foundUser.isAccepted ? OrgMembershipStatus.Accepted : OrgMembershipStatus.Invited // if user is fully completed, then set status to accepted, otherwise set it to invited so we can update it later + status: foundUser.isAccepted ? OrgMembershipStatus.Accepted : OrgMembershipStatus.Invited, // if user is fully completed, then set status to accepted, otherwise set it to invited so we can update it later + isActive: true }, tx ); @@ -457,7 +458,8 @@ export const samlConfigServiceFactory = ({ inviteEmail: email, orgId, role: OrgMembershipRole.Member, - status: newUser.isAccepted ? OrgMembershipStatus.Accepted : OrgMembershipStatus.Invited // if user is fully completed, then set status to accepted, otherwise set it to invited so we can update it later + status: newUser.isAccepted ? OrgMembershipStatus.Accepted : OrgMembershipStatus.Invited, // if user is fully completed, then set status to accepted, otherwise set it to invited so we can update it later + isActive: true }, tx ); diff --git a/backend/src/ee/services/scim/scim-fns.ts b/backend/src/ee/services/scim/scim-fns.ts index 08b652185..cdfe63f43 100644 --- a/backend/src/ee/services/scim/scim-fns.ts +++ b/backend/src/ee/services/scim/scim-fns.ts @@ -32,12 +32,19 @@ export const parseScimFilter = (filterToParse: string | undefined) => { return { [attributeName]: parsedValue.replace(/"/g, "") }; }; +export function extractScimValueFromPath(path: string): string | null { + const regex = /members\[value eq "([^"]+)"\]/; + const match = path.match(regex); + return match ? match[1] : null; +} + export const buildScimUser = ({ orgMembershipId, username, email, firstName, lastName, + groups = [], active }: { orgMembershipId: string; @@ -45,6 +52,10 @@ export const buildScimUser = ({ email?: string | null; firstName: string; lastName: string; + groups?: { + value: string; + display: string; + }[]; active: boolean; }): TScimUser => { const scimUser = { @@ -67,7 +78,7 @@ export const buildScimUser = ({ ] : [], active, - groups: [], + groups, meta: { resourceType: "User", location: null diff --git a/backend/src/ee/services/scim/scim-service.ts b/backend/src/ee/services/scim/scim-service.ts index dc175f15b..5787924dd 100644 --- a/backend/src/ee/services/scim/scim-service.ts +++ b/backend/src/ee/services/scim/scim-service.ts @@ -2,13 +2,14 @@ import { ForbiddenError } from "@casl/ability"; import slugify from "@sindresorhus/slugify"; import jwt from "jsonwebtoken"; -import { OrgMembershipRole, OrgMembershipStatus, TableName, TGroups, TOrgMemberships, TUsers } from "@app/db/schemas"; +import { OrgMembershipRole, OrgMembershipStatus, TableName, TOrgMemberships, TUsers } from "@app/db/schemas"; import { TGroupDALFactory } from "@app/ee/services/group/group-dal"; import { addUsersToGroupByUserIds, removeUsersFromGroupByUserIds } from "@app/ee/services/group/group-fns"; import { TUserGroupMembershipDALFactory } from "@app/ee/services/group/user-group-membership-dal"; import { TScimDALFactory } from "@app/ee/services/scim/scim-dal"; import { getConfig } from "@app/lib/config/env"; import { BadRequestError, ScimRequestError, UnauthorizedError } from "@app/lib/errors"; +import { logger } from "@app/lib/logger"; import { alphaNumericNanoId } from "@app/lib/nanoid"; import { TOrgPermission } from "@app/lib/types"; import { AuthTokenType } from "@app/services/auth/auth-type"; @@ -30,7 +31,14 @@ import { UserAliasType } from "@app/services/user-alias/user-alias-types"; import { TLicenseServiceFactory } from "../license/license-service"; import { OrgPermissionActions, OrgPermissionSubjects } from "../permission/org-permission"; import { TPermissionServiceFactory } from "../permission/permission-service"; -import { buildScimGroup, buildScimGroupList, buildScimUser, buildScimUserList, parseScimFilter } from "./scim-fns"; +import { + buildScimGroup, + buildScimGroupList, + buildScimUser, + buildScimUserList, + extractScimValueFromPath, + parseScimFilter +} from "./scim-fns"; import { TCreateScimGroupDTO, TCreateScimTokenDTO, @@ -44,6 +52,7 @@ import { TListScimUsers, TListScimUsersDTO, TReplaceScimUserDTO, + TScimGroup, TScimTokenJwtPayload, TUpdateScimGroupNamePatchDTO, TUpdateScimGroupNamePutDTO, @@ -61,17 +70,23 @@ type TScimServiceFactoryDep = { TOrgDALFactory, "createMembership" | "findById" | "findMembership" | "deleteMembershipById" | "transaction" | "updateMembershipById" >; - orgMembershipDAL: Pick; + orgMembershipDAL: Pick; projectDAL: Pick; projectMembershipDAL: Pick; groupDAL: Pick< TGroupDALFactory, - "create" | "findOne" | "findAllGroupMembers" | "update" | "delete" | "findGroups" | "transaction" + "create" | "findOne" | "findAllGroupMembers" | "delete" | "findGroups" | "transaction" | "updateById" | "update" >; groupProjectDAL: Pick; userGroupMembershipDAL: Pick< TUserGroupMembershipDALFactory, - "find" | "transaction" | "insertMany" | "filterProjectsByUserMembership" | "delete" + | "find" + | "transaction" + | "insertMany" + | "filterProjectsByUserMembership" + | "delete" + | "findGroupMembershipsByUserIdInOrg" + | "findGroupMembershipsByGroupIdInOrg" >; projectKeyDAL: Pick; projectBotDAL: Pick; @@ -197,14 +212,14 @@ export const scimServiceFactory = ({ findOpts ); - const scimUsers = users.map(({ id, externalId, username, firstName, lastName, email }) => + const scimUsers = users.map(({ id, externalId, username, firstName, lastName, email, isActive }) => buildScimUser({ orgMembershipId: id ?? "", username: externalId ?? username, firstName: firstName ?? "", lastName: lastName ?? "", email, - active: true + active: isActive }) ); @@ -240,13 +255,22 @@ export const scimServiceFactory = ({ status: 403 }); + const groupMembershipsInOrg = await userGroupMembershipDAL.findGroupMembershipsByUserIdInOrg( + membership.userId, + orgId + ); + return buildScimUser({ orgMembershipId: membership.id, username: membership.externalId ?? membership.username, email: membership.email ?? "", firstName: membership.firstName as string, lastName: membership.lastName as string, - active: true + active: membership.isActive, + groups: groupMembershipsInOrg.map((group) => ({ + value: group.groupId, + display: group.groupName + })) }); }; @@ -296,7 +320,8 @@ export const scimServiceFactory = ({ inviteEmail: email, orgId, role: OrgMembershipRole.Member, - status: user.isAccepted ? OrgMembershipStatus.Accepted : OrgMembershipStatus.Invited // if user is fully completed, then set status to accepted, otherwise set it to invited so we can update it later + status: user.isAccepted ? OrgMembershipStatus.Accepted : OrgMembershipStatus.Invited, // if user is fully completed, then set status to accepted, otherwise set it to invited so we can update it later + isActive: true }, tx ); @@ -364,7 +389,8 @@ export const scimServiceFactory = ({ inviteEmail: email, orgId, role: OrgMembershipRole.Member, - status: user.isAccepted ? OrgMembershipStatus.Accepted : OrgMembershipStatus.Invited // if user is fully completed, then set status to accepted, otherwise set it to invited so we can update it later + status: user.isAccepted ? OrgMembershipStatus.Accepted : OrgMembershipStatus.Invited, // if user is fully completed, then set status to accepted, otherwise set it to invited so we can update it later + isActive: true }, tx ); @@ -401,7 +427,7 @@ export const scimServiceFactory = ({ firstName: createdUser.firstName as string, lastName: createdUser.lastName as string, email: createdUser.email ?? "", - active: true + active: createdOrgMembership.isActive }); }; @@ -445,14 +471,8 @@ export const scimServiceFactory = ({ }); if (!active) { - await deleteOrgMembershipFn({ - orgMembershipId: membership.id, - orgId: membership.orgId, - orgDAL, - projectMembershipDAL, - projectKeyDAL, - userAliasDAL, - licenseService + await orgMembershipDAL.updateById(membership.id, { + isActive: false }); } @@ -491,17 +511,14 @@ export const scimServiceFactory = ({ status: 403 }); - if (!active) { - await deleteOrgMembershipFn({ - orgMembershipId: membership.id, - orgId: membership.orgId, - orgDAL, - projectMembershipDAL, - projectKeyDAL, - userAliasDAL, - licenseService - }); - } + await orgMembershipDAL.updateById(membership.id, { + isActive: active + }); + + const groupMembershipsInOrg = await userGroupMembershipDAL.findGroupMembershipsByUserIdInOrg( + membership.userId, + orgId + ); return buildScimUser({ orgMembershipId: membership.id, @@ -509,7 +526,11 @@ export const scimServiceFactory = ({ email: membership.email, firstName: membership.firstName as string, lastName: membership.lastName as string, - active + active, + groups: groupMembershipsInOrg.map((group) => ({ + value: group.groupId, + display: group.groupName + })) }); }; @@ -577,13 +598,20 @@ export const scimServiceFactory = ({ } ); - const scimGroups = groups.map((group) => - buildScimGroup({ + const scimGroups: TScimGroup[] = []; + + for await (const group of groups) { + const members = await userGroupMembershipDAL.findGroupMembershipsByGroupIdInOrg(group.id, orgId); + const scimGroup = buildScimGroup({ groupId: group.id, name: group.name, - members: [] // does this need to be populated? - }) - ); + members: members.map((member) => ({ + value: member.orgMembershipId, + display: `${member.firstName ?? ""} ${member.lastName ?? ""}` + })) + }); + scimGroups.push(scimGroup); + } return buildScimGroupList({ scimGroups, @@ -817,7 +845,6 @@ export const scimServiceFactory = ({ }); }; - // TODO: add support for add/remove op const updateScimGroupNamePatch = async ({ groupId, orgId, operations }: TUpdateScimGroupNamePatchDTO) => { const plan = await licenseService.getPlan(orgId); if (!plan.groups) @@ -840,27 +867,64 @@ export const scimServiceFactory = ({ status: 403 }); - let group: TGroups | undefined; + let group = await groupDAL.findOne({ + id: groupId, + orgId + }); + + if (!group) { + throw new ScimRequestError({ + detail: "Group Not Found", + status: 404 + }); + } + for await (const operation of operations) { switch (operation.op) { case "replace": { - await groupDAL.update( - { - id: groupId, - orgId - }, - { - name: operation.value.displayName - } - ); + group = await groupDAL.updateById(group.id, { + name: operation.value.displayName + }); break; } case "add": { - // TODO + try { + const orgMemberships = await orgMembershipDAL.find({ + $in: { + id: operation.value.map((member) => member.value) + } + }); + + await addUsersToGroupByUserIds({ + group, + userIds: orgMemberships.map((membership) => membership.userId as string), + userDAL, + userGroupMembershipDAL, + orgDAL, + groupProjectDAL, + projectKeyDAL, + projectDAL, + projectBotDAL + }); + } catch { + logger.info("Repeat SCIM user-group add operation"); + } + break; } case "remove": { - // TODO + const orgMembershipId = extractScimValueFromPath(operation.path); + if (!orgMembershipId) throw new ScimRequestError({ detail: "Invalid path value", status: 400 }); + const orgMembership = await orgMembershipDAL.findById(orgMembershipId); + if (!orgMembership) throw new ScimRequestError({ detail: "Org Membership Not Found", status: 400 }); + await removeUsersFromGroupByUserIds({ + group, + userIds: [orgMembership.userId as string], + userDAL, + userGroupMembershipDAL, + groupProjectDAL, + projectKeyDAL + }); break; } default: { @@ -872,17 +936,15 @@ export const scimServiceFactory = ({ } } - if (!group) { - throw new ScimRequestError({ - detail: "Group Not Found", - status: 404 - }); - } + const members = await userGroupMembershipDAL.findGroupMembershipsByGroupIdInOrg(group.id, orgId); return buildScimGroup({ groupId: group.id, name: group.name, - members: [] + members: members.map((member) => ({ + value: member.orgMembershipId, + display: `${member.firstName ?? ""} ${member.lastName ?? ""}` + })) }); }; diff --git a/backend/src/ee/services/scim/scim-types.ts b/backend/src/ee/services/scim/scim-types.ts index cffc80407..51caf585b 100644 --- a/backend/src/ee/services/scim/scim-types.ts +++ b/backend/src/ee/services/scim/scim-types.ts @@ -125,10 +125,11 @@ type TRemoveOp = { type TAddOp = { op: "add"; + path: string; value: { value: string; display?: string; - }; + }[]; }; export type TDeleteScimGroupDTO = { @@ -157,7 +158,10 @@ export type TScimUser = { type: string; }[]; active: boolean; - groups: string[]; + groups: { + value: string; + display: string; + }[]; meta: { resourceType: string; location: null; diff --git a/backend/src/ee/services/secret-approval-policy/secret-approval-policy-service.ts b/backend/src/ee/services/secret-approval-policy/secret-approval-policy-service.ts index 2db825c88..366ad69ea 100644 --- a/backend/src/ee/services/secret-approval-policy/secret-approval-policy-service.ts +++ b/backend/src/ee/services/secret-approval-policy/secret-approval-policy-service.ts @@ -45,12 +45,13 @@ export const secretApprovalPolicyServiceFactory = ({ actorOrgId, actorAuthMethod, approvals, - approverUserIds, + approvers, projectId, secretPath, - environment + environment, + enforcementLevel }: TCreateSapDTO) => { - if (approvals > approverUserIds.length) + if (approvals > approvers.length) throw new BadRequestError({ message: "Approvals cannot be greater than approvers" }); const { permission } = await permissionService.getProjectPermission( @@ -73,12 +74,13 @@ export const secretApprovalPolicyServiceFactory = ({ envId: env.id, approvals, secretPath, - name + name, + enforcementLevel }, tx ); await secretApprovalPolicyApproverDAL.insertMany( - approverUserIds.map((approverUserId) => ({ + approvers.map((approverUserId) => ({ approverUserId, policyId: doc.id })), @@ -90,7 +92,7 @@ export const secretApprovalPolicyServiceFactory = ({ }; const updateSecretApprovalPolicy = async ({ - approverUserIds, + approvers, secretPath, name, actorId, @@ -98,7 +100,8 @@ export const secretApprovalPolicyServiceFactory = ({ actorOrgId, actorAuthMethod, approvals, - secretPolicyId + secretPolicyId, + enforcementLevel }: TUpdateSapDTO) => { const secretApprovalPolicy = await secretApprovalPolicyDAL.findById(secretPolicyId); if (!secretApprovalPolicy) throw new BadRequestError({ message: "Secret approval policy not found" }); @@ -118,14 +121,15 @@ export const secretApprovalPolicyServiceFactory = ({ { approvals, secretPath, - name + name, + enforcementLevel }, tx ); - if (approverUserIds) { + if (approvers) { await secretApprovalPolicyApproverDAL.delete({ policyId: doc.id }, tx); await secretApprovalPolicyApproverDAL.insertMany( - approverUserIds.map((approverUserId) => ({ + approvers.map((approverUserId) => ({ approverUserId, policyId: doc.id })), diff --git a/backend/src/ee/services/secret-approval-policy/secret-approval-policy-types.ts b/backend/src/ee/services/secret-approval-policy/secret-approval-policy-types.ts index 1a527289c..8e7099c98 100644 --- a/backend/src/ee/services/secret-approval-policy/secret-approval-policy-types.ts +++ b/backend/src/ee/services/secret-approval-policy/secret-approval-policy-types.ts @@ -1,20 +1,22 @@ -import { TProjectPermission } from "@app/lib/types"; +import { EnforcementLevel, TProjectPermission } from "@app/lib/types"; export type TCreateSapDTO = { approvals: number; secretPath?: string | null; environment: string; - approverUserIds: string[]; + approvers: string[]; projectId: string; name: string; + enforcementLevel: EnforcementLevel; } & Omit; export type TUpdateSapDTO = { secretPolicyId: string; approvals?: number; secretPath?: string | null; - approverUserIds: string[]; + approvers: string[]; name?: string; + enforcementLevel?: EnforcementLevel; } & Omit; export type TDeleteSapDTO = { diff --git a/backend/src/ee/services/secret-approval-request/secret-approval-request-dal.ts b/backend/src/ee/services/secret-approval-request/secret-approval-request-dal.ts index 06c48ac8b..855442e6f 100644 --- a/backend/src/ee/services/secret-approval-request/secret-approval-request-dal.ts +++ b/backend/src/ee/services/secret-approval-request/secret-approval-request-dal.ts @@ -94,6 +94,8 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => { tx.ref("projectId").withSchema(TableName.Environment), tx.ref("slug").withSchema(TableName.Environment).as("environment"), tx.ref("secretPath").withSchema(TableName.SecretApprovalPolicy).as("policySecretPath"), + tx.ref("envId").withSchema(TableName.SecretApprovalPolicy).as("policyEnvId"), + tx.ref("enforcementLevel").withSchema(TableName.SecretApprovalPolicy).as("policyEnforcementLevel"), tx.ref("approvals").withSchema(TableName.SecretApprovalPolicy).as("policyApprovals") ); @@ -128,7 +130,9 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => { id: el.policyId, name: el.policyName, approvals: el.policyApprovals, - secretPath: el.policySecretPath + secretPath: el.policySecretPath, + enforcementLevel: el.policyEnforcementLevel, + envId: el.policyEnvId } }), childrenMapper: [ @@ -282,6 +286,7 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => { `DENSE_RANK() OVER (partition by ${TableName.Environment}."projectId" ORDER BY ${TableName.SecretApprovalRequest}."id" DESC) as rank` ), db.ref("secretPath").withSchema(TableName.SecretApprovalPolicy).as("policySecretPath"), + db.ref("enforcementLevel").withSchema(TableName.SecretApprovalPolicy).as("policyEnforcementLevel"), db.ref("approvals").withSchema(TableName.SecretApprovalPolicy).as("policyApprovals"), db.ref("approverUserId").withSchema(TableName.SecretApprovalPolicyApprover), db.ref("email").withSchema("committerUser").as("committerUserEmail"), @@ -308,7 +313,8 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => { id: el.policyId, name: el.policyName, approvals: el.policyApprovals, - secretPath: el.policySecretPath + secretPath: el.policySecretPath, + enforcementLevel: el.policyEnforcementLevel }, committerUser: { userId: el.committerUserId, diff --git a/backend/src/ee/services/secret-approval-request/secret-approval-request-service.ts b/backend/src/ee/services/secret-approval-request/secret-approval-request-service.ts index a519af4fd..68e0271d2 100644 --- a/backend/src/ee/services/secret-approval-request/secret-approval-request-service.ts +++ b/backend/src/ee/services/secret-approval-request/secret-approval-request-service.ts @@ -7,13 +7,16 @@ import { SecretType, TSecretApprovalRequestsSecretsInsert } from "@app/db/schemas"; +import { getConfig } from "@app/lib/config/env"; import { decryptSymmetric128BitHexKeyUTF8 } from "@app/lib/crypto"; import { BadRequestError, UnauthorizedError } from "@app/lib/errors"; import { groupBy, pick, unique } from "@app/lib/fn"; import { alphaNumericNanoId } from "@app/lib/nanoid"; +import { EnforcementLevel } from "@app/lib/types"; import { ActorType } from "@app/services/auth/auth-type"; import { TProjectDALFactory } from "@app/services/project/project-dal"; import { TProjectBotServiceFactory } from "@app/services/project-bot/project-bot-service"; +import { TProjectEnvDALFactory } from "@app/services/project-env/project-env-dal"; import { TSecretDALFactory } from "@app/services/secret/secret-dal"; import { fnSecretBlindIndexCheck, @@ -30,6 +33,8 @@ import { TSecretVersionTagDALFactory } from "@app/services/secret/secret-version import { TSecretBlindIndexDALFactory } from "@app/services/secret-blind-index/secret-blind-index-dal"; import { TSecretFolderDALFactory } from "@app/services/secret-folder/secret-folder-dal"; import { TSecretTagDALFactory } from "@app/services/secret-tag/secret-tag-dal"; +import { SmtpTemplates, TSmtpService } from "@app/services/smtp/smtp-service"; +import { TUserDALFactory } from "@app/services/user/user-dal"; import { TPermissionServiceFactory } from "../permission/permission-service"; import { ProjectPermissionActions, ProjectPermissionSub } from "../permission/project-permission"; @@ -62,8 +67,11 @@ type TSecretApprovalRequestServiceFactoryDep = { snapshotService: Pick; secretVersionDAL: Pick; secretVersionTagDAL: Pick; - projectDAL: Pick; + projectDAL: Pick; secretQueueService: Pick; + smtpService: Pick; + userDAL: Pick; + projectEnvDAL: Pick; }; export type TSecretApprovalRequestServiceFactory = ReturnType; @@ -82,7 +90,10 @@ export const secretApprovalRequestServiceFactory = ({ snapshotService, secretVersionDAL, secretQueueService, - projectBotService + projectBotService, + smtpService, + userDAL, + projectEnvDAL }: TSecretApprovalRequestServiceFactoryDep) => { const requestCount = async ({ projectId, actor, actorId, actorOrgId, actorAuthMethod }: TApprovalRequestCountDTO) => { if (actor === ActorType.SERVICE) throw new BadRequestError({ message: "Cannot use service token" }); @@ -257,7 +268,8 @@ export const secretApprovalRequestServiceFactory = ({ actor, actorId, actorOrgId, - actorAuthMethod + actorAuthMethod, + bypassReason }: TMergeSecretApprovalRequestDTO) => { const secretApprovalRequest = await secretApprovalRequestDAL.findById(approvalId); if (!secretApprovalRequest) throw new BadRequestError({ message: "Secret approval request not found" }); @@ -289,7 +301,10 @@ export const secretApprovalRequestServiceFactory = ({ ({ userId: approverId }) => reviewers[approverId.toString()] === ApprovalStatus.APPROVED ).length; - if (!hasMinApproval) throw new BadRequestError({ message: "Doesn't have minimum approvals needed" }); + const isSoftEnforcement = secretApprovalRequest.policy.enforcementLevel === EnforcementLevel.Soft; + + if (!hasMinApproval && !isSoftEnforcement) + throw new BadRequestError({ message: "Doesn't have minimum approvals needed" }); const secretApprovalSecrets = await secretApprovalRequestSecretDAL.findByRequestId(secretApprovalRequest.id); if (!secretApprovalSecrets) throw new BadRequestError({ message: "No secrets found" }); @@ -466,7 +481,8 @@ export const secretApprovalRequestServiceFactory = ({ conflicts: JSON.stringify(conflicts), hasMerged: true, status: RequestState.Closed, - statusChangedByUserId: actorId + statusChangedByUserId: actorId, + bypassReason }, tx ); @@ -485,6 +501,35 @@ export const secretApprovalRequestServiceFactory = ({ actorId, actor }); + + if (isSoftEnforcement) { + const cfg = getConfig(); + const project = await projectDAL.findProjectById(projectId); + const env = await projectEnvDAL.findOne({ id: policy.envId }); + const requestedByUser = await userDAL.findOne({ id: actorId }); + const approverUsers = await userDAL.find({ + $in: { + id: policy.approvers.map((approver: { userId: string }) => approver.userId) + } + }); + + await smtpService.sendMail({ + recipients: approverUsers.filter((approver) => approver.email).map((approver) => approver.email!), + subjectLine: "Infisical Secret Change Policy Bypassed", + + substitutions: { + projectName: project.name, + requesterFullName: `${requestedByUser.firstName} ${requestedByUser.lastName}`, + requesterEmail: requestedByUser.email, + bypassReason, + secretPath: policy.secretPath, + environment: env.name, + approvalUrl: `${cfg.SITE_URL}/project/${project.id}/approval` + }, + template: SmtpTemplates.AccessSecretRequestBypassed + }); + } + return mergeStatus; }; diff --git a/backend/src/ee/services/secret-approval-request/secret-approval-request-types.ts b/backend/src/ee/services/secret-approval-request/secret-approval-request-types.ts index 1fbb75418..db3dc176b 100644 --- a/backend/src/ee/services/secret-approval-request/secret-approval-request-types.ts +++ b/backend/src/ee/services/secret-approval-request/secret-approval-request-types.ts @@ -39,6 +39,7 @@ export type TGenerateSecretApprovalRequestDTO = { export type TMergeSecretApprovalRequestDTO = { approvalId: string; + bypassReason?: string; } & Omit; export type TStatusChangeDTO = { diff --git a/backend/src/lib/api-docs/constants.ts b/backend/src/lib/api-docs/constants.ts index de0a1d4c2..5aae289bf 100644 --- a/backend/src/lib/api-docs/constants.ts +++ b/backend/src/lib/api-docs/constants.ts @@ -70,13 +70,13 @@ export const UNIVERSAL_AUTH = { "The maximum number of times that an access token can be used; a value of 0 implies infinite number of uses." }, RETRIEVE: { - identityId: "The ID of the identity to retrieve." + identityId: "The ID of the identity to retrieve the auth method for." }, REVOKE: { - identityId: "The ID of the identity to revoke." + identityId: "The ID of the identity to revoke the auth method for." }, UPDATE: { - identityId: "The ID of the identity to update.", + identityId: "The ID of the identity to update the auth method for.", clientSecretTrustedIps: "The new list of IPs or CIDR ranges that the Client Secret can be used from.", accessTokenTrustedIps: "The new list of IPs or CIDR ranges that access tokens can be used from.", accessTokenTTL: "The new lifetime for an access token in seconds.", @@ -119,26 +119,228 @@ export const AWS_AUTH = { "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." }, + ATTACH: { + identityId: "The ID of the identity to attach the configuration onto.", + allowedPrincipalArns: + "The comma-separated list of trusted IAM principal ARNs that are allowed to authenticate with Infisical.", + allowedAccountIds: + "The comma-separated list of trusted AWS account IDs that are allowed to authenticate with Infisical.", + accessTokenTTL: "The lifetime for an acccess token in seconds.", + accessTokenMaxTTL: "The maximum lifetime for an acccess token in seconds.", + stsEndpoint: "The endpoint URL for the AWS STS API.", + accessTokenNumUsesLimit: "The maximum number of times that an access token can be used.", + accessTokenTrustedIps: "The IPs or CIDR ranges that access tokens can be used from." + }, + UPDATE: { + identityId: "The ID of the identity to update the auth method for.", + allowedPrincipalArns: + "The new comma-separated list of trusted IAM principal ARNs that are allowed to authenticate with Infisical.", + allowedAccountIds: + "The new comma-separated list of trusted AWS account IDs that are allowed to authenticate with Infisical.", + accessTokenTTL: "The new lifetime for an acccess token in seconds.", + accessTokenMaxTTL: "The new maximum lifetime for an acccess token in seconds.", + stsEndpoint: "The new endpoint URL for the AWS STS API.", + accessTokenNumUsesLimit: "The new maximum number of times that an access token can be used.", + accessTokenTrustedIps: "The new IPs or CIDR ranges that access tokens can be used from." + }, + RETRIEVE: { + identityId: "The ID of the identity to retrieve the auth method for." + }, REVOKE: { - identityId: "The ID of the identity to revoke." + identityId: "The ID of the identity to revoke the auth method for." } } as const; export const AZURE_AUTH = { + LOGIN: { + identityId: "The ID of the identity to login." + }, + ATTACH: { + identityId: "The ID of the identity to attach the configuration onto.", + tenantId: "The tenant ID for the Azure AD organization.", + resource: "The resource URL for the application registered in Azure AD.", + allowedServicePrincipalIds: + "The comma-separated list of Azure AD service principal IDs that are allowed to authenticate with Infisical.", + accessTokenTrustedIps: "The IPs or CIDR ranges that access tokens can be used from.", + accessTokenTTL: "The lifetime for an acccess token in seconds.", + accessTokenMaxTTL: "The maximum lifetime for an acccess token in seconds.", + accessTokenNumUsesLimit: "The maximum number of times that an access token can be used." + }, + UPDATE: { + identityId: "The ID of the identity to update the auth method for.", + tenantId: "The new tenant ID for the Azure AD organization.", + resource: "The new resource URL for the application registered in Azure AD.", + allowedServicePrincipalIds: + "The new comma-separated list of Azure AD service principal IDs that are allowed to authenticate with Infisical.", + accessTokenTrustedIps: "The new IPs or CIDR ranges that access tokens can be used from.", + accessTokenTTL: "The new lifetime for an acccess token in seconds.", + accessTokenMaxTTL: "The new maximum lifetime for an acccess token in seconds.", + accessTokenNumUsesLimit: "The new maximum number of times that an access token can be used." + }, + RETRIEVE: { + identityId: "The ID of the identity to retrieve the auth method for." + }, REVOKE: { - identityId: "The ID of the identity to revoke." + identityId: "The ID of the identity to revoke the auth method for." } } as const; export const GCP_AUTH = { + LOGIN: { + identityId: "The ID of the identity to login." + }, + ATTACH: { + identityId: "The ID of the identity to attach the configuration onto.", + allowedServiceAccounts: + "The comma-separated list of trusted service account emails corresponding to the GCE resource(s) allowed to authenticate with Infisical.", + allowedProjects: + "The comma-separated list of trusted GCP projects that the GCE instance must belong to authenticate with Infisical.", + allowedZones: + "The comma-separated list of trusted zones that the GCE instances must belong to authenticate with Infisical.", + accessTokenTrustedIps: "The IPs or CIDR ranges that access tokens can be used from.", + accessTokenTTL: "The lifetime for an acccess token in seconds.", + accessTokenMaxTTL: "The maximum lifetime for an acccess token in seconds.", + accessTokenNumUsesLimit: "The maximum number of times that an access token can be used." + }, + UPDATE: { + identityId: "The ID of the identity to update the auth method for.", + allowedServiceAccounts: + "The new comma-separated list of trusted service account emails corresponding to the GCE resource(s) allowed to authenticate with Infisical.", + allowedProjects: + "The new comma-separated list of trusted GCP projects that the GCE instance must belong to authenticate with Infisical.", + allowedZones: + "The new comma-separated list of trusted zones that the GCE instances must belong to authenticate with Infisical.", + accessTokenTrustedIps: "The new IPs or CIDR ranges that access tokens can be used from.", + accessTokenTTL: "The new lifetime for an acccess token in seconds.", + accessTokenMaxTTL: "The new maximum lifetime for an acccess token in seconds.", + accessTokenNumUsesLimit: "The new maximum number of times that an access token can be used." + }, + RETRIEVE: { + identityId: "The ID of the identity to retrieve the auth method for." + }, REVOKE: { - identityId: "The ID of the identity to revoke." + identityId: "The ID of the identity to revoke the auth method for." } } as const; export const KUBERNETES_AUTH = { + LOGIN: { + identityId: "The ID of the identity to login." + }, + ATTACH: { + identityId: "The ID of the identity to attach the configuration onto.", + kubernetesHost: "The host string, host:port pair, or URL to the base of the Kubernetes API server.", + caCert: "The PEM-encoded CA cert for the Kubernetes API server.", + tokenReviewerJwt: + "The long-lived service account JWT token for Infisical to access the TokenReview API to validate other service account JWT tokens submitted by applications/pods.", + allowedNamespaces: + "The comma-separated list of trusted namespaces that service accounts must belong to authenticate with Infisical.", + allowedNames: "The comma-separated list of trusted service account names that can authenticate with Infisical.", + allowedAudience: + "The optional audience claim that the service account JWT token must have to authenticate with Infisical.", + accessTokenTrustedIps: "The IPs or CIDR ranges that access tokens can be used from.", + accessTokenTTL: "The lifetime for an acccess token in seconds.", + accessTokenMaxTTL: "The maximum lifetime for an acccess token in seconds.", + accessTokenNumUsesLimit: "The maximum number of times that an access token can be used." + }, + UPDATE: { + identityId: "The ID of the identity to update the auth method for.", + kubernetesHost: "The new host string, host:port pair, or URL to the base of the Kubernetes API server.", + caCert: "The new PEM-encoded CA cert for the Kubernetes API server.", + tokenReviewerJwt: + "The new long-lived service account JWT token for Infisical to access the TokenReview API to validate other service account JWT tokens submitted by applications/pods.", + allowedNamespaces: + "The new comma-separated list of trusted namespaces that service accounts must belong to authenticate with Infisical.", + allowedNames: "The new comma-separated list of trusted service account names that can authenticate with Infisical.", + allowedAudience: + "The new optional audience claim that the service account JWT token must have to authenticate with Infisical.", + accessTokenTrustedIps: "The new IPs or CIDR ranges that access tokens can be used from.", + accessTokenTTL: "The new lifetime for an acccess token in seconds.", + accessTokenMaxTTL: "The new maximum lifetime for an acccess token in seconds.", + accessTokenNumUsesLimit: "The new maximum number of times that an access token can be used." + }, + RETRIEVE: { + identityId: "The ID of the identity to retrieve the auth method for." + }, REVOKE: { - identityId: "The ID of the identity to revoke." + identityId: "The ID of the identity to revoke the auth method for." + } +} as const; + +export const TOKEN_AUTH = { + ATTACH: { + identityId: "The ID of the identity to attach the configuration onto.", + accessTokenTrustedIps: "The IPs or CIDR ranges that access tokens can be used from.", + accessTokenTTL: "The lifetime for an acccess token in seconds.", + accessTokenMaxTTL: "The maximum lifetime for an acccess token in seconds.", + accessTokenNumUsesLimit: "The maximum number of times that an access token can be used." + }, + UPDATE: { + identityId: "The ID of the identity to update the auth method for.", + accessTokenTrustedIps: "The new IPs or CIDR ranges that access tokens can be used from.", + accessTokenTTL: "The new lifetime for an acccess token in seconds.", + accessTokenMaxTTL: "The new maximum lifetime for an acccess token in seconds.", + accessTokenNumUsesLimit: "The new maximum number of times that an access token can be used." + }, + RETRIEVE: { + identityId: "The ID of the identity to retrieve the auth method for." + }, + REVOKE: { + identityId: "The ID of the identity to revoke the auth method for." + }, + GET_TOKENS: { + identityId: "The ID of the identity to list token metadata for.", + offset: "The offset to start from. If you enter 10, it will start from the 10th token.", + limit: "The number of tokens to return" + }, + CREATE_TOKEN: { + identityId: "The ID of the identity to create the token for.", + name: "The name of the token to create" + }, + UPDATE_TOKEN: { + tokenId: "The ID of the token to update metadata for", + name: "The name of the token to update to" + }, + REVOKE_TOKEN: { + tokenId: "The ID of the token to revoke" + } +} as const; + +export const OIDC_AUTH = { + LOGIN: { + identityId: "The ID of the identity to login." + }, + ATTACH: { + identityId: "The ID of the identity to attach the configuration onto.", + oidcDiscoveryUrl: "The URL used to retrieve the OpenID Connect configuration from the identity provider.", + caCert: "The PEM-encoded CA cert for establishing secure communication with the Identity Provider endpoints.", + boundIssuer: "The unique identifier of the identity provider issuing the JWT.", + boundAudiences: "The list of intended recipients.", + boundClaims: "The attributes that should be present in the JWT for it to be valid.", + boundSubject: "The expected principal that is the subject of the JWT.", + accessTokenTrustedIps: "The IPs or CIDR ranges that access tokens can be used from.", + accessTokenTTL: "The lifetime for an acccess token in seconds.", + accessTokenMaxTTL: "The maximum lifetime for an acccess token in seconds.", + accessTokenNumUsesLimit: "The maximum number of times that an access token can be used." + }, + UPDATE: { + identityId: "The ID of the identity to update the auth method for.", + oidcDiscoveryUrl: "The new URL used to retrieve the OpenID Connect configuration from the identity provider.", + caCert: "The new PEM-encoded CA cert for establishing secure communication with the Identity Provider endpoints.", + boundIssuer: "The new unique identifier of the identity provider issuing the JWT.", + boundAudiences: "The new list of intended recipients.", + boundClaims: "The new attributes that should be present in the JWT for it to be valid.", + boundSubject: "The new expected principal that is the subject of the JWT.", + accessTokenTrustedIps: "The new IPs or CIDR ranges that access tokens can be used from.", + accessTokenTTL: "The new lifetime for an acccess token in seconds.", + accessTokenMaxTTL: "The new maximum lifetime for an acccess token in seconds.", + accessTokenNumUsesLimit: "The new maximum number of times that an access token can be used." + }, + RETRIEVE: { + identityId: "The ID of the identity to retrieve the auth method for." + }, + REVOKE: { + identityId: "The ID of the identity to revoke the auth method for." } } as const; @@ -146,10 +348,15 @@ export const ORGANIZATIONS = { LIST_USER_MEMBERSHIPS: { organizationId: "The ID of the organization to get memberships from." }, + GET_USER_MEMBERSHIP: { + organizationId: "The ID of the organization to get the membership for.", + membershipId: "The ID of the membership to get." + }, UPDATE_USER_MEMBERSHIP: { organizationId: "The ID of the organization to update the membership for.", membershipId: "The ID of the membership to update.", - role: "The new role of the membership." + role: "The new role of the membership.", + isActive: "The active status of the membership" }, DELETE_USER_MEMBERSHIP: { organizationId: "The ID of the organization to delete the membership from.", @@ -313,6 +520,9 @@ export const FOLDERS = { path: "The path to list folders from.", directory: "The directory to list folders from. (Deprecated in favor of path)" }, + GET_BY_ID: { + folderId: "The id of the folder to get details." + }, CREATE: { workspaceId: "The ID of the project to create the folder in.", environment: "The slug of the environment to create the folder in.", diff --git a/backend/src/lib/types/index.ts b/backend/src/lib/types/index.ts index 2c41f4d23..4d892b02c 100644 --- a/backend/src/lib/types/index.ts +++ b/backend/src/lib/types/index.ts @@ -42,3 +42,13 @@ export type RequiredKeys = { }[keyof T]; export type PickRequired = Pick>; + +export enum EnforcementLevel { + Hard = "hard", + Soft = "soft" +} + +export enum SecretSharingAccessType { + Anyone = "anyone", + Organization = "organization" +} diff --git a/backend/src/server/routes/index.ts b/backend/src/server/routes/index.ts index 069d61569..59aa8d301 100644 --- a/backend/src/server/routes/index.ts +++ b/backend/src/server/routes/index.ts @@ -22,6 +22,8 @@ import { buildDynamicSecretProviders } from "@app/ee/services/dynamic-secret/pro import { dynamicSecretLeaseDALFactory } from "@app/ee/services/dynamic-secret-lease/dynamic-secret-lease-dal"; import { dynamicSecretLeaseQueueServiceFactory } from "@app/ee/services/dynamic-secret-lease/dynamic-secret-lease-queue"; import { dynamicSecretLeaseServiceFactory } from "@app/ee/services/dynamic-secret-lease/dynamic-secret-lease-service"; +import { externalKmsDALFactory } from "@app/ee/services/external-kms/external-kms-dal"; +import { externalKmsServiceFactory } from "@app/ee/services/external-kms/external-kms-service"; import { groupDALFactory } from "@app/ee/services/group/group-dal"; import { groupServiceFactory } from "@app/ee/services/group/group-service"; import { userGroupMembershipDALFactory } from "@app/ee/services/group/user-group-membership-dal"; @@ -102,6 +104,8 @@ import { identityGcpAuthDALFactory } from "@app/services/identity-gcp-auth/ident import { identityGcpAuthServiceFactory } from "@app/services/identity-gcp-auth/identity-gcp-auth-service"; import { identityKubernetesAuthDALFactory } from "@app/services/identity-kubernetes-auth/identity-kubernetes-auth-dal"; import { identityKubernetesAuthServiceFactory } from "@app/services/identity-kubernetes-auth/identity-kubernetes-auth-service"; +import { identityOidcAuthDALFactory } from "@app/services/identity-oidc-auth/identity-oidc-auth-dal"; +import { identityOidcAuthServiceFactory } from "@app/services/identity-oidc-auth/identity-oidc-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"; @@ -114,7 +118,8 @@ import { integrationDALFactory } from "@app/services/integration/integration-dal import { integrationServiceFactory } from "@app/services/integration/integration-service"; import { integrationAuthDALFactory } from "@app/services/integration-auth/integration-auth-dal"; import { integrationAuthServiceFactory } from "@app/services/integration-auth/integration-auth-service"; -import { kmsDALFactory } from "@app/services/kms/kms-dal"; +import { internalKmsDALFactory } from "@app/services/kms/internal-kms-dal"; +import { kmskeyDALFactory } from "@app/services/kms/kms-key-dal"; import { kmsRootConfigDALFactory } from "@app/services/kms/kms-root-config-dal"; import { kmsServiceFactory } from "@app/services/kms/kms-service"; import { incidentContactDALFactory } from "@app/services/org/incident-contacts-dal"; @@ -242,6 +247,7 @@ export const registerRoutes = async ( const identityUaClientSecretDAL = identityUaClientSecretDALFactory(db); const identityAwsAuthDAL = identityAwsAuthDALFactory(db); const identityGcpAuthDAL = identityGcpAuthDALFactory(db); + const identityOidcAuthDAL = identityOidcAuthDALFactory(db); const identityAzureAuthDAL = identityAzureAuthDALFactory(db); const auditLogDAL = auditLogDALFactory(db); @@ -285,7 +291,9 @@ export const registerRoutes = async ( const dynamicSecretDAL = dynamicSecretDALFactory(db); const dynamicSecretLeaseDAL = dynamicSecretLeaseDALFactory(db); - const kmsDAL = kmsDALFactory(db); + const kmsDAL = kmskeyDALFactory(db); + const internalKmsDAL = internalKmsDALFactory(db); + const externalKmsDAL = externalKmsDALFactory(db); const kmsRootConfigDAL = kmsRootConfigDALFactory(db); const permissionService = permissionServiceFactory({ @@ -299,7 +307,16 @@ export const registerRoutes = async ( const kmsService = kmsServiceFactory({ kmsRootConfigDAL, keyStore, - kmsDAL + kmsDAL, + internalKmsDAL, + orgDAL, + projectDAL + }); + const externalKmsService = externalKmsServiceFactory({ + kmsDAL, + kmsService, + permissionService, + externalKmsDAL }); const trustedIpService = trustedIpServiceFactory({ @@ -328,7 +345,7 @@ export const registerRoutes = async ( permissionService, secretApprovalPolicyDAL }); - const tokenService = tokenServiceFactory({ tokenDAL: authTokenDAL, userDAL }); + const tokenService = tokenServiceFactory({ tokenDAL: authTokenDAL, userDAL, orgMembershipDAL }); const samlService = samlConfigServiceFactory({ permissionService, @@ -440,6 +457,7 @@ export const registerRoutes = async ( tokenService, projectDAL, projectMembershipDAL, + orgMembershipDAL, projectKeyDAL, smtpService, userDAL, @@ -466,7 +484,8 @@ export const registerRoutes = async ( authService: loginService, serverCfgDAL: superAdminDAL, orgService, - keyStore + keyStore, + licenseService }); const rateLimitService = rateLimitServiceFactory({ rateLimitDAL, @@ -641,7 +660,8 @@ export const registerRoutes = async ( const webhookService = webhookServiceFactory({ permissionService, webhookDAL, - projectEnvDAL + projectEnvDAL, + projectDAL }); const secretTagService = secretTagServiceFactory({ secretTagDAL, permissionService }); @@ -709,12 +729,16 @@ export const registerRoutes = async ( secretQueueService, secretImportDAL, projectEnvDAL, - projectBotService + projectBotService, + secretApprovalPolicyService, + secretApprovalRequestDAL, + secretApprovalRequestSecretDAL }); const secretSharingService = secretSharingServiceFactory({ permissionService, - secretSharingDAL + secretSharingDAL, + orgDAL }); const secretApprovalRequestService = secretApprovalRequestServiceFactory({ @@ -731,7 +755,10 @@ export const registerRoutes = async ( secretApprovalRequestDAL, snapshotService, secretVersionTagDAL, - secretQueueService + secretQueueService, + smtpService, + userDAL, + projectEnvDAL }); const accessApprovalPolicyService = accessApprovalPolicyServiceFactory({ @@ -885,6 +912,16 @@ export const registerRoutes = async ( licenseService }); + const identityOidcAuthService = identityOidcAuthServiceFactory({ + identityOidcAuthDAL, + identityOrgMembershipDAL, + identityAccessTokenDAL, + identityDAL, + permissionService, + licenseService, + orgBotDAL + }); + const dynamicSecretProviders = buildDynamicSecretProviders(); const dynamicSecretQueueService = dynamicSecretLeaseQueueServiceFactory({ queueService, @@ -988,6 +1025,7 @@ export const registerRoutes = async ( identityGcpAuth: identityGcpAuthService, identityAwsAuth: identityAwsAuthService, identityAzureAuth: identityAzureAuthService, + identityOidcAuth: identityOidcAuthService, accessApprovalPolicy: accessApprovalPolicyService, accessApprovalRequest: accessApprovalRequestService, secretApprovalPolicy: secretApprovalPolicyService, @@ -1012,7 +1050,8 @@ export const registerRoutes = async ( projectUserAdditionalPrivilege: projectUserAdditionalPrivilegeService, identityProjectAdditionalPrivilege: identityProjectAdditionalPrivilegeService, secretSharing: secretSharingService, - userEngagement: userEngagementService + userEngagement: userEngagementService, + externalKms: externalKmsService }); const cronJobs: CronJob[] = []; 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 index 8a85323a6..9721e46c5 100644 --- a/backend/src/server/routes/v1/identity-aws-iam-auth-router.ts +++ b/backend/src/server/routes/v1/identity-aws-iam-auth-router.ts @@ -77,35 +77,45 @@ export const registerIdentityAwsAuthRouter = async (server: FastifyZodProvider) } ], params: z.object({ - identityId: z.string().trim() + identityId: z.string().trim().describe(AWS_AUTH.ATTACH.identityId) }), body: z.object({ - stsEndpoint: z.string().trim().min(1).default("https://sts.amazonaws.com/"), - allowedPrincipalArns: validatePrincipalArns, - allowedAccountIds: validateAccountIds, + stsEndpoint: z + .string() + .trim() + .min(1) + .default("https://sts.amazonaws.com/") + .describe(AWS_AUTH.ATTACH.stsEndpoint), + allowedPrincipalArns: validatePrincipalArns.describe(AWS_AUTH.ATTACH.allowedPrincipalArns), + allowedAccountIds: validateAccountIds.describe(AWS_AUTH.ATTACH.allowedAccountIds), accessTokenTrustedIps: z .object({ ipAddress: z.string().trim() }) .array() .min(1) - .default([{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }]), + .default([{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }]) + .describe(AWS_AUTH.ATTACH.accessTokenTrustedIps), accessTokenTTL: z .number() .int() .min(1) + .max(315360000) .refine((value) => value !== 0, { message: "accessTokenTTL must have a non zero number" }) - .default(2592000), + .default(2592000) + .describe(AWS_AUTH.ATTACH.accessTokenTTL), accessTokenMaxTTL: z .number() .int() + .max(315360000) .refine((value) => value !== 0, { message: "accessTokenMaxTTL must have a non zero number" }) - .default(2592000), - accessTokenNumUsesLimit: z.number().int().min(0).default(0) + .default(2592000) + .describe(AWS_AUTH.ATTACH.accessTokenMaxTTL), + accessTokenNumUsesLimit: z.number().int().min(0).default(0).describe(AWS_AUTH.ATTACH.accessTokenNumUsesLimit) }), response: { 200: z.object({ @@ -160,28 +170,31 @@ export const registerIdentityAwsAuthRouter = async (server: FastifyZodProvider) } ], params: z.object({ - identityId: z.string() + identityId: z.string().describe(AWS_AUTH.UPDATE.identityId) }), body: z.object({ - stsEndpoint: z.string().trim().min(1).optional(), - allowedPrincipalArns: validatePrincipalArns, - allowedAccountIds: validateAccountIds, + stsEndpoint: z.string().trim().min(1).optional().describe(AWS_AUTH.UPDATE.stsEndpoint), + allowedPrincipalArns: validatePrincipalArns.describe(AWS_AUTH.UPDATE.allowedPrincipalArns), + allowedAccountIds: validateAccountIds.describe(AWS_AUTH.UPDATE.allowedAccountIds), 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(), + .optional() + .describe(AWS_AUTH.UPDATE.accessTokenTrustedIps), + accessTokenTTL: z.number().int().min(0).max(315360000).optional().describe(AWS_AUTH.UPDATE.accessTokenTTL), + accessTokenNumUsesLimit: z.number().int().min(0).optional().describe(AWS_AUTH.UPDATE.accessTokenNumUsesLimit), accessTokenMaxTTL: z .number() .int() + .max(315360000) .refine((value) => value !== 0, { message: "accessTokenMaxTTL must have a non zero number" }) .optional() + .describe(AWS_AUTH.UPDATE.accessTokenMaxTTL) }), response: { 200: z.object({ @@ -236,7 +249,7 @@ export const registerIdentityAwsAuthRouter = async (server: FastifyZodProvider) } ], params: z.object({ - identityId: z.string() + identityId: z.string().describe(AWS_AUTH.RETRIEVE.identityId) }), response: { 200: z.object({ diff --git a/backend/src/server/routes/v1/identity-azure-auth-router.ts b/backend/src/server/routes/v1/identity-azure-auth-router.ts index 6b4a7fb37..589f9e92c 100644 --- a/backend/src/server/routes/v1/identity-azure-auth-router.ts +++ b/backend/src/server/routes/v1/identity-azure-auth-router.ts @@ -19,7 +19,7 @@ export const registerIdentityAzureAuthRouter = async (server: FastifyZodProvider schema: { description: "Login with Azure Auth", body: z.object({ - identityId: z.string(), + identityId: z.string().describe(AZURE_AUTH.LOGIN.identityId), jwt: z.string() }), response: { @@ -72,35 +72,40 @@ export const registerIdentityAzureAuthRouter = async (server: FastifyZodProvider } ], params: z.object({ - identityId: z.string().trim() + identityId: z.string().trim().describe(AZURE_AUTH.LOGIN.identityId) }), body: z.object({ - tenantId: z.string().trim(), - resource: z.string().trim(), - allowedServicePrincipalIds: validateAzureAuthField, + tenantId: z.string().trim().describe(AZURE_AUTH.ATTACH.tenantId), + resource: z.string().trim().describe(AZURE_AUTH.ATTACH.resource), + allowedServicePrincipalIds: validateAzureAuthField.describe(AZURE_AUTH.ATTACH.allowedServicePrincipalIds), accessTokenTrustedIps: z .object({ ipAddress: z.string().trim() }) .array() .min(1) - .default([{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }]), + .default([{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }]) + .describe(AZURE_AUTH.ATTACH.accessTokenTrustedIps), accessTokenTTL: z .number() .int() .min(1) + .max(315360000) .refine((value) => value !== 0, { message: "accessTokenTTL must have a non zero number" }) - .default(2592000), + .default(2592000) + .describe(AZURE_AUTH.ATTACH.accessTokenTTL), accessTokenMaxTTL: z .number() .int() + .max(315360000) .refine((value) => value !== 0, { message: "accessTokenMaxTTL must have a non zero number" }) - .default(2592000), - accessTokenNumUsesLimit: z.number().int().min(0).default(0) + .default(2592000) + .describe(AZURE_AUTH.ATTACH.accessTokenMaxTTL), + accessTokenNumUsesLimit: z.number().int().min(0).default(0).describe(AZURE_AUTH.ATTACH.accessTokenNumUsesLimit) }), response: { 200: z.object({ @@ -154,28 +159,33 @@ export const registerIdentityAzureAuthRouter = async (server: FastifyZodProvider } ], params: z.object({ - identityId: z.string().trim() + identityId: z.string().trim().describe(AZURE_AUTH.UPDATE.identityId) }), body: z.object({ - tenantId: z.string().trim().optional(), - resource: z.string().trim().optional(), - allowedServicePrincipalIds: validateAzureAuthField.optional(), + tenantId: z.string().trim().optional().describe(AZURE_AUTH.UPDATE.tenantId), + resource: z.string().trim().optional().describe(AZURE_AUTH.UPDATE.resource), + allowedServicePrincipalIds: validateAzureAuthField + .optional() + .describe(AZURE_AUTH.UPDATE.allowedServicePrincipalIds), 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(), + .optional() + .describe(AZURE_AUTH.UPDATE.accessTokenTrustedIps), + accessTokenTTL: z.number().int().min(0).max(315360000).optional().describe(AZURE_AUTH.UPDATE.accessTokenTTL), + accessTokenNumUsesLimit: z.number().int().min(0).optional().describe(AZURE_AUTH.UPDATE.accessTokenNumUsesLimit), accessTokenMaxTTL: z .number() .int() + .max(315360000) .refine((value) => value !== 0, { message: "accessTokenMaxTTL must have a non zero number" }) .optional() + .describe(AZURE_AUTH.UPDATE.accessTokenMaxTTL) }), response: { 200: z.object({ @@ -229,7 +239,7 @@ export const registerIdentityAzureAuthRouter = async (server: FastifyZodProvider } ], params: z.object({ - identityId: z.string() + identityId: z.string().describe(AZURE_AUTH.RETRIEVE.identityId) }), response: { 200: z.object({ diff --git a/backend/src/server/routes/v1/identity-gcp-auth-router.ts b/backend/src/server/routes/v1/identity-gcp-auth-router.ts index 0deeb95d3..2022aef58 100644 --- a/backend/src/server/routes/v1/identity-gcp-auth-router.ts +++ b/backend/src/server/routes/v1/identity-gcp-auth-router.ts @@ -19,7 +19,7 @@ export const registerIdentityGcpAuthRouter = async (server: FastifyZodProvider) schema: { description: "Login with GCP Auth", body: z.object({ - identityId: z.string(), + identityId: z.string().describe(GCP_AUTH.LOGIN.identityId), jwt: z.string() }), response: { @@ -72,36 +72,41 @@ export const registerIdentityGcpAuthRouter = async (server: FastifyZodProvider) } ], params: z.object({ - identityId: z.string().trim() + identityId: z.string().trim().describe(GCP_AUTH.ATTACH.identityId) }), body: z.object({ type: z.enum(["iam", "gce"]), - allowedServiceAccounts: validateGcpAuthField, - allowedProjects: validateGcpAuthField, - allowedZones: validateGcpAuthField, + allowedServiceAccounts: validateGcpAuthField.describe(GCP_AUTH.ATTACH.allowedServiceAccounts), + allowedProjects: validateGcpAuthField.describe(GCP_AUTH.ATTACH.allowedProjects), + allowedZones: validateGcpAuthField.describe(GCP_AUTH.ATTACH.allowedZones), accessTokenTrustedIps: z .object({ ipAddress: z.string().trim() }) .array() .min(1) - .default([{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }]), + .default([{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }]) + .describe(GCP_AUTH.ATTACH.accessTokenTrustedIps), accessTokenTTL: z .number() .int() .min(1) + .max(315360000) .refine((value) => value !== 0, { message: "accessTokenTTL must have a non zero number" }) - .default(2592000), + .default(2592000) + .describe(GCP_AUTH.ATTACH.accessTokenTTL), accessTokenMaxTTL: z .number() .int() + .max(315360000) .refine((value) => value !== 0, { message: "accessTokenMaxTTL must have a non zero number" }) - .default(2592000), - accessTokenNumUsesLimit: z.number().int().min(0).default(0) + .default(2592000) + .describe(GCP_AUTH.ATTACH.accessTokenMaxTTL), + accessTokenNumUsesLimit: z.number().int().min(0).default(0).describe(GCP_AUTH.ATTACH.accessTokenNumUsesLimit) }), response: { 200: z.object({ @@ -157,29 +162,32 @@ export const registerIdentityGcpAuthRouter = async (server: FastifyZodProvider) } ], params: z.object({ - identityId: z.string().trim() + identityId: z.string().trim().describe(GCP_AUTH.UPDATE.identityId) }), body: z.object({ type: z.enum(["iam", "gce"]).optional(), - allowedServiceAccounts: validateGcpAuthField.optional(), - allowedProjects: validateGcpAuthField.optional(), - allowedZones: validateGcpAuthField.optional(), + allowedServiceAccounts: validateGcpAuthField.optional().describe(GCP_AUTH.UPDATE.allowedServiceAccounts), + allowedProjects: validateGcpAuthField.optional().describe(GCP_AUTH.UPDATE.allowedProjects), + allowedZones: validateGcpAuthField.optional().describe(GCP_AUTH.UPDATE.allowedZones), 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(), + .optional() + .describe(GCP_AUTH.UPDATE.accessTokenTrustedIps), + accessTokenTTL: z.number().int().min(0).max(315360000).optional().describe(GCP_AUTH.UPDATE.accessTokenTTL), + accessTokenNumUsesLimit: z.number().int().min(0).optional().describe(GCP_AUTH.UPDATE.accessTokenNumUsesLimit), accessTokenMaxTTL: z .number() .int() + .max(315360000) .refine((value) => value !== 0, { message: "accessTokenMaxTTL must have a non zero number" }) .optional() + .describe(GCP_AUTH.UPDATE.accessTokenMaxTTL) }), response: { 200: z.object({ @@ -235,7 +243,7 @@ export const registerIdentityGcpAuthRouter = async (server: FastifyZodProvider) } ], params: z.object({ - identityId: z.string() + identityId: z.string().describe(GCP_AUTH.RETRIEVE.identityId) }), response: { 200: z.object({ diff --git a/backend/src/server/routes/v1/identity-kubernetes-auth-router.ts b/backend/src/server/routes/v1/identity-kubernetes-auth-router.ts index 4c54f1e7c..3a71ba7a2 100644 --- a/backend/src/server/routes/v1/identity-kubernetes-auth-router.ts +++ b/backend/src/server/routes/v1/identity-kubernetes-auth-router.ts @@ -30,7 +30,7 @@ export const registerIdentityKubernetesRouter = async (server: FastifyZodProvide schema: { description: "Login with Kubernetes Auth", body: z.object({ - identityId: z.string().trim(), + identityId: z.string().trim().describe(KUBERNETES_AUTH.LOGIN.identityId), jwt: z.string().trim() }), response: { @@ -85,38 +85,48 @@ export const registerIdentityKubernetesRouter = async (server: FastifyZodProvide } ], params: z.object({ - identityId: z.string().trim() + identityId: z.string().trim().describe(KUBERNETES_AUTH.ATTACH.identityId) }), body: z.object({ - kubernetesHost: z.string().trim().min(1), - caCert: z.string().trim().default(""), - tokenReviewerJwt: z.string().trim().min(1), - allowedNamespaces: z.string(), // TODO: validation - allowedNames: z.string(), - allowedAudience: z.string(), + kubernetesHost: z.string().trim().min(1).describe(KUBERNETES_AUTH.ATTACH.kubernetesHost), + caCert: z.string().trim().default("").describe(KUBERNETES_AUTH.ATTACH.caCert), + tokenReviewerJwt: z.string().trim().min(1).describe(KUBERNETES_AUTH.ATTACH.tokenReviewerJwt), + allowedNamespaces: z.string().describe(KUBERNETES_AUTH.ATTACH.allowedNamespaces), // TODO: validation + allowedNames: z.string().describe(KUBERNETES_AUTH.ATTACH.allowedNames), + allowedAudience: z.string().describe(KUBERNETES_AUTH.ATTACH.allowedAudience), accessTokenTrustedIps: z .object({ ipAddress: z.string().trim() }) .array() .min(1) - .default([{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }]), + .default([{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }]) + .describe(KUBERNETES_AUTH.ATTACH.accessTokenTrustedIps), accessTokenTTL: z .number() .int() .min(1) + .max(315360000) .refine((value) => value !== 0, { message: "accessTokenTTL must have a non zero number" }) - .default(2592000), + .default(2592000) + .describe(KUBERNETES_AUTH.ATTACH.accessTokenTTL), accessTokenMaxTTL: z .number() .int() + .max(315360000) .refine((value) => value !== 0, { message: "accessTokenMaxTTL must have a non zero number" }) - .default(2592000), - accessTokenNumUsesLimit: z.number().int().min(0).default(0) + .default(2592000) + .describe(KUBERNETES_AUTH.ATTACH.accessTokenMaxTTL), + accessTokenNumUsesLimit: z + .number() + .int() + .min(0) + .default(0) + .describe(KUBERNETES_AUTH.ATTACH.accessTokenNumUsesLimit) }), response: { 200: z.object({ @@ -171,31 +181,45 @@ export const registerIdentityKubernetesRouter = async (server: FastifyZodProvide } ], params: z.object({ - identityId: z.string() + identityId: z.string().describe(KUBERNETES_AUTH.UPDATE.identityId) }), body: z.object({ - kubernetesHost: z.string().trim().min(1).optional(), - caCert: z.string().trim().optional(), - tokenReviewerJwt: z.string().trim().min(1).optional(), - allowedNamespaces: z.string().optional(), // TODO: validation - allowedNames: z.string().optional(), - allowedAudience: z.string().optional(), + kubernetesHost: z.string().trim().min(1).optional().describe(KUBERNETES_AUTH.UPDATE.kubernetesHost), + caCert: z.string().trim().optional().describe(KUBERNETES_AUTH.UPDATE.caCert), + tokenReviewerJwt: z.string().trim().min(1).optional().describe(KUBERNETES_AUTH.UPDATE.tokenReviewerJwt), + allowedNamespaces: z.string().optional().describe(KUBERNETES_AUTH.UPDATE.allowedNamespaces), // TODO: validation + allowedNames: z.string().optional().describe(KUBERNETES_AUTH.UPDATE.allowedNames), + allowedAudience: z.string().optional().describe(KUBERNETES_AUTH.UPDATE.allowedAudience), 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(), + .optional() + .describe(KUBERNETES_AUTH.UPDATE.accessTokenTrustedIps), + accessTokenTTL: z + .number() + .int() + .min(0) + .max(315360000) + .optional() + .describe(KUBERNETES_AUTH.UPDATE.accessTokenTTL), + accessTokenNumUsesLimit: z + .number() + .int() + .min(0) + .optional() + .describe(KUBERNETES_AUTH.UPDATE.accessTokenNumUsesLimit), accessTokenMaxTTL: z .number() .int() + .max(315360000) .refine((value) => value !== 0, { message: "accessTokenMaxTTL must have a non zero number" }) .optional() + .describe(KUBERNETES_AUTH.UPDATE.accessTokenMaxTTL) }), response: { 200: z.object({ @@ -250,7 +274,7 @@ export const registerIdentityKubernetesRouter = async (server: FastifyZodProvide } ], params: z.object({ - identityId: z.string() + identityId: z.string().describe(KUBERNETES_AUTH.RETRIEVE.identityId) }), response: { 200: z.object({ diff --git a/backend/src/server/routes/v1/identity-oidc-auth-router.ts b/backend/src/server/routes/v1/identity-oidc-auth-router.ts new file mode 100644 index 000000000..280dbc5d5 --- /dev/null +++ b/backend/src/server/routes/v1/identity-oidc-auth-router.ts @@ -0,0 +1,361 @@ +import { z } from "zod"; + +import { IdentityOidcAuthsSchema } from "@app/db/schemas"; +import { EventType } from "@app/ee/services/audit-log/audit-log-types"; +import { OIDC_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 { + validateOidcAuthAudiencesField, + validateOidcBoundClaimsField +} from "@app/services/identity-oidc-auth/identity-oidc-auth-validators"; + +const IdentityOidcAuthResponseSchema = IdentityOidcAuthsSchema.omit({ + encryptedCaCert: true, + caCertIV: true, + caCertTag: true +}).extend({ + caCert: z.string() +}); + +export const registerIdentityOidcAuthRouter = async (server: FastifyZodProvider) => { + server.route({ + method: "POST", + url: "/oidc-auth/login", + config: { + rateLimit: writeLimit + }, + schema: { + description: "Login with OIDC Auth", + body: z.object({ + identityId: z.string().trim().describe(OIDC_AUTH.LOGIN.identityId), + jwt: z.string().trim() + }), + response: { + 200: z.object({ + accessToken: z.string(), + expiresIn: z.coerce.number(), + accessTokenMaxTTL: z.coerce.number(), + tokenType: z.literal("Bearer") + }) + } + }, + handler: async (req) => { + const { identityOidcAuth, accessToken, identityAccessToken, identityMembershipOrg } = + await server.services.identityOidcAuth.login({ + identityId: req.body.identityId, + jwt: req.body.jwt + }); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + orgId: identityMembershipOrg?.orgId, + event: { + type: EventType.LOGIN_IDENTITY_OIDC_AUTH, + metadata: { + identityId: identityOidcAuth.identityId, + identityAccessTokenId: identityAccessToken.id, + identityOidcAuthId: identityOidcAuth.id + } + } + }); + return { + accessToken, + tokenType: "Bearer" as const, + expiresIn: identityOidcAuth.accessTokenTTL, + accessTokenMaxTTL: identityOidcAuth.accessTokenMaxTTL + }; + } + }); + + server.route({ + method: "POST", + url: "/oidc-auth/identities/:identityId", + config: { + rateLimit: writeLimit + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + schema: { + description: "Attach OIDC Auth configuration onto identity", + security: [ + { + bearerAuth: [] + } + ], + params: z.object({ + identityId: z.string().trim().describe(OIDC_AUTH.ATTACH.identityId) + }), + body: z.object({ + oidcDiscoveryUrl: z.string().url().min(1).describe(OIDC_AUTH.ATTACH.oidcDiscoveryUrl), + caCert: z.string().trim().default("").describe(OIDC_AUTH.ATTACH.caCert), + boundIssuer: z.string().min(1).describe(OIDC_AUTH.ATTACH.boundIssuer), + boundAudiences: validateOidcAuthAudiencesField.describe(OIDC_AUTH.ATTACH.boundAudiences), + boundClaims: validateOidcBoundClaimsField.describe(OIDC_AUTH.ATTACH.boundClaims), + boundSubject: z.string().optional().default("").describe(OIDC_AUTH.ATTACH.boundSubject), + accessTokenTrustedIps: z + .object({ + ipAddress: z.string().trim() + }) + .array() + .min(1) + .default([{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }]) + .describe(OIDC_AUTH.ATTACH.accessTokenTrustedIps), + accessTokenTTL: z + .number() + .int() + .min(1) + .max(315360000) + .refine((value) => value !== 0, { + message: "accessTokenTTL must have a non zero number" + }) + .default(2592000) + .describe(OIDC_AUTH.ATTACH.accessTokenTTL), + accessTokenMaxTTL: z + .number() + .int() + .max(315360000) + .refine((value) => value !== 0, { + message: "accessTokenMaxTTL must have a non zero number" + }) + .default(2592000) + .describe(OIDC_AUTH.ATTACH.accessTokenMaxTTL), + accessTokenNumUsesLimit: z.number().int().min(0).default(0).describe(OIDC_AUTH.ATTACH.accessTokenNumUsesLimit) + }), + response: { + 200: z.object({ + identityOidcAuth: IdentityOidcAuthResponseSchema + }) + } + }, + handler: async (req) => { + const identityOidcAuth = await server.services.identityOidcAuth.attachOidcAuth({ + 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: identityOidcAuth.orgId, + event: { + type: EventType.ADD_IDENTITY_OIDC_AUTH, + metadata: { + identityId: identityOidcAuth.identityId, + oidcDiscoveryUrl: identityOidcAuth.oidcDiscoveryUrl, + caCert: identityOidcAuth.caCert, + boundIssuer: identityOidcAuth.boundIssuer, + boundAudiences: identityOidcAuth.boundAudiences, + boundClaims: identityOidcAuth.boundClaims as Record, + boundSubject: identityOidcAuth.boundSubject as string, + accessTokenTTL: identityOidcAuth.accessTokenTTL, + accessTokenMaxTTL: identityOidcAuth.accessTokenMaxTTL, + accessTokenTrustedIps: identityOidcAuth.accessTokenTrustedIps as TIdentityTrustedIp[], + accessTokenNumUsesLimit: identityOidcAuth.accessTokenNumUsesLimit + } + } + }); + + return { + identityOidcAuth + }; + } + }); + + server.route({ + method: "PATCH", + url: "/oidc-auth/identities/:identityId", + config: { + rateLimit: writeLimit + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + schema: { + description: "Update OIDC Auth configuration on identity", + security: [ + { + bearerAuth: [] + } + ], + params: z.object({ + identityId: z.string().trim().describe(OIDC_AUTH.UPDATE.identityId) + }), + body: z + .object({ + oidcDiscoveryUrl: z.string().url().min(1).describe(OIDC_AUTH.UPDATE.oidcDiscoveryUrl), + caCert: z.string().trim().default("").describe(OIDC_AUTH.UPDATE.caCert), + boundIssuer: z.string().min(1).describe(OIDC_AUTH.UPDATE.boundIssuer), + boundAudiences: validateOidcAuthAudiencesField.describe(OIDC_AUTH.UPDATE.boundAudiences), + boundClaims: validateOidcBoundClaimsField.describe(OIDC_AUTH.UPDATE.boundClaims), + boundSubject: z.string().optional().default("").describe(OIDC_AUTH.UPDATE.boundSubject), + accessTokenTrustedIps: z + .object({ + ipAddress: z.string().trim() + }) + .array() + .min(1) + .default([{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }]) + .describe(OIDC_AUTH.UPDATE.accessTokenTrustedIps), + accessTokenTTL: z + .number() + .int() + .min(1) + .max(315360000) + .refine((value) => value !== 0, { + message: "accessTokenTTL must have a non zero number" + }) + .default(2592000) + .describe(OIDC_AUTH.UPDATE.accessTokenTTL), + accessTokenMaxTTL: z + .number() + .int() + .max(315360000) + .refine((value) => value !== 0, { + message: "accessTokenMaxTTL must have a non zero number" + }) + .default(2592000) + .describe(OIDC_AUTH.UPDATE.accessTokenMaxTTL), + + accessTokenNumUsesLimit: z.number().int().min(0).default(0).describe(OIDC_AUTH.UPDATE.accessTokenNumUsesLimit) + }) + .partial(), + response: { + 200: z.object({ + identityOidcAuth: IdentityOidcAuthResponseSchema + }) + } + }, + handler: async (req) => { + const identityOidcAuth = await server.services.identityOidcAuth.updateOidcAuth({ + actor: req.permission.type, + actorId: req.permission.id, + actorOrgId: req.permission.orgId, + actorAuthMethod: req.permission.authMethod, + ...req.body, + identityId: req.params.identityId + }); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + orgId: identityOidcAuth.orgId, + event: { + type: EventType.UPDATE_IDENTITY_OIDC_AUTH, + metadata: { + identityId: identityOidcAuth.identityId, + oidcDiscoveryUrl: identityOidcAuth.oidcDiscoveryUrl, + caCert: identityOidcAuth.caCert, + boundIssuer: identityOidcAuth.boundIssuer, + boundAudiences: identityOidcAuth.boundAudiences, + boundClaims: identityOidcAuth.boundClaims as Record, + boundSubject: identityOidcAuth.boundSubject as string, + accessTokenTTL: identityOidcAuth.accessTokenTTL, + accessTokenMaxTTL: identityOidcAuth.accessTokenMaxTTL, + accessTokenTrustedIps: identityOidcAuth.accessTokenTrustedIps as TIdentityTrustedIp[], + accessTokenNumUsesLimit: identityOidcAuth.accessTokenNumUsesLimit + } + } + }); + + return { identityOidcAuth }; + } + }); + + server.route({ + method: "GET", + url: "/oidc-auth/identities/:identityId", + config: { + rateLimit: readLimit + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + schema: { + description: "Retrieve OIDC Auth configuration on identity", + security: [ + { + bearerAuth: [] + } + ], + params: z.object({ + identityId: z.string().describe(OIDC_AUTH.RETRIEVE.identityId) + }), + response: { + 200: z.object({ + identityOidcAuth: IdentityOidcAuthResponseSchema + }) + } + }, + handler: async (req) => { + const identityOidcAuth = await server.services.identityOidcAuth.getOidcAuth({ + 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: identityOidcAuth.orgId, + event: { + type: EventType.GET_IDENTITY_OIDC_AUTH, + metadata: { + identityId: identityOidcAuth.identityId + } + } + }); + + return { identityOidcAuth }; + } + }); + + server.route({ + method: "DELETE", + url: "/oidc-auth/identities/:identityId", + config: { + rateLimit: writeLimit + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + schema: { + description: "Delete OIDC Auth configuration on identity", + security: [ + { + bearerAuth: [] + } + ], + params: z.object({ + identityId: z.string().describe(OIDC_AUTH.REVOKE.identityId) + }), + response: { + 200: z.object({ + identityOidcAuth: IdentityOidcAuthResponseSchema.omit({ + caCert: true + }) + }) + } + }, + handler: async (req) => { + const identityOidcAuth = await server.services.identityOidcAuth.revokeOidcAuth({ + actor: req.permission.type, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + identityId: req.params.identityId + }); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + orgId: identityOidcAuth.orgId, + event: { + type: EventType.REVOKE_IDENTITY_OIDC_AUTH, + metadata: { + identityId: identityOidcAuth.identityId + } + } + }); + + return { identityOidcAuth }; + } + }); +}; diff --git a/backend/src/server/routes/v1/identity-token-auth-router.ts b/backend/src/server/routes/v1/identity-token-auth-router.ts index ac36c3c58..f367e6033 100644 --- a/backend/src/server/routes/v1/identity-token-auth-router.ts +++ b/backend/src/server/routes/v1/identity-token-auth-router.ts @@ -2,6 +2,7 @@ import { z } from "zod"; import { IdentityAccessTokensSchema, IdentityTokenAuthsSchema } from "@app/db/schemas"; import { EventType } from "@app/ee/services/audit-log/audit-log-types"; +import { TOKEN_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"; @@ -23,7 +24,7 @@ export const registerIdentityTokenAuthRouter = async (server: FastifyZodProvider } ], params: z.object({ - identityId: z.string().trim() + identityId: z.string().trim().describe(TOKEN_AUTH.ATTACH.identityId) }), body: z.object({ accessTokenTrustedIps: z @@ -32,23 +33,28 @@ export const registerIdentityTokenAuthRouter = async (server: FastifyZodProvider }) .array() .min(1) - .default([{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }]), + .default([{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }]) + .describe(TOKEN_AUTH.ATTACH.accessTokenTrustedIps), accessTokenTTL: z .number() .int() .min(1) + .max(315360000) .refine((value) => value !== 0, { message: "accessTokenTTL must have a non zero number" }) - .default(2592000), + .default(2592000) + .describe(TOKEN_AUTH.ATTACH.accessTokenTTL), accessTokenMaxTTL: z .number() .int() + .max(315360000) .refine((value) => value !== 0, { message: "accessTokenMaxTTL must have a non zero number" }) - .default(2592000), - accessTokenNumUsesLimit: z.number().int().min(0).default(0) + .default(2592000) + .describe(TOKEN_AUTH.ATTACH.accessTokenMaxTTL), + accessTokenNumUsesLimit: z.number().int().min(0).default(0).describe(TOKEN_AUTH.ATTACH.accessTokenNumUsesLimit) }), response: { 200: z.object({ @@ -102,7 +108,7 @@ export const registerIdentityTokenAuthRouter = async (server: FastifyZodProvider } ], params: z.object({ - identityId: z.string().trim() + identityId: z.string().trim().describe(TOKEN_AUTH.UPDATE.identityId) }), body: z.object({ accessTokenTrustedIps: z @@ -111,16 +117,19 @@ export const registerIdentityTokenAuthRouter = async (server: FastifyZodProvider }) .array() .min(1) - .optional(), - accessTokenTTL: z.number().int().min(0).optional(), - accessTokenNumUsesLimit: z.number().int().min(0).optional(), + .optional() + .describe(TOKEN_AUTH.UPDATE.accessTokenTrustedIps), + accessTokenTTL: z.number().int().min(0).max(315360000).optional().describe(TOKEN_AUTH.UPDATE.accessTokenTTL), + accessTokenNumUsesLimit: z.number().int().min(0).optional().describe(TOKEN_AUTH.UPDATE.accessTokenNumUsesLimit), accessTokenMaxTTL: z .number() .int() + .max(315360000) .refine((value) => value !== 0, { message: "accessTokenMaxTTL must have a non zero number" }) .optional() + .describe(TOKEN_AUTH.UPDATE.accessTokenMaxTTL) }), response: { 200: z.object({ @@ -174,7 +183,7 @@ export const registerIdentityTokenAuthRouter = async (server: FastifyZodProvider } ], params: z.object({ - identityId: z.string() + identityId: z.string().describe(TOKEN_AUTH.RETRIEVE.identityId) }), response: { 200: z.object({ @@ -221,7 +230,7 @@ export const registerIdentityTokenAuthRouter = async (server: FastifyZodProvider } ], params: z.object({ - identityId: z.string() + identityId: z.string().describe(TOKEN_AUTH.REVOKE.identityId) }), response: { 200: z.object({ @@ -253,15 +262,6 @@ export const registerIdentityTokenAuthRouter = async (server: FastifyZodProvider } }); - // proposed - // update token by id: PATCH /token-auth/tokens/:tokenId - // revoke token by id: POST /token-auth/tokens/:tokenId/revoke - - // current - // revoke token by id: POST /token/revoke-by-id - - // token-auth/identities/:identityId/tokens - server.route({ method: "POST", url: "/token-auth/identities/:identityId/tokens", @@ -270,17 +270,17 @@ export const registerIdentityTokenAuthRouter = async (server: FastifyZodProvider }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), schema: { - description: "Create token for identity with Token Auth configured", + description: "Create token for identity with Token Auth", security: [ { bearerAuth: [] } ], params: z.object({ - identityId: z.string() + identityId: z.string().describe(TOKEN_AUTH.CREATE_TOKEN.identityId) }), body: z.object({ - name: z.string().optional() + name: z.string().optional().describe(TOKEN_AUTH.CREATE_TOKEN.name) }), response: { 200: z.object({ @@ -331,18 +331,18 @@ export const registerIdentityTokenAuthRouter = async (server: FastifyZodProvider }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), schema: { - description: "Get tokens for identity with Token Auth configured", + description: "Get tokens for identity with Token Auth", security: [ { bearerAuth: [] } ], params: z.object({ - identityId: z.string() + identityId: z.string().describe(TOKEN_AUTH.GET_TOKENS.identityId) }), querystring: z.object({ - offset: z.coerce.number().min(0).max(100).default(0), - limit: z.coerce.number().min(1).max(100).default(20) + offset: z.coerce.number().min(0).max(100).default(0).describe(TOKEN_AUTH.GET_TOKENS.offset), + limit: z.coerce.number().min(1).max(100).default(20).describe(TOKEN_AUTH.GET_TOKENS.limit) }), response: { 200: z.object({ @@ -383,17 +383,17 @@ export const registerIdentityTokenAuthRouter = async (server: FastifyZodProvider }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), schema: { - description: "Update token for identity with Token Auth configured", + description: "Update token for identity with Token Auth", security: [ { bearerAuth: [] } ], params: z.object({ - tokenId: z.string() + tokenId: z.string().describe(TOKEN_AUTH.UPDATE_TOKEN.tokenId) }), body: z.object({ - name: z.string().optional() + name: z.string().optional().describe(TOKEN_AUTH.UPDATE_TOKEN.name) }), response: { 200: z.object({ @@ -436,14 +436,14 @@ export const registerIdentityTokenAuthRouter = async (server: FastifyZodProvider }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), schema: { - description: "Revoke token for identity with Token Auth configured", + description: "Revoke token for identity with Token Auth", security: [ { bearerAuth: [] } ], params: z.object({ - tokenId: z.string() + tokenId: z.string().describe(TOKEN_AUTH.REVOKE_TOKEN.tokenId) }), response: { 200: z.object({ diff --git a/backend/src/server/routes/v1/identity-universal-auth-router.ts b/backend/src/server/routes/v1/identity-universal-auth-router.ts index b5a63f0db..f103a39e0 100644 --- a/backend/src/server/routes/v1/identity-universal-auth-router.ts +++ b/backend/src/server/routes/v1/identity-universal-auth-router.ts @@ -107,6 +107,7 @@ export const registerIdentityUaRouter = async (server: FastifyZodProvider) => { .number() .int() .min(1) + .max(315360000) .refine((value) => value !== 0, { message: "accessTokenTTL must have a non zero number" }) @@ -115,6 +116,7 @@ export const registerIdentityUaRouter = async (server: FastifyZodProvider) => { accessTokenMaxTTL: z .number() .int() + .max(315360000) .refine((value) => value !== 0, { message: "accessTokenMaxTTL must have a non zero number" }) @@ -196,7 +198,13 @@ export const registerIdentityUaRouter = async (server: FastifyZodProvider) => { .min(1) .optional() .describe(UNIVERSAL_AUTH.UPDATE.accessTokenTrustedIps), - accessTokenTTL: z.number().int().min(0).optional().describe(UNIVERSAL_AUTH.UPDATE.accessTokenTTL), + accessTokenTTL: z + .number() + .int() + .min(0) + .max(315360000) + .optional() + .describe(UNIVERSAL_AUTH.UPDATE.accessTokenTTL), accessTokenNumUsesLimit: z .number() .int() @@ -206,6 +214,7 @@ export const registerIdentityUaRouter = async (server: FastifyZodProvider) => { accessTokenMaxTTL: z .number() .int() + .max(315360000) .refine((value) => value !== 0, { message: "accessTokenMaxTTL must have a non zero number" }) @@ -362,7 +371,7 @@ export const registerIdentityUaRouter = async (server: FastifyZodProvider) => { body: z.object({ description: z.string().trim().default("").describe(UNIVERSAL_AUTH.CREATE_CLIENT_SECRET.description), numUsesLimit: z.number().min(0).default(0).describe(UNIVERSAL_AUTH.CREATE_CLIENT_SECRET.numUsesLimit), - ttl: z.number().min(0).default(0).describe(UNIVERSAL_AUTH.CREATE_CLIENT_SECRET.ttl) + ttl: z.number().min(0).max(315360000).default(0).describe(UNIVERSAL_AUTH.CREATE_CLIENT_SECRET.ttl) }), response: { 200: z.object({ diff --git a/backend/src/server/routes/v1/index.ts b/backend/src/server/routes/v1/index.ts index 0e9e48abd..43ce44eaa 100644 --- a/backend/src/server/routes/v1/index.ts +++ b/backend/src/server/routes/v1/index.ts @@ -8,6 +8,7 @@ import { registerIdentityAwsAuthRouter } from "./identity-aws-iam-auth-router"; import { registerIdentityAzureAuthRouter } from "./identity-azure-auth-router"; import { registerIdentityGcpAuthRouter } from "./identity-gcp-auth-router"; import { registerIdentityKubernetesRouter } from "./identity-kubernetes-auth-router"; +import { registerIdentityOidcAuthRouter } from "./identity-oidc-auth-router"; import { registerIdentityRouter } from "./identity-router"; import { registerIdentityTokenAuthRouter } from "./identity-token-auth-router"; import { registerIdentityUaRouter } from "./identity-universal-auth-router"; @@ -42,6 +43,7 @@ export const registerV1Routes = async (server: FastifyZodProvider) => { await authRouter.register(registerIdentityAccessTokenRouter); await authRouter.register(registerIdentityAwsAuthRouter); await authRouter.register(registerIdentityAzureAuthRouter); + await authRouter.register(registerIdentityOidcAuthRouter); }, { prefix: "/auth" } ); diff --git a/backend/src/server/routes/v1/project-router.ts b/backend/src/server/routes/v1/project-router.ts index 99f05cf94..b6c4ce7bd 100644 --- a/backend/src/server/routes/v1/project-router.ts +++ b/backend/src/server/routes/v1/project-router.ts @@ -78,6 +78,7 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => { lastName: true, id: true }).merge(UserEncryptionKeysSchema.pick({ publicKey: true })), + project: ProjectsSchema.pick({ name: true, id: true }), roles: z.array( z.object({ id: z.string(), diff --git a/backend/src/server/routes/v1/secret-folder-router.ts b/backend/src/server/routes/v1/secret-folder-router.ts index 1a1747f64..276bb36ef 100644 --- a/backend/src/server/routes/v1/secret-folder-router.ts +++ b/backend/src/server/routes/v1/secret-folder-router.ts @@ -292,4 +292,39 @@ export const registerSecretFolderRouter = async (server: FastifyZodProvider) => return { folders }; } }); + + server.route({ + method: "GET", + url: "/:id", + config: { + rateLimit: readLimit + }, + schema: { + description: "Get folder by id", + security: [ + { + bearerAuth: [] + } + ], + params: z.object({ + id: z.string().trim().describe(FOLDERS.GET_BY_ID.folderId) + }), + response: { + 200: z.object({ + folder: SecretFoldersSchema + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const folder = await server.services.folder.getFolderById({ + actorId: req.permission.id, + actor: req.permission.type, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + id: req.params.id + }); + return { folder }; + } + }); }; diff --git a/backend/src/server/routes/v1/secret-sharing-router.ts b/backend/src/server/routes/v1/secret-sharing-router.ts index 4ec2737fb..d5c6a9884 100644 --- a/backend/src/server/routes/v1/secret-sharing-router.ts +++ b/backend/src/server/routes/v1/secret-sharing-router.ts @@ -1,6 +1,7 @@ import { z } from "zod"; import { SecretSharingSchema } from "@app/db/schemas"; +import { SecretSharingAccessType } from "@app/lib/types"; import { publicEndpointLimit, publicSecretShareCreationLimit, @@ -55,14 +56,18 @@ export const registerSecretSharingRouter = async (server: FastifyZodProvider) => iv: true, tag: true, expiresAt: true, - expiresAfterViews: true + expiresAfterViews: true, + accessType: true + }).extend({ + orgName: z.string().optional() }) } }, handler: async (req) => { const sharedSecret = await req.server.services.secretSharing.getActiveSharedSecretByIdAndHashedHex( req.params.id, - req.query.hashedHex + req.query.hashedHex, + req.permission?.orgId ); if (!sharedSecret) return undefined; return { @@ -70,7 +75,9 @@ export const registerSecretSharingRouter = async (server: FastifyZodProvider) => iv: sharedSecret.iv, tag: sharedSecret.tag, expiresAt: sharedSecret.expiresAt, - expiresAfterViews: sharedSecret.expiresAfterViews + expiresAfterViews: sharedSecret.expiresAfterViews, + accessType: sharedSecret.accessType, + orgName: sharedSecret.orgName }; } }); @@ -104,7 +111,8 @@ export const registerSecretSharingRouter = async (server: FastifyZodProvider) => tag, hashedHex, expiresAt: new Date(expiresAt), - expiresAfterViews + expiresAfterViews, + accessType: SecretSharingAccessType.Anyone }); return { id: sharedSecret.id }; } @@ -123,7 +131,8 @@ export const registerSecretSharingRouter = async (server: FastifyZodProvider) => tag: z.string(), hashedHex: z.string(), expiresAt: z.string(), - expiresAfterViews: z.number() + expiresAfterViews: z.number(), + accessType: z.nativeEnum(SecretSharingAccessType).default(SecretSharingAccessType.Organization) }), response: { 200: z.object({ @@ -145,7 +154,8 @@ export const registerSecretSharingRouter = async (server: FastifyZodProvider) => tag, hashedHex, expiresAt: new Date(expiresAt), - expiresAfterViews + expiresAfterViews, + accessType: req.body.accessType }); return { id: sharedSecret.id }; } diff --git a/backend/src/server/routes/v2/identity-project-router.ts b/backend/src/server/routes/v2/identity-project-router.ts index d259a46fd..806a54d99 100644 --- a/backend/src/server/routes/v2/identity-project-router.ts +++ b/backend/src/server/routes/v2/identity-project-router.ts @@ -5,6 +5,7 @@ import { IdentitiesSchema, IdentityProjectMembershipsSchema, ProjectMembershipRole, + ProjectsSchema, ProjectUserMembershipRolesSchema } from "@app/db/schemas"; import { PROJECT_IDENTITIES } from "@app/lib/api-docs"; @@ -234,7 +235,8 @@ export const registerIdentityProjectRouter = async (server: FastifyZodProvider) temporaryAccessEndTime: z.date().nullable().optional() }) ), - identity: IdentitiesSchema.pick({ name: true, id: true, authMethod: true }) + identity: IdentitiesSchema.pick({ name: true, id: true, authMethod: true }), + project: ProjectsSchema.pick({ name: true, id: true }) }) .array() }) @@ -291,7 +293,8 @@ export const registerIdentityProjectRouter = async (server: FastifyZodProvider) temporaryAccessEndTime: z.date().nullable().optional() }) ), - identity: IdentitiesSchema.pick({ name: true, id: true, authMethod: true }) + identity: IdentitiesSchema.pick({ name: true, id: true, authMethod: true }), + project: ProjectsSchema.pick({ name: true, id: true }) }) }) } diff --git a/backend/src/server/routes/v2/organization-router.ts b/backend/src/server/routes/v2/organization-router.ts index 07074eba3..49488776b 100644 --- a/backend/src/server/routes/v2/organization-router.ts +++ b/backend/src/server/routes/v2/organization-router.ts @@ -1,6 +1,13 @@ import { z } from "zod"; -import { OrganizationsSchema, OrgMembershipsSchema, UserEncryptionKeysSchema, UsersSchema } from "@app/db/schemas"; +import { + OrganizationsSchema, + OrgMembershipsSchema, + ProjectMembershipsSchema, + ProjectsSchema, + UserEncryptionKeysSchema, + UsersSchema +} from "@app/db/schemas"; import { ORGANIZATIONS } from "@app/lib/api-docs"; import { creationLimit, readLimit, writeLimit } from "@app/server/config/rateLimiter"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; @@ -30,6 +37,7 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => { user: UsersSchema.pick({ username: true, email: true, + isEmailVerified: true, firstName: true, lastName: true, id: true @@ -103,6 +111,54 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => { } }); + server.route({ + method: "GET", + url: "/:organizationId/memberships/:membershipId", + config: { + rateLimit: writeLimit + }, + schema: { + description: "Get organization user membership", + security: [ + { + bearerAuth: [] + } + ], + params: z.object({ + organizationId: z.string().trim().describe(ORGANIZATIONS.GET_USER_MEMBERSHIP.organizationId), + membershipId: z.string().trim().describe(ORGANIZATIONS.GET_USER_MEMBERSHIP.membershipId) + }), + response: { + 200: z.object({ + membership: OrgMembershipsSchema.merge( + z.object({ + user: UsersSchema.pick({ + username: true, + email: true, + isEmailVerified: true, + firstName: true, + lastName: true, + id: true + }).merge(z.object({ publicKey: z.string().nullable() })) + }) + ).omit({ createdAt: true, updatedAt: true }) + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const membership = await server.services.org.getOrgMembership({ + actor: req.permission.type, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + orgId: req.params.organizationId, + membershipId: req.params.membershipId + }); + return { membership }; + } + }); + server.route({ method: "PATCH", url: "/:organizationId/memberships/:membershipId", @@ -121,7 +177,8 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => { membershipId: z.string().trim().describe(ORGANIZATIONS.UPDATE_USER_MEMBERSHIP.membershipId) }), body: z.object({ - role: z.string().trim().describe(ORGANIZATIONS.UPDATE_USER_MEMBERSHIP.role) + role: z.string().trim().optional().describe(ORGANIZATIONS.UPDATE_USER_MEMBERSHIP.role), + isActive: z.boolean().optional().describe(ORGANIZATIONS.UPDATE_USER_MEMBERSHIP.isActive) }), response: { 200: z.object({ @@ -129,17 +186,17 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => { }) } }, - onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY, AuthMode.IDENTITY_ACCESS_TOKEN]), + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), handler: async (req) => { if (req.auth.actor !== ActorType.USER) return; const membership = await server.services.org.updateOrgMembership({ userId: req.permission.id, - role: req.body.role, actorAuthMethod: req.permission.authMethod, orgId: req.params.organizationId, membershipId: req.params.membershipId, - actorOrgId: req.permission.orgId + actorOrgId: req.permission.orgId, + ...req.body }); return { membership }; } @@ -183,6 +240,69 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => { } }); + server.route({ + // TODO: re-think endpoint structure in future so users only need to pass in membershipId bc organizationId is redundant + method: "GET", + url: "/:organizationId/memberships/:membershipId/project-memberships", + config: { + rateLimit: writeLimit + }, + schema: { + description: "Get project memberships given organization membership", + security: [ + { + bearerAuth: [] + } + ], + params: z.object({ + organizationId: z.string().trim().describe(ORGANIZATIONS.DELETE_USER_MEMBERSHIP.organizationId), + membershipId: z.string().trim().describe(ORGANIZATIONS.DELETE_USER_MEMBERSHIP.membershipId) + }), + response: { + 200: z.object({ + memberships: ProjectMembershipsSchema.extend({ + user: UsersSchema.pick({ + email: true, + username: true, + firstName: true, + lastName: true, + id: true + }).merge(UserEncryptionKeysSchema.pick({ publicKey: true })), + project: ProjectsSchema.pick({ name: true, id: true }), + roles: z.array( + z.object({ + id: z.string(), + role: z.string(), + customRoleId: z.string().optional().nullable(), + customRoleName: z.string().optional().nullable(), + customRoleSlug: z.string().optional().nullable(), + isTemporary: z.boolean(), + temporaryMode: z.string().optional().nullable(), + temporaryRange: z.string().nullable().optional(), + temporaryAccessStartTime: z.date().nullable().optional(), + temporaryAccessEndTime: z.date().nullable().optional() + }) + ) + }) + .omit({ createdAt: true, updatedAt: true }) + .array() + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const memberships = await server.services.org.listProjectMembershipsByOrgMembershipId({ + actor: req.permission.type, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + orgId: req.params.organizationId, + orgMembershipId: req.params.membershipId + }); + return { memberships }; + } + }); + server.route({ method: "POST", url: "/", diff --git a/backend/src/server/routes/v3/secret-router.ts b/backend/src/server/routes/v3/secret-router.ts index 910cc3db9..2a4f9b464 100644 --- a/backend/src/server/routes/v3/secret-router.ts +++ b/backend/src/server/routes/v3/secret-router.ts @@ -1325,6 +1325,61 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { } }); + server.route({ + method: "POST", + url: "/move", + config: { + rateLimit: secretsLimit + }, + schema: { + body: z.object({ + projectSlug: z.string().trim(), + sourceEnvironment: z.string().trim(), + sourceSecretPath: z.string().trim().default("/").transform(removeTrailingSlash), + destinationEnvironment: z.string().trim(), + destinationSecretPath: z.string().trim().default("/").transform(removeTrailingSlash), + secretIds: z.string().array(), + shouldOverwrite: z.boolean().default(false) + }), + response: { + 200: z.object({ + isSourceUpdated: z.boolean(), + isDestinationUpdated: z.boolean() + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const { projectId, isSourceUpdated, isDestinationUpdated } = await server.services.secret.moveSecrets({ + actorId: req.permission.id, + actor: req.permission.type, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + ...req.body + }); + + await server.services.auditLog.createAuditLog({ + projectId, + ...req.auditLogInfo, + event: { + type: EventType.MOVE_SECRETS, + metadata: { + sourceEnvironment: req.body.sourceEnvironment, + sourceSecretPath: req.body.sourceSecretPath, + destinationEnvironment: req.body.destinationEnvironment, + destinationSecretPath: req.body.destinationSecretPath, + secretIds: req.body.secretIds + } + } + }); + + return { + isSourceUpdated, + isDestinationUpdated + }; + } + }); + server.route({ method: "POST", url: "/batch", diff --git a/backend/src/services/auth-token/auth-token-service.ts b/backend/src/services/auth-token/auth-token-service.ts index b1f8aa2f6..3610bcded 100644 --- a/backend/src/services/auth-token/auth-token-service.ts +++ b/backend/src/services/auth-token/auth-token-service.ts @@ -4,7 +4,8 @@ import bcrypt from "bcrypt"; import { TAuthTokens, TAuthTokenSessions } from "@app/db/schemas"; import { getConfig } from "@app/lib/config/env"; -import { UnauthorizedError } from "@app/lib/errors"; +import { ForbiddenRequestError, UnauthorizedError } from "@app/lib/errors"; +import { TOrgMembershipDALFactory } from "@app/services/org-membership/org-membership-dal"; import { AuthModeJwtTokenPayload } from "../auth/auth-type"; import { TUserDALFactory } from "../user/user-dal"; @@ -14,6 +15,7 @@ import { TCreateTokenForUserDTO, TIssueAuthTokenDTO, TokenType, TValidateTokenFo type TAuthTokenServiceFactoryDep = { tokenDAL: TTokenDALFactory; userDAL: Pick; + orgMembershipDAL: Pick; }; export type TAuthTokenServiceFactory = ReturnType; @@ -67,7 +69,7 @@ export const getTokenConfig = (tokenType: TokenType) => { } }; -export const tokenServiceFactory = ({ tokenDAL, userDAL }: TAuthTokenServiceFactoryDep) => { +export const tokenServiceFactory = ({ tokenDAL, userDAL, orgMembershipDAL }: TAuthTokenServiceFactoryDep) => { const createTokenForUser = async ({ type, userId, orgId }: TCreateTokenForUserDTO) => { const { token, ...tkCfg } = getTokenConfig(type); const appCfg = getConfig(); @@ -154,6 +156,16 @@ export const tokenServiceFactory = ({ tokenDAL, userDAL }: TAuthTokenServiceFact const user = await userDAL.findById(session.userId); if (!user || !user.isAccepted) throw new UnauthorizedError({ name: "Token user not found" }); + if (token.organizationId) { + const orgMembership = await orgMembershipDAL.findOne({ + userId: user.id, + orgId: token.organizationId + }); + + if (!orgMembership) throw new ForbiddenRequestError({ message: "User not member of organization" }); + if (!orgMembership.isActive) throw new ForbiddenRequestError({ message: "User not active in organization" }); + } + return { user, tokenVersionId: token.tokenVersionId, orgId: token.organizationId }; }; diff --git a/backend/src/services/certificate-authority/certificate-authority-fns.ts b/backend/src/services/certificate-authority/certificate-authority-fns.ts index cf42a058e..9f98dcb83 100644 --- a/backend/src/services/certificate-authority/certificate-authority-fns.ts +++ b/backend/src/services/certificate-authority/certificate-authority-fns.ts @@ -75,8 +75,10 @@ export const getCaCredentials = async ({ kmsService }); - const decryptedPrivateKey = await kmsService.decrypt({ - kmsId: keyId, + const kmsDecryptor = await kmsService.decryptWithKmsKey({ + kmsId: keyId + }); + const decryptedPrivateKey = kmsDecryptor({ cipherTextBlob: caSecret.encryptedPrivateKey }); @@ -123,15 +125,17 @@ export const getCaCertChain = async ({ kmsService }); - const decryptedCaCert = await kmsService.decrypt({ - kmsId: keyId, + const kmsDecryptor = await kmsService.decryptWithKmsKey({ + kmsId: keyId + }); + + const decryptedCaCert = kmsDecryptor({ cipherTextBlob: caCert.encryptedCertificate }); const caCertObj = new x509.X509Certificate(decryptedCaCert); - const decryptedChain = await kmsService.decrypt({ - kmsId: keyId, + const decryptedChain = kmsDecryptor({ cipherTextBlob: caCert.encryptedCertificateChain }); @@ -168,8 +172,11 @@ export const rebuildCaCrl = async ({ kmsService }); - const privateKey = await kmsService.decrypt({ - kmsId: keyId, + const kmsDecryptor = await kmsService.decryptWithKmsKey({ + kmsId: keyId + }); + + const privateKey = kmsDecryptor({ cipherTextBlob: caSecret.encryptedPrivateKey }); @@ -200,8 +207,10 @@ export const rebuildCaCrl = async ({ signingKey: sk }); - const { cipherTextBlob: encryptedCrl } = await kmsService.encrypt({ - kmsId: keyId, + const kmsEncryptor = await kmsService.encryptWithKmsKey({ + kmsId: keyId + }); + const { cipherTextBlob: encryptedCrl } = kmsEncryptor({ plainText: Buffer.from(new Uint8Array(crl.rawData)) }); diff --git a/backend/src/services/certificate-authority/certificate-authority-queue.ts b/backend/src/services/certificate-authority/certificate-authority-queue.ts index 384f45c09..30da119d0 100644 --- a/backend/src/services/certificate-authority/certificate-authority-queue.ts +++ b/backend/src/services/certificate-authority/certificate-authority-queue.ts @@ -25,7 +25,7 @@ type TCertificateAuthorityQueueFactoryDep = { certificateAuthoritySecretDAL: TCertificateAuthoritySecretDALFactory; certificateDAL: TCertificateDALFactory; projectDAL: Pick; - kmsService: Pick; + kmsService: Pick; queueService: TQueueServiceFactory; }; export type TCertificateAuthorityQueueFactory = ReturnType; @@ -88,8 +88,10 @@ export const certificateAuthorityQueueFactory = ({ kmsService }); - const privateKey = await kmsService.decrypt({ - kmsId: keyId, + const kmsDecryptor = await kmsService.decryptWithKmsKey({ + kmsId: keyId + }); + const privateKey = kmsDecryptor({ cipherTextBlob: caSecret.encryptedPrivateKey }); @@ -120,8 +122,10 @@ export const certificateAuthorityQueueFactory = ({ signingKey: sk }); - const { cipherTextBlob: encryptedCrl } = await kmsService.encrypt({ - kmsId: keyId, + const kmsEncryptor = await kmsService.encryptWithKmsKey({ + kmsId: keyId + }); + const { cipherTextBlob: encryptedCrl } = kmsEncryptor({ plainText: Buffer.from(new Uint8Array(crl.rawData)) }); diff --git a/backend/src/services/certificate-authority/certificate-authority-service.ts b/backend/src/services/certificate-authority/certificate-authority-service.ts index 7d87545e2..afc8d7efb 100644 --- a/backend/src/services/certificate-authority/certificate-authority-service.ts +++ b/backend/src/services/certificate-authority/certificate-authority-service.ts @@ -53,7 +53,7 @@ type TCertificateAuthorityServiceFactoryDep = { certificateDAL: Pick; certificateBodyDAL: Pick; projectDAL: Pick; - kmsService: Pick; + kmsService: Pick; permissionService: Pick; }; @@ -154,11 +154,14 @@ export const certificateAuthorityServiceFactory = ({ tx ); - const keyId = await getProjectKmsCertificateKeyId({ + const certificateManagerKmsId = await getProjectKmsCertificateKeyId({ projectId: project.id, projectDAL, kmsService }); + const kmsEncryptor = await kmsService.encryptWithKmsKey({ + kmsId: certificateManagerKmsId + }); if (type === CaType.ROOT) { // note: create self-signed cert only applicable for root CA @@ -178,13 +181,11 @@ export const certificateAuthorityServiceFactory = ({ ] }); - const { cipherTextBlob: encryptedCertificate } = await kmsService.encrypt({ - kmsId: keyId, + const { cipherTextBlob: encryptedCertificate } = kmsEncryptor({ plainText: Buffer.from(new Uint8Array(cert.rawData)) }); - const { cipherTextBlob: encryptedCertificateChain } = await kmsService.encrypt({ - kmsId: keyId, + const { cipherTextBlob: encryptedCertificateChain } = kmsEncryptor({ plainText: Buffer.alloc(0) }); @@ -208,8 +209,7 @@ export const certificateAuthorityServiceFactory = ({ signingKey: keys.privateKey }); - const { cipherTextBlob: encryptedCrl } = await kmsService.encrypt({ - kmsId: keyId, + const { cipherTextBlob: encryptedCrl } = kmsEncryptor({ plainText: Buffer.from(new Uint8Array(crl.rawData)) }); @@ -224,8 +224,7 @@ export const certificateAuthorityServiceFactory = ({ // https://nodejs.org/api/crypto.html#static-method-keyobjectfromkey const skObj = KeyObject.from(keys.privateKey); - const { cipherTextBlob: encryptedPrivateKey } = await kmsService.encrypt({ - kmsId: keyId, + const { cipherTextBlob: encryptedPrivateKey } = kmsEncryptor({ plainText: skObj.export({ type: "pkcs8", format: "der" @@ -449,15 +448,17 @@ export const certificateAuthorityServiceFactory = ({ const alg = keyAlgorithmToAlgCfg(ca.keyAlgorithm as CertKeyAlgorithm); - const keyId = await getProjectKmsCertificateKeyId({ + const certificateManagerKmsId = await getProjectKmsCertificateKeyId({ projectId: ca.projectId, projectDAL, kmsService }); + const kmsDecryptor = await kmsService.decryptWithKmsKey({ + kmsId: certificateManagerKmsId + }); const caCert = await certificateAuthorityCertDAL.findOne({ caId: ca.id }); - const decryptedCaCert = await kmsService.decrypt({ - kmsId: keyId, + const decryptedCaCert = kmsDecryptor({ cipherTextBlob: caCert.encryptedCertificate }); @@ -605,19 +606,20 @@ export const certificateAuthorityServiceFactory = ({ dn: parentCertSubject }); - const keyId = await getProjectKmsCertificateKeyId({ + const certificateManagerKmsId = await getProjectKmsCertificateKeyId({ projectId: ca.projectId, projectDAL, kmsService }); + const kmsEncryptor = await kmsService.encryptWithKmsKey({ + kmsId: certificateManagerKmsId + }); - const { cipherTextBlob: encryptedCertificate } = await kmsService.encrypt({ - kmsId: keyId, + const { cipherTextBlob: encryptedCertificate } = kmsEncryptor({ plainText: Buffer.from(new Uint8Array(certObj.rawData)) }); - const { cipherTextBlob: encryptedCertificateChain } = await kmsService.encrypt({ - kmsId: keyId, + const { cipherTextBlob: encryptedCertificateChain } = kmsEncryptor({ plainText: Buffer.from(certificateChain) }); @@ -682,14 +684,16 @@ export const certificateAuthorityServiceFactory = ({ const caCert = await certificateAuthorityCertDAL.findOne({ caId: ca.id }); if (!caCert) throw new BadRequestError({ message: "CA does not have a certificate installed" }); - const keyId = await getProjectKmsCertificateKeyId({ + const certificateManagerKmsId = await getProjectKmsCertificateKeyId({ projectId: ca.projectId, projectDAL, kmsService }); + const kmsDecryptor = await kmsService.decryptWithKmsKey({ + kmsId: certificateManagerKmsId + }); - const decryptedCaCert = await kmsService.decrypt({ - kmsId: keyId, + const decryptedCaCert = kmsDecryptor({ cipherTextBlob: caCert.encryptedCertificate }); @@ -796,8 +800,10 @@ export const certificateAuthorityServiceFactory = ({ const skLeafObj = KeyObject.from(leafKeys.privateKey); const skLeaf = skLeafObj.export({ format: "pem", type: "pkcs8" }) as string; - const { cipherTextBlob: encryptedCertificate } = await kmsService.encrypt({ - kmsId: keyId, + const kmsEncryptor = await kmsService.encryptWithKmsKey({ + kmsId: certificateManagerKmsId + }); + const { cipherTextBlob: encryptedCertificate } = kmsEncryptor({ plainText: Buffer.from(new Uint8Array(leafCert.rawData)) }); diff --git a/backend/src/services/certificate-authority/certificate-authority-types.ts b/backend/src/services/certificate-authority/certificate-authority-types.ts index 8af8b679c..7818c3a3e 100644 --- a/backend/src/services/certificate-authority/certificate-authority-types.ts +++ b/backend/src/services/certificate-authority/certificate-authority-types.ts @@ -95,7 +95,7 @@ export type TGetCaCredentialsDTO = { certificateAuthorityDAL: Pick; certificateAuthoritySecretDAL: Pick; projectDAL: Pick; - kmsService: Pick; + kmsService: Pick; }; export type TGetCaCertChainDTO = { @@ -103,7 +103,7 @@ export type TGetCaCertChainDTO = { certificateAuthorityDAL: Pick; certificateAuthorityCertDAL: Pick; projectDAL: Pick; - kmsService: Pick; + kmsService: Pick; }; export type TRebuildCaCrlDTO = { @@ -113,7 +113,7 @@ export type TRebuildCaCrlDTO = { certificateAuthoritySecretDAL: Pick; projectDAL: Pick; certificateDAL: Pick; - kmsService: Pick; + kmsService: Pick; }; export type TRotateCaCrlTriggerDTO = { diff --git a/backend/src/services/certificate/certificate-service.ts b/backend/src/services/certificate/certificate-service.ts index ba865caa1..401a55cc9 100644 --- a/backend/src/services/certificate/certificate-service.ts +++ b/backend/src/services/certificate/certificate-service.ts @@ -25,7 +25,7 @@ type TCertificateServiceFactoryDep = { certificateAuthorityCrlDAL: Pick; certificateAuthoritySecretDAL: Pick; projectDAL: Pick; - kmsService: Pick; + kmsService: Pick; permissionService: Pick; }; @@ -164,14 +164,16 @@ export const certificateServiceFactory = ({ const certBody = await certificateBodyDAL.findOne({ certId: cert.id }); - const keyId = await getProjectKmsCertificateKeyId({ + const certificateManagerKeyId = await getProjectKmsCertificateKeyId({ projectId: ca.projectId, projectDAL, kmsService }); - const decryptedCert = await kmsService.decrypt({ - kmsId: keyId, + const kmsDecryptor = await kmsService.decryptWithKmsKey({ + kmsId: certificateManagerKeyId + }); + const decryptedCert = kmsDecryptor({ cipherTextBlob: certBody.encryptedCertificate }); diff --git a/backend/src/services/identity-access-token/identity-access-token-dal.ts b/backend/src/services/identity-access-token/identity-access-token-dal.ts index 4f04ef0ac..931c65550 100644 --- a/backend/src/services/identity-access-token/identity-access-token-dal.ts +++ b/backend/src/services/identity-access-token/identity-access-token-dal.ts @@ -51,6 +51,18 @@ export const identityAccessTokenDALFactory = (db: TDbClient) => { `${TableName.IdentityKubernetesAuth}.identityId` ); }) + .leftJoin(TableName.IdentityOidcAuth, (qb) => { + qb.on(`${TableName.Identity}.authMethod`, db.raw("?", [IdentityAuthMethod.OIDC_AUTH])).andOn( + `${TableName.Identity}.id`, + `${TableName.IdentityOidcAuth}.identityId` + ); + }) + .leftJoin(TableName.IdentityTokenAuth, (qb) => { + qb.on(`${TableName.Identity}.authMethod`, db.raw("?", [IdentityAuthMethod.TOKEN_AUTH])).andOn( + `${TableName.Identity}.id`, + `${TableName.IdentityTokenAuth}.identityId` + ); + }) .select(selectAllTableCols(TableName.IdentityAccessToken)) .select( db.ref("accessTokenTrustedIps").withSchema(TableName.IdentityUniversalAuth).as("accessTokenTrustedIpsUa"), @@ -58,6 +70,8 @@ export const identityAccessTokenDALFactory = (db: TDbClient) => { db.ref("accessTokenTrustedIps").withSchema(TableName.IdentityAwsAuth).as("accessTokenTrustedIpsAws"), db.ref("accessTokenTrustedIps").withSchema(TableName.IdentityAzureAuth).as("accessTokenTrustedIpsAzure"), db.ref("accessTokenTrustedIps").withSchema(TableName.IdentityKubernetesAuth).as("accessTokenTrustedIpsK8s"), + db.ref("accessTokenTrustedIps").withSchema(TableName.IdentityOidcAuth).as("accessTokenTrustedIpsOidc"), + db.ref("accessTokenTrustedIps").withSchema(TableName.IdentityTokenAuth).as("accessTokenTrustedIpsToken"), db.ref("name").withSchema(TableName.Identity) ) .first(); @@ -71,7 +85,9 @@ export const identityAccessTokenDALFactory = (db: TDbClient) => { doc.accessTokenTrustedIpsGcp || doc.accessTokenTrustedIpsAws || doc.accessTokenTrustedIpsAzure || - doc.accessTokenTrustedIpsK8s + doc.accessTokenTrustedIpsK8s || + doc.accessTokenTrustedIpsOidc || + doc.accessTokenTrustedIpsToken }; } catch (error) { throw new DatabaseError({ error, name: "IdAccessTokenFindOne" }); diff --git a/backend/src/services/identity-aws-auth/identity-aws-auth-service.ts b/backend/src/services/identity-aws-auth/identity-aws-auth-service.ts index 9cb39aece..6e303c1f7 100644 --- a/backend/src/services/identity-aws-auth/identity-aws-auth-service.ts +++ b/backend/src/services/identity-aws-auth/identity-aws-auth-service.ts @@ -78,7 +78,10 @@ export const identityAwsAuthServiceFactory = ({ .map((accountId) => accountId.trim()) .some((accountId) => accountId === Account); - if (!isAccountAllowed) throw new UnauthorizedError(); + if (!isAccountAllowed) + throw new ForbiddenRequestError({ + message: "Access denied: AWS account ID not allowed." + }); } if (identityAwsAuth.allowedPrincipalArns) { @@ -94,7 +97,10 @@ export const identityAwsAuthServiceFactory = ({ return regex.test(extractPrincipalArn(Arn)); }); - if (!isArnAllowed) throw new UnauthorizedError(); + if (!isArnAllowed) + throw new ForbiddenRequestError({ + message: "Access denied: AWS principal ARN not allowed." + }); } const identityAccessToken = await identityAwsAuthDAL.transaction(async (tx) => { diff --git a/backend/src/services/identity-azure-auth/identity-azure-auth-fns.ts b/backend/src/services/identity-azure-auth/identity-azure-auth-fns.ts index ad9e6f12d..7c89a35b9 100644 --- a/backend/src/services/identity-azure-auth/identity-azure-auth-fns.ts +++ b/backend/src/services/identity-azure-auth/identity-azure-auth-fns.ts @@ -17,6 +17,7 @@ export const validateAzureIdentity = async ({ const jwksUri = `https://login.microsoftonline.com/${tenantId}/discovery/keys`; const decodedJwt = jwt.decode(azureJwt, { complete: true }) as TDecodedAzureAuthJwt; + const { kid } = decodedJwt.header; const { data }: { data: TAzureJwksUriResponse } = await axios.get(jwksUri); @@ -27,6 +28,13 @@ export const validateAzureIdentity = async ({ const publicKey = `-----BEGIN CERTIFICATE-----\n${signingKey.x5c[0]}\n-----END CERTIFICATE-----`; + // Case: This can happen when the user uses a custom resource (such as https://management.azure.com&client_id=value). + // In this case, the audience in the decoded JWT will not have a trailing slash, but the resource will. + if (!decodedJwt.payload.aud.endsWith("/") && resource.endsWith("/")) { + // eslint-disable-next-line no-param-reassign + resource = resource.slice(0, -1); + } + return jwt.verify(azureJwt, publicKey, { audience: resource, issuer: `https://sts.windows.net/${tenantId}/` diff --git a/backend/src/services/identity-gcp-auth/identity-gcp-auth-service.ts b/backend/src/services/identity-gcp-auth/identity-gcp-auth-service.ts index edac7c132..fdbb8490d 100644 --- a/backend/src/services/identity-gcp-auth/identity-gcp-auth-service.ts +++ b/backend/src/services/identity-gcp-auth/identity-gcp-auth-service.ts @@ -81,7 +81,10 @@ export const identityGcpAuthServiceFactory = ({ .map((serviceAccount) => serviceAccount.trim()) .some((serviceAccount) => serviceAccount === gcpIdentityDetails.email); - if (!isServiceAccountAllowed) throw new UnauthorizedError(); + if (!isServiceAccountAllowed) + throw new ForbiddenRequestError({ + message: "Access denied: GCP service account not allowed." + }); } if (identityGcpAuth.type === "gce" && identityGcpAuth.allowedProjects && gcpIdentityDetails.computeEngineDetails) { @@ -92,7 +95,10 @@ export const identityGcpAuthServiceFactory = ({ .map((project) => project.trim()) .some((project) => project === gcpIdentityDetails.computeEngineDetails?.project_id); - if (!isProjectAllowed) throw new UnauthorizedError(); + if (!isProjectAllowed) + throw new ForbiddenRequestError({ + message: "Access denied: GCP project not allowed." + }); } if (identityGcpAuth.type === "gce" && identityGcpAuth.allowedZones && gcpIdentityDetails.computeEngineDetails) { @@ -101,7 +107,10 @@ export const identityGcpAuthServiceFactory = ({ .map((zone) => zone.trim()) .some((zone) => zone === gcpIdentityDetails.computeEngineDetails?.zone); - if (!isZoneAllowed) throw new UnauthorizedError(); + if (!isZoneAllowed) + throw new ForbiddenRequestError({ + message: "Access denied: GCP zone not allowed." + }); } const identityAccessToken = await identityGcpAuthDAL.transaction(async (tx) => { diff --git a/backend/src/services/identity-kubernetes-auth/identity-kubernetes-auth-service.ts b/backend/src/services/identity-kubernetes-auth/identity-kubernetes-auth-service.ts index 820777b46..d0a1b07cf 100644 --- a/backend/src/services/identity-kubernetes-auth/identity-kubernetes-auth-service.ts +++ b/backend/src/services/identity-kubernetes-auth/identity-kubernetes-auth-service.ts @@ -139,7 +139,10 @@ export const identityKubernetesAuthServiceFactory = ({ .map((namespace) => namespace.trim()) .some((namespace) => namespace === targetNamespace); - if (!isNamespaceAllowed) throw new UnauthorizedError(); + if (!isNamespaceAllowed) + throw new ForbiddenRequestError({ + message: "Access denied: K8s namespace not allowed." + }); } if (identityKubernetesAuth.allowedNames) { @@ -150,7 +153,10 @@ export const identityKubernetesAuthServiceFactory = ({ .map((name) => name.trim()) .some((name) => name === targetName); - if (!isNameAllowed) throw new UnauthorizedError(); + if (!isNameAllowed) + throw new ForbiddenRequestError({ + message: "Access denied: K8s name not allowed." + }); } if (identityKubernetesAuth.allowedAudience) { @@ -159,7 +165,10 @@ export const identityKubernetesAuthServiceFactory = ({ (audience) => audience === identityKubernetesAuth.allowedAudience ); - if (!isAudienceAllowed) throw new UnauthorizedError(); + if (!isAudienceAllowed) + throw new ForbiddenRequestError({ + message: "Access denied: K8s audience not allowed." + }); } const identityAccessToken = await identityKubernetesAuthDAL.transaction(async (tx) => { diff --git a/backend/src/services/identity-oidc-auth/identity-oidc-auth-dal.ts b/backend/src/services/identity-oidc-auth/identity-oidc-auth-dal.ts new file mode 100644 index 000000000..1d8ab3c13 --- /dev/null +++ b/backend/src/services/identity-oidc-auth/identity-oidc-auth-dal.ts @@ -0,0 +1,10 @@ +import { TDbClient } from "@app/db"; +import { TableName } from "@app/db/schemas"; +import { ormify } from "@app/lib/knex"; + +export type TIdentityOidcAuthDALFactory = ReturnType; + +export const identityOidcAuthDALFactory = (db: TDbClient) => { + const oidcAuthOrm = ormify(db, TableName.IdentityOidcAuth); + return oidcAuthOrm; +}; diff --git a/backend/src/services/identity-oidc-auth/identity-oidc-auth-service.ts b/backend/src/services/identity-oidc-auth/identity-oidc-auth-service.ts new file mode 100644 index 000000000..457e93058 --- /dev/null +++ b/backend/src/services/identity-oidc-auth/identity-oidc-auth-service.ts @@ -0,0 +1,540 @@ +import { ForbiddenError } from "@casl/ability"; +import axios from "axios"; +import https from "https"; +import jwt from "jsonwebtoken"; +import { JwksClient } from "jwks-rsa"; + +import { IdentityAuthMethod, SecretKeyEncoding, TIdentityOidcAuthsUpdate } 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 { isAtLeastAsPrivileged } from "@app/lib/casl"; +import { getConfig } from "@app/lib/config/env"; +import { generateAsymmetricKeyPair } from "@app/lib/crypto"; +import { + decryptSymmetric, + encryptSymmetric, + generateSymmetricKey, + infisicalSymmetricDecrypt, + infisicalSymmetricEncypt +} from "@app/lib/crypto/encryption"; +import { BadRequestError, ForbiddenRequestError, UnauthorizedError } from "@app/lib/errors"; +import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip"; + +import { ActorType, 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 { TOrgBotDALFactory } from "../org/org-bot-dal"; +import { TIdentityOidcAuthDALFactory } from "./identity-oidc-auth-dal"; +import { + TAttachOidcAuthDTO, + TGetOidcAuthDTO, + TLoginOidcAuthDTO, + TRevokeOidcAuthDTO, + TUpdateOidcAuthDTO +} from "./identity-oidc-auth-types"; + +type TIdentityOidcAuthServiceFactoryDep = { + identityOidcAuthDAL: TIdentityOidcAuthDALFactory; + identityOrgMembershipDAL: Pick; + identityAccessTokenDAL: Pick; + identityDAL: Pick; + permissionService: Pick; + licenseService: Pick; + orgBotDAL: Pick; +}; + +export type TIdentityOidcAuthServiceFactory = ReturnType; + +export const identityOidcAuthServiceFactory = ({ + identityOidcAuthDAL, + identityOrgMembershipDAL, + identityDAL, + permissionService, + licenseService, + identityAccessTokenDAL, + orgBotDAL +}: TIdentityOidcAuthServiceFactoryDep) => { + const login = async ({ identityId, jwt: oidcJwt }: TLoginOidcAuthDTO) => { + const identityOidcAuth = await identityOidcAuthDAL.findOne({ identityId }); + if (!identityOidcAuth) { + throw new UnauthorizedError(); + } + + const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ + identityId: identityOidcAuth.identityId + }); + if (!identityMembershipOrg) { + throw new BadRequestError({ message: "Failed to find identity" }); + } + + const orgBot = await orgBotDAL.findOne({ orgId: identityMembershipOrg.orgId }); + if (!orgBot) { + throw new BadRequestError({ message: "Org bot not found", name: "OrgBotNotFound" }); + } + + const key = infisicalSymmetricDecrypt({ + ciphertext: orgBot.encryptedSymmetricKey, + iv: orgBot.symmetricKeyIV, + tag: orgBot.symmetricKeyTag, + keyEncoding: orgBot.symmetricKeyKeyEncoding as SecretKeyEncoding + }); + + const { encryptedCaCert, caCertIV, caCertTag } = identityOidcAuth; + + let caCert = ""; + if (encryptedCaCert && caCertIV && caCertTag) { + caCert = decryptSymmetric({ + ciphertext: encryptedCaCert, + iv: caCertIV, + tag: caCertTag, + key + }); + } + + const requestAgent = new https.Agent({ ca: caCert, rejectUnauthorized: !!caCert }); + const { data: discoveryDoc } = await axios.get<{ jwks_uri: string }>( + `${identityOidcAuth.oidcDiscoveryUrl}/.well-known/openid-configuration`, + { + httpsAgent: requestAgent + } + ); + const jwksUri = discoveryDoc.jwks_uri; + + const decodedToken = jwt.decode(oidcJwt, { complete: true }); + if (!decodedToken) { + throw new BadRequestError({ + message: "Invalid JWT" + }); + } + + const client = new JwksClient({ + jwksUri, + requestAgent + }); + + const { kid } = decodedToken.header; + const oidcSigningKey = await client.getSigningKey(kid); + + const tokenData = jwt.verify(oidcJwt, oidcSigningKey.getPublicKey(), { + issuer: identityOidcAuth.boundIssuer + }) as Record; + + if (identityOidcAuth.boundSubject) { + if (tokenData.sub !== identityOidcAuth.boundSubject) { + throw new ForbiddenRequestError({ + message: "Access denied: OIDC subject not allowed." + }); + } + } + + if (identityOidcAuth.boundAudiences) { + if (!identityOidcAuth.boundAudiences.split(", ").includes(tokenData.aud)) { + throw new ForbiddenRequestError({ + message: "Access denied: OIDC audience not allowed." + }); + } + } + + if (identityOidcAuth.boundClaims) { + Object.keys(identityOidcAuth.boundClaims).forEach((claimKey) => { + const claimValue = (identityOidcAuth.boundClaims as Record)[claimKey]; + // handle both single and multi-valued claims + if (!claimValue.split(", ").some((claimEntry) => tokenData[claimKey] === claimEntry)) { + throw new ForbiddenRequestError({ + message: "Access denied: OIDC claim not allowed." + }); + } + }); + } + + const identityAccessToken = await identityOidcAuthDAL.transaction(async (tx) => { + const newToken = await identityAccessTokenDAL.create( + { + identityId: identityOidcAuth.identityId, + isAccessTokenRevoked: false, + accessTokenTTL: identityOidcAuth.accessTokenTTL, + accessTokenMaxTTL: identityOidcAuth.accessTokenMaxTTL, + accessTokenNumUses: 0, + accessTokenNumUsesLimit: identityOidcAuth.accessTokenNumUsesLimit + }, + tx + ); + return newToken; + }); + + const appCfg = getConfig(); + const accessToken = jwt.sign( + { + identityId: identityOidcAuth.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, identityOidcAuth, identityAccessToken, identityMembershipOrg }; + }; + + const attachOidcAuth = async ({ + identityId, + oidcDiscoveryUrl, + caCert, + boundIssuer, + boundAudiences, + boundClaims, + boundSubject, + accessTokenTTL, + accessTokenMaxTTL, + accessTokenNumUsesLimit, + accessTokenTrustedIps, + actorId, + actorAuthMethod, + actor, + actorOrgId + }: TAttachOidcAuthDTO) => { + 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 OIDC 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 orgBot = await orgBotDAL.transaction(async (tx) => { + const doc = await orgBotDAL.findOne({ orgId: identityMembershipOrg.orgId }, tx); + if (doc) return doc; + + const { privateKey, publicKey } = generateAsymmetricKeyPair(); + const key = generateSymmetricKey(); + const { + ciphertext: encryptedPrivateKey, + iv: privateKeyIV, + tag: privateKeyTag, + encoding: privateKeyKeyEncoding, + algorithm: privateKeyAlgorithm + } = infisicalSymmetricEncypt(privateKey); + const { + ciphertext: encryptedSymmetricKey, + iv: symmetricKeyIV, + tag: symmetricKeyTag, + encoding: symmetricKeyKeyEncoding, + algorithm: symmetricKeyAlgorithm + } = infisicalSymmetricEncypt(key); + + return orgBotDAL.create( + { + name: "Infisical org bot", + publicKey, + privateKeyIV, + encryptedPrivateKey, + symmetricKeyIV, + symmetricKeyTag, + encryptedSymmetricKey, + symmetricKeyAlgorithm, + orgId: identityMembershipOrg.orgId, + privateKeyTag, + privateKeyAlgorithm, + privateKeyKeyEncoding, + symmetricKeyKeyEncoding + }, + tx + ); + }); + + const key = infisicalSymmetricDecrypt({ + ciphertext: orgBot.encryptedSymmetricKey, + iv: orgBot.symmetricKeyIV, + tag: orgBot.symmetricKeyTag, + keyEncoding: orgBot.symmetricKeyKeyEncoding as SecretKeyEncoding + }); + + const { ciphertext: encryptedCaCert, iv: caCertIV, tag: caCertTag } = encryptSymmetric(caCert, key); + + const identityOidcAuth = await identityOidcAuthDAL.transaction(async (tx) => { + const doc = await identityOidcAuthDAL.create( + { + identityId: identityMembershipOrg.identityId, + oidcDiscoveryUrl, + encryptedCaCert, + caCertIV, + caCertTag, + boundIssuer, + boundAudiences, + boundClaims, + boundSubject, + accessTokenMaxTTL, + accessTokenTTL, + accessTokenNumUsesLimit, + accessTokenTrustedIps: JSON.stringify(reformattedAccessTokenTrustedIps) + }, + tx + ); + await identityDAL.updateById( + identityMembershipOrg.identityId, + { + authMethod: IdentityAuthMethod.OIDC_AUTH + }, + tx + ); + return doc; + }); + return { ...identityOidcAuth, orgId: identityMembershipOrg.orgId, caCert }; + }; + + const updateOidcAuth = async ({ + identityId, + oidcDiscoveryUrl, + caCert, + boundIssuer, + boundAudiences, + boundClaims, + boundSubject, + accessTokenTTL, + accessTokenMaxTTL, + accessTokenNumUsesLimit, + accessTokenTrustedIps, + actorId, + actorAuthMethod, + actor, + actorOrgId + }: TUpdateOidcAuthDTO) => { + const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId }); + if (!identityMembershipOrg) { + throw new BadRequestError({ message: "Failed to find identity" }); + } + + if (identityMembershipOrg.identity?.authMethod !== IdentityAuthMethod.OIDC_AUTH) { + throw new BadRequestError({ + message: "Failed to update OIDC Auth" + }); + } + + const identityOidcAuth = await identityOidcAuthDAL.findOne({ identityId }); + + if ( + (accessTokenMaxTTL || identityOidcAuth.accessTokenMaxTTL) > 0 && + (accessTokenTTL || identityOidcAuth.accessTokenMaxTTL) > (accessTokenMaxTTL || identityOidcAuth.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 updateQuery: TIdentityOidcAuthsUpdate = { + oidcDiscoveryUrl, + boundIssuer, + boundAudiences, + boundClaims, + boundSubject, + accessTokenMaxTTL, + accessTokenTTL, + accessTokenNumUsesLimit, + accessTokenTrustedIps: reformattedAccessTokenTrustedIps + ? JSON.stringify(reformattedAccessTokenTrustedIps) + : undefined + }; + + const orgBot = await orgBotDAL.findOne({ orgId: identityMembershipOrg.orgId }); + if (!orgBot) { + throw new BadRequestError({ message: "Org bot not found", name: "OrgBotNotFound" }); + } + + const key = infisicalSymmetricDecrypt({ + ciphertext: orgBot.encryptedSymmetricKey, + iv: orgBot.symmetricKeyIV, + tag: orgBot.symmetricKeyTag, + keyEncoding: orgBot.symmetricKeyKeyEncoding as SecretKeyEncoding + }); + + if (caCert !== undefined) { + const { ciphertext: encryptedCACert, iv: caCertIV, tag: caCertTag } = encryptSymmetric(caCert, key); + updateQuery.encryptedCaCert = encryptedCACert; + updateQuery.caCertIV = caCertIV; + updateQuery.caCertTag = caCertTag; + } + + const updatedOidcAuth = await identityOidcAuthDAL.updateById(identityOidcAuth.id, updateQuery); + const updatedCACert = + updatedOidcAuth.encryptedCaCert && updatedOidcAuth.caCertIV && updatedOidcAuth.caCertTag + ? decryptSymmetric({ + ciphertext: updatedOidcAuth.encryptedCaCert, + iv: updatedOidcAuth.caCertIV, + tag: updatedOidcAuth.caCertTag, + key + }) + : ""; + + return { + ...updatedOidcAuth, + orgId: identityMembershipOrg.orgId, + caCert: updatedCACert + }; + }; + + const getOidcAuth = async ({ identityId, actorId, actor, actorAuthMethod, actorOrgId }: TGetOidcAuthDTO) => { + const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId }); + if (!identityMembershipOrg) { + throw new BadRequestError({ message: "Failed to find identity" }); + } + + if (identityMembershipOrg.identity?.authMethod !== IdentityAuthMethod.OIDC_AUTH) { + throw new BadRequestError({ + message: "The identity does not have OIDC Auth attached" + }); + } + + const { permission } = await permissionService.getOrgPermission( + actor, + actorId, + identityMembershipOrg.orgId, + actorAuthMethod, + actorOrgId + ); + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Identity); + + const identityOidcAuth = await identityOidcAuthDAL.findOne({ identityId }); + + const orgBot = await orgBotDAL.findOne({ orgId: identityMembershipOrg.orgId }); + if (!orgBot) { + throw new BadRequestError({ message: "Org bot not found", name: "OrgBotNotFound" }); + } + + const key = infisicalSymmetricDecrypt({ + ciphertext: orgBot.encryptedSymmetricKey, + iv: orgBot.symmetricKeyIV, + tag: orgBot.symmetricKeyTag, + keyEncoding: orgBot.symmetricKeyKeyEncoding as SecretKeyEncoding + }); + + const caCert = decryptSymmetric({ + ciphertext: identityOidcAuth.encryptedCaCert, + iv: identityOidcAuth.caCertIV, + tag: identityOidcAuth.caCertTag, + key + }); + + return { ...identityOidcAuth, orgId: identityMembershipOrg.orgId, caCert }; + }; + + const revokeOidcAuth = async ({ identityId, actorId, actor, actorAuthMethod, actorOrgId }: TRevokeOidcAuthDTO) => { + const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId }); + if (!identityMembershipOrg) { + throw new BadRequestError({ message: "Failed to find identity" }); + } + + if (identityMembershipOrg.identity?.authMethod !== IdentityAuthMethod.OIDC_AUTH) { + throw new BadRequestError({ + message: "The identity does not have OIDC auth" + }); + } + + const { permission } = await permissionService.getOrgPermission( + actor, + actorId, + identityMembershipOrg.orgId, + actorAuthMethod, + actorOrgId + ); + + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Identity); + + const { permission: rolePermission } = await permissionService.getOrgPermission( + ActorType.IDENTITY, + identityMembershipOrg.identityId, + identityMembershipOrg.orgId, + actorAuthMethod, + actorOrgId + ); + + const hasPriviledge = isAtLeastAsPrivileged(permission, rolePermission); + if (!hasPriviledge) { + throw new ForbiddenRequestError({ + message: "Failed to revoke OIDC auth of identity with more privileged role" + }); + } + + const revokedIdentityOidcAuth = await identityOidcAuthDAL.transaction(async (tx) => { + const deletedOidcAuth = await identityOidcAuthDAL.delete({ identityId }, tx); + await identityDAL.updateById(identityId, { authMethod: null }, tx); + return { ...deletedOidcAuth?.[0], orgId: identityMembershipOrg.orgId }; + }); + + return revokedIdentityOidcAuth; + }; + + return { + attachOidcAuth, + updateOidcAuth, + getOidcAuth, + revokeOidcAuth, + login + }; +}; diff --git a/backend/src/services/identity-oidc-auth/identity-oidc-auth-types.ts b/backend/src/services/identity-oidc-auth/identity-oidc-auth-types.ts new file mode 100644 index 000000000..761f68aa7 --- /dev/null +++ b/backend/src/services/identity-oidc-auth/identity-oidc-auth-types.ts @@ -0,0 +1,42 @@ +import { TProjectPermission } from "@app/lib/types"; + +export type TAttachOidcAuthDTO = { + identityId: string; + oidcDiscoveryUrl: string; + caCert: string; + boundIssuer: string; + boundAudiences: string; + boundClaims: Record; + boundSubject: string; + accessTokenTTL: number; + accessTokenMaxTTL: number; + accessTokenNumUsesLimit: number; + accessTokenTrustedIps: { ipAddress: string }[]; +} & Omit; + +export type TUpdateOidcAuthDTO = { + identityId: string; + oidcDiscoveryUrl?: string; + caCert?: string; + boundIssuer?: string; + boundAudiences?: string; + boundClaims?: Record; + boundSubject?: string; + accessTokenTTL?: number; + accessTokenMaxTTL?: number; + accessTokenNumUsesLimit?: number; + accessTokenTrustedIps?: { ipAddress: string }[]; +} & Omit; + +export type TGetOidcAuthDTO = { + identityId: string; +} & Omit; + +export type TLoginOidcAuthDTO = { + identityId: string; + jwt: string; +}; + +export type TRevokeOidcAuthDTO = { + identityId: string; +} & Omit; diff --git a/backend/src/services/identity-oidc-auth/identity-oidc-auth-validators.ts b/backend/src/services/identity-oidc-auth/identity-oidc-auth-validators.ts new file mode 100644 index 000000000..f20702604 --- /dev/null +++ b/backend/src/services/identity-oidc-auth/identity-oidc-auth-validators.ts @@ -0,0 +1,25 @@ +import { z } from "zod"; + +export const validateOidcAuthAudiencesField = z + .string() + .trim() + .default("") + .transform((data) => { + if (data === "") return ""; + return data + .split(",") + .map((id) => id.trim()) + .join(", "); + }); + +export const validateOidcBoundClaimsField = z.record(z.string()).transform((data) => { + const formattedClaims: Record = {}; + Object.keys(data).forEach((key) => { + formattedClaims[key] = data[key] + .split(",") + .map((id) => id.trim()) + .join(", "); + }); + + return formattedClaims; +}); diff --git a/backend/src/services/identity-project/identity-project-dal.ts b/backend/src/services/identity-project/identity-project-dal.ts index adfc9cf77..76618889b 100644 --- a/backend/src/services/identity-project/identity-project-dal.ts +++ b/backend/src/services/identity-project/identity-project-dal.ts @@ -111,6 +111,7 @@ export const identityProjectDALFactory = (db: TDbClient) => { try { const docs = await (tx || db.replicaNode())(TableName.IdentityProjectMembership) .where(`${TableName.IdentityProjectMembership}.projectId`, projectId) + .join(TableName.Project, `${TableName.IdentityProjectMembership}.projectId`, `${TableName.Project}.id`) .join(TableName.Identity, `${TableName.IdentityProjectMembership}.identityId`, `${TableName.Identity}.id`) .where((qb) => { if (filter.identityId) { @@ -149,12 +150,13 @@ export const identityProjectDALFactory = (db: TDbClient) => { db.ref("isTemporary").withSchema(TableName.IdentityProjectMembershipRole), db.ref("temporaryRange").withSchema(TableName.IdentityProjectMembershipRole), db.ref("temporaryAccessStartTime").withSchema(TableName.IdentityProjectMembershipRole), - db.ref("temporaryAccessEndTime").withSchema(TableName.IdentityProjectMembershipRole) + db.ref("temporaryAccessEndTime").withSchema(TableName.IdentityProjectMembershipRole), + db.ref("name").as("projectName").withSchema(TableName.Project) ); const members = sqlNestRelationships({ data: docs, - parentMapper: ({ identityId, identityName, identityAuthMethod, id, createdAt, updatedAt }) => ({ + parentMapper: ({ identityId, identityName, identityAuthMethod, id, createdAt, updatedAt, projectName }) => ({ id, identityId, createdAt, @@ -163,6 +165,10 @@ export const identityProjectDALFactory = (db: TDbClient) => { id: identityId, name: identityName, authMethod: identityAuthMethod + }, + project: { + id: projectId, + name: projectName } }), key: "id", diff --git a/backend/src/services/integration-auth/integration-auth-service.ts b/backend/src/services/integration-auth/integration-auth-service.ts index a5514de71..e5126532f 100644 --- a/backend/src/services/integration-auth/integration-auth-service.ts +++ b/backend/src/services/integration-auth/integration-auth-service.ts @@ -574,14 +574,14 @@ export const integrationAuthServiceFactory = ({ const botKey = await projectBotService.getBotKey(integrationAuth.projectId); const { accessId, accessToken } = await getIntegrationAccessToken(integrationAuth, botKey); - AWS.config.update({ + const kms = new AWS.KMS({ region, credentials: { accessKeyId: String(accessId), secretAccessKey: accessToken } }); - const kms = new AWS.KMS(); + const aliases = await kms.listAliases({}).promise(); const keyAliases = aliases.Aliases!.filter((alias) => { diff --git a/backend/src/services/kms/internal-kms-dal.ts b/backend/src/services/kms/internal-kms-dal.ts new file mode 100644 index 000000000..f038fc3db --- /dev/null +++ b/backend/src/services/kms/internal-kms-dal.ts @@ -0,0 +1,10 @@ +import { TDbClient } from "@app/db"; +import { TableName } from "@app/db/schemas"; +import { ormify } from "@app/lib/knex"; + +export type TInternalKmsDALFactory = ReturnType; + +export const internalKmsDALFactory = (db: TDbClient) => { + const internalKmsOrm = ormify(db, TableName.InternalKms); + return internalKmsOrm; +}; diff --git a/backend/src/services/kms/kms-dal.ts b/backend/src/services/kms/kms-dal.ts deleted file mode 100644 index bee667e10..000000000 --- a/backend/src/services/kms/kms-dal.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { TDbClient } from "@app/db"; -import { TableName } from "@app/db/schemas"; -import { ormify } from "@app/lib/knex"; - -export type TKmsDALFactory = ReturnType; - -export const kmsDALFactory = (db: TDbClient) => { - const kmsOrm = ormify(db, TableName.KmsKey); - return kmsOrm; -}; diff --git a/backend/src/services/kms/kms-key-dal.ts b/backend/src/services/kms/kms-key-dal.ts new file mode 100644 index 000000000..8e1e17cd1 --- /dev/null +++ b/backend/src/services/kms/kms-key-dal.ts @@ -0,0 +1,64 @@ +import { Knex } from "knex"; + +import { TDbClient } from "@app/db"; +import { KmsKeysSchema, TableName } from "@app/db/schemas"; +import { DatabaseError } from "@app/lib/errors"; +import { ormify, selectAllTableCols } from "@app/lib/knex"; + +export type TKmsKeyDALFactory = ReturnType; + +export const kmskeyDALFactory = (db: TDbClient) => { + const kmsOrm = ormify(db, TableName.KmsKey); + + const findByIdWithAssociatedKms = async (id: string, tx?: Knex) => { + try { + const result = await (tx || db.replicaNode())(TableName.KmsKey) + .where({ [`${TableName.KmsKey}.id` as "id"]: id }) + .leftJoin(TableName.InternalKms, `${TableName.KmsKey}.id`, `${TableName.InternalKms}.kmsKeyId`) + .leftJoin(TableName.ExternalKms, `${TableName.KmsKey}.id`, `${TableName.ExternalKms}.kmsKeyId`) + .first() + .select(selectAllTableCols(TableName.KmsKey)) + .select( + db.ref("id").withSchema(TableName.InternalKms).as("internalKmsId"), + db.ref("encryptedKey").withSchema(TableName.InternalKms).as("internalKmsEncryptedKey"), + db.ref("encryptionAlgorithm").withSchema(TableName.InternalKms).as("internalKmsEncryptionAlgorithm"), + db.ref("version").withSchema(TableName.InternalKms).as("internalKmsVersion"), + db.ref("id").withSchema(TableName.InternalKms).as("internalKmsId") + ) + .select( + db.ref("id").withSchema(TableName.ExternalKms).as("externalKmsId"), + db.ref("provider").withSchema(TableName.ExternalKms).as("externalKmsProvider"), + db.ref("encryptedProviderInputs").withSchema(TableName.ExternalKms).as("externalKmsEncryptedProviderInput"), + db.ref("status").withSchema(TableName.ExternalKms).as("externalKmsStatus"), + db.ref("statusDetails").withSchema(TableName.ExternalKms).as("externalKmsStatusDetails") + ); + + const data = { + ...KmsKeysSchema.parse(result), + isExternal: Boolean(result?.externalKmsId), + externalKms: result?.externalKmsId + ? { + id: result.externalKmsId, + provider: result.externalKmsProvider, + encryptedProviderInput: result.externalKmsEncryptedProviderInput, + status: result.externalKmsStatus, + statusDetails: result.externalKmsStatusDetails + } + : undefined, + internalKms: result?.internalKmsId + ? { + id: result.internalKmsId, + encryptedKey: result.internalKmsEncryptedKey, + encryptionAlgorithm: result.internalKmsEncryptionAlgorithm, + version: result.internalKmsVersion + } + : undefined + }; + return data; + } catch (error) { + throw new DatabaseError({ error, name: "Find by id" }); + } + }; + + return { ...kmsOrm, findByIdWithAssociatedKms }; +}; diff --git a/backend/src/services/kms/kms-service.ts b/backend/src/services/kms/kms-service.ts index 63aba8939..dbc86b860 100644 --- a/backend/src/services/kms/kms-service.ts +++ b/backend/src/services/kms/kms-service.ts @@ -1,18 +1,34 @@ +import slugify from "@sindresorhus/slugify"; +import { Knex } from "knex"; + import { TKeyStoreFactory } from "@app/keystore/keystore"; import { getConfig } from "@app/lib/config/env"; import { randomSecureBytes } from "@app/lib/crypto"; import { symmetricCipherService, SymmetricEncryption } from "@app/lib/crypto/cipher"; import { BadRequestError } from "@app/lib/errors"; import { logger } from "@app/lib/logger"; +import { alphaNumericNanoId } from "@app/lib/nanoid"; -import { TKmsDALFactory } from "./kms-dal"; +import { TOrgDALFactory } from "../org/org-dal"; +import { TProjectDALFactory } from "../project/project-dal"; +import { TInternalKmsDALFactory } from "./internal-kms-dal"; +import { TKmsKeyDALFactory } from "./kms-key-dal"; import { TKmsRootConfigDALFactory } from "./kms-root-config-dal"; -import { TDecryptWithKmsDTO, TEncryptWithKmsDTO, TGenerateKMSDTO } from "./kms-types"; +import { + TDecryptWithKeyDTO, + TDecryptWithKmsDTO, + TEncryptionWithKeyDTO, + TEncryptWithKmsDTO, + TGenerateKMSDTO +} from "./kms-types"; type TKmsServiceFactoryDep = { - kmsDAL: TKmsDALFactory; + kmsDAL: TKmsKeyDALFactory; + projectDAL: Pick; + orgDAL: Pick; kmsRootConfigDAL: Pick; keyStore: Pick; + internalKmsDAL: Pick; }; export type TKmsServiceFactory = ReturnType; @@ -25,54 +41,161 @@ const KMS_ROOT_CREATION_WAIT_TIME = 10; // akhilmhdh: Don't edit this value. This is measured for blob concatination in kms const KMS_VERSION = "v01"; const KMS_VERSION_BLOB_LENGTH = 3; -export const kmsServiceFactory = ({ kmsDAL, kmsRootConfigDAL, keyStore }: TKmsServiceFactoryDep) => { +export const kmsServiceFactory = ({ + kmsDAL, + kmsRootConfigDAL, + keyStore, + internalKmsDAL, + orgDAL, + projectDAL +}: TKmsServiceFactoryDep) => { let ROOT_ENCRYPTION_KEY = Buffer.alloc(0); // this is used symmetric encryption - const generateKmsKey = async ({ scopeId, scopeType, isReserved = true, tx }: TGenerateKMSDTO) => { + const generateKmsKey = async ({ orgId, isReserved = true, tx, slug }: TGenerateKMSDTO) => { const cipher = symmetricCipherService(SymmetricEncryption.AES_GCM_256); const kmsKeyMaterial = randomSecureBytes(32); const encryptedKeyMaterial = cipher.encrypt(kmsKeyMaterial, ROOT_ENCRYPTION_KEY); + const sanitizedSlug = slug ? slugify(slug) : slugify(alphaNumericNanoId(8).toLowerCase()); + const dbQuery = async (db: Knex) => { + const kmsDoc = await kmsDAL.create( + { + slug: sanitizedSlug, + orgId, + isReserved + }, + db + ); - const { encryptedKey, ...doc } = await kmsDAL.create( - { - version: 1, - encryptedKey: encryptedKeyMaterial, - encryptionAlgorithm: SymmetricEncryption.AES_GCM_256, - isReserved, - orgId: scopeType === "org" ? scopeId : undefined, - projectId: scopeType === "project" ? scopeId : undefined - }, - tx - ); + await internalKmsDAL.create( + { + version: 1, + encryptedKey: encryptedKeyMaterial, + encryptionAlgorithm: SymmetricEncryption.AES_GCM_256, + kmsKeyId: kmsDoc.id + }, + db + ); + return kmsDoc; + }; + if (tx) return dbQuery(tx); + const doc = await kmsDAL.transaction(async (tx2) => dbQuery(tx2)); return doc; }; - const encrypt = async ({ kmsId, plainText }: TEncryptWithKmsDTO) => { - const kmsDoc = await kmsDAL.findById(kmsId); + const encryptWithKmsKey = async ({ kmsId }: Omit) => { + const kmsDoc = await kmsDAL.findByIdWithAssociatedKms(kmsId); if (!kmsDoc) throw new BadRequestError({ message: "KMS ID not found" }); // akhilmhdh: as more encryption are added do a check here on kmsDoc.encryptionAlgorithm const cipher = symmetricCipherService(SymmetricEncryption.AES_GCM_256); + return ({ plainText }: Pick) => { + const kmsKey = cipher.decrypt(kmsDoc.internalKms?.encryptedKey as Buffer, ROOT_ENCRYPTION_KEY); + const encryptedPlainTextBlob = cipher.encrypt(plainText, kmsKey); - const kmsKey = cipher.decrypt(kmsDoc.encryptedKey, ROOT_ENCRYPTION_KEY); - const encryptedPlainTextBlob = cipher.encrypt(plainText, kmsKey); - - // Buffer#1 encrypted text + Buffer#2 version number - const versionBlob = Buffer.from(KMS_VERSION, "utf8"); // length is 3 - const cipherTextBlob = Buffer.concat([encryptedPlainTextBlob, versionBlob]); - return { cipherTextBlob }; + // Buffer#1 encrypted text + Buffer#2 version number + const versionBlob = Buffer.from(KMS_VERSION, "utf8"); // length is 3 + const cipherTextBlob = Buffer.concat([encryptedPlainTextBlob, versionBlob]); + return { cipherTextBlob }; + }; }; - const decrypt = async ({ cipherTextBlob: versionedCipherTextBlob, kmsId }: TDecryptWithKmsDTO) => { - const kmsDoc = await kmsDAL.findById(kmsId); - if (!kmsDoc) throw new BadRequestError({ message: "KMS ID not found" }); + const encryptWithInputKey = async ({ key }: Omit) => { // akhilmhdh: as more encryption are added do a check here on kmsDoc.encryptionAlgorithm const cipher = symmetricCipherService(SymmetricEncryption.AES_GCM_256); - const kmsKey = cipher.decrypt(kmsDoc.encryptedKey, ROOT_ENCRYPTION_KEY); + return ({ plainText }: Pick) => { + const encryptedPlainTextBlob = cipher.encrypt(plainText, key); + // Buffer#1 encrypted text + Buffer#2 version number + const versionBlob = Buffer.from(KMS_VERSION, "utf8"); // length is 3 + const cipherTextBlob = Buffer.concat([encryptedPlainTextBlob, versionBlob]); + return { cipherTextBlob }; + }; + }; - const cipherTextBlob = versionedCipherTextBlob.subarray(0, -KMS_VERSION_BLOB_LENGTH); - const decryptedBlob = cipher.decrypt(cipherTextBlob, kmsKey); - return decryptedBlob; + const decryptWithKmsKey = async ({ kmsId }: Omit) => { + const kmsDoc = await kmsDAL.findByIdWithAssociatedKms(kmsId); + if (!kmsDoc) throw new BadRequestError({ message: "KMS ID not found" }); + const cipher = symmetricCipherService(SymmetricEncryption.AES_GCM_256); + const kmsKey = cipher.decrypt(kmsDoc.internalKms?.encryptedKey as Buffer, ROOT_ENCRYPTION_KEY); + + return ({ cipherTextBlob: versionedCipherTextBlob }: Pick) => { + const cipherTextBlob = versionedCipherTextBlob.subarray(0, -KMS_VERSION_BLOB_LENGTH); + const decryptedBlob = cipher.decrypt(cipherTextBlob, kmsKey); + return decryptedBlob; + }; + }; + + const decryptWithInputKey = async ({ key }: Omit) => { + const cipher = symmetricCipherService(SymmetricEncryption.AES_GCM_256); + + return ({ cipherTextBlob: versionedCipherTextBlob }: Pick) => { + const cipherTextBlob = versionedCipherTextBlob.subarray(0, -KMS_VERSION_BLOB_LENGTH); + const decryptedBlob = cipher.decrypt(cipherTextBlob, key); + return decryptedBlob; + }; + }; + + const getOrgKmsKeyId = async (orgId: string) => { + const keyId = await orgDAL.transaction(async (tx) => { + const org = await orgDAL.findById(orgId, tx); + if (!org) { + throw new BadRequestError({ message: "Org not found" }); + } + + if (!org.kmsDefaultKeyId) { + // create default kms key for certificate service + const key = await generateKmsKey({ + isReserved: true, + orgId: org.id, + tx + }); + + await orgDAL.updateById( + org.id, + { + kmsDefaultKeyId: key.id + }, + tx + ); + + return key.id; + } + + return org.kmsDefaultKeyId; + }); + + return keyId; + }; + + const getProjectSecretManagerKmsKeyId = async (projectId: string) => { + const keyId = await projectDAL.transaction(async (tx) => { + const project = await projectDAL.findById(projectId, tx); + if (!project) { + throw new BadRequestError({ message: "Project not found" }); + } + + if (!project.kmsSecretManagerKeyId) { + // create default kms key for certificate service + const key = await generateKmsKey({ + isReserved: true, + orgId: project.orgId, + tx + }); + + await projectDAL.updateById( + projectId, + { + kmsSecretManagerKeyId: key.id + }, + tx + ); + + return key.id; + } + + return project.kmsSecretManagerKeyId; + }); + + return keyId; }; const startService = async () => { @@ -123,7 +246,11 @@ export const kmsServiceFactory = ({ kmsDAL, kmsRootConfigDAL, keyStore }: TKmsSe return { startService, generateKmsKey, - encrypt, - decrypt + encryptWithKmsKey, + encryptWithInputKey, + decryptWithKmsKey, + decryptWithInputKey, + getOrgKmsKeyId, + getProjectSecretManagerKmsKeyId }; }; diff --git a/backend/src/services/kms/kms-types.ts b/backend/src/services/kms/kms-types.ts index 63fdaf484..5ba6c1343 100644 --- a/backend/src/services/kms/kms-types.ts +++ b/backend/src/services/kms/kms-types.ts @@ -1,9 +1,9 @@ import { Knex } from "knex"; export type TGenerateKMSDTO = { - scopeType: "project" | "org"; - scopeId: string; + orgId: string; isReserved?: boolean; + slug?: string; tx?: Knex; }; @@ -12,7 +12,17 @@ export type TEncryptWithKmsDTO = { plainText: Buffer; }; +export type TEncryptionWithKeyDTO = { + key: Buffer; + plainText: Buffer; +}; + export type TDecryptWithKmsDTO = { kmsId: string; cipherTextBlob: Buffer; }; + +export type TDecryptWithKeyDTO = { + key: Buffer; + cipherTextBlob: Buffer; +}; diff --git a/backend/src/services/org-membership/org-membership-dal.ts b/backend/src/services/org-membership/org-membership-dal.ts index 9990d9c3d..ccd995296 100644 --- a/backend/src/services/org-membership/org-membership-dal.ts +++ b/backend/src/services/org-membership/org-membership-dal.ts @@ -1,5 +1,6 @@ import { TDbClient } from "@app/db"; -import { TableName } from "@app/db/schemas"; +import { TableName, TUserEncryptionKeys } from "@app/db/schemas"; +import { DatabaseError } from "@app/lib/errors"; import { ormify } from "@app/lib/knex"; export type TOrgMembershipDALFactory = ReturnType; @@ -7,7 +8,51 @@ export type TOrgMembershipDALFactory = ReturnType { const orgMembershipOrm = ormify(db, TableName.OrgMembership); + const findOrgMembershipById = async (membershipId: string) => { + try { + const member = await db + .replicaNode()(TableName.OrgMembership) + .where(`${TableName.OrgMembership}.id`, membershipId) + .join(TableName.Users, `${TableName.OrgMembership}.userId`, `${TableName.Users}.id`) + .leftJoin( + TableName.UserEncryptionKey, + `${TableName.UserEncryptionKey}.userId`, + `${TableName.Users}.id` + ) + .select( + db.ref("id").withSchema(TableName.OrgMembership), + db.ref("inviteEmail").withSchema(TableName.OrgMembership), + db.ref("orgId").withSchema(TableName.OrgMembership), + db.ref("role").withSchema(TableName.OrgMembership), + db.ref("roleId").withSchema(TableName.OrgMembership), + db.ref("status").withSchema(TableName.OrgMembership), + db.ref("isActive").withSchema(TableName.OrgMembership), + db.ref("email").withSchema(TableName.Users), + db.ref("username").withSchema(TableName.Users), + db.ref("firstName").withSchema(TableName.Users), + db.ref("lastName").withSchema(TableName.Users), + db.ref("isEmailVerified").withSchema(TableName.Users), + db.ref("id").withSchema(TableName.Users).as("userId"), + db.ref("publicKey").withSchema(TableName.UserEncryptionKey) + ) + .where({ isGhost: false }) // MAKE SURE USER IS NOT A GHOST USER + .first(); + + if (!member) return undefined; + + const { email, isEmailVerified, username, firstName, lastName, userId, publicKey, ...data } = member; + + return { + ...data, + user: { email, isEmailVerified, username, firstName, lastName, id: userId, publicKey } + }; + } catch (error) { + throw new DatabaseError({ error, name: "Find org membership by id" }); + } + }; + return { - ...orgMembershipOrm + ...orgMembershipOrm, + findOrgMembershipById }; }; diff --git a/backend/src/services/org/org-dal.ts b/backend/src/services/org/org-dal.ts index d518a698a..051869429 100644 --- a/backend/src/services/org/org-dal.ts +++ b/backend/src/services/org/org-dal.ts @@ -74,7 +74,9 @@ export const orgDALFactory = (db: TDbClient) => { db.ref("role").withSchema(TableName.OrgMembership), db.ref("roleId").withSchema(TableName.OrgMembership), db.ref("status").withSchema(TableName.OrgMembership), + db.ref("isActive").withSchema(TableName.OrgMembership), db.ref("email").withSchema(TableName.Users), + db.ref("isEmailVerified").withSchema(TableName.Users), db.ref("username").withSchema(TableName.Users), db.ref("firstName").withSchema(TableName.Users), db.ref("lastName").withSchema(TableName.Users), @@ -83,9 +85,9 @@ export const orgDALFactory = (db: TDbClient) => { ) .where({ isGhost: false }); // MAKE SURE USER IS NOT A GHOST USER - return members.map(({ email, username, firstName, lastName, userId, publicKey, ...data }) => ({ + return members.map(({ email, isEmailVerified, username, firstName, lastName, userId, publicKey, ...data }) => ({ ...data, - user: { email, username, firstName, lastName, id: userId, publicKey } + user: { email, isEmailVerified, username, firstName, lastName, id: userId, publicKey } })); } catch (error) { throw new DatabaseError({ error, name: "Find all org members" }); @@ -207,9 +209,9 @@ export const orgDALFactory = (db: TDbClient) => { } }; - const updateById = async (orgId: string, data: Partial) => { + const updateById = async (orgId: string, data: Partial, tx?: Knex) => { try { - const [org] = await db(TableName.Organization) + const [org] = await (tx || db)(TableName.Organization) .where({ id: orgId }) .update({ ...data }) .returning("*"); diff --git a/backend/src/services/org/org-service.ts b/backend/src/services/org/org-service.ts index 248bab568..3ffc2e203 100644 --- a/backend/src/services/org/org-service.ts +++ b/backend/src/services/org/org-service.ts @@ -15,9 +15,10 @@ import { getConfig } from "@app/lib/config/env"; import { generateAsymmetricKeyPair } from "@app/lib/crypto"; import { generateSymmetricKey, infisicalSymmetricEncypt } from "@app/lib/crypto/encryption"; import { generateUserSrpKeys } from "@app/lib/crypto/srp"; -import { BadRequestError, UnauthorizedError } from "@app/lib/errors"; +import { BadRequestError, NotFoundError, UnauthorizedError } from "@app/lib/errors"; import { alphaNumericNanoId } from "@app/lib/nanoid"; import { isDisposableEmail } from "@app/lib/validator"; +import { TOrgMembershipDALFactory } from "@app/services/org-membership/org-membership-dal"; import { TUserAliasDALFactory } from "@app/services/user-alias/user-alias-dal"; import { ActorAuthMethod, ActorType, AuthMethod, AuthTokenType } from "../auth/auth-type"; @@ -38,7 +39,9 @@ import { TFindAllWorkspacesDTO, TFindOrgMembersByEmailDTO, TGetOrgGroupsDTO, + TGetOrgMembershipDTO, TInviteUserToOrgDTO, + TListProjectMembershipsByOrgMembershipIdDTO, TUpdateOrgDTO, TUpdateOrgMembershipDTO, TVerifyUserToOrgDTO @@ -54,6 +57,7 @@ type TOrgServiceFactoryDep = { projectDAL: TProjectDALFactory; projectMembershipDAL: Pick; projectKeyDAL: Pick; + orgMembershipDAL: Pick; incidentContactDAL: TIncidentContactsDALFactory; samlConfigDAL: Pick; smtpService: TSmtpService; @@ -79,6 +83,7 @@ export const orgServiceFactory = ({ projectDAL, projectMembershipDAL, projectKeyDAL, + orgMembershipDAL, tokenService, orgBotDAL, licenseService, @@ -144,10 +149,7 @@ export const orgServiceFactory = ({ return members; }; - const findAllWorkspaces = async ({ actor, actorId, actorOrgId, actorAuthMethod, orgId }: TFindAllWorkspacesDTO) => { - const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId); - ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Workspace); - + const findAllWorkspaces = async ({ actor, actorId, orgId }: TFindAllWorkspacesDTO) => { const organizationWorkspaceIds = new Set((await projectDAL.find({ orgId })).map((workspace) => workspace.id)); let workspaces: (TProjects & { organization: string } & { @@ -207,7 +209,8 @@ export const orgServiceFactory = ({ orgId, userId: user.id, role: OrgMembershipRole.Admin, - status: OrgMembershipStatus.Accepted + status: OrgMembershipStatus.Accepted, + isActive: true }; await orgDAL.createMembership(createMembershipData, tx); @@ -311,7 +314,8 @@ export const orgServiceFactory = ({ userId, orgId: org.id, role: OrgMembershipRole.Admin, - status: OrgMembershipStatus.Accepted + status: OrgMembershipStatus.Accepted, + isActive: true }, tx ); @@ -365,6 +369,7 @@ export const orgServiceFactory = ({ * */ const updateOrgMembership = async ({ role, + isActive, orgId, userId, membershipId, @@ -374,8 +379,16 @@ export const orgServiceFactory = ({ const { permission } = await permissionService.getUserOrgPermission(userId, orgId, actorAuthMethod, actorOrgId); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Member); + const foundMembership = await orgMembershipDAL.findOne({ + id: membershipId, + orgId + }); + if (!foundMembership) throw new NotFoundError({ message: "Failed to find organization membership" }); + if (foundMembership.userId === userId) + throw new BadRequestError({ message: "Cannot update own organization membership" }); + const isCustomRole = !Object.values(OrgMembershipRole).includes(role as OrgMembershipRole); - if (isCustomRole) { + if (role && isCustomRole) { const customRole = await orgRoleDAL.findOne({ slug: role, orgId }); if (!customRole) throw new BadRequestError({ name: "Update membership", message: "Role not found" }); @@ -395,7 +408,7 @@ export const orgServiceFactory = ({ return membership; } - const [membership] = await orgDAL.updateMembership({ id: membershipId, orgId }, { role, roleId: null }); + const [membership] = await orgDAL.updateMembership({ id: membershipId, orgId }, { role, roleId: null, isActive }); return membership; }; /* @@ -460,7 +473,8 @@ export const orgServiceFactory = ({ inviteEmail: inviteeEmail, orgId, role: OrgMembershipRole.Member, - status: OrgMembershipStatus.Invited + status: OrgMembershipStatus.Invited, + isActive: true }, tx ); @@ -491,7 +505,8 @@ export const orgServiceFactory = ({ orgId, userId: user.id, role: OrgMembershipRole.Member, - status: OrgMembershipStatus.Invited + status: OrgMembershipStatus.Invited, + isActive: true }, tx ); @@ -584,6 +599,24 @@ export const orgServiceFactory = ({ return { token, user }; }; + const getOrgMembership = async ({ + membershipId, + orgId, + actor, + actorId, + actorAuthMethod, + actorOrgId + }: TGetOrgMembershipDTO) => { + const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId); + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Member); + + const membership = await orgMembershipDAL.findOrgMembershipById(membershipId); + if (!membership) throw new NotFoundError({ message: "Failed to find organization membership" }); + if (membership.orgId !== orgId) throw new NotFoundError({ message: "Failed to find organization membership" }); + + return membership; + }; + const deleteOrgMembership = async ({ orgId, userId, @@ -607,6 +640,26 @@ export const orgServiceFactory = ({ return deletedMembership; }; + const listProjectMembershipsByOrgMembershipId = async ({ + orgMembershipId, + orgId, + actor, + actorId, + actorAuthMethod, + actorOrgId + }: TListProjectMembershipsByOrgMembershipIdDTO) => { + const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId); + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Member); + + const membership = await orgMembershipDAL.findOrgMembershipById(orgMembershipId); + if (!membership) throw new NotFoundError({ message: "Failed to find organization membership" }); + if (membership.orgId !== orgId) throw new NotFoundError({ message: "Failed to find organization membership" }); + + const projectMemberships = await projectMembershipDAL.findProjectMembershipsByUserId(orgId, membership.user.id); + + return projectMemberships; + }; + /* * CRUD operations of incident contacts * */ @@ -667,6 +720,7 @@ export const orgServiceFactory = ({ findOrgMembersByUsername, createOrganization, deleteOrganizationById, + getOrgMembership, deleteOrgMembership, findAllWorkspaces, addGhostUser, @@ -675,6 +729,7 @@ export const orgServiceFactory = ({ findIncidentContacts, createIncidentContact, deleteIncidentContact, - getOrgGroups + getOrgGroups, + listProjectMembershipsByOrgMembershipId }; }; diff --git a/backend/src/services/org/org-types.ts b/backend/src/services/org/org-types.ts index 0efc7ffe1..08b4c1c92 100644 --- a/backend/src/services/org/org-types.ts +++ b/backend/src/services/org/org-types.ts @@ -6,11 +6,16 @@ export type TUpdateOrgMembershipDTO = { userId: string; orgId: string; membershipId: string; - role: string; + role?: string; + isActive?: boolean; actorOrgId: string | undefined; actorAuthMethod: ActorAuthMethod; }; +export type TGetOrgMembershipDTO = { + membershipId: string; +} & TOrgPermission; + export type TDeleteOrgMembershipDTO = { userId: string; orgId: string; @@ -55,3 +60,7 @@ export type TUpdateOrgDTO = { } & TOrgPermission; export type TGetOrgGroupsDTO = TOrgPermission; + +export type TListProjectMembershipsByOrgMembershipIdDTO = { + orgMembershipId: string; +} & TOrgPermission; diff --git a/backend/src/services/project-membership/project-membership-dal.ts b/backend/src/services/project-membership/project-membership-dal.ts index 93ec6597e..19503ba3b 100644 --- a/backend/src/services/project-membership/project-membership-dal.ts +++ b/backend/src/services/project-membership/project-membership-dal.ts @@ -16,6 +16,7 @@ export const projectMembershipDALFactory = (db: TDbClient) => { const docs = await db .replicaNode()(TableName.ProjectMembership) .where({ [`${TableName.ProjectMembership}.projectId` as "projectId"]: projectId }) + .join(TableName.Project, `${TableName.ProjectMembership}.projectId`, `${TableName.Project}.id`) .join(TableName.Users, `${TableName.ProjectMembership}.userId`, `${TableName.Users}.id`) .where((qb) => { if (filter.usernames) { @@ -58,17 +59,22 @@ export const projectMembershipDALFactory = (db: TDbClient) => { db.ref("isTemporary").withSchema(TableName.ProjectUserMembershipRole), db.ref("temporaryRange").withSchema(TableName.ProjectUserMembershipRole), db.ref("temporaryAccessStartTime").withSchema(TableName.ProjectUserMembershipRole), - db.ref("temporaryAccessEndTime").withSchema(TableName.ProjectUserMembershipRole) + db.ref("temporaryAccessEndTime").withSchema(TableName.ProjectUserMembershipRole), + db.ref("name").as("projectName").withSchema(TableName.Project) ) .where({ isGhost: false }); const members = sqlNestRelationships({ data: docs, - parentMapper: ({ email, firstName, username, lastName, publicKey, isGhost, id, userId }) => ({ + parentMapper: ({ email, firstName, username, lastName, publicKey, isGhost, id, userId, projectName }) => ({ id, userId, projectId, - user: { email, username, firstName, lastName, id: userId, publicKey, isGhost } + user: { email, username, firstName, lastName, id: userId, publicKey, isGhost }, + project: { + id: projectId, + name: projectName + } }), key: "id", childrenMapper: [ @@ -151,14 +157,95 @@ export const projectMembershipDALFactory = (db: TDbClient) => { const findProjectMembershipsByUserId = async (orgId: string, userId: string) => { try { - const memberships = await db + const docs = await db .replicaNode()(TableName.ProjectMembership) - .where({ userId }) .join(TableName.Project, `${TableName.ProjectMembership}.projectId`, `${TableName.Project}.id`) - .where({ [`${TableName.Project}.orgId` as "orgId"]: orgId }) - .select(selectAllTableCols(TableName.ProjectMembership)); + .join(TableName.Users, `${TableName.ProjectMembership}.userId`, `${TableName.Users}.id`) + .where(`${TableName.Users}.id`, userId) + .where(`${TableName.Project}.orgId`, orgId) + .join( + TableName.UserEncryptionKey, + `${TableName.UserEncryptionKey}.userId`, + `${TableName.Users}.id` + ) + .join( + TableName.ProjectUserMembershipRole, + `${TableName.ProjectUserMembershipRole}.projectMembershipId`, + `${TableName.ProjectMembership}.id` + ) + .leftJoin( + TableName.ProjectRoles, + `${TableName.ProjectUserMembershipRole}.customRoleId`, + `${TableName.ProjectRoles}.id` + ) + .select( + db.ref("id").withSchema(TableName.ProjectMembership), + db.ref("isGhost").withSchema(TableName.Users), + db.ref("username").withSchema(TableName.Users), + db.ref("email").withSchema(TableName.Users), + db.ref("publicKey").withSchema(TableName.UserEncryptionKey), + db.ref("firstName").withSchema(TableName.Users), + db.ref("lastName").withSchema(TableName.Users), + db.ref("id").withSchema(TableName.Users).as("userId"), + db.ref("role").withSchema(TableName.ProjectUserMembershipRole), + db.ref("id").withSchema(TableName.ProjectUserMembershipRole).as("membershipRoleId"), + db.ref("customRoleId").withSchema(TableName.ProjectUserMembershipRole), + db.ref("name").withSchema(TableName.ProjectRoles).as("customRoleName"), + db.ref("slug").withSchema(TableName.ProjectRoles).as("customRoleSlug"), + db.ref("temporaryMode").withSchema(TableName.ProjectUserMembershipRole), + db.ref("isTemporary").withSchema(TableName.ProjectUserMembershipRole), + db.ref("temporaryRange").withSchema(TableName.ProjectUserMembershipRole), + db.ref("temporaryAccessStartTime").withSchema(TableName.ProjectUserMembershipRole), + db.ref("temporaryAccessEndTime").withSchema(TableName.ProjectUserMembershipRole), + db.ref("name").as("projectName").withSchema(TableName.Project), + db.ref("id").as("projectId").withSchema(TableName.Project) + ) + .where({ isGhost: false }); - return memberships; + const members = sqlNestRelationships({ + data: docs, + parentMapper: ({ email, firstName, username, lastName, publicKey, isGhost, id, projectId, projectName }) => ({ + id, + userId, + projectId, + user: { email, username, firstName, lastName, id: userId, publicKey, isGhost }, + project: { + id: projectId, + name: projectName + } + }), + key: "id", + childrenMapper: [ + { + label: "roles" as const, + key: "membershipRoleId", + mapper: ({ + role, + customRoleId, + customRoleName, + customRoleSlug, + membershipRoleId, + temporaryRange, + temporaryMode, + temporaryAccessEndTime, + temporaryAccessStartTime, + isTemporary + }) => ({ + id: membershipRoleId, + role, + customRoleId, + customRoleName, + customRoleSlug, + temporaryRange, + temporaryMode, + temporaryAccessEndTime, + temporaryAccessStartTime, + isTemporary + }) + } + ] + }); + return members; } catch (error) { throw new DatabaseError({ error, name: "Find project memberships by user id" }); } diff --git a/backend/src/services/project/project-fns.ts b/backend/src/services/project/project-fns.ts index 78c7b442f..d6b010e0b 100644 --- a/backend/src/services/project/project-fns.ts +++ b/backend/src/services/project/project-fns.ts @@ -71,9 +71,8 @@ export const getProjectKmsCertificateKeyId = async ({ if (!project.kmsCertificateKeyId) { // create default kms key for certificate service const key = await kmsService.generateKmsKey({ - scopeId: projectId, - scopeType: "project", isReserved: true, + orgId: project.orgId, tx }); diff --git a/backend/src/services/secret-folder/secret-folder-dal.ts b/backend/src/services/secret-folder/secret-folder-dal.ts index 283f60c7c..1803f8977 100644 --- a/backend/src/services/secret-folder/secret-folder-dal.ts +++ b/backend/src/services/secret-folder/secret-folder-dal.ts @@ -322,7 +322,7 @@ export const secretFolderDALFactory = (db: TDbClient) => { .first(); if (folder) { const { envId, envName, envSlug, ...el } = folder; - return { ...el, environment: { envId, envName, envSlug } }; + return { ...el, environment: { envId, envName, envSlug }, envId }; } } catch (error) { throw new DatabaseError({ error, name: "Find by id" }); diff --git a/backend/src/services/secret-folder/secret-folder-service.ts b/backend/src/services/secret-folder/secret-folder-service.ts index 97258c006..9d6c29454 100644 --- a/backend/src/services/secret-folder/secret-folder-service.ts +++ b/backend/src/services/secret-folder/secret-folder-service.ts @@ -6,7 +6,7 @@ import { TSecretFoldersInsert } from "@app/db/schemas"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; import { TSecretSnapshotServiceFactory } from "@app/ee/services/secret-snapshot/secret-snapshot-service"; -import { BadRequestError } from "@app/lib/errors"; +import { BadRequestError, NotFoundError } from "@app/lib/errors"; import { TProjectDALFactory } from "../project/project-dal"; import { TProjectEnvDALFactory } from "../project-env/project-env-dal"; @@ -14,6 +14,7 @@ import { TSecretFolderDALFactory } from "./secret-folder-dal"; import { TCreateFolderDTO, TDeleteFolderDTO, + TGetFolderByIdDTO, TGetFolderDTO, TUpdateFolderDTO, TUpdateManyFoldersDTO @@ -368,11 +369,22 @@ export const secretFolderServiceFactory = ({ return folders; }; + const getFolderById = async ({ actor, actorId, actorOrgId, actorAuthMethod, id }: TGetFolderByIdDTO) => { + const folder = await folderDAL.findById(id); + if (!folder) throw new NotFoundError({ message: "folder not found" }); + // folder list is allowed to be read by anyone + // permission to check does user has access + await permissionService.getProjectPermission(actor, actorId, folder.projectId, actorAuthMethod, actorOrgId); + + return folder; + }; + return { createFolder, updateFolder, updateManyFolders, deleteFolder, - getFolders + getFolders, + getFolderById }; }; diff --git a/backend/src/services/secret-folder/secret-folder-types.ts b/backend/src/services/secret-folder/secret-folder-types.ts index c01d5f7b8..5c80b9582 100644 --- a/backend/src/services/secret-folder/secret-folder-types.ts +++ b/backend/src/services/secret-folder/secret-folder-types.ts @@ -37,3 +37,7 @@ export type TGetFolderDTO = { environment: string; path: string; } & TProjectPermission; + +export type TGetFolderByIdDTO = { + id: string; +} & Omit; diff --git a/backend/src/services/secret-import/secret-import-fns.ts b/backend/src/services/secret-import/secret-import-fns.ts index 06ffbc903..f1a3a3464 100644 --- a/backend/src/services/secret-import/secret-import-fns.ts +++ b/backend/src/services/secret-import/secret-import-fns.ts @@ -90,7 +90,7 @@ export const fnSecretsFromImports = async ({ const secretsFromdeeperImportGroupedByFolderId = groupBy(secretsFromDeeperImports, (i) => i.importFolderId); const secrets = allowedImports.map(({ importPath, importEnv, id, folderId }, i) => { - const sourceImportFolder = importedFolderGroupBySourceImport[`${importEnv.id}-${importPath}`][0]; + const sourceImportFolder = importedFolderGroupBySourceImport?.[`${importEnv.id}-${importPath}`]?.[0]; const folderDeeperImportSecrets = secretsFromdeeperImportGroupedByFolderId?.[sourceImportFolder?.id || ""]?.[0]?.secrets || []; diff --git a/backend/src/services/secret-sharing/secret-sharing-dal.ts b/backend/src/services/secret-sharing/secret-sharing-dal.ts index 6b5090d66..16b66d871 100644 --- a/backend/src/services/secret-sharing/secret-sharing-dal.ts +++ b/backend/src/services/secret-sharing/secret-sharing-dal.ts @@ -1,9 +1,9 @@ import { Knex } from "knex"; import { TDbClient } from "@app/db"; -import { TableName } from "@app/db/schemas"; +import { TableName, TSecretSharing } from "@app/db/schemas"; import { DatabaseError } from "@app/lib/errors"; -import { ormify } from "@app/lib/knex"; +import { ormify, selectAllTableCols } from "@app/lib/knex"; export type TSecretSharingDALFactory = ReturnType; @@ -13,15 +13,58 @@ export const secretSharingDALFactory = (db: TDbClient) => { const pruneExpiredSharedSecrets = async (tx?: Knex) => { try { const today = new Date(); - const docs = await (tx || db)(TableName.SecretSharing).where("expiresAt", "<", today).del(); + const docs = await (tx || db)(TableName.SecretSharing) + .where("expiresAt", "<", today) + .andWhere("encryptedValue", "<>", "") + .update({ + encryptedValue: "", + tag: "", + iv: "", + hashedHex: "" + }); return docs; } catch (error) { throw new DatabaseError({ error, name: "pruneExpiredSharedSecrets" }); } }; + const findActiveSharedSecrets = async (filters: Partial, tx?: Knex) => { + try { + const now = new Date(); + return await (tx || db)(TableName.SecretSharing) + .where(filters) + .andWhere("expiresAt", ">", now) + .andWhere("encryptedValue", "<>", "") + .select(selectAllTableCols(TableName.SecretSharing)) + .orderBy("expiresAt", "asc"); + } catch (error) { + throw new DatabaseError({ + error, + name: "Find Active Shared Secrets" + }); + } + }; + + const softDeleteById = async (id: string) => { + try { + await sharedSecretOrm.updateById(id, { + encryptedValue: "", + iv: "", + tag: "", + hashedHex: "" + }); + } catch (error) { + throw new DatabaseError({ + error, + name: "Soft Delete Shared Secret" + }); + } + }; + return { ...sharedSecretOrm, - pruneExpiredSharedSecrets + pruneExpiredSharedSecrets, + softDeleteById, + findActiveSharedSecrets }; }; diff --git a/backend/src/services/secret-sharing/secret-sharing-service.ts b/backend/src/services/secret-sharing/secret-sharing-service.ts index 012b0f130..ca6da24e1 100644 --- a/backend/src/services/secret-sharing/secret-sharing-service.ts +++ b/backend/src/services/secret-sharing/secret-sharing-service.ts @@ -1,6 +1,8 @@ import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; import { BadRequestError, UnauthorizedError } from "@app/lib/errors"; +import { SecretSharingAccessType } from "@app/lib/types"; +import { TOrgDALFactory } from "../org/org-dal"; import { TSecretSharingDALFactory } from "./secret-sharing-dal"; import { TCreatePublicSharedSecretDTO, @@ -12,13 +14,15 @@ import { type TSecretSharingServiceFactoryDep = { permissionService: Pick; secretSharingDAL: TSecretSharingDALFactory; + orgDAL: TOrgDALFactory; }; export type TSecretSharingServiceFactory = ReturnType; export const secretSharingServiceFactory = ({ permissionService, - secretSharingDAL + secretSharingDAL, + orgDAL }: TSecretSharingServiceFactoryDep) => { const createSharedSecret = async (createSharedSecretInput: TCreateSharedSecretDTO) => { const { @@ -30,6 +34,7 @@ export const secretSharingServiceFactory = ({ encryptedValue, iv, tag, + accessType, hashedHex, expiresAt, expiresAfterViews @@ -62,13 +67,14 @@ export const secretSharingServiceFactory = ({ expiresAt, expiresAfterViews, userId: actorId, - orgId + orgId, + accessType }); return { id: newSharedSecret.id }; }; const createPublicSharedSecret = async (createSharedSecretInput: TCreatePublicSharedSecretDTO) => { - const { encryptedValue, iv, tag, hashedHex, expiresAt, expiresAfterViews } = createSharedSecretInput; + const { encryptedValue, iv, tag, hashedHex, expiresAt, expiresAfterViews, accessType } = createSharedSecretInput; if (new Date(expiresAt) < new Date()) { throw new BadRequestError({ message: "Expiration date cannot be in the past" }); } @@ -92,7 +98,8 @@ export const secretSharingServiceFactory = ({ tag, hashedHex, expiresAt, - expiresAfterViews + expiresAfterViews, + accessType }); return { id: newSharedSecret.id }; }; @@ -101,24 +108,39 @@ export const secretSharingServiceFactory = ({ const { actor, actorId, orgId, actorAuthMethod, actorOrgId } = getSharedSecretsInput; const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId); if (!permission) throw new UnauthorizedError({ name: "User not in org" }); - const userSharedSecrets = await secretSharingDAL.find({ userId: actorId, orgId }, { sort: [["expiresAt", "asc"]] }); + const userSharedSecrets = await secretSharingDAL.findActiveSharedSecrets({ userId: actorId, orgId }); return userSharedSecrets; }; - const getActiveSharedSecretByIdAndHashedHex = async (sharedSecretId: string, hashedHex: string) => { + const getActiveSharedSecretByIdAndHashedHex = async (sharedSecretId: string, hashedHex: string, orgId?: string) => { const sharedSecret = await secretSharingDAL.findOne({ id: sharedSecretId, hashedHex }); if (!sharedSecret) return; + + const orgName = sharedSecret.orgId ? (await orgDAL.findOrgById(sharedSecret.orgId))?.name : ""; + // Support organization level access for secret sharing + if (sharedSecret.accessType === SecretSharingAccessType.Organization && orgId !== sharedSecret.orgId) { + return { + ...sharedSecret, + encryptedValue: "", + iv: "", + tag: "", + orgName + }; + } if (sharedSecret.expiresAt && sharedSecret.expiresAt < new Date()) { return; } if (sharedSecret.expiresAfterViews != null && sharedSecret.expiresAfterViews >= 0) { if (sharedSecret.expiresAfterViews === 0) { - await secretSharingDAL.deleteById(sharedSecretId); + await secretSharingDAL.softDeleteById(sharedSecretId); return; } await secretSharingDAL.updateById(sharedSecretId, { $decr: { expiresAfterViews: 1 } }); } - return sharedSecret; + if (sharedSecret.accessType === SecretSharingAccessType.Organization && orgId === sharedSecret.orgId) { + return { ...sharedSecret, orgName }; + } + return { ...sharedSecret, orgName: undefined }; }; const deleteSharedSecretById = async (deleteSharedSecretInput: TDeleteSharedSecretDTO) => { diff --git a/backend/src/services/secret-sharing/secret-sharing-types.ts b/backend/src/services/secret-sharing/secret-sharing-types.ts index 769bb4479..a9c7dcbd9 100644 --- a/backend/src/services/secret-sharing/secret-sharing-types.ts +++ b/backend/src/services/secret-sharing/secret-sharing-types.ts @@ -1,3 +1,5 @@ +import { SecretSharingAccessType } from "@app/lib/types"; + import { ActorAuthMethod, ActorType } from "../auth/auth-type"; export type TSharedSecretPermission = { @@ -6,6 +8,7 @@ export type TSharedSecretPermission = { actorAuthMethod: ActorAuthMethod; actorOrgId: string; orgId: string; + accessType?: SecretSharingAccessType; }; export type TCreatePublicSharedSecretDTO = { @@ -15,6 +18,7 @@ export type TCreatePublicSharedSecretDTO = { hashedHex: string; expiresAt: Date; expiresAfterViews: number; + accessType: SecretSharingAccessType; }; export type TCreateSharedSecretDTO = TSharedSecretPermission & TCreatePublicSharedSecretDTO; diff --git a/backend/src/services/secret/secret-queue.ts b/backend/src/services/secret/secret-queue.ts index afcb3a6bd..0f0645f58 100644 --- a/backend/src/services/secret/secret-queue.ts +++ b/backend/src/services/secret/secret-queue.ts @@ -642,7 +642,7 @@ export const secretQueueFactory = ({ }); queueService.start(QueueName.SecretWebhook, async (job) => { - await fnTriggerWebhook({ ...job.data, projectEnvDAL, webhookDAL }); + await fnTriggerWebhook({ ...job.data, projectEnvDAL, webhookDAL, projectDAL }); }); return { diff --git a/backend/src/services/secret/secret-service.ts b/backend/src/services/secret/secret-service.ts index cbfccba91..efe5af03e 100644 --- a/backend/src/services/secret/secret-service.ts +++ b/backend/src/services/secret/secret-service.ts @@ -11,6 +11,9 @@ import { } from "@app/db/schemas"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; +import { TSecretApprovalPolicyServiceFactory } from "@app/ee/services/secret-approval-policy/secret-approval-policy-service"; +import { TSecretApprovalRequestDALFactory } from "@app/ee/services/secret-approval-request/secret-approval-request-dal"; +import { TSecretApprovalRequestSecretDALFactory } from "@app/ee/services/secret-approval-request/secret-approval-request-secret-dal"; import { TSecretSnapshotServiceFactory } from "@app/ee/services/secret-snapshot/secret-snapshot-service"; import { getConfig } from "@app/lib/config/env"; import { @@ -18,9 +21,10 @@ import { decryptSymmetric128BitHexKeyUTF8, encryptSymmetric128BitHexKeyUTF8 } from "@app/lib/crypto"; -import { BadRequestError } from "@app/lib/errors"; +import { BadRequestError, NotFoundError } from "@app/lib/errors"; import { groupBy, pick } from "@app/lib/fn"; import { logger } from "@app/lib/logger"; +import { alphaNumericNanoId } from "@app/lib/nanoid"; import { ActorType } from "../auth/auth-type"; import { TProjectDALFactory } from "../project/project-dal"; @@ -44,6 +48,7 @@ import { } from "./secret-fns"; import { TSecretQueueFactory } from "./secret-queue"; import { + SecretOperations, TAttachSecretTagsDTO, TBackFillSecretReferencesDTO, TCreateBulkSecretDTO, @@ -59,6 +64,7 @@ import { TGetSecretsDTO, TGetSecretsRawDTO, TGetSecretVersionsDTO, + TMoveSecretsDTO, TUpdateBulkSecretDTO, TUpdateManySecretRawDTO, TUpdateSecretDTO, @@ -84,6 +90,12 @@ type TSecretServiceFactoryDep = { projectBotService: Pick; secretImportDAL: Pick; secretVersionTagDAL: Pick; + secretApprovalPolicyService: Pick; + secretApprovalRequestDAL: Pick; + secretApprovalRequestSecretDAL: Pick< + TSecretApprovalRequestSecretDALFactory, + "insertMany" | "insertApprovalSecretTags" + >; }; export type TSecretServiceFactory = ReturnType; @@ -100,7 +112,10 @@ export const secretServiceFactory = ({ projectDAL, projectBotService, secretImportDAL, - secretVersionTagDAL + secretVersionTagDAL, + secretApprovalPolicyService, + secretApprovalRequestDAL, + secretApprovalRequestSecretDAL }: TSecretServiceFactoryDep) => { const getSecretReference = async (projectId: string) => { // if bot key missing means e2e still exist @@ -1683,6 +1698,393 @@ export const secretServiceFactory = ({ return { message: "Successfully backfilled secret references" }; }; + const moveSecrets = async ({ + sourceEnvironment, + sourceSecretPath, + destinationEnvironment, + destinationSecretPath, + secretIds, + projectSlug, + shouldOverwrite, + actor, + actorId, + actorAuthMethod, + actorOrgId + }: TMoveSecretsDTO) => { + const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId); + if (!project) { + throw new NotFoundError({ + message: "Project not found." + }); + } + + const { permission } = await permissionService.getProjectPermission( + actor, + actorId, + project.id, + actorAuthMethod, + actorOrgId + ); + + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Delete, + subject(ProjectPermissionSub.Secrets, { environment: sourceEnvironment, secretPath: sourceSecretPath }) + ); + + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Create, + subject(ProjectPermissionSub.Secrets, { environment: destinationEnvironment, secretPath: destinationSecretPath }) + ); + + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Edit, + subject(ProjectPermissionSub.Secrets, { environment: destinationEnvironment, secretPath: destinationSecretPath }) + ); + + const botKey = await projectBotService.getBotKey(project.id); + if (!botKey) { + throw new BadRequestError({ message: "Project bot not found", name: "bot_not_found_error" }); + } + + const sourceFolder = await folderDAL.findBySecretPath(project.id, sourceEnvironment, sourceSecretPath); + if (!sourceFolder) { + throw new NotFoundError({ + message: "Source path does not exist." + }); + } + + const destinationFolder = await folderDAL.findBySecretPath( + project.id, + destinationEnvironment, + destinationSecretPath + ); + + if (!destinationFolder) { + throw new NotFoundError({ + message: "Destination path does not exist." + }); + } + + const sourceSecrets = await secretDAL.find({ + type: SecretType.Shared, + $in: { + id: secretIds + } + }); + + if (sourceSecrets.length !== secretIds.length) { + throw new BadRequestError({ + message: "Invalid secrets" + }); + } + + const decryptedSourceSecrets = sourceSecrets.map((secret) => ({ + ...secret, + secretKey: decryptSymmetric128BitHexKeyUTF8({ + ciphertext: secret.secretKeyCiphertext, + iv: secret.secretKeyIV, + tag: secret.secretKeyTag, + key: botKey + }), + secretValue: decryptSymmetric128BitHexKeyUTF8({ + ciphertext: secret.secretValueCiphertext, + iv: secret.secretValueIV, + tag: secret.secretValueTag, + key: botKey + }) + })); + + let isSourceUpdated = false; + let isDestinationUpdated = false; + + // Moving secrets is a two-step process. + await secretDAL.transaction(async (tx) => { + // First step is to create/update the secret in the destination: + const destinationSecretsFromDB = await secretDAL.find( + { + folderId: destinationFolder.id + }, + { tx } + ); + + const decryptedDestinationSecrets = destinationSecretsFromDB.map((secret) => { + return { + ...secret, + secretKey: decryptSymmetric128BitHexKeyUTF8({ + ciphertext: secret.secretKeyCiphertext, + iv: secret.secretKeyIV, + tag: secret.secretKeyTag, + key: botKey + }), + secretValue: decryptSymmetric128BitHexKeyUTF8({ + ciphertext: secret.secretValueCiphertext, + iv: secret.secretValueIV, + tag: secret.secretValueTag, + key: botKey + }) + }; + }); + + const destinationSecretsGroupedByBlindIndex = groupBy( + decryptedDestinationSecrets.filter(({ secretBlindIndex }) => Boolean(secretBlindIndex)), + (i) => i.secretBlindIndex as string + ); + + const locallyCreatedSecrets = decryptedSourceSecrets + .filter(({ secretBlindIndex }) => !destinationSecretsGroupedByBlindIndex[secretBlindIndex as string]?.[0]) + .map((el) => ({ ...el, operation: SecretOperations.Create })); + + const locallyUpdatedSecrets = decryptedSourceSecrets + .filter( + ({ secretBlindIndex, secretKey, secretValue }) => + destinationSecretsGroupedByBlindIndex[secretBlindIndex as string]?.[0] && + // if key or value changed + (destinationSecretsGroupedByBlindIndex[secretBlindIndex as string]?.[0]?.secretKey !== secretKey || + destinationSecretsGroupedByBlindIndex[secretBlindIndex as string]?.[0]?.secretValue !== secretValue) + ) + .map((el) => ({ ...el, operation: SecretOperations.Update })); + + if (locallyUpdatedSecrets.length > 0 && !shouldOverwrite) { + const existingKeys = locallyUpdatedSecrets.map((s) => s.secretKey); + + throw new BadRequestError({ + message: `Failed to move secrets. The following secrets already exist in the destination: ${existingKeys.join( + "," + )}` + }); + } + + const isEmpty = locallyCreatedSecrets.length + locallyUpdatedSecrets.length === 0; + + if (isEmpty) { + throw new BadRequestError({ + message: "Selected secrets already exist in the destination." + }); + } + const destinationFolderPolicy = await secretApprovalPolicyService.getSecretApprovalPolicy( + project.id, + destinationFolder.environment.slug, + destinationFolder.path + ); + + if (destinationFolderPolicy && actor === ActorType.USER) { + // if secret approval policy exists for destination, we create the secret approval request + const localSecretsIds = decryptedDestinationSecrets.map(({ id }) => id); + const latestSecretVersions = await secretVersionDAL.findLatestVersionMany( + destinationFolder.id, + localSecretsIds, + tx + ); + + const approvalRequestDoc = await secretApprovalRequestDAL.create( + { + folderId: destinationFolder.id, + slug: alphaNumericNanoId(), + policyId: destinationFolderPolicy.id, + status: "open", + hasMerged: false, + committerUserId: actorId + }, + tx + ); + + const commits = locallyCreatedSecrets.concat(locallyUpdatedSecrets).map((doc) => { + const { operation } = doc; + const localSecret = destinationSecretsGroupedByBlindIndex[doc.secretBlindIndex as string]?.[0]; + + return { + op: operation, + keyEncoding: doc.keyEncoding, + algorithm: doc.algorithm, + requestId: approvalRequestDoc.id, + metadata: doc.metadata, + secretKeyIV: doc.secretKeyIV, + secretKeyTag: doc.secretKeyTag, + secretKeyCiphertext: doc.secretKeyCiphertext, + secretValueIV: doc.secretValueIV, + secretValueTag: doc.secretValueTag, + secretValueCiphertext: doc.secretValueCiphertext, + secretBlindIndex: doc.secretBlindIndex, + secretCommentIV: doc.secretCommentIV, + secretCommentTag: doc.secretCommentTag, + secretCommentCiphertext: doc.secretCommentCiphertext, + skipMultilineEncoding: doc.skipMultilineEncoding, + // except create operation other two needs the secret id and version id + ...(operation !== SecretOperations.Create + ? { secretId: localSecret.id, secretVersion: latestSecretVersions[localSecret.id].id } + : {}) + }; + }); + await secretApprovalRequestSecretDAL.insertMany(commits, tx); + } else { + // apply changes directly + if (locallyCreatedSecrets.length) { + await fnSecretBulkInsert({ + folderId: destinationFolder.id, + secretVersionDAL, + secretDAL, + tx, + secretTagDAL, + secretVersionTagDAL, + inputSecrets: locallyCreatedSecrets.map((doc) => { + return { + keyEncoding: doc.keyEncoding, + algorithm: doc.algorithm, + type: doc.type, + metadata: doc.metadata, + secretKeyIV: doc.secretKeyIV, + secretKeyTag: doc.secretKeyTag, + secretKeyCiphertext: doc.secretKeyCiphertext, + secretValueIV: doc.secretValueIV, + secretValueTag: doc.secretValueTag, + secretValueCiphertext: doc.secretValueCiphertext, + secretBlindIndex: doc.secretBlindIndex, + secretCommentIV: doc.secretCommentIV, + secretCommentTag: doc.secretCommentTag, + secretCommentCiphertext: doc.secretCommentCiphertext, + skipMultilineEncoding: doc.skipMultilineEncoding + }; + }) + }); + } + if (locallyUpdatedSecrets.length) { + await fnSecretBulkUpdate({ + projectId: project.id, + folderId: destinationFolder.id, + secretVersionDAL, + secretDAL, + tx, + secretTagDAL, + secretVersionTagDAL, + inputSecrets: locallyUpdatedSecrets.map((doc) => { + return { + filter: { + folderId: destinationFolder.id, + id: destinationSecretsGroupedByBlindIndex[doc.secretBlindIndex as string][0].id + }, + data: { + keyEncoding: doc.keyEncoding, + algorithm: doc.algorithm, + type: doc.type, + metadata: doc.metadata, + secretKeyIV: doc.secretKeyIV, + secretKeyTag: doc.secretKeyTag, + secretKeyCiphertext: doc.secretKeyCiphertext, + secretValueIV: doc.secretValueIV, + secretValueTag: doc.secretValueTag, + secretValueCiphertext: doc.secretValueCiphertext, + secretBlindIndex: doc.secretBlindIndex, + secretCommentIV: doc.secretCommentIV, + secretCommentTag: doc.secretCommentTag, + secretCommentCiphertext: doc.secretCommentCiphertext, + skipMultilineEncoding: doc.skipMultilineEncoding + } + }; + }) + }); + } + + isDestinationUpdated = true; + } + + // Next step is to delete the secrets from the source folder: + const sourceSecretsGroupByBlindIndex = groupBy(sourceSecrets, (i) => i.secretBlindIndex as string); + const locallyDeletedSecrets = decryptedSourceSecrets.map((el) => ({ ...el, operation: SecretOperations.Delete })); + + const sourceFolderPolicy = await secretApprovalPolicyService.getSecretApprovalPolicy( + project.id, + sourceFolder.environment.slug, + sourceFolder.path + ); + + if (sourceFolderPolicy && actor === ActorType.USER) { + // if secret approval policy exists for source, we create the secret approval request + const localSecretsIds = decryptedSourceSecrets.map(({ id }) => id); + const latestSecretVersions = await secretVersionDAL.findLatestVersionMany(sourceFolder.id, localSecretsIds, tx); + const approvalRequestDoc = await secretApprovalRequestDAL.create( + { + folderId: sourceFolder.id, + slug: alphaNumericNanoId(), + policyId: sourceFolderPolicy.id, + status: "open", + hasMerged: false, + committerUserId: actorId + }, + tx + ); + + const commits = locallyDeletedSecrets.map((doc) => { + const { operation } = doc; + const localSecret = sourceSecretsGroupByBlindIndex[doc.secretBlindIndex as string]?.[0]; + + return { + op: operation, + keyEncoding: doc.keyEncoding, + algorithm: doc.algorithm, + requestId: approvalRequestDoc.id, + metadata: doc.metadata, + secretKeyIV: doc.secretKeyIV, + secretKeyTag: doc.secretKeyTag, + secretKeyCiphertext: doc.secretKeyCiphertext, + secretValueIV: doc.secretValueIV, + secretValueTag: doc.secretValueTag, + secretValueCiphertext: doc.secretValueCiphertext, + secretBlindIndex: doc.secretBlindIndex, + secretCommentIV: doc.secretCommentIV, + secretCommentTag: doc.secretCommentTag, + secretCommentCiphertext: doc.secretCommentCiphertext, + skipMultilineEncoding: doc.skipMultilineEncoding, + secretId: localSecret.id, + secretVersion: latestSecretVersions[localSecret.id].id + }; + }); + + await secretApprovalRequestSecretDAL.insertMany(commits, tx); + } else { + // if no secret approval policy is present, we delete directly. + await secretDAL.delete( + { + $in: { + id: locallyDeletedSecrets.map(({ id }) => id) + }, + folderId: sourceFolder.id + }, + tx + ); + + isSourceUpdated = true; + } + }); + + if (isDestinationUpdated) { + await snapshotService.performSnapshot(destinationFolder.id); + await secretQueueService.syncSecrets({ + projectId: project.id, + secretPath: destinationFolder.path, + environmentSlug: destinationFolder.environment.slug, + actorId, + actor + }); + } + + if (isSourceUpdated) { + await snapshotService.performSnapshot(sourceFolder.id); + await secretQueueService.syncSecrets({ + projectId: project.id, + secretPath: sourceFolder.path, + environmentSlug: sourceFolder.environment.slug, + actorId, + actor + }); + } + + return { + projectId: project.id, + isSourceUpdated, + isDestinationUpdated + }; + }; + return { attachTags, detachTags, @@ -1703,6 +2105,7 @@ export const secretServiceFactory = ({ updateManySecretsRaw, deleteManySecretsRaw, getSecretVersions, - backfillSecretReferences + backfillSecretReferences, + moveSecrets }; }; diff --git a/backend/src/services/secret/secret-types.ts b/backend/src/services/secret/secret-types.ts index 10df2f258..d806eab11 100644 --- a/backend/src/services/secret/secret-types.ts +++ b/backend/src/services/secret/secret-types.ts @@ -397,3 +397,13 @@ export type TSyncSecretsDTO = { // used for import creation to trigger replication pickOnlyImportIds?: string[]; }); + +export type TMoveSecretsDTO = { + projectSlug: string; + sourceEnvironment: string; + sourceSecretPath: string; + destinationEnvironment: string; + destinationSecretPath: string; + secretIds: string[]; + shouldOverwrite: boolean; +} & Omit; diff --git a/backend/src/services/smtp/smtp-service.ts b/backend/src/services/smtp/smtp-service.ts index 1fb89c553..151127484 100644 --- a/backend/src/services/smtp/smtp-service.ts +++ b/backend/src/services/smtp/smtp-service.ts @@ -23,6 +23,7 @@ export enum SmtpTemplates { EmailMfa = "emailMfa.handlebars", UnlockAccount = "unlockAccount.handlebars", AccessApprovalRequest = "accessApprovalRequest.handlebars", + AccessSecretRequestBypassed = "accessSecretRequestBypassed.handlebars", HistoricalSecretList = "historicalSecretLeakIncident.handlebars", NewDeviceJoin = "newDevice.handlebars", OrgInvite = "organizationInvitation.handlebars", diff --git a/backend/src/services/smtp/templates/accessSecretRequestBypassed.handlebars b/backend/src/services/smtp/templates/accessSecretRequestBypassed.handlebars new file mode 100644 index 000000000..3313d352f --- /dev/null +++ b/backend/src/services/smtp/templates/accessSecretRequestBypassed.handlebars @@ -0,0 +1,28 @@ + + + + + Secret Approval Request Policy Bypassed + + + +

Infisical

+

Secret Approval Request Bypassed

+

A secret approval request has been bypassed in the project "{{projectName}}".

+ +

+ {{requesterFullName}} ({{requesterEmail}}) has merged + a secret to environment {{environment}} at secret path {{secretPath}} + without obtaining the required approvals. +

+

+ The following reason was provided for bypassing the policy: + {{bypassReason}} +

+ +

+ To review this action, please visit the request panel + here. +

+ + \ No newline at end of file diff --git a/backend/src/services/super-admin/super-admin-service.ts b/backend/src/services/super-admin/super-admin-service.ts index 650910681..e7798500f 100644 --- a/backend/src/services/super-admin/super-admin-service.ts +++ b/backend/src/services/super-admin/super-admin-service.ts @@ -1,6 +1,7 @@ import bcrypt from "bcrypt"; import { TSuperAdmin, TSuperAdminUpdate } from "@app/db/schemas"; +import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; import { TKeyStoreFactory } from "@app/keystore/keystore"; import { getConfig } from "@app/lib/config/env"; import { infisicalSymmetricEncypt } from "@app/lib/crypto/encryption"; @@ -20,6 +21,7 @@ type TSuperAdminServiceFactoryDep = { authService: Pick; orgService: Pick; keyStore: Pick; + licenseService: Pick; }; export type TSuperAdminServiceFactory = ReturnType; @@ -36,7 +38,8 @@ export const superAdminServiceFactory = ({ userDAL, authService, orgService, - keyStore + keyStore, + licenseService }: TSuperAdminServiceFactoryDep) => { const initServerCfg = async () => { // TODO(akhilmhdh): bad pattern time less change this later to me itself @@ -219,6 +222,12 @@ export const superAdminServiceFactory = ({ }; const deleteUser = async (userId: string) => { + if (!licenseService.onPremFeatures?.instanceUserManagement) { + throw new BadRequestError({ + message: "Failed to delete user due to plan restriction. Upgrade to Infisical's Pro plan." + }); + } + const user = await userDAL.deleteById(userId); return user; }; diff --git a/backend/src/services/webhook/webhook-fns.ts b/backend/src/services/webhook/webhook-fns.ts index 2439c7d65..4690edba9 100644 --- a/backend/src/services/webhook/webhook-fns.ts +++ b/backend/src/services/webhook/webhook-fns.ts @@ -9,6 +9,7 @@ import { infisicalSymmetricDecrypt } from "@app/lib/crypto/encryption"; import { BadRequestError } from "@app/lib/errors"; import { logger } from "@app/lib/logger"; +import { TProjectDALFactory } from "../project/project-dal"; import { TProjectEnvDALFactory } from "../project-env/project-env-dal"; import { TWebhookDALFactory } from "./webhook-dal"; import { WebhookType } from "./webhook-types"; @@ -66,11 +67,16 @@ export const triggerWebhookRequest = async (webhook: TWebhooks, data: Record { + const { workspaceName, workspaceId, environment, secretPath, type } = details; + switch (type) { case WebhookType.SLACK: return { @@ -80,8 +86,8 @@ export const getWebhookPayload = ( color: "#E7F256", fields: [ { - title: "Workspace ID", - value: workspaceId, + title: "Project", + value: workspaceName, short: false }, { @@ -117,7 +123,9 @@ export type TFnTriggerWebhookDTO = { environment: string; webhookDAL: Pick; projectEnvDAL: Pick; + projectDAL: Pick; }; + // this is reusable function // used in secret queue to trigger webhook and update status when secrets changes export const fnTriggerWebhook = async ({ @@ -125,7 +133,8 @@ export const fnTriggerWebhook = async ({ secretPath, projectId, webhookDAL, - projectEnvDAL + projectEnvDAL, + projectDAL }: TFnTriggerWebhookDTO) => { const webhooks = await webhookDAL.findAllWebhooks(projectId, environment); const toBeTriggeredHooks = webhooks.filter( @@ -134,9 +143,19 @@ export const fnTriggerWebhook = async ({ ); if (!toBeTriggeredHooks.length) return; logger.info("Secret webhook job started", { environment, secretPath, projectId }); + const project = await projectDAL.findById(projectId); const webhooksTriggered = await Promise.allSettled( toBeTriggeredHooks.map((hook) => - triggerWebhookRequest(hook, getWebhookPayload("secrets.modified", projectId, environment, secretPath, hook.type)) + triggerWebhookRequest( + hook, + getWebhookPayload("secrets.modified", { + workspaceName: project.name, + workspaceId: projectId, + environment, + secretPath, + type: hook.type + }) + ) ) ); diff --git a/backend/src/services/webhook/webhook-service.ts b/backend/src/services/webhook/webhook-service.ts index 272c9de90..41dacd34b 100644 --- a/backend/src/services/webhook/webhook-service.ts +++ b/backend/src/services/webhook/webhook-service.ts @@ -6,6 +6,7 @@ import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services import { infisicalSymmetricEncypt } from "@app/lib/crypto/encryption"; import { BadRequestError } from "@app/lib/errors"; +import { TProjectDALFactory } from "../project/project-dal"; import { TProjectEnvDALFactory } from "../project-env/project-env-dal"; import { TWebhookDALFactory } from "./webhook-dal"; import { decryptWebhookDetails, getWebhookPayload, triggerWebhookRequest } from "./webhook-fns"; @@ -20,12 +21,18 @@ import { type TWebhookServiceFactoryDep = { webhookDAL: TWebhookDALFactory; projectEnvDAL: TProjectEnvDALFactory; + projectDAL: Pick; permissionService: Pick; }; export type TWebhookServiceFactory = ReturnType; -export const webhookServiceFactory = ({ webhookDAL, projectEnvDAL, permissionService }: TWebhookServiceFactoryDep) => { +export const webhookServiceFactory = ({ + webhookDAL, + projectEnvDAL, + permissionService, + projectDAL +}: TWebhookServiceFactoryDep) => { const createWebhook = async ({ actor, actorId, @@ -124,13 +131,21 @@ export const webhookServiceFactory = ({ webhookDAL, projectEnvDAL, permissionSer actorAuthMethod, actorOrgId ); - ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Webhooks); + const project = await projectDAL.findById(webhook.projectId); + + ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Webhooks); let webhookError: string | undefined; try { await triggerWebhookRequest( webhook, - getWebhookPayload("test", webhook.projectId, webhook.environment.slug, webhook.secretPath, webhook.type) + getWebhookPayload("test", { + workspaceName: project.name, + workspaceId: webhook.projectId, + environment: webhook.environment.slug, + secretPath: webhook.secretPath, + type: webhook.type + }) ); } catch (err) { webhookError = (err as Error).message; diff --git a/cli/go.mod b/cli/go.mod index a2144b674..82f84cebb 100644 --- a/cli/go.mod +++ b/cli/go.mod @@ -10,7 +10,7 @@ require ( github.com/fatih/semgroup v1.2.0 github.com/gitleaks/go-gitdiff v0.8.0 github.com/h2non/filetype v1.1.3 - github.com/infisical/go-sdk v0.2.0 + github.com/infisical/go-sdk v0.3.0 github.com/mattn/go-isatty v0.0.14 github.com/muesli/ansi v0.0.0-20221106050444-61f0cd9a192a github.com/muesli/mango-cobra v1.2.0 @@ -19,7 +19,7 @@ require ( github.com/petar-dambovaliev/aho-corasick v0.0.0-20211021192214-5ab2d9280aa9 github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 github.com/posthog/posthog-go v0.0.0-20221221115252-24dfed35d71a - github.com/rs/cors v1.9.0 + github.com/rs/cors v1.11.0 github.com/rs/zerolog v1.26.1 github.com/spf13/cobra v1.6.1 github.com/spf13/viper v1.8.1 @@ -52,7 +52,7 @@ require ( github.com/chzyer/readline v1.5.1 // indirect github.com/danieljoos/wincred v1.2.0 // indirect github.com/davecgh/go-spew v1.1.1 // indirect - github.com/dvsekhvalnov/jose2go v1.5.0 // indirect + github.com/dvsekhvalnov/jose2go v1.6.0 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect github.com/fsnotify/fsnotify v1.4.9 // indirect github.com/go-logr/logr v1.4.1 // indirect diff --git a/cli/go.sum b/cli/go.sum index 3784ce4d8..e0791eec2 100644 --- a/cli/go.sum +++ b/cli/go.sum @@ -117,8 +117,8 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/denisbrodbeck/machineid v1.0.1 h1:geKr9qtkB876mXguW2X6TU4ZynleN6ezuMSRhl4D7AQ= github.com/denisbrodbeck/machineid v1.0.1/go.mod h1:dJUwb7PTidGDeYyUBmXZ2GphQBbjJCrnectwCyxcUSI= -github.com/dvsekhvalnov/jose2go v1.5.0 h1:3j8ya4Z4kMCwT5nXIKFSV84YS+HdqSSO0VsTQxaLAeM= -github.com/dvsekhvalnov/jose2go v1.5.0/go.mod h1:QsHjhyTlD/lAVqn/NSbVZmSCGeDehTB/mPZadG+mhXU= +github.com/dvsekhvalnov/jose2go v1.6.0 h1:Y9gnSnP4qEI0+/uQkHvFXeD2PLPJeXEL+ySMEA2EjTY= +github.com/dvsekhvalnov/jose2go v1.6.0/go.mod h1:QsHjhyTlD/lAVqn/NSbVZmSCGeDehTB/mPZadG+mhXU= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= @@ -263,8 +263,8 @@ github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1: github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/inconshreveable/mousetrap v1.0.1 h1:U3uMjPSQEBMNp1lFxmllqCPM6P5u/Xq7Pgzkat/bFNc= github.com/inconshreveable/mousetrap v1.0.1/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= -github.com/infisical/go-sdk v0.2.0 h1:n1/KNdYpeQavSqVwC9BfeV8VRzf3N2X9zO1tzQOSj5Q= -github.com/infisical/go-sdk v0.2.0/go.mod h1:vHTDVw3k+wfStXab513TGk1n53kaKF2xgLqpw/xvtl4= +github.com/infisical/go-sdk v0.3.0 h1:Ls71t227F4CWVQWdStcwv8WDyfHe8eRlyAuMRNHsmlQ= +github.com/infisical/go-sdk v0.3.0/go.mod h1:vHTDVw3k+wfStXab513TGk1n53kaKF2xgLqpw/xvtl4= github.com/jedib0t/go-pretty v4.3.0+incompatible h1:CGs8AVhEKg/n9YbUenWmNStRW2PHJzaeDodcfvRAbIo= github.com/jedib0t/go-pretty v4.3.0+incompatible/go.mod h1:XemHduiw8R651AF9Pt4FwCTKeG3oo7hrHJAoznj9nag= github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= @@ -356,8 +356,8 @@ github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= -github.com/rs/cors v1.9.0 h1:l9HGsTsHJcvW14Nk7J9KFz8bzeAWXn3CG6bgt7LsrAE= -github.com/rs/cors v1.9.0/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU= +github.com/rs/cors v1.11.0 h1:0B9GE/r9Bc2UxRMMtymBkHTenPkHDv0CW4Y98GBY+po= +github.com/rs/cors v1.11.0/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU= github.com/rs/xid v1.3.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= github.com/rs/zerolog v1.26.1 h1:/ihwxqH+4z8UxyI70wM1z9yCvkWcfz/a3mj48k/Zngc= github.com/rs/zerolog v1.26.1/go.mod h1:/wSSJWX7lVrsOwlbyTRSOJvqRlc+WjWlfes+CiJ+tmc= diff --git a/cli/packages/cmd/login.go b/cli/packages/cmd/login.go index 0efe7af9e..abf664ed0 100644 --- a/cli/packages/cmd/login.go +++ b/cli/packages/cmd/login.go @@ -122,6 +122,21 @@ func handleAwsIamAuthLogin(cmd *cobra.Command, infisicalClient infisicalSdk.Infi return infisicalClient.Auth().AwsIamAuthLogin(identityId) } +func handleOidcAuthLogin(cmd *cobra.Command, infisicalClient infisicalSdk.InfisicalClientInterface) (credential infisicalSdk.MachineIdentityCredential, e error) { + + identityId, err := util.GetCmdFlagOrEnv(cmd, "machine-identity-id", util.INFISICAL_MACHINE_IDENTITY_ID_NAME) + if err != nil { + return infisicalSdk.MachineIdentityCredential{}, err + } + + jwt, err := util.GetCmdFlagOrEnv(cmd, "oidc-jwt", util.INFISICAL_OIDC_AUTH_JWT_NAME) + if err != nil { + return infisicalSdk.MachineIdentityCredential{}, err + } + + return infisicalClient.Auth().OidcAuthLogin(identityId, jwt) +} + func formatAuthMethod(authMethod string) string { return strings.ReplaceAll(authMethod, "-", " ") } @@ -257,6 +272,7 @@ var loginCmd = &cobra.Command{ util.AuthStrategy.GCP_ID_TOKEN_AUTH: handleGcpIdTokenAuthLogin, util.AuthStrategy.GCP_IAM_AUTH: handleGcpIamAuthLogin, util.AuthStrategy.AWS_IAM_AUTH: handleAwsIamAuthLogin, + util.AuthStrategy.OIDC_AUTH: handleOidcAuthLogin, } credential, err := authStrategies[strategy](cmd, infisicalClient) @@ -456,6 +472,7 @@ func init() { loginCmd.Flags().String("machine-identity-id", "", "machine identity id for kubernetes, azure, gcp-id-token, gcp-iam, and aws-iam auth methods") loginCmd.Flags().String("service-account-token-path", "", "service account token path for kubernetes auth") loginCmd.Flags().String("service-account-key-file-path", "", "service account key file path for GCP IAM auth") + loginCmd.Flags().String("oidc-jwt", "", "JWT for OIDC authentication") } func DomainOverridePrompt() (bool, error) { @@ -616,7 +633,7 @@ func getFreshUserCredentials(email string, password string) (*api.GetLoginOneV2R loginTwoResponseResult, err := api.CallLogin2V2(httpClient, api.GetLoginTwoV2Request{ Email: email, ClientProof: hex.EncodeToString(srpM1), - Password: password, + Password: password, }) if err != nil { diff --git a/cli/packages/util/auth.go b/cli/packages/util/auth.go index d27bbc2c8..cdcd7b50a 100644 --- a/cli/packages/util/auth.go +++ b/cli/packages/util/auth.go @@ -9,6 +9,7 @@ var AuthStrategy = struct { GCP_ID_TOKEN_AUTH AuthStrategyType GCP_IAM_AUTH AuthStrategyType AWS_IAM_AUTH AuthStrategyType + OIDC_AUTH AuthStrategyType }{ UNIVERSAL_AUTH: "universal-auth", KUBERNETES_AUTH: "kubernetes", @@ -16,6 +17,7 @@ var AuthStrategy = struct { GCP_ID_TOKEN_AUTH: "gcp-id-token", GCP_IAM_AUTH: "gcp-iam", AWS_IAM_AUTH: "aws-iam", + OIDC_AUTH: "oidc-auth", } var AVAILABLE_AUTH_STRATEGIES = []AuthStrategyType{ @@ -25,6 +27,7 @@ var AVAILABLE_AUTH_STRATEGIES = []AuthStrategyType{ AuthStrategy.GCP_ID_TOKEN_AUTH, AuthStrategy.GCP_IAM_AUTH, AuthStrategy.AWS_IAM_AUTH, + AuthStrategy.OIDC_AUTH, } func IsAuthMethodValid(authMethod string, allowUserAuth bool) (isValid bool, strategy AuthStrategyType) { diff --git a/cli/packages/util/constants.go b/cli/packages/util/constants.go index a3cc2cfcb..5b0a93513 100644 --- a/cli/packages/util/constants.go +++ b/cli/packages/util/constants.go @@ -19,6 +19,9 @@ const ( // GCP Auth INFISICAL_GCP_IAM_SERVICE_ACCOUNT_KEY_FILE_PATH_NAME = "INFISICAL_GCP_IAM_SERVICE_ACCOUNT_KEY_FILE_PATH" + // OIDC Auth + INFISICAL_OIDC_AUTH_JWT_NAME = "INFISICAL_OIDC_AUTH_JWT" + // Generic env variable used for auth methods that require a machine identity ID INFISICAL_MACHINE_IDENTITY_ID_NAME = "INFISICAL_MACHINE_IDENTITY_ID" diff --git a/company/handbook/hiring.mdx b/company/handbook/hiring.mdx new file mode 100644 index 000000000..af0e775eb --- /dev/null +++ b/company/handbook/hiring.mdx @@ -0,0 +1,50 @@ +--- +title: "Hiring" +sidebarTitle: "Hiring" +description: "The guide to hiring at Infisical." +--- + +Infisical is actively growing and we are hiring for many positions at any given time. This page describes some details of the hiring process we have. + +## Strategy + +Infisical recruitment strategy relies on 100% inbound interest by default. Many of our team members have previously used Infisical or contributed to our [open source project](https://github.com/Infisical/infisical). This allows us to hire the best candidates who are most interested in working at Infisical. + +## Geography + +Infisical is a remote-first company, and we have team members across the whole globe. That being said, there are some legal and accounting limitations that we need to abide by. As a result, we are currently only open to hiring from the following countries: + +- Australia +- Austria +- Belgium +- Brazil +- Canada +- Chile +- Costa Rica +- Denmark +- Finland +- France +- Germany +- India +- Ireland +- Israel +- Italy +- Japan +- Kenya +- Latvia +- Luxembourg +- Mexico +- Netherlands +- New Zealand +- Philippines +- Poland +- Portugal +- Singapore +- South Africa +- South Korea +- Spain +- Switzerland +- Sweden +- UAE +- United Kingdom +- United States diff --git a/company/mint.json b/company/mint.json index ea9c41e21..e6ef851cc 100644 --- a/company/mint.json +++ b/company/mint.json @@ -58,7 +58,8 @@ "pages": [ "handbook/onboarding", "handbook/spending-money", - "handbook/time-off" + "handbook/time-off", + "handbook/hiring" ] } ], diff --git a/docs/api-reference/endpoints/aws-auth/attach.mdx b/docs/api-reference/endpoints/aws-auth/attach.mdx new file mode 100644 index 000000000..85b2d6819 --- /dev/null +++ b/docs/api-reference/endpoints/aws-auth/attach.mdx @@ -0,0 +1,4 @@ +--- +title: "Attach" +openapi: "POST /api/v1/auth/aws-auth/identities/{identityId}" +--- diff --git a/docs/api-reference/endpoints/aws-auth/login.mdx b/docs/api-reference/endpoints/aws-auth/login.mdx new file mode 100644 index 000000000..751903be6 --- /dev/null +++ b/docs/api-reference/endpoints/aws-auth/login.mdx @@ -0,0 +1,4 @@ +--- +title: "Login" +openapi: "POST /api/v1/auth/aws-auth/login" +--- diff --git a/docs/api-reference/endpoints/aws-auth/retrieve.mdx b/docs/api-reference/endpoints/aws-auth/retrieve.mdx new file mode 100644 index 000000000..4b1980dd2 --- /dev/null +++ b/docs/api-reference/endpoints/aws-auth/retrieve.mdx @@ -0,0 +1,4 @@ +--- +title: "Retrieve" +openapi: "GET /api/v1/auth/aws-auth/identities/{identityId}" +--- diff --git a/docs/api-reference/endpoints/aws-auth/revoke.mdx b/docs/api-reference/endpoints/aws-auth/revoke.mdx new file mode 100644 index 000000000..4d19fa95f --- /dev/null +++ b/docs/api-reference/endpoints/aws-auth/revoke.mdx @@ -0,0 +1,4 @@ +--- +title: "Revoke" +openapi: "DELETE /api/v1/auth/aws-auth/identities/{identityId}" +--- diff --git a/docs/api-reference/endpoints/aws-auth/update.mdx b/docs/api-reference/endpoints/aws-auth/update.mdx new file mode 100644 index 000000000..949d2a901 --- /dev/null +++ b/docs/api-reference/endpoints/aws-auth/update.mdx @@ -0,0 +1,4 @@ +--- +title: "Update" +openapi: "PATCH /api/v1/auth/aws-auth/identities/{identityId}" +--- diff --git a/docs/api-reference/endpoints/azure-auth/attach.mdx b/docs/api-reference/endpoints/azure-auth/attach.mdx new file mode 100644 index 000000000..cafaf6df1 --- /dev/null +++ b/docs/api-reference/endpoints/azure-auth/attach.mdx @@ -0,0 +1,4 @@ +--- +title: "Attach" +openapi: "POST /api/v1/auth/azure-auth/identities/{identityId}" +--- diff --git a/docs/api-reference/endpoints/azure-auth/login.mdx b/docs/api-reference/endpoints/azure-auth/login.mdx new file mode 100644 index 000000000..c9e51d51b --- /dev/null +++ b/docs/api-reference/endpoints/azure-auth/login.mdx @@ -0,0 +1,4 @@ +--- +title: "Login" +openapi: "POST /api/v1/auth/azure-auth/login" +--- diff --git a/docs/api-reference/endpoints/azure-auth/retrieve.mdx b/docs/api-reference/endpoints/azure-auth/retrieve.mdx new file mode 100644 index 000000000..b07ee2ad4 --- /dev/null +++ b/docs/api-reference/endpoints/azure-auth/retrieve.mdx @@ -0,0 +1,4 @@ +--- +title: "Retrieve" +openapi: "GET /api/v1/auth/azure-auth/identities/{identityId}" +--- diff --git a/docs/api-reference/endpoints/azure-auth/revoke.mdx b/docs/api-reference/endpoints/azure-auth/revoke.mdx new file mode 100644 index 000000000..7254bdd8b --- /dev/null +++ b/docs/api-reference/endpoints/azure-auth/revoke.mdx @@ -0,0 +1,4 @@ +--- +title: "Revoke" +openapi: "DELETE /api/v1/auth/azure-auth/identities/{identityId}" +--- diff --git a/docs/api-reference/endpoints/azure-auth/update.mdx b/docs/api-reference/endpoints/azure-auth/update.mdx new file mode 100644 index 000000000..ee830a1f3 --- /dev/null +++ b/docs/api-reference/endpoints/azure-auth/update.mdx @@ -0,0 +1,4 @@ +--- +title: "Update" +openapi: "PATCH /api/v1/auth/azure-auth/identities/{identityId}" +--- diff --git a/docs/api-reference/endpoints/folders/get-by-id.mdx b/docs/api-reference/endpoints/folders/get-by-id.mdx new file mode 100644 index 000000000..db3c4d0cc --- /dev/null +++ b/docs/api-reference/endpoints/folders/get-by-id.mdx @@ -0,0 +1,4 @@ +--- +title: "Get by ID" +openapi: "GET /api/v1/folders/{id}" +--- diff --git a/docs/api-reference/endpoints/gcp-auth/attach.mdx b/docs/api-reference/endpoints/gcp-auth/attach.mdx new file mode 100644 index 000000000..04bc754f5 --- /dev/null +++ b/docs/api-reference/endpoints/gcp-auth/attach.mdx @@ -0,0 +1,4 @@ +--- +title: "Attach" +openapi: "POST /api/v1/auth/gcp-auth/identities/{identityId}" +--- diff --git a/docs/api-reference/endpoints/gcp-auth/login.mdx b/docs/api-reference/endpoints/gcp-auth/login.mdx new file mode 100644 index 000000000..ff5caf2ac --- /dev/null +++ b/docs/api-reference/endpoints/gcp-auth/login.mdx @@ -0,0 +1,4 @@ +--- +title: "Login" +openapi: "POST /api/v1/auth/gcp-auth/login" +--- diff --git a/docs/api-reference/endpoints/gcp-auth/retrieve.mdx b/docs/api-reference/endpoints/gcp-auth/retrieve.mdx new file mode 100644 index 000000000..48124f2b9 --- /dev/null +++ b/docs/api-reference/endpoints/gcp-auth/retrieve.mdx @@ -0,0 +1,4 @@ +--- +title: "Retrieve" +openapi: "GET /api/v1/auth/gcp-auth/identities/{identityId}" +--- diff --git a/docs/api-reference/endpoints/gcp-auth/revoke.mdx b/docs/api-reference/endpoints/gcp-auth/revoke.mdx new file mode 100644 index 000000000..1e1b3f1a4 --- /dev/null +++ b/docs/api-reference/endpoints/gcp-auth/revoke.mdx @@ -0,0 +1,4 @@ +--- +title: "Revoke" +openapi: "DELETE /api/v1/auth/gcp-auth/identities/{identityId}" +--- diff --git a/docs/api-reference/endpoints/gcp-auth/update.mdx b/docs/api-reference/endpoints/gcp-auth/update.mdx new file mode 100644 index 000000000..ee1e269d3 --- /dev/null +++ b/docs/api-reference/endpoints/gcp-auth/update.mdx @@ -0,0 +1,4 @@ +--- +title: "Update" +openapi: "PATCH /api/v1/auth/gcp-auth/identities/{identityId}" +--- diff --git a/docs/api-reference/endpoints/kubernetes-auth/attach.mdx b/docs/api-reference/endpoints/kubernetes-auth/attach.mdx new file mode 100644 index 000000000..969bbd8a1 --- /dev/null +++ b/docs/api-reference/endpoints/kubernetes-auth/attach.mdx @@ -0,0 +1,4 @@ +--- +title: "Attach" +openapi: "POST /api/v1/auth/kubernetes-auth/identities/{identityId}" +--- diff --git a/docs/api-reference/endpoints/kubernetes-auth/login.mdx b/docs/api-reference/endpoints/kubernetes-auth/login.mdx new file mode 100644 index 000000000..0b6539be5 --- /dev/null +++ b/docs/api-reference/endpoints/kubernetes-auth/login.mdx @@ -0,0 +1,4 @@ +--- +title: "Login" +openapi: "POST /api/v1/auth/kubernetes-auth/login" +--- diff --git a/docs/api-reference/endpoints/kubernetes-auth/retrieve.mdx b/docs/api-reference/endpoints/kubernetes-auth/retrieve.mdx new file mode 100644 index 000000000..ca7b30d10 --- /dev/null +++ b/docs/api-reference/endpoints/kubernetes-auth/retrieve.mdx @@ -0,0 +1,4 @@ +--- +title: "Retrieve" +openapi: "GET /api/v1/auth/kubernetes-auth/identities/{identityId}" +--- diff --git a/docs/api-reference/endpoints/kubernetes-auth/revoke.mdx b/docs/api-reference/endpoints/kubernetes-auth/revoke.mdx new file mode 100644 index 000000000..94c11cf73 --- /dev/null +++ b/docs/api-reference/endpoints/kubernetes-auth/revoke.mdx @@ -0,0 +1,4 @@ +--- +title: "Revoke" +openapi: "DELETE /api/v1/auth/kubernetes-auth/identities/{identityId}" +--- diff --git a/docs/api-reference/endpoints/kubernetes-auth/update.mdx b/docs/api-reference/endpoints/kubernetes-auth/update.mdx new file mode 100644 index 000000000..8eb173ac8 --- /dev/null +++ b/docs/api-reference/endpoints/kubernetes-auth/update.mdx @@ -0,0 +1,4 @@ +--- +title: "Update" +openapi: "PATCH /api/v1/auth/kubernetes-auth/identities/{identityId}" +--- diff --git a/docs/api-reference/endpoints/oidc-auth/attach.mdx b/docs/api-reference/endpoints/oidc-auth/attach.mdx new file mode 100644 index 000000000..c75bdc69f --- /dev/null +++ b/docs/api-reference/endpoints/oidc-auth/attach.mdx @@ -0,0 +1,4 @@ +--- +title: "Attach" +openapi: "POST /api/v1/auth/oidc-auth/identities/{identityId}" +--- diff --git a/docs/api-reference/endpoints/oidc-auth/login.mdx b/docs/api-reference/endpoints/oidc-auth/login.mdx new file mode 100644 index 000000000..baac1bef9 --- /dev/null +++ b/docs/api-reference/endpoints/oidc-auth/login.mdx @@ -0,0 +1,4 @@ +--- +title: "Login" +openapi: "POST /api/v1/auth/oidc-auth/login" +--- diff --git a/docs/api-reference/endpoints/oidc-auth/retrieve.mdx b/docs/api-reference/endpoints/oidc-auth/retrieve.mdx new file mode 100644 index 000000000..a870ffc03 --- /dev/null +++ b/docs/api-reference/endpoints/oidc-auth/retrieve.mdx @@ -0,0 +1,4 @@ +--- +title: "Retrieve" +openapi: "GET /api/v1/auth/oidc-auth/identities/{identityId}" +--- diff --git a/docs/api-reference/endpoints/oidc-auth/revoke.mdx b/docs/api-reference/endpoints/oidc-auth/revoke.mdx new file mode 100644 index 000000000..df46d6c45 --- /dev/null +++ b/docs/api-reference/endpoints/oidc-auth/revoke.mdx @@ -0,0 +1,4 @@ +--- +title: "Revoke" +openapi: "DELETE /api/v1/auth/oidc-auth/identities/{identityId}" +--- diff --git a/docs/api-reference/endpoints/oidc-auth/update.mdx b/docs/api-reference/endpoints/oidc-auth/update.mdx new file mode 100644 index 000000000..0d29c3db7 --- /dev/null +++ b/docs/api-reference/endpoints/oidc-auth/update.mdx @@ -0,0 +1,4 @@ +--- +title: "Update" +openapi: "PATCH /api/v1/auth/oidc-auth/identities/{identityId}" +--- diff --git a/docs/api-reference/endpoints/token-auth/attach.mdx b/docs/api-reference/endpoints/token-auth/attach.mdx new file mode 100644 index 000000000..a53855db3 --- /dev/null +++ b/docs/api-reference/endpoints/token-auth/attach.mdx @@ -0,0 +1,4 @@ +--- +title: "Attach" +openapi: "POST /api/v1/auth/token-auth/identities/{identityId}" +--- diff --git a/docs/api-reference/endpoints/token-auth/create-token.mdx b/docs/api-reference/endpoints/token-auth/create-token.mdx new file mode 100644 index 000000000..e147a4e12 --- /dev/null +++ b/docs/api-reference/endpoints/token-auth/create-token.mdx @@ -0,0 +1,4 @@ +--- +title: "Create Token" +openapi: "POST /api/v1/auth/token-auth/identities/{identityId}/tokens" +--- diff --git a/docs/api-reference/endpoints/token-auth/get-tokens.mdx b/docs/api-reference/endpoints/token-auth/get-tokens.mdx new file mode 100644 index 000000000..5f690a298 --- /dev/null +++ b/docs/api-reference/endpoints/token-auth/get-tokens.mdx @@ -0,0 +1,4 @@ +--- +title: "Get Tokens" +openapi: "GET /api/v1/auth/token-auth/identities/{identityId}/tokens" +--- diff --git a/docs/api-reference/endpoints/token-auth/retrieve.mdx b/docs/api-reference/endpoints/token-auth/retrieve.mdx new file mode 100644 index 000000000..ed6bb762c --- /dev/null +++ b/docs/api-reference/endpoints/token-auth/retrieve.mdx @@ -0,0 +1,4 @@ +--- +title: "Retrieve" +openapi: "GET /api/v1/auth/token-auth/identities/{identityId}" +--- diff --git a/docs/api-reference/endpoints/token-auth/revoke-token.mdx b/docs/api-reference/endpoints/token-auth/revoke-token.mdx new file mode 100644 index 000000000..ca6a0f056 --- /dev/null +++ b/docs/api-reference/endpoints/token-auth/revoke-token.mdx @@ -0,0 +1,4 @@ +--- +title: "Revoke Token" +openapi: "POST /api/v1/auth/token-auth/tokens/{tokenId}/revoke" +--- diff --git a/docs/api-reference/endpoints/token-auth/revoke.mdx b/docs/api-reference/endpoints/token-auth/revoke.mdx new file mode 100644 index 000000000..52e295fd0 --- /dev/null +++ b/docs/api-reference/endpoints/token-auth/revoke.mdx @@ -0,0 +1,4 @@ +--- +title: "Revoke" +openapi: "DELETE /api/v1/auth/token-auth/identities/{identityId}" +--- diff --git a/docs/api-reference/endpoints/token-auth/update-token.mdx b/docs/api-reference/endpoints/token-auth/update-token.mdx new file mode 100644 index 000000000..cf5e0f568 --- /dev/null +++ b/docs/api-reference/endpoints/token-auth/update-token.mdx @@ -0,0 +1,4 @@ +--- +title: "Update Token" +openapi: "PATCH /api/v1/auth/token-auth/tokens/{tokenId}" +--- diff --git a/docs/api-reference/endpoints/token-auth/update.mdx b/docs/api-reference/endpoints/token-auth/update.mdx new file mode 100644 index 000000000..c9e332e8f --- /dev/null +++ b/docs/api-reference/endpoints/token-auth/update.mdx @@ -0,0 +1,4 @@ +--- +title: "Update" +openapi: "PATCH /api/v1/auth/token-auth/identities/{identityId}" +--- diff --git a/docs/cli/commands/login.mdx b/docs/cli/commands/login.mdx index d97cb4c2b..721f905dd 100644 --- a/docs/cli/commands/login.mdx +++ b/docs/cli/commands/login.mdx @@ -8,7 +8,8 @@ infisical login ``` ### Description -The CLI uses authentication to verify your identity. When you enter the correct email and password for your account, a token is generated and saved in your system Keyring to allow you to make future interactions with the CLI. + +The CLI uses authentication to verify your identity. When you enter the correct email and password for your account, a token is generated and saved in your system Keyring to allow you to make future interactions with the CLI. To change where the login credentials are stored, visit the [vaults command](./vault). @@ -17,12 +18,12 @@ If you have added multiple users, you can switch between the users by using the When you authenticate with **any other method than `user`**, an access token will be printed to the console upon successful login. This token can be used to authenticate with the Infisical API and the CLI by passing it in the `--token` flag when applicable. - Use flag `--plain` along with `--silent` to print only the token in plain text when using a machine identity auth method. - + Use flag `--plain` along with `--silent` to print only the token in plain text when using a machine identity auth method. + - ### Flags + The login command supports a number of flags that you can use for different authentication methods. Below is a list of all the flags that can be used with the login command. @@ -52,6 +53,7 @@ The login command supports a number of flags that you can use for different auth The `client-id` flag can be substituted with the `INFISICAL_UNIVERSAL_AUTH_CLIENT_ID` environment variable. + ```bash @@ -63,6 +65,7 @@ The login command supports a number of flags that you can use for different auth The `client-secret` flag can be substituted with the `INFISICAL_UNIVERSAL_AUTH_CLIENT_SECRET` environment variable. + ```bash @@ -75,6 +78,7 @@ The login command supports a number of flags that you can use for different auth The `machine-identity-id` flag can be substituted with the `INFISICAL_MACHINE_IDENTITY_ID` environment variable. + ```bash @@ -88,6 +92,7 @@ The login command supports a number of flags that you can use for different auth The `service-account-token-path` flag can be substituted with the `INFISICAL_KUBERNETES_SERVICE_ACCOUNT_TOKEN_PATH` environment variable. + ```bash @@ -100,9 +105,23 @@ The login command supports a number of flags that you can use for different auth The `service-account-key-path` flag can be substituted with the `INFISICAL_GCP_IAM_SERVICE_ACCOUNT_KEY_FILE_PATH` environment variable. + + + ```bash + infisical login --oidc-jwt= + ``` + + #### Description + The JWT provided by an identity provider for OIDC authentication. + + + The `oidc-jwt` flag can be substituted with the `INFISICAL_OIDC_AUTH_JWT` environment variable. + + + ### Authentication Methods @@ -121,6 +140,7 @@ The Infisical CLI supports multiple authentication methods. Below are the availa Your machine identity client secret. + @@ -134,6 +154,7 @@ The Infisical CLI supports multiple authentication methods. Below are the availa infisical login --method=universal-auth --client-id= --client-secret= ``` + @@ -148,6 +169,7 @@ The Infisical CLI supports multiple authentication methods. Below are the availa Path to the Kubernetes service account token to use. Default: `/var/run/secrets/kubernetes.io/serviceaccount/token`. + @@ -162,6 +184,7 @@ The Infisical CLI supports multiple authentication methods. Below are the availa infisical login --method=kubernetes --machine-identity-id= --service-account-token-path= ``` + @@ -213,6 +236,7 @@ The Infisical CLI supports multiple authentication methods. Below are the availa ``` + The GCP IAM method is used to authenticate with Infisical with a GCP service account key. @@ -235,11 +259,12 @@ The Infisical CLI supports multiple authentication methods. Below are the availa Run the `login` command with the following flags to obtain an access token: - ```bash + ```bash infisical login --method=gcp-iam --machine-identity-id= --service-account-key-file-path= ``` + The AWS IAM method is used to authenticate with Infisical with an AWS IAM role while running in an AWS environment like EC2, Lambda, etc. @@ -264,10 +289,40 @@ The Infisical CLI supports multiple authentication methods. Below are the availa ``` + + + + The OIDC Auth method is used to authenticate with Infisical via identity tokens with OIDC. + + + + + Your machine identity ID. + + + The OIDC JWT from the identity provider. + + + + + + + To create an OIDC machine identity, follow the step by step guide outlined [here](/documentation/platform/identities/oidc-auth/general). + + + Run the `login` command with the following flags to obtain an access token: + + ```bash + infisical login --method=oidc-auth --machine-identity-id= --oidc-jwt= + ``` + + + ### Machine Identity Authentication Quick Start + In this example we'll be using the `universal-auth` method to login to obtain an Infisical access token, which we will then use to fetch secrets with. @@ -277,8 +332,8 @@ In this example we'll be using the `universal-auth` method to login to obtain an ``` Now that we've set the `INFISICAL_TOKEN` environment variable, we can use the CLI to interact with Infisical. The CLI will automatically check for the presence of the `INFISICAL_TOKEN` environment variable and use it for authentication. - - + + Alternatively, if you would rather use the `--token` flag to pass the token directly, you can do so by running the following command: ```bash @@ -297,6 +352,7 @@ In this example we'll be using the `universal-auth` method to login to obtain an The `--recursive`, and `--env` flag is optional and will fetch all secrets in subfolders. The default environment is `dev` if no `--env` flag is provided. + And that's it! Now you're ready to start using the Infisical CLI to interact with your secrets, with the use of Machine Identities. diff --git a/docs/documentation/platform/access-controls/access-requests.mdx b/docs/documentation/platform/access-controls/access-requests.mdx index 45c155ab4..76cc4b74e 100644 --- a/docs/documentation/platform/access-controls/access-requests.mdx +++ b/docs/documentation/platform/access-controls/access-requests.mdx @@ -6,7 +6,7 @@ description: "Learn how to request access to sensitive resources in Infisical." In certain situations, developers need to expand their access to a certain new project or a sensitive environment. For those use cases, it is helpful to utilize Infisical's **Access Requests** functionality. This functionality works in the following way: -1. A project administrator sets up a policy that assigns access managers (also known as eligible approvers) to a certain sensitive folder or environment. +1. A project administrator sets up an access policy that assigns access managers (also known as eligible approvers) to a certain sensitive folder or environment. ![Create Access Request Policy Modal](/images/platform/access-controls/create-access-request-policy.png) ![Access Request Policies](/images/platform/access-controls/access-request-policies.png) @@ -14,9 +14,14 @@ This functionality works in the following way: ![Access Request Create](/images/platform/access-controls/request-access.png) ![Access Request Dashboard](/images/platform/access-controls/access-requests-pending.png) -3. An eligible approver can approve or reject the access request. -![Access Request Review](/images/platform/access-controls/review-access-request.png) +4. An eligible approver can approve or reject the access request. +{/* ![Access Request Review](/images/platform/access-controls/review-access-request.png) */} +![Access Request Bypass](/images/platform/access-controls/access-request-bypass.png) -4. As soon as the request is approved, developer is able to access the sought resources. + + If the access request matches with a policy that has a **Soft** enforcement level, the requester may bypass the policy and get access to the resource without full approval. + + +5. As soon as the request is approved, developer is able to access the sought resources. ![Access Request Dashboard](/images/platform/access-controls/access-requests-completed.png) diff --git a/docs/documentation/platform/identities/machine-identities.mdx b/docs/documentation/platform/identities/machine-identities.mdx index f92439c86..98d654c22 100644 --- a/docs/documentation/platform/identities/machine-identities.mdx +++ b/docs/documentation/platform/identities/machine-identities.mdx @@ -36,6 +36,7 @@ To interact with various resources in Infisical, Machine Identities can authenti - [AWS Auth](/documentation/platform/identities/aws-auth): An AWS-native authentication method for AWS services (e.g. EC2, Lambda functions, etc.). - [Azure Auth](/documentation/platform/identities/azure-auth): An Azure-native authentication method for Azure resources (e.g. Azure VMs, Azure App Services, Azure Functions, Azure Kubernetes Service, etc.). - [GCP Auth](/documentation/platform/identities/gcp-auth): A GCP-native authentication method for GCP resources (e.g. Compute Engine, App Engine, Cloud Run, Google Kubernetes Engine, IAM service accounts, etc.). +- [OIDC Auth](/documentation/platform/identities/oidc-auth): A platform-agnostic, JWT-based authentication method for workloads using an OpenID Connect identity provider. ## FAQ diff --git a/docs/documentation/platform/identities/oidc-auth/general.mdx b/docs/documentation/platform/identities/oidc-auth/general.mdx new file mode 100644 index 000000000..ac9b4f2d7 --- /dev/null +++ b/docs/documentation/platform/identities/oidc-auth/general.mdx @@ -0,0 +1,165 @@ +--- +title: General +description: "Learn how to authenticate with Infisical from any platform or environment using OpenID Connect (OIDC)." +--- + +**OIDC Auth** is a platform-agnostic JWT-based authentication method that can be used to authenticate from any platform or environment using an identity provider with OpenID Connect. + +## Diagram + +The following sequence diagram illustrates the OIDC Auth workflow for authenticating clients with Infisical. + +```mermaid +sequenceDiagram + participant Client as Client + participant Idp as Identity Provider + participant Infis as Infisical + + Client->>Idp: Step 1: Request identity token + Idp-->>Client: Return JWT with verifiable claims + + Note over Client,Infis: Step 2: Login Operation + Client->>Infis: Send signed JWT to /api/v1/auth/oidc-auth/login + + Note over Infis,Idp: Step 3: Query verification + Infis->>Idp: Request JWT public key using OIDC Discovery + Idp-->>Infis: Return public key + + Note over Infis: Step 4: JWT validation + Infis->>Client: Return short-lived access token + + Note over Client,Infis: Step 5: Access Infisical API with Token + Client->>Infis: Make authenticated requests using the short-lived access token +``` + +## Concept + +At a high-level, Infisical authenticates a client by verifying the JWT and checking that it meets specific requirements (e.g. it is issued by a trusted identity provider) at the `/api/v1/auth/oidc-auth/login` endpoint. If successful, +then Infisical returns a short-lived access token that can be used to make authenticated requests to the Infisical API. + +To be more specific: + +1. The client requests an identity token from its identity provider. +2. The client sends the identity token to Infisical at the `/api/v1/auth/oidc-auth/login` endpoint. +3. Infisical fetches the public key that was used to sign the identity token from the identity provider using OIDC Discovery. +4. Infisical validates the JWT using the public key provided by the identity provider and checks that the subject, audience, and claims of the token matches with the set criteria. +5. If all is well, Infisical returns a short-lived access token that the client can use to make authenticated requests to the Infisical API. + + + Infisical needs network-level access to the identity provider configuration + endpoints. + + +## Guide + +In the following steps, we explore how to create and use identities to access the Infisical API using the OIDC Auth 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 redirected to a page where you can manage the identity. + + ![identities page](/images/platform/identities/identities-page.png) + + Since the identity has been configured with Universal Auth by default, you should re-configure it to use OIDC Auth instead. To do this, press to edit the **Authentication** section, + remove the existing Universal Auth configuration, and add a new OIDC Auth configuration onto the identity. + + ![identities page remove default auth](/images/platform/identities/identities-page-remove-default-auth.png) + + ![identities create oidc auth method](/images/platform/identities/identities-org-create-oidc-auth-method.png) + + Restrict access by configuring the Subject, Audiences, and Claims fields + + Here's some more guidance on each field: + - OIDC Discovery URL: The URL used to retrieve the OpenID Connect configuration from the identity provider. This will be used to fetch the public key needed for verifying the provided JWT. + - Issuer: The unique identifier of the identity provider issuing the JWT. This value is used to verify the iss (issuer) claim in the JWT to ensure the token is issued by a trusted provider. + - CA Certificate: The PEM-encoded CA cert for establishing secure communication with the Identity Provider endpoints. + - Subject: The expected principal that is the subject of the JWT. The `sub` (subject) claim in the JWT should match this value. + - Audiences: A list of intended recipients. This value is checked against the aud (audience) claim in the token. The token's aud claim should match at least one of the audiences for it to be valid. + - Claims: Additional information or attributes that should be present in the JWT for it to be valid. + - 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 fetch an identity token from an identity provider and make a request to the `/api/v1/auth/oidc-auth/login` endpoint in exchange for an access token. + + We provide an example below of how authentication is done with Infisical using OIDC. It is a snippet from the [official Github secrets action](https://github.com/Infisical/secrets-action). + + #### Sample usage + ```javascript + export const oidcLogin = async ({ identityId, domain, oidcAudience }) => { + const idToken = await core.getIDToken(oidcAudience); + + const loginData = querystring.stringify({ + identityId, + jwt: idToken, + }); + + try { + const response = await axios({ + method: "post", + url: `${domain}/api/v1/auth/oidc-auth/login`, + headers: { + "Content-Type": "application/x-www-form-urlencoded", + }, + data: loginData, + }); + + return response.data.accessToken; + } catch (err) { + core.error("Error:", err.message); + throw err; + } + }; + ``` + + #### Sample OIDC login response + + ```bash Response + { + "accessToken": "...", + "expiresIn": 7200, + "accessTokenMaxTTL": 43244 + "tokenType": "Bearer" + } + ``` + + + We recommend using one of Infisical's clients like SDKs or the Infisical Agent to authenticate with Infisical using OIDC Auth as they handle the authentication process including the fetching of identity tokens 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/oidc-auth/github.mdx b/docs/documentation/platform/identities/oidc-auth/github.mdx new file mode 100644 index 000000000..4a9e2c671 --- /dev/null +++ b/docs/documentation/platform/identities/oidc-auth/github.mdx @@ -0,0 +1,170 @@ +--- +title: Github +description: "Learn how to authenticate Github workflows with Infisical using OpenID Connect (OIDC)." +--- + +**OIDC Auth** is a platform-agnostic JWT-based authentication method that can be used to authenticate from any platform or environment using an identity provider with OpenID Connect. + +## Diagram + +The following sequence diagram illustrates the OIDC Auth workflow for authenticating Github workflows with Infisical. + +```mermaid +sequenceDiagram + participant Client as Github Workflow + participant Idp as Identity Provider + participant Infis as Infisical + + Client->>Idp: Step 1: Request identity token + Idp-->>Client: Return JWT with verifiable claims + + Note over Client,Infis: Step 2: Login Operation + Client->>Infis: Send signed JWT to /api/v1/auth/oidc-auth/login + + Note over Infis,Idp: Step 3: Query verification + Infis->>Idp: Request JWT public key using OIDC Discovery + Idp-->>Infis: Return public key + + Note over Infis: Step 4: JWT validation + Infis->>Client: Return short-lived access token + + Note over Client,Infis: Step 5: Access Infisical API with Token + Client->>Infis: Make authenticated requests using the short-lived access token +``` + +## Concept + +At a high-level, Infisical authenticates a client by verifying the JWT and checking that it meets specific requirements (e.g. it is issued by a trusted identity provider) at the `/api/v1/auth/oidc-auth/login` endpoint. If successful, +then Infisical returns a short-lived access token that can be used to make authenticated requests to the Infisical API. + +To be more specific: + +1. The Github workflow requests an identity token from Github's identity provider. +2. The fetched identity token is sent to Infisical at the `/api/v1/auth/oidc-auth/login` endpoint. +3. Infisical fetches the public key that was used to sign the identity token from Github's identity provider using OIDC Discovery. +4. Infisical validates the JWT using the public key provided by the identity provider and checks that the subject, audience, and claims of the token matches with the set criteria. +5. If all is well, Infisical returns a short-lived access token that the Github workflow can use to make authenticated requests to the Infisical API. + + + Infisical needs network-level access to Github's identity provider endpoints. + + +## Guide + +In the following steps, we explore how to create and use identities to access the Infisical API using the OIDC Auth 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 redirected to a page where you can manage the identity. + + ![identities page](/images/platform/identities/identities-page.png) + + Since the identity has been configured with Universal Auth by default, you should re-configure it to use OIDC Auth instead. To do this, press to edit the **Authentication** section, + remove the existing Universal Auth configuration, and add a new OIDC Auth configuration onto the identity. + + ![identities page remove default auth](/images/platform/identities/identities-page-remove-default-auth.png) + + ![identities create oidc auth method](/images/platform/identities/identities-org-create-oidc-auth-method.png) + + Restrict access by configuring the Subject, Audiences, and Claims fields + + Here's some more guidance on each field: + - OIDC Discovery URL: The URL used to retrieve the OpenID Connect configuration from the identity provider. This will be used to fetch the public key needed for verifying the provided JWT. This should be set to `https://token.actions.githubusercontent.com` + - Issuer: The unique identifier of the identity provider issuing the JWT. This value is used to verify the iss (issuer) claim in the JWT to ensure the token is issued by a trusted provider. This should be set to `https://token.actions.githubusercontent.com` + - CA Certificate: The PEM-encoded CA cert for establishing secure communication with the Identity Provider endpoints. For Github workflows, this can be left as blank. + - Subject: The expected principal that is the subject of the JWT. The format of the sub field for GitHub workflow OIDC tokens is as follows: `"repo:/:"`. The environment can be where the GitHub workflow is running, such as `environment`, `ref`, or `job_workflow_ref`. For example, if you have a repository owned by octocat named example-repo, and the GitHub workflow is running on the main branch, the subject field might look like this: `repo:octocat/example-repo:ref:refs/heads/main` + - Audiences: A list of intended recipients. This value is checked against the aud (audience) claim in the token. By default, set this to the URL of the repository owner, such as the organization that owns the repository (e.g. `https://github.com/octo-org`). + - Claims: Additional information or attributes that should be present in the JWT for it to be valid. You can refer to Github's [documentation](https://docs.github.com/en/actions/deployment/security-hardening-your-deployments/about-security-hardening-with-openid-connect#understanding-the-oidc-token) for the complete list of supported claims. + - 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. + + If you are unsure about what to configure for the subject, audience, and claims fields you can use [github/actions-oidc-debugger](https://github.com/github/actions-oidc-debugger) to get the appropriate values. Alternatively, you can fetch the JWT from the workflow and inspect the fields manually. + + + 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) + + + As a prerequisite, you will need to set `id-token:write` permissions for the Github workflow. This setting allows the JWT to be requested from Github's OIDC provider. + + ```yaml + permissions: + id-token: write # This is required for requesting the JWT + ... + ``` + + To access the Infisical API as the identity, you need to fetch an identity token from Github's identity provider and make a request to the `/api/v1/auth/oidc-auth/login` endpoint in exchange for an access token. + The identity token can be fetched using either of the following approaches: + - Using environment variables on the runner (`ACTIONS_ID_TOKEN_REQUEST_URL` and `ACTIONS_ID_TOKEN_REQUEST_TOKEN`). + ```yaml + steps: + - name: Request OIDC Token + run: | + echo "Requesting OIDC token..." + TOKEN=$(curl -s -H "Authorization: Bearer $ACTIONS_ID_TOKEN_REQUEST_TOKEN" "$ACTIONS_ID_TOKEN_REQUEST_URL" | jq -r '.value') + echo "TOKEN=$TOKEN" >> $GITHUB_ENV + ``` + + - Using `getIDToken()` from the Github Actions toolkit. + + Below is an example of how a Github workflow can be configured to fetch secrets from Infisical using the [Infisical Secrets Action](https://github.com/Infisical/secrets-action) with OIDC Auth. + ```yaml + name: Manual workflow + + on: + workflow_dispatch: + + permissions: + id-token: write # This is required for requesting the JWT + + jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: Infisical/secrets-action@v1.0.7 + with: + method: "oidc" + env-slug: "dev" + project-slug: "ggggg-9-des" + identity-id: "6b579c00-5c85-4b44-aabe-f8a + ... + ``` + + Preceding steps can then use the secret values injected onto the workflow's environment. + + + We recommend using [Infisical Secrets Action](https://github.com/Infisical/secrets-action) to authenticate with Infisical using OIDC Auth as it handles the authentication process including the fetching of identity tokens 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/pr-workflows.mdx b/docs/documentation/platform/pr-workflows.mdx index 9df123612..d582c838b 100644 --- a/docs/documentation/platform/pr-workflows.mdx +++ b/docs/documentation/platform/pr-workflows.mdx @@ -18,16 +18,26 @@ In a similar way, to solve the above-mentioned issues, Infisical provides a feat ### Setting a policy -First, you would need to create a set of policies for a certain environment. In the example below, a generic policy for a production environment is shown. In this case, any user who submits a change to `prod` would first have to get an approval by a predefined approver (or multiple approvers). +First, you would need to create a set of policies for a certain environment. In the example below, a generic change policy for a production environment is shown. In this case, any user who submits a change to `prod` would first have to get an approval by a predefined approver (or multiple approvers). ![create secret update policy](../../images/platform/pr-workflows/secret-update-policy.png) +### Policy enforcement levels + +The enforcement level determines how strict the policy is. A **Hard** enforcement level means that any change that matches the policy will need full approval prior merging. A **Soft** enforcement level allows for break glass functionality on the request. If a change request is bypassed, the approvers will be notified via email. + +### Example of creating a change policy + +When creating a policy, you can choose the type of policy you want to create. In this case, we will be creating a `Change Policy`. Other types of policies include `Access Policy` that creates policies for **[Access Requests](/documentation/platform/access-controls/access-requests)**. + +![create panel secret update policy](../../images/platform/pr-workflows/create-change-policy.png) + ### Example of updating secrets with Approval workflows When a user submits a change to an enviropnment that is under a particular policy, a corresponsing change request will go to a predefined approver (or multiple approvers). ![secret update change requests](../../images/platform/pr-workflows/secret-update-request.png) -An approver is notified by email and/or Slack as soon as the request is initiated. In the Infisical Dashboard, they will be able to `approve` and `merge` (or `deny`) a request for a change in a particular environment. After that, depending on the workflows setup, the change will be automatically propagated to the right applications (e.g., using [Infisical Kubernetes Operator](https://infisical.com/docs/integrations/platforms/kubernetes)). +Approvers are notified by email and/or Slack as soon as the request is initiated. In the Infisical Dashboard, they will be able to `approve` and `merge` (or `deny`) a request for a change in a particular environment. After that, depending on the workflows setup, the change will be automatically propagated to the right applications (e.g., using [Infisical Kubernetes Operator](https://infisical.com/docs/integrations/platforms/kubernetes)). ![secrets update pull request](../../images/platform/pr-workflows/secret-update-pr.png) \ No newline at end of file diff --git a/docs/documentation/platform/secret-sharing.mdx b/docs/documentation/platform/secret-sharing.mdx index 9c0821508..4ff3a326b 100644 --- a/docs/documentation/platform/secret-sharing.mdx +++ b/docs/documentation/platform/secret-sharing.mdx @@ -21,7 +21,8 @@ With its zero-knowledge architecture, secrets shared via Infisical remain unread zero knowledge architecture. -3. Click on the **Share Secret** button. Set the secret, its expiration time as well as the number of views allowed. It expires as soon as any of the conditions are met. +3. Click on the **Share Secret** button. Set the secret, its expiration time and specify if the secret can be viewed only once. It expires as soon as any of the conditions are met. +Also, specify if the secret can be accessed by anyone or only people within your organization. ![Add View-Bound Sharing Secret](../../images/platform/secret-sharing/create-new-secret.png) diff --git a/docs/images/platform/access-controls/access-request-bypass.png b/docs/images/platform/access-controls/access-request-bypass.png new file mode 100644 index 000000000..248150574 Binary files /dev/null and b/docs/images/platform/access-controls/access-request-bypass.png differ diff --git a/docs/images/platform/access-controls/access-request-policies.png b/docs/images/platform/access-controls/access-request-policies.png index d7ea4829c..a0eca9dfd 100644 Binary files a/docs/images/platform/access-controls/access-request-policies.png and b/docs/images/platform/access-controls/access-request-policies.png differ diff --git a/docs/images/platform/access-controls/create-access-request-policy.png b/docs/images/platform/access-controls/create-access-request-policy.png index 6593fd733..56f9840cf 100644 Binary files a/docs/images/platform/access-controls/create-access-request-policy.png and b/docs/images/platform/access-controls/create-access-request-policy.png differ diff --git a/docs/images/platform/identities/identities-org-create-oidc-auth-method.png b/docs/images/platform/identities/identities-org-create-oidc-auth-method.png new file mode 100644 index 000000000..ae4373752 Binary files /dev/null and b/docs/images/platform/identities/identities-org-create-oidc-auth-method.png differ diff --git a/docs/images/platform/pr-workflows/create-change-policy.png b/docs/images/platform/pr-workflows/create-change-policy.png new file mode 100644 index 000000000..4ff1ad884 Binary files /dev/null and b/docs/images/platform/pr-workflows/create-change-policy.png differ diff --git a/docs/images/platform/pr-workflows/secret-update-policy.png b/docs/images/platform/pr-workflows/secret-update-policy.png index 45e6322f1..53a4e92ca 100644 Binary files a/docs/images/platform/pr-workflows/secret-update-policy.png and b/docs/images/platform/pr-workflows/secret-update-policy.png differ diff --git a/docs/images/platform/secret-sharing/create-new-secret.png b/docs/images/platform/secret-sharing/create-new-secret.png index 335fca2b2..03a34e19d 100644 Binary files a/docs/images/platform/secret-sharing/create-new-secret.png and b/docs/images/platform/secret-sharing/create-new-secret.png differ diff --git a/docs/images/self-hosting/deployment-options/native/ha-stack.png b/docs/images/self-hosting/deployment-options/native/ha-stack.png new file mode 100644 index 000000000..1f71dc9e2 Binary files /dev/null and b/docs/images/self-hosting/deployment-options/native/ha-stack.png differ diff --git a/docs/images/self-hosting/deployment-options/native/haproxy-stats.png b/docs/images/self-hosting/deployment-options/native/haproxy-stats.png new file mode 100644 index 000000000..d8cb38d60 Binary files /dev/null and b/docs/images/self-hosting/deployment-options/native/haproxy-stats.png differ diff --git a/docs/integrations/platforms/kubernetes.mdx b/docs/integrations/platforms/kubernetes.mdx index 9f3a63f8c..dc76f2584 100644 --- a/docs/integrations/platforms/kubernetes.mdx +++ b/docs/integrations/platforms/kubernetes.mdx @@ -122,6 +122,7 @@ spec: # Azure Auth azureAuth: identityId: + resource: https://management.azure.com/&client_id=CLIENT_ID # (Optional) This is the Azure resource that you want to access. For example, "https://management.azure.com/". If no value is provided, it will default to "https://management.azure.com/" # secretsScope is identical to the secrets scope in the universalAuth field in this sample. secretsScope: diff --git a/docs/mint.json b/docs/mint.json index aac76b99b..f03320e1e 100644 --- a/docs/mint.json +++ b/docs/mint.json @@ -168,6 +168,13 @@ "documentation/platform/identities/gcp-auth", "documentation/platform/identities/azure-auth", "documentation/platform/identities/aws-auth", + { + "group": "OIDC Auth", + "pages": [ + "documentation/platform/identities/oidc-auth/general", + "documentation/platform/identities/oidc-auth/github" + ] + }, "documentation/platform/mfa", { "group": "SSO", @@ -210,7 +217,14 @@ "pages": [ "self-hosting/overview", { - "group": "Installation methods", + "group": "Native installation methods", + "pages": [ + "self-hosting/deployment-options/native/standalone-binary", + "self-hosting/deployment-options/native/high-availability" + ] + }, + { + "group": "Containerized installation methods", "pages": [ "self-hosting/deployment-options/standalone-infisical", "self-hosting/deployment-options/docker-swarm", @@ -427,6 +441,19 @@ "api-reference/endpoints/identities/list" ] }, + { + "group": "Token Auth", + "pages": [ + "api-reference/endpoints/token-auth/attach", + "api-reference/endpoints/token-auth/retrieve", + "api-reference/endpoints/token-auth/update", + "api-reference/endpoints/token-auth/revoke", + "api-reference/endpoints/token-auth/get-tokens", + "api-reference/endpoints/token-auth/create-token", + "api-reference/endpoints/token-auth/update-token", + "api-reference/endpoints/token-auth/revoke-token" + ] + }, { "group": "Universal Auth", "pages": [ @@ -443,6 +470,56 @@ "api-reference/endpoints/universal-auth/revoke-access-token" ] }, + { + "group": "GCP Auth", + "pages": [ + "api-reference/endpoints/gcp-auth/login", + "api-reference/endpoints/gcp-auth/attach", + "api-reference/endpoints/gcp-auth/retrieve", + "api-reference/endpoints/gcp-auth/update", + "api-reference/endpoints/gcp-auth/revoke" + ] + }, + { + "group": "AWS Auth", + "pages": [ + "api-reference/endpoints/aws-auth/login", + "api-reference/endpoints/aws-auth/attach", + "api-reference/endpoints/aws-auth/retrieve", + "api-reference/endpoints/aws-auth/update", + "api-reference/endpoints/aws-auth/revoke" + ] + }, + { + "group": "Azure Auth", + "pages": [ + "api-reference/endpoints/azure-auth/login", + "api-reference/endpoints/azure-auth/attach", + "api-reference/endpoints/azure-auth/retrieve", + "api-reference/endpoints/azure-auth/update", + "api-reference/endpoints/azure-auth/revoke" + ] + }, + { + "group": "Kubernetes Auth", + "pages": [ + "api-reference/endpoints/kubernetes-auth/login", + "api-reference/endpoints/kubernetes-auth/attach", + "api-reference/endpoints/kubernetes-auth/retrieve", + "api-reference/endpoints/kubernetes-auth/update", + "api-reference/endpoints/kubernetes-auth/revoke" + ] + }, + { + "group": "OIDC Auth", + "pages": [ + "api-reference/endpoints/oidc-auth/login", + "api-reference/endpoints/oidc-auth/attach", + "api-reference/endpoints/oidc-auth/retrieve", + "api-reference/endpoints/oidc-auth/update", + "api-reference/endpoints/oidc-auth/revoke" + ] + }, { "group": "Organizations", "pages": [ @@ -506,6 +583,7 @@ "group": "Folders", "pages": [ "api-reference/endpoints/folders/list", + "api-reference/endpoints/folders/get-by-id", "api-reference/endpoints/folders/create", "api-reference/endpoints/folders/update", "api-reference/endpoints/folders/delete" diff --git a/docs/self-hosting/configuration/envars.mdx b/docs/self-hosting/configuration/envars.mdx index 03bea6fb0..24ec3a372 100644 --- a/docs/self-hosting/configuration/envars.mdx +++ b/docs/self-hosting/configuration/envars.mdx @@ -328,6 +328,27 @@ SMTP_FROM_NAME=Infisical + +1. Create an account and configure [SMTP2Go](https://www.smtp2go.com/) to send emails. +2. Turn on SMTP authentication +``` +SMTP_HOST=mail.smtp2go.com +SMTP_PORT=You can use one of the following ports: 2525, 80, 25, 8025, or 587 +SMTP_USERNAME=username #Your SMTP2GO account's SMTP username +SMTP_PASSWORD=password #Your SMTP2GO account's SMTP password +SMTP_FROM_ADDRESS=hey@example.com # your email address being used to send out emails +SMTP_FROM_NAME=Infisical +``` +{" "} + + +Optional (for TLS/SSL): + +TLS: Available on the same ports (2525, 80, 25, 8025, or 587) +SSL: Available on ports 465, 8465, and 443 + + + ## Authentication By default, users can only login via email/password based login method. diff --git a/docs/self-hosting/deployment-options/native/high-availability.mdx b/docs/self-hosting/deployment-options/native/high-availability.mdx new file mode 100644 index 000000000..931acb4df --- /dev/null +++ b/docs/self-hosting/deployment-options/native/high-availability.mdx @@ -0,0 +1,520 @@ +--- +title: "Automatically deploy Infisical with High Availability" +sidebarTitle: "High Availability" +--- + + +# Self-Hosting Infisical with a native High Availability (HA) deployment + +This page describes the Infisical architecture designed to provide high availability (HA) and how to deploy Infisical with high availability. The high availability deployment is designed to ensure that Infisical services are always available and can handle service failures gracefully, without causing service disruptions. + + + This deployment option is currently only available for Debian-based nodes (e.g., Ubuntu, Debian). + We plan on adding support for other operating systems in the future. + + +## High availability architecture +| Service | Nodes | Configuration | GCP | AWS | +|----------------------------------|----------------|------------------------------|---------------|--------------| +| External load balancer$^1$ | 1 | 4 vCPU, 3.6 GB memory | n1-highcpu-4 | c5n.xlarge | +| Internal load balancer$^2$ | 1 | 4 vCPU, 3.6 GB memory | n1-highcpu-4 | c5n.xlarge | +| Etcd cluster$^3$ | 3 | 4 vCPU, 3.6 GB memory | n1-highcpu-4 | c5n.xlarge | +| PostgreSQL$^4$ | 3 | 2 vCPU, 7.5 GB memory | n1-standard-2 | m5.large | +| Sentinel$^4$ | 3 | 2 vCPU, 7.5 GB memory | n1-standard-2 | m5.large | +| Redis$^4$ | 3 | 2 vCPU, 7.5 GB memory | n1-standard-2 | m5.large | +| Infisical Core | 3 | 8 vCPU, 7.2 GB memory | n1-highcpu-8 | c5.2xlarge | + +**Footnotes:** +1. External load balancer: If you wish to have multiple instances of the internal load balancer, you will need to use an external load balancer to distribute incoming traffic across multiple internal load balancers. + Using multiple internal load balancers is recommended for high-traffic environments. In the following guide we will use a single internal load balancer, as external load balancing falls outside the scope of this guide. +2. Internal load balancer: The internal load balancer (a HAProxy instance) is used to distribute incoming traffic across multiple Infisical Core instances, Postgres nodes, and Redis nodes. The internal load balancer exposes a set of ports _(80 for Infiscial, 5000 for Read/Write postgres, 5001 for Read-only postgres, and 6379 for Redis)_. Where these ports route to is determained by the internal load balancer based on the availability and health of the service nodes. + The internal load balancer is only accessible from within the same network, and is not exposed to the public internet. +3. Etcd cluster: Etcd is a distributed key-value store used to store and distribute data between the PostgreSQL nodes. Etcd is dependent on high disk I/O performance, therefore it is highly recommended to use highly performant SSD disks for the Etcd nodes, with _at least_ 80GB of disk space. +4. The Redis and PostgreSQL nodes will automatically be configured for high availability and used in your Infisical Core instances. However, you can optionally choose to bring your own database (BYOD), and skip these nodes. See more on how to [provide your own databases](#provide-your-own-databases). + + + For all services that require multiple nodes, it is recommended to deploy them across multiple availability zones (AZs) to ensure high availability and fault tolerance. This will help prevent service disruptions in the event of an AZ failure. + + +![High availability stack](../../images/self-hosting/deployment-options/native/ha-stack.png) +The image above shows how a high availability deployment of Infisical is structured. In this example, an external load balancer is used to distribute incoming traffic across multiple internal load balancers. The internal load balancers. The external load balancer isn't required, and it will require additional configuration to set up. + +### Fault Tolerance +This setup provides N+1 redundancy, meaning it can tolerate the failure of any single node without service interruption. + +## Ansible +### What is Ansible +Ansible is an open-source automation tool that simplifies application deployment, configuration management, and task automation. +At Infisical, we use Ansible to automate the deployment of Infisical services. The Ansible roles are designed to make it easy to deploy Infisical services in a high availability environment. + +### Installing Ansible + + + ```bash + pipx install --include-deps ansible + ``` + + + ```bash + ansible --version + ``` + + + + +### Understanding Ansible Concepts + +* Inventory _(inventory.ini)_: A file that lists your target hosts. +* Playbook _(playbook.yml)_: YAML file containing a set of tasks to be executed on hosts. +* Roles: Reusable units of organization for playbooks. Roles are used to group tasks together in a structured and reusable manner. + + +### Basic Ansible Commands +Running a playbook with with an invetory file: +```bash + ansible-playbook -i inventory.ini playbook.yml +``` + +This is how you would run the playbook containing the roles for setting up Infisical in a high availability environment. + +### Installing the Infisical High Availability Deployment Ansible Role +The Infisical Ansible role is available on Ansible Galaxy. You can install the role by running the following command: +```bash + ansible-galaxy collection install infisical.infisical_core_ha_deployment +``` + + +## Set up components +1. External load balancer (optional, and not covered in this guide) +2. [Configure Etcd cluster](#configure-etcd-cluster) +3. [Configure PostgreSQL database](#configure-postgresql-database) +4. [Configure Redis/Sentinel](#configure-redis-and-sentinel) +5. [Configure Infisical Core](#configure-infisical-core) + + +The servers start on the same 52.1.0.0/24 private network range, and can connect to each other freely on these addresses. + +The following list includes descriptions of each server and its assigned IP: + +52.1.0.1: External Load Balancer +52.1.0.2: Internal Load Balancer +52.1.0.3: Etcd 1 +52.1.0.4: Etcd 2 +52.1.0.5: Etcd 3 +52.1.0.6: PostgreSQL 1 +52.1.0.7: PostgreSQL 2 +52.1.0.8: PostgreSQL 3 +52.1.0.9: Redis 1 +52.1.0.10: Redis 2 +52.1.0.11: Redis 3 +52.1.0.12: Sentinel 1 +52.1.0.13: Sentinel 2 +52.1.0.14: Sentinel 3 +52.1.0.15: Infisical Core 1 +52.1.0.16: Infisical Core 2 +52.1.0.17: Infisical Core 3 + + + +### Configure Etcd cluster + +Configuring the ETCD cluster is the first step in setting up a high availability deployment of Infisical. +The ETCD cluster is used to store and distribute data between the PostgreSQL nodes. The ETCD cluster is a distributed key-value store that is highly available and fault-tolerant. + +```yaml example.playbook.yml + - hosts: all + gather_facts: true + + - name: Set up etcd cluster + hosts: etcd + become: true + collections: + - infisical.infisical_core_ha_deployment + roles: + - role: etcd +``` + +```ini example.inventory.ini + [etcd] + etcd1 ansible_host=52.1.0.3 + etcd2 ansible_host=52.1.0.4 + etcd3 ansible_host=52.1.0.5 + + [etcd:vars] + ansible_user=ubuntu + ansible_ssh_private_key_file=./ssh-key.pem + ansible_ssh_common_args='-o StrictHostKeyChecking=no' +``` + +### Configure PostgreSQL database + +The Postgres role takes a set of parameters that are used to configure your PostgreSQL database. + +Make sure to set the following variables in your playbook.yml file: +- `postgres_super_user_password`: The password for the 'postgres' database user. +- `postgres_db_name`: The name of the database that will be created on the leader node and replicated to the secondary nodes. +- `postgres_user`: The name of the user that will be created on the leader node and replicated to the secondary nodes. +- `postgres_user_password`: The password for the user that will be created on the leader node and replicated to the secondary nodes. +- `etcd_hosts`: The list of etcd hosts that the PostgreSQL nodes will use to communicate with etcd. By default you want to keep this value set to `"{{ groups['etcd'] }}"` + +```yaml example.playbook.yml + - hosts: all + gather_facts: true + + - name: Set up PostgreSQL with Patroni + hosts: postgres + become: true + collections: + - infisical.infisical_core_ha_deployment + roles: + - role: postgres + vars: + postgres_super_user_password: "your-super-user-password" + postgres_user: infisical-user + postgres_user_password: "your-password" + postgres_db_name: infisical-db + + etcd_hosts: "{{ groups['etcd'] }}" +``` + +```ini example.inventory.ini + [postgres] + postgres1 ansible_host=52.1.0.6 + postgres2 ansible_host=52.1.0.7 + postgres3 ansible_host=52.1.0.8 +``` + +### Configure Redis and Sentinel + +The Redis role takes a single variable as input, which is the redis password. +The Sentinel and Redis hosts will run the same role, therefore we are running the task for both the sentinel and redis hosts, `hosts: redis:sentinel`. + +- `redis_password`: The password that will be set for the Redis instance. + +```yaml example.playbook.yml + - hosts: all + gather_facts: true + + - name: Setup Redis and Sentinel + hosts: redis:sentinel + become: true + collections: + - infisical.infisical_core_ha_deployment + roles: + - role: redis + vars: + redis_password: "REDIS_PASSWORD" +``` + +```ini example.inventory.ini + [redis] + redis1 ansible_host=52.1.0.9 + redis2 ansible_host=52.1.0.10 + redis3 ansible_host=52.1.0.11 + + [sentinel] + sentinel1 ansible_host=52.1.0.12 + sentinel2 ansible_host=52.1.0.13 + sentinel3 ansible_host=52.1.0.14 +``` + +### Configure Internal Load Balancer + +The internal load balancer used is HAProxy. HAProxy will expose a set of ports as listed below. Each port will route to a different service based on the availability and health of the service nodes. + +- Port 80: Infisical Core +- Port 5000: Read/Write PostgreSQL +- Port 5001: Read-only PostgreSQL +- Port 6379: Redis +- Port 7000: HAProxy monitoring +These ports will need to be exposed on your network to become accessible from the outside world. + +The HAProxy configuration file is generated by the Infisical Core role, and is located at `/etc/haproxy/haproxy.cfg` on your internal load balancer node. + +The HAProxy setup comes with a monitoring panel. You have to set the username/password combination for the monitoring panel by setting the `stats_user` and `stats_password` variables in the HAProxy role. + + +Once the HAProxy role has fully executed, you can monitor your HA setup by navigating to `http://52.1.0.2:7000/haproxy?stats` in your browser. + +```ini example.inventory.ini +[haproxy] +internal_lb ansible_host=52.1.0.2 +``` + +```yaml example.playbook.yml +- name: Set up HAProxy + hosts: haproxy + become: true + collections: + - infisical.infisical_core_ha_deployment + roles: + - role: haproxy + vars: + stats_user: "stats-username" + stats_password: "stats-password!" + + postgres_servers: "{{ groups['postgres'] }}" + infisical_servers: "{{ groups['infisical'] }}" + redis_servers: "{{ groups['redis'] }}" +``` + + + +### Configure Infisical Core + +The Infisical Core role will set up your actual Infisical instances. + +The `env_vars` variable is used to set the environment variables that Infisical will use. The minimum required environment variables are `ENCRYPTION_KEY` and `AUTH_SECRET`. You can find a list of all available environment variables [here](/docs/self-hosting/configuration/envars#general-platform). +The `DB_CONNECTION_URI` and `REDIS_URL` variables will automatically be set if you're running the full playbook. However, you can choose to set them yourself, and skip the Postgres, etcd, redis/sentinel roles entirely. + + + If you later need to add new environment varibles to your Infisical deployments, it's important you add the variables to **all** your Infisical nodes.
+ You can find the environment file for Infisical at `/etc/infisical/environment`.
+ After editing the environment file, you need to reload the Infisical service by doing `systemctl restart infisical`. +
+ +```yaml example.playbook.yml + - hosts: all + gather_facts: true + + - name: Setup Infisical + hosts: infisical + become: true + collections: + - infisical.infisical_core_ha_deployment + roles: + - role: infisical + env_vars: + ENCRYPTION_KEY: "YOUR_ENCRYPTION_KEY" # openssl rand -hex 16 + AUTH_SECRET: "YOUR_AUTH_SECRET" # openssl rand -base64 32 +``` + +```ini example.inventory.ini + [infisical] + infisical1 ansible_host=52.1.0.15 + infisical2 ansible_host=52.1.0.16 + infisical3 ansible_host=52.1.0.17 +``` + +## Provide your own databases +Bringing your own database is an option using the Infisical Core deployment role. +By bringing your own database, you're able to skip the Etcd, Postgres, and Redis/Sentinel roles entirely. + +To bring your own database, you need to set the `DB_CONNECTION_URI` and `REDIS_URL` environment variables in the Infisical Core role. + +```yaml example.playbook.yml + - hosts: all + gather_facts: true + + - name: Setup Infisical + hosts: infisical + become: true + collections: + - infisical.infisical_core_ha_deployment + roles: + - role: infisical + env_vars: + ENCRYPTION_KEY: "YOUR_ENCRYPTION_KEY" # openssl rand -hex 16 + AUTH_SECRET: "YOUR_AUTH_SECRET" # openssl rand -base64 32 + DB_CONNECTION_URI: "postgres://user:password@localhost:5432/infisical" + REDIS_URL: "redis://localhost:6379" +``` + +```ini example.inventory.ini + [infisical] + infisical1 ansible_host=52.1.0.15 + infisical2 ansible_host=52.1.0.16 + infisical3 ansible_host=52.1.0.17 +``` + +## Full deployment example +To make it easier to get started, we've provided a full deployment example that you can use to deploy Infisical in a high availability environment. + +- This deployment does not use an external load balancer. +- You **must** change the environment variables defined in the `playbook.yml` example. +- You have update the IP addresses in the `inventory.ini` file to match your own network configuration. +- You need to set the SSH key and ssh user in the `inventory.ini` file. + + + + Install Ansible using the pipx Python package manager. + ```bash + pipx install --include-deps ansible + ``` + + + + Install the Infisical deployment role from Ansible Galaxy. + ```bash + ansible-galaxy collection install infisical.infisical_core_ha_deployment + ``` + + + + Create an `inventory.ini` file, and define your hosts and their IP addresses. You can use the example below as a template, and update the IP addresses to match your own network configuration. + Make sure to set the SSH key and ssh user in the `inventory.ini` file. Please see the example below. + + ```ini example.inventory.ini + [etcd] + etcd1 ansible_host=52.1.0.3 + etcd2 ansible_host=52.1.0.4 + etcd3 ansible_host=52.1.0.5 + + [postgres] + postgres1 ansible_host=52.1.0.6 + postgres2 ansible_host=52.1.0.7 + postgres3 ansible_host=52.1.0.8 + + [infisical] + infisical1 ansible_host=52.1.0.15 + infisical2 ansible_host=52.1.0.16 + infisical3 ansible_host=52.1.0.17 + + [redis] + redis1 ansible_host=52.1.0.9 + redis2 ansible_host=52.1.0.10 + redis3 ansible_host=52.1.0.11 + + [sentinel] + sentinel1 ansible_host=52.1.0.12 + sentinel2 ansible_host=52.1.0.13 + sentinel3 ansible_host=52.1.0.14 + + [haproxy] + internal_lb ansible_host=52.1.0.2 + + ; This can be defined individually for each host, or globally for all hosts. + ; In this case the credentials are the same for all hosts, so we define them globally as seen below ([all:vars]). + [all:vars] + ansible_user=ubuntu + ansible_ssh_private_key_file=./your-ssh-key.pem + ansible_ssh_common_args='-o StrictHostKeyChecking=no' + ``` + + + The Ansible playbook is where you define which roles/tasks to execute on which hosts. + + ```yaml example.playbook.yml + --- + # Important, we must gather facts from all hosts prior to running the roles to ensure we have all the information we need. + - hosts: all + gather_facts: true + + - name: Set up etcd cluster + hosts: etcd + become: true + collections: + - infisical.infisical_core_ha_deployment + roles: + - role: etcd + + - name: Set up PostgreSQL with Patroni + hosts: postgres + become: true + collections: + - infisical.infisical_core_ha_deployment + roles: + - role: postgres + vars: + postgres_super_user_password: "" # Password for the 'postgres' database user + + # A database with these credentials will be created on the leader node, and replicated to the secondary nodes. + postgres_db_name: + postgres_user: + postgres_user_password: + + etcd_hosts: "{{ groups['etcd'] }}" + + - name: Setup Redis and Sentinel + hosts: redis:sentinel + become: true + collections: + - infisical.infisical_core_ha_deployment + roles: + - role: redis + vars: + redis_password: "" + + - name: Set up HAProxy + hosts: haproxy + become: true + collections: + - infisical.infisical_core_ha_deployment + roles: + - role: haproxy + vars: + stats_user: "" + stats_password: "" + + postgres_servers: "{{ groups['postgres'] }}" + infisical_servers: "{{ groups['infisical'] }}" + redis_servers: "{{ groups['redis'] }}" + - name: Setup Infisical + hosts: infisical + become: true + collections: + - infisical.infisical_core_ha_deployment + roles: + - role: infisical + env_vars: + ENCRYPTION_KEY: "YOUR_ENCRYPTION_KEY" # openssl rand -hex 16 + AUTH_SECRET: "YOUR_AUTH_SECRET" # openssl rand -base64 32 + ``` + + + After creating the `playbook.yml` and `inventory.ini` files, you can run the playbook using the following command + ```bash + ansible-playbook -i inventory.ini playbook.yml + ``` + + This step may take upwards of 10 minutes to complete, depending on the number of nodes and the network speed. + Once the playbook has completed, you should have a fully deployed high availability Infisical environment. + + To access Infisical, you can try navigating to `http://52.1.0.2`, in order to view your newly deployed Infisical instance. + + + + +## Post-deployment steps +After deploying Infisical in a high availability environment, you should perform the following post-deployment steps: +- Check your deployment to ensure that all services are running as expected. You can use the HAProxy monitoring panel to check the status of your services (http://52.1.0.2:7000/haproxy?stats) +- Attempt to access the Infisical Core instances to ensure that they are accessible from the internal load balancer. (http://52.1.0.2) + +A HAProxy stats page indicating success will look like the image below +![HAProxy stats page](../../images/self-hosting/deployment-options/native/haproxy-stats.png) + + +## Security Considerations +### Network Security +Secure the network that your instances run on. While this falls outside the scope of Infisical deployment, it's crucial for overall security. +AWS-specific recommendations: + +Use Virtual Private Cloud (VPC) to isolate your infrastructure. +Configure security groups to restrict inbound and outbound traffic. +Use Network Access Control Lists (NACLs) for additional network-level security. + + + Please take note that the Infisical team cannot provide infrastructure support for **free self-hosted** deployments.
If you need help with infrastructure, we recommend upgrading to a [paid plan](https://infisical.com/pricing) which includes infrastructure support. + + You can also join our community [Slack](https://infisical.com/slack) for help and support from the community. +
+ + +### Troubleshooting + + If you encounter this issue, please update your ansible config (`ansible.cfg`) file with the following configuration: + ```ini + [defaults] + allow_world_readable_tmpfiles = true + ``` + + You can read more about the solution [here](https://docs.ansible.com/ansible/latest/collections/ansible/builtin/sh_shell.html#parameter-world_readable_temp) + + + + This issue can be caused by a number of reasons, mostly realted to the network configuration. Here are a few things you can check: + 1. Ensure that the firewall is not blocking the connection. You can check this by running `ufw status`. Ensure that port 80 is open. + 2. If you're using a cloud provider like AWS or GCP, ensure that the security group allows traffic on port 80. + 3. Ensure that the HAProxy service is running. You can check this by running `systemctl status haproxy`. + 4. Ensure that the Infisical service is running. You can check this by running `systemctl status infisical`. + \ No newline at end of file diff --git a/docs/self-hosting/deployment-options/native/standalone-binary.mdx b/docs/self-hosting/deployment-options/native/standalone-binary.mdx new file mode 100644 index 000000000..f187902f9 --- /dev/null +++ b/docs/self-hosting/deployment-options/native/standalone-binary.mdx @@ -0,0 +1,203 @@ +--- +title: "Standalone" +description: "Learn how to deploy Infisical in a standalone environment." +--- + +# Self-Hosting Infisical with Standalone Infisical + +Deploying Infisical in a standalone environment is a great way to get started with Infisical without having to use containers. This guide will walk you through the process of deploying Infisical in a standalone environment. +This is one of the easiest ways to deploy Infisical. It is a single executable, currently only supported on Debian-based systems. + +The standalone deployment implements the "bring your own database" (BYOD) approach. This means that you will need to provide your own databases (specifically Postgres and Redis) for the Infisical services to use. The standalone deployment does not include any databases. + +If you wish to streamline the deployment process, we recommend using the Ansible role for Infisical. The Ansible role automates the deployment process and includes the databases: +- [Automated Deployment](https://google.com) +- [Automated Deployment with high availability (HA)](https://google.com) + + +## Prerequisites +- A server running a Debian-based operating system (e.g., Ubuntu, Debian) +- A Postgres database +- A Redis database + +## Installing Infisical +Installing Infisical is as simple as running a single command. You can install Infisical by running the following command: + +```bash + $ curl -1sLf 'https://dl.cloudsmith.io/public/infisical/infisical-core/cfg/setup/bash.deb.sh' | sudo bash && sudo apt-get install -y infisical-core +``` + +## Running Infisical +Running Infisical and serving it to the web has a few steps. Below are the steps to get you started with running Infisical in a standalone environment. + * Setup environment variables + * Running Postgres migrations + * Create system daemon + * Exposing Infisical to the internet + + + + + To use Infisical you'll need to configure the environment variables beforehand. You can acheive this by creating an environment file to be used by Infisical. + + + #### Create environment file + ```bash + $ mkdir -p /etc/infisical && touch /etc/infisical/environment + ``` + + After creating the environment file, you'll need to fill it out with your environment variables. + + #### Edit environment file + ```bash + $ nano /etc/infisical/environment + ``` + + ```bash + DB_CONNECTION_URI=postgres://user:password@localhost:5432/infisical # Replace with your Postgres database connection URI + REDIS_URL=redis://localhost:6379 # Replace with your Redis connection URI + ENCRYPTION_KEY=your_encryption_key # Replace with your encryption key (can be generated with: openssl rand -hex 16) + AUTH_SECRET=your_auth_secret # Replace with your auth secret (can be generated with: openssl rand -base64 32) + ``` + + + The minimum required environment variables are `DB_CONNECTION_URI`, `REDIS_URL`, `ENCRYPTION_KEY`, and `AUTH_SECRET`. We recommend You take a look at our [list of all available environment variables](/docs/self-hosting/configuration/envars#general-platform), and configure the ones you need. + + + + + Assuming you're starting with a fresh Postgres database, you'll need to run the Postgres migrations to syncronize the database schema. + The migration command will use the environment variables you configured in the previous step. + + + ```bash + $ eval $(cat /etc/infisical/environment) infisical-core migration:latest + ``` + + + This step will need to be repeated if you update Infisical in the future. + + + + + + ```bash + $ nano /etc/systemd/system/infisical.service + ``` + + + + Create a systemd service file for Infisical. Creating a systemd service file will allow Infisical to start automatically when the system boots or in case of a crash. + + ```bash + $ nano /etc/systemd/system/infisical.service + ``` + + ```ini + [Unit] + Description=Infisical Service + After=network.target + + [Service] + # The path to the environment file we created in the previous step + EnvironmentFile=/etc/infisical/environment + Type=simple + # Change the user to the user you want to run Infisical as + User=root + ExecStart=/usr/local/bin/infisical-core + Restart=always + RestartSec=30 + + [Install] + WantedBy=multi-user.target + ``` + + Now we need to reload the systemd daemon and start the Infisical service. + + ```bash + $ systemctl daemon-reload + $ systemctl start infisical + $ systemctl enable infisical + ``` + + + You can check the status of the Infisical service by running `systemctl status infisical`. + It is also a good idea to check the logs for any errors by running `journalctl --no-pager -u infisical`. + + + + Exposing Infisical to the internet requires setting up a reverse proxy. You can use any reverse proxy of your choice, but we recommend using HAProxy or Nginx. Below is an example of how to set up a reverse proxy using HAProxy. + + #### Install HAProxy + ```bash + $ apt-get install -y haproxy + ``` + + #### Edit HAProxy configuration + ```bash + $ nano /etc/haproxy/haproxy.cfg + ``` + + ```ini + global + log /dev/log local0 + log /dev/log local1 notice + chroot /var/lib/haproxy + stats socket /run/haproxy/admin.sock mode 660 level admin expose-fd listeners + stats timeout 30s + user haproxy + group haproxy + daemon + + defaults + log global + mode http + option httplog + option dontlognull + timeout connect 5000 + timeout client 50000 + timeout server 50000 + + frontend http-in + bind *:80 + default_backend infisical + + backend infisical + server infisicalapp 127.0.0.1:8080 check + ``` + + + If you decide to use Nginx, then please be aware that the configuration will be different. **Infisical listens on port 8080**. + + + #### Restart HAProxy + ```bash + $ systemctl restart haproxy + ``` + + + + +And that's it! You have successfully deployed Infisical in a standalone environment. You can now access Infisical by visiting `http://your-server-ip`. + + + Please take note that the Infisical team cannot provide infrastructure support for **free self-hosted** deployments.
If you need help with infrastructure, we recommend upgrading to a [paid plan](https://infisical.com/pricing) which includes infrastructure support. + + You can also join our community [Slack](https://infisical.com/slack) for help and support from the community. +
+ +## Troubleshooting + + + This is a common issue related to the HAProxy configuration file. The error is caused by the missing newline character at the end of the file. You can fix this by adding a newline character at the end of the file. + + ```bash + $ echo "" >> /etc/haproxy/haproxy.cfg + ``` + + + This issue can be caused by a number of reasons, mostly realted to the network configuration. Here are a few things you can check: + 1. Ensure that the firewall is not blocking the connection. You can check this by running `ufw status`. Ensure that port 80 is open. + 2. If you're using a cloud provider like AWS or GCP, ensure that the security group allows traffic on port 80. + 3. Ensure that the HAProxy service is running. You can check this by running `systemctl status haproxy`. + 4. Ensure that the Infisical service is running. You can check this by running `systemctl status infisical`. + \ No newline at end of file diff --git a/docs/self-hosting/ee.mdx b/docs/self-hosting/ee.mdx index a72bad908..d77510c11 100644 --- a/docs/self-hosting/ee.mdx +++ b/docs/self-hosting/ee.mdx @@ -15,15 +15,30 @@ This guide walks through how you can use these paid features on a self hosted in Depending on whether or not the environment where Infisical is deployed has internet access, you may be issued a regular license or an offline license. - - - If using a regular license, you should set the value of the environment variable `LICENSE_KEY` in Infisical to the issued license key. - - If using an offline license, you should set the value of the environment variable `LICENSE_KEY_OFFLINE` in Infisical to the issued license key. - - How you set the environment variable will depend on the deployment method you used. Please refer to the documentation of your deployment method for specific instructions. - + + + + - Assign the issued license key to the `LICENSE_KEY` environment variable in your Infisical instance. + + - Your Infisical instance will need to communicate with the Infisical license server to validate the license key. + If you want to limit outgoing connections only to the Infisical license server, you can use the following IP addresses: `13.248.249.247` and `35.71.190.59` + + + Ensure that your firewall or network settings allow outbound connections to these IP addresses to avoid any issues with license validation. + + + + - Assign the issued license key to the `LICENSE_KEY_OFFLINE` environment variable in your Infisical instance. + + + How you set the environment variable will depend on the deployment method you used. Please refer to the documentation of your deployment method for specific instructions. + + + Once your instance starts up, the license key will be validated and you’ll be able to use the paid features. However, when the license expires, Infisical will continue to run, but EE features will be disabled until the license is renewed or a new one is purchased. + diff --git a/docs/self-hosting/overview.mdx b/docs/self-hosting/overview.mdx index ccc4ae912..3c402f904 100644 --- a/docs/self-hosting/overview.mdx +++ b/docs/self-hosting/overview.mdx @@ -33,3 +33,21 @@ Choose from a number of deployment options listed below to get started. Use our Helm chart to Install Infisical on your Kubernetes cluster. + + + Install Infisical on your Debian-based system without containers using our standalone binary. + + + Install Infisical on your Debian-based instances without containers using our standalone binary with high availability out of the box. + + diff --git a/frontend/next.config.js b/frontend/next.config.js index 5e48e70da..d1d5876ed 100644 --- a/frontend/next.config.js +++ b/frontend/next.config.js @@ -2,7 +2,7 @@ const path = require("path"); const ContentSecurityPolicy = ` default-src 'self'; - script-src 'self' https://app.posthog.com https://js.stripe.com https://api.stripe.com https://widget.intercom.io https://js.intercomcdn.com https://hcaptcha.com https://*.hcaptcha.com 'unsafe-inline' 'unsafe-eval'; + script-src 'self' https://*.posthog.com https://*.*.posthog.com https://js.stripe.com https://api.stripe.com https://widget.intercom.io https://js.intercomcdn.com https://hcaptcha.com https://*.hcaptcha.com 'unsafe-inline' 'unsafe-eval'; style-src 'self' https://rsms.me 'unsafe-inline' https://hcaptcha.com https://*.hcaptcha.com; child-src https://api.stripe.com; frame-src https://js.stripe.com/ https://api.stripe.com https://www.youtube.com/ https://hcaptcha.com https://*.hcaptcha.com; diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 5837e8d7e..6007a282d 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -47,7 +47,7 @@ "@ucast/mongo2js": "^1.3.4", "add": "^2.0.6", "argon2-browser": "^1.18.0", - "axios": "^0.27.2", + "axios": "^0.28.0", "axios-auth-refresh": "^3.3.6", "base64-loader": "^1.0.0", "classnames": "^2.3.1", @@ -136,10 +136,10 @@ "eslint-plugin-react-hooks": "^4.6.0", "eslint-plugin-simple-import-sort": "^8.0.0", "eslint-plugin-storybook": "^0.6.12", - "postcss": "^8.4.14", + "postcss": "^8.4.39", "prettier": "^2.8.3", "prettier-plugin-tailwindcss": "^0.2.2", - "storybook": "^7.5.2", + "storybook": "^7.6.20", "storybook-dark-mode": "^3.0.0", "tailwindcss": "3.2", "typescript": "^4.9.3" @@ -6200,15 +6200,15 @@ } }, "node_modules/@storybook/builder-manager": { - "version": "7.6.19", - "resolved": "https://registry.npmjs.org/@storybook/builder-manager/-/builder-manager-7.6.19.tgz", - "integrity": "sha512-Dt5OLh97xeWh4h2mk9uG0SbCxBKHPhIiHLHAKEIDzIZBdwUhuyncVNDPHW2NlXM+S7U0/iKs2tw05waqh2lHvg==", + "version": "7.6.20", + "resolved": "https://registry.npmjs.org/@storybook/builder-manager/-/builder-manager-7.6.20.tgz", + "integrity": "sha512-e2GzpjLaw6CM/XSmc4qJRzBF8GOoOyotyu3JrSPTYOt4RD8kjUsK4QlismQM1DQRu8i39aIexxmRbiJyD74xzQ==", "dev": true, "dependencies": { "@fal-works/esbuild-plugin-global-externals": "^2.1.2", - "@storybook/core-common": "7.6.19", - "@storybook/manager": "7.6.19", - "@storybook/node-logger": "7.6.19", + "@storybook/core-common": "7.6.20", + "@storybook/manager": "7.6.20", + "@storybook/node-logger": "7.6.20", "@types/ejs": "^3.1.1", "@types/find-cache-dir": "^3.2.1", "@yarnpkg/esbuild-plugin-pnp": "^3.0.0-rc.10", @@ -6228,13 +6228,13 @@ } }, "node_modules/@storybook/builder-manager/node_modules/@storybook/channels": { - "version": "7.6.19", - "resolved": "https://registry.npmjs.org/@storybook/channels/-/channels-7.6.19.tgz", - "integrity": "sha512-2JGh+i95GwjtjqWqhtEh15jM5ifwbRGmXeFqkY7dpdHH50EEWafYHr2mg3opK3heVDwg0rJ/VBptkmshloXuvA==", + "version": "7.6.20", + "resolved": "https://registry.npmjs.org/@storybook/channels/-/channels-7.6.20.tgz", + "integrity": "sha512-4hkgPSH6bJclB2OvLnkZOGZW1WptJs09mhQ6j6qLjgBZzL/ZdD6priWSd7iXrmPiN5TzUobkG4P4Dp7FjkiO7A==", "dev": true, "dependencies": { - "@storybook/client-logger": "7.6.19", - "@storybook/core-events": "7.6.19", + "@storybook/client-logger": "7.6.20", + "@storybook/core-events": "7.6.20", "@storybook/global": "^5.0.0", "qs": "^6.10.0", "telejson": "^7.2.0", @@ -6246,9 +6246,9 @@ } }, "node_modules/@storybook/builder-manager/node_modules/@storybook/client-logger": { - "version": "7.6.19", - "resolved": "https://registry.npmjs.org/@storybook/client-logger/-/client-logger-7.6.19.tgz", - "integrity": "sha512-oGzOxbmLmciSIfd5gsxDzPmX8DttWhoYdPKxjMuCuWLTO2TWpkCWp1FTUMWO72mm/6V/FswT/aqpJJBBvdZ3RQ==", + "version": "7.6.20", + "resolved": "https://registry.npmjs.org/@storybook/client-logger/-/client-logger-7.6.20.tgz", + "integrity": "sha512-NwG0VIJQCmKrSaN5GBDFyQgTAHLNishUPLW1NrzqTDNAhfZUoef64rPQlinbopa0H4OXmlB+QxbQIb3ubeXmSQ==", "dev": true, "dependencies": { "@storybook/global": "^5.0.0" @@ -6259,14 +6259,14 @@ } }, "node_modules/@storybook/builder-manager/node_modules/@storybook/core-common": { - "version": "7.6.19", - "resolved": "https://registry.npmjs.org/@storybook/core-common/-/core-common-7.6.19.tgz", - "integrity": "sha512-njwpGzFJrfbJr/AFxGP8KMrfPfxN85KOfSlxYnQwRm5Z0H1D/lT33LhEBf5m37gaGawHeG7KryxO6RvaioMt2Q==", + "version": "7.6.20", + "resolved": "https://registry.npmjs.org/@storybook/core-common/-/core-common-7.6.20.tgz", + "integrity": "sha512-8H1zPWPjcmeD4HbDm4FDD0WLsfAKGVr566IZ4hG+h3iWVW57II9JW9MLBtiR2LPSd8u7o0kw64lwRGmtCO1qAw==", "dev": true, "dependencies": { - "@storybook/core-events": "7.6.19", - "@storybook/node-logger": "7.6.19", - "@storybook/types": "7.6.19", + "@storybook/core-events": "7.6.20", + "@storybook/node-logger": "7.6.20", + "@storybook/types": "7.6.20", "@types/find-cache-dir": "^3.2.1", "@types/node": "^18.0.0", "@types/node-fetch": "^2.6.4", @@ -6294,9 +6294,9 @@ } }, "node_modules/@storybook/builder-manager/node_modules/@storybook/core-events": { - "version": "7.6.19", - "resolved": "https://registry.npmjs.org/@storybook/core-events/-/core-events-7.6.19.tgz", - "integrity": "sha512-K/W6Uvum0ocZSgjbi8hiotpe+wDEHDZlvN+KlPqdh9ae9xDK8aBNBq9IelCoqM+uKO1Zj+dDfSQds7CD781DJg==", + "version": "7.6.20", + "resolved": "https://registry.npmjs.org/@storybook/core-events/-/core-events-7.6.20.tgz", + "integrity": "sha512-tlVDuVbDiNkvPDFAu+0ou3xBBYbx9zUURQz4G9fAq0ScgBOs/bpzcRrFb4mLpemUViBAd47tfZKdH4MAX45KVQ==", "dev": true, "dependencies": { "ts-dedent": "^2.0.0" @@ -6307,9 +6307,9 @@ } }, "node_modules/@storybook/builder-manager/node_modules/@storybook/node-logger": { - "version": "7.6.19", - "resolved": "https://registry.npmjs.org/@storybook/node-logger/-/node-logger-7.6.19.tgz", - "integrity": "sha512-2g29QC44Zl1jKY37DmQ0/dO7+VSKnGgPI/x0mwVwQffypSapxH3rwLLT5Q5XLHeFyD+fhRu5w9Cj4vTGynJgpA==", + "version": "7.6.20", + "resolved": "https://registry.npmjs.org/@storybook/node-logger/-/node-logger-7.6.20.tgz", + "integrity": "sha512-l2i4qF1bscJkOplNffcRTsgQWYR7J51ewmizj5YrTM8BK6rslWT1RntgVJWB1RgPqvx6VsCz1gyP3yW1oKxvYw==", "dev": true, "funding": { "type": "opencollective", @@ -6317,12 +6317,12 @@ } }, "node_modules/@storybook/builder-manager/node_modules/@storybook/types": { - "version": "7.6.19", - "resolved": "https://registry.npmjs.org/@storybook/types/-/types-7.6.19.tgz", - "integrity": "sha512-DeGYrRPRMGTVfT7o2rEZtRzyLT2yKTI2exgpnxbwPWEFAduZCSfzBrcBXZ/nb5B0pjA9tUNWls1YzGkJGlkhpg==", + "version": "7.6.20", + "resolved": "https://registry.npmjs.org/@storybook/types/-/types-7.6.20.tgz", + "integrity": "sha512-GncdY3x0LpbhmUAAJwXYtJDUQEwfF175gsjH0/fxPkxPoV7Sef9TM41jQLJW/5+6TnZoCZP/+aJZTJtq3ni23Q==", "dev": true, "dependencies": { - "@storybook/channels": "7.6.19", + "@storybook/channels": "7.6.20", "@types/babel__core": "^7.0.0", "@types/express": "^4.7.0", "file-system-cache": "2.3.0" @@ -6438,23 +6438,23 @@ } }, "node_modules/@storybook/cli": { - "version": "7.6.19", - "resolved": "https://registry.npmjs.org/@storybook/cli/-/cli-7.6.19.tgz", - "integrity": "sha512-7OVy7nPgkLfgivv6/dmvoyU6pKl9EzWFk+g9izyQHiM/jS8jOiEyn6akG8Ebj6k5pWslo5lgiXUSW+cEEZUnqQ==", + "version": "7.6.20", + "resolved": "https://registry.npmjs.org/@storybook/cli/-/cli-7.6.20.tgz", + "integrity": "sha512-ZlP+BJyqg7HlnXf7ypjG2CKMI/KVOn03jFIiClItE/jQfgR6kRFgtjRU7uajh427HHfjv9DRiur8nBzuO7vapA==", "dev": true, "dependencies": { "@babel/core": "^7.23.2", "@babel/preset-env": "^7.23.2", "@babel/types": "^7.23.0", "@ndelangen/get-tarball": "^3.0.7", - "@storybook/codemod": "7.6.19", - "@storybook/core-common": "7.6.19", - "@storybook/core-events": "7.6.19", - "@storybook/core-server": "7.6.19", - "@storybook/csf-tools": "7.6.19", - "@storybook/node-logger": "7.6.19", - "@storybook/telemetry": "7.6.19", - "@storybook/types": "7.6.19", + "@storybook/codemod": "7.6.20", + "@storybook/core-common": "7.6.20", + "@storybook/core-events": "7.6.20", + "@storybook/core-server": "7.6.20", + "@storybook/csf-tools": "7.6.20", + "@storybook/node-logger": "7.6.20", + "@storybook/telemetry": "7.6.20", + "@storybook/types": "7.6.20", "@types/semver": "^7.3.4", "@yarnpkg/fslib": "2.10.3", "@yarnpkg/libzip": "2.3.0", @@ -6494,13 +6494,13 @@ } }, "node_modules/@storybook/cli/node_modules/@storybook/channels": { - "version": "7.6.19", - "resolved": "https://registry.npmjs.org/@storybook/channels/-/channels-7.6.19.tgz", - "integrity": "sha512-2JGh+i95GwjtjqWqhtEh15jM5ifwbRGmXeFqkY7dpdHH50EEWafYHr2mg3opK3heVDwg0rJ/VBptkmshloXuvA==", + "version": "7.6.20", + "resolved": "https://registry.npmjs.org/@storybook/channels/-/channels-7.6.20.tgz", + "integrity": "sha512-4hkgPSH6bJclB2OvLnkZOGZW1WptJs09mhQ6j6qLjgBZzL/ZdD6priWSd7iXrmPiN5TzUobkG4P4Dp7FjkiO7A==", "dev": true, "dependencies": { - "@storybook/client-logger": "7.6.19", - "@storybook/core-events": "7.6.19", + "@storybook/client-logger": "7.6.20", + "@storybook/core-events": "7.6.20", "@storybook/global": "^5.0.0", "qs": "^6.10.0", "telejson": "^7.2.0", @@ -6512,9 +6512,9 @@ } }, "node_modules/@storybook/cli/node_modules/@storybook/client-logger": { - "version": "7.6.19", - "resolved": "https://registry.npmjs.org/@storybook/client-logger/-/client-logger-7.6.19.tgz", - "integrity": "sha512-oGzOxbmLmciSIfd5gsxDzPmX8DttWhoYdPKxjMuCuWLTO2TWpkCWp1FTUMWO72mm/6V/FswT/aqpJJBBvdZ3RQ==", + "version": "7.6.20", + "resolved": "https://registry.npmjs.org/@storybook/client-logger/-/client-logger-7.6.20.tgz", + "integrity": "sha512-NwG0VIJQCmKrSaN5GBDFyQgTAHLNishUPLW1NrzqTDNAhfZUoef64rPQlinbopa0H4OXmlB+QxbQIb3ubeXmSQ==", "dev": true, "dependencies": { "@storybook/global": "^5.0.0" @@ -6525,14 +6525,14 @@ } }, "node_modules/@storybook/cli/node_modules/@storybook/core-common": { - "version": "7.6.19", - "resolved": "https://registry.npmjs.org/@storybook/core-common/-/core-common-7.6.19.tgz", - "integrity": "sha512-njwpGzFJrfbJr/AFxGP8KMrfPfxN85KOfSlxYnQwRm5Z0H1D/lT33LhEBf5m37gaGawHeG7KryxO6RvaioMt2Q==", + "version": "7.6.20", + "resolved": "https://registry.npmjs.org/@storybook/core-common/-/core-common-7.6.20.tgz", + "integrity": "sha512-8H1zPWPjcmeD4HbDm4FDD0WLsfAKGVr566IZ4hG+h3iWVW57II9JW9MLBtiR2LPSd8u7o0kw64lwRGmtCO1qAw==", "dev": true, "dependencies": { - "@storybook/core-events": "7.6.19", - "@storybook/node-logger": "7.6.19", - "@storybook/types": "7.6.19", + "@storybook/core-events": "7.6.20", + "@storybook/node-logger": "7.6.20", + "@storybook/types": "7.6.20", "@types/find-cache-dir": "^3.2.1", "@types/node": "^18.0.0", "@types/node-fetch": "^2.6.4", @@ -6560,9 +6560,9 @@ } }, "node_modules/@storybook/cli/node_modules/@storybook/core-events": { - "version": "7.6.19", - "resolved": "https://registry.npmjs.org/@storybook/core-events/-/core-events-7.6.19.tgz", - "integrity": "sha512-K/W6Uvum0ocZSgjbi8hiotpe+wDEHDZlvN+KlPqdh9ae9xDK8aBNBq9IelCoqM+uKO1Zj+dDfSQds7CD781DJg==", + "version": "7.6.20", + "resolved": "https://registry.npmjs.org/@storybook/core-events/-/core-events-7.6.20.tgz", + "integrity": "sha512-tlVDuVbDiNkvPDFAu+0ou3xBBYbx9zUURQz4G9fAq0ScgBOs/bpzcRrFb4mLpemUViBAd47tfZKdH4MAX45KVQ==", "dev": true, "dependencies": { "ts-dedent": "^2.0.0" @@ -6573,9 +6573,9 @@ } }, "node_modules/@storybook/cli/node_modules/@storybook/csf-tools": { - "version": "7.6.19", - "resolved": "https://registry.npmjs.org/@storybook/csf-tools/-/csf-tools-7.6.19.tgz", - "integrity": "sha512-8Vzia3cHhDdGHuS3XKXJReCRxmfRq3vmTm/Te9yKZnPSAsC58CCKcMh8FNEFJ44vxYF9itKTkRutjGs+DprKLQ==", + "version": "7.6.20", + "resolved": "https://registry.npmjs.org/@storybook/csf-tools/-/csf-tools-7.6.20.tgz", + "integrity": "sha512-rwcwzCsAYh/m/WYcxBiEtLpIW5OH1ingxNdF/rK9mtGWhJxXRDV8acPkFrF8rtFWIVKoOCXu5USJYmc3f2gdYQ==", "dev": true, "dependencies": { "@babel/generator": "^7.23.0", @@ -6583,7 +6583,7 @@ "@babel/traverse": "^7.23.2", "@babel/types": "^7.23.0", "@storybook/csf": "^0.1.2", - "@storybook/types": "7.6.19", + "@storybook/types": "7.6.20", "fs-extra": "^11.1.0", "recast": "^0.23.1", "ts-dedent": "^2.0.0" @@ -6594,9 +6594,9 @@ } }, "node_modules/@storybook/cli/node_modules/@storybook/node-logger": { - "version": "7.6.19", - "resolved": "https://registry.npmjs.org/@storybook/node-logger/-/node-logger-7.6.19.tgz", - "integrity": "sha512-2g29QC44Zl1jKY37DmQ0/dO7+VSKnGgPI/x0mwVwQffypSapxH3rwLLT5Q5XLHeFyD+fhRu5w9Cj4vTGynJgpA==", + "version": "7.6.20", + "resolved": "https://registry.npmjs.org/@storybook/node-logger/-/node-logger-7.6.20.tgz", + "integrity": "sha512-l2i4qF1bscJkOplNffcRTsgQWYR7J51ewmizj5YrTM8BK6rslWT1RntgVJWB1RgPqvx6VsCz1gyP3yW1oKxvYw==", "dev": true, "funding": { "type": "opencollective", @@ -6604,12 +6604,12 @@ } }, "node_modules/@storybook/cli/node_modules/@storybook/types": { - "version": "7.6.19", - "resolved": "https://registry.npmjs.org/@storybook/types/-/types-7.6.19.tgz", - "integrity": "sha512-DeGYrRPRMGTVfT7o2rEZtRzyLT2yKTI2exgpnxbwPWEFAduZCSfzBrcBXZ/nb5B0pjA9tUNWls1YzGkJGlkhpg==", + "version": "7.6.20", + "resolved": "https://registry.npmjs.org/@storybook/types/-/types-7.6.20.tgz", + "integrity": "sha512-GncdY3x0LpbhmUAAJwXYtJDUQEwfF175gsjH0/fxPkxPoV7Sef9TM41jQLJW/5+6TnZoCZP/+aJZTJtq3ni23Q==", "dev": true, "dependencies": { - "@storybook/channels": "7.6.19", + "@storybook/channels": "7.6.20", "@types/babel__core": "^7.0.0", "@types/express": "^4.7.0", "file-system-cache": "2.3.0" @@ -6703,18 +6703,18 @@ } }, "node_modules/@storybook/codemod": { - "version": "7.6.19", - "resolved": "https://registry.npmjs.org/@storybook/codemod/-/codemod-7.6.19.tgz", - "integrity": "sha512-bmHE0iEEgWZ65dXCmasd+GreChjPiWkXu2FEa0cJmNz/PqY12GsXGls4ke1TkNTj4gdSZnbtJxbclPZZnib2tQ==", + "version": "7.6.20", + "resolved": "https://registry.npmjs.org/@storybook/codemod/-/codemod-7.6.20.tgz", + "integrity": "sha512-8vmSsksO4XukNw0TmqylPmk7PxnfNfE21YsxFa7mnEBmEKQcZCQsNil4ZgWfG0IzdhTfhglAN4r++Ew0WE+PYA==", "dev": true, "dependencies": { "@babel/core": "^7.23.2", "@babel/preset-env": "^7.23.2", "@babel/types": "^7.23.0", "@storybook/csf": "^0.1.2", - "@storybook/csf-tools": "7.6.19", - "@storybook/node-logger": "7.6.19", - "@storybook/types": "7.6.19", + "@storybook/csf-tools": "7.6.20", + "@storybook/node-logger": "7.6.20", + "@storybook/types": "7.6.20", "@types/cross-spawn": "^6.0.2", "cross-spawn": "^7.0.3", "globby": "^11.0.2", @@ -6729,13 +6729,13 @@ } }, "node_modules/@storybook/codemod/node_modules/@storybook/channels": { - "version": "7.6.19", - "resolved": "https://registry.npmjs.org/@storybook/channels/-/channels-7.6.19.tgz", - "integrity": "sha512-2JGh+i95GwjtjqWqhtEh15jM5ifwbRGmXeFqkY7dpdHH50EEWafYHr2mg3opK3heVDwg0rJ/VBptkmshloXuvA==", + "version": "7.6.20", + "resolved": "https://registry.npmjs.org/@storybook/channels/-/channels-7.6.20.tgz", + "integrity": "sha512-4hkgPSH6bJclB2OvLnkZOGZW1WptJs09mhQ6j6qLjgBZzL/ZdD6priWSd7iXrmPiN5TzUobkG4P4Dp7FjkiO7A==", "dev": true, "dependencies": { - "@storybook/client-logger": "7.6.19", - "@storybook/core-events": "7.6.19", + "@storybook/client-logger": "7.6.20", + "@storybook/core-events": "7.6.20", "@storybook/global": "^5.0.0", "qs": "^6.10.0", "telejson": "^7.2.0", @@ -6747,9 +6747,9 @@ } }, "node_modules/@storybook/codemod/node_modules/@storybook/client-logger": { - "version": "7.6.19", - "resolved": "https://registry.npmjs.org/@storybook/client-logger/-/client-logger-7.6.19.tgz", - "integrity": "sha512-oGzOxbmLmciSIfd5gsxDzPmX8DttWhoYdPKxjMuCuWLTO2TWpkCWp1FTUMWO72mm/6V/FswT/aqpJJBBvdZ3RQ==", + "version": "7.6.20", + "resolved": "https://registry.npmjs.org/@storybook/client-logger/-/client-logger-7.6.20.tgz", + "integrity": "sha512-NwG0VIJQCmKrSaN5GBDFyQgTAHLNishUPLW1NrzqTDNAhfZUoef64rPQlinbopa0H4OXmlB+QxbQIb3ubeXmSQ==", "dev": true, "dependencies": { "@storybook/global": "^5.0.0" @@ -6760,9 +6760,9 @@ } }, "node_modules/@storybook/codemod/node_modules/@storybook/core-events": { - "version": "7.6.19", - "resolved": "https://registry.npmjs.org/@storybook/core-events/-/core-events-7.6.19.tgz", - "integrity": "sha512-K/W6Uvum0ocZSgjbi8hiotpe+wDEHDZlvN+KlPqdh9ae9xDK8aBNBq9IelCoqM+uKO1Zj+dDfSQds7CD781DJg==", + "version": "7.6.20", + "resolved": "https://registry.npmjs.org/@storybook/core-events/-/core-events-7.6.20.tgz", + "integrity": "sha512-tlVDuVbDiNkvPDFAu+0ou3xBBYbx9zUURQz4G9fAq0ScgBOs/bpzcRrFb4mLpemUViBAd47tfZKdH4MAX45KVQ==", "dev": true, "dependencies": { "ts-dedent": "^2.0.0" @@ -6773,9 +6773,9 @@ } }, "node_modules/@storybook/codemod/node_modules/@storybook/csf-tools": { - "version": "7.6.19", - "resolved": "https://registry.npmjs.org/@storybook/csf-tools/-/csf-tools-7.6.19.tgz", - "integrity": "sha512-8Vzia3cHhDdGHuS3XKXJReCRxmfRq3vmTm/Te9yKZnPSAsC58CCKcMh8FNEFJ44vxYF9itKTkRutjGs+DprKLQ==", + "version": "7.6.20", + "resolved": "https://registry.npmjs.org/@storybook/csf-tools/-/csf-tools-7.6.20.tgz", + "integrity": "sha512-rwcwzCsAYh/m/WYcxBiEtLpIW5OH1ingxNdF/rK9mtGWhJxXRDV8acPkFrF8rtFWIVKoOCXu5USJYmc3f2gdYQ==", "dev": true, "dependencies": { "@babel/generator": "^7.23.0", @@ -6783,7 +6783,7 @@ "@babel/traverse": "^7.23.2", "@babel/types": "^7.23.0", "@storybook/csf": "^0.1.2", - "@storybook/types": "7.6.19", + "@storybook/types": "7.6.20", "fs-extra": "^11.1.0", "recast": "^0.23.1", "ts-dedent": "^2.0.0" @@ -6794,9 +6794,9 @@ } }, "node_modules/@storybook/codemod/node_modules/@storybook/node-logger": { - "version": "7.6.19", - "resolved": "https://registry.npmjs.org/@storybook/node-logger/-/node-logger-7.6.19.tgz", - "integrity": "sha512-2g29QC44Zl1jKY37DmQ0/dO7+VSKnGgPI/x0mwVwQffypSapxH3rwLLT5Q5XLHeFyD+fhRu5w9Cj4vTGynJgpA==", + "version": "7.6.20", + "resolved": "https://registry.npmjs.org/@storybook/node-logger/-/node-logger-7.6.20.tgz", + "integrity": "sha512-l2i4qF1bscJkOplNffcRTsgQWYR7J51ewmizj5YrTM8BK6rslWT1RntgVJWB1RgPqvx6VsCz1gyP3yW1oKxvYw==", "dev": true, "funding": { "type": "opencollective", @@ -6804,12 +6804,12 @@ } }, "node_modules/@storybook/codemod/node_modules/@storybook/types": { - "version": "7.6.19", - "resolved": "https://registry.npmjs.org/@storybook/types/-/types-7.6.19.tgz", - "integrity": "sha512-DeGYrRPRMGTVfT7o2rEZtRzyLT2yKTI2exgpnxbwPWEFAduZCSfzBrcBXZ/nb5B0pjA9tUNWls1YzGkJGlkhpg==", + "version": "7.6.20", + "resolved": "https://registry.npmjs.org/@storybook/types/-/types-7.6.20.tgz", + "integrity": "sha512-GncdY3x0LpbhmUAAJwXYtJDUQEwfF175gsjH0/fxPkxPoV7Sef9TM41jQLJW/5+6TnZoCZP/+aJZTJtq3ni23Q==", "dev": true, "dependencies": { - "@storybook/channels": "7.6.19", + "@storybook/channels": "7.6.20", "@types/babel__core": "^7.0.0", "@types/express": "^4.7.0", "file-system-cache": "2.3.0" @@ -7063,26 +7063,26 @@ } }, "node_modules/@storybook/core-server": { - "version": "7.6.19", - "resolved": "https://registry.npmjs.org/@storybook/core-server/-/core-server-7.6.19.tgz", - "integrity": "sha512-7mKL73Wv5R2bEl0kJ6QJ9bOu5YY53Idu24QgvTnUdNsQazp2yUONBNwHIrNDnNEXm8SfCi4Mc9o0mmNRMIoiRA==", + "version": "7.6.20", + "resolved": "https://registry.npmjs.org/@storybook/core-server/-/core-server-7.6.20.tgz", + "integrity": "sha512-qC5BdbqqwMLTdCwMKZ1Hbc3+3AaxHYWLiJaXL9e8s8nJw89xV8c8l30QpbJOGvcDmsgY6UTtXYaJ96OsTr7MrA==", "dev": true, "dependencies": { "@aw-web-design/x-default-browser": "1.4.126", "@discoveryjs/json-ext": "^0.5.3", - "@storybook/builder-manager": "7.6.19", - "@storybook/channels": "7.6.19", - "@storybook/core-common": "7.6.19", - "@storybook/core-events": "7.6.19", + "@storybook/builder-manager": "7.6.20", + "@storybook/channels": "7.6.20", + "@storybook/core-common": "7.6.20", + "@storybook/core-events": "7.6.20", "@storybook/csf": "^0.1.2", - "@storybook/csf-tools": "7.6.19", + "@storybook/csf-tools": "7.6.20", "@storybook/docs-mdx": "^0.1.0", "@storybook/global": "^5.0.0", - "@storybook/manager": "7.6.19", - "@storybook/node-logger": "7.6.19", - "@storybook/preview-api": "7.6.19", - "@storybook/telemetry": "7.6.19", - "@storybook/types": "7.6.19", + "@storybook/manager": "7.6.20", + "@storybook/node-logger": "7.6.20", + "@storybook/preview-api": "7.6.20", + "@storybook/telemetry": "7.6.20", + "@storybook/types": "7.6.20", "@types/detect-port": "^1.3.0", "@types/node": "^18.0.0", "@types/pretty-hrtime": "^1.0.0", @@ -7095,7 +7095,6 @@ "express": "^4.17.3", "fs-extra": "^11.1.0", "globby": "^11.0.2", - "ip": "^2.0.1", "lodash": "^4.17.21", "open": "^8.4.0", "pretty-hrtime": "^1.0.3", @@ -7116,13 +7115,13 @@ } }, "node_modules/@storybook/core-server/node_modules/@storybook/channels": { - "version": "7.6.19", - "resolved": "https://registry.npmjs.org/@storybook/channels/-/channels-7.6.19.tgz", - "integrity": "sha512-2JGh+i95GwjtjqWqhtEh15jM5ifwbRGmXeFqkY7dpdHH50EEWafYHr2mg3opK3heVDwg0rJ/VBptkmshloXuvA==", + "version": "7.6.20", + "resolved": "https://registry.npmjs.org/@storybook/channels/-/channels-7.6.20.tgz", + "integrity": "sha512-4hkgPSH6bJclB2OvLnkZOGZW1WptJs09mhQ6j6qLjgBZzL/ZdD6priWSd7iXrmPiN5TzUobkG4P4Dp7FjkiO7A==", "dev": true, "dependencies": { - "@storybook/client-logger": "7.6.19", - "@storybook/core-events": "7.6.19", + "@storybook/client-logger": "7.6.20", + "@storybook/core-events": "7.6.20", "@storybook/global": "^5.0.0", "qs": "^6.10.0", "telejson": "^7.2.0", @@ -7134,9 +7133,9 @@ } }, "node_modules/@storybook/core-server/node_modules/@storybook/client-logger": { - "version": "7.6.19", - "resolved": "https://registry.npmjs.org/@storybook/client-logger/-/client-logger-7.6.19.tgz", - "integrity": "sha512-oGzOxbmLmciSIfd5gsxDzPmX8DttWhoYdPKxjMuCuWLTO2TWpkCWp1FTUMWO72mm/6V/FswT/aqpJJBBvdZ3RQ==", + "version": "7.6.20", + "resolved": "https://registry.npmjs.org/@storybook/client-logger/-/client-logger-7.6.20.tgz", + "integrity": "sha512-NwG0VIJQCmKrSaN5GBDFyQgTAHLNishUPLW1NrzqTDNAhfZUoef64rPQlinbopa0H4OXmlB+QxbQIb3ubeXmSQ==", "dev": true, "dependencies": { "@storybook/global": "^5.0.0" @@ -7147,14 +7146,14 @@ } }, "node_modules/@storybook/core-server/node_modules/@storybook/core-common": { - "version": "7.6.19", - "resolved": "https://registry.npmjs.org/@storybook/core-common/-/core-common-7.6.19.tgz", - "integrity": "sha512-njwpGzFJrfbJr/AFxGP8KMrfPfxN85KOfSlxYnQwRm5Z0H1D/lT33LhEBf5m37gaGawHeG7KryxO6RvaioMt2Q==", + "version": "7.6.20", + "resolved": "https://registry.npmjs.org/@storybook/core-common/-/core-common-7.6.20.tgz", + "integrity": "sha512-8H1zPWPjcmeD4HbDm4FDD0WLsfAKGVr566IZ4hG+h3iWVW57II9JW9MLBtiR2LPSd8u7o0kw64lwRGmtCO1qAw==", "dev": true, "dependencies": { - "@storybook/core-events": "7.6.19", - "@storybook/node-logger": "7.6.19", - "@storybook/types": "7.6.19", + "@storybook/core-events": "7.6.20", + "@storybook/node-logger": "7.6.20", + "@storybook/types": "7.6.20", "@types/find-cache-dir": "^3.2.1", "@types/node": "^18.0.0", "@types/node-fetch": "^2.6.4", @@ -7182,9 +7181,9 @@ } }, "node_modules/@storybook/core-server/node_modules/@storybook/core-events": { - "version": "7.6.19", - "resolved": "https://registry.npmjs.org/@storybook/core-events/-/core-events-7.6.19.tgz", - "integrity": "sha512-K/W6Uvum0ocZSgjbi8hiotpe+wDEHDZlvN+KlPqdh9ae9xDK8aBNBq9IelCoqM+uKO1Zj+dDfSQds7CD781DJg==", + "version": "7.6.20", + "resolved": "https://registry.npmjs.org/@storybook/core-events/-/core-events-7.6.20.tgz", + "integrity": "sha512-tlVDuVbDiNkvPDFAu+0ou3xBBYbx9zUURQz4G9fAq0ScgBOs/bpzcRrFb4mLpemUViBAd47tfZKdH4MAX45KVQ==", "dev": true, "dependencies": { "ts-dedent": "^2.0.0" @@ -7195,9 +7194,9 @@ } }, "node_modules/@storybook/core-server/node_modules/@storybook/csf-tools": { - "version": "7.6.19", - "resolved": "https://registry.npmjs.org/@storybook/csf-tools/-/csf-tools-7.6.19.tgz", - "integrity": "sha512-8Vzia3cHhDdGHuS3XKXJReCRxmfRq3vmTm/Te9yKZnPSAsC58CCKcMh8FNEFJ44vxYF9itKTkRutjGs+DprKLQ==", + "version": "7.6.20", + "resolved": "https://registry.npmjs.org/@storybook/csf-tools/-/csf-tools-7.6.20.tgz", + "integrity": "sha512-rwcwzCsAYh/m/WYcxBiEtLpIW5OH1ingxNdF/rK9mtGWhJxXRDV8acPkFrF8rtFWIVKoOCXu5USJYmc3f2gdYQ==", "dev": true, "dependencies": { "@babel/generator": "^7.23.0", @@ -7205,7 +7204,7 @@ "@babel/traverse": "^7.23.2", "@babel/types": "^7.23.0", "@storybook/csf": "^0.1.2", - "@storybook/types": "7.6.19", + "@storybook/types": "7.6.20", "fs-extra": "^11.1.0", "recast": "^0.23.1", "ts-dedent": "^2.0.0" @@ -7216,9 +7215,9 @@ } }, "node_modules/@storybook/core-server/node_modules/@storybook/node-logger": { - "version": "7.6.19", - "resolved": "https://registry.npmjs.org/@storybook/node-logger/-/node-logger-7.6.19.tgz", - "integrity": "sha512-2g29QC44Zl1jKY37DmQ0/dO7+VSKnGgPI/x0mwVwQffypSapxH3rwLLT5Q5XLHeFyD+fhRu5w9Cj4vTGynJgpA==", + "version": "7.6.20", + "resolved": "https://registry.npmjs.org/@storybook/node-logger/-/node-logger-7.6.20.tgz", + "integrity": "sha512-l2i4qF1bscJkOplNffcRTsgQWYR7J51ewmizj5YrTM8BK6rslWT1RntgVJWB1RgPqvx6VsCz1gyP3yW1oKxvYw==", "dev": true, "funding": { "type": "opencollective", @@ -7226,17 +7225,17 @@ } }, "node_modules/@storybook/core-server/node_modules/@storybook/preview-api": { - "version": "7.6.19", - "resolved": "https://registry.npmjs.org/@storybook/preview-api/-/preview-api-7.6.19.tgz", - "integrity": "sha512-04hdMSQucroJT4dBjQzRd7ZwH2hij8yx2nm5qd4HYGkd1ORkvlH6GOLph4XewNJl5Um3xfzFQzBhvkqvG0WaCQ==", + "version": "7.6.20", + "resolved": "https://registry.npmjs.org/@storybook/preview-api/-/preview-api-7.6.20.tgz", + "integrity": "sha512-3ic2m9LDZEPwZk02wIhNc3n3rNvbi7VDKn52hDXfAxnL5EYm7yDICAkaWcVaTfblru2zn0EDJt7ROpthscTW5w==", "dev": true, "dependencies": { - "@storybook/channels": "7.6.19", - "@storybook/client-logger": "7.6.19", - "@storybook/core-events": "7.6.19", + "@storybook/channels": "7.6.20", + "@storybook/client-logger": "7.6.20", + "@storybook/core-events": "7.6.20", "@storybook/csf": "^0.1.2", "@storybook/global": "^5.0.0", - "@storybook/types": "7.6.19", + "@storybook/types": "7.6.20", "@types/qs": "^6.9.5", "dequal": "^2.0.2", "lodash": "^4.17.21", @@ -7252,12 +7251,12 @@ } }, "node_modules/@storybook/core-server/node_modules/@storybook/types": { - "version": "7.6.19", - "resolved": "https://registry.npmjs.org/@storybook/types/-/types-7.6.19.tgz", - "integrity": "sha512-DeGYrRPRMGTVfT7o2rEZtRzyLT2yKTI2exgpnxbwPWEFAduZCSfzBrcBXZ/nb5B0pjA9tUNWls1YzGkJGlkhpg==", + "version": "7.6.20", + "resolved": "https://registry.npmjs.org/@storybook/types/-/types-7.6.20.tgz", + "integrity": "sha512-GncdY3x0LpbhmUAAJwXYtJDUQEwfF175gsjH0/fxPkxPoV7Sef9TM41jQLJW/5+6TnZoCZP/+aJZTJtq3ni23Q==", "dev": true, "dependencies": { - "@storybook/channels": "7.6.19", + "@storybook/channels": "7.6.20", "@types/babel__core": "^7.0.0", "@types/express": "^4.7.0", "file-system-cache": "2.3.0" @@ -7372,9 +7371,9 @@ "dev": true }, "node_modules/@storybook/manager": { - "version": "7.6.19", - "resolved": "https://registry.npmjs.org/@storybook/manager/-/manager-7.6.19.tgz", - "integrity": "sha512-fZWQcf59x4P0iiBhrL74PZrqKJAPuk9sWjP8BIkGbf8wTZtUunbY5Sv4225fOL4NLJbuX9/RYLUPoxQ3nucGHA==", + "version": "7.6.20", + "resolved": "https://registry.npmjs.org/@storybook/manager/-/manager-7.6.20.tgz", + "integrity": "sha512-0Cf6WN0t7yEG2DR29tN5j+i7H/TH5EfPppg9h9/KiQSoFHk+6KLoy2p5do94acFU+Ro4+zzxvdCGbcYGKuArpg==", "dev": true, "funding": { "type": "opencollective", @@ -7810,14 +7809,14 @@ } }, "node_modules/@storybook/telemetry": { - "version": "7.6.19", - "resolved": "https://registry.npmjs.org/@storybook/telemetry/-/telemetry-7.6.19.tgz", - "integrity": "sha512-rA5xum4I36M57iiD3uzmW0MOdpl0vEpHWBSAa5hK0a0ALPeY9TgAsQlI/0dSyNYJ/K7aczEEN6d4qm1NC4u10A==", + "version": "7.6.20", + "resolved": "https://registry.npmjs.org/@storybook/telemetry/-/telemetry-7.6.20.tgz", + "integrity": "sha512-dmAOCWmOscYN6aMbhCMmszQjoycg7tUPRVy2kTaWg6qX10wtMrvEtBV29W4eMvqdsoRj5kcvoNbzRdYcWBUOHQ==", "dev": true, "dependencies": { - "@storybook/client-logger": "7.6.19", - "@storybook/core-common": "7.6.19", - "@storybook/csf-tools": "7.6.19", + "@storybook/client-logger": "7.6.20", + "@storybook/core-common": "7.6.20", + "@storybook/csf-tools": "7.6.20", "chalk": "^4.1.0", "detect-package-manager": "^2.0.1", "fetch-retry": "^5.0.2", @@ -7830,13 +7829,13 @@ } }, "node_modules/@storybook/telemetry/node_modules/@storybook/channels": { - "version": "7.6.19", - "resolved": "https://registry.npmjs.org/@storybook/channels/-/channels-7.6.19.tgz", - "integrity": "sha512-2JGh+i95GwjtjqWqhtEh15jM5ifwbRGmXeFqkY7dpdHH50EEWafYHr2mg3opK3heVDwg0rJ/VBptkmshloXuvA==", + "version": "7.6.20", + "resolved": "https://registry.npmjs.org/@storybook/channels/-/channels-7.6.20.tgz", + "integrity": "sha512-4hkgPSH6bJclB2OvLnkZOGZW1WptJs09mhQ6j6qLjgBZzL/ZdD6priWSd7iXrmPiN5TzUobkG4P4Dp7FjkiO7A==", "dev": true, "dependencies": { - "@storybook/client-logger": "7.6.19", - "@storybook/core-events": "7.6.19", + "@storybook/client-logger": "7.6.20", + "@storybook/core-events": "7.6.20", "@storybook/global": "^5.0.0", "qs": "^6.10.0", "telejson": "^7.2.0", @@ -7848,9 +7847,9 @@ } }, "node_modules/@storybook/telemetry/node_modules/@storybook/client-logger": { - "version": "7.6.19", - "resolved": "https://registry.npmjs.org/@storybook/client-logger/-/client-logger-7.6.19.tgz", - "integrity": "sha512-oGzOxbmLmciSIfd5gsxDzPmX8DttWhoYdPKxjMuCuWLTO2TWpkCWp1FTUMWO72mm/6V/FswT/aqpJJBBvdZ3RQ==", + "version": "7.6.20", + "resolved": "https://registry.npmjs.org/@storybook/client-logger/-/client-logger-7.6.20.tgz", + "integrity": "sha512-NwG0VIJQCmKrSaN5GBDFyQgTAHLNishUPLW1NrzqTDNAhfZUoef64rPQlinbopa0H4OXmlB+QxbQIb3ubeXmSQ==", "dev": true, "dependencies": { "@storybook/global": "^5.0.0" @@ -7861,14 +7860,14 @@ } }, "node_modules/@storybook/telemetry/node_modules/@storybook/core-common": { - "version": "7.6.19", - "resolved": "https://registry.npmjs.org/@storybook/core-common/-/core-common-7.6.19.tgz", - "integrity": "sha512-njwpGzFJrfbJr/AFxGP8KMrfPfxN85KOfSlxYnQwRm5Z0H1D/lT33LhEBf5m37gaGawHeG7KryxO6RvaioMt2Q==", + "version": "7.6.20", + "resolved": "https://registry.npmjs.org/@storybook/core-common/-/core-common-7.6.20.tgz", + "integrity": "sha512-8H1zPWPjcmeD4HbDm4FDD0WLsfAKGVr566IZ4hG+h3iWVW57II9JW9MLBtiR2LPSd8u7o0kw64lwRGmtCO1qAw==", "dev": true, "dependencies": { - "@storybook/core-events": "7.6.19", - "@storybook/node-logger": "7.6.19", - "@storybook/types": "7.6.19", + "@storybook/core-events": "7.6.20", + "@storybook/node-logger": "7.6.20", + "@storybook/types": "7.6.20", "@types/find-cache-dir": "^3.2.1", "@types/node": "^18.0.0", "@types/node-fetch": "^2.6.4", @@ -7896,9 +7895,9 @@ } }, "node_modules/@storybook/telemetry/node_modules/@storybook/core-events": { - "version": "7.6.19", - "resolved": "https://registry.npmjs.org/@storybook/core-events/-/core-events-7.6.19.tgz", - "integrity": "sha512-K/W6Uvum0ocZSgjbi8hiotpe+wDEHDZlvN+KlPqdh9ae9xDK8aBNBq9IelCoqM+uKO1Zj+dDfSQds7CD781DJg==", + "version": "7.6.20", + "resolved": "https://registry.npmjs.org/@storybook/core-events/-/core-events-7.6.20.tgz", + "integrity": "sha512-tlVDuVbDiNkvPDFAu+0ou3xBBYbx9zUURQz4G9fAq0ScgBOs/bpzcRrFb4mLpemUViBAd47tfZKdH4MAX45KVQ==", "dev": true, "dependencies": { "ts-dedent": "^2.0.0" @@ -7909,9 +7908,9 @@ } }, "node_modules/@storybook/telemetry/node_modules/@storybook/csf-tools": { - "version": "7.6.19", - "resolved": "https://registry.npmjs.org/@storybook/csf-tools/-/csf-tools-7.6.19.tgz", - "integrity": "sha512-8Vzia3cHhDdGHuS3XKXJReCRxmfRq3vmTm/Te9yKZnPSAsC58CCKcMh8FNEFJ44vxYF9itKTkRutjGs+DprKLQ==", + "version": "7.6.20", + "resolved": "https://registry.npmjs.org/@storybook/csf-tools/-/csf-tools-7.6.20.tgz", + "integrity": "sha512-rwcwzCsAYh/m/WYcxBiEtLpIW5OH1ingxNdF/rK9mtGWhJxXRDV8acPkFrF8rtFWIVKoOCXu5USJYmc3f2gdYQ==", "dev": true, "dependencies": { "@babel/generator": "^7.23.0", @@ -7919,7 +7918,7 @@ "@babel/traverse": "^7.23.2", "@babel/types": "^7.23.0", "@storybook/csf": "^0.1.2", - "@storybook/types": "7.6.19", + "@storybook/types": "7.6.20", "fs-extra": "^11.1.0", "recast": "^0.23.1", "ts-dedent": "^2.0.0" @@ -7930,9 +7929,9 @@ } }, "node_modules/@storybook/telemetry/node_modules/@storybook/node-logger": { - "version": "7.6.19", - "resolved": "https://registry.npmjs.org/@storybook/node-logger/-/node-logger-7.6.19.tgz", - "integrity": "sha512-2g29QC44Zl1jKY37DmQ0/dO7+VSKnGgPI/x0mwVwQffypSapxH3rwLLT5Q5XLHeFyD+fhRu5w9Cj4vTGynJgpA==", + "version": "7.6.20", + "resolved": "https://registry.npmjs.org/@storybook/node-logger/-/node-logger-7.6.20.tgz", + "integrity": "sha512-l2i4qF1bscJkOplNffcRTsgQWYR7J51ewmizj5YrTM8BK6rslWT1RntgVJWB1RgPqvx6VsCz1gyP3yW1oKxvYw==", "dev": true, "funding": { "type": "opencollective", @@ -7940,12 +7939,12 @@ } }, "node_modules/@storybook/telemetry/node_modules/@storybook/types": { - "version": "7.6.19", - "resolved": "https://registry.npmjs.org/@storybook/types/-/types-7.6.19.tgz", - "integrity": "sha512-DeGYrRPRMGTVfT7o2rEZtRzyLT2yKTI2exgpnxbwPWEFAduZCSfzBrcBXZ/nb5B0pjA9tUNWls1YzGkJGlkhpg==", + "version": "7.6.20", + "resolved": "https://registry.npmjs.org/@storybook/types/-/types-7.6.20.tgz", + "integrity": "sha512-GncdY3x0LpbhmUAAJwXYtJDUQEwfF175gsjH0/fxPkxPoV7Sef9TM41jQLJW/5+6TnZoCZP/+aJZTJtq3ni23Q==", "dev": true, "dependencies": { - "@storybook/channels": "7.6.19", + "@storybook/channels": "7.6.20", "@types/babel__core": "^7.0.0", "@types/express": "^4.7.0", "file-system-cache": "2.3.0" @@ -10040,12 +10039,13 @@ } }, "node_modules/axios": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/axios/-/axios-0.27.2.tgz", - "integrity": "sha512-t+yRIyySRTp/wua5xEr+z1q60QmLq8ABsS5O9Me1AsE5dfKqgnCFzwiCZZ/cGNd1lq4/7akDWMxdhVlucjmnOQ==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.28.0.tgz", + "integrity": "sha512-Tu7NYoGY4Yoc7I+Npf9HhUMtEEpV7ZiLH9yndTCoNhcpBH0kwcvFbzYN9/u5QKI5A6uefjsNNWaz5olJVYS62Q==", "dependencies": { - "follow-redirects": "^1.14.9", - "form-data": "^4.0.0" + "follow-redirects": "^1.15.0", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" } }, "node_modules/axios-auth-refresh": { @@ -10056,6 +10056,11 @@ "axios": ">= 0.18 < 0.19.0 || >= 0.19.1" } }, + "node_modules/axios/node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" + }, "node_modules/axobject-query": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-3.2.1.tgz", @@ -11451,6 +11456,12 @@ "safe-buffer": "~5.1.0" } }, + "node_modules/confbox": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.1.7.tgz", + "integrity": "sha512-uJcB/FKZtBMCJpK8MQji6bJHgu1tixKPxRLeGkNzBoOZzpnZUJm0jm2/sBDWcuBx1dYgxV4JU+g5hmNxCyAmdA==", + "dev": true + }, "node_modules/confusing-browser-globals": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/confusing-browser-globals/-/confusing-browser-globals-1.0.11.tgz", @@ -14389,9 +14400,9 @@ "dev": true }, "node_modules/flow-parser": { - "version": "0.237.2", - "resolved": "https://registry.npmjs.org/flow-parser/-/flow-parser-0.237.2.tgz", - "integrity": "sha512-mvI/kdfr3l1waaPbThPA8dJa77nHXrfZIun+SWvFwSwDjmeByU7mGJGRmv1+7guU6ccyLV8e1lqZA1lD4iMGnQ==", + "version": "0.239.1", + "resolved": "https://registry.npmjs.org/flow-parser/-/flow-parser-0.239.1.tgz", + "integrity": "sha512-topOrETNxJ6T2gAnQiWqAlzGPj8uI2wtmNOlDIMNB+qyvGJZ6R++STbUOTAYmvPhOMz2gXnXPH0hOvURYmrBow==", "dev": true, "engines": { "node": ">=0.4.0" @@ -18147,6 +18158,30 @@ "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", "dev": true }, + "node_modules/mlly": { + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.7.1.tgz", + "integrity": "sha512-rrVRZRELyQzrIUAVMHxP97kv+G786pHmOKzuFII8zDYahFBS7qnHh2AlYSl1GAHhaMPCz6/oHjVMcfFYgFYHgA==", + "dev": true, + "dependencies": { + "acorn": "^8.11.3", + "pathe": "^1.1.2", + "pkg-types": "^1.1.1", + "ufo": "^1.5.3" + } + }, + "node_modules/mlly/node_modules/acorn": { + "version": "8.12.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.1.tgz", + "integrity": "sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==", + "dev": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/mri": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz", @@ -18621,16 +18656,17 @@ } }, "node_modules/nypm": { - "version": "0.3.8", - "resolved": "https://registry.npmjs.org/nypm/-/nypm-0.3.8.tgz", - "integrity": "sha512-IGWlC6So2xv6V4cIDmoV0SwwWx7zLG086gyqkyumteH2fIgCAM4nDVFB2iDRszDvmdSVW9xb1N+2KjQ6C7d4og==", + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/nypm/-/nypm-0.3.9.tgz", + "integrity": "sha512-BI2SdqqTHg2d4wJh8P9A1W+bslg33vOE9IZDY6eR2QC+Pu1iNBVZUqczrd43rJb+fMzHU7ltAYKsEFY/kHMFcw==", "dev": true, "dependencies": { "citty": "^0.1.6", "consola": "^3.2.3", "execa": "^8.0.1", "pathe": "^1.1.2", - "ufo": "^1.4.0" + "pkg-types": "^1.1.1", + "ufo": "^1.5.3" }, "bin": { "nypm": "dist/cli.mjs" @@ -19360,9 +19396,9 @@ "devOptional": true }, "node_modules/picocolors": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", - "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==" + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.1.tgz", + "integrity": "sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==" }, "node_modules/picomatch": { "version": "2.3.1", @@ -19405,6 +19441,17 @@ "node": ">=10" } }, + "node_modules/pkg-types": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.1.3.tgz", + "integrity": "sha512-+JrgthZG6m3ckicaOB74TwQ+tBWsFl3qVQg7mN8ulwSOElJ7gBhKzj2VkCPnZ4NlF6kEquYU+RIYNVAvzd54UA==", + "dev": true, + "dependencies": { + "confbox": "^0.1.7", + "mlly": "^1.7.1", + "pathe": "^1.1.2" + } + }, "node_modules/pnp-webpack-plugin": { "version": "1.7.0", "resolved": "https://registry.npmjs.org/pnp-webpack-plugin/-/pnp-webpack-plugin-1.7.0.tgz", @@ -19441,9 +19488,9 @@ } }, "node_modules/postcss": { - "version": "8.4.33", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.33.tgz", - "integrity": "sha512-Kkpbhhdjw2qQs2O2DGX+8m5OVqEcbB9HRBvuYM9pgrjEFUg30A9LmXNlTAUj4S9kgtGyrMbTzVjH7E+s5Re2yg==", + "version": "8.4.39", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.39.tgz", + "integrity": "sha512-0vzE+lAiG7hZl1/9I8yzKLx3aR9Xbof3fBHKunvMfOCYAtMhrsnccJY2iTURb9EZd5+pLuiNV9/c/GZJOHsgIw==", "funding": [ { "type": "opencollective", @@ -19460,8 +19507,8 @@ ], "dependencies": { "nanoid": "^3.3.7", - "picocolors": "^1.0.0", - "source-map-js": "^1.0.2" + "picocolors": "^1.0.1", + "source-map-js": "^1.2.0" }, "engines": { "node": "^10 || ^12 || >=14" @@ -20265,9 +20312,9 @@ } }, "node_modules/puppeteer-core/node_modules/ws": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/ws/-/ws-6.2.2.tgz", - "integrity": "sha512-zmhltoSR8u1cnDsD43TX59mzoMZsLKqUweyYBAIvTngR3shc0W6aOZylZmq/7hqyVxPdi+5Ud2QInblgyE72fw==", + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-6.2.3.tgz", + "integrity": "sha512-jmTjYU0j60B+vHey6TfR3Z7RD61z/hmxBS3VMSGIrroOWXQEneK1zNuotOUrGyBHQj0yrpsLHPWtigEFd13ndA==", "dev": true, "dependencies": { "async-limiter": "~1.0.0" @@ -22254,9 +22301,9 @@ } }, "node_modules/source-map-js": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", - "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz", + "integrity": "sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==", "engines": { "node": ">=0.10.0" } @@ -22420,12 +22467,12 @@ "dev": true }, "node_modules/storybook": { - "version": "7.6.19", - "resolved": "https://registry.npmjs.org/storybook/-/storybook-7.6.19.tgz", - "integrity": "sha512-xWD1C4vD/4KMffCrBBrUpsLUO/9uNpm8BVW8+Vcb30gkQDfficZ0oziWkmLexpT53VSioa24iazGXMwBqllYjQ==", + "version": "7.6.20", + "resolved": "https://registry.npmjs.org/storybook/-/storybook-7.6.20.tgz", + "integrity": "sha512-Wt04pPTO71pwmRmsgkyZhNo4Bvdb/1pBAMsIFb9nQLykEdzzpXjvingxFFvdOG4nIowzwgxD+CLlyRqVJqnATw==", "dev": true, "dependencies": { - "@storybook/cli": "7.6.19" + "@storybook/cli": "7.6.20" }, "bin": { "sb": "index.js", @@ -24661,9 +24708,9 @@ } }, "node_modules/ws": { - "version": "8.17.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.0.tgz", - "integrity": "sha512-uJq6108EgZMAl20KagGkzCKfMEjxmKvZHG7Tlq0Z6nOky7YF7aq4mOx6xK8TJ/i1LeK4Qus7INktacctDgY8Ow==", + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", + "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", "dev": true, "engines": { "node": ">=10.0.0" diff --git a/frontend/package.json b/frontend/package.json index fa3033073..acbf220e1 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -55,7 +55,7 @@ "@ucast/mongo2js": "^1.3.4", "add": "^2.0.6", "argon2-browser": "^1.18.0", - "axios": "^0.27.2", + "axios": "^0.28.0", "axios-auth-refresh": "^3.3.6", "base64-loader": "^1.0.0", "classnames": "^2.3.1", @@ -144,10 +144,10 @@ "eslint-plugin-react-hooks": "^4.6.0", "eslint-plugin-simple-import-sort": "^8.0.0", "eslint-plugin-storybook": "^0.6.12", - "postcss": "^8.4.14", + "postcss": "^8.4.39", "prettier": "^2.8.3", "prettier-plugin-tailwindcss": "^0.2.2", - "storybook": "^7.5.2", + "storybook": "^7.6.20", "storybook-dark-mode": "^3.0.0", "tailwindcss": "3.2", "typescript": "^4.9.3" diff --git a/frontend/src/components/v2/DeleteActionModal/DeleteActionModal.tsx b/frontend/src/components/v2/DeleteActionModal/DeleteActionModal.tsx index 2657686de..76804c137 100644 --- a/frontend/src/components/v2/DeleteActionModal/DeleteActionModal.tsx +++ b/frontend/src/components/v2/DeleteActionModal/DeleteActionModal.tsx @@ -25,7 +25,7 @@ export const DeleteActionModal = ({ deleteKey, onDeleteApproved, title, - subTitle = "This action is irreversible!", + subTitle = "This action is irreversible.", buttonText = "Delete" }: Props): JSX.Element => { const [inputData, setInputData] = useState(""); @@ -86,7 +86,7 @@ export const DeleteActionModal = ({ - Type {deleteKey} to delete the resource + Type {deleteKey} to perform this action } className="mb-0" @@ -94,7 +94,7 @@ export const DeleteActionModal = ({ setInputData(e.target.value)} - placeholder="Type to delete..." + placeholder="Type confirm..." /> diff --git a/frontend/src/components/v2/EmptyState/EmptyState.tsx b/frontend/src/components/v2/EmptyState/EmptyState.tsx index e285500c1..9816a3fe2 100644 --- a/frontend/src/components/v2/EmptyState/EmptyState.tsx +++ b/frontend/src/components/v2/EmptyState/EmptyState.tsx @@ -21,7 +21,7 @@ export const EmptyState = ({ }: Props) => (
diff --git a/frontend/src/components/v2/FormControl/FormControl.tsx b/frontend/src/components/v2/FormControl/FormControl.tsx index 35f85383d..9711e3e68 100644 --- a/frontend/src/components/v2/FormControl/FormControl.tsx +++ b/frontend/src/components/v2/FormControl/FormControl.tsx @@ -1,9 +1,11 @@ import { cloneElement, ReactNode } from "react"; -import { faExclamationTriangle } from "@fortawesome/free-solid-svg-icons"; +import { faExclamationTriangle, faQuestionCircle } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import * as Label from "@radix-ui/react-label"; import { twMerge } from "tailwind-merge"; +import { Tooltip } from "../Tooltip"; + export type FormLabelProps = { id?: string; isRequired?: boolean; @@ -11,9 +13,10 @@ export type FormLabelProps = { label?: ReactNode; icon?: ReactNode; className?: string; + tooltipText?: string; }; -export const FormLabel = ({ id, label, isRequired, icon, className,isOptional }: FormLabelProps) => ( +export const FormLabel = ({ id, label, isRequired, icon, className,isOptional, tooltipText }: FormLabelProps) => ( *} {isOptional && - Optional} - {icon && ( + {icon && !tooltipText && ( {icon} )} + {tooltipText && ( + + + + )} ); @@ -64,6 +76,7 @@ export type FormControlProps = { children: JSX.Element; className?: string; icon?: ReactNode; + tooltipText?: string; }; export const FormControl = ({ @@ -76,7 +89,8 @@ export const FormControl = ({ id, isError, icon, - className + className, + tooltipText }: FormControlProps): JSX.Element => { return (
@@ -87,6 +101,7 @@ export const FormControl = ({ isRequired={isRequired} id={id} icon={icon} + tooltipText={tooltipText} /> ) : ( label diff --git a/frontend/src/helpers/policies.ts b/frontend/src/helpers/policies.ts new file mode 100644 index 000000000..c6d7c935a --- /dev/null +++ b/frontend/src/helpers/policies.ts @@ -0,0 +1,12 @@ +import { PolicyType } from "@app/hooks/api/policies/enums"; + +export const policyDetails: Record = { + [PolicyType.AccessPolicy]: { + className: "bg-lime-900 text-lime-100", + name: "Access Policy" + }, + [PolicyType.ChangePolicy]: { + className: "bg-indigo-900 text-indigo-100", + name: "Change Policy" + } +}; \ No newline at end of file diff --git a/frontend/src/hooks/api/accessApproval/mutation.tsx b/frontend/src/hooks/api/accessApproval/mutation.tsx index 5f595c8a2..251e1c626 100644 --- a/frontend/src/hooks/api/accessApproval/mutation.tsx +++ b/frontend/src/hooks/api/accessApproval/mutation.tsx @@ -16,14 +16,15 @@ export const useCreateAccessApprovalPolicy = () => { const queryClient = useQueryClient(); return useMutation<{}, {}, TCreateAccessPolicyDTO>({ - mutationFn: async ({ environment, projectSlug, approvals, approvers, name, secretPath }) => { + mutationFn: async ({ environment, projectSlug, approvals, approvers, name, secretPath, enforcementLevel }) => { const { data } = await apiRequest.post("/api/v1/access-approvals/policies", { environment, projectSlug, approvals, approvers, secretPath, - name + name, + enforcementLevel }); return data; }, @@ -37,12 +38,13 @@ export const useUpdateAccessApprovalPolicy = () => { const queryClient = useQueryClient(); return useMutation<{}, {}, TUpdateAccessPolicyDTO>({ - mutationFn: async ({ id, approvers, approvals, name, secretPath }) => { + mutationFn: async ({ id, approvers, approvals, name, secretPath, enforcementLevel }) => { const { data } = await apiRequest.patch(`/api/v1/access-approvals/policies/${id}`, { approvals, approvers, secretPath, - name + name, + enforcementLevel }); return data; }, diff --git a/frontend/src/hooks/api/accessApproval/types.ts b/frontend/src/hooks/api/accessApproval/types.ts index 2176b8bc1..e17fe41d0 100644 --- a/frontend/src/hooks/api/accessApproval/types.ts +++ b/frontend/src/hooks/api/accessApproval/types.ts @@ -1,3 +1,4 @@ +import { EnforcementLevel, PolicyType } from "../policies/enums"; import { TProjectPermission } from "../roles/types"; import { WorkspaceEnv } from "../workspace/types"; @@ -11,6 +12,11 @@ export type TAccessApprovalPolicy = { environment: WorkspaceEnv; projectId: string; approvers: string[]; + policyType: PolicyType; + approversRequired: boolean; + enforcementLevel: EnforcementLevel; + updatedAt: Date; + userApprovers?: { userId: string }[]; }; export type TAccessApprovalRequest = { @@ -47,6 +53,7 @@ export type TAccessApprovalRequest = { approvers: string[]; secretPath?: string | null; envId: string; + enforcementLevel: EnforcementLevel; }; reviewers: { @@ -119,6 +126,7 @@ export type TCreateAccessPolicyDTO = { approvers?: string[]; approvals?: number; secretPath?: string; + enforcementLevel?: EnforcementLevel; }; export type TUpdateAccessPolicyDTO = { @@ -128,6 +136,7 @@ export type TUpdateAccessPolicyDTO = { secretPath?: string; environment?: string; approvals?: number; + enforcementLevel?: EnforcementLevel; // for invalidating list projectSlug: string; }; diff --git a/frontend/src/hooks/api/identities/constants.tsx b/frontend/src/hooks/api/identities/constants.tsx index bfa514745..0c57ee82c 100644 --- a/frontend/src/hooks/api/identities/constants.tsx +++ b/frontend/src/hooks/api/identities/constants.tsx @@ -6,5 +6,6 @@ export const identityAuthToNameMap: { [I in IdentityAuthMethod]: string } = { [IdentityAuthMethod.KUBERNETES_AUTH]: "Kubernetes Auth", [IdentityAuthMethod.GCP_AUTH]: "GCP Auth", [IdentityAuthMethod.AWS_AUTH]: "AWS Auth", - [IdentityAuthMethod.AZURE_AUTH]: "Azure Auth" + [IdentityAuthMethod.AZURE_AUTH]: "Azure Auth", + [IdentityAuthMethod.OIDC_AUTH]: "OIDC Auth" }; diff --git a/frontend/src/hooks/api/identities/enums.tsx b/frontend/src/hooks/api/identities/enums.tsx index 4ad5337f1..5e445521a 100644 --- a/frontend/src/hooks/api/identities/enums.tsx +++ b/frontend/src/hooks/api/identities/enums.tsx @@ -4,5 +4,6 @@ export enum IdentityAuthMethod { KUBERNETES_AUTH = "kubernetes-auth", GCP_AUTH = "gcp-auth", AWS_AUTH = "aws-auth", - AZURE_AUTH = "azure-auth" + AZURE_AUTH = "azure-auth", + OIDC_AUTH = "oidc-auth" } diff --git a/frontend/src/hooks/api/identities/index.tsx b/frontend/src/hooks/api/identities/index.tsx index ceece2c13..5c7bcc3e7 100644 --- a/frontend/src/hooks/api/identities/index.tsx +++ b/frontend/src/hooks/api/identities/index.tsx @@ -5,6 +5,7 @@ export { useAddIdentityAzureAuth, useAddIdentityGcpAuth, useAddIdentityKubernetesAuth, + useAddIdentityOidcAuth, useAddIdentityTokenAuth, useAddIdentityUniversalAuth, useCreateIdentity, @@ -15,6 +16,7 @@ export { useDeleteIdentityAzureAuth, useDeleteIdentityGcpAuth, useDeleteIdentityKubernetesAuth, + useDeleteIdentityOidcAuth, useDeleteIdentityTokenAuth, useDeleteIdentityUniversalAuth, useRevokeIdentityTokenAuthToken, @@ -24,6 +26,7 @@ export { useUpdateIdentityAzureAuth, useUpdateIdentityGcpAuth, useUpdateIdentityKubernetesAuth, + useUpdateIdentityOidcAuth, useUpdateIdentityTokenAuth, useUpdateIdentityTokenAuthToken, useUpdateIdentityUniversalAuth} from "./mutations"; @@ -33,9 +36,9 @@ export { useGetIdentityById, useGetIdentityGcpAuth, useGetIdentityKubernetesAuth, + useGetIdentityOidcAuth, useGetIdentityProjectMemberships, useGetIdentityTokenAuth, useGetIdentityTokensTokenAuth, useGetIdentityUniversalAuth, - useGetIdentityUniversalAuthClientSecrets -} from "./queries"; + useGetIdentityUniversalAuthClientSecrets} from "./queries"; diff --git a/frontend/src/hooks/api/identities/mutations.tsx b/frontend/src/hooks/api/identities/mutations.tsx index 86fca63e9..f4ee8bf9c 100644 --- a/frontend/src/hooks/api/identities/mutations.tsx +++ b/frontend/src/hooks/api/identities/mutations.tsx @@ -9,6 +9,7 @@ import { AddIdentityAzureAuthDTO, AddIdentityGcpAuthDTO, AddIdentityKubernetesAuthDTO, + AddIdentityOidcAuthDTO, AddIdentityTokenAuthDTO, AddIdentityUniversalAuthDTO, ClientSecretData, @@ -22,6 +23,7 @@ import { DeleteIdentityDTO, DeleteIdentityGcpAuthDTO, DeleteIdentityKubernetesAuthDTO, + DeleteIdentityOidcAuthDTO, DeleteIdentityTokenAuthDTO, DeleteIdentityUniversalAuthClientSecretDTO, DeleteIdentityUniversalAuthDTO, @@ -31,6 +33,7 @@ import { IdentityAzureAuth, IdentityGcpAuth, IdentityKubernetesAuth, + IdentityOidcAuth, IdentityTokenAuth, IdentityUniversalAuth, RevokeTokenDTO, @@ -40,6 +43,7 @@ import { UpdateIdentityDTO, UpdateIdentityGcpAuthDTO, UpdateIdentityKubernetesAuthDTO, + UpdateIdentityOidcAuthDTO, UpdateIdentityTokenAuthDTO, UpdateIdentityUniversalAuthDTO, UpdateTokenIdentityTokenAuthDTO @@ -409,6 +413,111 @@ export const useDeleteIdentityAwsAuth = () => { }); }; +export const useUpdateIdentityOidcAuth = () => { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: async ({ + identityId, + accessTokenTTL, + accessTokenMaxTTL, + accessTokenNumUsesLimit, + accessTokenTrustedIps, + oidcDiscoveryUrl, + caCert, + boundIssuer, + boundAudiences, + boundClaims, + boundSubject + }) => { + const { + data: { identityOidcAuth } + } = await apiRequest.patch<{ identityOidcAuth: IdentityOidcAuth }>( + `/api/v1/auth/oidc-auth/identities/${identityId}`, + { + oidcDiscoveryUrl, + caCert, + boundIssuer, + boundAudiences, + boundClaims, + boundSubject, + accessTokenTTL, + accessTokenMaxTTL, + accessTokenNumUsesLimit, + accessTokenTrustedIps + } + ); + + return identityOidcAuth; + }, + onSuccess: (_, { identityId, organizationId }) => { + queryClient.invalidateQueries(organizationKeys.getOrgIdentityMemberships(organizationId)); + queryClient.invalidateQueries(identitiesKeys.getIdentityById(identityId)); + queryClient.invalidateQueries(identitiesKeys.getIdentityOidcAuth(identityId)); + } + }); +}; + +export const useAddIdentityOidcAuth = () => { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: async ({ + identityId, + oidcDiscoveryUrl, + caCert, + boundIssuer, + boundAudiences, + boundClaims, + boundSubject, + accessTokenTTL, + accessTokenMaxTTL, + accessTokenNumUsesLimit, + accessTokenTrustedIps + }) => { + const { + data: { identityOidcAuth } + } = await apiRequest.post<{ identityOidcAuth: IdentityOidcAuth }>( + `/api/v1/auth/oidc-auth/identities/${identityId}`, + { + oidcDiscoveryUrl, + caCert, + boundIssuer, + boundAudiences, + boundClaims, + boundSubject, + accessTokenTTL, + accessTokenMaxTTL, + accessTokenNumUsesLimit, + accessTokenTrustedIps + } + ); + + return identityOidcAuth; + }, + onSuccess: (_, { identityId, organizationId }) => { + queryClient.invalidateQueries(organizationKeys.getOrgIdentityMemberships(organizationId)); + queryClient.invalidateQueries(identitiesKeys.getIdentityById(identityId)); + queryClient.invalidateQueries(identitiesKeys.getIdentityOidcAuth(identityId)); + } + }); +}; + +export const useDeleteIdentityOidcAuth = () => { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: async ({ identityId }) => { + const { + data: { identityOidcAuth } + } = await apiRequest.delete(`/api/v1/auth/oidc-auth/identities/${identityId}`); + return identityOidcAuth; + }, + onSuccess: (_, { organizationId, identityId }) => { + queryClient.invalidateQueries(organizationKeys.getOrgIdentityMemberships(organizationId)); + queryClient.invalidateQueries(identitiesKeys.getIdentityById(identityId)); + queryClient.invalidateQueries(identitiesKeys.getIdentityOidcAuth(identityId)); + } + }); +}; + export const useAddIdentityAzureAuth = () => { const queryClient = useQueryClient(); return useMutation({ diff --git a/frontend/src/hooks/api/identities/queries.tsx b/frontend/src/hooks/api/identities/queries.tsx index 5a72ecb78..0e07136c8 100644 --- a/frontend/src/hooks/api/identities/queries.tsx +++ b/frontend/src/hooks/api/identities/queries.tsx @@ -9,7 +9,9 @@ import { IdentityAzureAuth, IdentityGcpAuth, IdentityKubernetesAuth, + IdentityMembership, IdentityMembershipOrg, + IdentityOidcAuth, IdentityTokenAuth, IdentityUniversalAuth} from "./types"; @@ -22,6 +24,7 @@ export const identitiesKeys = { getIdentityKubernetesAuth: (identityId: string) => [{ identityId }, "identity-kubernetes-auth"] as const, getIdentityGcpAuth: (identityId: string) => [{ identityId }, "identity-gcp-auth"] as const, + getIdentityOidcAuth: (identityId: string) => [{ identityId }, "identity-oidc-auth"] as const, getIdentityAwsAuth: (identityId: string) => [{ identityId }, "identity-aws-auth"] as const, getIdentityAzureAuth: (identityId: string) => [{ identityId }, "identity-azure-auth"] as const, getIdentityTokenAuth: (identityId: string) => [{ identityId }, "identity-token-auth"] as const, @@ -53,7 +56,9 @@ export const useGetIdentityProjectMemberships = (identityId: string) => { queryFn: async () => { const { data: { identityMemberships } - } = await apiRequest.get(`/api/v1/identities/${identityId}/identity-memberships`); + } = await apiRequest.get<{ identityMemberships: IdentityMembership[] }>( + `/api/v1/identities/${identityId}/identity-memberships` + ); return identityMemberships; } }); @@ -190,3 +195,20 @@ export const useGetIdentityTokensTokenAuth = (identityId: string) => { } }); }; + +export const useGetIdentityOidcAuth = (identityId: string) => { + return useQuery({ + enabled: Boolean(identityId), + queryKey: identitiesKeys.getIdentityOidcAuth(identityId), + queryFn: async () => { + const { + data: { identityOidcAuth } + } = await apiRequest.get<{ identityOidcAuth: IdentityOidcAuth }>( + `/api/v1/auth/oidc-auth/identities/${identityId}` + ); + return identityOidcAuth; + }, + staleTime: 0, + cacheTime: 0 + }); +}; diff --git a/frontend/src/hooks/api/identities/types.ts b/frontend/src/hooks/api/identities/types.ts index 96dc70547..a944493c9 100644 --- a/frontend/src/hooks/api/identities/types.ts +++ b/frontend/src/hooks/api/identities/types.ts @@ -1,4 +1,5 @@ import { TOrgRole } from "../roles/types"; +import { Workspace } from "../workspace/types"; import { IdentityAuthMethod } from "./enums"; export type IdentityTrustedIp = { @@ -45,6 +46,7 @@ export type IdentityMembershipOrg = { export type IdentityMembership = { id: string; identity: Identity; + project: Pick; roles: Array< { id: string; @@ -181,6 +183,59 @@ export type DeleteIdentityGcpAuthDTO = { identityId: string; }; +export type IdentityOidcAuth = { + identityId: string; + oidcDiscoveryUrl: string; + caCert: string; + boundIssuer: string; + boundAudiences: string; + boundClaims: Record; + boundSubject: string; + accessTokenTTL: number; + accessTokenMaxTTL: number; + accessTokenNumUsesLimit: number; + accessTokenTrustedIps: IdentityTrustedIp[]; +}; + +export type AddIdentityOidcAuthDTO = { + organizationId: string; + identityId: string; + oidcDiscoveryUrl: string; + caCert: string; + boundIssuer: string; + boundAudiences: string; + boundClaims: Record; + boundSubject: string; + accessTokenTTL: number; + accessTokenMaxTTL: number; + accessTokenNumUsesLimit: number; + accessTokenTrustedIps: { + ipAddress: string; + }[]; +}; + +export type UpdateIdentityOidcAuthDTO = { + organizationId: string; + identityId: string; + oidcDiscoveryUrl?: string; + caCert?: string; + boundIssuer?: string; + boundAudiences?: string; + boundClaims?: Record; + boundSubject?: string; + accessTokenTTL?: number; + accessTokenMaxTTL?: number; + accessTokenNumUsesLimit?: number; + accessTokenTrustedIps?: { + ipAddress: string; + }[]; +}; + +export type DeleteIdentityOidcAuthDTO = { + organizationId: string; + identityId: string; +}; + export type IdentityAwsAuth = { identityId: string; type: "iam"; diff --git a/frontend/src/hooks/api/policies/enums.ts b/frontend/src/hooks/api/policies/enums.ts new file mode 100644 index 000000000..f91bcb98c --- /dev/null +++ b/frontend/src/hooks/api/policies/enums.ts @@ -0,0 +1,9 @@ +export enum EnforcementLevel { + Hard = "hard", + Soft = "soft" +} + +export enum PolicyType { + ChangePolicy = "change", + AccessPolicy = "access" +} diff --git a/frontend/src/hooks/api/secretApproval/mutation.tsx b/frontend/src/hooks/api/secretApproval/mutation.tsx index 991111ef9..2ad79932b 100644 --- a/frontend/src/hooks/api/secretApproval/mutation.tsx +++ b/frontend/src/hooks/api/secretApproval/mutation.tsx @@ -9,14 +9,15 @@ export const useCreateSecretApprovalPolicy = () => { const queryClient = useQueryClient(); return useMutation<{}, {}, TCreateSecretPolicyDTO>({ - mutationFn: async ({ environment, workspaceId, approvals, approverUserIds, secretPath, name }) => { + mutationFn: async ({ environment, workspaceId, approvals, approvers, secretPath, name, enforcementLevel }) => { const { data } = await apiRequest.post("/api/v1/secret-approvals", { environment, workspaceId, approvals, - approverUserIds, + approvers, secretPath, - name + name, + enforcementLevel }); return data; }, @@ -30,12 +31,13 @@ export const useUpdateSecretApprovalPolicy = () => { const queryClient = useQueryClient(); return useMutation<{}, {}, TUpdateSecretPolicyDTO>({ - mutationFn: async ({ id, approverUserIds, approvals, secretPath, name }) => { + mutationFn: async ({ id, approvers, approvals, secretPath, name, enforcementLevel }) => { const { data } = await apiRequest.patch(`/api/v1/secret-approvals/${id}`, { approvals, - approverUserIds, + approvers, secretPath, - name + name, + enforcementLevel }); return data; }, diff --git a/frontend/src/hooks/api/secretApproval/types.ts b/frontend/src/hooks/api/secretApproval/types.ts index f3b8639f7..06ffad432 100644 --- a/frontend/src/hooks/api/secretApproval/types.ts +++ b/frontend/src/hooks/api/secretApproval/types.ts @@ -1,3 +1,4 @@ +import { EnforcementLevel } from "../policies/enums"; import { WorkspaceEnv } from "../workspace/types"; export type TSecretApprovalPolicy = { @@ -9,6 +10,8 @@ export type TSecretApprovalPolicy = { secretPath?: string; approvals: number; userApprovers: { userId: string }[]; + updatedAt: Date; + enforcementLevel: EnforcementLevel; }; export type TGetSecretApprovalPoliciesDTO = { @@ -26,16 +29,18 @@ export type TCreateSecretPolicyDTO = { name?: string; environment: string; secretPath?: string | null; - approverUserIds?: string[]; + approvers?: string[]; approvals?: number; + enforcementLevel: EnforcementLevel; }; export type TUpdateSecretPolicyDTO = { id: string; name?: string; - approverUserIds?: string[]; + approvers?: string[]; secretPath?: string | null; approvals?: number; + enforcementLevel?: EnforcementLevel; // for invalidating list workspaceId: string; }; diff --git a/frontend/src/hooks/api/secretApprovalRequest/mutation.tsx b/frontend/src/hooks/api/secretApprovalRequest/mutation.tsx index 94358ed79..f3b389644 100644 --- a/frontend/src/hooks/api/secretApprovalRequest/mutation.tsx +++ b/frontend/src/hooks/api/secretApprovalRequest/mutation.tsx @@ -46,8 +46,10 @@ export const usePerformSecretApprovalRequestMerge = () => { const queryClient = useQueryClient(); return useMutation<{}, {}, TPerformSecretApprovalRequestMerge>({ - mutationFn: async ({ id }) => { - const { data } = await apiRequest.post(`/api/v1/secret-approval-requests/${id}/merge`); + mutationFn: async ({ id, bypassReason }) => { + const { data } = await apiRequest.post(`/api/v1/secret-approval-requests/${id}/merge`, { + bypassReason + }); return data; }, onSuccess: (_, { id, workspaceId }) => { diff --git a/frontend/src/hooks/api/secretApprovalRequest/types.ts b/frontend/src/hooks/api/secretApprovalRequest/types.ts index f039cc815..ba7cbc38d 100644 --- a/frontend/src/hooks/api/secretApprovalRequest/types.ts +++ b/frontend/src/hooks/api/secretApprovalRequest/types.ts @@ -133,4 +133,5 @@ export type TUpdateSecretApprovalRequestStatusDTO = { export type TPerformSecretApprovalRequestMerge = { id: string; workspaceId: string; + bypassReason?: string; }; diff --git a/frontend/src/hooks/api/secretSharing/queries.ts b/frontend/src/hooks/api/secretSharing/queries.ts index 886b0a82e..44b2c3193 100644 --- a/frontend/src/hooks/api/secretSharing/queries.ts +++ b/frontend/src/hooks/api/secretSharing/queries.ts @@ -2,7 +2,7 @@ import { useQuery } from "@tanstack/react-query"; import { apiRequest } from "@app/config/request"; -import { TSharedSecret, TViewSharedSecretResponse } from "./types"; +import { SecretSharingAccessType, TSharedSecret, TViewSharedSecretResponse } from "./types"; export const useGetSharedSecrets = () => { return useQuery({ @@ -17,7 +17,7 @@ export const useGetSharedSecrets = () => { export const useGetActiveSharedSecretByIdAndHashedHex = (id: string, hashedHex: string) => { return useQuery({ queryFn: async () => { - if(!id || !hashedHex) return Promise.resolve({ encryptedValue: "", iv: "", tag: "" }); + if(!id || !hashedHex) return Promise.resolve({ encryptedValue: "", iv: "", tag: "", accessType: SecretSharingAccessType.Organization, orgName: "" }); const { data } = await apiRequest.get( `/api/v1/secret-sharing/public/${id}?hashedHex=${hashedHex}` ); @@ -25,6 +25,8 @@ export const useGetActiveSharedSecretByIdAndHashedHex = (id: string, hashedHex: encryptedValue: data.encryptedValue, iv: data.iv, tag: data.tag, + accessType: data.accessType, + orgName: data.orgName }; } }); diff --git a/frontend/src/hooks/api/secretSharing/types.ts b/frontend/src/hooks/api/secretSharing/types.ts index 424e3525c..69c0d9a65 100644 --- a/frontend/src/hooks/api/secretSharing/types.ts +++ b/frontend/src/hooks/api/secretSharing/types.ts @@ -13,14 +13,22 @@ export type TCreateSharedSecretRequest = { hashedHex: string; expiresAt: Date; expiresAfterViews: number; + accessType: SecretSharingAccessType; }; export type TViewSharedSecretResponse = { encryptedValue: string; iv: string; tag: string; + accessType: SecretSharingAccessType; + orgName?: string; }; export type TDeleteSharedSecretRequest = { sharedSecretId: string; }; + +export enum SecretSharingAccessType { + Anyone = "anyone", + Organization = "organization" +} \ No newline at end of file diff --git a/frontend/src/hooks/api/secrets/index.ts b/frontend/src/hooks/api/secrets/index.ts index b58e8779a..737ebf41a 100644 --- a/frontend/src/hooks/api/secrets/index.ts +++ b/frontend/src/hooks/api/secrets/index.ts @@ -4,6 +4,7 @@ export { useCreateSecretV3, useDeleteSecretBatch, useDeleteSecretV3, + useMoveSecrets, useUpdateSecretBatch, useUpdateSecretV3 } from "./mutations"; diff --git a/frontend/src/hooks/api/secrets/mutations.tsx b/frontend/src/hooks/api/secrets/mutations.tsx index e397c7b55..9c3fd23f0 100644 --- a/frontend/src/hooks/api/secrets/mutations.tsx +++ b/frontend/src/hooks/api/secrets/mutations.tsx @@ -17,6 +17,7 @@ import { TCreateSecretsV3DTO, TDeleteSecretBatchDTO, TDeleteSecretsV3DTO, + TMoveSecretsDTO, TUpdateSecretBatchDTO, TUpdateSecretsV3DTO } from "./types"; @@ -87,11 +88,11 @@ export const useCreateSecretV3 = ({ const randomBytes = latestFileKey ? decryptAssymmetric({ - ciphertext: latestFileKey.encryptedKey, - nonce: latestFileKey.nonce, - publicKey: latestFileKey.sender.publicKey, - privateKey: PRIVATE_KEY - }) + ciphertext: latestFileKey.encryptedKey, + nonce: latestFileKey.nonce, + publicKey: latestFileKey.sender.publicKey, + privateKey: PRIVATE_KEY + }) : crypto.randomBytes(16).toString("hex"); const reqBody = { @@ -148,11 +149,11 @@ export const useUpdateSecretV3 = ({ const randomBytes = latestFileKey ? decryptAssymmetric({ - ciphertext: latestFileKey.encryptedKey, - nonce: latestFileKey.nonce, - publicKey: latestFileKey.sender.publicKey, - privateKey: PRIVATE_KEY - }) + ciphertext: latestFileKey.encryptedKey, + nonce: latestFileKey.nonce, + publicKey: latestFileKey.sender.publicKey, + privateKey: PRIVATE_KEY + }) : crypto.randomBytes(16).toString("hex"); const reqBody = { @@ -244,11 +245,11 @@ export const useCreateSecretBatch = ({ const PRIVATE_KEY = localStorage.getItem("PRIVATE_KEY") as string; const randomBytes = latestFileKey ? decryptAssymmetric({ - ciphertext: latestFileKey.encryptedKey, - nonce: latestFileKey.nonce, - publicKey: latestFileKey.sender.publicKey, - privateKey: PRIVATE_KEY - }) + ciphertext: latestFileKey.encryptedKey, + nonce: latestFileKey.nonce, + publicKey: latestFileKey.sender.publicKey, + privateKey: PRIVATE_KEY + }) : crypto.randomBytes(16).toString("hex"); const reqBody = { @@ -297,11 +298,11 @@ export const useUpdateSecretBatch = ({ const PRIVATE_KEY = localStorage.getItem("PRIVATE_KEY") as string; const randomBytes = latestFileKey ? decryptAssymmetric({ - ciphertext: latestFileKey.encryptedKey, - nonce: latestFileKey.nonce, - publicKey: latestFileKey.sender.publicKey, - privateKey: PRIVATE_KEY - }) + ciphertext: latestFileKey.encryptedKey, + nonce: latestFileKey.nonce, + publicKey: latestFileKey.sender.publicKey, + privateKey: PRIVATE_KEY + }) : crypto.randomBytes(16).toString("hex"); const reqBody = { @@ -375,6 +376,73 @@ export const useDeleteSecretBatch = ({ }); }; +export const useMoveSecrets = ({ + options +}: { + options?: Omit, "mutationFn">; +} = {}) => { + const queryClient = useQueryClient(); + + return useMutation< + { + isSourceUpdated: boolean; + isDestinationUpdated: boolean; + }, + {}, + TMoveSecretsDTO + >({ + mutationFn: async ({ + sourceEnvironment, + sourceSecretPath, + projectSlug, + destinationEnvironment, + destinationSecretPath, + secretIds, + shouldOverwrite + }) => { + const { data } = await apiRequest.post<{ + isSourceUpdated: boolean; + isDestinationUpdated: boolean; + }>("/api/v3/secrets/move", { + sourceEnvironment, + sourceSecretPath, + projectSlug, + destinationEnvironment, + destinationSecretPath, + secretIds, + shouldOverwrite + }); + + return data; + }, + onSuccess: (_, { projectId, sourceEnvironment, sourceSecretPath }) => { + queryClient.invalidateQueries( + secretKeys.getProjectSecret({ + workspaceId: projectId, + environment: sourceEnvironment, + secretPath: sourceSecretPath + }) + ); + queryClient.invalidateQueries( + secretSnapshotKeys.list({ + environment: sourceEnvironment, + workspaceId: projectId, + directory: sourceSecretPath + }) + ); + queryClient.invalidateQueries( + secretSnapshotKeys.count({ + environment: sourceEnvironment, + workspaceId: projectId, + directory: sourceSecretPath + }) + ); + queryClient.invalidateQueries(secretApprovalRequestKeys.count({ workspaceId: projectId })); + }, + ...options + }); +}; + export const createSecret = async (dto: CreateSecretDTO) => { const { data } = await apiRequest.post(`/api/v3/secrets/${dto.secretKey}`, dto); return data; diff --git a/frontend/src/hooks/api/secrets/types.ts b/frontend/src/hooks/api/secrets/types.ts index 378405c96..de7e1d503 100644 --- a/frontend/src/hooks/api/secrets/types.ts +++ b/frontend/src/hooks/api/secrets/types.ts @@ -177,6 +177,17 @@ export type TDeleteSecretBatchDTO = { }>; }; +export type TMoveSecretsDTO = { + projectSlug: string; + projectId: string; + sourceEnvironment: string; + sourceSecretPath: string; + destinationEnvironment: string; + destinationSecretPath: string; + secretIds: string[]; + shouldOverwrite: boolean; +}; + export type CreateSecretDTO = { workspaceId: string; environment: string; diff --git a/frontend/src/hooks/api/subscriptions/types.ts b/frontend/src/hooks/api/subscriptions/types.ts index 89635a953..6a8b053b4 100644 --- a/frontend/src/hooks/api/subscriptions/types.ts +++ b/frontend/src/hooks/api/subscriptions/types.ts @@ -39,4 +39,5 @@ export type SubscriptionPlan = { trial_end: number | null; has_used_trial: boolean; caCrl: boolean; + instanceUserManagement: boolean; }; diff --git a/frontend/src/hooks/api/users/index.tsx b/frontend/src/hooks/api/users/index.tsx index 521c36468..2026cede1 100644 --- a/frontend/src/hooks/api/users/index.tsx +++ b/frontend/src/hooks/api/users/index.tsx @@ -16,6 +16,8 @@ export { useGetMyIp, useGetMyOrganizationProjects, useGetMySessions, + useGetOrgMembership, + useGetOrgMembershipProjectMemberships, useGetOrgUsers, useGetUser, useGetUserAction, @@ -23,6 +25,5 @@ export { useRegisterUserAction, useRevokeMySessions, useUpdateMfaEnabled, - useUpdateOrgUserRole, - useUpdateUserAuthMethods -} from "./queries"; + useUpdateOrgMembership, + useUpdateUserAuthMethods} from "./queries"; diff --git a/frontend/src/hooks/api/users/mutation.tsx b/frontend/src/hooks/api/users/mutation.tsx index 26e932ac6..eb9bb8007 100644 --- a/frontend/src/hooks/api/users/mutation.tsx +++ b/frontend/src/hooks/api/users/mutation.tsx @@ -57,8 +57,9 @@ export const useAddUserToWsNonE2EE = () => { }); return data; }, - onSuccess: (_, { projectId }) => { + onSuccess: (_, { orgId, projectId }) => { queryClient.invalidateQueries(workspaceKeys.getWorkspaceUsers(projectId)); + queryClient.invalidateQueries(userKeys.allOrgMembershipProjectMemberships(orgId)); } }); }; diff --git a/frontend/src/hooks/api/users/queries.tsx b/frontend/src/hooks/api/users/queries.tsx index 5ca47fc63..ea3f44676 100644 --- a/frontend/src/hooks/api/users/queries.tsx +++ b/frontend/src/hooks/api/users/queries.tsx @@ -13,7 +13,8 @@ import { OrgUser, RenameUserDTO, TokenVersion, - UpdateOrgUserRoleDTO, + TWorkspaceUser, + UpdateOrgMembershipDTO, User, UserEnc } from "./types"; @@ -23,6 +24,13 @@ export const userKeys = { getPrivateKey: ["user"] as const, userAction: ["user-action"] as const, userProjectFavorites: (orgId: string) => [{ orgId }, "user-project-favorites"] as const, + getOrgMembership: (orgId: string, orgMembershipId: string) => + [{ orgId, orgMembershipId }, "org-membership"] as const, + allOrgMembershipProjectMemberships: (orgId: string) => [orgId, "all-user-memberships"] as const, + forOrgMembershipProjectMemberships: (orgId: string, orgMembershipId: string) => + [...userKeys.allOrgMembershipProjectMemberships(orgId), { orgMembershipId }] as const, + getOrgMembershipProjectMemberships: (orgId: string, username: string) => + [{ orgId, username }, "org-membership-project-memberships"] as const, getOrgUsers: (orgId: string) => [{ orgId }, "user"], myIp: ["ip"] as const, myAPIKeys: ["api-keys"] as const, @@ -167,6 +175,41 @@ export const useAddUserToOrg = () => { }); }; +export const useGetOrgMembership = (organizationId: string, orgMembershipId: string) => { + return useQuery({ + queryKey: userKeys.getOrgMembership(organizationId, orgMembershipId), + queryFn: async () => { + const { + data: { membership } + } = await apiRequest.get<{ membership: OrgUser }>( + `/api/v2/organizations/${organizationId}/memberships/${orgMembershipId}` + ); + + return membership; + }, + enabled: Boolean(organizationId) && Boolean(orgMembershipId) + }); +}; + +export const useGetOrgMembershipProjectMemberships = ( + organizationId: string, + orgMembershipId: string +) => { + return useQuery({ + queryKey: userKeys.forOrgMembershipProjectMemberships(organizationId, orgMembershipId), + queryFn: async () => { + const { + data: { memberships } + } = await apiRequest.get<{ memberships: TWorkspaceUser[] }>( + `/api/v2/organizations/${organizationId}/memberships/${orgMembershipId}/project-memberships` + ); + + return memberships; + }, + enabled: Boolean(organizationId) && Boolean(orgMembershipId) + }); +}; + export const useDeleteOrgMembership = () => { const queryClient = useQueryClient(); @@ -180,24 +223,43 @@ export const useDeleteOrgMembership = () => { }); }; -export const useUpdateOrgUserRole = () => { +export const useDeactivateOrgMembership = () => { const queryClient = useQueryClient(); - return useMutation<{}, {}, UpdateOrgUserRoleDTO>({ - mutationFn: ({ organizationId, membershipId, role }) => { + return useMutation<{}, {}, DeletOrgMembershipDTO>({ + mutationFn: ({ membershipId, orgId }) => { + return apiRequest.post( + `/api/v2/organizations/${orgId}/memberships/${membershipId}/deactivate` + ); + }, + onSuccess: (_, { orgId, membershipId }) => { + queryClient.invalidateQueries(userKeys.getOrgUsers(orgId)); + queryClient.invalidateQueries(userKeys.getOrgMembership(orgId, membershipId)); + } + }); +}; + +export const useUpdateOrgMembership = () => { + const queryClient = useQueryClient(); + + return useMutation<{}, {}, UpdateOrgMembershipDTO>({ + mutationFn: ({ organizationId, membershipId, role, isActive }) => { return apiRequest.patch( `/api/v2/organizations/${organizationId}/memberships/${membershipId}`, { - role + role, + isActive } ); }, - onSuccess: (_, { organizationId }) => { + onSuccess: (_, { organizationId, membershipId }) => { queryClient.invalidateQueries(userKeys.getOrgUsers(organizationId)); + queryClient.invalidateQueries(userKeys.getOrgMembership(organizationId, membershipId)); }, // to remove old states - onError: (_, { organizationId }) => { + onError: (_, { organizationId, membershipId }) => { queryClient.invalidateQueries(userKeys.getOrgUsers(organizationId)); + queryClient.invalidateQueries(userKeys.getOrgMembership(organizationId, membershipId)); } }); }; diff --git a/frontend/src/hooks/api/users/types.ts b/frontend/src/hooks/api/users/types.ts index 43e408571..4e2d7d3a2 100644 --- a/frontend/src/hooks/api/users/types.ts +++ b/frontend/src/hooks/api/users/types.ts @@ -49,6 +49,7 @@ export type OrgUser = { user: { username: string; email?: string; + isEmailVerified: boolean; firstName: string; lastName: string; id: string; @@ -60,6 +61,7 @@ export type OrgUser = { status: "invited" | "accepted" | "verified" | "completed"; deniedPermissions: any[]; roleId: string; + isActive: boolean; }; export type TProjectMembership = { @@ -81,6 +83,11 @@ export type TWorkspaceUser = { id: string; publicKey: string; }; + projectId: string; + project: { + id: string; + name: string; + }; inviteEmail: string; organization: string; roles: ( @@ -126,12 +133,14 @@ export type AddUserToWsDTOE2EE = { export type AddUserToWsDTONonE2EE = { projectId: string; usernames: string[]; + orgId: string; }; -export type UpdateOrgUserRoleDTO = { +export type UpdateOrgMembershipDTO = { organizationId: string; membershipId: string; - role: string; + role?: string; + isActive?: boolean; }; export type DeletOrgMembershipDTO = { diff --git a/frontend/src/hooks/api/workspace/queries.tsx b/frontend/src/hooks/api/workspace/queries.tsx index 202480de8..11579b760 100644 --- a/frontend/src/hooks/api/workspace/queries.tsx +++ b/frontend/src/hooks/api/workspace/queries.tsx @@ -6,10 +6,12 @@ import { CaStatus } from "../ca/enums"; import { TCertificateAuthority } from "../ca/types"; import { TCertificate } from "../certificates/types"; import { TGroupMembership } from "../groups/types"; +import { identitiesKeys } from "../identities/queries"; import { IdentityMembership } from "../identities/types"; import { IntegrationAuth } from "../integrationAuth/types"; import { TIntegration } from "../integrations/types"; import { EncryptedSecret } from "../secrets/types"; +import { userKeys } from "../users/queries"; import { TWorkspaceUser } from "../users/types"; import { CreateEnvironmentDTO, @@ -152,7 +154,7 @@ export const useGetWorkspaceById = (workspaceId: string) => { return useQuery({ queryKey: workspaceKeys.getWorkspaceById(workspaceId), queryFn: () => fetchWorkspaceById(workspaceId), - enabled: true + enabled: Boolean(workspaceId) }); }; @@ -384,6 +386,7 @@ export const useDeleteUserFromWorkspace = () => { }: { workspaceId: string; usernames: string[]; + orgId: string; }) => { const { data: { deletedMembership } @@ -392,8 +395,9 @@ export const useDeleteUserFromWorkspace = () => { }); return deletedMembership; }, - onSuccess: (_, { workspaceId }) => { + onSuccess: (_, { orgId, workspaceId }) => { queryClient.invalidateQueries(workspaceKeys.getWorkspaceUsers(workspaceId)); + queryClient.invalidateQueries(userKeys.allOrgMembershipProjectMemberships(orgId)); } }); }; @@ -441,8 +445,9 @@ export const useAddIdentityToWorkspace = () => { return identityMembership; }, - onSuccess: (_, { workspaceId }) => { + onSuccess: (_, { identityId, workspaceId }) => { queryClient.invalidateQueries(workspaceKeys.getWorkspaceIdentityMemberships(workspaceId)); + queryClient.invalidateQueries(identitiesKeys.getIdentityProjectMemberships(identityId)); } }); }; @@ -462,8 +467,9 @@ export const useUpdateIdentityWorkspaceRole = () => { return identityMembership; }, - onSuccess: (_, { workspaceId }) => { + onSuccess: (_, { identityId, workspaceId }) => { queryClient.invalidateQueries(workspaceKeys.getWorkspaceIdentityMemberships(workspaceId)); + queryClient.invalidateQueries(identitiesKeys.getIdentityProjectMemberships(identityId)); } }); }; @@ -485,8 +491,9 @@ export const useDeleteIdentityFromWorkspace = () => { ); return identityMembership; }, - onSuccess: (_, { workspaceId }) => { + onSuccess: (_, { identityId, workspaceId }) => { queryClient.invalidateQueries(workspaceKeys.getWorkspaceIdentityMemberships(workspaceId)); + queryClient.invalidateQueries(identitiesKeys.getIdentityProjectMemberships(identityId)); } }); }; diff --git a/frontend/src/layouts/AppLayout/AppLayout.tsx b/frontend/src/layouts/AppLayout/AppLayout.tsx index dc0ef370e..be4cf2086 100644 --- a/frontend/src/layouts/AppLayout/AppLayout.tsx +++ b/frontend/src/layouts/AppLayout/AppLayout.tsx @@ -264,7 +264,8 @@ export const AppLayout = ({ children }: LayoutProps) => { usernames: orgUsers .map((member) => member.user.username) .filter((username) => username !== user.username), - projectId: newProjectId + projectId: newProjectId, + orgId: currentOrg.id }); } diff --git a/frontend/src/pages/org/[id]/memberships/[membershipId]/index.tsx b/frontend/src/pages/org/[id]/memberships/[membershipId]/index.tsx new file mode 100644 index 000000000..093b7cd78 --- /dev/null +++ b/frontend/src/pages/org/[id]/memberships/[membershipId]/index.tsx @@ -0,0 +1,20 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ +import { useTranslation } from "react-i18next"; +import Head from "next/head"; + +import { UserPage } from "@app/views/Org/UserPage"; + +export default function User() { + const { t } = useTranslation(); + return ( + <> + + {t("common.head-title", { title: t("settings.org.title") })} + + + + + ); +} + +User.requireAuth = true; diff --git a/frontend/src/pages/org/[id]/overview/index.tsx b/frontend/src/pages/org/[id]/overview/index.tsx index d443b35b1..6956f71a3 100644 --- a/frontend/src/pages/org/[id]/overview/index.tsx +++ b/frontend/src/pages/org/[id]/overview/index.tsx @@ -77,7 +77,7 @@ const features = [ "Pull secrets into your Kubernetes containers and automatically redeploy upon secret changes." }, { - _id: 1, + id: 1, name: "Infisical Agent", link: "https://infisical.com/docs/infisical-agent/overview", description: "Inject secrets into your apps without modifying any application logic." @@ -541,7 +541,8 @@ const OrganizationPage = withPermission( usernames: orgUsers .map((member) => member.user.username) .filter((username) => username !== user.username), - projectId: newProjectId + projectId: newProjectId, + orgId: currentOrg.id }); } @@ -889,27 +890,27 @@ const OrganizationPage = withPermission(
{features.map((feature) => (
{feature.name}
-
+
{feature.description}
-
-
+
+

Setup time: 20 min -

+

Learn more{" "}
diff --git a/frontend/src/views/Org/IdentityPage/components/IdentityAuthenticationSection/IdentityClientSecrets.tsx b/frontend/src/views/Org/IdentityPage/components/IdentityAuthenticationSection/IdentityClientSecrets.tsx index 21402fb6d..19810c3d0 100644 --- a/frontend/src/views/Org/IdentityPage/components/IdentityAuthenticationSection/IdentityClientSecrets.tsx +++ b/frontend/src/views/Org/IdentityPage/components/IdentityAuthenticationSection/IdentityClientSecrets.tsx @@ -1,4 +1,4 @@ -import { faCheck, faCopy,faKey, faTrash } from "@fortawesome/free-solid-svg-icons"; +import { faCheck, faCopy, faKey, faTrash } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { format } from "date-fns"; @@ -37,21 +37,23 @@ export const IdentityClientSecrets = ({ identityId, handlePopUpOpen }: Props) =>

Client ID

-
+

{identityUniversalAuth?.clientId ?? ""}

- - { - navigator.clipboard.writeText(identityUniversalAuth?.clientId ?? ""); - setCopyTextClientId("Copied"); - }} - > - - - +
+ + { + navigator.clipboard.writeText(identityUniversalAuth?.clientId ?? ""); + setCopyTextClientId("Copied"); + }} + > + + + +
{clientSecrets?.length ? ( diff --git a/frontend/src/views/Org/IdentityPage/components/IdentityClientSecretModal.tsx b/frontend/src/views/Org/IdentityPage/components/IdentityClientSecretModal.tsx index 49c21587d..3f4de4fc1 100644 --- a/frontend/src/views/Org/IdentityPage/components/IdentityClientSecretModal.tsx +++ b/frontend/src/views/Org/IdentityPage/components/IdentityClientSecretModal.tsx @@ -22,7 +22,9 @@ import { UsePopUpState } from "@app/hooks/usePopUp"; const schema = z .object({ description: z.string(), - ttl: z.string(), + ttl: z.string().refine((val) => Number(val) <= 315360000, { + message: "TTL cannot be greater than 315360000" + }), numUsesLimit: z.string() }) .required(); diff --git a/frontend/src/views/Org/IdentityPage/components/IdentityDetailsSection.tsx b/frontend/src/views/Org/IdentityPage/components/IdentityDetailsSection.tsx index b2adb451d..aa7536bb8 100644 --- a/frontend/src/views/Org/IdentityPage/components/IdentityDetailsSection.tsx +++ b/frontend/src/views/Org/IdentityPage/components/IdentityDetailsSection.tsx @@ -1,4 +1,4 @@ -import { faCheck,faCopy, faPencil } from "@fortawesome/free-solid-svg-icons"; +import { faCheck, faCopy, faPencil } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { OrgPermissionCan } from "@app/components/permissions"; @@ -54,21 +54,23 @@ export const IdentityDetailsSection = ({ identityId, handlePopUpOpen }: Props) =

Identity ID

-
+

{data.identity.id}

- - { - navigator.clipboard.writeText(data.identity.id); - setCopyTextId("Copied"); - }} - > - - - +
+ + { + navigator.clipboard.writeText(data.identity.id); + setCopyTextId("Copied"); + }} + > + + + +
diff --git a/frontend/src/views/Org/IdentityPage/components/IdentityProjectsSection.tsx b/frontend/src/views/Org/IdentityPage/components/IdentityProjectsSection.tsx deleted file mode 100644 index abf8f950b..000000000 --- a/frontend/src/views/Org/IdentityPage/components/IdentityProjectsSection.tsx +++ /dev/null @@ -1,57 +0,0 @@ -import { faKey } from "@fortawesome/free-solid-svg-icons"; - -import { - EmptyState, - Table, - TableContainer, - TableSkeleton, - TBody, - Td, - Th, - THead, - Tr -} from "@app/components/v2"; -import { useGetIdentityProjectMemberships } from "@app/hooks/api"; - -type Props = { - identityId: string; -}; - -export const IdentityProjectsSection = ({ identityId }: Props) => { - const { data: projectMemberships, isLoading } = useGetIdentityProjectMemberships(identityId); - return ( -
-
-

Projects

-
-
- - - - - - - - - - {isLoading && } - {!isLoading && - projectMemberships?.map((membership: any) => { - // TODO: fix any - return ( - - - - - ); - })} - -
NameRole
{membership.project.name}{membership.roles[0].role}
- {!isLoading && !projectMemberships?.length && ( - - )} -
-
-
- ); -}; diff --git a/frontend/src/views/Org/IdentityPage/components/IdentityProjectsSection/IdentityAddToProjectModal.tsx b/frontend/src/views/Org/IdentityPage/components/IdentityProjectsSection/IdentityAddToProjectModal.tsx new file mode 100644 index 000000000..58697afcc --- /dev/null +++ b/frontend/src/views/Org/IdentityPage/components/IdentityProjectsSection/IdentityAddToProjectModal.tsx @@ -0,0 +1,178 @@ +import { useMemo } from "react"; +import { Controller, useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { z } from "zod"; + +import { createNotification } from "@app/components/notifications"; +import { Button, FormControl, Modal, ModalContent, Select, SelectItem } from "@app/components/v2"; +import { useOrganization,useWorkspace } from "@app/context"; +import { + useAddIdentityToWorkspace, + useGetIdentityProjectMemberships, + useGetProjectRoles, + useGetWorkspaceById +} from "@app/hooks/api"; +import { UsePopUpState } from "@app/hooks/usePopUp"; + +const schema = z + .object({ + projectId: z.string(), + role: z.string() + }) + .required(); + +type FormData = z.infer; + +type Props = { + identityId: string; + popUp: UsePopUpState<["addIdentityToProject"]>; + handlePopUpToggle: ( + popUpName: keyof UsePopUpState<["addIdentityToProject"]>, + state?: boolean + ) => void; +}; + +export const IdentityAddToProjectModal = ({ identityId, popUp, handlePopUpToggle }: Props) => { + const { currentOrg } = useOrganization(); + const { workspaces } = useWorkspace(); + const { mutateAsync: addIdentityToWorkspace } = useAddIdentityToWorkspace(); + + const { + control, + handleSubmit, + reset, + formState: { isSubmitting }, + watch + } = useForm({ + resolver: zodResolver(schema) + }); + + const projectId = watch("projectId"); + const { data: projectMemberships } = useGetIdentityProjectMemberships(identityId); + const { data: project } = useGetWorkspaceById(projectId); + const { data: roles } = useGetProjectRoles(project?.slug ?? ""); + + const filteredWorkspaces = useMemo(() => { + const wsWorkspaceIds = new Map(); + + projectMemberships?.forEach((projectMembership) => { + wsWorkspaceIds.set(projectMembership.project.id, true); + }); + + return (workspaces || []).filter( + ({ id, orgId }) => !wsWorkspaceIds.has(id) && orgId === currentOrg?.id + ); + }, [workspaces, projectMemberships]); + + const onFormSubmit = async ({ projectId: workspaceId, role }: FormData) => { + try { + await addIdentityToWorkspace({ + workspaceId, + identityId, + role: role || undefined + }); + + createNotification({ + text: "Successfully added identity to project", + type: "success" + }); + + reset(); + handlePopUpToggle("addIdentityToProject", false); + } catch (err) { + console.error(err); + const error = err as any; + const text = error?.response?.data?.message ?? "Failed to add identity to project"; + + createNotification({ + text, + type: "error" + }); + } + }; + + return ( + { + handlePopUpToggle("addIdentityToProject", isOpen); + reset(); + }} + > + +
+ ( + + + + )} + /> + ( + + + + )} + /> +
+ + +
+ +
+
+ ); +}; diff --git a/frontend/src/views/Org/IdentityPage/components/IdentityProjectsSection/IdentityProjectRow.tsx b/frontend/src/views/Org/IdentityPage/components/IdentityProjectsSection/IdentityProjectRow.tsx new file mode 100644 index 000000000..e5ec47f6c --- /dev/null +++ b/frontend/src/views/Org/IdentityPage/components/IdentityProjectsSection/IdentityProjectRow.tsx @@ -0,0 +1,96 @@ +import { useMemo } from "react"; +import { useRouter } from "next/router"; +import { faTrash } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { format } from "date-fns"; + +import { createNotification } from "@app/components/notifications"; +import { IconButton, Td, Tooltip, Tr } from "@app/components/v2"; +import { useWorkspace } from "@app/context"; +import { IdentityMembership } from "@app/hooks/api/identities/types"; +import { ProjectMembershipRole } from "@app/hooks/api/roles/types"; +import { UsePopUpState } from "@app/hooks/usePopUp"; + +type Props = { + membership: IdentityMembership; + handlePopUpOpen: ( + popUpName: keyof UsePopUpState<["removeIdentityFromProject"]>, + data?: {} + ) => void; +}; + +const formatRoleName = (role: string, customRoleName?: string) => { + if (role === ProjectMembershipRole.Custom) return customRoleName; + if (role === ProjectMembershipRole.Admin) return "Admin"; + if (role === ProjectMembershipRole.Member) return "Developer"; + if (role === ProjectMembershipRole.Viewer) return "Viewer"; + if (role === ProjectMembershipRole.NoAccess) return "No Access"; + return role; +}; + +export const IdentityProjectRow = ({ + membership: { id, createdAt, identity, project, roles }, + handlePopUpOpen +}: Props) => { + const { workspaces } = useWorkspace(); + const router = useRouter(); + + const isAccessible = useMemo(() => { + const workspaceIds = new Map(); + + workspaces?.forEach((workspace) => { + workspaceIds.set(workspace.id, true); + }); + + return workspaceIds.has(project.id); + }, [workspaces, project]); + + return ( + { + if (isAccessible) { + router.push(`/project/${project.id}/members`); + return; + } + + createNotification({ + text: "Unable to access project", + type: "error" + }); + }} + > + {project.name} + {`${formatRoleName(roles[0].role, roles[0].customRoleName)}${ + roles.length > 1 ? ` (+${roles.length - 1})` : "" + }`} + {format(new Date(createdAt), "yyyy-MM-dd")} + + {isAccessible && ( +
+ + { + e.stopPropagation(); + handlePopUpOpen("removeIdentityFromProject", { + identityId: identity.id, + identityName: identity.name, + projectId: project.id, + projectName: project.name + }); + }} + > + + + +
+ )} + + + ); +}; diff --git a/frontend/src/views/Org/IdentityPage/components/IdentityProjectsSection/IdentityProjectsSection.tsx b/frontend/src/views/Org/IdentityPage/components/IdentityProjectsSection/IdentityProjectsSection.tsx new file mode 100644 index 000000000..b0c13009d --- /dev/null +++ b/frontend/src/views/Org/IdentityPage/components/IdentityProjectsSection/IdentityProjectsSection.tsx @@ -0,0 +1,92 @@ +import { faPlus } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; + +import { createNotification } from "@app/components/notifications"; +import { DeleteActionModal, IconButton } from "@app/components/v2"; +import { useDeleteIdentityFromWorkspace } from "@app/hooks/api"; +import { usePopUp } from "@app/hooks/usePopUp"; + +import { IdentityAddToProjectModal } from "./IdentityAddToProjectModal"; +import { IdentityProjectsTable } from "./IdentityProjectsTable"; + +type Props = { + identityId: string; +}; + +export const IdentityProjectsSection = ({ identityId }: Props) => { + const { mutateAsync: deleteMutateAsync } = useDeleteIdentityFromWorkspace(); + + const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([ + "addIdentityToProject", + "removeIdentityFromProject" + ] as const); + + const onRemoveIdentitySubmit = async (id: string, projectId: string) => { + try { + await deleteMutateAsync({ + identityId: id, + workspaceId: projectId + }); + + createNotification({ + text: "Successfully removed identity from project", + type: "success" + }); + + handlePopUpClose("removeIdentityFromProject"); + } catch (err) { + console.error(err); + const error = err as any; + const text = error?.response?.data?.message ?? "Failed to remove identity from project"; + + createNotification({ + text, + type: "error" + }); + } + }; + + return ( +
+
+

Projects

+ { + handlePopUpOpen("addIdentityToProject"); + }} + > + + +
+
+ +
+ handlePopUpToggle("removeIdentityFromProject", isOpen)} + deleteKey="confirm" + onDeleteApproved={() => { + const popupData = popUp?.removeIdentityFromProject?.data as { + identityId: string; + projectId: string; + }; + + return onRemoveIdentitySubmit(popupData.identityId, popupData.projectId); + }} + /> + +
+ ); +}; diff --git a/frontend/src/views/Org/IdentityPage/components/IdentityProjectsSection/IdentityProjectsTable.tsx b/frontend/src/views/Org/IdentityPage/components/IdentityProjectsSection/IdentityProjectsTable.tsx new file mode 100644 index 000000000..a005ad1c8 --- /dev/null +++ b/frontend/src/views/Org/IdentityPage/components/IdentityProjectsSection/IdentityProjectsTable.tsx @@ -0,0 +1,58 @@ +import { faFolder } from "@fortawesome/free-solid-svg-icons"; + +import { + EmptyState, + Table, + TableContainer, + TableSkeleton, + TBody, + Th, + THead, + Tr +} from "@app/components/v2"; +import { useGetIdentityProjectMemberships } from "@app/hooks/api"; +import { UsePopUpState } from "@app/hooks/usePopUp"; + +import { IdentityProjectRow } from "./IdentityProjectRow"; + +type Props = { + identityId: string; + handlePopUpOpen: ( + popUpName: keyof UsePopUpState<["removeIdentityFromProject"]>, + data?: {} + ) => void; +}; + +export const IdentityProjectsTable = ({ identityId, handlePopUpOpen }: Props) => { + const { data: projectMemberships, isLoading } = useGetIdentityProjectMemberships(identityId); + return ( + + + + + + + + + + + {isLoading && } + {!isLoading && + projectMemberships?.map((membership) => { + return ( + + ); + })} + +
NameRoleAdded On +
+ {!isLoading && !projectMemberships?.length && ( + + )} +
+ ); +}; diff --git a/frontend/src/views/Org/IdentityPage/components/IdentityProjectsSection/index.tsx b/frontend/src/views/Org/IdentityPage/components/IdentityProjectsSection/index.tsx new file mode 100644 index 000000000..4eb7d40a9 --- /dev/null +++ b/frontend/src/views/Org/IdentityPage/components/IdentityProjectsSection/index.tsx @@ -0,0 +1 @@ +export { IdentityProjectsSection } from "./IdentityProjectsSection"; diff --git a/frontend/src/views/Org/IdentityPage/components/IdentityTokenModal.tsx b/frontend/src/views/Org/IdentityPage/components/IdentityTokenModal.tsx index dfc26592e..e4f04752a 100644 --- a/frontend/src/views/Org/IdentityPage/components/IdentityTokenModal.tsx +++ b/frontend/src/views/Org/IdentityPage/components/IdentityTokenModal.tsx @@ -92,7 +92,6 @@ export const IdentityTokenModal = ({ popUp, handlePopUpToggle }: Props) => { }); setToken(newTokenData.accessToken); - // note: may be helpful to tell user ttl etc. } createNotification({ diff --git a/frontend/src/views/Org/IdentityPage/components/index.tsx b/frontend/src/views/Org/IdentityPage/components/index.tsx index f9a810b7d..8fcbcfc21 100644 --- a/frontend/src/views/Org/IdentityPage/components/index.tsx +++ b/frontend/src/views/Org/IdentityPage/components/index.tsx @@ -1,6 +1,6 @@ export { IdentityAuthenticationSection } from "./IdentityAuthenticationSection/IdentityAuthenticationSection"; export { IdentityClientSecretModal } from "./IdentityClientSecretModal"; export { IdentityDetailsSection } from "./IdentityDetailsSection"; -export { IdentityProjectsSection } from "./IdentityProjectsSection"; +export { IdentityProjectsSection } from "./IdentityProjectsSection/IdentityProjectsSection"; export { IdentityTokenListModal } from "./IdentityTokenListModal"; export { IdentityTokenModal } from "./IdentityTokenModal"; diff --git a/frontend/src/views/Org/MembersPage/MembersPage.tsx b/frontend/src/views/Org/MembersPage/MembersPage.tsx index 99edd5015..0a9363d09 100644 --- a/frontend/src/views/Org/MembersPage/MembersPage.tsx +++ b/frontend/src/views/Org/MembersPage/MembersPage.tsx @@ -3,16 +3,10 @@ import { Tab, TabList, TabPanel, Tabs } from "@app/components/v2"; import { OrgPermissionActions, OrgPermissionSubjects } from "@app/context"; import { withPermission } from "@app/hoc"; -import { - OrgGroupsTab, - OrgIdentityTab, - OrgMembersTab, - OrgRoleTabSection -} from "./components"; +import { OrgIdentityTab, OrgMembersTab, OrgRoleTabSection } from "./components"; enum TabSections { Member = "members", - Groups = "groups", Roles = "roles", Identities = "identities" } @@ -25,8 +19,7 @@ export const MembersPage = withPermission(

Organization Access Control

- People - Groups + Users

Machine Identities

@@ -37,9 +30,6 @@ export const MembersPage = withPermission( - - - diff --git a/frontend/src/views/Org/MembersPage/components/OrgGroupsTab/components/OrgGroupsSection/OrgGroupsSection.tsx b/frontend/src/views/Org/MembersPage/components/OrgGroupsTab/components/OrgGroupsSection/OrgGroupsSection.tsx index 320c115fd..2df78fe5f 100644 --- a/frontend/src/views/Org/MembersPage/components/OrgGroupsTab/components/OrgGroupsSection/OrgGroupsSection.tsx +++ b/frontend/src/views/Org/MembersPage/components/OrgGroupsTab/components/OrgGroupsSection/OrgGroupsSection.tsx @@ -3,16 +3,8 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { createNotification } from "@app/components/notifications"; import { OrgPermissionCan } from "@app/components/permissions"; -import { - Button, - DeleteActionModal, - UpgradePlanModal -} from "@app/components/v2"; -import { - OrgPermissionActions, - OrgPermissionSubjects, - useSubscription -} from "@app/context"; +import { Button, DeleteActionModal, UpgradePlanModal } from "@app/components/v2"; +import { OrgPermissionActions, OrgPermissionSubjects, useSubscription } from "@app/context"; import { useDeleteGroup } from "@app/hooks/api"; import { usePopUp } from "@app/hooks/usePopUp"; @@ -21,100 +13,88 @@ import { OrgGroupModal } from "./OrgGroupModal"; import { OrgGroupsTable } from "./OrgGroupsTable"; export const OrgGroupsSection = () => { - const { subscription } = useSubscription(); - const { mutateAsync: deleteMutateAsync } = useDeleteGroup(); - - const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([ - "group", - "groupMembers", - "deleteGroup", - "upgradePlan" - ] as const); - - const handleAddGroupModal = () => { - if (!subscription?.groups) { - handlePopUpOpen("upgradePlan", { - description: "You can manage users more efficiently with groups if you upgrade your Infisical plan." - }); - } else { - handlePopUpOpen("group"); - } + const { subscription } = useSubscription(); + const { mutateAsync: deleteMutateAsync } = useDeleteGroup(); + + const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([ + "group", + "groupMembers", + "deleteGroup", + "upgradePlan" + ] as const); + + const handleAddGroupModal = () => { + if (!subscription?.groups) { + handlePopUpOpen("upgradePlan", { + description: + "You can manage users more efficiently with groups if you upgrade your Infisical plan." + }); + } else { + handlePopUpOpen("group"); } - - const onDeleteGroupSubmit = async ({ - name, + }; + + const onDeleteGroupSubmit = async ({ name, slug }: { name: string; slug: string }) => { + try { + await deleteMutateAsync({ slug - }: { - name: string; - slug: string; - }) => { - try { - await deleteMutateAsync({ - slug - }); - createNotification({ - text: `Successfully deleted the group named ${name}`, - type: "success" - }); - } catch (err) { - console.error(err); - createNotification({ - text: `Failed to delete the group named ${name}`, - type: "error" - }); - } - - handlePopUpClose("deleteGroup"); + }); + createNotification({ + text: `Successfully deleted the group named ${name}`, + type: "success" + }); + } catch (err) { + console.error(err); + createNotification({ + text: `Failed to delete the group named ${name}`, + type: "error" + }); } - - return ( -
-
-

Groups

- - {(isAllowed) => ( - - )} - -
- - - - handlePopUpToggle("deleteGroup", isOpen)} - deleteKey="confirm" - onDeleteApproved={() => - onDeleteGroupSubmit( - (popUp?.deleteGroup?.data as { name: string; slug: string }) - ) - } - /> - handlePopUpToggle("upgradePlan", isOpen)} - text={(popUp.upgradePlan?.data as { description: string })?.description} - /> -
- ); -} \ No newline at end of file + + handlePopUpClose("deleteGroup"); + }; + + return ( +
+
+

User Groups

+ + {(isAllowed) => ( + + )} + +
+ + + + handlePopUpToggle("deleteGroup", isOpen)} + deleteKey="confirm" + onDeleteApproved={() => + onDeleteGroupSubmit(popUp?.deleteGroup?.data as { name: string; slug: string }) + } + /> + handlePopUpToggle("upgradePlan", isOpen)} + text={(popUp.upgradePlan?.data as { description: string })?.description} + /> +
+ ); +}; diff --git a/frontend/src/views/Org/MembersPage/components/OrgGroupsTab/components/OrgGroupsSection/OrgGroupsTable.tsx b/frontend/src/views/Org/MembersPage/components/OrgGroupsTab/components/OrgGroupsSection/OrgGroupsTable.tsx index 79e08bc14..336022189 100644 --- a/frontend/src/views/Org/MembersPage/components/OrgGroupsTab/components/OrgGroupsSection/OrgGroupsTable.tsx +++ b/frontend/src/views/Org/MembersPage/components/OrgGroupsTab/components/OrgGroupsSection/OrgGroupsTable.tsx @@ -1,12 +1,16 @@ import { useState } from "react"; -import { faMagnifyingGlass, faPencil, faUsers, faXmark } from "@fortawesome/free-solid-svg-icons"; +import { faEllipsis,faMagnifyingGlass, faUsers } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { twMerge } from "tailwind-merge"; import { createNotification } from "@app/components/notifications"; import { OrgPermissionCan } from "@app/components/permissions"; import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, EmptyState, - IconButton, Input, Select, SelectItem, @@ -17,218 +21,200 @@ import { Td, Th, THead, - Tooltip, Tr } from "@app/components/v2"; -import { - OrgPermissionActions, - OrgPermissionSubjects, - useOrganization} from "@app/context"; -import { - useGetOrganizationGroups, - useGetOrgRoles, - useUpdateGroup -} from "@app/hooks/api"; +import { OrgPermissionActions, OrgPermissionSubjects, useOrganization } from "@app/context"; +import { useGetOrganizationGroups, useGetOrgRoles, useUpdateGroup } from "@app/hooks/api"; import { UsePopUpState } from "@app/hooks/usePopUp"; type Props = { - handlePopUpOpen: ( - popUpName: keyof UsePopUpState< - ["group", "deleteGroup", "groupMembers"] - >, - data?: { - groupId?: string; - name?: string; - slug?: string; - role?: string; - customRole?: { - name: string; - slug: string; - } - } - ) => void; - }; + handlePopUpOpen: ( + popUpName: keyof UsePopUpState<["group", "deleteGroup", "groupMembers"]>, + data?: { + groupId?: string; + name?: string; + slug?: string; + role?: string; + customRole?: { + name: string; + slug: string; + }; + } + ) => void; +}; -export const OrgGroupsTable = ({ - handlePopUpOpen -}: Props) => { - const [searchGroupsFilter, setSearchGroupsFilter] = useState(""); - const { currentOrg } = useOrganization(); - const orgId = currentOrg?.id || ""; - const { isLoading, data: groups } = useGetOrganizationGroups(orgId); - const { mutateAsync: updateMutateAsync } = useUpdateGroup(); - - const { data: roles } = useGetOrgRoles(orgId); - - const handleChangeRole = async ({ +export const OrgGroupsTable = ({ handlePopUpOpen }: Props) => { + const [searchGroupsFilter, setSearchGroupsFilter] = useState(""); + const { currentOrg } = useOrganization(); + const orgId = currentOrg?.id || ""; + const { isLoading, data: groups } = useGetOrganizationGroups(orgId); + const { mutateAsync: updateMutateAsync } = useUpdateGroup(); + + const { data: roles } = useGetOrgRoles(orgId); + + const handleChangeRole = async ({ currentSlug, role }: { currentSlug: string; role: string }) => { + try { + await updateMutateAsync({ currentSlug, role - }: { - currentSlug: string; - role: string; - }) => { - try { - await updateMutateAsync({ - currentSlug, - role - }); - - createNotification({ - text: "Successfully updated group role", - type: "success" - }); - } catch (err) { - console.error(err); - createNotification({ - text: "Failed to update group role", - type: "error" - }); - } + }); + + createNotification({ + text: "Successfully updated group role", + type: "success" + }); + } catch (err) { + console.error(err); + createNotification({ + text: "Failed to update group role", + type: "error" + }); } - - return ( -
- setSearchGroupsFilter(e.target.value)} - leftIcon={} - placeholder="Search groups..." - /> - - - - - - - - - - - {isLoading && } - {!isLoading && groups?.map(({ id, name, slug, role, customRole }) => { - return ( - - - - - - - ); - })} - -
NameSlugRole -
{name}{slug} - - {(isAllowed) => { - return ( - - ); - }} - - -
- - {(isAllowed) => ( - - { - handlePopUpOpen("groupMembers", { - slug - }); - }} - size="lg" - colorSchema="primary" - variant="plain" - ariaLabel="update" - isDisabled={!isAllowed} - > - - - - )} - - - {(isAllowed) => ( - - { - handlePopUpOpen("group", { - groupId: id, - name, - slug, - role, - customRole - }); - }} - size="lg" - colorSchema="primary" - variant="plain" - ariaLabel="update" - className="ml-4" - isDisabled={!isAllowed} - > - - - - )} - - - {(isAllowed) => ( - - { - handlePopUpOpen("deleteGroup", { - slug, - name - }); - }} - size="lg" - colorSchema="danger" - variant="plain" - ariaLabel="update" - className="ml-4" - isDisabled={!isAllowed} - > - - - - )} - -
-
- {groups?.length === 0 && ( - - )} -
-
- ); -} + }; + + return ( +
+ setSearchGroupsFilter(e.target.value)} + leftIcon={} + placeholder="Search groups..." + /> + + + + + + + + + + + {isLoading && } + {!isLoading && + groups?.map(({ id, name, slug, role, customRole }) => { + return ( + + + + + + + ); + })} + +
NameSlugRole +
{name}{slug} + + {(isAllowed) => { + return ( + + ); + }} + + + + +
+ +
+
+ + + {(isAllowed) => ( + { + e.stopPropagation(); + handlePopUpOpen("groupMembers", { + slug + }); + }} + disabled={!isAllowed} + > + Manage Users + + )} + + + {(isAllowed) => ( + { + e.stopPropagation(); + handlePopUpOpen("group", { + groupId: id, + name, + slug, + role, + customRole + }); + }} + disabled={!isAllowed} + > + Edit Group + + )} + + + {(isAllowed) => ( + { + e.stopPropagation(); + handlePopUpOpen("deleteGroup", { + slug, + name + }); + }} + disabled={!isAllowed} + > + Delete Group + + )} + + +
+
+ {groups?.length === 0 && } +
+
+ ); +}; 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 5acdd6e3b..b144c9cc8 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 @@ -11,22 +11,25 @@ import { ModalContent, Select, SelectItem, - UpgradePlanModal} from "@app/components/v2"; + UpgradePlanModal +} from "@app/components/v2"; import { useOrganization } from "@app/context"; import { useDeleteIdentityAwsAuth, useDeleteIdentityAzureAuth, useDeleteIdentityGcpAuth, useDeleteIdentityKubernetesAuth, + useDeleteIdentityOidcAuth, useDeleteIdentityTokenAuth, useDeleteIdentityUniversalAuth} from "@app/hooks/api"; -import { IdentityAuthMethod , identityAuthToNameMap } from "@app/hooks/api/identities"; +import { IdentityAuthMethod, identityAuthToNameMap } from "@app/hooks/api/identities"; import { UsePopUpState } from "@app/hooks/usePopUp"; import { IdentityAwsAuthForm } from "./IdentityAwsAuthForm"; import { IdentityAzureAuthForm } from "./IdentityAzureAuthForm"; import { IdentityGcpAuthForm } from "./IdentityGcpAuthForm"; import { IdentityKubernetesAuthForm } from "./IdentityKubernetesAuthForm"; +import { IdentityOidcAuthForm } from "./IdentityOidcAuthForm"; import { IdentityTokenAuthForm } from "./IdentityTokenAuthForm"; import { IdentityUniversalAuthForm } from "./IdentityUniversalAuthForm"; @@ -45,7 +48,8 @@ const identityAuthMethods = [ { label: "Kubernetes Auth", value: IdentityAuthMethod.KUBERNETES_AUTH }, { label: "GCP Auth", value: IdentityAuthMethod.GCP_AUTH }, { label: "AWS Auth", value: IdentityAuthMethod.AWS_AUTH }, - { label: "Azure Auth", value: IdentityAuthMethod.AZURE_AUTH } + { label: "Azure Auth", value: IdentityAuthMethod.AZURE_AUTH }, + { label: "OIDC Auth", value: IdentityAuthMethod.OIDC_AUTH } ]; const schema = yup @@ -66,6 +70,7 @@ export const IdentityAuthMethodModal = ({ popUp, handlePopUpOpen, handlePopUpTog const { mutateAsync: revokeGcpAuth } = useDeleteIdentityGcpAuth(); const { mutateAsync: revokeAwsAuth } = useDeleteIdentityAwsAuth(); const { mutateAsync: revokeAzureAuth } = useDeleteIdentityAzureAuth(); + const { mutateAsync: revokeOidcAuth } = useDeleteIdentityOidcAuth(); const { control, watch, setValue } = useForm({ resolver: yupResolver(schema), @@ -138,6 +143,15 @@ export const IdentityAuthMethodModal = ({ popUp, handlePopUpOpen, handlePopUpTog /> ); } + case IdentityAuthMethod.OIDC_AUTH: { + return ( + + ); + } case IdentityAuthMethod.TOKEN_AUTH: { return ( Number(value) <= 315360000 + ), + accessTokenMaxTTL: yup + .string() + .required("Access Max Token TTL is required") + .test( + "is-value-valid", + "Access Token Max TTL cannot be greater than 315360000", + (value) => Number(value) <= 315360000 + ), accessTokenNumUsesLimit: yup.string().required("Access Token Max Number of Uses is required"), accessTokenTrustedIps: yup .array( diff --git a/frontend/src/views/Org/MembersPage/components/OrgIdentityTab/components/IdentitySection/IdentityAzureAuthForm.tsx b/frontend/src/views/Org/MembersPage/components/OrgIdentityTab/components/IdentitySection/IdentityAzureAuthForm.tsx index c0aaca2f6..a3902d120 100644 --- a/frontend/src/views/Org/MembersPage/components/OrgIdentityTab/components/IdentitySection/IdentityAzureAuthForm.tsx +++ b/frontend/src/views/Org/MembersPage/components/OrgIdentityTab/components/IdentitySection/IdentityAzureAuthForm.tsx @@ -22,8 +22,12 @@ const schema = z tenantId: z.string(), resource: z.string(), allowedServicePrincipalIds: z.string(), - accessTokenTTL: z.string(), - accessTokenMaxTTL: z.string(), + accessTokenTTL: z.string().refine((val) => Number(val) <= 315360000, { + message: "Access Token TTL cannot be greater than 315360000" + }), + accessTokenMaxTTL: z.string().refine((val) => Number(val) <= 315360000, { + message: "Access Token Max TTL cannot be greater than 315360000" + }), accessTokenNumUsesLimit: z.string(), accessTokenTrustedIps: z .array( diff --git a/frontend/src/views/Org/MembersPage/components/OrgIdentityTab/components/IdentitySection/IdentityGcpAuthForm.tsx b/frontend/src/views/Org/MembersPage/components/OrgIdentityTab/components/IdentitySection/IdentityGcpAuthForm.tsx index eb07c3c36..a0834b3ae 100644 --- a/frontend/src/views/Org/MembersPage/components/OrgIdentityTab/components/IdentitySection/IdentityGcpAuthForm.tsx +++ b/frontend/src/views/Org/MembersPage/components/OrgIdentityTab/components/IdentitySection/IdentityGcpAuthForm.tsx @@ -23,8 +23,12 @@ const schema = z allowedServiceAccounts: z.string(), allowedProjects: z.string(), allowedZones: z.string(), - accessTokenTTL: z.string(), - accessTokenMaxTTL: z.string(), + accessTokenTTL: z.string().refine((val) => Number(val) <= 315360000, { + message: "Access Token TTL cannot be greater than 315360000" + }), + accessTokenMaxTTL: z.string().refine((val) => Number(val) <= 315360000, { + message: "Access Token Max TTL cannot be greater than 315360000" + }), accessTokenNumUsesLimit: z.string(), accessTokenTrustedIps: z .array( diff --git a/frontend/src/views/Org/MembersPage/components/OrgIdentityTab/components/IdentitySection/IdentityKubernetesAuthForm.tsx b/frontend/src/views/Org/MembersPage/components/OrgIdentityTab/components/IdentitySection/IdentityKubernetesAuthForm.tsx index 6baa6a405..3724fc014 100644 --- a/frontend/src/views/Org/MembersPage/components/OrgIdentityTab/components/IdentitySection/IdentityKubernetesAuthForm.tsx +++ b/frontend/src/views/Org/MembersPage/components/OrgIdentityTab/components/IdentitySection/IdentityKubernetesAuthForm.tsx @@ -25,8 +25,12 @@ const schema = z allowedNamespaces: z.string(), allowedAudience: z.string(), caCert: z.string(), - accessTokenTTL: z.string(), - accessTokenMaxTTL: z.string(), + accessTokenTTL: z.string().refine((val) => Number(val) <= 315360000, { + message: "Access Token TTL cannot be greater than 315360000" + }), + accessTokenMaxTTL: z.string().refine((val) => Number(val) <= 315360000, { + message: "Access Token Max TTL cannot be greater than 315360000" + }), accessTokenNumUsesLimit: z.string(), accessTokenTrustedIps: z .array( diff --git a/frontend/src/views/Org/MembersPage/components/OrgIdentityTab/components/IdentitySection/IdentityOidcAuthForm.tsx b/frontend/src/views/Org/MembersPage/components/OrgIdentityTab/components/IdentitySection/IdentityOidcAuthForm.tsx new file mode 100644 index 000000000..30663d470 --- /dev/null +++ b/frontend/src/views/Org/MembersPage/components/OrgIdentityTab/components/IdentitySection/IdentityOidcAuthForm.tsx @@ -0,0 +1,487 @@ +import { useEffect } from "react"; +import { Controller, useFieldArray, useForm } from "react-hook-form"; +import { faPlus, faXmark } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { z } from "zod"; + +import { createNotification } from "@app/components/notifications"; +import { Button, FormControl, IconButton, Input, TextArea } from "@app/components/v2"; +import { useOrganization, useSubscription } from "@app/context"; +import { useAddIdentityOidcAuth, useUpdateIdentityOidcAuth } from "@app/hooks/api"; +import { IdentityAuthMethod } from "@app/hooks/api/identities"; +import { useGetIdentityOidcAuth } from "@app/hooks/api/identities/queries"; +import { IdentityTrustedIp } from "@app/hooks/api/identities/types"; +import { UsePopUpState } from "@app/hooks/usePopUp"; + +const schema = z.object({ + accessTokenTrustedIps: z + .array( + z.object({ + ipAddress: z.string().max(50) + }) + ) + .min(1), + accessTokenTTL: z.string().refine((val) => Number(val) <= 315360000, { + message: "Access Token TTL cannot be greater than 315360000" + }), + accessTokenMaxTTL: z.string().refine((val) => Number(val) <= 315360000, { + message: "Access Token Max TTL cannot be greater than 315360000" + }), + accessTokenNumUsesLimit: z.string(), + oidcDiscoveryUrl: z.string().url().min(1), + caCert: z.string().trim().default(""), + boundIssuer: z.string().min(1), + boundAudiences: z.string().optional().default(""), + boundClaims: z.array( + z.object({ + key: z.string(), + value: z.string() + }) + ), + boundSubject: z.string().optional().default("") +}); + +export type FormData = z.infer; + +type Props = { + handlePopUpOpen: (popUpName: keyof UsePopUpState<["upgradePlan"]>) => void; + handlePopUpToggle: ( + popUpName: keyof UsePopUpState<["identityAuthMethod", "revokeAuthMethod"]>, + state?: boolean + ) => void; + identityAuthMethodData: { + identityId: string; + name: string; + authMethod?: IdentityAuthMethod; + }; +}; + +export const IdentityOidcAuthForm = ({ + handlePopUpOpen, + handlePopUpToggle, + identityAuthMethodData +}: Props) => { + const { currentOrg } = useOrganization(); + const orgId = currentOrg?.id || ""; + const { subscription } = useSubscription(); + + const { mutateAsync: addMutateAsync } = useAddIdentityOidcAuth(); + const { mutateAsync: updateMutateAsync } = useUpdateIdentityOidcAuth(); + + const { data } = useGetIdentityOidcAuth(identityAuthMethodData?.identityId ?? ""); + + const { + control, + handleSubmit, + reset, + formState: { isSubmitting } + } = useForm({ + resolver: zodResolver(schema), + defaultValues: { + accessTokenTTL: "2592000", + accessTokenMaxTTL: "2592000", + accessTokenNumUsesLimit: "0", + accessTokenTrustedIps: [{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }] + } + }); + + const { + fields: boundClaimsFields, + append: appendBoundClaimField, + remove: removeBoundClaimField + } = useFieldArray({ + control, + name: "boundClaims" + }); + + const { + fields: accessTokenTrustedIpsFields, + append: appendAccessTokenTrustedIp, + remove: removeAccessTokenTrustedIp + } = useFieldArray({ control, name: "accessTokenTrustedIps" }); + + useEffect(() => { + if (data) { + reset({ + oidcDiscoveryUrl: data.oidcDiscoveryUrl, + caCert: data.caCert, + boundIssuer: data.boundIssuer, + boundAudiences: data.boundAudiences, + boundClaims: Object.entries(data.boundClaims).map(([key, value]) => ({ + key, + value + })), + boundSubject: data.boundSubject, + 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({ + oidcDiscoveryUrl: "", + caCert: "", + boundIssuer: "", + boundAudiences: "", + boundClaims: [], + boundSubject: "", + accessTokenTTL: "2592000", + accessTokenMaxTTL: "2592000", + accessTokenNumUsesLimit: "0", + accessTokenTrustedIps: [{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }] + }); + } + }, [data]); + + const onFormSubmit = async ({ + accessTokenTrustedIps, + accessTokenTTL, + accessTokenMaxTTL, + accessTokenNumUsesLimit, + oidcDiscoveryUrl, + caCert, + boundIssuer, + boundAudiences, + boundClaims, + boundSubject + }: FormData) => { + try { + if (!identityAuthMethodData) { + return; + } + + if (data) { + await updateMutateAsync({ + identityId: identityAuthMethodData.identityId, + organizationId: orgId, + oidcDiscoveryUrl, + caCert, + boundIssuer, + boundAudiences, + boundClaims: Object.fromEntries(boundClaims.map((entry) => [entry.key, entry.value])), + boundSubject, + accessTokenTTL: Number(accessTokenTTL), + accessTokenMaxTTL: Number(accessTokenMaxTTL), + accessTokenNumUsesLimit: Number(accessTokenNumUsesLimit), + accessTokenTrustedIps + }); + } else { + await addMutateAsync({ + identityId: identityAuthMethodData.identityId, + oidcDiscoveryUrl, + caCert, + boundIssuer, + boundAudiences, + boundClaims: Object.fromEntries(boundClaims.map((entry) => [entry.key, entry.value])), + boundSubject, + organizationId: orgId, + 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 ( +
+ ( + + + + )} + /> + ( + + + + )} + /> + ( + +