diff --git a/backend/package-lock.json b/backend/package-lock.json index 62426ad22..c743979e6 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -20,6 +20,7 @@ "@types/crypto-js": "^4.1.1", "@types/libsodium-wrappers": "^0.7.10", "@ucast/mongo2js": "^1.3.4", + "ajv": "^8.12.0", "argon2": "^0.30.3", "aws-sdk": "^2.1364.0", "axios": "^1.3.5", @@ -38,12 +39,14 @@ "helmet": "^5.1.1", "infisical-node": "^1.2.1", "ioredis": "^5.3.2", + "jmespath": "^0.16.0", "js-yaml": "^4.1.0", "jsonwebtoken": "^9.0.0", "jsrp": "^0.2.4", "libsodium-wrappers": "^0.7.10", "lodash": "^4.17.21", "mongoose": "^7.4.1", + "mysql2": "^3.6.2", "nanoid": "^3.3.6", "node-cache": "^5.1.2", "nodemailer": "^6.8.0", @@ -51,6 +54,7 @@ "passport-github": "^1.1.0", "passport-gitlab2": "^5.0.0", "passport-google-oauth20": "^2.0.0", + "pg": "^8.11.3", "pino": "^8.16.1", "pino-http": "^8.5.1", "posthog-node": "^2.6.0", @@ -75,11 +79,13 @@ "@types/cors": "^2.8.12", "@types/express": "^4.17.14", "@types/jest": "^29.5.0", + "@types/jmespath": "^0.15.1", "@types/jsonwebtoken": "^8.5.9", "@types/lodash": "^4.14.191", "@types/node": "^18.11.3", "@types/nodemailer": "^6.4.6", "@types/passport": "^1.0.12", + "@types/pg": "^8.10.7", "@types/picomatch": "^2.3.0", "@types/pino": "^7.0.5", "@types/supertest": "^2.0.12", @@ -3507,6 +3513,28 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/@eslint/eslintrc/node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/@eslint/eslintrc/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, "node_modules/@eslint/js": { "version": "8.44.0", "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.44.0.tgz", @@ -5849,6 +5877,12 @@ "pretty-format": "^29.0.0" } }, + "node_modules/@types/jmespath": { + "version": "0.15.1", + "resolved": "https://registry.npmjs.org/@types/jmespath/-/jmespath-0.15.1.tgz", + "integrity": "sha512-RWN1HQ71Hjl2ixw4a8s7/Bcz6S9uaBTaoCQ5cJB7OsjgHBFi3GaWMy0vRgZBPSYXdsMKFNxGLUUEh9uRf00Spw==", + "dev": true + }, "node_modules/@types/js-yaml": { "version": "4.0.5", "resolved": "https://registry.npmjs.org/@types/js-yaml/-/js-yaml-4.0.5.tgz", @@ -5921,6 +5955,74 @@ "@types/passport": "*" } }, + "node_modules/@types/pg": { + "version": "8.10.7", + "resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.10.7.tgz", + "integrity": "sha512-ksJqHipwYaSEHz9e1fr6H6erjoEdNNaOxwyJgPx9bNeaqOW3iWBQgVHfpwiSAoqGzchfc+ZyRLwEfeCcyYD3uQ==", + "dev": true, + "dependencies": { + "@types/node": "*", + "pg-protocol": "*", + "pg-types": "^4.0.1" + } + }, + "node_modules/@types/pg/node_modules/pg-types": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-4.0.1.tgz", + "integrity": "sha512-hRCSDuLII9/LE3smys1hRHcu5QGcLs9ggT7I/TCs0IE+2Eesxi9+9RWAAwZ0yaGjxoWICF/YHLOEjydGujoJ+g==", + "dev": true, + "dependencies": { + "pg-int8": "1.0.1", + "pg-numeric": "1.0.2", + "postgres-array": "~3.0.1", + "postgres-bytea": "~3.0.0", + "postgres-date": "~2.0.1", + "postgres-interval": "^3.0.0", + "postgres-range": "^1.1.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@types/pg/node_modules/postgres-array": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-3.0.2.tgz", + "integrity": "sha512-6faShkdFugNQCLwucjPcY5ARoW1SlbnrZjmGl0IrrqewpvxvhSLHimCVzqeuULCbG0fQv7Dtk1yDbG3xv7Veog==", + "dev": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/@types/pg/node_modules/postgres-bytea": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-3.0.0.tgz", + "integrity": "sha512-CNd4jim9RFPkObHSjVHlVrxoVQXz7quwNFpz7RY1okNNme49+sVyiTvTRobiLV548Hx/hb1BG+iE7h9493WzFw==", + "dev": true, + "dependencies": { + "obuf": "~1.1.2" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/@types/pg/node_modules/postgres-date": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-2.0.1.tgz", + "integrity": "sha512-YtMKdsDt5Ojv1wQRvUhnyDJNSr2dGIC96mQVKz7xufp07nfuFONzdaowrMHjlAzY6GDLd4f+LUHHAAM1h4MdUw==", + "dev": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/@types/pg/node_modules/postgres-interval": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-3.0.0.tgz", + "integrity": "sha512-BSNDnbyZCXSxgA+1f5UU2GmwhoI0aU5yMxRGO8CdFEcY2BQF9xm/7MqKnYoM1nJDk8nONNWDk9WeSmePFhQdlw==", + "dev": true, + "engines": { + "node": ">=12" + } + }, "node_modules/@types/picomatch": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/@types/picomatch/-/picomatch-2.3.0.tgz", @@ -6419,14 +6521,13 @@ } }, "node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "dev": true, + "version": "8.12.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", + "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", "dependencies": { "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", "uri-js": "^4.2.2" }, "funding": { @@ -7063,6 +7164,14 @@ "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", "dev": true }, + "node_modules/buffer-writer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/buffer-writer/-/buffer-writer-2.0.0.tgz", + "integrity": "sha512-a7ZpuTZU1TRtnwyCNW3I5dc0wWNC3VR9S++Ewyk2HHZdrO3CQJqSpd+95Us590V6AL7JqUAH2IwZ/398PmNFgw==", + "engines": { + "node": ">=4" + } + }, "node_modules/bull": { "version": "4.10.4", "resolved": "https://registry.npmjs.org/bull/-/bull-4.10.4.tgz", @@ -7912,6 +8021,22 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/eslint/node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, "node_modules/eslint/node_modules/eslint-scope": { "version": "7.2.1", "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.1.tgz", @@ -7937,6 +8062,12 @@ "node": ">=4.0" } }, + "node_modules/eslint/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, "node_modules/espree": { "version": "9.6.1", "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", @@ -8267,8 +8398,7 @@ "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" }, "node_modules/fast-glob": { "version": "3.3.0", @@ -8613,6 +8743,14 @@ "node": ">=10" } }, + "node_modules/generate-function": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.3.1.tgz", + "integrity": "sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ==", + "dependencies": { + "is-property": "^1.0.2" + } + }, "node_modules/gensync": { "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", @@ -9288,6 +9426,11 @@ "node": ">=0.10.0" } }, + "node_modules/is-property": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz", + "integrity": "sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g==" + }, "node_modules/is-retry-allowed": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/is-retry-allowed/-/is-retry-allowed-2.2.0.tgz", @@ -10038,10 +10181,9 @@ "dev": true }, "node_modules/json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==" }, "node_modules/json-stable-stringify-without-jsonify": { "version": "1.0.1", @@ -10268,6 +10410,11 @@ "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", "dev": true }, + "node_modules/long": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/long/-/long-5.2.3.tgz", + "integrity": "sha512-lcHwpNoggQTObv5apGNCTdJrO69eHOZMi4BNC+rTLER8iHAqGrUVeLh/irVIM7zTw2bOXA8T6uNPeujwOLg/2Q==" + }, "node_modules/lru_map": { "version": "0.3.3", "resolved": "https://registry.npmjs.org/lru_map/-/lru_map-0.3.3.tgz", @@ -10715,6 +10862,78 @@ "@msgpackr-extract/msgpackr-extract-win32-x64": "3.0.2" } }, + "node_modules/mysql2": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.6.2.tgz", + "integrity": "sha512-m5erE6bMoWfPXW1D5UrVwlT8PowAoSX69KcZzPuARQ3wY1RJ52NW9PdvdPo076XiSIkQ5IBTis7hxdlrQTlyug==", + "dependencies": { + "denque": "^2.1.0", + "generate-function": "^2.3.1", + "iconv-lite": "^0.6.3", + "long": "^5.2.1", + "lru-cache": "^8.0.0", + "named-placeholders": "^1.1.3", + "seq-queue": "^0.0.5", + "sqlstring": "^2.3.2" + }, + "engines": { + "node": ">= 8.0" + } + }, + "node_modules/mysql2/node_modules/denque": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz", + "integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==", + "engines": { + "node": ">=0.10" + } + }, + "node_modules/mysql2/node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/mysql2/node_modules/lru-cache": { + "version": "8.0.5", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-8.0.5.tgz", + "integrity": "sha512-MhWWlVnuab1RG5/zMRRcVGXZLCXrZTgfwMikgzCegsPnG62yDQo5JnqKkrK4jO5iKqDAZGItAqN5CtKBCBWRUA==", + "engines": { + "node": ">=16.14" + } + }, + "node_modules/mysql2/node_modules/sqlstring": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/sqlstring/-/sqlstring-2.3.3.tgz", + "integrity": "sha512-qC9iz2FlN7DQl3+wjwn3802RTyjCx7sDvfQEXchwa6CWOx07/WVfh91gBmQ9fahw8snwGEWU3xGzOt4tFyHLxg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/named-placeholders": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/named-placeholders/-/named-placeholders-1.1.3.tgz", + "integrity": "sha512-eLoBxg6wE/rZkJPhU/xRX1WTpkFEwDJEN96oxFrTsqBdbT5ec295Q+CoHrL9IT0DipqKhmGcaZmwOt8OON5x1w==", + "dependencies": { + "lru-cache": "^7.14.1" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/named-placeholders/node_modules/lru-cache": { + "version": "7.18.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", + "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", + "engines": { + "node": ">=12" + } + }, "node_modules/nanoid": { "version": "3.3.6", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.6.tgz", @@ -13543,6 +13762,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/obuf": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/obuf/-/obuf-1.1.2.tgz", + "integrity": "sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==", + "dev": true + }, "node_modules/octokit-auth-probot": { "version": "1.2.9", "resolved": "https://registry.npmjs.org/octokit-auth-probot/-/octokit-auth-probot-1.2.9.tgz", @@ -13684,6 +13909,11 @@ "node": ">=6" } }, + "node_modules/packet-reader": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/packet-reader/-/packet-reader-1.0.0.tgz", + "integrity": "sha512-HAKu/fG3HpHFO0AA8WE8q2g+gBJaZ9MG7fcKk+IJPLTGAD6Psw4443l+9DGRbOIh3/aXr7Phy0TjilYivJo5XQ==" + }, "node_modules/parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", @@ -13849,6 +14079,98 @@ "resolved": "https://registry.npmjs.org/pause/-/pause-0.0.1.tgz", "integrity": "sha512-KG8UEiEVkR3wGEb4m5yZkVCzigAD+cVEJck2CzYZO37ZGJfctvVptVO192MwrtPhzONn6go8ylnOdMhKqi4nfg==" }, + "node_modules/pg": { + "version": "8.11.3", + "resolved": "https://registry.npmjs.org/pg/-/pg-8.11.3.tgz", + "integrity": "sha512-+9iuvG8QfaaUrrph+kpF24cXkH1YOOUeArRNYIxq1viYHZagBxrTno7cecY1Fa44tJeZvaoG+Djpkc3JwehN5g==", + "dependencies": { + "buffer-writer": "2.0.0", + "packet-reader": "1.0.0", + "pg-connection-string": "^2.6.2", + "pg-pool": "^3.6.1", + "pg-protocol": "^1.6.0", + "pg-types": "^2.1.0", + "pgpass": "1.x" + }, + "engines": { + "node": ">= 8.0.0" + }, + "optionalDependencies": { + "pg-cloudflare": "^1.1.1" + }, + "peerDependencies": { + "pg-native": ">=3.0.1" + }, + "peerDependenciesMeta": { + "pg-native": { + "optional": true + } + } + }, + "node_modules/pg-cloudflare": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.1.1.tgz", + "integrity": "sha512-xWPagP/4B6BgFO+EKz3JONXv3YDgvkbVrGw2mTo3D6tVDQRh1e7cqVGvyR3BE+eQgAvx1XhW/iEASj4/jCWl3Q==", + "optional": true + }, + "node_modules/pg-connection-string": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.6.2.tgz", + "integrity": "sha512-ch6OwaeaPYcova4kKZ15sbJ2hKb/VP48ZD2gE7i1J+L4MspCtBMAx8nMgz7bksc7IojCIIWuEhHibSMFH8m8oA==" + }, + "node_modules/pg-int8": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz", + "integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/pg-numeric": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/pg-numeric/-/pg-numeric-1.0.2.tgz", + "integrity": "sha512-BM/Thnrw5jm2kKLE5uJkXqqExRUY/toLHda65XgFTBTFYZyopbKjBe29Ii3RbkvlsMoFwD+tHeGaCjjv0gHlyw==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/pg-pool": { + "version": "3.6.1", + "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.6.1.tgz", + "integrity": "sha512-jizsIzhkIitxCGfPRzJn1ZdcosIt3pz9Sh3V01fm1vZnbnCMgmGl5wvGGdNN2EL9Rmb0EcFoCkixH4Pu+sP9Og==", + "peerDependencies": { + "pg": ">=8.0" + } + }, + "node_modules/pg-protocol": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.6.0.tgz", + "integrity": "sha512-M+PDm637OY5WM307051+bsDia5Xej6d9IR4GwJse1qA1DIhiKlksvrneZOYQq42OM+spubpcNYEo2FcKQrDk+Q==" + }, + "node_modules/pg-types": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz", + "integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==", + "dependencies": { + "pg-int8": "1.0.1", + "postgres-array": "~2.0.0", + "postgres-bytea": "~1.0.0", + "postgres-date": "~1.0.4", + "postgres-interval": "^1.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/pgpass": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz", + "integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==", + "dependencies": { + "split2": "^4.1.0" + } + }, "node_modules/picocolors": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", @@ -14232,6 +14554,47 @@ "node": ">=8" } }, + "node_modules/postgres-array": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz", + "integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==", + "engines": { + "node": ">=4" + } + }, + "node_modules/postgres-bytea": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.0.tgz", + "integrity": "sha512-xy3pmLuQqRBZBXDULy7KbaitYqLcmxigw14Q5sj8QBVLqEwXfeybIKVWiqAXTlcvdvb0+xkOtDbfQMOf4lST1w==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-date": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz", + "integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-interval": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz", + "integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==", + "dependencies": { + "xtend": "^4.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-range": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/postgres-range/-/postgres-range-1.1.3.tgz", + "integrity": "sha512-VdlZoocy5lCP0c/t66xAfclglEapXPCIVhqqJRncYpvbCgImF0w67aPKfbqUMr72tO2k5q0TdTZwCLjPTI6C9g==", + "dev": true + }, "node_modules/posthog-node": { "version": "2.6.0", "resolved": "https://registry.npmjs.org/posthog-node/-/posthog-node-2.6.0.tgz", @@ -15015,6 +15378,14 @@ "node": ">=0.10.0" } }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/resolve": { "version": "1.22.2", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.2.tgz", @@ -15244,6 +15615,11 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" }, + "node_modules/seq-queue": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/seq-queue/-/seq-queue-0.0.5.tgz", + "integrity": "sha512-hr3Wtp/GZIc/6DAGPDcV4/9WoZhjrkXsi5B/07QgX8tsdc6ilr7BFM6PM6rbdAX1kFSDYeZGLipIZZKyQP0O5Q==" + }, "node_modules/serve-static": { "version": "1.15.0", "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz", @@ -16197,7 +16573,6 @@ "version": "4.4.1", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", - "dev": true, "dependencies": { "punycode": "^2.1.0" } @@ -16206,7 +16581,6 @@ "version": "2.3.0", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz", "integrity": "sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==", - "dev": true, "engines": { "node": ">=6" } @@ -16515,6 +16889,14 @@ "node": ">=0.6.0" } }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "engines": { + "node": ">=0.4" + } + }, "node_modules/y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", @@ -19407,6 +19789,26 @@ "js-yaml": "^4.1.0", "minimatch": "^3.1.2", "strip-json-comments": "^3.1.1" + }, + "dependencies": { + "ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "requires": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + }, + "json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + } } }, "@eslint/js": { @@ -21263,6 +21665,12 @@ "pretty-format": "^29.0.0" } }, + "@types/jmespath": { + "version": "0.15.1", + "resolved": "https://registry.npmjs.org/@types/jmespath/-/jmespath-0.15.1.tgz", + "integrity": "sha512-RWN1HQ71Hjl2ixw4a8s7/Bcz6S9uaBTaoCQ5cJB7OsjgHBFi3GaWMy0vRgZBPSYXdsMKFNxGLUUEh9uRf00Spw==", + "dev": true + }, "@types/js-yaml": { "version": "4.0.5", "resolved": "https://registry.npmjs.org/@types/js-yaml/-/js-yaml-4.0.5.tgz", @@ -21335,6 +21743,61 @@ "@types/passport": "*" } }, + "@types/pg": { + "version": "8.10.7", + "resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.10.7.tgz", + "integrity": "sha512-ksJqHipwYaSEHz9e1fr6H6erjoEdNNaOxwyJgPx9bNeaqOW3iWBQgVHfpwiSAoqGzchfc+ZyRLwEfeCcyYD3uQ==", + "dev": true, + "requires": { + "@types/node": "*", + "pg-protocol": "*", + "pg-types": "^4.0.1" + }, + "dependencies": { + "pg-types": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-4.0.1.tgz", + "integrity": "sha512-hRCSDuLII9/LE3smys1hRHcu5QGcLs9ggT7I/TCs0IE+2Eesxi9+9RWAAwZ0yaGjxoWICF/YHLOEjydGujoJ+g==", + "dev": true, + "requires": { + "pg-int8": "1.0.1", + "pg-numeric": "1.0.2", + "postgres-array": "~3.0.1", + "postgres-bytea": "~3.0.0", + "postgres-date": "~2.0.1", + "postgres-interval": "^3.0.0", + "postgres-range": "^1.1.1" + } + }, + "postgres-array": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-3.0.2.tgz", + "integrity": "sha512-6faShkdFugNQCLwucjPcY5ARoW1SlbnrZjmGl0IrrqewpvxvhSLHimCVzqeuULCbG0fQv7Dtk1yDbG3xv7Veog==", + "dev": true + }, + "postgres-bytea": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-3.0.0.tgz", + "integrity": "sha512-CNd4jim9RFPkObHSjVHlVrxoVQXz7quwNFpz7RY1okNNme49+sVyiTvTRobiLV548Hx/hb1BG+iE7h9493WzFw==", + "dev": true, + "requires": { + "obuf": "~1.1.2" + } + }, + "postgres-date": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-2.0.1.tgz", + "integrity": "sha512-YtMKdsDt5Ojv1wQRvUhnyDJNSr2dGIC96mQVKz7xufp07nfuFONzdaowrMHjlAzY6GDLd4f+LUHHAAM1h4MdUw==", + "dev": true + }, + "postgres-interval": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-3.0.0.tgz", + "integrity": "sha512-BSNDnbyZCXSxgA+1f5UU2GmwhoI0aU5yMxRGO8CdFEcY2BQF9xm/7MqKnYoM1nJDk8nONNWDk9WeSmePFhQdlw==", + "dev": true + } + } + }, "@types/picomatch": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/@types/picomatch/-/picomatch-2.3.0.tgz", @@ -21717,14 +22180,13 @@ } }, "ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "dev": true, + "version": "8.12.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", + "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", "requires": { "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", "uri-js": "^4.2.2" } }, @@ -22210,6 +22672,11 @@ "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", "dev": true }, + "buffer-writer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/buffer-writer/-/buffer-writer-2.0.0.tgz", + "integrity": "sha512-a7ZpuTZU1TRtnwyCNW3I5dc0wWNC3VR9S++Ewyk2HHZdrO3CQJqSpd+95Us590V6AL7JqUAH2IwZ/398PmNFgw==" + }, "bull": { "version": "4.10.4", "resolved": "https://registry.npmjs.org/bull/-/bull-4.10.4.tgz", @@ -22789,6 +23256,18 @@ "text-table": "^0.2.0" }, "dependencies": { + "ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "requires": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + }, "eslint-scope": { "version": "7.2.1", "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.1.tgz", @@ -22804,6 +23283,12 @@ "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", "dev": true + }, + "json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true } } }, @@ -23090,8 +23575,7 @@ "fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" }, "fast-glob": { "version": "3.3.0", @@ -23357,6 +23841,14 @@ "wide-align": "^1.1.2" } }, + "generate-function": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.3.1.tgz", + "integrity": "sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ==", + "requires": { + "is-property": "^1.0.2" + } + }, "gensync": { "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", @@ -23833,6 +24325,11 @@ "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz", "integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==" }, + "is-property": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz", + "integrity": "sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g==" + }, "is-retry-allowed": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/is-retry-allowed/-/is-retry-allowed-2.2.0.tgz", @@ -24406,10 +24903,9 @@ "dev": true }, "json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==" }, "json-stable-stringify-without-jsonify": { "version": "1.0.1", @@ -24595,6 +25091,11 @@ "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", "dev": true }, + "long": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/long/-/long-5.2.3.tgz", + "integrity": "sha512-lcHwpNoggQTObv5apGNCTdJrO69eHOZMi4BNC+rTLER8iHAqGrUVeLh/irVIM7zTw2bOXA8T6uNPeujwOLg/2Q==" + }, "lru_map": { "version": "0.3.3", "resolved": "https://registry.npmjs.org/lru_map/-/lru_map-0.3.3.tgz", @@ -24919,6 +25420,61 @@ "node-gyp-build-optional-packages": "5.0.7" } }, + "mysql2": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.6.2.tgz", + "integrity": "sha512-m5erE6bMoWfPXW1D5UrVwlT8PowAoSX69KcZzPuARQ3wY1RJ52NW9PdvdPo076XiSIkQ5IBTis7hxdlrQTlyug==", + "requires": { + "denque": "^2.1.0", + "generate-function": "^2.3.1", + "iconv-lite": "^0.6.3", + "long": "^5.2.1", + "lru-cache": "^8.0.0", + "named-placeholders": "^1.1.3", + "seq-queue": "^0.0.5", + "sqlstring": "^2.3.2" + }, + "dependencies": { + "denque": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz", + "integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==" + }, + "iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "requires": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + } + }, + "lru-cache": { + "version": "8.0.5", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-8.0.5.tgz", + "integrity": "sha512-MhWWlVnuab1RG5/zMRRcVGXZLCXrZTgfwMikgzCegsPnG62yDQo5JnqKkrK4jO5iKqDAZGItAqN5CtKBCBWRUA==" + }, + "sqlstring": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/sqlstring/-/sqlstring-2.3.3.tgz", + "integrity": "sha512-qC9iz2FlN7DQl3+wjwn3802RTyjCx7sDvfQEXchwa6CWOx07/WVfh91gBmQ9fahw8snwGEWU3xGzOt4tFyHLxg==" + } + } + }, + "named-placeholders": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/named-placeholders/-/named-placeholders-1.1.3.tgz", + "integrity": "sha512-eLoBxg6wE/rZkJPhU/xRX1WTpkFEwDJEN96oxFrTsqBdbT5ec295Q+CoHrL9IT0DipqKhmGcaZmwOt8OON5x1w==", + "requires": { + "lru-cache": "^7.14.1" + }, + "dependencies": { + "lru-cache": { + "version": "7.18.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", + "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==" + } + } + }, "nanoid": { "version": "3.3.6", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.6.tgz", @@ -26900,6 +27456,12 @@ "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.3.tgz", "integrity": "sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g==" }, + "obuf": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/obuf/-/obuf-1.1.2.tgz", + "integrity": "sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==", + "dev": true + }, "octokit-auth-probot": { "version": "1.2.9", "resolved": "https://registry.npmjs.org/octokit-auth-probot/-/octokit-auth-probot-1.2.9.tgz", @@ -27004,6 +27566,11 @@ "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==" }, + "packet-reader": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/packet-reader/-/packet-reader-1.0.0.tgz", + "integrity": "sha512-HAKu/fG3HpHFO0AA8WE8q2g+gBJaZ9MG7fcKk+IJPLTGAD6Psw4443l+9DGRbOIh3/aXr7Phy0TjilYivJo5XQ==" + }, "parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", @@ -27119,6 +27686,74 @@ "resolved": "https://registry.npmjs.org/pause/-/pause-0.0.1.tgz", "integrity": "sha512-KG8UEiEVkR3wGEb4m5yZkVCzigAD+cVEJck2CzYZO37ZGJfctvVptVO192MwrtPhzONn6go8ylnOdMhKqi4nfg==" }, + "pg": { + "version": "8.11.3", + "resolved": "https://registry.npmjs.org/pg/-/pg-8.11.3.tgz", + "integrity": "sha512-+9iuvG8QfaaUrrph+kpF24cXkH1YOOUeArRNYIxq1viYHZagBxrTno7cecY1Fa44tJeZvaoG+Djpkc3JwehN5g==", + "requires": { + "buffer-writer": "2.0.0", + "packet-reader": "1.0.0", + "pg-cloudflare": "^1.1.1", + "pg-connection-string": "^2.6.2", + "pg-pool": "^3.6.1", + "pg-protocol": "^1.6.0", + "pg-types": "^2.1.0", + "pgpass": "1.x" + } + }, + "pg-cloudflare": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.1.1.tgz", + "integrity": "sha512-xWPagP/4B6BgFO+EKz3JONXv3YDgvkbVrGw2mTo3D6tVDQRh1e7cqVGvyR3BE+eQgAvx1XhW/iEASj4/jCWl3Q==", + "optional": true + }, + "pg-connection-string": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.6.2.tgz", + "integrity": "sha512-ch6OwaeaPYcova4kKZ15sbJ2hKb/VP48ZD2gE7i1J+L4MspCtBMAx8nMgz7bksc7IojCIIWuEhHibSMFH8m8oA==" + }, + "pg-int8": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz", + "integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==" + }, + "pg-numeric": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/pg-numeric/-/pg-numeric-1.0.2.tgz", + "integrity": "sha512-BM/Thnrw5jm2kKLE5uJkXqqExRUY/toLHda65XgFTBTFYZyopbKjBe29Ii3RbkvlsMoFwD+tHeGaCjjv0gHlyw==", + "dev": true + }, + "pg-pool": { + "version": "3.6.1", + "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.6.1.tgz", + "integrity": "sha512-jizsIzhkIitxCGfPRzJn1ZdcosIt3pz9Sh3V01fm1vZnbnCMgmGl5wvGGdNN2EL9Rmb0EcFoCkixH4Pu+sP9Og==", + "requires": {} + }, + "pg-protocol": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.6.0.tgz", + "integrity": "sha512-M+PDm637OY5WM307051+bsDia5Xej6d9IR4GwJse1qA1DIhiKlksvrneZOYQq42OM+spubpcNYEo2FcKQrDk+Q==" + }, + "pg-types": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz", + "integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==", + "requires": { + "pg-int8": "1.0.1", + "postgres-array": "~2.0.0", + "postgres-bytea": "~1.0.0", + "postgres-date": "~1.0.4", + "postgres-interval": "^1.1.0" + } + }, + "pgpass": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz", + "integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==", + "requires": { + "split2": "^4.1.0" + } + }, "picocolors": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", @@ -27387,6 +28022,35 @@ } } }, + "postgres-array": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz", + "integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==" + }, + "postgres-bytea": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.0.tgz", + "integrity": "sha512-xy3pmLuQqRBZBXDULy7KbaitYqLcmxigw14Q5sj8QBVLqEwXfeybIKVWiqAXTlcvdvb0+xkOtDbfQMOf4lST1w==" + }, + "postgres-date": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz", + "integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==" + }, + "postgres-interval": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz", + "integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==", + "requires": { + "xtend": "^4.0.0" + } + }, + "postgres-range": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/postgres-range/-/postgres-range-1.1.3.tgz", + "integrity": "sha512-VdlZoocy5lCP0c/t66xAfclglEapXPCIVhqqJRncYpvbCgImF0w67aPKfbqUMr72tO2k5q0TdTZwCLjPTI6C9g==", + "dev": true + }, "posthog-node": { "version": "2.6.0", "resolved": "https://registry.npmjs.org/posthog-node/-/posthog-node-2.6.0.tgz", @@ -28043,6 +28707,11 @@ "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", "dev": true }, + "require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==" + }, "resolve": { "version": "1.22.2", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.2.tgz", @@ -28209,6 +28878,11 @@ } } }, + "seq-queue": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/seq-queue/-/seq-queue-0.0.5.tgz", + "integrity": "sha512-hr3Wtp/GZIc/6DAGPDcV4/9WoZhjrkXsi5B/07QgX8tsdc6ilr7BFM6PM6rbdAX1kFSDYeZGLipIZZKyQP0O5Q==" + }, "serve-static": { "version": "1.15.0", "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz", @@ -28910,7 +29584,6 @@ "version": "4.4.1", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", - "dev": true, "requires": { "punycode": "^2.1.0" }, @@ -28918,8 +29591,7 @@ "punycode": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz", - "integrity": "sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==", - "dev": true + "integrity": "sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==" } } }, @@ -29165,6 +29837,11 @@ "resolved": "https://registry.npmjs.org/xpath/-/xpath-0.0.27.tgz", "integrity": "sha512-fg03WRxtkCV6ohClePNAECYsmpKKTv5L8y/X3Dn1hQrec3POx2jHZ/0P2qQ6HvsrU1BmeqXcof3NGGueG6LxwQ==" }, + "xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==" + }, "y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", diff --git a/backend/package.json b/backend/package.json index 768f3d3af..9768762e8 100644 --- a/backend/package.json +++ b/backend/package.json @@ -11,6 +11,7 @@ "@types/crypto-js": "^4.1.1", "@types/libsodium-wrappers": "^0.7.10", "@ucast/mongo2js": "^1.3.4", + "ajv": "^8.12.0", "argon2": "^0.30.3", "aws-sdk": "^2.1364.0", "axios": "^1.3.5", @@ -29,12 +30,14 @@ "helmet": "^5.1.1", "infisical-node": "^1.2.1", "ioredis": "^5.3.2", + "jmespath": "^0.16.0", "js-yaml": "^4.1.0", "jsonwebtoken": "^9.0.0", "jsrp": "^0.2.4", "libsodium-wrappers": "^0.7.10", "lodash": "^4.17.21", "mongoose": "^7.4.1", + "mysql2": "^3.6.2", "nanoid": "^3.3.6", "node-cache": "^5.1.2", "nodemailer": "^6.8.0", @@ -44,6 +47,7 @@ "passport-google-oauth20": "^2.0.0", "pino": "^8.16.1", "pino-http": "^8.5.1", + "pg": "^8.11.3", "posthog-node": "^2.6.0", "probot": "^12.3.1", "query-string": "^7.1.3", @@ -98,11 +102,13 @@ "@types/cors": "^2.8.12", "@types/express": "^4.17.14", "@types/jest": "^29.5.0", + "@types/jmespath": "^0.15.1", "@types/jsonwebtoken": "^8.5.9", "@types/lodash": "^4.14.191", "@types/node": "^18.11.3", "@types/nodemailer": "^6.4.6", "@types/passport": "^1.0.12", + "@types/pg": "^8.10.7", "@types/picomatch": "^2.3.0", "@types/pino": "^7.0.5", "@types/supertest": "^2.0.12", diff --git a/backend/src/controllers/v3/secretsController.ts b/backend/src/controllers/v3/secretsController.ts index 2e0c8514a..dedb15b65 100644 --- a/backend/src/controllers/v3/secretsController.ts +++ b/backend/src/controllers/v3/secretsController.ts @@ -140,7 +140,7 @@ export const getSecretsRaw = async (req: Request, res: Response) => { query: { secretPath, environment, workspaceId } } = validatedData; const { - query: { folderId, include_imports: includeImports } + query: { include_imports: includeImports } } = validatedData; // if the service token has single scope, it will get all secrets for that scope by default @@ -156,13 +156,6 @@ export const getSecretsRaw = async (req: Request, res: Response) => { workspaceId = serviceTokenDetails.workspace.toString(); } - if (folderId && folderId !== "root") { - const folder = await Folder.findOne({ workspace: workspaceId, environment }); - if (!folder) throw BadRequestError({ message: "Folder not found" }); - - secretPath = getFolderWithPathFromId(folder.nodes, folderId).folderPath; - } - if (!environment || !workspaceId) throw BadRequestError({ message: "Missing environment or workspace id" }); @@ -177,7 +170,6 @@ export const getSecretsRaw = async (req: Request, res: Response) => { const secrets = await SecretService.getSecrets({ workspaceId: new Types.ObjectId(workspaceId), environment, - folderId, secretPath, authData: req.authData }); @@ -467,20 +459,13 @@ export const deleteSecretByNameRaw = async (req: Request, res: Response) => { export const getSecrets = async (req: Request, res: Response) => { const validatedData = await validateRequest(reqValidator.GetSecretsV3, req); const { - query: { environment, workspaceId, include_imports: includeImports, folderId } + query: { environment, workspaceId, include_imports: includeImports } } = validatedData; let { query: { secretPath } } = validatedData; - if (folderId && folderId !== "root") { - const folder = await Folder.findOne({ workspace: workspaceId, environment }); - if (!folder) return res.send({ secrets: [] }); - - secretPath = getFolderWithPathFromId(folder.nodes, folderId).folderPath; - } - const { authVerifier: permissionCheckFn } = await checkSecretsPermission({ authData: req.authData, workspaceId, @@ -492,7 +477,6 @@ export const getSecrets = async (req: Request, res: Response) => { const secrets = await SecretService.getSecrets({ workspaceId: new Types.ObjectId(workspaceId), environment, - folderId, secretPath, authData: req.authData }); @@ -875,6 +859,14 @@ export const createSecretByNameBatch = async (req: Request, res: Response) => { authData: req.authData }); + await EventService.handleEvent({ + event: eventPushSecrets({ + workspaceId: new Types.ObjectId(workspaceId), + environment, + secretPath + }) + }); + return res.status(200).send({ secrets: createdSecrets }); @@ -919,6 +911,14 @@ export const updateSecretByNameBatch = async (req: Request, res: Response) => { authData: req.authData }); + await EventService.handleEvent({ + event: eventPushSecrets({ + workspaceId: new Types.ObjectId(workspaceId), + environment, + secretPath + }) + }); + return res.status(200).send({ secrets: updatedSecrets }); diff --git a/backend/src/ee/controllers/v1/index.ts b/backend/src/ee/controllers/v1/index.ts index b023599e7..dc161c271 100644 --- a/backend/src/ee/controllers/v1/index.ts +++ b/backend/src/ee/controllers/v1/index.ts @@ -10,6 +10,8 @@ import * as cloudProductsController from "./cloudProductsController"; import * as roleController from "./roleController"; import * as secretApprovalPolicyController from "./secretApprovalPolicyController"; import * as secretApprovalRequestController from "./secretApprovalRequestsController"; +import * as secretRotationProviderController from "./secretRotationProviderController"; +import * as secretRotationController from "./secretRotationController"; export { secretController, @@ -23,5 +25,7 @@ export { cloudProductsController, roleController, secretApprovalPolicyController, - secretApprovalRequestController + secretApprovalRequestController, + secretRotationProviderController, + secretRotationController }; diff --git a/backend/src/ee/controllers/v1/secretRotationController.ts b/backend/src/ee/controllers/v1/secretRotationController.ts new file mode 100644 index 000000000..a8a2fd2dd --- /dev/null +++ b/backend/src/ee/controllers/v1/secretRotationController.ts @@ -0,0 +1,91 @@ +import { Request, Response } from "express"; +import { validateRequest } from "../../../helpers/validation"; +import * as reqValidator from "../../validation/secretRotation"; +import * as secretRotationService from "../../secretRotation/service"; +import { + getUserProjectPermissions, + ProjectPermissionActions, + ProjectPermissionSub +} from "../../services/ProjectRoleService"; +import { ForbiddenError } from "@casl/ability"; + +export const createSecretRotation = async (req: Request, res: Response) => { + const { + body: { + provider, + customProvider, + interval, + outputs, + secretPath, + environment, + workspaceId, + inputs + } + } = await validateRequest(reqValidator.createSecretRotationV1, req); + + const { permission } = await getUserProjectPermissions(req.user._id, workspaceId); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Create, + ProjectPermissionSub.SecretRotation + ); + + const secretRotation = await secretRotationService.createSecretRotation({ + workspaceId, + inputs, + environment, + secretPath, + outputs, + interval, + customProvider, + provider + }); + + return res.send({ secretRotation }); +}; + +export const restartSecretRotations = async (req: Request, res: Response) => { + const { + body: { id } + } = await validateRequest(reqValidator.restartSecretRotationV1, req); + + const doc = await secretRotationService.getSecretRotationById({ id }); + const { permission } = await getUserProjectPermissions(req.user._id, doc.workspace.toString()); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Edit, + ProjectPermissionSub.SecretRotation + ); + + const secretRotation = await secretRotationService.restartSecretRotation({ id }); + return res.send({ secretRotation }); +}; + +export const deleteSecretRotations = async (req: Request, res: Response) => { + const { + params: { id } + } = await validateRequest(reqValidator.removeSecretRotationV1, req); + + const doc = await secretRotationService.getSecretRotationById({ id }); + const { permission } = await getUserProjectPermissions(req.user._id, doc.workspace.toString()); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Delete, + ProjectPermissionSub.SecretRotation + ); + + const secretRotations = await secretRotationService.deleteSecretRotation({ id }); + return res.send({ secretRotations }); +}; + +export const getSecretRotations = async (req: Request, res: Response) => { + const { + query: { workspaceId } + } = await validateRequest(reqValidator.getSecretRotationV1, req); + + const { permission } = await getUserProjectPermissions(req.user._id, workspaceId); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Read, + ProjectPermissionSub.SecretRotation + ); + + const secretRotations = await secretRotationService.getSecretRotationOfWorkspace(workspaceId); + return res.send({ secretRotations }); +}; diff --git a/backend/src/ee/controllers/v1/secretRotationProviderController.ts b/backend/src/ee/controllers/v1/secretRotationProviderController.ts new file mode 100644 index 000000000..7d62ed5f5 --- /dev/null +++ b/backend/src/ee/controllers/v1/secretRotationProviderController.ts @@ -0,0 +1,28 @@ +import { Request, Response } from "express"; +import { validateRequest } from "../../../helpers/validation"; +import * as reqValidator from "../../validation/secretRotationProvider"; +import * as secretRotationProviderService from "../../secretRotation/service"; +import { + getUserProjectPermissions, + ProjectPermissionActions, + ProjectPermissionSub +} from "../../services/ProjectRoleService"; +import { ForbiddenError } from "@casl/ability"; + +export const getProviderTemplates = async (req: Request, res: Response) => { + const { + params: { workspaceId } + } = await validateRequest(reqValidator.getSecretRotationProvidersV1, req); + + const { permission } = await getUserProjectPermissions(req.user._id, workspaceId); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Read, + ProjectPermissionSub.SecretRotation + ); + + const rotationProviderList = await secretRotationProviderService.getProviderTemplate({ + workspaceId + }); + + return res.send(rotationProviderList); +}; diff --git a/backend/src/ee/models/auditLog/enums.ts b/backend/src/ee/models/auditLog/enums.ts index a7be37a65..45ba56345 100644 --- a/backend/src/ee/models/auditLog/enums.ts +++ b/backend/src/ee/models/auditLog/enums.ts @@ -1,7 +1,8 @@ export enum ActorType { - USER = "user", - SERVICE = "service", - SERVICE_V3 = "service-v3" + USER = "user", + SERVICE = "service", + SERVICE_V3 = "service-v3", + Machine = "machine" } export enum UserAgentType { diff --git a/backend/src/ee/models/auditLog/types.ts b/backend/src/ee/models/auditLog/types.ts index 579a02ccb..24796c70a 100644 --- a/backend/src/ee/models/auditLog/types.ts +++ b/backend/src/ee/models/auditLog/types.ts @@ -1,11 +1,5 @@ -import { - ActorType, - EventType -} from "./enums"; -import { - IServiceTokenV3Scope, - IServiceTokenV3TrustedIp -} from "../../../models/serviceTokenDataV3"; +import { ActorType, EventType } from "./enums"; +import { IServiceTokenV3Scope, IServiceTokenV3TrustedIp } from "../../../models/serviceTokenDataV3"; interface UserActorMetadata { userId: string; @@ -28,14 +22,15 @@ export interface ServiceActor { } export interface ServiceActorV3 { - type: ActorType.SERVICE_V3; - metadata: ServiceActorMetadata; + type: ActorType.SERVICE_V3; + metadata: ServiceActorMetadata; } -export type Actor = - | UserActor - | ServiceActor - | ServiceActorV3; +export interface MachineActor { + type: ActorType.Machine; +} + +export type Actor = UserActor | ServiceActor | ServiceActorV3 | MachineActor; interface GetSecretsEvent { type: EventType.GET_SECRETS; @@ -226,36 +221,36 @@ interface DeleteServiceTokenEvent { } interface CreateServiceTokenV3Event { - type: EventType.CREATE_SERVICE_TOKEN_V3; - metadata: { - name: string; - isActive: boolean; - scopes: Array; - trustedIps: Array; - expiresAt?: Date; - } + type: EventType.CREATE_SERVICE_TOKEN_V3; + metadata: { + name: string; + isActive: boolean; + scopes: Array; + trustedIps: Array; + expiresAt?: Date; + }; } interface UpdateServiceTokenV3Event { - type: EventType.UPDATE_SERVICE_TOKEN_V3; - metadata: { - name?: string; - isActive?: boolean; - scopes?: Array; - trustedIps?: Array; - expiresAt?: Date; - } + type: EventType.UPDATE_SERVICE_TOKEN_V3; + metadata: { + name?: string; + isActive?: boolean; + scopes?: Array; + trustedIps?: Array; + expiresAt?: Date; + }; } interface DeleteServiceTokenV3Event { - type: EventType.DELETE_SERVICE_TOKEN_V3; - metadata: { - name: string; - isActive: boolean; - scopes: Array; - expiresAt?: Date; - trustedIps: Array; - } + type: EventType.DELETE_SERVICE_TOKEN_V3; + metadata: { + name: string; + isActive: boolean; + scopes: Array; + expiresAt?: Date; + trustedIps: Array; + }; } interface CreateEnvironmentEvent { @@ -427,15 +422,15 @@ interface UpdateUserRole { } interface UpdateUserDeniedPermissions { - type: EventType.UPDATE_USER_WORKSPACE_DENIED_PERMISSIONS, - metadata: { - userId: string; - email: string; - deniedPermissions: { - environmentSlug: string; - ability: string; - }[] - } + type: EventType.UPDATE_USER_WORKSPACE_DENIED_PERMISSIONS; + metadata: { + userId: string; + email: string; + deniedPermissions: { + environmentSlug: string; + ability: string; + }[]; + }; } interface SecretApprovalMerge { type: EventType.SECRET_APPROVAL_MERGED; diff --git a/backend/src/ee/routes/v1/index.ts b/backend/src/ee/routes/v1/index.ts index 34477cf72..40145e81f 100644 --- a/backend/src/ee/routes/v1/index.ts +++ b/backend/src/ee/routes/v1/index.ts @@ -10,6 +10,8 @@ import secretScanning from "./secretScanning"; import roles from "./role"; import secretApprovalPolicy from "./secretApprovalPolicy"; import secretApprovalRequest from "./secretApprovalRequest"; +import secretRotationProvider from "./secretRotationProvider"; +import secretRotation from "./secretRotation"; export { secret, @@ -23,5 +25,7 @@ export { secretScanning, roles, secretApprovalPolicy, - secretApprovalRequest + secretApprovalRequest, + secretRotationProvider, + secretRotation }; diff --git a/backend/src/ee/routes/v1/secretRotation.ts b/backend/src/ee/routes/v1/secretRotation.ts new file mode 100644 index 000000000..a4da8a72f --- /dev/null +++ b/backend/src/ee/routes/v1/secretRotation.ts @@ -0,0 +1,41 @@ +import express from "express"; + +import { AuthMode } from "../../../variables"; +import { requireAuth } from "../../../middleware"; +import { secretRotationController } from "../../controllers/v1"; + +const router = express.Router(); + +router.post( + "/", + requireAuth({ + acceptedAuthModes: [AuthMode.JWT] + }), + secretRotationController.createSecretRotation +); + +router.post( + "/restart", + requireAuth({ + acceptedAuthModes: [AuthMode.JWT] + }), + secretRotationController.restartSecretRotations +); + +router.get( + "/", + requireAuth({ + acceptedAuthModes: [AuthMode.JWT] + }), + secretRotationController.getSecretRotations +); + +router.delete( + "/:id", + requireAuth({ + acceptedAuthModes: [AuthMode.JWT] + }), + secretRotationController.deleteSecretRotations +); + +export default router; diff --git a/backend/src/ee/routes/v1/secretRotationProvider.ts b/backend/src/ee/routes/v1/secretRotationProvider.ts new file mode 100644 index 000000000..16ab17184 --- /dev/null +++ b/backend/src/ee/routes/v1/secretRotationProvider.ts @@ -0,0 +1,17 @@ +import express from "express"; + +import { AuthMode } from "../../../variables"; +import { requireAuth } from "../../../middleware"; +import { secretRotationProviderController } from "../../controllers/v1"; + +const router = express.Router(); + +router.get( + "/:workspaceId", + requireAuth({ + acceptedAuthModes: [AuthMode.JWT] + }), + secretRotationProviderController.getProviderTemplates +); + +export default router; diff --git a/backend/src/ee/secretRotation/db.ts b/backend/src/ee/secretRotation/db.ts new file mode 100644 index 000000000..e69de29bb diff --git a/backend/src/ee/secretRotation/models.ts b/backend/src/ee/secretRotation/models.ts new file mode 100644 index 000000000..0ddde5d83 --- /dev/null +++ b/backend/src/ee/secretRotation/models.ts @@ -0,0 +1,91 @@ +import { Schema, model } from "mongoose"; +import { + ALGORITHM_AES_256_GCM, + ENCODING_SCHEME_BASE64, + ENCODING_SCHEME_UTF8 +} from "../../variables"; +import { ISecretRotation } from "./types"; + +const secretRotationSchema = new Schema( + { + workspace: { + type: Schema.Types.ObjectId, + ref: "Workspace" + }, + provider: { + type: String, + required: true + }, + customProvider: { + type: Schema.Types.ObjectId, + ref: "SecretRotationProvider" + }, + environment: { + type: String, + required: true + }, + secretPath: { + type: String, + required: true + }, + interval: { + type: Number, + required: true + }, + lastRotatedAt: { + type: String + }, + status: { + type: String, + enum: ["success", "failed"] + }, + statusMessage: { + type: String + }, + // encrypted data on input keys and secrets got + encryptedData: { + type: String, + select: false + }, + encryptedDataIV: { + type: String, + select: false + }, + encryptedDataTag: { + type: String, + select: false + }, + algorithm: { + // the encryption algorithm used + type: String, + enum: [ALGORITHM_AES_256_GCM], + required: true, + select: false, + default: ALGORITHM_AES_256_GCM + }, + keyEncoding: { + type: String, + enum: [ENCODING_SCHEME_UTF8, ENCODING_SCHEME_BASE64], + required: true, + select: false, + default: ENCODING_SCHEME_UTF8 + }, + outputs: [ + { + key: { + type: String, + required: true + }, + secret: { + type: Schema.Types.ObjectId, + ref: "Secret" + } + } + ] + }, + { + timestamps: true + } +); + +export const SecretRotation = model("SecretRotation", secretRotationSchema); diff --git a/backend/src/ee/secretRotation/queue/queue.ts b/backend/src/ee/secretRotation/queue/queue.ts new file mode 100644 index 000000000..ff63d32c7 --- /dev/null +++ b/backend/src/ee/secretRotation/queue/queue.ts @@ -0,0 +1,287 @@ +import Queue, { Job } from "bull"; +import { client, getEncryptionKey, getRootEncryptionKey } from "../../../config"; +import { BotService, EventService, TelemetryService } from "../../../services"; +import { SecretRotation } from "../models"; +import { rotationTemplates } from "../templates"; +import { + ISecretRotationData, + ISecretRotationEncData, + ISecretRotationProviderTemplate, + TProviderFunctionTypes +} from "../types"; +import { + decryptSymmetric128BitHexKeyUTF8, + encryptSymmetric128BitHexKeyUTF8 +} from "../../../utils/crypto"; +import { ISecret, Secret } from "../../../models"; +import { ENCODING_SCHEME_BASE64, ENCODING_SCHEME_UTF8, SECRET_SHARED } from "../../../variables"; +import { EESecretService } from "../../services"; +import { SecretVersion } from "../../models"; +import { eventPushSecrets } from "../../../events"; +import { logger } from "../../../utils/logging"; + +import { + secretRotationPreSetFn, + secretRotationRemoveFn, + secretRotationSetFn, + secretRotationTestFn +} from "./queue.utils"; + +const secretRotationQueue = new Queue("secret-rotation-service", process.env.REDIS_URL as string); + +secretRotationQueue.process(async (job: Job) => { + const rotationStratDocId = job.data.rotationDocId; + const secretRotation = await SecretRotation.findById(rotationStratDocId) + .select("+encryptedData +encryptedDataTag +encryptedDataIV +keyEncoding") + .populate<{ + outputs: [ + { + key: string; + secret: ISecret; + } + ]; + }>("outputs.secret"); + + const infisicalRotationProvider = rotationTemplates.find( + ({ name }) => name === secretRotation?.provider + ); + + try { + if (!infisicalRotationProvider || !secretRotation) + throw new Error("Failed to find rotation strategy"); + + if (secretRotation.outputs.some(({ secret }) => !secret)) + throw new Error("Secrets not found in dashboard"); + + const workspaceId = secretRotation.workspace; + + // deep copy + const provider = JSON.parse( + JSON.stringify(infisicalRotationProvider) + ) as ISecretRotationProviderTemplate; + + // decrypt user provided inputs for secret rotation + const encryptionKey = await getEncryptionKey(); + const rootEncryptionKey = await getRootEncryptionKey(); + let decryptedData = ""; + if (rootEncryptionKey && secretRotation.keyEncoding === ENCODING_SCHEME_BASE64) { + // case: encoding scheme is base64 + decryptedData = client.decryptSymmetric( + secretRotation.encryptedData, + rootEncryptionKey, + secretRotation.encryptedDataIV, + secretRotation.encryptedDataTag + ); + } else if (encryptionKey && secretRotation.keyEncoding === ENCODING_SCHEME_UTF8) { + // case: encoding scheme is utf8 + decryptedData = decryptSymmetric128BitHexKeyUTF8({ + ciphertext: secretRotation.encryptedData, + iv: secretRotation.encryptedDataIV, + tag: secretRotation.encryptedDataTag, + key: encryptionKey + }); + } + + const variables = JSON.parse(decryptedData) as ISecretRotationEncData; + + // rotation set cycle + const newCredential: ISecretRotationData = { + inputs: variables.inputs, + outputs: {}, + internal: {} + }; + // special glue code for database + if (provider.template.functions.set.type === TProviderFunctionTypes.DB) { + const lastCred = variables.creds.at(-1); + if (lastCred && variables.creds.length === 1) { + newCredential.internal.username = + lastCred.internal.username === variables.inputs.username1 + ? variables.inputs.username2 + : variables.inputs.username1; + } else { + newCredential.internal.username = lastCred + ? lastCred.internal.username + : variables.inputs.username1; + } + } + if (provider.template.functions.set?.pre) { + secretRotationPreSetFn(provider.template.functions.set.pre, newCredential); + } + await secretRotationSetFn(provider.template.functions.set, newCredential); + await secretRotationTestFn(provider.template.functions.test, newCredential); + + if (variables.creds.length === 2) { + const deleteCycleCred = variables.creds.pop(); + if (deleteCycleCred && provider.template.functions.remove) { + const deleteCycleVar = { inputs: variables.inputs, ...deleteCycleCred }; + await secretRotationRemoveFn(provider.template.functions.remove, deleteCycleVar); + } + } + variables.creds.unshift({ outputs: newCredential.outputs, internal: newCredential.internal }); + const { ciphertext, iv, tag } = client.encryptSymmetric( + JSON.stringify(variables), + rootEncryptionKey + ); + + // save the rotation state + await SecretRotation.findByIdAndUpdate(rotationStratDocId, { + encryptedData: ciphertext, + encryptedDataIV: iv, + encryptedDataTag: tag, + status: "success", + statusMessage: "Rotated successfully", + lastRotatedAt: new Date().toUTCString() + }); + + const key = await BotService.getWorkspaceKeyWithBot({ + workspaceId: secretRotation.workspace + }); + + const encryptedSecrets = secretRotation.outputs.map(({ key: outputKey, secret }) => ({ + secret, + value: encryptSymmetric128BitHexKeyUTF8({ + plaintext: + typeof newCredential.outputs[outputKey] === "object" + ? JSON.stringify(newCredential.outputs[outputKey]) + : String(newCredential.outputs[outputKey]), + key + }) + })); + + // now save the secret do a bulk update + // can't use the updateSecret function due to various parameter required issue + // REFACTOR(akhilmhdh): secret module should be lot more flexible. Ability to update bulk or individually by blindIndex, by id etc + await Secret.bulkWrite( + encryptedSecrets.map(({ secret, value }) => ({ + updateOne: { + filter: { + workspace: workspaceId, + environment: secretRotation.environment, + _id: secret._id, + type: SECRET_SHARED + }, + update: { + $inc: { + version: 1 + }, + secretValueCiphertext: value.ciphertext, + secretValueIV: value.iv, + secretValueTag: value.tag + } + } + })) + ); + + await EESecretService.addSecretVersions({ + secretVersions: encryptedSecrets.map(({ secret, value }) => { + const { + _id, + version, + workspace, + type, + folder, + secretBlindIndex, + secretKeyIV, + secretKeyTag, + secretKeyCiphertext, + skipMultilineEncoding, + environment, + algorithm, + keyEncoding + } = secret; + + return new SecretVersion({ + secret: _id, + version: version + 1, + workspace: workspace, + type, + folder, + environment, + isDeleted: false, + secretBlindIndex: secretBlindIndex, + secretKeyCiphertext: secretKeyCiphertext, + secretKeyIV: secretKeyIV, + secretKeyTag: secretKeyTag, + secretValueCiphertext: value.ciphertext, + secretValueIV: value.iv, + secretValueTag: value.tag, + algorithm, + keyEncoding, + skipMultilineEncoding + }); + }) + }); + + // akhilmhdh: @tony need to do something about this as its depend on authData which is not possibile in here + // await EEAuditLogService.createAuditLog( + // {actor:ActorType.Machine}, + // { + // type: EventType.UPDATE_SECRETS, + // metadata: { + // environment, + // secretPath, + // secrets: secretsToBeUpdated.map(({ _id, version, secretBlindIndex }) => ({ + // secretId: _id.toString(), + // secretKey: secretBlindIndexToKey[secretBlindIndex || ""], + // secretVersion: version + 1 + // })) + // } + // }, + // { + // workspaceId + // } + // ); + + const folderId = encryptedSecrets?.[0]?.secret?.folder; + // (EE) take a secret snapshot + await EESecretService.takeSecretSnapshot({ + workspaceId, + environment: secretRotation.environment, + folderId + }); + + await EventService.handleEvent({ + event: eventPushSecrets({ + workspaceId: secretRotation.workspace, + environment: secretRotation.environment, + secretPath: secretRotation.secretPath + }) + }); + + const postHogClient = await TelemetryService.getPostHogClient(); + if (postHogClient) { + postHogClient.capture({ + event: "secrets rotated", + properties: { + numberOfSecrets: encryptedSecrets.length, + environment: secretRotation.environment, + workspaceId, + folderId + } + }); + } + } catch (err) { + logger.error(err); + await SecretRotation.findByIdAndUpdate(rotationStratDocId, { + status: "failed", + statusMessage: (err as Error).message, + lastRotatedAt: new Date().toUTCString() + }); + } + + return Promise.resolve(); +}); + +const daysToMillisecond = (days: number) => days * 24 * 60 * 60 * 1000; +export const startSecretRotationQueue = async (rotationDocId: string, interval: number) => { + // when migration to bull mq just use the option immedite to trigger repeatable immediately + secretRotationQueue.add({ rotationDocId }, { jobId: rotationDocId, removeOnComplete: true }); + return secretRotationQueue.add( + { rotationDocId }, + { repeat: { every: daysToMillisecond(interval) }, jobId: rotationDocId } + ); +}; + +export const removeSecretRotationQueue = async (rotationDocId: string, interval: number) => { + return secretRotationQueue.removeRepeatable({ every: interval * 1000, jobId: rotationDocId }); +}; diff --git a/backend/src/ee/secretRotation/queue/queue.utils.ts b/backend/src/ee/secretRotation/queue/queue.utils.ts new file mode 100644 index 000000000..c1ddbefc1 --- /dev/null +++ b/backend/src/ee/secretRotation/queue/queue.utils.ts @@ -0,0 +1,179 @@ +import axios from "axios"; +import jmespath from "jmespath"; +import { customAlphabet } from "nanoid"; +import { Client as PgClient } from "pg"; +import mysql from "mysql2"; +import { + ISecretRotationData, + TAssignOp, + TDbProviderClients, + TDbProviderFunction, + TDirectAssignOp, + THttpProviderFunction, + TProviderFunction, + TProviderFunctionTypes +} from "../types"; +const REGEX = /\${([^}]+)}/g; +const SLUG_ALPHABETS = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"; +const nanoId = customAlphabet(SLUG_ALPHABETS, 10); + +export const interpolate = (data: any, getValue: (key: string) => unknown) => { + if (!data) return; + + if (typeof data === "number") return data; + + if (typeof data === "string") { + return data.replace(REGEX, (_a, b) => getValue(b) as string); + } + + if (typeof data === "object" && Array.isArray(data)) { + data.forEach((el, index) => { + data[index] = interpolate(el, getValue); + }); + } + + if (typeof data === "object") { + if ((data as { ref: string })?.ref) return getValue((data as { ref: string }).ref); + const temp = data as Record; // for converting ts object to record type + Object.keys(temp).forEach((key) => { + temp[key as keyof typeof temp] = interpolate(data[key as keyof typeof temp], getValue); + }); + } + return data; +}; + +const getInterpolationValue = (variables: ISecretRotationData) => (key: string) => { + if (key.includes("|")) { + const [keyword, ...arg] = key.split("|").map((el) => el.trim()); + switch (keyword) { + case "random": { + return nanoId(parseInt(arg[0], 10)); + } + default: { + throw Error(`Interpolation key not found - ${key}`); + } + } + } + const [type, keyName] = key.split(".").map((el) => el.trim()); + return variables[type as keyof ISecretRotationData][keyName]; +}; + +export const secretRotationHttpFn = async ( + func: THttpProviderFunction, + variables: ISecretRotationData +) => { + // string interpolation + const headers = interpolate(func.header, getInterpolationValue(variables)); + const url = interpolate(func.url, getInterpolationValue(variables)); + const body = interpolate(func.body, getInterpolationValue(variables)); + // axios will automatically throw error if req status is not between 2xx range + return axios({ method: func.method, url, headers, data: body }); +}; + +export const secretRotationDbFn = async ( + func: TDbProviderFunction, + variables: ISecretRotationData +) => { + const { type, client, pre, ...dbConnection } = func; + const { username, password, host, database, port, query, ca } = interpolate( + dbConnection, + getInterpolationValue(variables) + ); + const ssl = ca ? { rejectUnauthorized: false, ca } : undefined; + if (host === "localhost" || host === "127.0.0.1") throw new Error("Invalid db host"); + if (client === TDbProviderClients.Pg) { + const pgClient = new PgClient({ user: username, password, host, database, port, ssl }); + await pgClient.connect(); + const res = await pgClient.query(query); + await pgClient.end(); + return res.rows[0]; + } else if (client === TDbProviderClients.Sql) { + const sqlClient = mysql.createPool({ + user: username, + password, + host, + database, + port, + connectionLimit: 1, + ssl + }); + const res = await new Promise((resolve, reject) => { + sqlClient.query(query, (err, data) => { + if (err) return reject(err); + resolve(data); + }); + }); + await new Promise((resolve, reject) => { + sqlClient.end(function (err) { + if (err) return reject(err); + return resolve({}); + }); + }); + return (res as any)?.[0]; + } +}; + +export const secretRotationPreSetFn = ( + op: Record, + variables: ISecretRotationData +) => { + const getValFn = getInterpolationValue(variables); + Object.entries(op || {}).forEach(([key, assignFn]) => { + const [type, keyName] = key.split(".") as [keyof ISecretRotationData, string]; + variables[type][keyName] = interpolate(assignFn.value, getValFn); + }); +}; + +export const secretRotationSetFn = async ( + func: TProviderFunction, + variables: ISecretRotationData +) => { + const getValFn = getInterpolationValue(variables); + // http setter + if (func.type === TProviderFunctionTypes.HTTP) { + const res = await secretRotationHttpFn(func, variables); + Object.entries(func.setter || {}).forEach(([key, assignFn]) => { + const [type, keyName] = key.split(".") as [keyof ISecretRotationData, string]; + if (assignFn.assign === TAssignOp.JmesPath) { + variables[type][keyName] = jmespath.search(res.data, assignFn.path); + } else if (assignFn.value) { + variables[type][keyName] = interpolate(assignFn.value, getValFn); + } + }); + // db setter + } else if (func.type === TProviderFunctionTypes.DB) { + const data = await secretRotationDbFn(func, variables); + Object.entries(func.setter || {}).forEach(([key, assignFn]) => { + const [type, keyName] = key.split(".") as [keyof ISecretRotationData, string]; + if (assignFn.assign === TAssignOp.JmesPath) { + if (typeof data === "object") { + variables[type][keyName] = jmespath.search(data, assignFn.path); + } + } else if (assignFn.value) { + variables[type][keyName] = interpolate(assignFn.value, getValFn); + } + }); + } +}; + +export const secretRotationTestFn = async ( + func: TProviderFunction, + variables: ISecretRotationData +) => { + if (func.type === TProviderFunctionTypes.HTTP) { + await secretRotationHttpFn(func, variables); + } else if (func.type === TProviderFunctionTypes.DB) { + await secretRotationDbFn(func, variables); + } +}; + +export const secretRotationRemoveFn = async ( + func: TProviderFunction, + variables: ISecretRotationData +) => { + if (!func) return; + if (func.type === TProviderFunctionTypes.HTTP) { + // string interpolation + return await secretRotationHttpFn(func, variables); + } +}; diff --git a/backend/src/ee/secretRotation/service.ts b/backend/src/ee/secretRotation/service.ts new file mode 100644 index 000000000..9e00f20e1 --- /dev/null +++ b/backend/src/ee/secretRotation/service.ts @@ -0,0 +1,130 @@ +import { ISecretRotationEncData, TCreateSecretRotation, TGetProviderTemplates } from "./types"; +import { rotationTemplates } from "./templates"; +import { SecretRotation } from "./models"; +import { client, getEncryptionKey, getRootEncryptionKey } from "../../config"; +import { BadRequestError } from "../../utils/errors"; +import Ajv from "ajv"; +import { removeSecretRotationQueue, startSecretRotationQueue } from "./queue/queue"; +import { + ALGORITHM_AES_256_GCM, + ENCODING_SCHEME_BASE64, + ENCODING_SCHEME_UTF8 +} from "../../variables"; +import { encryptSymmetric128BitHexKeyUTF8 } from "../../utils/crypto"; + +const ajv = new Ajv({ strict: false }); + +export const getProviderTemplate = async ({ workspaceId }: TGetProviderTemplates) => { + return { + custom: [], + providers: rotationTemplates + }; +}; + +export const createSecretRotation = async ({ + workspaceId, + secretPath, + environment, + provider, + interval, + inputs, + outputs +}: TCreateSecretRotation) => { + const rotationTemplate = rotationTemplates.find(({ name }) => name === provider); + if (!rotationTemplate) throw BadRequestError({ message: "Provider not found" }); + + const formattedInputs: Record = {}; + Object.entries(inputs).forEach(([key, value]) => { + const type = rotationTemplate.template.inputs.properties[key].type; + if (type === "string") { + formattedInputs[key] = value; + return; + } + if (type === "integer") { + formattedInputs[key] = parseInt(value as string, 10); + return; + } + formattedInputs[key] = JSON.parse(value as string); + }); + // ensure input one follows the correct schema + const valid = ajv.validate(rotationTemplate.template.inputs, formattedInputs); + if (!valid) { + throw BadRequestError({ message: ajv.errors?.[0].message }); + } + + const encData: Partial = { + inputs: formattedInputs, + creds: [] + }; + + const secretRotation = new SecretRotation({ + workspace: workspaceId, + provider, + environment, + secretPath, + interval, + outputs: Object.entries(outputs).map(([key, secret]) => ({ key, secret })) + }); + + const encryptionKey = await getEncryptionKey(); + const rootEncryptionKey = await getRootEncryptionKey(); + + if (rootEncryptionKey) { + const { ciphertext, iv, tag } = client.encryptSymmetric( + JSON.stringify(encData), + rootEncryptionKey + ); + secretRotation.encryptedDataIV = iv; + secretRotation.encryptedDataTag = tag; + secretRotation.encryptedData = ciphertext; + secretRotation.algorithm = ALGORITHM_AES_256_GCM; + secretRotation.keyEncoding = ENCODING_SCHEME_BASE64; + } else if (encryptionKey) { + const { ciphertext, iv, tag } = encryptSymmetric128BitHexKeyUTF8({ + plaintext: JSON.stringify(encData), + key: encryptionKey + }); + secretRotation.encryptedDataIV = iv; + secretRotation.encryptedDataTag = tag; + secretRotation.encryptedData = ciphertext; + secretRotation.algorithm = ALGORITHM_AES_256_GCM; + secretRotation.keyEncoding = ENCODING_SCHEME_UTF8; + } + + await secretRotation.save(); + await startSecretRotationQueue(secretRotation._id.toString(), interval); + + return secretRotation; +}; + +export const deleteSecretRotation = async ({ id }: { id: string }) => { + const doc = await SecretRotation.findByIdAndRemove(id); + if (!doc) throw BadRequestError({ message: "Rotation not found" }); + + await removeSecretRotationQueue(doc._id.toString(), doc.interval); + return doc; +}; + +export const restartSecretRotation = async ({ id }: { id: string }) => { + const secretRotation = await SecretRotation.findById(id); + if (!secretRotation) throw BadRequestError({ message: "Rotation not found" }); + + await removeSecretRotationQueue(secretRotation._id.toString(), secretRotation.interval); + await startSecretRotationQueue(secretRotation._id.toString(), secretRotation.interval); + + return secretRotation; +}; + +export const getSecretRotationById = async ({ id }: { id: string }) => { + const doc = await SecretRotation.findById(id); + if (!doc) throw BadRequestError({ message: "Rotation not found" }); + return doc; +}; + +export const getSecretRotationOfWorkspace = async (workspaceId: string) => { + const secretRotations = await SecretRotation.find({ + workspace: workspaceId + }).populate("outputs.secret"); + + return secretRotations; +}; diff --git a/backend/src/ee/secretRotation/templates/index.ts b/backend/src/ee/secretRotation/templates/index.ts new file mode 100644 index 000000000..063d5149f --- /dev/null +++ b/backend/src/ee/secretRotation/templates/index.ts @@ -0,0 +1,28 @@ +import { ISecretRotationProviderTemplate } from "../types"; +import { MYSQL_TEMPLATE } from "./mysql"; +import { POSTGRES_TEMPLATE } from "./postgres"; +import { SENDGRID_TEMPLATE } from "./sendgrid"; + +export const rotationTemplates: ISecretRotationProviderTemplate[] = [ + { + name: "sendgrid", + title: "Twilio Sendgrid", + image: "sendgrid.png", + description: "Rotate Twilio Sendgrid API keys", + template: SENDGRID_TEMPLATE + }, + { + name: "postgres", + title: "PostgreSQL", + image: "postgres.png", + description: "Rotate PostgreSQL/CockroachDB user credentials", + template: POSTGRES_TEMPLATE + }, + { + name: "mysql", + title: "MySQL", + image: "mysql.png", + description: "Rotate MySQL@7/MariaDB user credentials", + template: MYSQL_TEMPLATE + } +]; diff --git a/backend/src/ee/secretRotation/templates/mysql.ts b/backend/src/ee/secretRotation/templates/mysql.ts new file mode 100644 index 000000000..ce44c753f --- /dev/null +++ b/backend/src/ee/secretRotation/templates/mysql.ts @@ -0,0 +1,83 @@ +import { TAssignOp, TDbProviderClients, TProviderFunctionTypes } from "../types"; + +export const MYSQL_TEMPLATE = { + inputs: { + type: "object" as const, + properties: { + admin_username: { type: "string" as const }, + admin_password: { type: "string" as const }, + host: { type: "string" as const }, + database: { type: "string" as const }, + port: { type: "integer" as const, default: "3306" }, + username1: { + type: "string", + default: "infisical-sql-user1", + desc: "This user must be created in your database" + }, + username2: { + type: "string", + default: "infisical-sql-user2", + desc: "This user must be created in your database" + }, + ca: { type: "string", desc: "SSL certificate for db auth(string)" } + }, + required: [ + "admin_username", + "admin_password", + "host", + "database", + "username1", + "username2", + "port" + ], + additionalProperties: false + }, + outputs: { + db_username: { type: "string" }, + db_password: { type: "string" } + }, + internal: { + rotated_password: { type: "string" }, + username: { type: "string" } + }, + functions: { + set: { + type: TProviderFunctionTypes.DB as const, + client: TDbProviderClients.Sql, + username: "${inputs.admin_username}", + password: "${inputs.admin_password}", + host: "${inputs.host}", + database: "${inputs.database}", + port: "${inputs.port}", + ca: "${inputs.ca}", + query: "ALTER USER ${internal.username} IDENTIFIED BY '${internal.rotated_password}'", + setter: { + "outputs.db_username": { + assign: TAssignOp.Direct as const, + value: "${internal.username}" + }, + "outputs.db_password": { + assign: TAssignOp.Direct as const, + value: "${internal.rotated_password}" + } + }, + pre: { + "internal.rotated_password": { + assign: TAssignOp.Direct as const, + value: "${random | 32}" + } + } + }, + test: { + type: TProviderFunctionTypes.DB as const, + client: TDbProviderClients.Sql, + username: "${internal.username}", + password: "${internal.rotated_password}", + host: "${inputs.host}", + database: "${inputs.database}", + port: "${inputs.port}", + ca: "${inputs.ca}", + query: "SELECT NOW()" + } + } +}; diff --git a/backend/src/ee/secretRotation/templates/postgres.ts b/backend/src/ee/secretRotation/templates/postgres.ts new file mode 100644 index 000000000..3b3153be1 --- /dev/null +++ b/backend/src/ee/secretRotation/templates/postgres.ts @@ -0,0 +1,83 @@ +import { TAssignOp, TDbProviderClients, TProviderFunctionTypes } from "../types"; + +export const POSTGRES_TEMPLATE = { + inputs: { + type: "object" as const, + properties: { + admin_username: { type: "string" as const }, + admin_password: { type: "string" as const }, + host: { type: "string" as const }, + database: { type: "string" as const }, + port: { type: "integer" as const, default: "5432" }, + username1: { + type: "string", + default: "infisical-pg-user1", + desc: "This user must be created in your database" + }, + username2: { + type: "string", + default: "infisical-pg-user2", + desc: "This user must be created in your database" + }, + ca: { type: "string", desc: "SSL certificate for db auth(string)" } + }, + required: [ + "admin_username", + "admin_password", + "host", + "database", + "username1", + "username2", + "port" + ], + additionalProperties: false + }, + outputs: { + db_username: { type: "string" }, + db_password: { type: "string" } + }, + internal: { + rotated_password: { type: "string" }, + username: { type: "string" } + }, + functions: { + set: { + type: TProviderFunctionTypes.DB as const, + client: TDbProviderClients.Pg, + username: "${inputs.admin_username}", + password: "${inputs.admin_password}", + host: "${inputs.host}", + database: "${inputs.database}", + port: "${inputs.port}", + ca: "${inputs.ca}", + query: "ALTER USER ${internal.username} WITH PASSWORD '${internal.rotated_password}'", + setter: { + "outputs.db_username": { + assign: TAssignOp.Direct as const, + value: "${internal.username}" + }, + "outputs.db_password": { + assign: TAssignOp.Direct as const, + value: "${internal.rotated_password}" + } + }, + pre: { + "internal.rotated_password": { + assign: TAssignOp.Direct as const, + value: "${random | 32}" + } + } + }, + test: { + type: TProviderFunctionTypes.DB as const, + client: TDbProviderClients.Pg, + username: "${internal.username}", + password: "${internal.rotated_password}", + host: "${inputs.host}", + database: "${inputs.database}", + port: "${inputs.port}", + ca: "${inputs.ca}", + query: "SELECT NOW()" + } + } +}; diff --git a/backend/src/ee/secretRotation/templates/sendgrid.ts b/backend/src/ee/secretRotation/templates/sendgrid.ts new file mode 100644 index 000000000..b600f3e0c --- /dev/null +++ b/backend/src/ee/secretRotation/templates/sendgrid.ts @@ -0,0 +1,63 @@ +import { TAssignOp, TProviderFunctionTypes } from "../types"; + +export const SENDGRID_TEMPLATE = { + inputs: { + type: "object" as const, + properties: { + admin_api_key: { type: "string" as const, desc: "Sendgrid admin api key to create new keys" }, + api_key_scopes: { + type: "array", + items: { type: "string" as const }, + desc: "Scopes for created tokens by rotation(Array)" + } + }, + required: ["admin_api_key", "api_key_scopes"], + additionalProperties: false + }, + outputs: { + api_key: { type: "string" } + }, + internal: { + api_key_id: { type: "string" } + }, + functions: { + set: { + type: TProviderFunctionTypes.HTTP as const, + url: "https://api.sendgrid.com/v3/api_keys", + method: "POST", + header: { + Authorization: "Bearer ${inputs.admin_api_key}" + }, + body: { + name: "infisical-${random | 16}", + scopes: { ref: "inputs.api_key_scopes" } + }, + setter: { + "outputs.api_key": { + assign: TAssignOp.JmesPath as const, + path: "api_key" + }, + "internal.api_key_id": { + assign: TAssignOp.JmesPath as const, + path: "api_key_id" + } + } + }, + remove: { + type: TProviderFunctionTypes.HTTP as const, + url: "https://api.sendgrid.com/v3/api_keys/${internal.api_key_id}", + header: { + Authorization: "Bearer ${inputs.admin_api_key}" + }, + method: "DELETE" + }, + test: { + type: TProviderFunctionTypes.HTTP as const, + url: "https://api.sendgrid.com/v3/api_keys/${internal.api_key_id}", + header: { + Authorization: "Bearer ${inputs.admin_api_key}" + }, + method: "GET" + } + } +}; diff --git a/backend/src/ee/secretRotation/types.ts b/backend/src/ee/secretRotation/types.ts new file mode 100644 index 000000000..36ad36798 --- /dev/null +++ b/backend/src/ee/secretRotation/types.ts @@ -0,0 +1,131 @@ +import { Document, Types } from "mongoose"; + +export interface ISecretRotation extends Document { + _id: Types.ObjectId; + name: string; + interval: number; + provider: string; + customProvider: Types.ObjectId; + workspace: Types.ObjectId; + environment: string; + secretPath: string; + outputs: Array<{ + key: string; + secret: Types.ObjectId; + }>; + status?: "success" | "failed"; + lastRotatedAt?: string; + statusMessage?: string; + encryptedData: string; + encryptedDataIV: string; + encryptedDataTag: string; + algorithm: string; + keyEncoding: string; +} + +export type ISecretRotationEncData = { + inputs: Record; + creds: Array<{ + outputs: Record; + internal: Record; + }>; +}; + +export type ISecretRotationData = { + inputs: Record; + outputs: Record; + internal: Record; +}; + +export type ISecretRotationProviderTemplate = { + name: string; + title: string; + image?: string; + description?: string; + template: TProviderTemplate; +}; + +export enum TProviderFunctionTypes { + HTTP = "http", + DB = "database" +} + +export enum TDbProviderClients { + // postgres, cockroack db, amazon red shift + Pg = "pg", + // mysql and maria db + Sql = "sql" +} + +export enum TAssignOp { + Direct = "direct", + JmesPath = "jmesopath" +} + +export type TJmesPathAssignOp = { + assign: TAssignOp.JmesPath; + path: string; +}; + +export type TDirectAssignOp = { + assign: TAssignOp.Direct; + value: string; +}; + +export type TAssignFunction = TJmesPathAssignOp | TDirectAssignOp; + +export type THttpProviderFunction = { + type: TProviderFunctionTypes.HTTP; + url: string; + method: string; + header?: Record; + query?: Record; + body?: Record; + setter?: Record; + pre?: Record; +}; + +export type TDbProviderFunction = { + type: TProviderFunctionTypes.DB; + client: TDbProviderClients; + username: string; + password: string; + host: string; + database: string; + port: string; + query: string; + setter?: Record; + pre?: Record; +}; + +export type TProviderFunction = THttpProviderFunction | TDbProviderFunction; + +export type TProviderTemplate = { + inputs: { + type: "object"; + properties: Record; + required?: string[]; + }; + outputs: Record; + functions: { + set: TProviderFunction; + remove?: TProviderFunction; + test: TProviderFunction; + }; +}; + +// function type args +export type TGetProviderTemplates = { + workspaceId: string; +}; + +export type TCreateSecretRotation = { + provider: string; + customProvider?: string; + workspaceId: string; + secretPath: string; + environment: string; + interval: number; + inputs: Record; + outputs: Record; +}; diff --git a/backend/src/ee/services/EELicenseService.ts b/backend/src/ee/services/EELicenseService.ts index f403f55d6..6baea04dc 100644 --- a/backend/src/ee/services/EELicenseService.ts +++ b/backend/src/ee/services/EELicenseService.ts @@ -38,6 +38,7 @@ interface FeatureSet { trial_end: number | null; has_used_trial: boolean; secretApproval: boolean; + secretRotation: boolean; } /** @@ -74,7 +75,8 @@ class EELicenseService { status: null, trial_end: null, has_used_trial: true, - secretApproval: false + secretApproval: false, + secretRotation: true, } public localFeatureSet: NodeCache; diff --git a/backend/src/ee/services/ProjectRoleService.ts b/backend/src/ee/services/ProjectRoleService.ts index 867edb7e5..ac71121de 100644 --- a/backend/src/ee/services/ProjectRoleService.ts +++ b/backend/src/ee/services/ProjectRoleService.ts @@ -50,7 +50,8 @@ export enum ProjectPermissionSub { Workspace = "workspace", Secrets = "secrets", SecretRollback = "secret-rollback", - SecretApproval = "secret-approval" + SecretApproval = "secret-approval", + SecretRotation = "secret-rotation" } type SubjectFields = { @@ -74,6 +75,7 @@ export type ProjectPermissionSet = | [ProjectPermissionActions, ProjectPermissionSub.Settings] | [ProjectPermissionActions, ProjectPermissionSub.ServiceTokens] | [ProjectPermissionActions, ProjectPermissionSub.SecretApproval] + | [ProjectPermissionActions, ProjectPermissionSub.SecretRotation] | [ProjectPermissionActions.Delete, ProjectPermissionSub.Workspace] | [ProjectPermissionActions.Edit, ProjectPermissionSub.Workspace] | [ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback] @@ -92,6 +94,11 @@ const buildAdminPermission = () => { can(ProjectPermissionActions.Edit, ProjectPermissionSub.SecretApproval); can(ProjectPermissionActions.Delete, ProjectPermissionSub.SecretApproval); + can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretRotation); + can(ProjectPermissionActions.Create, ProjectPermissionSub.SecretRotation); + can(ProjectPermissionActions.Edit, ProjectPermissionSub.SecretRotation); + can(ProjectPermissionActions.Delete, ProjectPermissionSub.SecretRotation); + can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback); can(ProjectPermissionActions.Create, ProjectPermissionSub.SecretRollback); @@ -162,6 +169,7 @@ const buildMemberPermission = () => { can(ProjectPermissionActions.Delete, ProjectPermissionSub.Secrets); can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretApproval); + can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretRotation); can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback); can(ProjectPermissionActions.Create, ProjectPermissionSub.SecretRollback); @@ -214,6 +222,7 @@ const buildViewerPermission = () => { can(ProjectPermissionActions.Read, ProjectPermissionSub.Secrets); can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretApproval); can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback); + can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretRotation); can(ProjectPermissionActions.Read, ProjectPermissionSub.Member); can(ProjectPermissionActions.Read, ProjectPermissionSub.Role); can(ProjectPermissionActions.Read, ProjectPermissionSub.Integrations); diff --git a/backend/src/ee/validation/secretRotation.ts b/backend/src/ee/validation/secretRotation.ts new file mode 100644 index 000000000..616844aaf --- /dev/null +++ b/backend/src/ee/validation/secretRotation.ts @@ -0,0 +1,32 @@ +import { z } from "zod"; + +export const createSecretRotationV1 = z.object({ + body: z.object({ + workspaceId: z.string().trim(), + secretPath: z.string().trim(), + environment: z.string().trim(), + interval: z.number().min(1), + provider: z.string().trim(), + customProvider: z.string().trim().optional(), + inputs: z.record(z.unknown()), + outputs: z.record(z.string()) + }) +}); + +export const restartSecretRotationV1 = z.object({ + body: z.object({ + id: z.string().trim() + }) +}); + +export const getSecretRotationV1 = z.object({ + query: z.object({ + workspaceId: z.string().trim() + }) +}); + +export const removeSecretRotationV1 = z.object({ + params: z.object({ + id: z.string().trim() + }) +}); diff --git a/backend/src/ee/validation/secretRotationProvider.ts b/backend/src/ee/validation/secretRotationProvider.ts new file mode 100644 index 000000000..d322939bb --- /dev/null +++ b/backend/src/ee/validation/secretRotationProvider.ts @@ -0,0 +1,7 @@ +import { z } from "zod"; + +export const getSecretRotationProvidersV1 = z.object({ + params: z.object({ + workspaceId: z.string() + }) +}); diff --git a/backend/src/helpers/secrets.ts b/backend/src/helpers/secrets.ts index 82a98d1e9..4b38079c1 100644 --- a/backend/src/helpers/secrets.ts +++ b/backend/src/helpers/secrets.ts @@ -553,14 +553,22 @@ export const getSecretsHelper = async ({ workspaceId, environment, authData, - folderId, secretPath = "/" }: GetSecretsParams) => { let secrets: ISecret[] = []; // if using service token filter towards the folderId by secretpath - if (!folderId) { - folderId = await getFolderIdFromServiceToken(workspaceId, environment, secretPath); + const folders = await Folder.findOne({ + workspace: workspaceId, + environment + }); + let folderId = "root"; + if (!folders && folderId !== "root") return []; + // get folder from folder tree + if (folders) { + const folder = getFolderByPath(folders.nodes, secretPath); + if (!folder) return []; + folderId = folder?.id; } // get personal secrets first diff --git a/backend/src/index.ts b/backend/src/index.ts index b1a7d80f5..c1e0baab9 100644 --- a/backend/src/index.ts +++ b/backend/src/index.ts @@ -27,8 +27,10 @@ import { users as eeUsersRouter, workspace as eeWorkspaceRouter, roles as v1RoleRouter, - secretApprovalPolicy as v1SecretApprovalPolicy, - secretApprovalRequest as v1SecretApprovalRequest, + secretApprovalPolicy as v1SecretApprovalPolicyRouter, + secretApprovalRequest as v1SecretApprovalRequestRouter, + secretRotation as v1SecretRotation, + secretRotationProvider as v1SecretRotationProviderRouter, secretScanning as v1SecretScanningRouter } from "./ee/routes/v1"; import { apiKeyData as v3apiKeyDataRouter } from "./ee/routes/v3"; @@ -194,6 +196,8 @@ const main = async () => { app.use("/api/v1/cloud-products", eeCloudProductsRouter); app.use("/api/v3/api-key", v3apiKeyDataRouter); // new app.use("/api/v3/service-token", v3ServiceTokenDataRouter); // new + app.use("/api/v1/secret-rotation-providers", v1SecretRotationProviderRouter); + app.use("/api/v1/secret-rotations", v1SecretRotation); // v1 routes app.use("/api/v1/signup", v1SignupRouter); @@ -217,9 +221,9 @@ const main = async () => { app.use("/api/v1/webhooks", v1WebhooksRouter); app.use("/api/v1/secret-imports", v1SecretImpsRouter); app.use("/api/v1/roles", v1RoleRouter); - app.use("/api/v1/secret-approvals", v1SecretApprovalPolicy); + app.use("/api/v1/secret-approvals", v1SecretApprovalPolicyRouter); app.use("/api/v1/sso", v1SSORouter); - app.use("/api/v1/secret-approval-requests", v1SecretApprovalRequest); + app.use("/api/v1/secret-approval-requests", v1SecretApprovalRequestRouter); // v2 routes (improvements) app.use("/api/v2/signup", v2SignupRouter); diff --git a/backend/src/interfaces/middleware/index.ts b/backend/src/interfaces/middleware/index.ts index 5146d25cd..0100d3ff6 100644 --- a/backend/src/interfaces/middleware/index.ts +++ b/backend/src/interfaces/middleware/index.ts @@ -1,39 +1,27 @@ import { Types } from "mongoose"; -import { - IServiceTokenData, - IServiceTokenDataV3, - IUser, -} from "../../models"; -import { - ServiceActor, - ServiceActorV3, - UserActor, - UserAgentType -} from "../../ee/models"; +import { IServiceTokenData, IServiceTokenDataV3, IUser } from "../../models"; +import { ServiceActor, ServiceActorV3, UserActor, UserAgentType } from "../../ee/models"; interface BaseAuthData { - ipAddress: string; - userAgent: string; - userAgentType: UserAgentType; - tokenVersionId?: Types.ObjectId; + ipAddress: string; + userAgent: string; + userAgentType: UserAgentType; + tokenVersionId?: Types.ObjectId; } export interface UserAuthData extends BaseAuthData { - actor: UserActor; - authPayload: IUser; + actor: UserActor; + authPayload: IUser; } export interface ServiceTokenV3AuthData extends BaseAuthData { - actor: ServiceActorV3; - authPayload: IServiceTokenDataV3; + actor: ServiceActorV3; + authPayload: IServiceTokenDataV3; } export interface ServiceTokenAuthData extends BaseAuthData { - actor: ServiceActor; - authPayload: IServiceTokenData; + actor: ServiceActor; + authPayload: IServiceTokenData; } -export type AuthData = - | UserAuthData - | ServiceTokenV3AuthData - | ServiceTokenAuthData; \ No newline at end of file +export type AuthData = UserAuthData | ServiceTokenV3AuthData | ServiceTokenAuthData; diff --git a/backend/src/interfaces/services/SecretService/index.ts b/backend/src/interfaces/services/SecretService/index.ts index a2d9c5bb5..631114a79 100644 --- a/backend/src/interfaces/services/SecretService/index.ts +++ b/backend/src/interfaces/services/SecretService/index.ts @@ -26,7 +26,6 @@ export interface CreateSecretParams { export interface GetSecretsParams { workspaceId: Types.ObjectId; environment: string; - folderId?: string; secretPath: string; authData: AuthData; } diff --git a/backend/src/validation/secrets.ts b/backend/src/validation/secrets.ts index 174ffa791..799e9531d 100644 --- a/backend/src/validation/secrets.ts +++ b/backend/src/validation/secrets.ts @@ -228,7 +228,6 @@ export const GetSecretsRawV3 = z.object({ workspaceId: z.string().trim().optional(), environment: z.string().trim().optional(), secretPath: z.string().trim().default("/"), - folderId: z.string().trim().optional(), include_imports: z .enum(["true", "false"]) .default("false") @@ -302,7 +301,6 @@ export const GetSecretsV3 = z.object({ workspaceId: z.string().trim(), environment: z.string().trim(), secretPath: z.string().trim().default("/"), - folderId: z.string().trim().optional(), include_imports: z .enum(["true", "false"]) .default("false") diff --git a/docs/documentation/platform/secret-rotation/mysql.mdx b/docs/documentation/platform/secret-rotation/mysql.mdx new file mode 100644 index 000000000..b630e349a --- /dev/null +++ b/docs/documentation/platform/secret-rotation/mysql.mdx @@ -0,0 +1,37 @@ +--- +title: "MySQL/MariaDB" +description: "Rotated database user password of a MySQL or MariaDB" +--- + +Infisical will update periodically the provided database user's password. + + + At present Infisical do require access to your database. We will soon be released Infisical agent based rotation which would help you rotate without direct database access from Infisical cloud. + + +## Working + +1. User's has to create the two user's for Infisical to rotate and provide them required database access +2. Infisical will connect with your database with admin access +3. If last rotated one was username1, then username2 is chosen to be rotated +5. Update it's password with random value +6. After testing it gets saved to the provided secret mapping + +## Rotation Configuration + +1. Head over to Secret Rotation configuration page of your project by clicking on side bar `Secret Rotation` +2. Click on `MySQL` +3. Provide the inputs + - Admin Username: DB admin username + - Admin Password: DB admin password + - Host: DB host + - Port: DB port(number) + - Username1: The first username in two to rotate + - Username2: The second username in two to rotate + - CA: Certificate to connect with database(string) +4. Final step + - Select `Environment`, `Secret Path` and `Interval` to rotate the secrets + - Finally select the secrets in your provided board to replace with new secret after each rotation + - Your done and good to go. + +Congrats. You have 10x your MySQL/MariaDB access security. diff --git a/docs/documentation/platform/secret-rotation/overview.mdx b/docs/documentation/platform/secret-rotation/overview.mdx new file mode 100644 index 000000000..a11142106 --- /dev/null +++ b/docs/documentation/platform/secret-rotation/overview.mdx @@ -0,0 +1,37 @@ +--- +title: "Secret Rotation Overview" +description: "Keep your credentials safe by rotation" +--- + +Secret rotation is the process of periodically changing the values of secrets. This is done to reduce the risk of secrets being compromised and used to gain unauthorized access to systems or data. + +Rotated secrets can be +1. API key for an external service +2. Database credentials + +## How does the rotation happen? + +There are four phases in secret rotation and its triggered periodically in an internval. + +1. Creation + +System will create secret by calling an external service like an API call, or randomly generate a value. +Now there exist three valid secrets. + +2. Test + +Test the new secret key by some check to ensure its working one. Thus only two will be considered active and the other is considered inactive. + +3. Deletion + +System will remove the inactive secret and now there exist two valid secrets + +4. Finish + +System will switch the secret value from the rotated ones and trigger side effects like webhooks and events. + +## Infisical Secret Rotation Strategies + +1. [SendGrid](./sendgrid) +2. [PostgreSQL/CockroachDB](./postgres) +3. [MySQL/MariaDB](./mysql) diff --git a/docs/documentation/platform/secret-rotation/postgres.mdx b/docs/documentation/platform/secret-rotation/postgres.mdx new file mode 100644 index 000000000..167b60fcd --- /dev/null +++ b/docs/documentation/platform/secret-rotation/postgres.mdx @@ -0,0 +1,37 @@ +--- +title: "PostgreSQL/CockroachDB" +description: "Rotated database user password of a postgreSQL or cochroach db" +--- + +Infisical will update periodically the provided database user's password. + + + At present Infisical do require access to your database. We will soon be released Infisical agent based rotation which would help you rotate without direct database access from Infisical cloud. + + +## Working + +1. User's has to create the two user's for Infisical to rotate and provide them required database access +2. Infisical will connect with your database with admin access +3. If last rotated one was username1, then username2 is chosen to be rotated +5. Update it's password with random value +6. After testing it gets saved to the provided secret mapping + +## Rotation Configuration + +1. Head over to Secret Rotation configuration page of your project by clicking on side bar `Secret Rotation` +2. Click on `PostgreSQL` +3. Provide the inputs + - Admin Username: DB admin username + - Admin Password: DB admin password + - Host: DB host + - Port: DB port(number) + - Username1: The first username in two to rotate + - Username2: The second username in two to rotate + - CA: Certificate to connect with database(string) +4. Final step + - Select `Environment`, `Secret Path` and `Interval` to rotate the secrets + - Finally select the secrets in your provided board to replace with new secret after each rotation + - Your done and good to go. + +Congrats. You have 10x your PostgreSQL/CockroachDB access security. diff --git a/docs/documentation/platform/secret-rotation/sendgrid.mdx b/docs/documentation/platform/secret-rotation/sendgrid.mdx new file mode 100644 index 000000000..c4dd2797f --- /dev/null +++ b/docs/documentation/platform/secret-rotation/sendgrid.mdx @@ -0,0 +1,31 @@ +--- +title: "Twilio SendGrid" +description: "Rotate Twilio SendGrid API keys" +--- + +Twilio SendGrid is a cloud-based email delivery platform that helps businesses send transactional and marketing emails. +It uses an API key to do various operations. Using Infisical you can easily dynamically change the keys. + +## Working + +1. Infisical will need an admin token of SendGrid to create API keys dynamically. +2. Using the given admin token and scope by user Infisical will create and rotate API keys periodically +3. Under the hood infisical uses [SendGrid API](https://docs.sendgrid.com/api-reference/api-keys/create-api-keys) + +## Rotation Configuration + +1. Head over to Secret Rotation configuration page of your project by clicking on side bar `Secret Rotation` +2. Click on `Twilio SendGrid Card` +3. Provide the inputs + - Admin API Key: + SendGrid admin key to create lower scoped API keys. + - API Key Scopes + SendGrid generated API Key's scopes. For more info refer [this doc](https://docs.sendgrid.com/api-reference/api-key-permissions/api-key-permissions) + +4. Final step + - Select `Environment`, `Secret Path` and `Interval` to rotate the secrets + - Finally select the secrets in your provided board to replace with new secret after each rotation + - Your done and good to go. + +Now your output mapped secret value will be replaced periodically by SendGrid. + diff --git a/docs/mint.json b/docs/mint.json index 92876198d..05904b56b 100644 --- a/docs/mint.json +++ b/docs/mint.json @@ -34,7 +34,10 @@ } }, "topbarLinks": [ - { "name": "Log In", "url": "https://app.infisical.com/login" } + { + "name": "Log In", + "url": "https://app.infisical.com/login" + } ], "topbarCtaButton": { "name": "Start for Free", @@ -120,6 +123,15 @@ "documentation/platform/audit-logs", "documentation/platform/token", "documentation/platform/mfa", + { + "group": "Secret Rotation", + "pages": [ + "documentation/platform/secret-rotation/overview", + "documentation/platform/secret-rotation/sendgrid", + "documentation/platform/secret-rotation/postgres", + "documentation/platform/secret-rotation/mysql" + ] + }, { "group": "SSO", "pages": [ diff --git a/frontend/public/images/secretRotation/mysql.png b/frontend/public/images/secretRotation/mysql.png new file mode 100644 index 000000000..d92befdbc Binary files /dev/null and b/frontend/public/images/secretRotation/mysql.png differ diff --git a/frontend/public/images/secretRotation/postgres.png b/frontend/public/images/secretRotation/postgres.png new file mode 100644 index 000000000..b7152860d Binary files /dev/null and b/frontend/public/images/secretRotation/postgres.png differ diff --git a/frontend/public/images/secretRotation/sendgrid.png b/frontend/public/images/secretRotation/sendgrid.png new file mode 100644 index 000000000..3d2c9a92d Binary files /dev/null and b/frontend/public/images/secretRotation/sendgrid.png differ diff --git a/frontend/public/lotties/rotation.json b/frontend/public/lotties/rotation.json new file mode 100644 index 000000000..d05d254a4 --- /dev/null +++ b/frontend/public/lotties/rotation.json @@ -0,0 +1 @@ +{"v":"5.12.1","fr":60,"ip":70,"op":130,"w":500,"h":500,"nm":"system-regular-18-autorenew","ddd":0,"assets":[{"id":"comp_0","nm":"in-autorenew","fr":60,"layers":[{"ddd":0,"ind":1,"ty":3,"nm":"NULL ","sr":1,"ks":{"o":{"a":0,"k":0,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.121],"y":[1]},"o":{"x":[0.089],"y":[0.223]},"t":0,"s":[0]},{"t":59,"s":[-360]}],"ix":10},"p":{"a":0,"k":[250,250,0],"ix":2,"l":2},"a":{"a":0,"k":[50,50,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"ip":0,"op":300,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":".primary.design","cl":"primary design","parent":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[56.365,-32.706,0],"ix":2,"l":2},"a":{"a":0,"k":[256.365,167.294,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0]],"v":[[36.203,36.206],[-36.201,36.206],[-36.203,-36.206]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3,"x":"var $bm_rt;\n$bm_rt = comp('system-regular-18-autorenew').layer('control').effect('primary')('Color');"},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":31.3,"ix":5},"lc":2,"lj":2,"bm":0,"nm":".primary","mn":"ADBE Vector Graphic - Stroke","hd":false,"cl":"primary"},{"ty":"tr","p":{"a":0,"k":[149.137,136.205],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[-54.792,0],[-7.083,-76.459]],"o":[[0,0],[26.458,-43.959],[78.542,0],[0,0]],"v":[[-139.584,5.208],[-139.584,5],[-10.208,-68.125],[139.584,68.125]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"tm","s":{"a":0,"k":0,"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.167],"y":[0.167]},"t":1,"s":[0]},{"t":9,"s":[100]}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3,"x":"var $bm_rt;\n$bm_rt = comp('system-regular-18-autorenew').layer('control').effect('primary')('Color');"},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":31.3,"ix":5},"lc":2,"lj":2,"bm":0,"nm":".primary","mn":"ADBE Vector Graphic - Stroke","hd":false,"cl":"primary"},{"ty":"tr","p":{"a":0,"k":[260.212,167.294],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 2","np":3,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false}],"ip":1,"op":60,"st":1,"ct":1,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":".primary.design","cl":"primary design","parent":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[43.319,132.712,0],"ix":2,"l":2},"a":{"a":0,"k":[243.319,332.712,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0]],"v":[[-36.203,-36.206],[36.201,-36.206],[36.203,36.206]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3,"x":"var $bm_rt;\n$bm_rt = comp('system-regular-18-autorenew').layer('control').effect('primary')('Color');"},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":31.3,"ix":5},"lc":2,"lj":2,"bm":0,"nm":".primary","mn":"ADBE Vector Graphic - Stroke","hd":false,"cl":"primary"},{"ty":"tr","p":{"a":0,"k":[350.223,363.801],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[54.792,0],[7.083,76.459]],"o":[[0,0],[-26.458,43.959],[-78.542,0],[0,0]],"v":[[139.584,-5.208],[139.584,-5],[10.208,68.125],[-139.584,-68.125]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3,"x":"var $bm_rt;\n$bm_rt = comp('system-regular-18-autorenew').layer('control').effect('primary')('Color');"},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":31.3,"ix":5},"lc":2,"lj":2,"bm":0,"nm":".primary","mn":"ADBE Vector Graphic - Stroke","hd":false,"cl":"primary"},{"ty":"tm","s":{"a":0,"k":0,"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.167],"y":[0.167]},"t":1,"s":[0]},{"t":9,"s":[100]}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":3,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false},{"ty":"tr","p":{"a":0,"k":[239.795,332.712],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 2","np":3,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false}],"ip":1,"op":60,"st":1,"ct":1,"bm":0},{"ddd":0,"ind":4,"ty":4,"nm":".primary.design","cl":"primary design","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[250.002,250.002,0],"ix":2,"l":2},"a":{"a":0,"k":[250,250,0],"ix":1,"l":2},"s":{"a":0,"k":[2083,2083,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0.41,0],[0,0],[0,-0.41],[-0.42,0],[0,0],[1.83,0],[0.31,3.35],[0.41,-0.05],[-0.04,-0.42],[-4.17,0],[-1.5,1.58],[0,0],[-0.42,0],[0,0.41],[0,0]],"o":[[0,0],[-0.42,0],[0,0.41],[0,0],[-1.22,1.28],[-3.39,0],[-0.04,-0.41],[-0.42,0.04],[0.38,4.12],[2.22,0],[0,0],[0,0.41],[0.42,0],[0,0],[-0.01,-0.42]],"v":[[6.917,-0.992],[3.417,-0.992],[2.657,-0.242],[3.417,0.508],[5.047,0.508],[0.317,2.528],[-6.173,-3.342],[-6.993,-4.022],[-7.673,-3.202],[0.317,4.028],[6.157,1.508],[6.157,3.238],[6.917,3.988],[7.677,3.238],[7.677,-0.242]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4,"x":"var $bm_rt;\n$bm_rt = comp('system-regular-18-autorenew').layer('control').effect('primary')('Color');"},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":".primary","mn":"ADBE Vector Graphic - Fill","hd":false,"cl":"primary"},{"ty":"tr","p":{"a":0,"k":[249.683,253.972],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[4.17,0],[1.51,-1.59],[0,0],[0.42,0],[0,-0.41],[0,0],[-0.42,0],[0,0],[0,0.41],[0.42,0],[0,0],[-1.83,0],[-0.31,-3.35],[-0.38,0],[-0.02,0],[0.04,0.42]],"o":[[-2.24,0],[0,0],[0,-0.41],[-0.42,0],[0,0],[0,0.41],[0,0],[0.42,0],[0,-0.41],[0,0],[1.21,-1.28],[3.39,0],[0.04,0.39],[0.02,0],[0.42,-0.04],[-0.38,-4.12]],"v":[[-0.307,-4.025],[-6.177,-1.475],[-6.177,-3.235],[-6.937,-3.985],[-7.687,-3.235],[-7.687,0.245],[-6.927,0.995],[-3.427,0.995],[-2.667,0.245],[-3.427,-0.505],[-5.027,-0.505],[-0.307,-2.525],[6.183,3.345],[6.933,4.025],[7.003,4.025],[7.683,3.205]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4,"x":"var $bm_rt;\n$bm_rt = comp('system-regular-18-autorenew').layer('control').effect('primary')('Color');"},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":".primary","mn":"ADBE Vector Graphic - Fill","hd":false,"cl":"primary"},{"ty":"tr","p":{"a":0,"k":[250.307,246.025],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 2","np":2,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false},{"ty":"tr","p":{"a":0,"k":[250.307,246.025],"ix":2},"a":{"a":0,"k":[250.307,246.025],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":60,"op":300,"st":0,"ct":1,"bm":0}]},{"id":"comp_1","nm":"hover-autorenew","fr":60,"layers":[{"ddd":0,"ind":1,"ty":3,"nm":"NULL ","sr":1,"ks":{"o":{"a":0,"k":0,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.15],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":1,"s":[0]},{"t":60,"s":[-360]}],"ix":10},"p":{"a":0,"k":[250,250,0],"ix":2,"l":2},"a":{"a":0,"k":[50,50,0],"ix":1,"l":2},"s":{"a":1,"k":[{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"t":1,"s":[101,100,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"t":4,"s":[100,100,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"t":40,"s":[100,100,100]},{"t":60,"s":[101,100,100]}],"ix":6,"l":2}},"ao":0,"ip":0,"op":300,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":".primary.design","cl":"primary design","parent":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[56.365,-32.706,0],"ix":2,"l":2},"a":{"a":0,"k":[256.365,167.294,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0]],"v":[[36.203,36.206],[-36.201,36.206],[-36.203,-36.206]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3,"x":"var $bm_rt;\n$bm_rt = comp('system-regular-18-autorenew').layer('control').effect('primary')('Color');"},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":31.3,"ix":5},"lc":2,"lj":2,"bm":0,"nm":".primary","mn":"ADBE Vector Graphic - Stroke","hd":false,"cl":"primary"},{"ty":"tr","p":{"a":0,"k":[149.137,136.205],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[-54.792,0],[-7.083,-76.459]],"o":[[0,0],[26.458,-43.959],[78.542,0],[0,0]],"v":[[-139.584,5.208],[-139.584,5],[-10.208,-68.125],[139.584,68.125]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"tm","s":{"a":0,"k":0,"ix":1},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3,"x":"var $bm_rt;\n$bm_rt = comp('system-regular-18-autorenew').layer('control').effect('primary')('Color');"},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":31.3,"ix":5},"lc":2,"lj":2,"bm":0,"nm":".primary","mn":"ADBE Vector Graphic - Stroke","hd":false,"cl":"primary"},{"ty":"tr","p":{"a":0,"k":[260.212,167.294],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 2","np":3,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false}],"ip":1,"op":60,"st":1,"ct":1,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":".primary.design","cl":"primary design","parent":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[43.319,132.712,0],"ix":2,"l":2},"a":{"a":0,"k":[243.319,332.712,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0]],"v":[[-36.203,-36.206],[36.201,-36.206],[36.203,36.206]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3,"x":"var $bm_rt;\n$bm_rt = comp('system-regular-18-autorenew').layer('control').effect('primary')('Color');"},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":31.3,"ix":5},"lc":2,"lj":2,"bm":0,"nm":".primary","mn":"ADBE Vector Graphic - Stroke","hd":false,"cl":"primary"},{"ty":"tr","p":{"a":0,"k":[350.223,363.801],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[54.792,0],[7.083,76.459]],"o":[[0,0],[-26.458,43.959],[-78.542,0],[0,0]],"v":[[139.584,-5.208],[139.584,-5],[10.208,68.125],[-139.584,-68.125]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3,"x":"var $bm_rt;\n$bm_rt = comp('system-regular-18-autorenew').layer('control').effect('primary')('Color');"},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":31.3,"ix":5},"lc":2,"lj":2,"bm":0,"nm":".primary","mn":"ADBE Vector Graphic - Stroke","hd":false,"cl":"primary"},{"ty":"tm","s":{"a":0,"k":0,"ix":1},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":3,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false},{"ty":"tr","p":{"a":0,"k":[239.795,332.712],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 2","np":3,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false}],"ip":1,"op":60,"st":1,"ct":1,"bm":0},{"ddd":0,"ind":4,"ty":4,"nm":".primary.design","cl":"primary design","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[250.002,250.002,0],"ix":2,"l":2},"a":{"a":0,"k":[250,250,0],"ix":1,"l":2},"s":{"a":0,"k":[2083,2083,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0.41,0],[0,0],[0,-0.41],[-0.42,0],[0,0],[1.83,0],[0.31,3.35],[0.41,-0.05],[-0.04,-0.42],[-4.17,0],[-1.5,1.58],[0,0],[-0.42,0],[0,0.41],[0,0]],"o":[[0,0],[-0.42,0],[0,0.41],[0,0],[-1.22,1.28],[-3.39,0],[-0.04,-0.41],[-0.42,0.04],[0.38,4.12],[2.22,0],[0,0],[0,0.41],[0.42,0],[0,0],[-0.01,-0.42]],"v":[[6.917,-0.992],[3.417,-0.992],[2.657,-0.242],[3.417,0.508],[5.047,0.508],[0.317,2.528],[-6.173,-3.342],[-6.993,-4.022],[-7.673,-3.202],[0.317,4.028],[6.157,1.508],[6.157,3.238],[6.917,3.988],[7.677,3.238],[7.677,-0.242]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4,"x":"var $bm_rt;\n$bm_rt = comp('system-regular-18-autorenew').layer('control').effect('primary')('Color');"},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":".primary","mn":"ADBE Vector Graphic - Fill","hd":false,"cl":"primary"},{"ty":"tr","p":{"a":0,"k":[249.683,253.972],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[4.17,0],[1.51,-1.59],[0,0],[0.42,0],[0,-0.41],[0,0],[-0.42,0],[0,0],[0,0.41],[0.42,0],[0,0],[-1.83,0],[-0.31,-3.35],[-0.38,0],[-0.02,0],[0.04,0.42]],"o":[[-2.24,0],[0,0],[0,-0.41],[-0.42,0],[0,0],[0,0.41],[0,0],[0.42,0],[0,-0.41],[0,0],[1.21,-1.28],[3.39,0],[0.04,0.39],[0.02,0],[0.42,-0.04],[-0.38,-4.12]],"v":[[-0.307,-4.025],[-6.177,-1.475],[-6.177,-3.235],[-6.937,-3.985],[-7.687,-3.235],[-7.687,0.245],[-6.927,0.995],[-3.427,0.995],[-2.667,0.245],[-3.427,-0.505],[-5.027,-0.505],[-0.307,-2.525],[6.183,3.345],[6.933,4.025],[7.003,4.025],[7.683,3.205]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4,"x":"var $bm_rt;\n$bm_rt = comp('system-regular-18-autorenew').layer('control').effect('primary')('Color');"},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":".primary","mn":"ADBE Vector Graphic - Fill","hd":false,"cl":"primary"},{"ty":"tr","p":{"a":0,"k":[250.307,246.025],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 2","np":2,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false},{"ty":"tr","p":{"a":0,"k":[250.307,246.025],"ix":2},"a":{"a":0,"k":[250.307,246.025],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":1,"st":-60,"ct":1,"bm":0},{"ddd":0,"ind":5,"ty":4,"nm":".primary.design","cl":"primary design","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[250.002,250.002,0],"ix":2,"l":2},"a":{"a":0,"k":[250,250,0],"ix":1,"l":2},"s":{"a":0,"k":[2083,2083,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0.41,0],[0,0],[0,-0.41],[-0.42,0],[0,0],[1.83,0],[0.31,3.35],[0.41,-0.05],[-0.04,-0.42],[-4.17,0],[-1.5,1.58],[0,0],[-0.42,0],[0,0.41],[0,0]],"o":[[0,0],[-0.42,0],[0,0.41],[0,0],[-1.22,1.28],[-3.39,0],[-0.04,-0.41],[-0.42,0.04],[0.38,4.12],[2.22,0],[0,0],[0,0.41],[0.42,0],[0,0],[-0.01,-0.42]],"v":[[6.917,-0.992],[3.417,-0.992],[2.657,-0.242],[3.417,0.508],[5.047,0.508],[0.317,2.528],[-6.173,-3.342],[-6.993,-4.022],[-7.673,-3.202],[0.317,4.028],[6.157,1.508],[6.157,3.238],[6.917,3.988],[7.677,3.238],[7.677,-0.242]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4,"x":"var $bm_rt;\n$bm_rt = comp('system-regular-18-autorenew').layer('control').effect('primary')('Color');"},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":".primary","mn":"ADBE Vector Graphic - Fill","hd":false,"cl":"primary"},{"ty":"tr","p":{"a":0,"k":[249.683,253.972],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[4.17,0],[1.51,-1.59],[0,0],[0.42,0],[0,-0.41],[0,0],[-0.42,0],[0,0],[0,0.41],[0.42,0],[0,0],[-1.83,0],[-0.31,-3.35],[-0.38,0],[-0.02,0],[0.04,0.42]],"o":[[-2.24,0],[0,0],[0,-0.41],[-0.42,0],[0,0],[0,0.41],[0,0],[0.42,0],[0,-0.41],[0,0],[1.21,-1.28],[3.39,0],[0.04,0.39],[0.02,0],[0.42,-0.04],[-0.38,-4.12]],"v":[[-0.307,-4.025],[-6.177,-1.475],[-6.177,-3.235],[-6.937,-3.985],[-7.687,-3.235],[-7.687,0.245],[-6.927,0.995],[-3.427,0.995],[-2.667,0.245],[-3.427,-0.505],[-5.027,-0.505],[-0.307,-2.525],[6.183,3.345],[6.933,4.025],[7.003,4.025],[7.683,3.205]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4,"x":"var $bm_rt;\n$bm_rt = comp('system-regular-18-autorenew').layer('control').effect('primary')('Color');"},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":".primary","mn":"ADBE Vector Graphic - Fill","hd":false,"cl":"primary"},{"ty":"tr","p":{"a":0,"k":[250.307,246.025],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 2","np":2,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false},{"ty":"tr","p":{"a":0,"k":[250.307,246.025],"ix":2},"a":{"a":0,"k":[250.307,246.025],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":60,"op":300,"st":0,"ct":1,"bm":0}]},{"id":"comp_2","nm":"loop-autorenew","fr":60,"layers":[{"ddd":0,"ind":1,"ty":3,"nm":"NULL ","sr":1,"ks":{"o":{"a":0,"k":0,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[0]},{"t":60,"s":[-360]}],"ix":10},"p":{"a":0,"k":[250,250,0],"ix":2,"l":2},"a":{"a":0,"k":[50,50,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"ip":0,"op":300,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":".primary.design","cl":"primary design","parent":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[56.365,-32.706,0],"ix":2,"l":2},"a":{"a":0,"k":[256.365,167.294,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0]],"v":[[36.203,36.206],[-36.201,36.206],[-36.203,-36.206]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3,"x":"var $bm_rt;\n$bm_rt = comp('system-regular-18-autorenew').layer('control').effect('primary')('Color');"},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":31.3,"ix":5},"lc":2,"lj":2,"bm":0,"nm":".primary","mn":"ADBE Vector Graphic - Stroke","hd":false,"cl":"primary"},{"ty":"tr","p":{"a":0,"k":[149.137,136.205],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[-54.792,0],[-7.083,-76.459]],"o":[[0,0],[26.458,-43.959],[78.542,0],[0,0]],"v":[[-139.584,5.208],[-139.584,5],[-10.208,-68.125],[139.584,68.125]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"tm","s":{"a":0,"k":0,"ix":1},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3,"x":"var $bm_rt;\n$bm_rt = comp('system-regular-18-autorenew').layer('control').effect('primary')('Color');"},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":31.3,"ix":5},"lc":2,"lj":2,"bm":0,"nm":".primary","mn":"ADBE Vector Graphic - Stroke","hd":false,"cl":"primary"},{"ty":"tr","p":{"a":0,"k":[260.212,167.294],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 2","np":3,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":75,"st":1,"ct":1,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":".primary.design","cl":"primary design","parent":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[43.319,132.712,0],"ix":2,"l":2},"a":{"a":0,"k":[243.319,332.712,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0]],"v":[[-36.203,-36.206],[36.201,-36.206],[36.203,36.206]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3,"x":"var $bm_rt;\n$bm_rt = comp('system-regular-18-autorenew').layer('control').effect('primary')('Color');"},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":31.3,"ix":5},"lc":2,"lj":2,"bm":0,"nm":".primary","mn":"ADBE Vector Graphic - Stroke","hd":false,"cl":"primary"},{"ty":"tr","p":{"a":0,"k":[350.223,363.801],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[54.792,0],[7.083,76.459]],"o":[[0,0],[-26.458,43.959],[-78.542,0],[0,0]],"v":[[139.584,-5.208],[139.584,-5],[10.208,68.125],[-139.584,-68.125]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3,"x":"var $bm_rt;\n$bm_rt = comp('system-regular-18-autorenew').layer('control').effect('primary')('Color');"},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":31.3,"ix":5},"lc":2,"lj":2,"bm":0,"nm":".primary","mn":"ADBE Vector Graphic - Stroke","hd":false,"cl":"primary"},{"ty":"tm","s":{"a":0,"k":0,"ix":1},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":3,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false},{"ty":"tr","p":{"a":0,"k":[239.795,332.712],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 2","np":3,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":75,"st":1,"ct":1,"bm":0}]}],"layers":[{"ddd":0,"ind":1,"ty":3,"nm":"control","sr":1,"ks":{"o":{"a":0,"k":0,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[0,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"ef":[{"ty":5,"nm":"primary","np":3,"mn":"ADBE Color Control","ix":1,"en":1,"ef":[{"ty":2,"nm":"Color","mn":"ADBE Color Control-0001","ix":1,"v":{"a":0,"k":[1,1,1],"ix":1}}]}],"ip":0,"op":201,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":0,"nm":"in-autorenew","refId":"comp_0","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[250,250,0],"ix":2,"l":2},"a":{"a":0,"k":[250,250,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"w":500,"h":500,"ip":0,"op":70,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":0,"nm":"hover-autorenew","refId":"comp_1","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[250,250,0],"ix":2,"l":2},"a":{"a":0,"k":[250,250,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"w":500,"h":500,"ip":70,"op":140,"st":70,"bm":0},{"ddd":0,"ind":4,"ty":0,"nm":"loop-autorenew","refId":"comp_2","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[250,250,0],"ix":2,"l":2},"a":{"a":0,"k":[250,250,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"w":500,"h":500,"ip":140,"op":210,"st":140,"bm":0}],"markers":[{"tm":0,"cm":"in-autorenew","dr":60},{"tm":70,"cm":"default:hover-autorenew","dr":60},{"tm":140,"cm":"loop-autorenew","dr":60}],"props":{}} \ No newline at end of file diff --git a/frontend/src/components/v2/FormControl/FormControl.tsx b/frontend/src/components/v2/FormControl/FormControl.tsx index 91dfd4139..8f2e57f48 100644 --- a/frontend/src/components/v2/FormControl/FormControl.tsx +++ b/frontend/src/components/v2/FormControl/FormControl.tsx @@ -9,16 +9,24 @@ export type FormLabelProps = { isRequired?: boolean; label?: ReactNode; icon?: ReactNode; + className?: string; }; -export const FormLabel = ({ id, label, isRequired, icon }: FormLabelProps) => ( +export const FormLabel = ({ id, label, isRequired, icon, className }: FormLabelProps) => ( {label} {isRequired && *} - {icon && {icon}} + {icon && ( + + {icon} + + )} ); diff --git a/frontend/src/components/v2/Stepper/Stepper.tsx b/frontend/src/components/v2/Stepper/Stepper.tsx new file mode 100644 index 000000000..8389121c0 --- /dev/null +++ b/frontend/src/components/v2/Stepper/Stepper.tsx @@ -0,0 +1,78 @@ +import { Children, cloneElement, ReactElement, ReactNode } from "react"; +import { faCheck } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { twMerge } from "tailwind-merge"; + +export type StepperProps = { + activeStep: number; + children: ReactNode; + direction: "vertical" | "horizontal"; + className?: string; +}; + +export const Stepper = ({ activeStep, children, direction, className }: StepperProps) => { + return ( +
+ {Children.map(children as ReactNode, (child: ReactNode, index) => { + const isCompleted = activeStep > index; + const isActive = index === activeStep; + const isNotLast = index + 1 !== (children as Array).length; + return ( +
+
+
+ {isCompleted ? : index + 1} +
+ {cloneElement(child as ReactElement, { + direction, + activeStep, + isCompleted, + isActive + })} +
+ {isNotLast && ( +
+ )} +
+ ); + })} +
+ ); +}; + +export type StepProps = { + title: string; + description?: ReactNode; + // isActive?: boolean; + // isCompleted?: boolean; + // activeStep?: number; + // direction?: "vertical" | "horizontal"; +}; + +export const Step = ({ title, description }: StepProps) => { + return ( +
+
{title}
+ {description &&
{description}
} +
+ ); +}; diff --git a/frontend/src/components/v2/Stepper/index.tsx b/frontend/src/components/v2/Stepper/index.tsx new file mode 100644 index 000000000..b90544cdd --- /dev/null +++ b/frontend/src/components/v2/Stepper/index.tsx @@ -0,0 +1,2 @@ +export type { StepperProps,StepProps } from "./Stepper"; +export { Step,Stepper } from "./Stepper"; diff --git a/frontend/src/components/v2/index.tsx b/frontend/src/components/v2/index.tsx index 31e2b4e57..26af93f37 100644 --- a/frontend/src/components/v2/index.tsx +++ b/frontend/src/components/v2/index.tsx @@ -22,6 +22,7 @@ export * from "./SecretInput"; export * from "./Select"; export * from "./Skeleton"; export * from "./Spinner"; +export * from "./Stepper"; export * from "./Switch"; export * from "./Table"; export * from "./Tabs"; diff --git a/frontend/src/context/ProjectPermissionContext/types.ts b/frontend/src/context/ProjectPermissionContext/types.ts index ae4b49d63..5ae91e756 100644 --- a/frontend/src/context/ProjectPermissionContext/types.ts +++ b/frontend/src/context/ProjectPermissionContext/types.ts @@ -21,7 +21,8 @@ export enum ProjectPermissionSub { Workspace = "workspace", Secrets = "secrets", SecretRollback = "secret-rollback", - SecretApproval = "secret-approval" + SecretApproval = "secret-approval", + SecretRotation = "secret-rotation" } type SubjectFields = { @@ -45,6 +46,7 @@ export type ProjectPermissionSet = | [ProjectPermissionActions, ProjectPermissionSub.Settings] | [ProjectPermissionActions, ProjectPermissionSub.ServiceTokens] | [ProjectPermissionActions, ProjectPermissionSub.SecretApproval] + | [ProjectPermissionActions, ProjectPermissionSub.SecretRotation] | [ProjectPermissionActions.Delete, ProjectPermissionSub.Workspace] | [ProjectPermissionActions.Edit, ProjectPermissionSub.Workspace] | [ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback] diff --git a/frontend/src/hooks/api/index.tsx b/frontend/src/hooks/api/index.tsx index 008f1e2fb..c707fe33f 100644 --- a/frontend/src/hooks/api/index.tsx +++ b/frontend/src/hooks/api/index.tsx @@ -12,6 +12,7 @@ export * from "./secretApproval"; export * from "./secretApprovalRequest"; export * from "./secretFolders"; export * from "./secretImports"; +export * from "./secretRotation"; export * from "./secrets"; export * from "./secretSnapshots"; export * from "./serviceAccounts"; diff --git a/frontend/src/hooks/api/secretRotation/index.ts b/frontend/src/hooks/api/secretRotation/index.ts new file mode 100644 index 000000000..906c8b498 --- /dev/null +++ b/frontend/src/hooks/api/secretRotation/index.ts @@ -0,0 +1,6 @@ +export { + useCreateSecretRotation, + useDeleteSecretRotation, + useRestartSecretRotation +} from "./mutation"; +export { useGetSecretRotationProviders, useGetSecretRotations } from "./queries"; diff --git a/frontend/src/hooks/api/secretRotation/mutation.tsx b/frontend/src/hooks/api/secretRotation/mutation.tsx new file mode 100644 index 000000000..cda4f8074 --- /dev/null +++ b/frontend/src/hooks/api/secretRotation/mutation.tsx @@ -0,0 +1,52 @@ +import { useMutation, useQueryClient } from "@tanstack/react-query"; + +import { apiRequest } from "@app/config/request"; + +import { secretRotationKeys } from "./queries"; +import { + TCreateSecretRotationDTO, + TDeleteSecretRotationDTO, + TRestartSecretRotationDTO +} from "./types"; + +export const useCreateSecretRotation = () => { + const queryClient = useQueryClient(); + + return useMutation<{}, {}, TCreateSecretRotationDTO>({ + mutationFn: async (dto) => { + const { data } = await apiRequest.post("/api/v1/secret-rotations", dto); + return data; + }, + onSuccess: (_, { workspaceId }) => { + queryClient.invalidateQueries(secretRotationKeys.list({ workspaceId })); + } + }); +}; + +export const useDeleteSecretRotation = () => { + const queryClient = useQueryClient(); + + return useMutation<{}, {}, TDeleteSecretRotationDTO>({ + mutationFn: async (dto) => { + const { data } = await apiRequest.delete(`/api/v1/secret-rotations/${dto.id}`); + return data; + }, + onSuccess: (_, { workspaceId }) => { + queryClient.invalidateQueries(secretRotationKeys.list({ workspaceId })); + } + }); +}; + +export const useRestartSecretRotation = () => { + const queryClient = useQueryClient(); + + return useMutation<{}, {}, TRestartSecretRotationDTO>({ + mutationFn: async (dto) => { + const { data } = await apiRequest.post("/api/v1/secret-rotations/restart", { id: dto.id }); + return data; + }, + onSuccess: (_, { workspaceId }) => { + queryClient.invalidateQueries(secretRotationKeys.list({ workspaceId })); + } + }); +}; diff --git a/frontend/src/hooks/api/secretRotation/queries.tsx b/frontend/src/hooks/api/secretRotation/queries.tsx new file mode 100644 index 000000000..a3c8e270e --- /dev/null +++ b/frontend/src/hooks/api/secretRotation/queries.tsx @@ -0,0 +1,110 @@ +import { useCallback } from "react"; +import { useQuery, UseQueryOptions } from "@tanstack/react-query"; + +import { + decryptAssymmetric, + decryptSymmetric +} from "@app/components/utilities/cryptography/crypto"; +import { apiRequest } from "@app/config/request"; + +import { + TGetSecretRotationList, + TGetSecretRotationProviders, + TSecretRotation, + TSecretRotationProviderList +} from "./types"; + +export const secretRotationKeys = { + listProviders: ({ workspaceId }: TGetSecretRotationProviders) => [ + { workspaceId }, + "secret-rotation-providers" + ], + list: ({ workspaceId }: Omit) => + [{ workspaceId }, "secret-rotations"] as const +}; + +const fetchSecretRotationProviders = async ({ workspaceId }: TGetSecretRotationProviders) => { + const { data } = await apiRequest.get( + `/api/v1/secret-rotation-providers/${workspaceId}` + ); + return data; +}; + +export const useGetSecretRotationProviders = ({ + workspaceId, + options = {} +}: TGetSecretRotationProviders & { + options?: Omit< + UseQueryOptions< + TSecretRotationProviderList, + unknown, + TSecretRotationProviderList, + ReturnType + >, + "queryKey" | "queryFn" + >; +}) => + useQuery({ + ...options, + queryKey: secretRotationKeys.listProviders({ workspaceId }), + enabled: Boolean(workspaceId) && (options?.enabled ?? true), + queryFn: async () => fetchSecretRotationProviders({ workspaceId }) + }); + +const fetchSecretRotations = async ({ + workspaceId +}: Omit) => { + const { data } = await apiRequest.get<{ secretRotations: TSecretRotation[] }>( + "/api/v1/secret-rotations", + { params: { workspaceId } } + ); + return data.secretRotations; +}; + +export const useGetSecretRotations = ({ + workspaceId, + decryptFileKey, + options = {} +}: TGetSecretRotationList & { + options?: Omit< + UseQueryOptions< + TSecretRotation[], + unknown, + TSecretRotation<{ key: string }>[], + ReturnType + >, + "queryKey" | "queryFn" + >; +}) => + useQuery({ + ...options, + queryKey: secretRotationKeys.list({ workspaceId }), + enabled: Boolean(workspaceId) && (options?.enabled ?? true), + queryFn: async () => fetchSecretRotations({ workspaceId }), + select: useCallback( + (data: TSecretRotation[]) => { + const PRIVATE_KEY = localStorage.getItem("PRIVATE_KEY") as string; + const decryptKey = decryptAssymmetric({ + ciphertext: decryptFileKey.encryptedKey, + nonce: decryptFileKey.nonce, + publicKey: decryptFileKey.sender.publicKey, + privateKey: PRIVATE_KEY + }); + return data.map((el) => ({ + ...el, + outputs: el.outputs.map(({ key, secret }) => ({ + key, + secret: { + key: decryptSymmetric({ + ciphertext: secret.secretValueCiphertext, + iv: secret.secretValueIV, + tag: secret.secretValueTag, + key: decryptKey + }) + } + })) + })); + }, + [decryptFileKey] + ) + }); diff --git a/frontend/src/hooks/api/secretRotation/types.ts b/frontend/src/hooks/api/secretRotation/types.ts new file mode 100644 index 000000000..76d3b5174 --- /dev/null +++ b/frontend/src/hooks/api/secretRotation/types.ts @@ -0,0 +1,133 @@ +import { UserWsKeyPair } from "../keys/types"; +import { EncryptedSecret } from "../secrets/types"; + +export enum TProviderFunctionTypes { + HTTP = "http", + DB = "database" +} + +export enum TDbProviderClients { + // postgres, cockroack db, amazon red shift + Pg = "pg", + // mysql and maria db + Sql = "sql" +} + +export enum TAssignOp { + Direct = "direct", + JmesPath = "jmesopath" +} + +export type TJmesPathAssignOp = { + assign: TAssignOp.JmesPath; + path: string; +}; + +export type TDirectAssignOp = { + assign: TAssignOp.Direct; + value: string; +}; + +export type TAssignFunction = TJmesPathAssignOp | TDirectAssignOp; + +export type THttpProviderFunction = { + type: TProviderFunctionTypes.HTTP; + url: string; + method: string; + header?: Record; + query?: Record; + body?: Record; + setter?: Record; + pre?: Record; +}; + +export type TDbProviderFunction = { + type: TProviderFunctionTypes.DB; + client: TDbProviderClients; + username: string; + password: string; + host: string; + database: string; + port: string; + query: string; + setter?: Record; + pre?: Record; +}; + +export type TProviderFunction = THttpProviderFunction | TDbProviderFunction; + +export type TProviderTemplate = { + inputs: { + properties: Record; + type: "object"; + required: string[]; + }; + outputs: Record; + functions: { + set: TProviderFunction; + remove?: TProviderFunction; + test: TProviderFunction; + }; +}; + +export type TSecretRotation = { + _id: string; + interval: number; + provider: string; + customProvider: string; + workspace: string; + environment: string; + secretPath: string; + outputs: Array<{ + key: string; + secret: T; + }>; + status?: "success" | "failed"; + lastRotatedAt?: string; + statusMessage?: string; + algorithm: string; + keyEncoding: string; +}; + +export type TSecretRotationProvider = { + name: string; + image: string; + title: string; + description: string; + template: TProviderTemplate; +}; + +export type TSecretRotationProviderList = { + custom: TSecretRotationProvider[]; + providers: TSecretRotationProvider[]; +}; + +export type TGetSecretRotationProviders = { + workspaceId: string; +}; + +export type TGetSecretRotationList = { + workspaceId: string; + decryptFileKey: UserWsKeyPair; +}; + +export type TCreateSecretRotationDTO = { + workspaceId: string; + secretPath: string; + environment: string; + interval: number; + provider: string; + customProvider?: string; + inputs: Record; + outputs: Record; +}; + +export type TDeleteSecretRotationDTO = { + id: string; + workspaceId: string; +}; + +export type TRestartSecretRotationDTO = { + id: string; + workspaceId: string; +}; diff --git a/frontend/src/hooks/api/subscriptions/types.ts b/frontend/src/hooks/api/subscriptions/types.ts index c61967eec..e38a7527f 100644 --- a/frontend/src/hooks/api/subscriptions/types.ts +++ b/frontend/src/hooks/api/subscriptions/types.ts @@ -12,6 +12,7 @@ export type SubscriptionPlan = { secretVersioning: boolean; slug: string; secretApproval: string; + secretRotation: string; tier: number; workspaceLimit: number; workspacesUsed: number; diff --git a/frontend/src/hooks/api/types.ts b/frontend/src/hooks/api/types.ts index f6131e5b2..422219adf 100644 --- a/frontend/src/hooks/api/types.ts +++ b/frontend/src/hooks/api/types.ts @@ -13,6 +13,12 @@ export type { export { ApprovalStatus, CommitType } from "./secretApprovalRequest/types"; export type { TSecretFolder } from "./secretFolders/types"; export type { TImportedSecrets, TSecretImports } from "./secretImports/types"; +export type { + TGetSecretRotationProviders, + TProviderTemplate, + TSecretRotationProvider, + TSecretRotationProviderList +} from "./secretRotation/types"; export * from "./secrets/types"; export type { CreateServiceTokenDTO, ServiceToken } from "./serviceTokens/types"; export type { SubscriptionPlan } from "./subscriptions/types"; diff --git a/frontend/src/layouts/AppLayout/AppLayout.tsx b/frontend/src/layouts/AppLayout/AppLayout.tsx index 04302548e..44b3964bc 100644 --- a/frontend/src/layouts/AppLayout/AppLayout.tsx +++ b/frontend/src/layouts/AppLayout/AppLayout.tsx @@ -497,6 +497,18 @@ export const AppLayout = ({ children }: LayoutProps) => { + + + + Secret Rotation + + + { } icon="system-outline-189-domain-verification" > - Secret approvals + Secret Approvals {Boolean(secretApprovalReqCount?.open) && ( {secretApprovalReqCount?.open} diff --git a/frontend/src/pages/project/[id]/secret-rotation/index.tsx b/frontend/src/pages/project/[id]/secret-rotation/index.tsx new file mode 100644 index 000000000..c6227c5c5 --- /dev/null +++ b/frontend/src/pages/project/[id]/secret-rotation/index.tsx @@ -0,0 +1,23 @@ +import { useTranslation } from "react-i18next"; +import Head from "next/head"; + +import { SecretRotationPage } from "@app/views/SecretRotationPage"; + +const SecretRotation = () => { + const { t } = useTranslation(); + + return ( +
+ + {t("common.head-title", { title: t("settings.project.title") })} + + + + +
+ ); +}; + +export default SecretRotation; + +SecretRotation.requireAuth = true; diff --git a/frontend/src/views/SecretRotationPage/SecretRotationPage.tsx b/frontend/src/views/SecretRotationPage/SecretRotationPage.tsx new file mode 100644 index 000000000..0cb179db3 --- /dev/null +++ b/frontend/src/views/SecretRotationPage/SecretRotationPage.tsx @@ -0,0 +1,407 @@ +import { useTranslation } from "react-i18next"; +import { + faArrowsSpin, + faExclamationTriangle, + faFolder, + faInfoCircle, + faRotate, + faTrash +} from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { formatDistance } from "date-fns"; + +import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider"; +import { ProjectPermissionCan } from "@app/components/permissions"; +import { + Button, + DeleteActionModal, + EmptyState, + IconButton, + Modal, + ModalContent, + Skeleton, + Spinner, + Table, + TableContainer, + TableSkeleton, + TBody, + Td, + Th, + THead, + Tooltip, + Tr, + UpgradePlanModal +} from "@app/components/v2"; +import { + ProjectPermissionActions, + ProjectPermissionSub, + useProjectPermission, + useSubscription, + useWorkspace +} from "@app/context"; +import { withProjectPermission } from "@app/hoc"; +import { usePopUp } from "@app/hooks"; +import { + useDeleteSecretRotation, + useGetSecretRotationProviders, + useGetSecretRotations, + useGetUserWsKey, + useGetWorkspaceBot, + useRestartSecretRotation, + useUpdateBotActiveStatus +} from "@app/hooks/api"; +import { TSecretRotationProvider } from "@app/hooks/api/types"; + +import { CreateRotationForm } from "./components/CreateRotationForm"; +import { generateBotKey } from "./SecretRotationPage.utils"; + +export const SecretRotationPage = withProjectPermission( + () => { + const { currentWorkspace } = useWorkspace(); + const { t } = useTranslation(); + const permission = useProjectPermission(); + const { createNotification } = useNotificationContext(); + const { popUp, handlePopUpOpen, handlePopUpToggle, handlePopUpClose } = usePopUp([ + "createRotation", + "activeBot", + "deleteRotation", + "upgradePlan" + ] as const); + const workspaceId = currentWorkspace?._id || ""; + const canCreateRotation = permission.can( + ProjectPermissionActions.Create, + ProjectPermissionSub.SecretRotation + ); + const { subscription } = useSubscription(); + + const { data: userWsKey } = useGetUserWsKey(workspaceId); + + const { data: secretRotationProviders, isLoading: isRotationProviderLoading } = + useGetSecretRotationProviders({ workspaceId }); + const { data: secretRotations, isLoading: isRotationLoading } = useGetSecretRotations({ + workspaceId, + decryptFileKey: userWsKey! + }); + + const { + mutateAsync: deleteSecretRotation, + variables: deleteSecretRotationVars, + isLoading: isDeletingRotation + } = useDeleteSecretRotation(); + const { + mutateAsync: restartSecretRotation, + variables: restartSecretRotationVar, + isLoading: isRestartingRotation + } = useRestartSecretRotation(); + + const { data: bot } = useGetWorkspaceBot(workspaceId); + const { mutateAsync: updateBotActiveStatus } = useUpdateBotActiveStatus(); + + const isBotActive = Boolean(bot?.isActive); + + const handleDeleteRotation = async () => { + const { id } = popUp.deleteRotation.data as { id: string }; + try { + await deleteSecretRotation({ + id, + workspaceId + }); + handlePopUpClose("deleteRotation"); + createNotification({ + type: "success", + text: "Successfully removed rotation" + }); + } catch (error) { + console.log(error); + createNotification({ + type: "error", + text: "Failed to remove rotation" + }); + } + }; + + const handleRestartRotation = async (id: string) => { + try { + await restartSecretRotation({ + id, + workspaceId + }); + createNotification({ + type: "success", + text: "Secret rotation initiated" + }); + } catch (error) { + console.log(error); + createNotification({ + type: "error", + text: "Failed to restart rotation" + }); + } + }; + + const handleUserAcceptBotCondition = async () => { + const provider = popUp.activeBot?.data as TSecretRotationProvider; + try { + if (bot?._id) { + const botKey = generateBotKey(bot.publicKey, userWsKey!); + await updateBotActiveStatus({ + isActive: true, + botId: bot._id, + workspaceId, + botKey + }); + } + handlePopUpOpen("createRotation", provider); + handlePopUpClose("activeBot"); + } catch (error) { + console.log(error); + createNotification({ + type: "error", + text: "Failed to create bot" + }); + } + }; + + const handleCreateRotation = async (provider: TSecretRotationProvider) => { + if (subscription && !subscription?.secretRotation) { + handlePopUpOpen("upgradePlan"); + return; + } + if (!canCreateRotation) { + createNotification({ type: "error", text: "Access permission denied!!" }); + return; + } + if (isBotActive) { + handlePopUpOpen("createRotation", provider); + } else { + handlePopUpOpen("activeBot", provider); + } + }; + + return ( +
+
+

