diff --git a/backend/package-lock.json b/backend/package-lock.json index 62426ad22..bf0abb3ed 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", + "mysql": "^2.18.1", "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,14 @@ "@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/mysql": "^2.15.23", "@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 +3514,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 +5878,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", @@ -5890,6 +5925,15 @@ "resolved": "https://registry.npmjs.org/@types/ms/-/ms-0.7.31.tgz", "integrity": "sha512-iiUgKzV9AuaEkZqkOLDIvlQiL6ltuZd9tGcW3gwpnX8JbuiuhFlEGmmFXEXkN50Cvq7Os88IY2v0dkDqXYWVgA==" }, + "node_modules/@types/mysql": { + "version": "2.15.23", + "resolved": "https://registry.npmjs.org/@types/mysql/-/mysql-2.15.23.tgz", + "integrity": "sha512-l3mEJpU1VNkt7uJP2vYWZrfjxlzQtwJNKSWe+7sgChuDjFoyRfu263f9pBDlhFPmyda23o8GNGq6FL5CHSd/QA==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/node": { "version": "18.16.19", "resolved": "https://registry.npmjs.org/@types/node/-/node-18.16.19.tgz", @@ -5921,6 +5965,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 +6531,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": { @@ -6900,6 +7011,14 @@ "@juanelas/base64": "^1.1.2" } }, + "node_modules/bignumber.js": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.0.0.tgz", + "integrity": "sha512-t/OYhhJ2SD+YGBQcjY8GzzDHEk9f3nerxjtfa6tlMXfe7frs/WozhvCNoGvpM0P3bNf3Gq5ZRMlGr5f3r4/N8A==", + "engines": { + "node": "*" + } + }, "node_modules/binary-extensions": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", @@ -7063,6 +7182,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", @@ -7450,8 +7577,7 @@ "node_modules/core-util-is": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", - "integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==", - "dev": true + "integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==" }, "node_modules/cors": { "version": "2.8.5", @@ -7912,6 +8038,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 +8079,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 +8415,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", @@ -10038,10 +10185,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", @@ -10715,6 +10861,47 @@ "@msgpackr-extract/msgpackr-extract-win32-x64": "3.0.2" } }, + "node_modules/mysql": { + "version": "2.18.1", + "resolved": "https://registry.npmjs.org/mysql/-/mysql-2.18.1.tgz", + "integrity": "sha512-Bca+gk2YWmqp2Uf6k5NFEurwY/0td0cpebAucFpY/3jhrwrVGuxU2uQFCHjU19SJfje0yQvi+rVWdq78hR5lig==", + "dependencies": { + "bignumber.js": "9.0.0", + "readable-stream": "2.3.7", + "safe-buffer": "5.1.2", + "sqlstring": "2.3.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mysql/node_modules/readable-stream": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/mysql/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "node_modules/mysql/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, "node_modules/nanoid": { "version": "3.3.6", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.6.tgz", @@ -13543,6 +13730,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 +13877,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 +14047,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 +14522,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", @@ -14710,6 +15041,11 @@ "node": ">= 0.6.0" } }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" + }, "node_modules/process-warning": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-2.3.0.tgz", @@ -15015,6 +15351,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", @@ -15498,6 +15842,14 @@ "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==" }, + "node_modules/sqlstring": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/sqlstring/-/sqlstring-2.3.1.tgz", + "integrity": "sha512-ooAzh/7dxIG5+uDik1z/Rd1vli0+38izZhGzSa34FwR7IbelPWCCKSNIl8jlL/F7ERvy8CB2jNeM1E9i9mXMAQ==", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/stack-utils": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", @@ -16197,7 +16549,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 +16557,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 +16865,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 +19765,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 +21641,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", @@ -21304,6 +21688,15 @@ "resolved": "https://registry.npmjs.org/@types/ms/-/ms-0.7.31.tgz", "integrity": "sha512-iiUgKzV9AuaEkZqkOLDIvlQiL6ltuZd9tGcW3gwpnX8JbuiuhFlEGmmFXEXkN50Cvq7Os88IY2v0dkDqXYWVgA==" }, + "@types/mysql": { + "version": "2.15.23", + "resolved": "https://registry.npmjs.org/@types/mysql/-/mysql-2.15.23.tgz", + "integrity": "sha512-l3mEJpU1VNkt7uJP2vYWZrfjxlzQtwJNKSWe+7sgChuDjFoyRfu263f9pBDlhFPmyda23o8GNGq6FL5CHSd/QA==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, "@types/node": { "version": "18.16.19", "resolved": "https://registry.npmjs.org/@types/node/-/node-18.16.19.tgz", @@ -21335,6 +21728,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 +22165,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" } }, @@ -22081,6 +22528,11 @@ "@juanelas/base64": "^1.1.2" } }, + "bignumber.js": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.0.0.tgz", + "integrity": "sha512-t/OYhhJ2SD+YGBQcjY8GzzDHEk9f3nerxjtfa6tlMXfe7frs/WozhvCNoGvpM0P3bNf3Gq5ZRMlGr5f3r4/N8A==" + }, "binary-extensions": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", @@ -22210,6 +22662,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", @@ -22485,8 +22942,7 @@ "core-util-is": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", - "integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==", - "dev": true + "integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==" }, "cors": { "version": "2.8.5", @@ -22789,6 +23245,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 +23272,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 +23564,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", @@ -24406,10 +24879,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", @@ -24919,6 +25391,46 @@ "node-gyp-build-optional-packages": "5.0.7" } }, + "mysql": { + "version": "2.18.1", + "resolved": "https://registry.npmjs.org/mysql/-/mysql-2.18.1.tgz", + "integrity": "sha512-Bca+gk2YWmqp2Uf6k5NFEurwY/0td0cpebAucFpY/3jhrwrVGuxU2uQFCHjU19SJfje0yQvi+rVWdq78hR5lig==", + "requires": { + "bignumber.js": "9.0.0", + "readable-stream": "2.3.7", + "safe-buffer": "5.1.2", + "sqlstring": "2.3.1" + }, + "dependencies": { + "readable-stream": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "requires": { + "safe-buffer": "~5.1.0" + } + } + } + }, "nanoid": { "version": "3.3.6", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.6.tgz", @@ -26900,6 +27412,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 +27522,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 +27642,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 +27978,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", @@ -27841,6 +28461,11 @@ "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==" }, + "process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" + }, "process-warning": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-2.3.0.tgz", @@ -28043,6 +28668,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", @@ -28413,6 +29043,11 @@ "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==" }, + "sqlstring": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/sqlstring/-/sqlstring-2.3.1.tgz", + "integrity": "sha512-ooAzh/7dxIG5+uDik1z/Rd1vli0+38izZhGzSa34FwR7IbelPWCCKSNIl8jlL/F7ERvy8CB2jNeM1E9i9mXMAQ==" + }, "stack-utils": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", @@ -28910,7 +29545,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 +29552,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 +29798,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..8d8f0228c 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", + "mysql": "^2.18.1", "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,14 @@ "@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/mysql": "^2.15.23", "@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/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/providerTemplates.ts b/backend/src/ee/secretRotation/providerTemplates.ts new file mode 100644 index 000000000..4becc2ccd --- /dev/null +++ b/backend/src/ee/secretRotation/providerTemplates.ts @@ -0,0 +1,215 @@ +import { + ISecretRotationProviderTemplate, + TProviderFunctionTypes, + TDbProviderClients, + TAssignOp +} from "./types"; + +const SENDGRID_TEMPLATE = { + inputs: { + type: "object" as const, + properties: { + admin_api_key: { type: "string" as const }, + scopes: { type: "array", items: { type: "string" as const } } + }, + required: ["admin_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.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" + } + } +}; + +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" }, + username2: { type: "string", default: "infisical-pg-user2" } + }, + required: ["admin_username", "admin_password", "host", "database"], + 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}", + 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}", + query: "SELECT NOW()" + } + } +}; + +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" }, + username2: { type: "string", default: "infisical-sql-user2" } + }, + required: ["admin_username", "admin_password", "host", "database"], + 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}", + query: + "ALTER USER ${internal.username} IDENTIFIED WITH mysql_native_password 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}", + query: "SELECT NOW()" + } + } +}; + +export const providerRotationTemplates: 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/queue.ts b/backend/src/ee/secretRotation/queue.ts new file mode 100644 index 000000000..a5fb368ec --- /dev/null +++ b/backend/src/ee/secretRotation/queue.ts @@ -0,0 +1,417 @@ +import axios from "axios"; +import Queue, { Job } from "bull"; +import jmespath from "jmespath"; +import { customAlphabet } from "nanoid"; +import { Client as PgClient } from "pg"; +import mysql from "mysql"; + +import { client, getRootEncryptionKey } from "../../config"; +import { BotService, TelemetryService } from "../../services"; +import { SecretRotation } from "./models"; +import { providerRotationTemplates } from "./providerTemplates"; +import { + ISecretRotationData, + ISecretRotationEncData, + ISecretRotationProviderTemplate, + TAssignOp, + TDbProviderClients, + TDbProviderFunction, + TDirectAssignOp, + THttpProviderFunction, + TProviderFunction, + TProviderFunctionTypes +} from "./types"; +import { encryptSymmetric128BitHexKeyUTF8 } from "../../utils/crypto"; +import { ISecret, Secret } from "../../models"; +import { SECRET_SHARED } from "../../variables"; +import { EESecretService } from "../services"; +import { SecretVersion } from "../models"; + +const REGEX = /\${([^}]+)}/g; +const SLUG_ALPHABETS = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"; +const nanoId = customAlphabet(SLUG_ALPHABETS, 10); + +const secretRotationQueue = new Queue("secret-rotation-service", process.env.REDIS_URL as string); + +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]; +}; + +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 }); +}; + +const secretRotationDbFn = async (func: TDbProviderFunction, variables: ISecretRotationData) => { + const { type, client, pre, ...dbConnection } = func; + const { username, password, host, database, port, query } = interpolate( + dbConnection, + getInterpolationValue(variables) + ); + 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 }); + 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 + }); + 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]; + } +}; + +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); + }); +}; + +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); + } + }); + } +}; + +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); + } +}; + +const secretRotationRemoveFn = async (func: TProviderFunction, variables: ISecretRotationData) => { + if (!func) return; + if (func.type === TProviderFunctionTypes.HTTP) { + // string interpolation + return await secretRotationHttpFn(func, variables); + } +}; + +secretRotationQueue.process(async (job: Job) => { + const rotationStratDocId = job.data.rotationDocId; + const secretRotation = await SecretRotation.findById(rotationStratDocId) + .select("+encryptedData +encryptedDataTag +encryptedDataIV") + .populate<{ + outputs: [ + { + key: string; + secret: ISecret; + } + ]; + }>("outputs.secret"); + + const infisicalRotationProvider = providerRotationTemplates.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 rootEncryptionKey = await getRootEncryptionKey(); + const decryptedData = client.decryptSymmetric( + secretRotation.encryptedData, + rootEncryptionKey, + secretRotation.encryptedDataIV, + secretRotation.encryptedDataTag + ); + + 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 + }); + + const postHogClient = await TelemetryService.getPostHogClient(); + + if (postHogClient) { + postHogClient.capture({ + event: "secrets rotated", + properties: { + numberOfSecrets: encryptedSecrets.length, + environment: secretRotation.environment, + workspaceId, + folderId + } + }); + } + } catch (err) { + console.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/service.ts b/backend/src/ee/secretRotation/service.ts new file mode 100644 index 000000000..7e02dc860 --- /dev/null +++ b/backend/src/ee/secretRotation/service.ts @@ -0,0 +1,116 @@ +import { + ISecretRotationEncData, + ISecretRotationProviderTemplate, + TCreateSecretRotation, + TGetProviderTemplates +} from "./types"; +import { + providerRotationTemplates as infisicalRotationTemplates, + providerRotationTemplates +} from "./providerTemplates"; +import { SecretRotation } from "./models"; +import { client, getRootEncryptionKey } from "../../config"; +import { BadRequestError } from "../../utils/errors"; +import Ajv from "ajv"; +import { removeSecretRotationQueue, startSecretRotationQueue } from "./queue"; + +const ajv = new Ajv(); + +export const getProviderTemplate = async ({ workspaceId }: TGetProviderTemplates) => { + return { + custom: [], + providers: infisicalRotationTemplates + }; +}; + +export const createSecretRotation = async ({ + workspaceId, + secretPath, + environment, + provider, + interval, + inputs, + outputs +}: TCreateSecretRotation) => { + const rotationTemplate = providerRotationTemplates.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 rootEncryptionKey = await getRootEncryptionKey(); + const { ciphertext, iv, tag } = client.encryptSymmetric( + JSON.stringify(encData), + rootEncryptionKey + ); + + const secretRotation = new SecretRotation({ + workspace: workspaceId, + provider, + environment, + secretPath, + interval, + outputs: Object.entries(outputs).map(([key, secret]) => ({ key, secret })), + encryptedData: ciphertext, + encryptedDataIV: iv, + encryptedDataTag: tag + }); + + 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/types.ts b/backend/src/ee/secretRotation/types.ts new file mode 100644 index 000000000..4aa5975de --- /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/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/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;