Secret Rotation

+

Auto rotate secrets for better security

+
+
+
Rotated Secrets
+
+ + + + + + + + + + + + + + {isRotationLoading && ( + + )} + {!isRotationLoading && secretRotations?.length === 0 && ( + + + + )} + {secretRotations?.map( + ({ + environment, + secretPath, + outputs, + provider, + _id, + lastRotatedAt, + status, + statusMessage + }) => { + const isDeleting = deleteSecretRotationVars?.id === _id && isDeletingRotation; + const isRestarting = + restartSecretRotationVar?.id === _id && isRestartingRotation; + return ( + + + + + + + + + ); + } + )} + +
Secret NameEnvironmentProviderStatusLast RotationAction
+ +
+ {outputs + .map(({ key }) => key) + .join(",") + .toUpperCase()} + +
+
{environment}
+
+ + {secretPath} +
+
+
{provider} +
+ {status} + {status === "failed" && ( + + + + )} +
+
+ {lastRotatedAt + ? formatDistance(new Date(lastRotatedAt), new Date()) + : "-"} + +
+ + {(isAllowed) => ( + handleRestartRotation(_id)} + > + {isRestarting ? ( + + ) : ( + + )} + + )} + + + {(isAllowed) => ( + handlePopUpOpen("deleteRotation", { id: _id })} + > + {isDeleting ? ( + + ) : ( + + )} + + )} + +
+
+
+
+
+
Infisical Rotation Providers
+
+ {isRotationProviderLoading && + Array.from({ length: 12 }).map((_, index) => ( + + ))} + {!isRotationProviderLoading && + secretRotationProviders?.providers.map((provider) => ( +
{ + if (evt.key === "Enter") handlePopUpOpen("createRotation", provider); + }} + onClick={() => handleCreateRotation(provider)} + > + rotation provider logo +
+ {provider.title} +
+
+ + + +
+
+ ))} +
+ handlePopUpToggle("createRotation", isOpen)} + provider={(popUp.createRotation.data as TSecretRotationProvider) || {}} + /> + handlePopUpToggle("activeBot", isOpen)} + > + + + +
+ } + > + {t("integrations.why-infisical-needs-access")} + + + handlePopUpToggle("deleteRotation", isOpen)} + deleteKey="delete" + onDeleteApproved={handleDeleteRotation} + /> + handlePopUpToggle("upgradePlan", isOpen)} + text="You can add secret rotation if you switch to Infisical's Team plan." + /> +
+ ); + }, + { action: ProjectPermissionActions.Read, subject: ProjectPermissionSub.SecretRotation } +); diff --git a/frontend/src/views/SecretRotationPage/SecretRotationPage.utils.ts b/frontend/src/views/SecretRotationPage/SecretRotationPage.utils.ts new file mode 100644 index 000000000..6dd346f8e --- /dev/null +++ b/frontend/src/views/SecretRotationPage/SecretRotationPage.utils.ts @@ -0,0 +1,30 @@ +import { UserWsKeyPair } from "@app/hooks/api/types"; + +import { + decryptAssymmetric, + encryptAssymmetric +} from "../../components/utilities/cryptography/crypto"; + +// refactor these to common function in frontend +export const generateBotKey = (botPublicKey: string, latestKey: UserWsKeyPair) => { + const PRIVATE_KEY = localStorage.getItem("PRIVATE_KEY"); + + if (!PRIVATE_KEY) { + throw new Error("Private Key missing"); + } + + const WORKSPACE_KEY = decryptAssymmetric({ + ciphertext: latestKey.encryptedKey, + nonce: latestKey.nonce, + publicKey: latestKey.sender.publicKey, + privateKey: PRIVATE_KEY + }); + + const { ciphertext, nonce } = encryptAssymmetric({ + plaintext: WORKSPACE_KEY, + publicKey: botPublicKey, + privateKey: PRIVATE_KEY + }); + + return { encryptedKey: ciphertext, nonce }; +}; diff --git a/frontend/src/views/SecretRotationPage/components/CreateRotationForm/CreateRotationForm.tsx b/frontend/src/views/SecretRotationPage/components/CreateRotationForm/CreateRotationForm.tsx new file mode 100644 index 000000000..3576cc0f9 --- /dev/null +++ b/frontend/src/views/SecretRotationPage/components/CreateRotationForm/CreateRotationForm.tsx @@ -0,0 +1,146 @@ +import { useRef, useState } from "react"; +import { AnimatePresence, motion } from "framer-motion"; + +import { Modal, ModalContent, Step, Stepper } from "@app/components/v2"; +import { useCreateSecretRotation } from "@app/hooks/api"; +import { TSecretRotationProvider } from "@app/hooks/api/types"; + +import { useNotificationContext } from "~/components/context/Notifications/NotificationProvider"; + +import { RotationInputForm } from "./steps/RotationInputForm"; +import { + RotationOutputForm, + TFormSchema as TRotationOutputSchema +} from "./steps/RotationOutputForm"; + +const WIZARD_STEPS = [ + { + title: "Inputs", + description: "Provider secrets" + }, + { + title: "Outputs", + description: "Map rotated secrets to keys" + } +]; + +type Props = { + isOpen?: boolean; + onToggle: (isOpen: boolean) => void; + customProvider?: string; + workspaceId: string; + provider: TSecretRotationProvider; +}; + +export const CreateRotationForm = ({ + isOpen, + onToggle, + provider, + workspaceId, + customProvider +}: Props) => { + const [wizardStep, setWizardStep] = useState(0); + const wizardData = useRef<{ + input?: Record; + output?: TRotationOutputSchema; + }>({}); + const { createNotification } = useNotificationContext(); + + const { mutateAsync: createSecretRotation } = useCreateSecretRotation(); + + const handleFormCancel = () => { + onToggle(false); + setWizardStep(0); + wizardData.current = {}; + }; + + const handleFormSubmit = async () => { + if (!wizardData.current.input || !wizardData.current.output) return; + try { + await createSecretRotation({ + workspaceId, + provider: provider.name, + customProvider, + secretPath: wizardData.current.output.secretPath, + environment: wizardData.current.output.environment, + interval: wizardData.current.output.interval, + inputs: wizardData.current.input, + outputs: wizardData.current.output.secrets + }); + setWizardStep(0); + onToggle(false); + wizardData.current = {}; + } catch (error) { + console.log(error); + createNotification({ + type: "error", + text: "Failed to create secret rotation" + }); + } + }; + + return ( + { + onToggle(state); + setWizardStep(0); + wizardData.current = {}; + }} + > + + + {WIZARD_STEPS.map(({ title, description }, index) => ( + + ))} + + + {wizardStep === 0 && ( + + { + wizardData.current.input = data; + setWizardStep((state) => state + 1); + }} + inputSchema={provider.template?.inputs || {}} + /> + + )} + {wizardStep === 1 && ( + + { + wizardData.current.output = data; + await handleFormSubmit(); + }} + /> + + )} + + + + ); +}; diff --git a/frontend/src/views/SecretRotationPage/components/CreateRotationForm/index.tsx b/frontend/src/views/SecretRotationPage/components/CreateRotationForm/index.tsx new file mode 100644 index 000000000..8392c4cdb --- /dev/null +++ b/frontend/src/views/SecretRotationPage/components/CreateRotationForm/index.tsx @@ -0,0 +1 @@ +export { CreateRotationForm } from "./CreateRotationForm"; diff --git a/frontend/src/views/SecretRotationPage/components/CreateRotationForm/steps/RotationInputForm.tsx b/frontend/src/views/SecretRotationPage/components/CreateRotationForm/steps/RotationInputForm.tsx new file mode 100644 index 000000000..125515bf1 --- /dev/null +++ b/frontend/src/views/SecretRotationPage/components/CreateRotationForm/steps/RotationInputForm.tsx @@ -0,0 +1,78 @@ +import { Controller, useForm } from "react-hook-form"; +import { faQuestionCircle } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { z } from "zod"; + +import { Button, FormControl, FormLabel, SecretInput, Tooltip } from "@app/components/v2"; + +type Props = { + onSubmit: (data: Record) => void; + onCancel: () => void; + inputSchema: { + properties: Record; + required: string[]; + }; +}; + +const formSchema = z.record(z.string().trim().optional()); + +export const RotationInputForm = ({ onSubmit, onCancel, inputSchema }: Props) => { + const { + control, + handleSubmit, + formState: { isSubmitting } + } = useForm>({ + resolver: zodResolver(formSchema) + }); + + return ( +
+ {Object.keys(inputSchema.properties || {}).map((inputName) => ( + ( + + + {Boolean(inputSchema.properties[inputName]?.desc) && ( + + + + )} + + } + > + + + )} + /> + ))} +
+ + +
+ + ); +}; diff --git a/frontend/src/views/SecretRotationPage/components/CreateRotationForm/steps/RotationOutputForm.tsx b/frontend/src/views/SecretRotationPage/components/CreateRotationForm/steps/RotationOutputForm.tsx new file mode 100644 index 000000000..e11038f7c --- /dev/null +++ b/frontend/src/views/SecretRotationPage/components/CreateRotationForm/steps/RotationOutputForm.tsx @@ -0,0 +1,153 @@ +import { Controller, useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { z } from "zod"; + +import { Button, FormControl, Input, Select, SelectItem, Spinner } from "@app/components/v2"; +import { useWorkspace } from "@app/context"; +import { useGetProjectSecrets, useGetUserWsKey } from "@app/hooks/api"; + +const formSchema = z.object({ + environment: z.string().trim(), + secretPath: z.string().trim().default("/"), + interval: z.number().min(1), + secrets: z.record(z.string()) +}); + +export type TFormSchema = z.infer; +type Props = { + outputSchema: Record; + onSubmit: (data: TFormSchema) => void; + onCancel: () => void; +}; + +export const RotationOutputForm = ({ onSubmit, onCancel, outputSchema = {} }: Props) => { + const { currentWorkspace } = useWorkspace(); + const environments = currentWorkspace?.environments || []; + const workspaceId = currentWorkspace?._id || ""; + const { + control, + handleSubmit, + watch, + formState: { isSubmitting } + } = useForm({ + resolver: zodResolver(formSchema) + }); + + const environment = watch("environment", environments?.[0]?.slug); + const secretPath = watch("secretPath"); + const selectedSecrets = watch("secrets"); + + const { data: userWsKey } = useGetUserWsKey(workspaceId); + const { data: secrets, isLoading: isSecretsLoading } = useGetProjectSecrets({ + workspaceId, + environment, + secretPath, + decryptFileKey: userWsKey! + }); + + return ( +
+ ( + + + + )} + /> + ( + + + + )} + /> + ( + + field.onChange(parseInt(evt.target.value, 10))} + /> + + )} + /> +
+
Mapping
+
Select keys for rotated value to get saved
+
+ {Object.keys(outputSchema).map((outputName) => ( + ( + + + + )} + /> + ))} +
+ + +
+ + ); +}; diff --git a/frontend/src/views/SecretRotationPage/index.tsx b/frontend/src/views/SecretRotationPage/index.tsx new file mode 100644 index 000000000..c2f41e76e --- /dev/null +++ b/frontend/src/views/SecretRotationPage/index.tsx @@ -0,0 +1 @@ +export { SecretRotationPage } from "./SecretRotationPage";