diff --git a/.infisicalignore b/.infisicalignore index fa3815a6e..b8fafe6db 100644 --- a/.infisicalignore +++ b/.infisicalignore @@ -1 +1 @@ -.github/resources/docker-compose.be-test.yml:generic-api-key:16 \ No newline at end of file +.github/resources/docker-compose.be-test.yml:generic-api-key:16 diff --git a/README.md b/README.md index 483195c8a..499f534a1 100644 --- a/README.md +++ b/README.md @@ -34,7 +34,7 @@ git commit activity - Cloudsmith downloads + Cloudsmith downloads Slack community channel diff --git a/backend/package-lock.json b/backend/package-lock.json index 1423cf03e..76b10ce2e 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -10,6 +10,8 @@ "license": "ISC", "dependencies": { "@aws-sdk/client-secrets-manager": "^3.319.0", + "@casl/ability": "^6.5.0", + "@casl/mongoose": "^7.2.1", "@godaddy/terminus": "^4.12.0", "@node-saml/passport-saml": "^4.0.4", "@octokit/rest": "^19.0.5", @@ -17,6 +19,7 @@ "@sentry/tracing": "^7.48.0", "@types/crypto-js": "^4.1.1", "@types/libsodium-wrappers": "^0.7.10", + "@ucast/mongo2js": "^1.3.4", "argon2": "^0.30.3", "aws-sdk": "^2.1364.0", "axios": "^1.3.5", @@ -52,14 +55,14 @@ "query-string": "^7.1.3", "rate-limit-mongo": "^2.3.2", "rimraf": "^3.0.2", - "swagger-autogen": "^2.22.0", "swagger-ui-express": "^4.6.2", "tweetnacl": "^1.0.3", "tweetnacl-util": "^0.15.1", "typescript": "^4.9.3", "utility-types": "^3.10.0", "winston": "^3.8.2", - "winston-loki": "^6.0.7" + "winston-loki": "^6.0.6", + "zod": "^3.21.4" }, "devDependencies": { "@jest/globals": "^29.3.1", @@ -92,6 +95,7 @@ "npm": "^8.19.3", "smee-client": "^1.2.3", "supertest": "^6.3.3", + "swagger-autogen": "^2.23.5", "ts-jest": "^29.0.3", "ts-node": "^10.9.1" } @@ -3339,6 +3343,26 @@ "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", "dev": true }, + "node_modules/@casl/ability": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@casl/ability/-/ability-6.5.0.tgz", + "integrity": "sha512-3guc94ugr5ylZQIpJTLz0CDfwNi0mxKVECj1vJUPAvs+Lwunh/dcuUjwzc4MHM9D8JOYX0XUZMEPedpB3vIbOw==", + "dependencies": { + "@ucast/mongo2js": "^1.3.0" + }, + "funding": { + "url": "https://github.com/stalniy/casl/blob/master/BACKERS.md" + } + }, + "node_modules/@casl/mongoose": { + "version": "7.2.1", + "resolved": "https://registry.npmjs.org/@casl/mongoose/-/mongoose-7.2.1.tgz", + "integrity": "sha512-pojgSWYKNIwFM6wWDNct1YD0+8nIxhe2jp5jBbK8JGU60dEs2o0Yw3mCo2y7nBwbvRC2oEots/BlLMVb1Wdo8A==", + "peerDependencies": { + "@casl/ability": "^6.3.2", + "mongoose": "^6.0.13 || ^7.0.0" + } + }, "node_modules/@colors/colors": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz", @@ -6174,6 +6198,37 @@ "url": "https://opencollective.com/typescript-eslint" } }, + "node_modules/@ucast/core": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/@ucast/core/-/core-1.10.2.tgz", + "integrity": "sha512-ons5CwXZ/51wrUPfoduC+cO7AS1/wRb0ybpQJ9RrssossDxVy4t49QxWoWgfBDvVKsz9VXzBk9z0wqTdZ+Cq8g==" + }, + "node_modules/@ucast/js": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@ucast/js/-/js-3.0.3.tgz", + "integrity": "sha512-jBBqt57T5WagkAjqfCIIE5UYVdaXYgGkOFYv2+kjq2AVpZ2RIbwCo/TujJpDlwTVluUI+WpnRpoGU2tSGlEvFQ==", + "dependencies": { + "@ucast/core": "^1.0.0" + } + }, + "node_modules/@ucast/mongo": { + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/@ucast/mongo/-/mongo-2.4.3.tgz", + "integrity": "sha512-XcI8LclrHWP83H+7H2anGCEeDq0n+12FU2mXCTz6/Tva9/9ddK/iacvvhCyW6cijAAOILmt0tWplRyRhVyZLsA==", + "dependencies": { + "@ucast/core": "^1.4.1" + } + }, + "node_modules/@ucast/mongo2js": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@ucast/mongo2js/-/mongo2js-1.3.4.tgz", + "integrity": "sha512-ahazOr1HtelA5AC1KZ9x0UwPMqqimvfmtSm/PRRSeKKeE5G2SCqTgwiNzO7i9jS8zA3dzXpKVPpXMkcYLnyItA==", + "dependencies": { + "@ucast/core": "^1.6.1", + "@ucast/js": "^3.0.0", + "@ucast/mongo": "^2.4.0" + } + }, "node_modules/@xmldom/xmldom": { "version": "0.8.10", "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.10.tgz", @@ -9968,6 +10023,7 @@ "version": "2.2.3", "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, "bin": { "json5": "lib/cli.js" }, @@ -15504,6 +15560,7 @@ "version": "2.23.5", "resolved": "https://registry.npmjs.org/swagger-autogen/-/swagger-autogen-2.23.5.tgz", "integrity": "sha512-4Tl2+XhZMyHoBYkABnScHtQE0lKPKUD3NBt09mClrI6UKOUYljKlYw1xiFVwsHCTGR2hAXmhT4PpgjruCtt1ZA==", + "dev": true, "dependencies": { "acorn": "^7.4.1", "deepmerge": "^4.2.2", @@ -15515,6 +15572,7 @@ "version": "7.4.1", "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz", "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==", + "dev": true, "bin": { "acorn": "bin/acorn" }, @@ -16626,6 +16684,14 @@ "funding": { "url": "https://github.com/sponsors/sindresorhus" } + }, + "node_modules/zod": { + "version": "3.21.4", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.21.4.tgz", + "integrity": "sha512-m46AKbrzKVzOzs/DZgVnG5H55N1sv1M8qZU3A8RIKbs3mrACDNeIOeilDymVb2HdmP8uwshOCF4uJ8uM9rCqJw==", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } } }, "dependencies": { @@ -19324,6 +19390,20 @@ "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", "dev": true }, + "@casl/ability": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@casl/ability/-/ability-6.5.0.tgz", + "integrity": "sha512-3guc94ugr5ylZQIpJTLz0CDfwNi0mxKVECj1vJUPAvs+Lwunh/dcuUjwzc4MHM9D8JOYX0XUZMEPedpB3vIbOw==", + "requires": { + "@ucast/mongo2js": "^1.3.0" + } + }, + "@casl/mongoose": { + "version": "7.2.1", + "resolved": "https://registry.npmjs.org/@casl/mongoose/-/mongoose-7.2.1.tgz", + "integrity": "sha512-pojgSWYKNIwFM6wWDNct1YD0+8nIxhe2jp5jBbK8JGU60dEs2o0Yw3mCo2y7nBwbvRC2oEots/BlLMVb1Wdo8A==", + "requires": {} + }, "@colors/colors": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz", @@ -21575,6 +21655,37 @@ "eslint-visitor-keys": "^3.3.0" } }, + "@ucast/core": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/@ucast/core/-/core-1.10.2.tgz", + "integrity": "sha512-ons5CwXZ/51wrUPfoduC+cO7AS1/wRb0ybpQJ9RrssossDxVy4t49QxWoWgfBDvVKsz9VXzBk9z0wqTdZ+Cq8g==" + }, + "@ucast/js": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@ucast/js/-/js-3.0.3.tgz", + "integrity": "sha512-jBBqt57T5WagkAjqfCIIE5UYVdaXYgGkOFYv2+kjq2AVpZ2RIbwCo/TujJpDlwTVluUI+WpnRpoGU2tSGlEvFQ==", + "requires": { + "@ucast/core": "^1.0.0" + } + }, + "@ucast/mongo": { + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/@ucast/mongo/-/mongo-2.4.3.tgz", + "integrity": "sha512-XcI8LclrHWP83H+7H2anGCEeDq0n+12FU2mXCTz6/Tva9/9ddK/iacvvhCyW6cijAAOILmt0tWplRyRhVyZLsA==", + "requires": { + "@ucast/core": "^1.4.1" + } + }, + "@ucast/mongo2js": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@ucast/mongo2js/-/mongo2js-1.3.4.tgz", + "integrity": "sha512-ahazOr1HtelA5AC1KZ9x0UwPMqqimvfmtSm/PRRSeKKeE5G2SCqTgwiNzO7i9jS8zA3dzXpKVPpXMkcYLnyItA==", + "requires": { + "@ucast/core": "^1.6.1", + "@ucast/js": "^3.0.0", + "@ucast/mongo": "^2.4.0" + } + }, "@xmldom/xmldom": { "version": "0.8.10", "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.10.tgz", @@ -24413,7 +24524,8 @@ "json5": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", - "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==" + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true }, "jsonwebtoken": { "version": "9.0.1", @@ -28506,6 +28618,7 @@ "version": "2.23.5", "resolved": "https://registry.npmjs.org/swagger-autogen/-/swagger-autogen-2.23.5.tgz", "integrity": "sha512-4Tl2+XhZMyHoBYkABnScHtQE0lKPKUD3NBt09mClrI6UKOUYljKlYw1xiFVwsHCTGR2hAXmhT4PpgjruCtt1ZA==", + "dev": true, "requires": { "acorn": "^7.4.1", "deepmerge": "^4.2.2", @@ -28516,7 +28629,8 @@ "acorn": { "version": "7.4.1", "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz", - "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==" + "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==", + "dev": true } } }, @@ -29273,6 +29387,11 @@ "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", "dev": true + }, + "zod": { + "version": "3.21.4", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.21.4.tgz", + "integrity": "sha512-m46AKbrzKVzOzs/DZgVnG5H55N1sv1M8qZU3A8RIKbs3mrACDNeIOeilDymVb2HdmP8uwshOCF4uJ8uM9rCqJw==" } } } diff --git a/backend/package.json b/backend/package.json index a70ea9974..e9846de54 100644 --- a/backend/package.json +++ b/backend/package.json @@ -1,6 +1,8 @@ { "dependencies": { "@aws-sdk/client-secrets-manager": "^3.319.0", + "@casl/ability": "^6.5.0", + "@casl/mongoose": "^7.2.1", "@godaddy/terminus": "^4.12.0", "@node-saml/passport-saml": "^4.0.4", "@octokit/rest": "^19.0.5", @@ -8,6 +10,7 @@ "@sentry/tracing": "^7.48.0", "@types/crypto-js": "^4.1.1", "@types/libsodium-wrappers": "^0.7.10", + "@ucast/mongo2js": "^1.3.4", "argon2": "^0.30.3", "aws-sdk": "^2.1364.0", "axios": "^1.3.5", @@ -43,14 +46,14 @@ "query-string": "^7.1.3", "rate-limit-mongo": "^2.3.2", "rimraf": "^3.0.2", - "swagger-autogen": "^2.22.0", "swagger-ui-express": "^4.6.2", "tweetnacl": "^1.0.3", "tweetnacl-util": "^0.15.1", "typescript": "^4.9.3", "utility-types": "^3.10.0", "winston": "^3.8.2", - "winston-loki": "^6.0.7" + "winston-loki": "^6.0.6", + "zod": "^3.21.4" }, "name": "infisical-api", "version": "1.0.0", @@ -110,6 +113,7 @@ "npm": "^8.19.3", "smee-client": "^1.2.3", "supertest": "^6.3.3", + "swagger-autogen": "^2.23.5", "ts-jest": "^29.0.3", "ts-node": "^10.9.1" }, diff --git a/backend/spec.json b/backend/spec.json index 1afbc8dce..ce6638839 100644 --- a/backend/spec.json +++ b/backend/spec.json @@ -68,9 +68,6 @@ } } } - }, - "400": { - "description": "Bad Request" } }, "security": [ @@ -112,9 +109,6 @@ } } } - }, - "400": { - "description": "Bad Request" } }, "security": [ @@ -156,9 +150,16 @@ "responses": { "200": { "description": "OK" - }, - "400": { - "description": "Bad Request" + } + } + } + }, + "/api/v1/users/me/ip": { + "get": { + "description": "", + "responses": { + "200": { + "description": "OK" } } } @@ -215,9 +216,6 @@ } } } - }, - "400": { - "description": "Bad Request" } }, "security": [ @@ -243,9 +241,6 @@ "responses": { "200": { "description": "OK" - }, - "400": { - "description": "Bad Request" } } } @@ -284,9 +279,6 @@ } } } - }, - "400": { - "description": "Bad Request" } }, "security": [ @@ -395,9 +387,6 @@ } } } - }, - "400": { - "description": "Bad Request" } }, "security": [ @@ -407,6 +396,147 @@ ] } }, + "/api/v1/workspace/{workspaceId}/audit-logs": { + "get": { + "description": "", + "parameters": [ + { + "name": "workspaceId", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/api/v1/workspace/{workspaceId}/audit-logs/filters/actors": { + "get": { + "description": "", + "parameters": [ + { + "name": "workspaceId", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/api/v1/workspace/{workspaceId}/trusted-ips": { + "get": { + "description": "", + "parameters": [ + { + "name": "workspaceId", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + }, + "post": { + "description": "", + "parameters": [ + { + "name": "workspaceId", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK" + }, + "400": { + "description": "Bad Request" + } + } + } + }, + "/api/v1/workspace/{workspaceId}/trusted-ips/{trustedIpId}": { + "patch": { + "description": "", + "parameters": [ + { + "name": "workspaceId", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "trustedIpId", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK" + }, + "400": { + "description": "Bad Request" + } + } + }, + "delete": { + "description": "", + "parameters": [ + { + "name": "workspaceId", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "trustedIpId", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK" + }, + "400": { + "description": "Bad Request" + } + } + } + }, "/api/v1/action/{actionId}": { "get": { "description": "", @@ -427,81 +557,495 @@ } } }, - "/api/v1/signup/email/signup": { + "/api/v1/organizations/{organizationId}/plans/table": { + "get": { + "description": "", + "parameters": [ + { + "name": "organizationId", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/api/v1/organizations/{organizationId}/plan": { + "get": { + "description": "", + "parameters": [ + { + "name": "organizationId", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/api/v1/organizations/{organizationId}/session/trial": { + "post": { + "description": "", + "parameters": [ + { + "name": "organizationId", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/api/v1/organizations/{organizationId}/plan/billing": { + "get": { + "description": "", + "parameters": [ + { + "name": "organizationId", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/api/v1/organizations/{organizationId}/plan/table": { + "get": { + "description": "", + "parameters": [ + { + "name": "organizationId", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/api/v1/organizations/{organizationId}/billing-details": { + "get": { + "description": "", + "parameters": [ + { + "name": "organizationId", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + }, + "patch": { + "description": "", + "parameters": [ + { + "name": "organizationId", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/api/v1/organizations/{organizationId}/billing-details/payment-methods": { + "get": { + "description": "", + "parameters": [ + { + "name": "organizationId", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + }, + "post": { + "description": "", + "parameters": [ + { + "name": "organizationId", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/api/v1/organizations/{organizationId}/billing-details/payment-methods/{pmtMethodId}": { + "delete": { + "description": "", + "parameters": [ + { + "name": "organizationId", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "pmtMethodId", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/api/v1/organizations/{organizationId}/billing-details/tax-ids": { + "get": { + "description": "", + "parameters": [ + { + "name": "organizationId", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + }, + "post": { + "description": "", + "parameters": [ + { + "name": "organizationId", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/api/v1/organizations/{organizationId}/billing-details/tax-ids/{taxId}": { + "delete": { + "description": "", + "parameters": [ + { + "name": "organizationId", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "taxId", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/api/v1/organizations/{organizationId}/invoices": { + "get": { + "description": "", + "parameters": [ + { + "name": "organizationId", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/api/v1/organizations/{organizationId}/licenses": { + "get": { + "description": "", + "parameters": [ + { + "name": "organizationId", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/api/v1/sso/redirect/google": { + "get": { + "description": "", + "parameters": [ + { + "name": "callback_port", + "in": "query", + "schema": { + "type": "string" + } + } + ], + "responses": { + "default": { + "description": "" + } + } + } + }, + "/api/v1/sso/google": { + "get": { + "description": "", + "responses": { + "default": { + "description": "" + } + } + } + }, + "/api/v1/sso/redirect/github": { + "get": { + "description": "", + "parameters": [ + { + "name": "callback_port", + "in": "query", + "schema": { + "type": "string" + } + } + ], + "responses": { + "default": { + "description": "" + } + } + } + }, + "/api/v1/sso/github": { + "get": { + "description": "", + "responses": { + "default": { + "description": "" + } + } + } + }, + "/api/v1/sso/redirect/saml2/{ssoIdentifier}": { + "get": { + "description": "", + "parameters": [ + { + "name": "ssoIdentifier", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "callback_port", + "in": "query", + "schema": { + "type": "string" + } + } + ], + "responses": { + "default": { + "description": "" + } + } + } + }, + "/api/v1/sso/saml2/{ssoIdentifier}": { + "post": { + "description": "", + "parameters": [ + { + "name": "ssoIdentifier", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "default": { + "description": "" + } + } + } + }, + "/api/v1/sso/config": { + "get": { + "description": "", + "responses": { + "200": { + "description": "OK" + } + } + }, "post": { "description": "", - "parameters": [], "responses": { "200": { "description": "OK" }, "400": { "description": "Bad Request" + } + } + }, + "patch": { + "description": "", + "responses": { + "200": { + "description": "OK" + }, + "400": { + "description": "Bad Request" + } + } + } + }, + "/api/v1/cloud-products/": { + "get": { + "description": "", + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/api/v1/signup/email/signup": { + "post": { + "description": "", + "responses": { + "200": { + "description": "OK" }, "403": { "description": "Forbidden" } - }, - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "email": { - "example": "any" - } - } - } - } - } } } }, "/api/v1/signup/email/verify": { "post": { "description": "", - "parameters": [], "responses": { "200": { "description": "OK" }, - "400": { - "description": "Bad Request" - }, "403": { "description": "Forbidden" } - }, - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "email": { - "example": "any" - }, - "code": { - "example": "any" - } - } - } - } - } } } }, "/api/v1/auth/token": { "post": { "description": "", - "parameters": [], "responses": { "200": { "description": "OK" - }, - "400": { - "description": "Bad Request" } } } @@ -509,30 +1053,9 @@ "/api/v1/auth/login1": { "post": { "description": "", - "parameters": [], "responses": { "200": { "description": "OK" - }, - "400": { - "description": "Bad Request" - } - }, - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "email": { - "example": "any" - }, - "clientPublicKey": { - "example": "any" - } - } - } - } } } } @@ -556,23 +1079,6 @@ "400": { "description": "Bad Request" } - }, - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "email": { - "example": "any" - }, - "clientProof": { - "example": "any" - } - } - } - } - } } } }, @@ -591,9 +1097,6 @@ "responses": { "200": { "description": "OK" - }, - "400": { - "description": "Bad Request" } } } @@ -601,7 +1104,16 @@ "/api/v1/auth/checkAuth": { "post": { "description": "", - "parameters": [], + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/api/v1/auth/sessions": { + "delete": { + "description": "", "responses": { "200": { "description": "OK" @@ -625,9 +1137,6 @@ "responses": { "200": { "description": "OK" - }, - "400": { - "description": "Bad Request" } } } @@ -652,30 +1161,12 @@ "400": { "description": "Bad Request" } - }, - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "isActive": { - "example": "any" - }, - "botKey": { - "example": "any" - } - } - } - } - } } } }, "/api/v1/user/": { "get": { "description": "", - "parameters": [], "responses": { "200": { "description": "OK" @@ -686,47 +1177,17 @@ "/api/v1/user-action/": { "post": { "description": "", - "parameters": [], "responses": { "200": { "description": "OK" - }, - "400": { - "description": "Bad Request" - } - }, - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "action": { - "example": "any" - } - } - } - } } } }, "get": { "description": "", - "parameters": [ - { - "name": "action", - "in": "query", - "schema": { - "type": "string" - } - } - ], "responses": { "200": { "description": "OK" - }, - "400": { - "description": "Bad Request" } } } @@ -734,39 +1195,17 @@ "/api/v1/organization/": { "get": { "description": "", - "parameters": [], "responses": { "200": { "description": "OK" - }, - "400": { - "description": "Bad Request" } } }, "post": { "description": "", - "parameters": [], "responses": { "200": { "description": "OK" - }, - "400": { - "description": "Bad Request" - } - }, - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "organizationName": { - "example": "any" - } - } - } - } } } } @@ -787,9 +1226,6 @@ "responses": { "200": { "description": "OK" - }, - "400": { - "description": "Bad Request" } } } @@ -810,9 +1246,6 @@ "responses": { "200": { "description": "OK" - }, - "400": { - "description": "Bad Request" } } } @@ -833,9 +1266,6 @@ "responses": { "200": { "description": "OK" - }, - "400": { - "description": "Bad Request" } } } @@ -856,23 +1286,6 @@ "responses": { "200": { "description": "OK" - }, - "400": { - "description": "Bad Request" - } - }, - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "name": { - "example": "any" - } - } - } - } } } } @@ -893,9 +1306,6 @@ "responses": { "200": { "description": "OK" - }, - "400": { - "description": "Bad Request" } } }, @@ -914,23 +1324,6 @@ "responses": { "200": { "description": "OK" - }, - "400": { - "description": "Bad Request" - } - }, - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "email": { - "example": "any" - } - } - } - } } } }, @@ -949,23 +1342,6 @@ "responses": { "200": { "description": "OK" - }, - "400": { - "description": "Bad Request" - } - }, - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "email": { - "example": "any" - } - } - } - } } } } @@ -986,32 +1362,6 @@ "responses": { "200": { "description": "OK" - }, - "400": { - "description": "Bad Request" - } - } - } - }, - "/api/v1/organization/{organizationId}/subscriptions": { - "get": { - "description": "", - "parameters": [ - { - "name": "organizationId", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "description": "OK" - }, - "400": { - "description": "Bad Request" } } } @@ -1052,9 +1402,6 @@ "responses": { "200": { "description": "OK" - }, - "400": { - "description": "Bad Request" } } } @@ -1075,9 +1422,6 @@ "responses": { "200": { "description": "OK" - }, - "400": { - "description": "Bad Request" } } } @@ -1085,19 +1429,14 @@ "/api/v1/workspace/": { "get": { "description": "", - "parameters": [], "responses": { "200": { "description": "OK" - }, - "400": { - "description": "Bad Request" } } }, "post": { "description": "", - "parameters": [], "responses": { "200": { "description": "OK" @@ -1105,23 +1444,6 @@ "400": { "description": "Bad Request" } - }, - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "workspaceName": { - "example": "any" - }, - "organizationId": { - "example": "any" - } - } - } - } - } } } }, @@ -1141,9 +1463,6 @@ "responses": { "200": { "description": "OK" - }, - "400": { - "description": "Bad Request" } } }, @@ -1162,9 +1481,6 @@ "responses": { "200": { "description": "OK" - }, - "400": { - "description": "Bad Request" } } } @@ -1185,23 +1501,6 @@ "responses": { "200": { "description": "OK" - }, - "400": { - "description": "Bad Request" - } - }, - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "name": { - "example": "any" - } - } - } - } } } } @@ -1222,23 +1521,6 @@ "responses": { "200": { "description": "OK" - }, - "400": { - "description": "Bad Request" - } - }, - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "email": { - "example": "any" - } - } - } - } } } } @@ -1259,9 +1541,6 @@ "responses": { "200": { "description": "OK" - }, - "400": { - "description": "Bad Request" } } } @@ -1282,9 +1561,6 @@ "responses": { "200": { "description": "OK" - }, - "400": { - "description": "Bad Request" } } } @@ -1305,9 +1581,6 @@ "responses": { "200": { "description": "OK" - }, - "400": { - "description": "Bad Request" } } } @@ -1346,8 +1619,8 @@ } ], "responses": { - "400": { - "description": "Bad Request" + "default": { + "description": "" } } } @@ -1368,9 +1641,6 @@ "responses": { "200": { "description": "OK" - }, - "400": { - "description": "Bad Request" } } } @@ -1391,9 +1661,6 @@ "responses": { "200": { "description": "OK" - }, - "400": { - "description": "Bad Request" } } } @@ -1414,57 +1681,6 @@ "responses": { "200": { "description": "OK" - }, - "400": { - "description": "Bad Request" - } - }, - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "role": { - "example": "any" - } - } - } - } - } - } - } - }, - "/api/v1/membership/{membershipId}/deny-permissions": { - "post": { - "description": "", - "parameters": [ - { - "name": "membershipId", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "description": "OK" - } - }, - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "permissions": { - "example": "any" - } - } - } - } } } } @@ -1485,23 +1701,6 @@ "responses": { "200": { "description": "OK" - }, - "400": { - "description": "Bad Request" - } - }, - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "key": { - "example": "any" - } - } - } - } } } } @@ -1522,9 +1721,6 @@ "responses": { "200": { "description": "OK" - }, - "400": { - "description": "Bad Request" } } } @@ -1548,53 +1744,15 @@ "400": { "description": "Bad Request" } - }, - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "organizationId": { - "example": "any" - }, - "inviteeEmail": { - "example": "any" - } - } - } - } - } } } }, "/api/v1/invite-org/verify": { "post": { "description": "", - "parameters": [], "responses": { "200": { "description": "OK" - }, - "400": { - "description": "Bad Request" - } - }, - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "email": { - "example": "any" - }, - "code": { - "example": "any" - } - } - } - } } } } @@ -1615,9 +1773,6 @@ "responses": { "200": { "description": "OK" - }, - "400": { - "description": "Bad Request" } }, "requestBody": { @@ -1673,9 +1828,6 @@ "responses": { "200": { "description": "OK" - }, - "400": { - "description": "Bad Request" } } } @@ -1710,9 +1862,6 @@ "responses": { "200": { "description": "OK" - }, - "400": { - "description": "Bad Request" } } } @@ -1720,7 +1869,6 @@ "/api/v1/service-token/": { "get": { "description": "", - "parameters": [], "responses": { "200": { "description": "OK" @@ -1729,7 +1877,6 @@ }, "post": { "description": "", - "parameters": [], "responses": { "200": { "description": "OK" @@ -1775,27 +1922,9 @@ "/api/v1/password/srp1": { "post": { "description": "", - "parameters": [], "responses": { "200": { "description": "OK" - }, - "400": { - "description": "Bad Request" - } - }, - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "clientPublicKey": { - "example": "any" - } - } - } - } } } } @@ -1803,7 +1932,6 @@ "/api/v1/password/change-password": { "post": { "description": "", - "parameters": [], "responses": { "200": { "description": "OK" @@ -1811,74 +1939,15 @@ "400": { "description": "Bad Request" } - }, - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "clientProof": { - "example": "any" - }, - "protectedKey": { - "example": "any" - }, - "protectedKeyIV": { - "example": "any" - }, - "protectedKeyTag": { - "example": "any" - }, - "encryptedPrivateKey": { - "example": "any" - }, - "encryptedPrivateKeyIV": { - "example": "any" - }, - "encryptedPrivateKeyTag": { - "example": "any" - }, - "salt": { - "example": "any" - }, - "verifier": { - "example": "any" - } - } - } - } - } } } }, "/api/v1/password/email/password-reset": { "post": { "description": "", - "parameters": [], "responses": { "200": { "description": "OK" - }, - "400": { - "description": "Bad Request" - }, - "403": { - "description": "Forbidden" - } - }, - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "email": { - "example": "any" - } - } - } - } } } } @@ -1886,53 +1955,27 @@ "/api/v1/password/email/password-reset-verify": { "post": { "description": "", - "parameters": [], "responses": { "200": { "description": "OK" }, - "400": { - "description": "Bad Request" - }, "403": { "description": "Forbidden" } - }, - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "email": { - "example": "any" - }, - "code": { - "example": "any" - } - } - } - } - } } } }, "/api/v1/password/backup-private-key": { "get": { "description": "", - "parameters": [], "responses": { "200": { "description": "OK" - }, - "400": { - "description": "Bad Request" } } }, "post": { "description": "", - "parameters": [], "responses": { "200": { "description": "OK" @@ -1940,105 +1983,15 @@ "400": { "description": "Bad Request" } - }, - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "clientProof": { - "example": "any" - }, - "encryptedPrivateKey": { - "example": "any" - }, - "iv": { - "example": "any" - }, - "tag": { - "example": "any" - }, - "salt": { - "example": "any" - }, - "verifier": { - "example": "any" - } - } - } - } - } } } }, "/api/v1/password/password-reset": { "post": { "description": "", - "parameters": [], "responses": { "200": { "description": "OK" - }, - "400": { - "description": "Bad Request" - } - }, - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "protectedKey": { - "example": "any" - }, - "protectedKeyIV": { - "example": "any" - }, - "protectedKeyTag": { - "example": "any" - }, - "encryptedPrivateKey": { - "example": "any" - }, - "encryptedPrivateKeyIV": { - "example": "any" - }, - "encryptedPrivateKeyTag": { - "example": "any" - }, - "salt": { - "example": "any" - }, - "verifier": { - "example": "any" - } - } - } - } - } - } - } - }, - "/api/v1/stripe/webhook": { - "post": { - "description": "", - "parameters": [ - { - "name": "stripe-signature", - "in": "header", - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "description": "OK" - }, - "400": { - "description": "Bad Request" } } } @@ -2046,60 +1999,9 @@ "/api/v1/integration/": { "post": { "description": "", - "parameters": [], "responses": { "200": { "description": "OK" - }, - "400": { - "description": "Bad Request" - } - }, - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "integrationAuthId": { - "example": "any" - }, - "app": { - "example": "any" - }, - "appId": { - "example": "any" - }, - "isActive": { - "example": "any" - }, - "sourceEnvironment": { - "example": "any" - }, - "targetEnvironment": { - "example": "any" - }, - "targetEnvironmentId": { - "example": "any" - }, - "targetService": { - "example": "any" - }, - "targetServiceId": { - "example": "any" - }, - "owner": { - "example": "any" - }, - "path": { - "example": "any" - }, - "region": { - "example": "any" - } - } - } - } } } } @@ -2120,38 +2022,6 @@ "responses": { "200": { "description": "OK" - }, - "400": { - "description": "Bad Request" - } - }, - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "environment": { - "example": "any" - }, - "isActive": { - "example": "any" - }, - "app": { - "example": "any" - }, - "appId": { - "example": "any" - }, - "targetEnvironment": { - "example": "any" - }, - "owner": { - "example": "any" - } - } - } - } } } }, @@ -2170,9 +2040,16 @@ "responses": { "200": { "description": "OK" - }, - "400": { - "description": "Bad Request" + } + } + } + }, + "/api/v1/integration/manual-sync": { + "post": { + "description": "", + "responses": { + "200": { + "description": "OK" } } } @@ -2180,7 +2057,6 @@ "/api/v1/integration-auth/integration-options": { "get": { "description": "", - "parameters": [], "responses": { "200": { "description": "OK" @@ -2235,33 +2111,9 @@ "/api/v1/integration-auth/oauth-token": { "post": { "description": "", - "parameters": [], "responses": { "200": { "description": "OK" - }, - "400": { - "description": "Bad Request" - } - }, - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "workspaceId": { - "example": "any" - }, - "code": { - "example": "any" - }, - "integration": { - "example": "any" - } - } - } - } } } } @@ -2269,36 +2121,9 @@ "/api/v1/integration-auth/access-token": { "post": { "description": "", - "parameters": [], "responses": { "200": { "description": "OK" - }, - "400": { - "description": "Bad Request" - } - }, - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "workspaceId": { - "example": "any" - }, - "accessId": { - "example": "any" - }, - "accessToken": { - "example": "any" - }, - "integration": { - "example": "any" - } - } - } - } } } } @@ -2314,21 +2139,11 @@ "schema": { "type": "string" } - }, - { - "name": "teamId", - "in": "query", - "schema": { - "type": "string" - } } ], "responses": { "200": { "description": "OK" - }, - "400": { - "description": "Bad Request" } } } @@ -2364,13 +2179,6 @@ "schema": { "type": "string" } - }, - { - "name": "appId", - "in": "query", - "schema": { - "type": "string" - } } ], "responses": { @@ -2391,13 +2199,6 @@ "schema": { "type": "string" } - }, - { - "name": "appId", - "in": "query", - "schema": { - "type": "string" - } } ], "responses": { @@ -2418,13 +2219,991 @@ "schema": { "type": "string" } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/api/v1/integration-auth/{integrationAuthId}/bitbucket/workspaces": { + "get": { + "description": "", + "parameters": [ + { + "name": "integrationAuthId", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/api/v1/integration-auth/{integrationAuthId}/northflank/secret-groups": { + "get": { + "description": "", + "parameters": [ + { + "name": "integrationAuthId", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/api/v1/integration-auth/{integrationAuthId}/teamcity/build-configs": { + "get": { + "description": "", + "parameters": [ + { + "name": "integrationAuthId", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/api/v1/folders/": { + "post": { + "summary": "Create a folder", + "description": "Create a new folder in a specified workspace and environment", + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "folder": { + "type": "object", + "properties": { + "id": { + "type": "string", + "example": "someFolderId" + }, + "name": { + "type": "string", + "example": "my_folder" + } + }, + "description": "Details of the created folder" + } + } + } + } + } + }, + "400": { + "description": "Bad Request. For example, 'Folder name cannot contain spaces. Only underscore and dashes'" + }, + "401": { + "description": "Unauthorized request. For example, 'Folder Permission Denied'" + } + }, + "security": [ + { + "apiKeyAuth": [] + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "workspaceId": { + "type": "string", + "description": "ID of the workspace where the folder will be created", + "example": "someWorkspaceId" + }, + "environment": { + "type": "string", + "description": "Environment where the folder will reside", + "example": "production" + }, + "folderName": { + "type": "string", + "description": "Name of the folder to be created", + "example": "my_folder" + }, + "parentFolderId": { + "type": "string", + "description": "ID of the parent folder under which this folder will be created. If not specified, it will be created at the root level.", + "example": "someParentFolderId" + } + }, + "required": [ + "workspaceId", + "environment", + "folderName" + ] + } + } + } + } + }, + "get": { + "summary": "Retrieve folders based on specific conditions", + "description": "Fetches folders from the specified workspace and environment, optionally providing either a parentFolderId or a parentFolderPath to narrow down results", + "parameters": [ + { + "name": "workspaceId", + "description": "ID of the workspace from which the folders are to be fetched", + "required": true, + "in": "query", + "schema": { + "type": "string" + } }, { - "name": "appId", + "name": "environment", + "description": "Environment where the folder is located", + "required": true, "in": "query", "schema": { "type": "string" } + }, + { + "name": "parentFolderId", + "description": "ID of the parent folder", + "required": false, + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "parentFolderPath", + "description": "Path of the parent folder, like /folder1/folder2", + "required": false, + "in": "query", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "folders": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string", + "example": "someFolderId" + }, + "name": { + "type": "string", + "example": "someFolderName" + } + } + }, + "description": "List of folders" + }, + "dir": { + "type": "array", + "items": { + "type": "object", + "properties": { + "name": { + "type": "string", + "example": "parentFolderName" + }, + "id": { + "type": "string", + "example": "parentFolderId" + } + } + }, + "description": "List of directories" + } + } + } + } + } + }, + "400": { + "description": "Bad Request. For instance, 'The folder doesn't exist'" + }, + "401": { + "description": "Unauthorized request. For example, 'Folder Permission Denied'" + } + }, + "security": [ + { + "apiKeyAuth": [] + } + ] + } + }, + "/api/v1/folders/{folderId}": { + "patch": { + "summary": "Update a folder by ID", + "description": "Update the name of a folder in a specified workspace and environment by its ID", + "parameters": [ + { + "name": "folderId", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "ID of the folder to be updated" + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "message": { + "type": "string", + "example": "Successfully updated folder" + }, + "folder": { + "type": "object", + "properties": { + "name": { + "type": "string", + "example": "updated_folder_name" + }, + "id": { + "type": "string", + "example": "someFolderId" + } + }, + "description": "Details of the updated folder" + } + } + } + } + } + }, + "400": { + "description": "Bad Request. Reasons can include 'The folder doesn't exist' or 'Folder name cannot contain spaces. Only underscore and dashes'" + }, + "401": { + "description": "Unauthorized request. For example, 'Folder Permission Denied'" + } + }, + "security": [ + { + "apiKeyAuth": [] + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "workspaceId": { + "type": "string", + "description": "ID of the workspace where the folder is located", + "example": "someWorkspaceId" + }, + "environment": { + "type": "string", + "description": "Environment where the folder is located", + "example": "production" + }, + "name": { + "type": "string", + "description": "New name for the folder", + "example": "updated_folder_name" + } + }, + "required": [ + "workspaceId", + "environment", + "name" + ] + } + } + } + } + }, + "delete": { + "summary": "Delete a folder by ID", + "description": "Delete the specified folder from a specified workspace and environment using its ID", + "parameters": [ + { + "name": "folderId", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "ID of the folder to be deleted" + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "message": { + "type": "string", + "example": "successfully deleted folders" + }, + "folders": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string", + "example": "someFolderId" + }, + "name": { + "type": "string", + "example": "someFolderName" + } + } + }, + "description": "List of IDs and names of the deleted folders" + } + } + } + } + } + }, + "400": { + "description": "Bad Request. Reasons can include 'The folder doesn't exist'" + }, + "401": { + "description": "Unauthorized request. For example, 'Folder Permission Denied'" + } + }, + "security": [ + { + "apiKeyAuth": [] + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "workspaceId": { + "type": "string", + "description": "ID of the workspace where the folder is located", + "example": "someWorkspaceId" + }, + "environment": { + "type": "string", + "description": "Environment where the folder is located", + "example": "production" + } + }, + "required": [ + "workspaceId", + "environment" + ] + } + } + } + } + } + }, + "/api/v1/secret-scanning/create-installation-session/organization/{organizationId}": { + "post": { + "description": "", + "parameters": [ + { + "name": "organizationId", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/api/v1/secret-scanning/link-installation": { + "post": { + "description": "", + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/api/v1/secret-scanning/installation-status/organization/{organizationId}": { + "get": { + "description": "", + "parameters": [ + { + "name": "organizationId", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/api/v1/secret-scanning/organization/{organizationId}/risks": { + "get": { + "description": "", + "parameters": [ + { + "name": "organizationId", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/api/v1/secret-scanning/organization/{organizationId}/risks/{riskId}/status": { + "post": { + "description": "", + "parameters": [ + { + "name": "organizationId", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "riskId", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/api/v1/webhooks/": { + "post": { + "description": "", + "responses": { + "200": { + "description": "OK" + } + } + }, + "get": { + "description": "", + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/api/v1/webhooks/{webhookId}": { + "patch": { + "description": "", + "parameters": [ + { + "name": "webhookId", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + }, + "delete": { + "description": "", + "parameters": [ + { + "name": "webhookId", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/api/v1/webhooks/{webhookId}/test": { + "post": { + "description": "", + "parameters": [ + { + "name": "webhookId", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK" + }, + "400": { + "description": "Bad Request" + } + } + } + }, + "/api/v1/secret-imports/": { + "post": { + "summary": "Create secret import", + "description": "Create a new secret import for a specified workspace and environment", + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "message": { + "type": "string", + "example": "successfully created secret import" + } + }, + "description": "Confirmation of secret import creation" + } + } + } + }, + "400": { + "description": "Bad Request. For example, 'Secret import already exist'" + }, + "401": { + "description": "Unauthorized request. For example, 'Folder Permission Denied'" + }, + "404": { + "description": "Resource Not Found. For example, 'Failed to find folder'" + } + }, + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "workspaceId": { + "type": "string", + "description": "ID of the workspace where the secret import will be created", + "example": "someWorkspaceId" + }, + "environment": { + "type": "string", + "description": "Environment to import to", + "example": "production" + }, + "folderId": { + "type": "string", + "description": "Folder ID. Use root for the root folder.", + "example": "my_folder" + }, + "secretImport": { + "type": "object", + "properties": { + "environment": { + "type": "string", + "description": "Import from environment", + "example": "development" + }, + "secretPath": { + "type": "string", + "description": "Import from secret path", + "example": "/user/oauth" + } + } + } + }, + "required": [ + "workspaceId", + "environment", + "folderName" + ] + } + } + } + } + }, + "get": { + "summary": "Retrieve secret imports", + "description": "Fetches the secret imports based on the workspaceId, environment, and folderId", + "parameters": [ + { + "name": "workspaceId", + "in": "query", + "description": "ID of the workspace of secret imports to get", + "required": true, + "example": "workspace12345", + "schema": { + "type": "string" + } + }, + { + "name": "environment", + "in": "query", + "description": "Environment of secret imports to get", + "required": true, + "example": "production", + "schema": { + "type": "string" + } + }, + { + "name": "folderId", + "in": "query", + "description": "ID of the folder containing the secret imports. Default: root", + "required": false, + "example": "folder12345", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Successfully retrieved secret import", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "secretImport": { + "type": "object", + "description": "Details of a secret import" + } + } + } + } + } + }, + "401": { + "description": "Unauthorized access due to invalid token or scope" + }, + "403": { + "description": "Forbidden access due to insufficient permissions" + } + } + } + }, + "/api/v1/secret-imports/{id}": { + "put": { + "summary": "Update a secret import", + "description": "Updates an existing secret import based on the provided ID and new import details", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "ID of the secret import to be updated", + "example": "import12345" + } + ], + "responses": { + "200": { + "description": "Successfully updated the secret import", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "message": { + "type": "string", + "example": "successfully updated secret import" + } + } + } + } + } + }, + "400": { + "description": "Bad Request - Import not found" + }, + "401": { + "description": "Unauthorized access due to invalid token or scope" + }, + "403": { + "description": "Forbidden access due to insufficient permissions" + } + }, + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "secretImports": { + "type": "array", + "description": "List of new secret imports", + "items": { + "type": "object", + "properties": { + "environment": { + "type": "string", + "description": "Environment of the secret import", + "example": "production" + }, + "secretPath": { + "type": "string", + "description": "Path of the secret import", + "example": "/path/to/secret" + } + }, + "required": [ + "environment", + "secretPath" + ] + } + } + }, + "required": [ + "secretImports" + ] + } + } + } + } + }, + "delete": { + "summary": "Delete secret import", + "description": "Delete secret import", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "ID of the secret import", + "example": "12345abcde" + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "message": { + "type": "string", + "example": "successfully delete secret import" + } + }, + "description": "Confirmation of secret import deletion" + } + } + } + } + }, + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "secretImportEnv": { + "type": "string", + "description": "Import from environment", + "example": "someWorkspaceId" + }, + "secretImportPath": { + "type": "string", + "description": "Import from secret path", + "example": "production" + } + }, + "required": [ + "id", + "secretImportEnv", + "secretImportPath" + ] + } + } + } + } + } + }, + "/api/v1/secret-imports/secrets": { + "get": { + "description": "", + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/api/v1/roles/": { + "post": { + "description": "", + "responses": { + "200": { + "description": "OK" + } + } + }, + "get": { + "description": "", + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/api/v1/roles/{id}": { + "patch": { + "description": "", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + }, + "delete": { + "description": "", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/api/v1/roles/organization/{orgId}/permissions": { + "get": { + "description": "", + "parameters": [ + { + "name": "orgId", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/api/v1/roles/workspace/{workspaceId}/permissions": { + "get": { + "description": "", + "parameters": [ + { + "name": "workspaceId", + "in": "path", + "required": true, + "schema": { + "type": "string" + } } ], "responses": { @@ -2437,14 +3216,19 @@ "/api/v2/signup/complete-account/signup": { "post": { "description": "", - "parameters": [], + "parameters": [ + { + "name": "user-agent", + "in": "header", + "schema": { + "type": "string" + } + } + ], "responses": { "200": { "description": "OK" }, - "400": { - "description": "Bad Request" - }, "403": { "description": "Forbidden" } @@ -2504,14 +3288,19 @@ "/api/v2/signup/complete-account/invite": { "post": { "description": "", - "parameters": [], + "parameters": [ + { + "name": "user-agent", + "in": "header", + "schema": { + "type": "string" + } + } + ], "responses": { "200": { "description": "OK" }, - "400": { - "description": "Bad Request" - }, "403": { "description": "Forbidden" } @@ -2568,13 +3357,9 @@ "/api/v2/auth/login1": { "post": { "description": "", - "parameters": [], "responses": { "200": { "description": "OK" - }, - "400": { - "description": "Bad Request" } }, "requestBody": { @@ -2638,27 +3423,9 @@ "/api/v2/auth/mfa/send": { "post": { "description": "", - "parameters": [], "responses": { "200": { "description": "OK" - }, - "400": { - "description": "Bad Request" - } - }, - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "email": { - "example": "any" - } - } - } - } } } } @@ -2679,23 +3446,6 @@ "200": { "description": "OK" } - }, - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "email": { - "example": "any" - }, - "mfaToken": { - "example": "any" - } - } - } - } - } } } }, @@ -2703,7 +3453,6 @@ "get": { "summary": "Retrieve the current user on the request", "description": "Retrieve the current user on the request", - "parameters": [], "responses": { "200": { "description": "OK", @@ -2721,9 +3470,6 @@ } } } - }, - "400": { - "description": "Bad Request" } }, "security": [ @@ -2736,7 +3482,26 @@ "/api/v2/users/me/mfa": { "patch": { "description": "", - "parameters": [], + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/api/v2/users/me/name": { + "patch": { + "description": "", + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/api/v2/users/me/auth-methods": { + "put": { + "description": "", "responses": { "200": { "description": "OK" @@ -2744,20 +3509,6 @@ "400": { "description": "Bad Request" } - }, - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "isMfaEnabled": { - "example": "any" - } - } - } - } - } } } }, @@ -2765,7 +3516,6 @@ "get": { "summary": "Return organizations that current user is part of", "description": "Return organizations that current user is part of", - "parameters": [], "responses": { "200": { "description": "OK", @@ -2785,9 +3535,6 @@ } } } - }, - "400": { - "description": "Bad Request" } }, "security": [ @@ -2797,6 +3544,62 @@ ] } }, + "/api/v2/users/me/api-keys": { + "get": { + "description": "", + "responses": { + "200": { + "description": "OK" + } + } + }, + "post": { + "description": "", + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/api/v2/users/me/api-keys/{apiKeyDataId}": { + "delete": { + "description": "", + "parameters": [ + { + "name": "apiKeyDataId", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/api/v2/users/me/sessions": { + "get": { + "description": "", + "responses": { + "200": { + "description": "OK" + } + } + }, + "delete": { + "description": "", + "responses": { + "200": { + "description": "OK" + } + } + } + }, "/api/v2/organizations/{organizationId}/memberships": { "get": { "summary": "Return organization memberships", @@ -2831,9 +3634,6 @@ } } } - }, - "400": { - "description": "Bad Request" } }, "security": [ @@ -2883,9 +3683,6 @@ } } } - }, - "400": { - "description": "Bad Request" } }, "security": [ @@ -2949,9 +3746,6 @@ } } } - }, - "400": { - "description": "Bad Request" } }, "security": [ @@ -3026,7 +3820,8 @@ }, "/api/v2/workspace/{workspaceId}/environments": { "post": { - "description": "", + "summary": "Create environment", + "description": "Create environment", "parameters": [ { "name": "workspaceId", @@ -3034,17 +3829,54 @@ "required": true, "schema": { "type": "string" - } + }, + "description": "ID of project" } ], "responses": { "200": { - "description": "OK" + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "message": { + "type": "string", + "example": "Successfully created new environment" + }, + "workspace": { + "type": "string", + "example": "someWorkspaceId" + }, + "environment": { + "type": "object", + "properties": { + "name": { + "type": "string", + "example": "someEnvironmentName" + }, + "slug": { + "type": "string", + "example": "someEnvironmentSlug" + } + } + } + }, + "description": "Response after creating a new environment" + } + } + } }, "400": { "description": "Bad Request" } }, + "security": [ + { + "apiKeyAuth": [] + } + ], "requestBody": { "content": { "application/json": { @@ -3052,19 +3884,28 @@ "type": "object", "properties": { "environmentName": { - "example": "any" + "type": "string", + "description": "Name of the environment", + "example": "development" }, "environmentSlug": { - "example": "any" + "type": "string", + "description": "Slug of the environment", + "example": "dev-environment" } - } + }, + "required": [ + "environmentName", + "environmentSlug" + ] } } } } }, "put": { - "description": "", + "summary": "Rename workspace environment", + "description": "Rename a specific environment within a workspace", "parameters": [ { "name": "workspaceId", @@ -3072,15 +3913,44 @@ "required": true, "schema": { "type": "string" - } + }, + "description": "ID of the workspace" } ], "responses": { "200": { - "description": "OK" - }, - "400": { - "description": "Bad Request" + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "message": { + "type": "string", + "example": "Successfully update environment" + }, + "workspace": { + "type": "string", + "example": "someWorkspaceId" + }, + "environment": { + "type": "object", + "properties": { + "name": { + "type": "string", + "example": "Staging-Renamed" + }, + "slug": { + "type": "string", + "example": "staging-renamed" + } + } + } + }, + "description": "Details of the renamed environment" + } + } + } } }, "requestBody": { @@ -3090,21 +3960,32 @@ "type": "object", "properties": { "environmentName": { - "example": "any" + "type": "string", + "description": "New name for the environment", + "example": "Staging-Renamed" }, "environmentSlug": { - "example": "any" + "type": "string", + "description": "New slug for the environment", + "example": "staging-renamed" }, "oldEnvironmentSlug": { - "example": "any" + "type": "string", + "description": "Current slug of the environment to rename", + "example": "staging-old" } - } + }, + "required": [ + "environmentName", + "environmentSlug", + "oldEnvironmentSlug" + ] } } } } }, - "delete": { + "patch": { "description": "", "parameters": [ { @@ -3119,11 +4000,55 @@ "responses": { "200": { "description": "OK" - }, - "400": { - "description": "Bad Request" + } + } + }, + "delete": { + "summary": "Delete workspace environment", + "description": "Delete a specific environment from a workspace", + "parameters": [ + { + "name": "workspaceId", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "ID of the workspace" + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "message": { + "type": "string", + "example": "Successfully deleted environment" + }, + "workspace": { + "type": "string", + "example": "someWorkspaceId" + }, + "environment": { + "type": "string", + "example": "dev-environment" + } + }, + "description": "Response after deleting an environment from a workspace" + } + } + } } }, + "security": [ + { + "apiKeyAuth": [] + } + ], "requestBody": { "content": { "application/json": { @@ -3131,16 +4056,22 @@ "type": "object", "properties": { "environmentSlug": { - "example": "any" + "type": "string", + "description": "Slug of the environment to delete", + "example": "dev-environment" } - } + }, + "required": [ + "environmentSlug" + ] } } } } }, "get": { - "description": "", + "summary": "Get all accessible environments of a workspace", + "description": "Fetch all environments that the user has access to in a specified workspace", "parameters": [ { "name": "workspaceId", @@ -3148,14 +4079,54 @@ "required": true, "schema": { "type": "string" - } + }, + "description": "ID of the workspace" } ], "responses": { "200": { - "description": "OK" + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "accessibleEnvironments": { + "type": "array", + "items": { + "type": "object", + "properties": { + "name": { + "type": "string", + "example": "Development" + }, + "slug": { + "type": "string", + "example": "development" + }, + "isWriteDenied": { + "type": "boolean", + "example": false + }, + "isReadDenied": { + "type": "boolean", + "example": false + } + } + } + } + }, + "description": "List of environments the user has access to in the specified workspace" + } + } + } } - } + }, + "security": [ + { + "apiKeyAuth": [] + } + ] } }, "/api/v2/workspace/{workspaceId}/tags": { @@ -3193,26 +4164,6 @@ "200": { "description": "OK" } - }, - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "name": { - "example": "any" - }, - "tagColor": { - "example": "any" - }, - "slug": { - "example": "any" - } - } - } - } - } } } }, @@ -3252,9 +4203,6 @@ "responses": { "200": { "description": "OK" - }, - "400": { - "description": "Bad Request" } }, "requestBody": { @@ -3310,9 +4258,6 @@ "responses": { "200": { "description": "OK" - }, - "400": { - "description": "Bad Request" } } } @@ -3346,9 +4291,6 @@ } } } - }, - "400": { - "description": "Bad Request" } }, "security": [ @@ -3374,9 +4316,6 @@ "responses": { "200": { "description": "OK" - }, - "400": { - "description": "Bad Request" } } } @@ -3415,9 +4354,6 @@ } } } - }, - "400": { - "description": "Bad Request" } }, "security": [ @@ -3467,9 +4403,6 @@ } } } - }, - "400": { - "description": "Bad Request" } }, "security": [ @@ -3533,9 +4466,6 @@ } } } - }, - "400": { - "description": "Bad Request" } }, "security": [ @@ -3561,23 +4491,6 @@ "responses": { "200": { "description": "OK" - }, - "400": { - "description": "Bad Request" - } - }, - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "autoCapitalization": { - "example": "any" - } - } - } - } } } } @@ -3873,26 +4786,6 @@ "200": { "description": "OK" } - }, - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "workspaceId": { - "example": "any" - }, - "environment": { - "example": "any" - }, - "requests": { - "example": "any" - } - } - } - } - } } } }, @@ -3988,13 +4881,6 @@ "schema": { "type": "string" } - }, - { - "name": "content", - "in": "query", - "schema": { - "type": "string" - } } ], "responses": { @@ -4027,7 +4913,6 @@ "patch": { "summary": "Update secret(s)", "description": "Update secret(s)", - "parameters": [], "responses": { "200": { "description": "OK", @@ -4131,7 +5016,6 @@ "get": { "summary": "Return Infisical Token data", "description": "Return Infisical Token data", - "parameters": [], "responses": { "200": { "description": "OK", @@ -4159,46 +5043,10 @@ }, "post": { "description": "", - "parameters": [], "responses": { "200": { "description": "OK" } - }, - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "name": { - "example": "any" - }, - "workspaceId": { - "example": "any" - }, - "environment": { - "example": "any" - }, - "encryptedKey": { - "example": "any" - }, - "iv": { - "example": "any" - }, - "tag": { - "example": "any" - }, - "expiresIn": { - "example": "any" - }, - "permissions": { - "example": "any" - } - } - } - } - } } } }, @@ -4222,10 +5070,9 @@ } } }, - "/api/v2/service-accounts/me": { - "get": { + "/api/v3/auth/login1": { + "post": { "description": "", - "parameters": [], "responses": { "200": { "description": "OK" @@ -4233,12 +5080,44 @@ } } }, - "/api/v2/service-accounts/{serviceAccountId}": { + "/api/v3/auth/login2": { + "post": { + "description": "", + "parameters": [ + { + "name": "user-agent", + "in": "header", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK" + }, + "400": { + "description": "Bad Request" + } + } + } + }, + "/api/v3/secrets/raw": { + "get": { + "description": "", + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/api/v3/secrets/raw/{secretName}": { "get": { "description": "", "parameters": [ { - "name": "serviceAccountId", + "name": "secretName", "in": "path", "required": true, "schema": { @@ -4252,11 +5131,11 @@ } } }, - "delete": { + "post": { "description": "", "parameters": [ { - "name": "serviceAccountId", + "name": "secretName", "in": "path", "required": true, "schema": { @@ -4269,59 +5148,12 @@ "description": "OK" } } - } - }, - "/api/v2/service-accounts/": { - "post": { - "description": "", - "parameters": [], - "responses": { - "200": { - "description": "OK" - } - } - } - }, - "/api/v2/service-accounts/{serviceAccountId}/name": { + }, "patch": { "description": "", "parameters": [ { - "name": "serviceAccountId", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "description": "OK" - } - }, - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "name": { - "example": "any" - } - } - } - } - } - } - } - }, - "/api/v2/service-accounts/{serviceAccountId}/permissions/workspace": { - "get": { - "description": "", - "parameters": [ - { - "name": "serviceAccountId", + "name": "secretName", "in": "path", "required": true, "schema": { @@ -4335,71 +5167,11 @@ } } }, - "post": { - "description": "", - "parameters": [ - { - "name": "serviceAccountId", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "description": "OK" - }, - "400": { - "description": "Bad Request" - } - }, - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "environment": { - "example": "any" - }, - "workspaceId": { - "example": "any" - }, - "read": { - "example": "any" - }, - "write": { - "example": "any" - }, - "encryptedKey": { - "example": "any" - }, - "nonce": { - "example": "any" - } - } - } - } - } - } - } - }, - "/api/v2/service-accounts/{serviceAccountId}/permissions/workspace/{serviceAccountWorkspacePermissionId}": { "delete": { "description": "", "parameters": [ { - "name": "serviceAccountId", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - }, - { - "name": "serviceAccountWorkspacePermissionId", + "name": "secretName", "in": "path", "required": true, "schema": { @@ -4414,118 +5186,9 @@ } } }, - "/api/v2/service-accounts/{serviceAccountId}/keys": { - "get": { - "description": "", - "parameters": [ - { - "name": "serviceAccountId", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - }, - { - "name": "workspaceId", - "in": "query", - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "description": "OK" - } - } - } - }, - "/api/v2/api-key/": { - "get": { - "description": "", - "parameters": [], - "responses": { - "200": { - "description": "OK" - }, - "400": { - "description": "Bad Request" - } - } - }, - "post": { - "description": "", - "parameters": [], - "responses": { - "200": { - "description": "OK" - }, - "400": { - "description": "Bad Request" - } - }, - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "name": { - "example": "any" - }, - "expiresIn": { - "example": "any" - } - } - } - } - } - } - } - }, - "/api/v2/api-key/{apiKeyDataId}": { - "delete": { - "description": "", - "parameters": [ - { - "name": "apiKeyDataId", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "description": "OK" - }, - "400": { - "description": "Bad Request" - } - } - } - }, "/api/v3/secrets/": { "get": { "description": "", - "parameters": [ - { - "name": "workspaceId", - "in": "query", - "schema": { - "type": "string" - } - }, - { - "name": "environment", - "in": "query", - "schema": { - "type": "string" - } - } - ], "responses": { "200": { "description": "OK" @@ -4550,53 +5213,6 @@ "200": { "description": "OK" } - }, - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "workspaceId": { - "example": "any" - }, - "environment": { - "example": "any" - }, - "type": { - "example": "any" - }, - "secretKeyCiphertext": { - "example": "any" - }, - "secretKeyIV": { - "example": "any" - }, - "secretKeyTag": { - "example": "any" - }, - "secretValueCiphertext": { - "example": "any" - }, - "secretValueIV": { - "example": "any" - }, - "secretValueTag": { - "example": "any" - }, - "secretCommentCiphertext": { - "example": "any" - }, - "secretCommentIV": { - "example": "any" - }, - "secretCommentTag": { - "example": "any" - } - } - } - } - } } }, "get": { @@ -4609,27 +5225,6 @@ "schema": { "type": "string" } - }, - { - "name": "workspaceId", - "in": "query", - "schema": { - "type": "string" - } - }, - { - "name": "environment", - "in": "query", - "schema": { - "type": "string" - } - }, - { - "name": "type", - "in": "query", - "schema": { - "type": "string" - } } ], "responses": { @@ -4654,35 +5249,6 @@ "200": { "description": "OK" } - }, - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "workspaceId": { - "example": "any" - }, - "environment": { - "example": "any" - }, - "type": { - "example": "any" - }, - "secretValueCiphertext": { - "example": "any" - }, - "secretValueIV": { - "example": "any" - }, - "secretValueTag": { - "example": "any" - } - } - } - } - } } }, "delete": { @@ -4701,26 +5267,6 @@ "200": { "description": "OK" } - }, - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "workspaceId": { - "example": "any" - }, - "environment": { - "example": "any" - }, - "type": { - "example": "any" - } - } - } - } - } } } }, @@ -4781,19 +5327,37 @@ "200": { "description": "OK" } - }, - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "secretsToUpdate": { - "example": "any" - } - } - } + } + } + }, + "/api/v3/signup/complete-account/signup": { + "post": { + "description": "", + "parameters": [ + { + "name": "authorization", + "in": "header", + "schema": { + "type": "string" } + }, + { + "name": "user-agent", + "in": "header", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK" + }, + "400": { + "description": "Bad Request" + }, + "403": { + "description": "Forbidden" } } } @@ -4801,7 +5365,6 @@ "/api/status": { "get": { "description": "", - "parameters": [], "responses": { "200": { "description": "OK" diff --git a/backend/src/controllers/v1/authController.ts b/backend/src/controllers/v1/authController.ts index 14c717e38..6f6411541 100644 --- a/backend/src/controllers/v1/authController.ts +++ b/backend/src/controllers/v1/authController.ts @@ -17,6 +17,8 @@ import { getJwtRefreshSecret } from "../../config"; import { ActorType } from "../../ee/models"; +import { validateRequest } from "../../helpers/validation"; +import * as reqValidator from "../../validation/auth"; declare module "jsonwebtoken" { export interface UserIDJwtPayload extends jwt.JwtPayload { @@ -32,7 +34,9 @@ declare module "jsonwebtoken" { * @returns */ export const login1 = async (req: Request, res: Response) => { - const { email, clientPublicKey }: { email: string; clientPublicKey: string } = req.body; + const { + body: { email, clientPublicKey } + } = await validateRequest(reqValidator.Login1V1, req); const user = await User.findOne({ email @@ -76,7 +80,10 @@ export const login1 = async (req: Request, res: Response) => { * @returns */ export const login2 = async (req: Request, res: Response) => { - const { email, clientProof } = req.body; + const { + body: { email, clientProof } + } = await validateRequest(reqValidator.Login2V1, req); + const user = await User.findOne({ email }).select("+salt +verifier +publicKey +encryptedPrivateKey +iv +tag"); diff --git a/backend/src/controllers/v1/botController.ts b/backend/src/controllers/v1/botController.ts index b2e757541..65251e83a 100644 --- a/backend/src/controllers/v1/botController.ts +++ b/backend/src/controllers/v1/botController.ts @@ -2,10 +2,19 @@ import { Request, Response } from "express"; import { Types } from "mongoose"; import { Bot, BotKey } from "../../models"; import { createBot } from "../../helpers/bot"; +import { validateRequest } from "../../helpers/validation"; +import * as reqValidator from "../../validation/bot"; +import { + ProjectPermissionActions, + ProjectPermissionSub, + getUserProjectPermissions +} from "../../ee/services/ProjectRoleService"; +import { ForbiddenError } from "@casl/ability"; +import { BadRequestError } from "../../utils/errors"; interface BotKey { - encryptedKey: string; - nonce: string; + encryptedKey: string; + nonce: string; } /** @@ -16,23 +25,30 @@ interface BotKey { * @returns */ export const getBotByWorkspaceId = async (req: Request, res: Response) => { - const { workspaceId } = req.params; + const { + params: { workspaceId } + } = await validateRequest(reqValidator.GetBotByWorkspaceIdV1, req); + const { permission } = await getUserProjectPermissions(req.user._id, workspaceId); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Read, + ProjectPermissionSub.Integrations + ); let bot = await Bot.findOne({ - workspace: workspaceId, + workspace: workspaceId }); - + if (!bot) { - // case: bot doesn't exist for workspace with id [workspaceId] - // -> create a new bot and return it - bot = await createBot({ - name: "Infisical Bot", - workspaceId: new Types.ObjectId(workspaceId), - }); + // case: bot doesn't exist for workspace with id [workspaceId] + // -> create a new bot and return it + bot = await createBot({ + name: "Infisical Bot", + workspaceId: new Types.ObjectId(workspaceId) + }); } - + return res.status(200).send({ - bot, + bot }); }; @@ -43,46 +59,69 @@ export const getBotByWorkspaceId = async (req: Request, res: Response) => { * @returns */ export const setBotActiveState = async (req: Request, res: Response) => { - const { isActive, botKey }: { isActive: boolean, botKey: BotKey } = req.body; - - if (isActive) { - // bot state set to active -> share workspace key with bot - if (!botKey?.encryptedKey || !botKey?.nonce) { - return res.status(400).send({ - message: "Failed to set bot state to active - missing bot key", - }); - } - - await BotKey.findOneAndUpdate({ - workspace: req.bot.workspace, - }, { - encryptedKey: botKey.encryptedKey, - nonce: botKey.nonce, - sender: req.user._id, - bot: req.bot._id, - workspace: req.bot.workspace, - }, { - upsert: true, - new: true, - }); - } else { - // case: bot state set to inactive -> delete bot's workspace key - await BotKey.deleteOne({ - bot: req.bot._id, - }); + const { + body: { botKey, isActive }, + params: { botId } + } = await validateRequest(reqValidator.SetBotActiveStateV1, req); + + const bot = await Bot.findById(botId); + if (!bot) { + throw BadRequestError({ message: "Bot not found" }); + } + const userId = req.user._id; + + const { permission } = await getUserProjectPermissions(userId, bot.workspace.toString()); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Edit, + ProjectPermissionSub.Integrations + ); + + if (isActive) { + // bot state set to active -> share workspace key with bot + if (!botKey?.encryptedKey || !botKey?.nonce) { + return res.status(400).send({ + message: "Failed to set bot state to active - missing bot key" + }); } - const bot = await Bot.findOneAndUpdate({ - _id: req.bot._id, - }, { - isActive, - }, { - new: true, - }); - - if (!bot) throw new Error("Failed to update bot active state"); - - return res.status(200).send({ - bot, + await BotKey.findOneAndUpdate( + { + workspace: bot.workspace + }, + { + encryptedKey: botKey.encryptedKey, + nonce: botKey.nonce, + sender: userId, + bot: bot._id, + workspace: bot.workspace + }, + { + upsert: true, + new: true + } + ); + } else { + // case: bot state set to inactive -> delete bot's workspace key + await BotKey.deleteOne({ + bot: bot._id }); + } + + const updatedBot = await Bot.findOneAndUpdate( + { + _id: bot._id + }, + { + isActive + }, + { + new: true + } + ); + + if (!updatedBot) throw new Error("Failed to update bot active state"); + + return res.status(200).send({ + bot + }); }; diff --git a/backend/src/controllers/v1/index.ts b/backend/src/controllers/v1/index.ts index 422aaa5ec..a9bde9971 100644 --- a/backend/src/controllers/v1/index.ts +++ b/backend/src/controllers/v1/index.ts @@ -15,7 +15,7 @@ import * as userController from "./userController"; import * as workspaceController from "./workspaceController"; import * as secretScanningController from "./secretScanningController"; import * as webhookController from "./webhookController"; -import * as secretImportController from "./secretImportController"; +import * as secretImpsController from "./secretImpsController"; export { authController, @@ -35,5 +35,5 @@ export { workspaceController, secretScanningController, webhookController, - secretImportController + secretImpsController }; diff --git a/backend/src/controllers/v1/integrationAuthController.ts b/backend/src/controllers/v1/integrationAuthController.ts index a32bedf72..a7390abaa 100644 --- a/backend/src/controllers/v1/integrationAuthController.ts +++ b/backend/src/controllers/v1/integrationAuthController.ts @@ -2,7 +2,7 @@ import { Request, Response } from "express"; import { Types } from "mongoose"; import { standardRequest } from "../../config/request"; import { getApps, getTeams, revokeAccess } from "../../integrations"; -import { Bot, IntegrationAuth } from "../../models"; +import { Bot, IntegrationAuth, Workspace } from "../../models"; import { EventType } from "../../ee/models"; import { IntegrationService } from "../../services"; import { EEAuditLogService } from "../../ee/services"; @@ -18,12 +18,25 @@ import { getIntegrationOptions as getIntegrationOptionsFunc } from "../../variables"; import { exchangeRefresh } from "../../integrations"; +import { validateRequest } from "../../helpers/validation"; +import * as reqValidator from "../../validation/integrationAuth"; +import { + ProjectPermissionActions, + ProjectPermissionSub, + getUserProjectPermissions +} from "../../ee/services/ProjectRoleService"; +import { ForbiddenError } from "@casl/ability"; +import { getIntegrationAuthAccessHelper } from "../../helpers"; +import { ObjectId } from "mongodb"; /*** * Return integration authorization with id [integrationAuthId] */ export const getIntegrationAuth = async (req: Request, res: Response) => { - const { integrationAuthId } = req.params; + const { + params: { integrationAuthId } + } = await validateRequest(reqValidator.GetIntegrationAuthV1, req); + const integrationAuth = await IntegrationAuth.findById(integrationAuthId); if (!integrationAuth) @@ -31,6 +44,15 @@ export const getIntegrationAuth = async (req: Request, res: Response) => { message: "Failed to find integration authorization" }); + const { permission } = await getUserProjectPermissions( + req.user._id, + integrationAuth.workspace.toString() + ); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Read, + ProjectPermissionSub.Integrations + ); + return res.status(200).send({ integrationAuth }); @@ -51,15 +73,19 @@ export const getIntegrationOptions = async (req: Request, res: Response) => { * @returns */ export const oAuthExchange = async (req: Request, res: Response) => { - const { - workspaceId, - code, - integration, - url - } = req.body; + const { + body: { integration, workspaceId, code, url } + } = await validateRequest(reqValidator.OauthExchangeV1, req); if (!INTEGRATION_SET.has(integration)) throw new Error("Failed to validate integration"); - const environments = req.membership.workspace?.environments || []; + const { permission } = await getUserProjectPermissions(req.user._id, workspaceId); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Create, + ProjectPermissionSub.Integrations + ); + + const workspace = await Workspace.findById(workspaceId); + const environments = workspace?.environments || []; if (environments.length === 0) { throw new Error("Failed to get environments"); } @@ -99,25 +125,16 @@ export const oAuthExchange = async (req: Request, res: Response) => { export const saveIntegrationToken = async (req: Request, res: Response) => { // TODO: refactor // TODO: check if access token is valid for each integration - let integrationAuth; const { - workspaceId, - accessId, - refreshToken, - accessToken, - url, - namespace, - integration - }: { - workspaceId: string; - accessId: string | undefined; - refreshToken: string | undefined; - accessToken: string | undefined; - url: string; - namespace: string; - integration: string; - } = req.body; + body: { workspaceId, integration, url, accessId, namespace, accessToken, refreshToken } + } = await validateRequest(reqValidator.SaveIntegrationAccessTokenV1, req); + + const { permission } = await getUserProjectPermissions(req.user._id, workspaceId); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Create, + ProjectPermissionSub.Integrations + ); const bot = await Bot.findOne({ workspace: new Types.ObjectId(workspaceId), @@ -138,18 +155,20 @@ export const saveIntegrationToken = async (req: Request, res: Response) => { namespace, algorithm: ALGORITHM_AES_256_GCM, keyEncoding: ENCODING_SCHEME_UTF8, - ...(integration === INTEGRATION_GCP_SECRET_MANAGER ? { - metadata: { - authMethod: "serviceAccount" - } - } : {}) + ...(integration === INTEGRATION_GCP_SECRET_MANAGER + ? { + metadata: { + authMethod: "serviceAccount" + } + } + : {}) }, { new: true, upsert: true } ); - + // encrypt and save integration access details if (refreshToken) { await exchangeRefresh({ @@ -169,7 +188,7 @@ export const saveIntegrationToken = async (req: Request, res: Response) => { } if (!integrationAuth) throw new Error("Failed to save integration access token"); - + await EEAuditLogService.createAuditLog( req.authData, { @@ -195,13 +214,29 @@ export const saveIntegrationToken = async (req: Request, res: Response) => { * @returns */ export const getIntegrationAuthApps = async (req: Request, res: Response) => { - const teamId = req.query.teamId as string; - const workspaceSlug = req.query.workspaceSlug as string; + const { + params: { integrationAuthId }, + query: { teamId, workspaceSlug } + } = await validateRequest(reqValidator.GetIntegrationAuthAppsV1, req); + + // TODO(akhilmhdh): remove class -> static function path and makes these into reusable independent functions + const { integrationAuth, accessToken, accessId } = await getIntegrationAuthAccessHelper({ + integrationAuthId: new ObjectId(integrationAuthId) + }); + + const { permission } = await getUserProjectPermissions( + req.user._id, + integrationAuth.workspace.toString() + ); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Read, + ProjectPermissionSub.Integrations + ); const apps = await getApps({ - integrationAuth: req.integrationAuth, - accessToken: req.accessToken, - accessId: req.accessId, + integrationAuth: integrationAuth, + accessToken: accessToken, + accessId: accessId, ...(teamId && { teamId }), ...(workspaceSlug && { workspaceSlug }) }); @@ -218,9 +253,27 @@ export const getIntegrationAuthApps = async (req: Request, res: Response) => { * @returns */ export const getIntegrationAuthTeams = async (req: Request, res: Response) => { + const { + params: { integrationAuthId } + } = await validateRequest(reqValidator.GetIntegrationAuthTeamsV1, req); + + // TODO(akhilmhdh): remove class -> static function path and makes these into reusable independent functions + const { integrationAuth, accessToken } = await getIntegrationAuthAccessHelper({ + integrationAuthId: new ObjectId(integrationAuthId) + }); + + const { permission } = await getUserProjectPermissions( + req.user._id, + integrationAuth.workspace.toString() + ); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Read, + ProjectPermissionSub.Integrations + ); + const teams = await getTeams({ - integrationAuth: req.integrationAuth, - accessToken: req.accessToken + integrationAuth: integrationAuth, + accessToken: accessToken }); return res.status(200).send({ @@ -235,7 +288,24 @@ export const getIntegrationAuthTeams = async (req: Request, res: Response) => { * @param res */ export const getIntegrationAuthVercelBranches = async (req: Request, res: Response) => { - const appId = req.query.appId as string; + const { + params: { integrationAuthId }, + query: { appId } + } = await validateRequest(reqValidator.GetIntegrationAuthVercelBranchesV1, req); + + // TODO(akhilmhdh): remove class -> static function path and makes these into reusable independent functions + const { integrationAuth, accessToken } = await getIntegrationAuthAccessHelper({ + integrationAuthId: new ObjectId(integrationAuthId) + }); + + const { permission } = await getUserProjectPermissions( + req.user._id, + integrationAuth.workspace.toString() + ); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Read, + ProjectPermissionSub.Integrations + ); interface VercelBranch { ref: string; @@ -245,9 +315,9 @@ export const getIntegrationAuthVercelBranches = async (req: Request, res: Respon const params = new URLSearchParams({ projectId: appId, - ...(req.integrationAuth.teamId + ...(integrationAuth.teamId ? { - teamId: req.integrationAuth.teamId + teamId: integrationAuth.teamId } : {}) }); @@ -260,7 +330,7 @@ export const getIntegrationAuthVercelBranches = async (req: Request, res: Respon { params, headers: { - Authorization: `Bearer ${req.accessToken}`, + Authorization: `Bearer ${accessToken}`, "Accept-Encoding": "application/json" } } @@ -281,7 +351,24 @@ export const getIntegrationAuthVercelBranches = async (req: Request, res: Respon * @param res */ export const getIntegrationAuthRailwayEnvironments = async (req: Request, res: Response) => { - const appId = req.query.appId as string; + const { + params: { integrationAuthId }, + query: { appId } + } = await validateRequest(reqValidator.GetIntegrationAuthRailwayEnvironmentsV1, req); + + // TODO(akhilmhdh): remove class -> static function path and makes these into reusable independent functions + const { integrationAuth, accessToken } = await getIntegrationAuthAccessHelper({ + integrationAuthId: new ObjectId(integrationAuthId) + }); + + const { permission } = await getUserProjectPermissions( + req.user._id, + integrationAuth.workspace.toString() + ); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Read, + ProjectPermissionSub.Integrations + ); interface RailwayEnvironment { node: { @@ -331,7 +418,7 @@ export const getIntegrationAuthRailwayEnvironments = async (req: Request, res: R }, { headers: { - Authorization: `Bearer ${req.accessToken}`, + Authorization: `Bearer ${accessToken}`, "Content-Type": "application/json" } } @@ -357,7 +444,24 @@ export const getIntegrationAuthRailwayEnvironments = async (req: Request, res: R * @param res */ export const getIntegrationAuthRailwayServices = async (req: Request, res: Response) => { - const appId = req.query.appId as string; + const { + params: { integrationAuthId }, + query: { appId } + } = await validateRequest(reqValidator.GetIntegrationAuthRailwayServicesV1, req); + + // TODO(akhilmhdh): remove class -> static function path and makes these into reusable independent functions + const { integrationAuth, accessToken } = await getIntegrationAuthAccessHelper({ + integrationAuthId: new ObjectId(integrationAuthId) + }); + + const { permission } = await getUserProjectPermissions( + req.user._id, + integrationAuth.workspace.toString() + ); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Read, + ProjectPermissionSub.Integrations + ); interface RailwayService { node: { @@ -422,7 +526,7 @@ export const getIntegrationAuthRailwayServices = async (req: Request, res: Respo }, { headers: { - Authorization: `Bearer ${req.accessToken}`, + Authorization: `Bearer ${accessToken}`, "Content-Type": "application/json" } } @@ -446,7 +550,6 @@ export const getIntegrationAuthRailwayServices = async (req: Request, res: Respo * @returns */ export const getIntegrationAuthBitBucketWorkspaces = async (req: Request, res: Response) => { - interface WorkspaceResponse { size: number; page: number; @@ -466,31 +569,46 @@ export const getIntegrationAuthBitBucketWorkspaces = async (req: Request, res: R updated_on: string; } + const { + params: { integrationAuthId } + } = await validateRequest(reqValidator.GetIntegrationAuthBitbucketWorkspacesV1, req); + + // TODO(akhilmhdh): remove class -> static function path and makes these into reusable independent functions + const { integrationAuth, accessToken } = await getIntegrationAuthAccessHelper({ + integrationAuthId: new ObjectId(integrationAuthId) + }); + + const { permission } = await getUserProjectPermissions( + req.user._id, + integrationAuth.workspace.toString() + ); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Read, + ProjectPermissionSub.Integrations + ); + const workspaces: Workspace[] = []; let hasNextPage = true; - let workspaceUrl = `${INTEGRATION_BITBUCKET_API_URL}/2.0/workspaces` + let workspaceUrl = `${INTEGRATION_BITBUCKET_API_URL}/2.0/workspaces`; while (hasNextPage) { - const { data }: { data: WorkspaceResponse } = await standardRequest.get( - workspaceUrl, - { - headers: { - Authorization: `Bearer ${req.accessToken}`, - "Accept-Encoding": "application/json" - } + const { data }: { data: WorkspaceResponse } = await standardRequest.get(workspaceUrl, { + headers: { + Authorization: `Bearer ${accessToken}`, + "Accept-Encoding": "application/json" } - ); - + }); + if (data?.values.length > 0) { data.values.forEach((workspace) => { - workspaces.push(workspace) - }) + workspaces.push(workspace); + }); } if (data.next) { - workspaceUrl = data.next + workspaceUrl = data.next; } else { - hasNextPage = false + hasNextPage = false; } } @@ -501,13 +619,30 @@ export const getIntegrationAuthBitBucketWorkspaces = async (req: Request, res: R /** * Return list of secret groups for Northflank project with id [appId] - * @param req - * @param res - * @returns + * @param req + * @param res + * @returns */ export const getIntegrationAuthNorthflankSecretGroups = async (req: Request, res: Response) => { - const appId = req.query.appId as string; - + const { + params: { integrationAuthId }, + query: { appId } + } = await validateRequest(reqValidator.GetIntegrationAuthNorthflankSecretGroupsV1, req); + + // TODO(akhilmhdh): remove class -> static function path and makes these into reusable independent functions + const { integrationAuth, accessToken } = await getIntegrationAuthAccessHelper({ + integrationAuthId: new ObjectId(integrationAuthId) + }); + + const { permission } = await getUserProjectPermissions( + req.user._id, + integrationAuth.workspace.toString() + ); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Read, + ProjectPermissionSub.Integrations + ); + interface NorthflankSecretGroup { id: string; name: string; @@ -515,43 +650,41 @@ export const getIntegrationAuthNorthflankSecretGroups = async (req: Request, res priority: number; projectId: string; } - + interface SecretGroup { name: string; groupId: string; } - + const secretGroups: SecretGroup[] = []; - if (appId && appId !== "") { + if (appId && appId !== "") { let page = 1; const perPage = 10; let hasMorePages = true; - - while(hasMorePages) { + + while (hasMorePages) { const params = new URLSearchParams({ page: String(page), per_page: String(perPage), - filter: "all", + filter: "all" }); const { data: { - data: { - secrets - } + data: { secrets } } - } = await standardRequest.get<{ data: { secrets: NorthflankSecretGroup[] }}>( + } = await standardRequest.get<{ data: { secrets: NorthflankSecretGroup[] } }>( `${INTEGRATION_NORTHFLANK_API_URL}/v1/projects/${appId}/secrets`, { params, headers: { - Authorization: `Bearer ${req.accessToken}`, - "Accept-Encoding": "application/json", - }, + Authorization: `Bearer ${accessToken}`, + "Accept-Encoding": "application/json" + } } ); - + secrets.forEach((a: any) => { secretGroups.push({ name: a.name, @@ -566,21 +699,37 @@ export const getIntegrationAuthNorthflankSecretGroups = async (req: Request, res page++; } } - + return res.status(200).send({ secretGroups }); -} +}; /** * Return list of build configs for TeamCity project with id [appId] - * @param req - * @param res - * @returns + * @param req + * @param res + * @returns */ export const getIntegrationAuthTeamCityBuildConfigs = async (req: Request, res: Response) => { - const appId = req.query.appId as string; - + const { + params: { integrationAuthId, appId } + } = await validateRequest(reqValidator.GetIntegrationAuthTeamCityBuildConfigsV1, req); + + // TODO(akhilmhdh): remove class -> static function path and makes these into reusable independent functions + const { integrationAuth } = await getIntegrationAuthAccessHelper({ + integrationAuthId: new ObjectId(integrationAuthId) + }); + + const { permission } = await getUserProjectPermissions( + req.user._id, + integrationAuth.workspace.toString() + ); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Read, + ProjectPermissionSub.Integrations + ); + interface TeamCityBuildConfig { id: string; name: string; @@ -589,27 +738,29 @@ export const getIntegrationAuthTeamCityBuildConfigs = async (req: Request, res: href: string; webUrl: string; } - + interface GetTeamCityBuildConfigsRes { count: number; href: string; buildType: TeamCityBuildConfig[]; } - if (appId && appId !== "") { - const { data: { buildType } } = ( - await standardRequest.get(`${req.integrationAuth.url}/app/rest/buildTypes`, { + const { + data: { buildType } + } = await standardRequest.get( + `${req.integrationAuth.url}/app/rest/buildTypes`, + { params: { locator: `project:${appId}` }, headers: { Authorization: `Bearer ${req.accessToken}`, - Accept: "application/json", - }, - }) + Accept: "application/json" + } + } ); - + return res.status(200).send({ buildConfigs: buildType.map((buildConfig) => ({ name: buildConfig.name, @@ -617,11 +768,11 @@ export const getIntegrationAuthTeamCityBuildConfigs = async (req: Request, res: })) }); } - + return res.status(200).send({ buildConfigs: [] }); -} +}; /** * Delete integration authorization with id [integrationAuthId] @@ -630,30 +781,48 @@ export const getIntegrationAuthTeamCityBuildConfigs = async (req: Request, res: * @returns */ export const deleteIntegrationAuth = async (req: Request, res: Response) => { - const integrationAuth = await revokeAccess({ - integrationAuth: req.integrationAuth, - accessToken: req.accessToken + const { + params: { integrationAuthId } + } = await validateRequest(reqValidator.DeleteIntegrationAuthV1, req); + + // TODO(akhilmhdh): remove class -> static function path and makes these into reusable independent functions + const { integrationAuth, accessToken } = await getIntegrationAuthAccessHelper({ + integrationAuthId: new ObjectId(integrationAuthId) }); - - if (!integrationAuth) return res.status(400).send({ - message: "Failed to find integration authorization" + + const { permission } = await getUserProjectPermissions( + req.user._id, + integrationAuth.workspace.toString() + ); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Delete, + ProjectPermissionSub.Integrations + ); + + const deletedIntegrationAuth = await revokeAccess({ + integrationAuth: integrationAuth, + accessToken: accessToken }); + if (!deletedIntegrationAuth) + return res.status(400).send({ + message: "Failed to find integration authorization" + }); + await EEAuditLogService.createAuditLog( req.authData, { type: EventType.UNAUTHORIZE_INTEGRATION, metadata: { - integration: integrationAuth.integration + integration: deletedIntegrationAuth.integration } }, { - workspaceId: integrationAuth.workspace + workspaceId: deletedIntegrationAuth.workspace } ); return res.status(200).send({ - integrationAuth + integrationAuth: deletedIntegrationAuth }); }; - diff --git a/backend/src/controllers/v1/integrationController.ts b/backend/src/controllers/v1/integrationController.ts index 84d00f6bb..b847b382c 100644 --- a/backend/src/controllers/v1/integrationController.ts +++ b/backend/src/controllers/v1/integrationController.ts @@ -1,6 +1,6 @@ import { Request, Response } from "express"; import { Types } from "mongoose"; -import { Folder, Integration } from "../../models"; +import { Folder, IWorkspace, Integration, IntegrationAuth } from "../../models"; import { EventService } from "../../services"; import { eventStartIntegration } from "../../events"; import { getFolderByPath } from "../../services/FolderService"; @@ -8,6 +8,14 @@ import { BadRequestError } from "../../utils/errors"; import { EEAuditLogService } from "../../ee/services"; import { EventType } from "../../ee/models"; import { syncSecretsToActiveIntegrationsQueue } from "../../queues/integrations/syncSecretsToThirdPartyServices"; +import { validateRequest } from "../../helpers/validation"; +import * as reqValidator from "../../validation/integration"; +import { + ProjectPermissionActions, + ProjectPermissionSub, + getUserProjectPermissions +} from "../../ee/services/ProjectRoleService"; +import { ForbiddenError } from "@casl/ability"; /** * Create/initialize an (empty) integration for integration authorization @@ -17,24 +25,43 @@ import { syncSecretsToActiveIntegrationsQueue } from "../../queues/integrations/ */ export const createIntegration = async (req: Request, res: Response) => { const { - integrationAuthId, - app, - appId, - isActive, - sourceEnvironment, - targetEnvironment, - targetEnvironmentId, - targetService, - targetServiceId, - owner, - path, - region, - secretPath, - metadata - } = req.body; + body: { + isActive, + sourceEnvironment, + secretPath, + app, + path, + appId, + owner, + region, + targetService, + targetServiceId, + integrationAuthId, + targetEnvironment, + targetEnvironmentId, + metadata + } + } = await validateRequest(reqValidator.CreateIntegrationV1, req); + + const integrationAuth = await IntegrationAuth.findById(integrationAuthId) + .populate<{ workspace: IWorkspace }>("workspace") + .select( + "+refreshCiphertext +refreshIV +refreshTag +accessCiphertext +accessIV +accessTag +accessExpiresAt" + ); + + if (!integrationAuth) throw BadRequestError({ message: "Integration auth not found" }); + + const { permission } = await getUserProjectPermissions( + req.user._id, + integrationAuth.workspace._id.toString() + ); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Create, + ProjectPermissionSub.Integrations + ); const folders = await Folder.findOne({ - workspace: req.integrationAuth.workspace._id, + workspace: integrationAuth.workspace._id, environment: sourceEnvironment }); @@ -42,7 +69,7 @@ export const createIntegration = async (req: Request, res: Response) => { const folder = getFolderByPath(folders.nodes, secretPath); if (!folder) { throw BadRequestError({ - message: "Path for service token does not exist" + message: "Folder path doesn't exist" }); } } @@ -51,7 +78,7 @@ export const createIntegration = async (req: Request, res: Response) => { // initialize new integration after saving integration access token const integration = await new Integration({ - workspace: req.integrationAuth.workspace._id, + workspace: integrationAuth.workspace._id, environment: sourceEnvironment, isActive, app, @@ -64,7 +91,7 @@ export const createIntegration = async (req: Request, res: Response) => { path, region, secretPath, - integration: req.integrationAuth.integration, + integration: integrationAuth.integration, integrationAuth: new Types.ObjectId(integrationAuthId), metadata }).save(); @@ -120,17 +147,32 @@ export const updateIntegration = async (req: Request, res: Response) => { // integration has the correct fields populated in [Integration] const { - environment, - isActive, - app, - appId, - targetEnvironment, - owner, // github-specific integration param - secretPath - } = req.body; + body: { + environment, + isActive, + app, + appId, + targetEnvironment, + owner, // github-specific integration param + secretPath + }, + params: { integrationId } + } = await validateRequest(reqValidator.UpdateIntegrationV1, req); + + const integration = await Integration.findById(integrationId); + if (!integration) throw BadRequestError({ message: "Integration not found" }); + + const { permission } = await getUserProjectPermissions( + req.user._id, + integration.workspace.toString() + ); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Edit, + ProjectPermissionSub.Integrations + ); const folders = await Folder.findOne({ - workspace: req.integration.workspace, + workspace: integration.workspace, environment }); @@ -143,9 +185,9 @@ export const updateIntegration = async (req: Request, res: Response) => { } } - const integration = await Integration.findOneAndUpdate( + const updatedIntegration = await Integration.findOneAndUpdate( { - _id: req.integration._id + _id: integration._id }, { environment, @@ -161,18 +203,18 @@ export const updateIntegration = async (req: Request, res: Response) => { } ); - if (integration) { + if (updatedIntegration) { // trigger event - push secrets EventService.handleEvent({ event: eventStartIntegration({ - workspaceId: integration.workspace, + workspaceId: updatedIntegration.workspace, environment }) }); } return res.status(200).send({ - integration + integration: updatedIntegration }); }; @@ -183,13 +225,27 @@ export const updateIntegration = async (req: Request, res: Response) => { * @returns */ export const deleteIntegration = async (req: Request, res: Response) => { - const { integrationId } = req.params; + const { + params: { integrationId } + } = await validateRequest(reqValidator.DeleteIntegrationV1, req); - const integration = await Integration.findOneAndDelete({ + const integration = await Integration.findById(integrationId); + if (!integration) throw BadRequestError({ message: "Integration not found" }); + + const { permission } = await getUserProjectPermissions( + req.user._id, + integration.workspace.toString() + ); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Delete, + ProjectPermissionSub.Integrations + ); + + const deletedIntegration = await Integration.findOneAndDelete({ _id: integrationId }); - if (!integration) throw new Error("Failed to find integration"); + if (!deletedIntegration) throw new Error("Failed to find integration"); await EEAuditLogService.createAuditLog( req.authData, @@ -221,14 +277,22 @@ export const deleteIntegration = async (req: Request, res: Response) => { }); }; -// Will trigger sync for all integrations within the given env and workspace id +// Will trigger sync for all integrations within the given env and workspace id export const manualSync = async (req: Request, res: Response) => { - const { workspaceId, environment } = req.body; + const { + body: { workspaceId, environment } + } = await validateRequest(reqValidator.ManualSyncV1, req); + + const { permission } = await getUserProjectPermissions(req.user._id, workspaceId); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Edit, + ProjectPermissionSub.Integrations + ); + syncSecretsToActiveIntegrationsQueue({ workspaceId, environment - }) + }); - res.status(200).send() + res.status(200).send(); }; - diff --git a/backend/src/controllers/v1/keyController.ts b/backend/src/controllers/v1/keyController.ts index caf4fbf40..242cb8f82 100644 --- a/backend/src/controllers/v1/keyController.ts +++ b/backend/src/controllers/v1/keyController.ts @@ -4,6 +4,14 @@ import { Key } from "../../models"; import { findMembership } from "../../helpers/membership"; import { EventType } from "../../ee/models"; import { EEAuditLogService } from "../../ee/services"; +import { validateRequest } from "../../helpers/validation"; +import * as reqValidator from "../../validation/key"; +import { + ProjectPermissionActions, + ProjectPermissionSub, + getUserProjectPermissions +} from "../../ee/services/ProjectRoleService"; +import { ForbiddenError } from "@casl/ability"; /** * Add (encrypted) copy of workspace key for workspace with id [workspaceId] for user with @@ -13,13 +21,21 @@ import { EEAuditLogService } from "../../ee/services"; * @returns */ export const uploadKey = async (req: Request, res: Response) => { - const { workspaceId } = req.params; - const { key } = req.body; + const { + params: { workspaceId }, + body: { key } + } = await validateRequest(reqValidator.UploadKeyV1, req); + + const { permission } = await getUserProjectPermissions(req.user._id, workspaceId); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Edit, + ProjectPermissionSub.Member + ); // validate membership of receiver const receiverMembership = await findMembership({ user: key.userId, - workspace: workspaceId, + workspace: workspaceId }); if (!receiverMembership) { @@ -31,12 +47,12 @@ export const uploadKey = async (req: Request, res: Response) => { nonce: key.nonce, sender: req.user._id, receiver: key.userId, - workspace: workspaceId, + workspace: workspaceId }).save(); - return res.status(200).send({ - message: "Successfully uploaded key to workspace", - }); + return res.status(200).send({ + message: "Successfully uploaded key to workspace" + }); }; /** @@ -46,21 +62,23 @@ export const uploadKey = async (req: Request, res: Response) => { * @returns */ export const getLatestKey = async (req: Request, res: Response) => { - const { workspaceId } = req.params; - + const { + params: { workspaceId } + } = await validateRequest(reqValidator.GetLatestKeyV1, req); + // get latest key const latestKey = await Key.find({ workspace: workspaceId, - receiver: req.user._id, + receiver: req.user._id }) .sort({ createdAt: -1 }) .limit(1) .populate("sender", "+publicKey"); - const resObj: any = {}; + const resObj: any = {}; - if (latestKey.length > 0) { - resObj["latestKey"] = latestKey[0]; + if (latestKey.length > 0) { + resObj["latestKey"] = latestKey[0]; await EEAuditLogService.createAuditLog( req.authData, { @@ -73,7 +91,7 @@ export const getLatestKey = async (req: Request, res: Response) => { workspaceId: new Types.ObjectId(workspaceId) } ); - } + } - return res.status(200).send(resObj); + return res.status(200).send(resObj); }; diff --git a/backend/src/controllers/v1/membershipController.ts b/backend/src/controllers/v1/membershipController.ts index 885f16690..7ad570885 100644 --- a/backend/src/controllers/v1/membershipController.ts +++ b/backend/src/controllers/v1/membershipController.ts @@ -1,12 +1,23 @@ import { Request, Response } from "express"; import { Types } from "mongoose"; -import { IUser, Key, Membership, MembershipOrg, User } from "../../models"; +import { IUser, Key, Membership, MembershipOrg, User, Workspace } from "../../models"; import { EventType } from "../../ee/models"; import { deleteMembership as deleteMember, findMembership } from "../../helpers/membership"; import { sendMail } from "../../helpers/nodemailer"; -import { ACCEPTED, ADMIN, MEMBER } from "../../variables"; +import { ACCEPTED, ADMIN, CUSTOM, MEMBER, VIEWER } from "../../variables"; import { getSiteURL } from "../../config"; import { EEAuditLogService } from "../../ee/services"; +import { validateRequest } from "../../helpers/validation"; +import * as reqValidator from "../../validation/membership"; +import { + ProjectPermissionActions, + ProjectPermissionSub, + getUserProjectPermissions +} from "../../ee/services/ProjectRoleService"; +import { ForbiddenError } from "@casl/ability"; +import Role from "../../ee/models/role"; +import { BadRequestError } from "../../utils/errors"; +import { InviteUserToWorkspaceV1 } from "../../validation/workspace"; /** * Check that user is a member of workspace with id [workspaceId] @@ -15,7 +26,10 @@ import { EEAuditLogService } from "../../ee/services"; * @returns */ export const validateMembership = async (req: Request, res: Response) => { - const { workspaceId } = req.params; + const { + params: { workspaceId } + } = await validateRequest(reqValidator.ValidateMembershipV1, req); + // validate membership const membership = await findMembership({ user: req.user._id, @@ -38,8 +52,10 @@ export const validateMembership = async (req: Request, res: Response) => { * @returns */ export const deleteMembership = async (req: Request, res: Response) => { - const { membershipId } = req.params; - + const { + params: { membershipId } + } = await validateRequest(reqValidator.DeleteMembershipV1, req); + // check if membership to delete exists const membershipToDelete = await Membership.findOne({ _id: membershipId @@ -49,27 +65,20 @@ export const deleteMembership = async (req: Request, res: Response) => { throw new Error("Failed to delete workspace membership that doesn't exist"); } - // check if user is a member and admin of the workspace - // whose membership we wish to delete - const membership = await Membership.findOne({ - user: req.user._id, - workspace: membershipToDelete.workspace - }); - - if (!membership) { - throw new Error("Failed to validate workspace membership"); - } - - if (membership.role !== ADMIN) { - // user is not an admin member of the workspace - throw new Error("Insufficient role for deleting workspace membership"); - } + const { permission } = await getUserProjectPermissions( + req.user._id, + membershipToDelete.workspace.toString() + ); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Delete, + ProjectPermissionSub.Member + ); // delete workspace membership const deletedMembership = await deleteMember({ membershipId: membershipToDelete._id.toString() }); - + await EEAuditLogService.createAuditLog( req.authData, { @@ -80,7 +89,7 @@ export const deleteMembership = async (req: Request, res: Response) => { } }, { - workspaceId: membership.workspace + workspaceId: membershipToDelete.workspace } ); @@ -96,43 +105,56 @@ export const deleteMembership = async (req: Request, res: Response) => { * @returns */ export const changeMembershipRole = async (req: Request, res: Response) => { - const { membershipId } = req.params; - const { role } = req.body; - - if (![ADMIN, MEMBER].includes(role)) { - throw new Error("Failed to validate role"); - } + const { + body: { role }, + params: { membershipId } + } = await validateRequest(reqValidator.ChangeMembershipRoleV1, req); // validate target membership - const membershipToChangeRole = await Membership - .findById(membershipId) - .populate<{ user: IUser }>("user"); + const membershipToChangeRole = await Membership.findById(membershipId).populate<{ user: IUser }>( + "user" + ); if (!membershipToChangeRole) { throw new Error("Failed to find membership to change role"); } - // check if user is a member and admin of target membership's - // workspace - const membership = await findMembership({ - user: req.user._id, - workspace: membershipToChangeRole.workspace - }); + const { permission } = await getUserProjectPermissions( + req.user._id, + membershipToChangeRole.workspace.toString() + ); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Edit, + ProjectPermissionSub.Member + ); - if (!membership) { - throw new Error("Failed to validate membership"); + const isCustomRole = ![ADMIN, MEMBER, VIEWER].includes(role); + if (isCustomRole) { + const wsRole = await Role.findOne({ + slug: role, + isOrgRole: false, + workspace: membershipToChangeRole.workspace + }); + if (!wsRole) throw BadRequestError({ message: "Role not found" }); + const membership = await Membership.findByIdAndUpdate(membershipId, { + role: CUSTOM, + customRole: wsRole + }); + return res.status(200).send({ + membership + }); } - if (membership.role !== ADMIN) { - // user is not an admin member of the workspace - throw new Error("Insufficient role for changing member roles"); - } - - const oldRole = membershipToChangeRole.role; + const membership = await Membership.findByIdAndUpdate( + membershipId, + { + role + }, + { + new: true + } + ); - membershipToChangeRole.role = role; - await membershipToChangeRole.save(); - await EEAuditLogService.createAuditLog( req.authData, { @@ -140,8 +162,8 @@ export const changeMembershipRole = async (req: Request, res: Response) => { metadata: { userId: membershipToChangeRole.user._id.toString(), email: membershipToChangeRole.user.email, - oldRole, - newRole: membershipToChangeRole.role + oldRole: membershipToChangeRole.role, + newRole: role } }, { @@ -150,7 +172,7 @@ export const changeMembershipRole = async (req: Request, res: Response) => { ); return res.status(200).send({ - membership: membershipToChangeRole + membership }); }; @@ -161,8 +183,15 @@ export const changeMembershipRole = async (req: Request, res: Response) => { * @returns */ export const inviteUserToWorkspace = async (req: Request, res: Response) => { - const { workspaceId } = req.params; - const { email }: { email: string } = req.body; + const { + params: { workspaceId }, + body: { email } + } = await validateRequest(InviteUserToWorkspaceV1, req); + const { permission } = await getUserProjectPermissions(req.user._id, workspaceId); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Create, + ProjectPermissionSub.Member + ); const invitee = await User.findOne({ email @@ -179,11 +208,13 @@ export const inviteUserToWorkspace = async (req: Request, res: Response) => { if (inviteeMembership) throw new Error("Failed to add existing member of workspace"); + const workspace = await Workspace.findById(workspaceId); + if (!workspace) throw new Error("Failed to find workspace"); // validate invitee's organization membership - ensure that only // (accepted) organization members can be added to the workspace const membershipOrg = await MembershipOrg.findOne({ user: invitee._id, - organization: req.membership.workspace.organization, + organization: workspace.organization, status: ACCEPTED }); @@ -211,7 +242,7 @@ export const inviteUserToWorkspace = async (req: Request, res: Response) => { substitutions: { inviterFirstName: req.user.firstName, inviterEmail: req.user.email, - workspaceName: req.membership.workspace.name, + workspaceName: workspace.name, callback_url: (await getSiteURL()) + "/login" } }); diff --git a/backend/src/controllers/v1/membershipOrgController.ts b/backend/src/controllers/v1/membershipOrgController.ts index 0c319fe62..57ebb79aa 100644 --- a/backend/src/controllers/v1/membershipOrgController.ts +++ b/backend/src/controllers/v1/membershipOrgController.ts @@ -8,14 +8,8 @@ import { updateSubscriptionOrgQuantity } from "../../helpers/organization"; import { sendMail } from "../../helpers/nodemailer"; import { TokenService } from "../../services"; import { EELicenseService } from "../../ee/services"; -import { - ACCEPTED, - ADMIN, - INVITED, - MEMBER, - OWNER, - TOKEN_EMAIL_ORG_INVITATION -} from "../../variables"; +import { ACCEPTED, INVITED, MEMBER, TOKEN_EMAIL_ORG_INVITATION } from "../../variables"; +import * as reqValidator from "../../validation/membershipOrg"; import { getJwtSignupLifetime, getJwtSignupSecret, @@ -23,6 +17,13 @@ import { getSmtpConfigured } from "../../config"; import { validateUserEmail } from "../../validation"; +import { validateRequest } from "../../helpers/validation"; +import { + OrgPermissionActions, + OrgPermissionSubjects, + getUserOrgPermissions +} from "../../ee/services/RoleService"; +import { ForbiddenError } from "@casl/ability"; /** * Delete organization membership with id [membershipOrgId] from organization @@ -31,7 +32,9 @@ import { validateUserEmail } from "../../validation"; * @returns */ export const deleteMembershipOrg = async (req: Request, _res: Response) => { - const { membershipOrgId } = req.params; + const { + params: { membershipOrgId } + } = await validateRequest(reqValidator.DelOrgMembershipv1, req); // check if organization membership to delete exists const membershipOrgToDelete = await MembershipOrg.findOne({ @@ -42,21 +45,14 @@ export const deleteMembershipOrg = async (req: Request, _res: Response) => { throw new Error("Failed to delete organization membership that doesn't exist"); } - // check if user is a member and admin of the organization - // whose membership we wish to delete - const membershipOrg = await MembershipOrg.findOne({ - user: req.user._id, - organization: membershipOrgToDelete.organization - }); - - if (!membershipOrg) { - throw new Error("Failed to validate organization membership"); - } - - if (membershipOrg.role !== OWNER && membershipOrg.role !== ADMIN) { - // user is not an admin member of the organization - throw new Error("Insufficient role for deleting organization membership"); - } + const { permission, membership: membershipOrg } = await getUserOrgPermissions( + req.user._id, + membershipOrgToDelete.organization.toString() + ); + ForbiddenError.from(permission).throwUnlessCan( + OrgPermissionActions.Delete, + OrgPermissionSubjects.Member + ); // delete organization membership await deleteMemberFromOrg({ @@ -96,22 +92,20 @@ export const changeMembershipOrgRole = async (req: Request, res: Response) => { */ export const inviteUserToOrganization = async (req: Request, res: Response) => { let inviteeMembershipOrg, completeInviteLink; - const { organizationId, inviteeEmail } = req.body; + const { + body: { inviteeEmail, organizationId } + } = await validateRequest(reqValidator.InviteUserToOrgv1, req); + + const { permission } = await getUserOrgPermissions(req.user._id, organizationId); + ForbiddenError.from(permission).throwUnlessCan( + OrgPermissionActions.Create, + OrgPermissionSubjects.Member + ); + const host = req.headers.host; const siteUrl = `${req.protocol}://${host}`; - - // validate membership - const membershipOrg = await MembershipOrg.findOne({ - user: req.user._id, - organization: new Types.ObjectId(organizationId) - }); - - if (!membershipOrg) { - throw new Error("Failed to validate organization membership"); - } - const plan = await EELicenseService.getPlan(new Types.ObjectId(organizationId)); - + const ssoConfig = await SSOConfig.findOne({ organization: new Types.ObjectId(organizationId) }); @@ -119,9 +113,8 @@ export const inviteUserToOrganization = async (req: Request, res: Response) => { if (ssoConfig && ssoConfig.isActive) { // case: SAML SSO is enabled for the organization return res.status(400).send({ - message: - "Failed to invite member due to SAML SSO configured for organization" - }); + message: "Failed to invite member due to SAML SSO configured for organization" + }); } if (plan.memberLimit !== null) { @@ -231,7 +224,10 @@ export const inviteUserToOrganization = async (req: Request, res: Response) => { */ export const verifyUserToOrganization = async (req: Request, res: Response) => { let user; - const { email, organizationId, code } = req.body; + + const { + body: { organizationId, email, code } + } = await validateRequest(reqValidator.VerifyUserToOrgv1, req); user = await User.findOne({ email }).select("+publicKey"); diff --git a/backend/src/controllers/v1/organizationController.ts b/backend/src/controllers/v1/organizationController.ts index f738891a2..edd57a3b8 100644 --- a/backend/src/controllers/v1/organizationController.ts +++ b/backend/src/controllers/v1/organizationController.ts @@ -1,28 +1,37 @@ import { Request, Response } from "express"; import { - IncidentContactOrg, - Membership, - MembershipOrg, - Organization, - Workspace, + IncidentContactOrg, + Membership, + MembershipOrg, + Organization, + Workspace } from "../../models"; import { createOrganization as create } from "../../helpers/organization"; import { addMembershipsOrg } from "../../helpers/membershipOrg"; import { ACCEPTED, OWNER } from "../../variables"; import { getLicenseServerUrl, getSiteURL } from "../../config"; import { licenseServerKeyRequest } from "../../config/request"; +import { validateRequest } from "../../helpers/validation"; +import * as reqValidator from "../../validation/organization"; +import { + OrgPermissionActions, + OrgPermissionSubjects, + getUserOrgPermissions +} from "../../ee/services/RoleService"; +import { OrganizationNotFoundError } from "../../utils/errors"; +import { ForbiddenError } from "@casl/ability"; export const getOrganizations = async (req: Request, res: Response) => { const organizations = ( await MembershipOrg.find({ user: req.user._id, - status: ACCEPTED, + status: ACCEPTED }).populate("organization") ).map((m) => m.organization); - return res.status(200).send({ - organizations, - }); + return res.status(200).send({ + organizations + }); }; /** @@ -33,28 +42,26 @@ export const getOrganizations = async (req: Request, res: Response) => { * @returns */ export const createOrganization = async (req: Request, res: Response) => { - const { organizationName } = req.body; - - if (organizationName.length < 1) { - throw new Error("Organization names must be at least 1-character long"); - } + const { + body: { organizationName } + } = await validateRequest(reqValidator.CreateOrgv1, req); // create organization and add user as member const organization = await create({ email: req.user.email, - name: organizationName, + name: organizationName }); await addMembershipsOrg({ userIds: [req.user._id.toString()], organizationId: organization._id.toString(), roles: [OWNER], - statuses: [ACCEPTED], + statuses: [ACCEPTED] }); - return res.status(200).send({ - organization, - }); + return res.status(200).send({ + organization + }); }; /** @@ -64,10 +71,23 @@ export const createOrganization = async (req: Request, res: Response) => { * @returns */ export const getOrganization = async (req: Request, res: Response) => { - const organization = req.organization - return res.status(200).send({ - organization, - }); + const { + params: { organizationId } + } = await validateRequest(reqValidator.GetOrgv1, req); + + // ensure user has membership + await getUserOrgPermissions(req.user._id, organizationId); + + const organization = await Organization.findById(organizationId); + if (!organization) { + throw OrganizationNotFoundError({ + message: "Failed to find organization" + }); + } + + return res.status(200).send({ + organization + }); }; /** @@ -77,15 +97,23 @@ export const getOrganization = async (req: Request, res: Response) => { * @returns */ export const getOrganizationMembers = async (req: Request, res: Response) => { - const { organizationId } = req.params; + const { + params: { organizationId } + } = await validateRequest(reqValidator.GetOrgMembersv1, req); + + const { permission } = await getUserOrgPermissions(req.user._id, organizationId); + ForbiddenError.from(permission).throwUnlessCan( + OrgPermissionActions.Read, + OrgPermissionSubjects.Member + ); const users = await MembershipOrg.find({ - organization: organizationId, + organization: organizationId }).populate("user", "+publicKey"); - return res.status(200).send({ - users, - }); + return res.status(200).send({ + users + }); }; /** @@ -94,17 +122,22 @@ export const getOrganizationMembers = async (req: Request, res: Response) => { * @param res * @returns */ -export const getOrganizationWorkspaces = async ( - req: Request, - res: Response -) => { - const { organizationId } = req.params; +export const getOrganizationWorkspaces = async (req: Request, res: Response) => { + const { + params: { organizationId } + } = await validateRequest(reqValidator.GetOrgWorkspacesv1, req); + + const { permission } = await getUserOrgPermissions(req.user._id, organizationId); + ForbiddenError.from(permission).throwUnlessCan( + OrgPermissionActions.Read, + OrgPermissionSubjects.Workspace + ); const workspacesSet = new Set( ( await Workspace.find( { - organization: organizationId, + organization: organizationId }, "_id" ) @@ -113,15 +146,15 @@ export const getOrganizationWorkspaces = async ( const workspaces = ( await Membership.find({ - user: req.user._id, + user: req.user._id }).populate("workspace") ) .filter((m) => workspacesSet.has(m.workspace._id.toString())) .map((m) => m.workspace); - return res.status(200).send({ - workspaces, - }); + return res.status(200).send({ + workspaces + }); }; /** @@ -131,25 +164,33 @@ export const getOrganizationWorkspaces = async ( * @returns */ export const changeOrganizationName = async (req: Request, res: Response) => { - const { organizationId } = req.params; - const { name } = req.body; + const { + params: { organizationId }, + body: { name } + } = await validateRequest(reqValidator.ChangeOrgNamev1, req); + + const { permission } = await getUserOrgPermissions(req.user._id, organizationId); + ForbiddenError.from(permission).throwUnlessCan( + OrgPermissionActions.Edit, + OrgPermissionSubjects.Settings + ); const organization = await Organization.findOneAndUpdate( { - _id: organizationId, + _id: organizationId }, { - name, + name }, { - new: true, + new: true } ); - return res.status(200).send({ - message: "Successfully changed organization name", - organization, - }); + return res.status(200).send({ + message: "Successfully changed organization name", + organization + }); }; /** @@ -158,19 +199,24 @@ export const changeOrganizationName = async (req: Request, res: Response) => { * @param res * @returns */ -export const getOrganizationIncidentContacts = async ( - req: Request, - res: Response -) => { - const { organizationId } = req.params; +export const getOrganizationIncidentContacts = async (req: Request, res: Response) => { + const { + params: { organizationId } + } = await validateRequest(reqValidator.GetOrgIncidentContactv1, req); + + const { permission } = await getUserOrgPermissions(req.user._id, organizationId); + ForbiddenError.from(permission).throwUnlessCan( + OrgPermissionActions.Read, + OrgPermissionSubjects.IncidentAccount + ); const incidentContactsOrg = await IncidentContactOrg.find({ - organization: organizationId, + organization: organizationId }); - return res.status(200).send({ - incidentContactsOrg, - }); + return res.status(200).send({ + incidentContactsOrg + }); }; /** @@ -179,12 +225,17 @@ export const getOrganizationIncidentContacts = async ( * @param res * @returns */ -export const addOrganizationIncidentContact = async ( - req: Request, - res: Response -) => { - const { organizationId } = req.params; - const { email } = req.body; +export const addOrganizationIncidentContact = async (req: Request, res: Response) => { + const { + params: { organizationId }, + body: { email } + } = await validateRequest(reqValidator.CreateOrgIncideContact, req); + + const { permission } = await getUserOrgPermissions(req.user._id, organizationId); + ForbiddenError.from(permission).throwUnlessCan( + OrgPermissionActions.Create, + OrgPermissionSubjects.IncidentAccount + ); const incidentContactOrg = await IncidentContactOrg.findOneAndUpdate( { email, organization: organizationId }, @@ -192,9 +243,9 @@ export const addOrganizationIncidentContact = async ( { upsert: true, new: true } ); - return res.status(200).send({ - incidentContactOrg, - }); + return res.status(200).send({ + incidentContactOrg + }); }; /** @@ -203,22 +254,27 @@ export const addOrganizationIncidentContact = async ( * @param res * @returns */ -export const deleteOrganizationIncidentContact = async ( - req: Request, - res: Response -) => { - const { organizationId } = req.params; - const { email } = req.body; +export const deleteOrganizationIncidentContact = async (req: Request, res: Response) => { + const { + params: { organizationId }, + body: { email } + } = await validateRequest(reqValidator.DelOrgIncideContact, req); + + const { permission } = await getUserOrgPermissions(req.user._id, organizationId); + ForbiddenError.from(permission).throwUnlessCan( + OrgPermissionActions.Delete, + OrgPermissionSubjects.IncidentAccount + ); const incidentContactOrg = await IncidentContactOrg.findOneAndDelete({ email, - organization: organizationId, + organization: organizationId }); - return res.status(200).send({ - message: "Successfully deleted organization incident contact", - incidentContactOrg, - }); + return res.status(200).send({ + message: "Successfully deleted organization incident contact", + incidentContactOrg + }); }; /** @@ -228,19 +284,41 @@ export const deleteOrganizationIncidentContact = async ( * @param res * @returns */ -export const createOrganizationPortalSession = async ( - req: Request, - res: Response -) => { - const { data: { pmtMethods } } = await licenseServerKeyRequest.get( - `${await getLicenseServerUrl()}/api/license-server/v1/customers/${req.organization.customerId}/billing-details/payment-methods`, +export const createOrganizationPortalSession = async (req: Request, res: Response) => { + const { + params: { organizationId } + } = await validateRequest(reqValidator.GetOrgPlanBillingInfov1, req); + + const { permission } = await getUserOrgPermissions(req.user._id, organizationId); + ForbiddenError.from(permission).throwUnlessCan( + OrgPermissionActions.Edit, + OrgPermissionSubjects.Billing ); - + + const organization = await Organization.findById(organizationId); + if (!organization) { + throw OrganizationNotFoundError({ + message: "Failed to find organization" + }); + } + + const { + data: { pmtMethods } + } = await licenseServerKeyRequest.get( + `${await getLicenseServerUrl()}/api/license-server/v1/customers/${ + organization.customerId + }/billing-details/payment-methods` + ); + if (pmtMethods.length < 1) { // case: organization has no payment method on file - // -> redirect to add payment method portal - const { data: { url } } = await licenseServerKeyRequest.post( - `${await getLicenseServerUrl()}/api/license-server/v1/customers/${req.organization.customerId}/billing-details/payment-methods`, + // -> redirect to add payment method portal + const { + data: { url } + } = await licenseServerKeyRequest.post( + `${await getLicenseServerUrl()}/api/license-server/v1/customers/${ + organization.customerId + }/billing-details/payment-methods`, { success_url: (await getSiteURL()) + "/dashboard", cancel_url: (await getSiteURL()) + "/dashboard" @@ -250,8 +328,12 @@ export const createOrganizationPortalSession = async ( } else { // case: organization has payment method on file // -> redirect to billing portal - const { data: { url } } = await licenseServerKeyRequest.post( - `${await getLicenseServerUrl()}/api/license-server/v1/customers/${req.organization.customerId}/billing-details/billing-portal`, + const { + data: { url } + } = await licenseServerKeyRequest.post( + `${await getLicenseServerUrl()}/api/license-server/v1/customers/${ + organization.customerId + }/billing-details/billing-portal`, { return_url: (await getSiteURL()) + "/dashboard" } @@ -266,36 +348,43 @@ export const createOrganizationPortalSession = async ( * @param res * @returns */ -export const getOrganizationMembersAndTheirWorkspaces = async ( - req: Request, - res: Response -) => { - const { organizationId } = req.params; +export const getOrganizationMembersAndTheirWorkspaces = async (req: Request, res: Response) => { + const { + params: { organizationId } + } = await validateRequest(reqValidator.GetOrgMembersv1, req); - const workspacesSet = ( - await Workspace.find( - { - organization: organizationId, - }, - "_id" - ) - ).map((w) => w._id.toString()); + const { permission } = await getUserOrgPermissions(req.user._id, organizationId); + ForbiddenError.from(permission).throwUnlessCan( + OrgPermissionActions.Read, + OrgPermissionSubjects.Member + ); + ForbiddenError.from(permission).throwUnlessCan( + OrgPermissionActions.Read, + OrgPermissionSubjects.Workspace + ); - const memberships = ( - await Membership.find({ - workspace: { $in: workspacesSet }, - }).populate("workspace") - ); - const userToWorkspaceIds: any = {}; + const workspacesSet = ( + await Workspace.find( + { + organization: organizationId + }, + "_id" + ) + ).map((w) => w._id.toString()); - memberships.forEach(membership => { - const user = membership.user.toString(); - if (userToWorkspaceIds[user]) { - userToWorkspaceIds[user].push(membership.workspace); - } else { - userToWorkspaceIds[user] = [membership.workspace]; - } - }); + const memberships = await Membership.find({ + workspace: { $in: workspacesSet } + }).populate("workspace"); + const userToWorkspaceIds: any = {}; - return res.json(userToWorkspaceIds); + memberships.forEach((membership) => { + const user = membership.user.toString(); + if (userToWorkspaceIds[user]) { + userToWorkspaceIds[user].push(membership.workspace); + } else { + userToWorkspaceIds[user] = [membership.workspace]; + } + }); + + return res.json(userToWorkspaceIds); }; diff --git a/backend/src/controllers/v1/passwordController.ts b/backend/src/controllers/v1/passwordController.ts index 22b90a61a..65447bcf8 100644 --- a/backend/src/controllers/v1/passwordController.ts +++ b/backend/src/controllers/v1/passwordController.ts @@ -14,6 +14,8 @@ import { getSiteURL } from "../../config"; import { ActorType } from "../../ee/models"; +import { validateRequest } from "../../helpers/validation"; +import * as reqValidator from "../../validation/auth"; /** * Password reset step 1: Send email verification link to email [email] @@ -23,7 +25,9 @@ import { ActorType } from "../../ee/models"; * @returns */ export const emailPasswordReset = async (req: Request, res: Response) => { - const email: string = req.body.email; + const { + body: { email } + } = await validateRequest(reqValidator.EmailPasswordResetV1, req); const user = await User.findOne({ email }).select("+publicKey"); if (!user || !user?.publicKey) { @@ -62,7 +66,9 @@ export const emailPasswordReset = async (req: Request, res: Response) => { * @returns */ export const emailPasswordResetVerify = async (req: Request, res: Response) => { - const { email, code } = req.body; + const { + body: { email, code } + } = await validateRequest(reqValidator.EmailPasswordResetVerifyV1, req); const user = await User.findOne({ email }).select("+publicKey"); if (!user || !user?.publicKey) { @@ -103,8 +109,10 @@ export const emailPasswordResetVerify = async (req: Request, res: Response) => { */ export const srp1 = async (req: Request, res: Response) => { // return salt, serverPublicKey as part of first step of SRP protocol + const { + body: { clientPublicKey } + } = await validateRequest(reqValidator.Srp1V1, req); - const { clientPublicKey } = req.body; const user = await User.findOne({ email: req.user.email }).select("+salt +verifier"); @@ -149,16 +157,18 @@ export const srp1 = async (req: Request, res: Response) => { */ export const changePassword = async (req: Request, res: Response) => { const { - clientProof, - protectedKey, - protectedKeyIV, - protectedKeyTag, - encryptedPrivateKey, - encryptedPrivateKeyIV, - encryptedPrivateKeyTag, - salt, - verifier - } = req.body; + body: { + clientProof, + protectedKey, + protectedKeyIV, + protectedKeyTag, + encryptedPrivateKey, + encryptedPrivateKeyIV, + encryptedPrivateKeyTag, + salt, + verifier + } + } = await validateRequest(reqValidator.ChangePasswordV1, req); const user = await User.findOne({ email: req.user.email @@ -208,10 +218,7 @@ export const changePassword = async (req: Request, res: Response) => { } ); - if ( - req.authData.actor.type === ActorType.USER && - req.authData.tokenVersionId - ) { + if (req.authData.actor.type === ActorType.USER && req.authData.tokenVersionId) { await clearTokens(req.authData.tokenVersionId); } @@ -246,8 +253,9 @@ export const createBackupPrivateKey = async (req: Request, res: Response) => { // create/change backup private key // requires verifying [clientProof] as part of second step of SRP protocol // as initiated in /srp1 - - const { clientProof, encryptedPrivateKey, iv, tag, salt, verifier } = req.body; + const { + body: { clientProof, encryptedPrivateKey, salt, verifier, iv, tag } + } = await validateRequest(reqValidator.CreateBackupPrivateKeyV1, req); const user = await User.findOne({ email: req.user.email }).select("+salt +verifier"); @@ -325,15 +333,17 @@ export const getBackupPrivateKey = async (req: Request, res: Response) => { export const resetPassword = async (req: Request, res: Response) => { const { - protectedKey, - protectedKeyIV, - protectedKeyTag, - encryptedPrivateKey, - encryptedPrivateKeyIV, - encryptedPrivateKeyTag, - salt, - verifier - } = req.body; + body: { + encryptedPrivateKey, + protectedKeyTag, + protectedKey, + protectedKeyIV, + salt, + verifier, + encryptedPrivateKeyIV, + encryptedPrivateKeyTag + } + } = await validateRequest(reqValidator.ResetPasswordV1, req); await User.findByIdAndUpdate( req.user._id.toString(), diff --git a/backend/src/controllers/v1/secretImportController.ts b/backend/src/controllers/v1/secretImportController.ts deleted file mode 100644 index 23be98096..000000000 --- a/backend/src/controllers/v1/secretImportController.ts +++ /dev/null @@ -1,352 +0,0 @@ -import { Request, Response } from "express"; -import { isValidScope, validateMembership } from "../../helpers"; -import { Folder, SecretImport, ServiceTokenData } from "../../models"; -import { getAllImportedSecrets } from "../../services/SecretImportService"; -import { getFolderWithPathFromId } from "../../services/FolderService"; -import { BadRequestError, ResourceNotFoundError,UnauthorizedRequestError } from "../../utils/errors"; -import { ADMIN, MEMBER } from "../../variables"; -import { EEAuditLogService } from "../../ee/services"; -import { EventType } from "../../ee/models"; - -export const createSecretImport = async (req: Request, res: Response) => { - const { workspaceId, environment, folderId, secretImport } = req.body; - - const folders = await Folder.findOne({ - workspace: workspaceId, - environment - }).lean(); - - if (!folders && folderId !== "root") { - throw ResourceNotFoundError({ - message: "Failed to find folder" - }); - } - - let secretPath = "/"; - if (folders) { - const { folderPath } = getFolderWithPathFromId(folders.nodes, folderId); - secretPath = folderPath; - } - if (req.authData.authPayload instanceof ServiceTokenData) { - // root check - const isValidScopeAccess = isValidScope(req.authData.authPayload, environment, secretPath); - if (!isValidScopeAccess) { - throw UnauthorizedRequestError({ message: "Folder Permission Denied" }); - } - } - - const importSecDoc = await SecretImport.findOne({ - workspace: workspaceId, - environment, - folderId - }); - - const importToSecretPath = folders?getFolderWithPathFromId(folders.nodes, folderId).folderPath:"/"; - - if (!importSecDoc) { - const doc = new SecretImport({ - workspace: workspaceId, - environment, - folderId, - imports: [{ environment: secretImport.environment, secretPath: secretImport.secretPath }] - }); - - await doc.save(); - await EEAuditLogService.createAuditLog( - req.authData, - { - type: EventType.CREATE_SECRET_IMPORT, - metadata: { - secretImportId: doc._id.toString(), - folderId: doc.folderId.toString(), - importFromEnvironment: secretImport.environment, - importFromSecretPath: secretImport.secretPath, - importToEnvironment: environment, - importToSecretPath - } - }, - { - workspaceId: doc.workspace - } - ); - return res.status(200).json({ message: "successfully created secret import" }); - } - - const doesImportExist = importSecDoc.imports.find( - (el) => el.environment === secretImport.environment && el.secretPath === secretImport.secretPath - ); - if (doesImportExist) { - throw BadRequestError({ message: "Secret import already exist" }); - } - - importSecDoc.imports.push({ - environment: secretImport.environment, - secretPath: secretImport.secretPath - }); - await importSecDoc.save(); - - await EEAuditLogService.createAuditLog( - req.authData, - { - type: EventType.CREATE_SECRET_IMPORT, - metadata: { - secretImportId: importSecDoc._id.toString(), - folderId: importSecDoc.folderId.toString(), - importFromEnvironment: secretImport.environment, - importFromSecretPath: secretImport.secretPath, - importToEnvironment: environment, - importToSecretPath - } - }, - { - workspaceId: importSecDoc.workspace - } - ); - return res.status(200).json({ message: "successfully created secret import" }); -}; - -// to keep the ordering, you must pass all the imports in here not the only updated one -// this is because the order decide which import gets overriden -export const updateSecretImport = async (req: Request, res: Response) => { - const { id } = req.params; - const { secretImports } = req.body; - const importSecDoc = await SecretImport.findById(id); - if (!importSecDoc) { - throw BadRequestError({ message: "Import not found" }); - } - - if (!(req.authData.authPayload instanceof ServiceTokenData)) { - await validateMembership({ - userId: req.user._id.toString(), - workspaceId: importSecDoc.workspace, - acceptedRoles: [ADMIN, MEMBER] - }); - } else { - // check for service token validity - const folders = await Folder.findOne({ - workspace: importSecDoc.workspace, - environment: importSecDoc.environment - }).lean(); - - let secretPath = "/"; - if (folders) { - const { folderPath } = getFolderWithPathFromId(folders.nodes, importSecDoc.folderId); - secretPath = folderPath; - } - - const isValidScopeAccess = isValidScope( - req.authData.authPayload, - importSecDoc.environment, - secretPath - ); - if (!isValidScopeAccess) { - throw UnauthorizedRequestError({ message: "Folder Permission Denied" }); - } - } - - const orderBefore = importSecDoc.imports; - importSecDoc.imports = secretImports; - - await importSecDoc.save(); - - const folders = await Folder.findOne({ - workspace: importSecDoc.workspace, - environment: importSecDoc.environment, - }).lean(); - - if (!folders) throw ResourceNotFoundError({ - message: "Failed to find folder" - }); - - const importToSecretPath = folders?getFolderWithPathFromId(folders.nodes, importSecDoc.folderId).folderPath:"/"; - - await EEAuditLogService.createAuditLog( - req.authData, - { - type: EventType.UPDATE_SECRET_IMPORT, - metadata: { - importToEnvironment: importSecDoc.environment, - importToSecretPath, - secretImportId: importSecDoc._id.toString(), - folderId: importSecDoc.folderId.toString(), - orderBefore, - orderAfter: secretImports - } - }, - { - workspaceId: importSecDoc.workspace - } - ); - return res.status(200).json({ message: "successfully updated secret import" }); -}; - -export const deleteSecretImport = async (req: Request, res: Response) => { - const { id } = req.params; - const { secretImportEnv, secretImportPath } = req.body; - const importSecDoc = await SecretImport.findById(id); - if (!importSecDoc) { - throw BadRequestError({ message: "Import not found" }); - } - - if (!(req.authData.authPayload instanceof ServiceTokenData)) { - await validateMembership({ - userId: req.user._id.toString(), - workspaceId: importSecDoc.workspace, - acceptedRoles: [ADMIN, MEMBER] - }); - } else { - // check for service token validity - const folders = await Folder.findOne({ - workspace: importSecDoc.workspace, - environment: importSecDoc.environment - }).lean(); - - let secretPath = "/"; - if (folders) { - const { folderPath } = getFolderWithPathFromId(folders.nodes, importSecDoc.folderId); - secretPath = folderPath; - } - - const isValidScopeAccess = isValidScope( - req.authData.authPayload, - importSecDoc.environment, - secretPath - ); - if (!isValidScopeAccess) { - throw UnauthorizedRequestError({ message: "Folder Permission Denied" }); - } - } - importSecDoc.imports = importSecDoc.imports.filter( - ({ environment, secretPath }) => - !(environment === secretImportEnv && secretPath === secretImportPath) - ); - await importSecDoc.save(); - - const folders = await Folder.findOne({ - workspace: importSecDoc.workspace, - environment: importSecDoc.environment, - }).lean(); - - if (!folders) throw ResourceNotFoundError({ - message: "Failed to find folder" - }); - - const importToSecretPath = folders?getFolderWithPathFromId(folders.nodes, importSecDoc.folderId).folderPath:"/"; - - await EEAuditLogService.createAuditLog( - req.authData, - { - type: EventType.DELETE_SECRET_IMPORT, - metadata: { - secretImportId: importSecDoc._id.toString(), - folderId: importSecDoc.folderId.toString(), - importFromEnvironment: secretImportEnv, - importFromSecretPath: secretImportPath, - importToEnvironment: importSecDoc.environment, - importToSecretPath - } - }, - { - workspaceId: importSecDoc.workspace - } - ); - - return res.status(200).json({ message: "successfully delete secret import" }); -}; - -export const getSecretImports = async (req: Request, res: Response) => { - const { workspaceId, environment, folderId } = req.query; - const importSecDoc = await SecretImport.findOne({ - workspace: workspaceId, - environment, - folderId - }); - - if (!importSecDoc) { - return res.status(200).json({ secretImport: {} }); - } - - if (req.authData.authPayload instanceof ServiceTokenData) { - // check for service token validity - const folders = await Folder.findOne({ - workspace: importSecDoc.workspace, - environment: importSecDoc.environment - }).lean(); - - let secretPath = "/"; - if (folders) { - const { folderPath } = getFolderWithPathFromId(folders.nodes, importSecDoc.folderId); - secretPath = folderPath; - } - - const isValidScopeAccess = isValidScope( - req.authData.authPayload, - importSecDoc.environment, - secretPath - ); - if (!isValidScopeAccess) { - throw UnauthorizedRequestError({ message: "Folder Permission Denied" }); - } - } - - return res.status(200).json({ secretImport: importSecDoc }); -}; - -export const getAllSecretsFromImport = async (req: Request, res: Response) => { - const { workspaceId, environment, folderId } = req.query as { - workspaceId: string; - environment: string; - folderId: string; - }; - const importSecDoc = await SecretImport.findOne({ - workspace: workspaceId, - environment, - folderId - }); - - if (!importSecDoc) { - return res.status(200).json({ secrets: [] }); - } - - if (req.authData.authPayload instanceof ServiceTokenData) { - // check for service token validity - const folders = await Folder.findOne({ - workspace: importSecDoc.workspace, - environment: importSecDoc.environment - }).lean(); - - let secretPath = "/"; - if (folders) { - const { folderPath } = getFolderWithPathFromId(folders.nodes, importSecDoc.folderId); - secretPath = folderPath; - } - - const isValidScopeAccess = isValidScope( - req.authData.authPayload, - importSecDoc.environment, - secretPath - ); - if (!isValidScopeAccess) { - throw UnauthorizedRequestError({ message: "Folder Permission Denied" }); - } - } - - await EEAuditLogService.createAuditLog( - req.authData, - { - type: EventType.GET_SECRET_IMPORTS, - metadata: { - environment, - secretImportId: importSecDoc._id.toString(), - folderId, - numberOfImports: importSecDoc.imports.length - } - }, - { - workspaceId: importSecDoc.workspace - } - ); - - const secrets = await getAllImportedSecrets(workspaceId, environment, folderId); - return res.status(200).json({ secrets }); -}; diff --git a/backend/src/controllers/v1/secretImpsController.ts b/backend/src/controllers/v1/secretImpsController.ts new file mode 100644 index 000000000..70275284b --- /dev/null +++ b/backend/src/controllers/v1/secretImpsController.ts @@ -0,0 +1,682 @@ +import { Request, Response } from "express"; +import { isValidScope } from "../../helpers"; +import { Folder, SecretImport, ServiceTokenData } from "../../models"; +import { getAllImportedSecrets } from "../../services/SecretImportService"; +import { getFolderWithPathFromId } from "../../services/FolderService"; +import { + BadRequestError, + ResourceNotFoundError, + UnauthorizedRequestError +} from "../../utils/errors"; +import { EEAuditLogService } from "../../ee/services"; +import { EventType } from "../../ee/models"; +import { validateRequest } from "../../helpers/validation"; +import * as reqValidator from "../../validation/secretImports"; +import { + ProjectPermissionActions, + ProjectPermissionSub, + getUserProjectPermissions +} from "../../ee/services/ProjectRoleService"; +import { ForbiddenError, subject } from "@casl/ability"; + +export const createSecretImp = async (req: Request, res: Response) => { + /* + #swagger.summary = 'Create secret import' + #swagger.description = 'Create a new secret import for a specified workspace and environment' + + #swagger.requestBody = { + content: { + "application/json": { + "schema": { + "type": "object", + "properties": { + "workspaceId": { + "type": "string", + "description": "ID of the workspace where the secret import will be created", + "example": "someWorkspaceId" + }, + "environment": { + "type": "string", + "description": "Environment to import to", + "example": "production" + }, + "folderId": { + "type": "string", + "description": "Folder ID. Use root for the root folder.", + "example": "my_folder" + }, + "secretImport": { + "type": "object", + "properties": { + "environment": { + "type": "string", + "description": "Import from environment", + "example": "development" + }, + "secretPath": { + "type": "string", + "description": "Import from secret path", + "example": "/user/oauth" + } + } + } + }, + "required": ["workspaceId", "environment", "folderName"] + } + } + } + } + + #swagger.responses[200] = { + content: { + "application/json": { + "schema": { + "type": "object", + "properties": { + "message": { + "type": "string", + "example": "successfully created secret import" + } + }, + "description": "Confirmation of secret import creation" + } + } + } + } + #swagger.responses[400] = { + description: "Bad Request. For example, 'Secret import already exist'" + } + #swagger.responses[401] = { + description: "Unauthorized request. For example, 'Folder Permission Denied'" + } + #swagger.responses[404] = { + description: "Resource Not Found. For example, 'Failed to find folder'" + } + */ + + const { + body: { workspaceId, environment, folderId, secretImport } + } = await validateRequest(reqValidator.CreateSecretImportV1, req); + + const folders = await Folder.findOne({ + workspace: workspaceId, + environment + }).lean(); + + if (!folders && folderId !== "root") { + throw ResourceNotFoundError({ + message: "Failed to find folder" + }); + } + + let secretPath = "/"; + if (folders) { + const { folderPath } = getFolderWithPathFromId(folders.nodes, folderId); + secretPath = folderPath; + } + + if (req.authData.authPayload instanceof ServiceTokenData) { + // root check + const isValidScopeAccess = isValidScope(req.authData.authPayload, environment, secretPath); + if (!isValidScopeAccess) { + throw UnauthorizedRequestError({ message: "Folder Permission Denied" }); + } + } else { + const { permission } = await getUserProjectPermissions(req.user._id, workspaceId); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Create, + subject(ProjectPermissionSub.SecretImports, { environment, secretPath }) + ); + } + + const importSecDoc = await SecretImport.findOne({ + workspace: workspaceId, + environment, + folderId + }); + + const importToSecretPath = folders + ? getFolderWithPathFromId(folders.nodes, folderId).folderPath + : "/"; + + if (!importSecDoc) { + const doc = new SecretImport({ + workspace: workspaceId, + environment, + folderId, + imports: [{ environment: secretImport.environment, secretPath: secretImport.secretPath }] + }); + + await doc.save(); + await EEAuditLogService.createAuditLog( + req.authData, + { + type: EventType.CREATE_SECRET_IMPORT, + metadata: { + secretImportId: doc._id.toString(), + folderId: doc.folderId.toString(), + importFromEnvironment: secretImport.environment, + importFromSecretPath: secretImport.secretPath, + importToEnvironment: environment, + importToSecretPath + } + }, + { + workspaceId: doc.workspace + } + ); + return res.status(200).json({ message: "successfully created secret import" }); + } + + const doesImportExist = importSecDoc.imports.find( + (el) => el.environment === secretImport.environment && el.secretPath === secretImport.secretPath + ); + if (doesImportExist) { + throw BadRequestError({ message: "Secret import already exist" }); + } + + importSecDoc.imports.push({ + environment: secretImport.environment, + secretPath: secretImport.secretPath + }); + await importSecDoc.save(); + + await EEAuditLogService.createAuditLog( + req.authData, + { + type: EventType.CREATE_SECRET_IMPORT, + metadata: { + secretImportId: importSecDoc._id.toString(), + folderId: importSecDoc.folderId.toString(), + importFromEnvironment: secretImport.environment, + importFromSecretPath: secretImport.secretPath, + importToEnvironment: environment, + importToSecretPath + } + }, + { + workspaceId: importSecDoc.workspace + } + ); + return res.status(200).json({ message: "successfully created secret import" }); +}; + +// to keep the ordering, you must pass all the imports in here not the only updated one +// this is because the order decide which import gets overriden + +/** + * Update secret import + * @param req + * @param res + * @returns + */ +export const updateSecretImport = async (req: Request, res: Response) => { + /* + #swagger.summary = 'Update a secret import' + #swagger.description = 'Updates an existing secret import based on the provided ID and new import details' + + #swagger.parameters['id'] = { + in: 'path', + description: 'ID of the secret import to be updated', + required: true, + type: 'string', + example: 'import12345' + } + + #swagger.requestBody = { + content: { + "application/json": { + "schema": { + "type": "object", + "properties": { + "secretImports": { + "type": "array", + "description": "List of new secret imports", + "items": { + "type": "object", + "properties": { + "environment": { + "type": "string", + "description": "Environment of the secret import", + "example": "production" + }, + "secretPath": { + "type": "string", + "description": "Path of the secret import", + "example": "/path/to/secret" + } + }, + "required": ["environment", "secretPath"] + } + } + }, + "required": ["secretImports"] + } + } + } + } + + #swagger.responses[200] = { + description: 'Successfully updated the secret import', + content: { + "application/json": { + "schema": { + "type": "object", + "properties": { + "message": { + "type": "string", + "example": "successfully updated secret import" + } + } + } + } + } + } + + #swagger.responses[400] = { + description: 'Bad Request - Import not found', + } + + #swagger.responses[403] = { + description: 'Forbidden access due to insufficient permissions', + } + + #swagger.responses[401] = { + description: 'Unauthorized access due to invalid token or scope', + } + */ + const { + body: { secretImports }, + params: { id } + } = await validateRequest(reqValidator.UpdateSecretImportV1, req); + + const importSecDoc = await SecretImport.findById(id); + if (!importSecDoc) { + throw BadRequestError({ message: "Import not found" }); + } + + // check for service token validity + const folders = await Folder.findOne({ + workspace: importSecDoc.workspace, + environment: importSecDoc.environment + }).lean(); + + let secretPath = "/"; + if (folders) { + const { folderPath } = getFolderWithPathFromId(folders.nodes, importSecDoc.folderId); + secretPath = folderPath; + } + + if (req.authData.authPayload instanceof ServiceTokenData) { + // token permission check + const isValidScopeAccess = isValidScope( + req.authData.authPayload, + importSecDoc.environment, + secretPath + ); + if (!isValidScopeAccess) { + throw UnauthorizedRequestError({ message: "Folder Permission Denied" }); + } + } else { + // non token entry check + const { permission } = await getUserProjectPermissions( + req.user._id, + importSecDoc.workspace.toString() + ); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Edit, + subject(ProjectPermissionSub.SecretImports, { + environment: importSecDoc.environment, + secretPath + }) + ); + } + + const orderBefore = importSecDoc.imports; + importSecDoc.imports = secretImports; + + await importSecDoc.save(); + + await EEAuditLogService.createAuditLog( + req.authData, + { + type: EventType.UPDATE_SECRET_IMPORT, + metadata: { + importToEnvironment: importSecDoc.environment, + importToSecretPath: secretPath, + secretImportId: importSecDoc._id.toString(), + folderId: importSecDoc.folderId.toString(), + orderBefore, + orderAfter: secretImports + } + }, + { + workspaceId: importSecDoc.workspace + } + ); + return res.status(200).json({ message: "successfully updated secret import" }); +}; + +/** + * Delete secret import + * @param req + * @param res + * @returns + */ +export const deleteSecretImport = async (req: Request, res: Response) => { + /* + #swagger.summary = 'Delete secret import' + #swagger.description = 'Delete secret import' + + #swagger.parameters['id'] = { + in: 'path', + description: 'ID of the secret import', + required: true, + type: 'string', + example: '12345abcde' + } + + #swagger.requestBody = { + content: { + "application/json": { + "schema": { + "type": "object", + "properties": { + "secretImportEnv": { + "type": "string", + "description": "Import from environment", + "example": "someWorkspaceId" + }, + "secretImportPath": { + "type": "string", + "description": "Import from secret path", + "example": "production" + } + }, + "required": ["id", "secretImportEnv", "secretImportPath"] + } + } + } + } + + #swagger.responses[200] = { + content: { + "application/json": { + "schema": { + "type": "object", + "properties": { + "message": { + "type": "string", + "example": "successfully delete secret import" + } + }, + "description": "Confirmation of secret import deletion" + } + } + } + } + */ + const { + params: { id }, + body: { secretImportEnv, secretImportPath } + } = await validateRequest(reqValidator.DeleteSecretImportV1, req); + + const importSecDoc = await SecretImport.findById(id); + if (!importSecDoc) { + throw BadRequestError({ message: "Import not found" }); + } + + // check for service token validity + const folders = await Folder.findOne({ + workspace: importSecDoc.workspace, + environment: importSecDoc.environment + }).lean(); + + let secretPath = "/"; + if (folders) { + const { folderPath } = getFolderWithPathFromId(folders.nodes, importSecDoc.folderId); + secretPath = folderPath; + } + + if (req.authData.authPayload instanceof ServiceTokenData) { + const isValidScopeAccess = isValidScope( + req.authData.authPayload, + importSecDoc.environment, + secretPath + ); + if (!isValidScopeAccess) { + throw UnauthorizedRequestError({ message: "Folder Permission Denied" }); + } + } else { + const { permission } = await getUserProjectPermissions( + req.user._id, + importSecDoc.workspace.toString() + ); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Delete, + subject(ProjectPermissionSub.SecretImports, { + environment: importSecDoc.environment, + secretPath + }) + ); + } + importSecDoc.imports = importSecDoc.imports.filter( + ({ environment, secretPath }) => + !(environment === secretImportEnv && secretPath === secretImportPath) + ); + await importSecDoc.save(); + + await EEAuditLogService.createAuditLog( + req.authData, + { + type: EventType.DELETE_SECRET_IMPORT, + metadata: { + secretImportId: importSecDoc._id.toString(), + folderId: importSecDoc.folderId.toString(), + importFromEnvironment: secretImportEnv, + importFromSecretPath: secretImportPath, + importToEnvironment: importSecDoc.environment, + importToSecretPath: secretPath + } + }, + { + workspaceId: importSecDoc.workspace + } + ); + + return res.status(200).json({ message: "successfully delete secret import" }); +}; + +/** + * Get secret imports + * @param req + * @param res + * @returns + */ +export const getSecretImports = async (req: Request, res: Response) => { + /* + #swagger.summary = 'Retrieve secret imports' + #swagger.description = 'Fetches the secret imports based on the workspaceId, environment, and folderId' + + #swagger.parameters['workspaceId'] = { + in: 'query', + description: 'ID of the workspace of secret imports to get', + required: true, + type: 'string', + example: 'workspace12345' + } + + #swagger.parameters['environment'] = { + in: 'query', + description: 'Environment of secret imports to get', + required: true, + type: 'string', + example: 'production' + } + + #swagger.parameters['folderId'] = { + in: 'query', + description: 'ID of the folder containing the secret imports. Default: root', + required: false, + type: 'string', + example: 'folder12345' + } + + #swagger.responses[200] = { + description: 'Successfully retrieved secret import', + content: { + "application/json": { + "schema": { + "type": "object", + "properties": { + "secretImport": { + "type": "object", + "description": "Details of a secret import" + } + } + } + } + } + } + + #swagger.responses[403] = { + description: 'Forbidden access due to insufficient permissions', + } + + #swagger.responses[401] = { + description: 'Unauthorized access due to invalid token or scope', + } + */ + const { + query: { workspaceId, environment, folderId } + } = await validateRequest(reqValidator.GetSecretImportsV1, req); + const importSecDoc = await SecretImport.findOne({ + workspace: workspaceId, + environment, + folderId + }); + + if (!importSecDoc) { + return res.status(200).json({ secretImport: {} }); + } + + // check for service token validity + const folders = await Folder.findOne({ + workspace: importSecDoc.workspace, + environment: importSecDoc.environment + }).lean(); + + let secretPath = "/"; + if (folders) { + const { folderPath } = getFolderWithPathFromId(folders.nodes, importSecDoc.folderId); + secretPath = folderPath; + } + + if (req.authData.authPayload instanceof ServiceTokenData) { + const isValidScopeAccess = isValidScope( + req.authData.authPayload, + importSecDoc.environment, + secretPath + ); + if (!isValidScopeAccess) { + throw UnauthorizedRequestError({ message: "Folder Permission Denied" }); + } + } else { + const { permission } = await getUserProjectPermissions( + req.user._id, + importSecDoc.workspace.toString() + ); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Read, + subject(ProjectPermissionSub.SecretImports, { + environment: importSecDoc.environment, + secretPath + }) + ); + } + + return res.status(200).json({ secretImport: importSecDoc }); +}; + +/** + * Get all secret imports + * @param req + * @param res + * @returns + */ +export const getAllSecretsFromImport = async (req: Request, res: Response) => { + const { + query: { workspaceId, environment, folderId } + } = await validateRequest(reqValidator.GetAllSecretsFromImportV1, req); + + const importSecDoc = await SecretImport.findOne({ + workspace: workspaceId, + environment, + folderId + }); + + if (!importSecDoc) { + return res.status(200).json({ secrets: [] }); + } + + const folders = await Folder.findOne({ + workspace: importSecDoc.workspace, + environment: importSecDoc.environment + }).lean(); + + let secretPath = "/"; + if (folders) { + const { folderPath } = getFolderWithPathFromId(folders.nodes, importSecDoc.folderId); + secretPath = folderPath; + } + + if (req.authData.authPayload instanceof ServiceTokenData) { + // check for service token validity + const isValidScopeAccess = isValidScope( + req.authData.authPayload, + importSecDoc.environment, + secretPath + ); + if (!isValidScopeAccess) { + throw UnauthorizedRequestError({ message: "Folder Permission Denied" }); + } + } else { + const { permission } = await getUserProjectPermissions( + req.user._id, + importSecDoc.workspace.toString() + ); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Read, + subject(ProjectPermissionSub.SecretImports, { + environment: importSecDoc.environment, + secretPath + }) + ); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Read, + subject(ProjectPermissionSub.Secrets, { + environment: importSecDoc.environment, + secretPath + }) + ); + } + + await EEAuditLogService.createAuditLog( + req.authData, + { + type: EventType.GET_SECRET_IMPORTS, + metadata: { + environment, + secretImportId: importSecDoc._id.toString(), + folderId, + numberOfImports: importSecDoc.imports.length + } + }, + { + workspaceId: importSecDoc.workspace + } + ); + + const secrets = await getAllImportedSecrets(workspaceId, environment, folderId); + return res.status(200).json({ secrets }); +}; diff --git a/backend/src/controllers/v1/secretScanningController.ts b/backend/src/controllers/v1/secretScanningController.ts index ee55a4fc5..ff1e5f3f0 100644 --- a/backend/src/controllers/v1/secretScanningController.ts +++ b/backend/src/controllers/v1/secretScanningController.ts @@ -2,20 +2,49 @@ import { Request, Response } from "express"; import GitAppInstallationSession from "../../ee/models/gitAppInstallationSession"; import crypto from "crypto"; import { Types } from "mongoose"; -import { UnauthorizedRequestError } from "../../utils/errors"; +import { OrganizationNotFoundError, UnauthorizedRequestError } from "../../utils/errors"; import GitAppOrganizationInstallation from "../../ee/models/gitAppOrganizationInstallation"; -import { MembershipOrg } from "../../models"; -import { scanGithubFullRepoForSecretLeaks } from "../../queues/secret-scanning/githubScanFullRepository" +import { scanGithubFullRepoForSecretLeaks } from "../../queues/secret-scanning/githubScanFullRepository"; import { getSecretScanningGitAppId, getSecretScanningPrivateKey } from "../../config"; -import GitRisks, { STATUS_RESOLVED_FALSE_POSITIVE, STATUS_RESOLVED_NOT_REVOKED, STATUS_RESOLVED_REVOKED } from "../../ee/models/gitRisks"; +import GitRisks, { + STATUS_RESOLVED_FALSE_POSITIVE, + STATUS_RESOLVED_NOT_REVOKED, + STATUS_RESOLVED_REVOKED +} from "../../ee/models/gitRisks"; import { ProbotOctokit } from "probot"; +import { Organization } from "../../models"; +import { validateRequest } from "../../helpers/validation"; +import * as reqValidator from "../../validation/secretScanning"; +import { + OrgPermissionActions, + OrgPermissionSubjects, + getUserOrgPermissions +} from "../../ee/services/RoleService"; +import { ForbiddenError } from "@casl/ability"; export const createInstallationSession = async (req: Request, res: Response) => { const sessionId = crypto.randomBytes(16).toString("hex"); + const { + params: { organizationId } + } = await validateRequest(reqValidator.CreateInstalLSessionv1, req); + + const organization = await Organization.findById(organizationId); + if (!organization) { + throw OrganizationNotFoundError({ + message: "Failed to find organization" + }); + } + + const { permission } = await getUserOrgPermissions(req.user._id, organizationId); + ForbiddenError.from(permission).throwUnlessCan( + OrgPermissionActions.Create, + OrgPermissionSubjects.SecretScanning + ); + await GitAppInstallationSession.findByIdAndUpdate( - req.organization, + organization, { - organization: new Types.ObjectId(req.organization), + organization: organization.id, sessionId: sessionId, user: new Types.ObjectId(req.user._id) }, @@ -24,83 +53,128 @@ export const createInstallationSession = async (req: Request, res: Response) => res.send({ sessionId: sessionId - }) -} + }); +}; export const linkInstallationToOrganization = async (req: Request, res: Response) => { - const { installationId, sessionId } = req.body + const { + body: { sessionId, installationId } + } = await validateRequest(reqValidator.LinkInstallationToOrgv1, req); - const installationSession = await GitAppInstallationSession.findOneAndDelete({ sessionId: sessionId }) + const installationSession = await GitAppInstallationSession.findOneAndDelete({ + sessionId: sessionId + }); if (!installationSession) { - throw UnauthorizedRequestError() + throw UnauthorizedRequestError(); } - const userMembership = await MembershipOrg.find({ user: req.user._id, organization: installationSession.organization }) - if (!userMembership) { - throw UnauthorizedRequestError() - } + const { permission } = await getUserOrgPermissions( + req.user._id, + installationSession.organization.toString() + ); + ForbiddenError.from(permission).throwUnlessCan( + OrgPermissionActions.Edit, + OrgPermissionSubjects.SecretScanning + ); - const installationLink = await GitAppOrganizationInstallation.findOneAndUpdate({ - organizationId: installationSession.organization, - }, { - installationId: installationId, - organizationId: installationSession.organization, - user: installationSession.user - }, { - upsert: true - }).lean() + const installationLink = await GitAppOrganizationInstallation.findOneAndUpdate( + { + organizationId: installationSession.organization + }, + { + installationId: installationId, + organizationId: installationSession.organization, + user: installationSession.user + }, + { + upsert: true + } + ).lean(); const octokit = new ProbotOctokit({ auth: { appId: await getSecretScanningGitAppId(), privateKey: await getSecretScanningPrivateKey(), installationId: installationId.toString() - }, + } }); - const { data: { repositories }}= await octokit.apps.listReposAccessibleToInstallation() + const { + data: { repositories } + } = await octokit.apps.listReposAccessibleToInstallation(); for (const repository of repositories) { - scanGithubFullRepoForSecretLeaks({organizationId: installationSession.organization.toString(), installationId, repository: {id: repository.id, fullName: repository.full_name}}) + scanGithubFullRepoForSecretLeaks({ + organizationId: installationSession.organization.toString(), + installationId, + repository: { id: repository.id, fullName: repository.full_name } + }); } - res.json(installationLink) -} + res.json(installationLink); +}; export const getCurrentOrganizationInstallationStatus = async (req: Request, res: Response) => { - const { organizationId } = req.params + const { organizationId } = req.params; try { - const appInstallation = await GitAppOrganizationInstallation.findOne({ organizationId: organizationId }).lean() + const appInstallation = await GitAppOrganizationInstallation.findOne({ + organizationId: organizationId + }).lean(); if (!appInstallation) { res.json({ appInstallationComplete: false - }) + }); } res.json({ appInstallationComplete: true - }) + }); } catch { res.json({ appInstallationComplete: false - }) + }); } -} +}; export const getRisksForOrganization = async (req: Request, res: Response) => { - const { organizationId } = req.params - const risks = await GitRisks.find({ organization: organizationId }).sort({ createdAt: -1 }).lean() + const { + params: { organizationId } + } = await validateRequest(reqValidator.GetOrgRisksv1, req); + + const { permission } = await getUserOrgPermissions(req.user._id, organizationId); + ForbiddenError.from(permission).throwUnlessCan( + OrgPermissionActions.Read, + OrgPermissionSubjects.SecretScanning + ); + + const risks = await GitRisks.find({ organization: organizationId }) + .sort({ createdAt: -1 }) + .lean(); res.json({ risks: risks - }) -} + }); +}; export const updateRisksStatus = async (req: Request, res: Response) => { - const { riskId } = req.params - const { status } = req.body - const isRiskResolved = status == STATUS_RESOLVED_FALSE_POSITIVE || status == STATUS_RESOLVED_REVOKED || status == STATUS_RESOLVED_NOT_REVOKED ? true : false + const { + params: { organizationId, riskId }, + body: { status } + } = await validateRequest(reqValidator.UpdateRiskStatusv1, req); + + const { permission } = await getUserOrgPermissions(req.user._id, organizationId); + ForbiddenError.from(permission).throwUnlessCan( + OrgPermissionActions.Edit, + OrgPermissionSubjects.SecretScanning + ); + + const isRiskResolved = + status == STATUS_RESOLVED_FALSE_POSITIVE || + status == STATUS_RESOLVED_REVOKED || + status == STATUS_RESOLVED_NOT_REVOKED + ? true + : false; const risk = await GitRisks.findByIdAndUpdate(riskId, { status: status, isResolved: isRiskResolved - }).lean() + }).lean(); - res.json(risk) -} \ No newline at end of file + res.json(risk); +}; diff --git a/backend/src/controllers/v1/secretsFolderController.ts b/backend/src/controllers/v1/secretsFolderController.ts index cbc642592..bb6ef7d86 100644 --- a/backend/src/controllers/v1/secretsFolderController.ts +++ b/backend/src/controllers/v1/secretsFolderController.ts @@ -1,10 +1,12 @@ +import { ForbiddenError, subject } from "@casl/ability"; import { Request, Response } from "express"; import { Types } from "mongoose"; import { EventType, FolderVersion } from "../../ee/models"; import { EEAuditLogService, EESecretService } from "../../ee/services"; -import { validateMembership } from "../../helpers/membership"; import { isValidScope } from "../../helpers/secrets"; -import { Folder, Secret, ServiceTokenData } from "../../models"; +import { validateRequest } from "../../helpers/validation"; +import { Secret, ServiceTokenData } from "../../models"; +import { Folder } from "../../models/folder"; import { appendFolder, deleteFolderById, @@ -15,12 +17,99 @@ import { getParentFromFolderId, validateFolderName } from "../../services/FolderService"; +import { + ProjectPermissionActions, + ProjectPermissionSub, + getUserProjectPermissions +} from "../../ee/services/ProjectRoleService"; import { BadRequestError, UnauthorizedRequestError } from "../../utils/errors"; -import { ADMIN, MEMBER } from "../../variables"; +import * as reqValidator from "../../validation/folders"; -// verify workspace id/environment +/** + * Create folder with name [folderName] for workspace with id [workspaceId] + * and environment [environment] + * @param req + * @param res + * @returns + */ export const createFolder = async (req: Request, res: Response) => { - const { workspaceId, environment, folderName, parentFolderId } = req.body; + /* + #swagger.summary = 'Create a folder' + #swagger.description = 'Create a new folder in a specified workspace and environment' + + #swagger.security = [{ + "apiKeyAuth": [] + }] + + #swagger.requestBody = { + content: { + "application/json": { + "schema": { + "type": "object", + "properties": { + "workspaceId": { + "type": "string", + "description": "ID of the workspace where the folder will be created", + "example": "someWorkspaceId" + }, + "environment": { + "type": "string", + "description": "Environment where the folder will reside", + "example": "production" + }, + "folderName": { + "type": "string", + "description": "Name of the folder to be created", + "example": "my_folder" + }, + "parentFolderId": { + "type": "string", + "description": "ID of the parent folder under which this folder will be created. If not specified, it will be created at the root level.", + "example": "someParentFolderId" + } + }, + "required": ["workspaceId", "environment", "folderName"] + } + } + } + } + + #swagger.responses[200] = { + content: { + "application/json": { + "schema": { + "type": "object", + "properties": { + "folder": { + "type": "object", + "properties": { + "id": { + "type": "string", + "example": "someFolderId" + }, + "name": { + "type": "string", + "example": "my_folder" + } + }, + "description": "Details of the created folder" + } + } + } + } + } + } + #swagger.responses[400] = { + description: "Bad Request. For example, 'Folder name cannot contain spaces. Only underscore and dashes'" + } + #swagger.responses[401] = { + description: "Unauthorized request. For example, 'Folder Permission Denied'" + } + */ + const { + body: { workspaceId, environment, folderName, parentFolderId } + } = await validateRequest(reqValidator.CreateFolderV1, req); + if (!validateFolderName(folderName)) { throw BadRequestError({ message: "Folder name cannot contain spaces. Only underscore and dashes" @@ -32,8 +121,20 @@ export const createFolder = async (req: Request, res: Response) => { environment }).lean(); + if (req.user) { + const { permission } = await getUserProjectPermissions(req.user._id, workspaceId); + const secretPath = + folders && parentFolderId + ? getFolderWithPathFromId(folders.nodes, parentFolderId).folderPath + : "/"; + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Create, + subject(ProjectPermissionSub.Folders, { environment, secretPath }) + ); + } + // space has no folders initialized - + if (!folders) { if (req.authData.authPayload instanceof ServiceTokenData) { // root check @@ -62,10 +163,10 @@ export const createFolder = async (req: Request, res: Response) => { }); await folderVersion.save(); await EESecretService.takeSecretSnapshot({ - workspaceId, + workspaceId: new Types.ObjectId(workspaceId), environment }); - + await EEAuditLogService.createAuditLog( req.authData, { @@ -86,9 +187,9 @@ export const createFolder = async (req: Request, res: Response) => { } const folder = appendFolder(folders.nodes, { folderName, parentFolderId }); - + await Folder.findByIdAndUpdate(folders._id, folders); - + const { folder: parentFolder, folderPath: parentFolderPath } = getFolderWithPathFromId( folders.nodes, parentFolderId || "root" @@ -116,13 +217,13 @@ export const createFolder = async (req: Request, res: Response) => { await folderVersion.save(); await EESecretService.takeSecretSnapshot({ - workspaceId, + workspaceId: new Types.ObjectId(workspaceId), environment, folderId: parentFolderId }); - - const {folderPath} = getFolderWithPathFromId(folders.nodes, folder.id); - + + const { folderPath } = getFolderWithPathFromId(folders.nodes, folder.id); + await EEAuditLogService.createAuditLog( req.authData, { @@ -142,9 +243,99 @@ export const createFolder = async (req: Request, res: Response) => { return res.json({ folder }); }; +/** + * Update folder with id [folderId] + * @param req + * @param res + * @returns + */ export const updateFolderById = async (req: Request, res: Response) => { - const { folderId } = req.params; - const { name, workspaceId, environment } = req.body; + /* + #swagger.summary = 'Update a folder by ID' + #swagger.description = 'Update the name of a folder in a specified workspace and environment by its ID' + + #swagger.security = [{ + "apiKeyAuth": [] + }] + + #swagger.parameters['folderId'] = { + "description": "ID of the folder to be updated", + "required": true, + "type": "string", + "in": "path" + } + + #swagger.requestBody = { + content: { + "application/json": { + "schema": { + "type": "object", + "properties": { + "workspaceId": { + "type": "string", + "description": "ID of the workspace where the folder is located", + "example": "someWorkspaceId" + }, + "environment": { + "type": "string", + "description": "Environment where the folder is located", + "example": "production" + }, + "name": { + "type": "string", + "description": "New name for the folder", + "example": "updated_folder_name" + } + }, + "required": ["workspaceId", "environment", "name"] + } + } + } + } + + #swagger.responses[200] = { + content: { + "application/json": { + "schema": { + "type": "object", + "properties": { + "message": { + "type": "string", + "example": "Successfully updated folder" + }, + "folder": { + "type": "object", + "properties": { + "name": { + "type": "string", + "example": "updated_folder_name" + }, + "id": { + "type": "string", + "example": "someFolderId" + } + }, + "description": "Details of the updated folder" + } + } + } + } + } + } + + #swagger.responses[400] = { + description: "Bad Request. Reasons can include 'The folder doesn't exist' or 'Folder name cannot contain spaces. Only underscore and dashes'" + } + + #swagger.responses[401] = { + description: "Unauthorized request. For example, 'Folder Permission Denied'" + } + */ + const { + body: { workspaceId, environment, name }, + params: { folderId } + } = await validateRequest(reqValidator.UpdateFolderV1, req); + if (!validateFolderName(name)) { throw BadRequestError({ message: "Folder name cannot contain spaces. Only underscore and dashes" @@ -156,21 +347,21 @@ export const updateFolderById = async (req: Request, res: Response) => { throw BadRequestError({ message: "The folder doesn't exist" }); } - if (!(req.authData.authPayload instanceof ServiceTokenData)) { - // check that user is a member of the workspace - await validateMembership({ - userId: req.user._id.toString(), - workspaceId, - acceptedRoles: [ADMIN, MEMBER] - }); - } - const parentFolder = getParentFromFolderId(folders.nodes, folderId); if (!parentFolder) { throw BadRequestError({ message: "The folder doesn't exist" }); } + + if (req.user) { + const { permission } = await getUserProjectPermissions(req.user._id, workspaceId); + const secretPath = getFolderWithPathFromId(folders.nodes, parentFolder.id).folderPath; + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Edit, + subject(ProjectPermissionSub.Folders, { environment, secretPath }) + ); + } + const folder = parentFolder.children.find(({ id }) => id === folderId); - if (!folder) { throw BadRequestError({ message: "The folder doesn't exist" }); } @@ -197,13 +388,13 @@ export const updateFolderById = async (req: Request, res: Response) => { await folderVersion.save(); await EESecretService.takeSecretSnapshot({ - workspaceId, + workspaceId: new Types.ObjectId(workspaceId), environment, folderId: parentFolder.id }); - const {folderPath} = getFolderWithPathFromId(folders.nodes, folder.id); - + const { folderPath } = getFolderWithPathFromId(folders.nodes, folder.id); + await EEAuditLogService.createAuditLog( req.authData, { @@ -227,38 +418,121 @@ export const updateFolderById = async (req: Request, res: Response) => { }); }; +/** + * Delete folder with id [folderId] + * @param req + * @param res + * @returns + */ export const deleteFolder = async (req: Request, res: Response) => { - const { folderId } = req.params; - const { workspaceId, environment } = req.body; + /* + #swagger.summary = 'Delete a folder by ID' + #swagger.description = 'Delete the specified folder from a specified workspace and environment using its ID' + + #swagger.security = [{ + "apiKeyAuth": [] + }] + + #swagger.parameters['folderId'] = { + "description": "ID of the folder to be deleted", + "required": true, + "type": "string", + "in": "path" + } + + #swagger.requestBody = { + content: { + "application/json": { + "schema": { + "type": "object", + "properties": { + "workspaceId": { + "type": "string", + "description": "ID of the workspace where the folder is located", + "example": "someWorkspaceId" + }, + "environment": { + "type": "string", + "description": "Environment where the folder is located", + "example": "production" + } + }, + "required": ["workspaceId", "environment"] + } + } + } + } + + #swagger.responses[200] = { + content: { + "application/json": { + "schema": { + "type": "object", + "properties": { + "message": { + "type": "string", + "example": "successfully deleted folders" + }, + "folders": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string", + "example": "someFolderId" + }, + "name": { + "type": "string", + "example": "someFolderName" + } + } + }, + "description": "List of IDs and names of the deleted folders" + } + } + } + } + } + } + + #swagger.responses[400] = { + description: "Bad Request. Reasons can include 'The folder doesn't exist'" + } + + #swagger.responses[401] = { + description: "Unauthorized request. For example, 'Folder Permission Denied'" + } + */ + const { + params: { folderId }, + body: { environment, workspaceId } + } = await validateRequest(reqValidator.DeleteFolderV1, req); const folders = await Folder.findOne({ workspace: workspaceId, environment }); if (!folders) { throw BadRequestError({ message: "The folder doesn't exist" }); } - if (!(req.authData.authPayload instanceof ServiceTokenData)) { - // check that user is a member of the workspace - await validateMembership({ - userId: req.user._id.toString(), - workspaceId, - acceptedRoles: [ADMIN, MEMBER] - }); - } - - const {folderPath} = getFolderWithPathFromId(folders.nodes, folderId); - const delOp = deleteFolderById(folders.nodes, folderId); if (!delOp) { throw BadRequestError({ message: "The folder doesn't exist" }); } const { deletedNode: delFolder, parent: parentFolder } = delOp; + const { folderPath: secretPath } = getFolderWithPathFromId(folders.nodes, parentFolder.id); if (req.authData.authPayload instanceof ServiceTokenData) { - const { folderPath: secretPath } = getFolderWithPathFromId(folders.nodes, parentFolder.id); const isValidScopeAccess = isValidScope(req.authData.authPayload, environment, secretPath); if (!isValidScopeAccess) { throw UnauthorizedRequestError({ message: "Folder Permission Denied" }); } + } else { + // check that user is a member of the workspace + const { permission } = await getUserProjectPermissions(req.user._id, workspaceId); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Delete, + subject(ProjectPermissionSub.Folders, { environment, secretPath }) + ); } parentFolder.version += 1; @@ -280,7 +554,7 @@ export const deleteFolder = async (req: Request, res: Response) => { } await EESecretService.takeSecretSnapshot({ - workspaceId, + workspaceId: new Types.ObjectId(workspaceId), environment, folderId: parentFolder.id }); @@ -288,12 +562,12 @@ export const deleteFolder = async (req: Request, res: Response) => { await EEAuditLogService.createAuditLog( req.authData, { - type: EventType.DELETE_FOLDER , + type: EventType.DELETE_FOLDER, metadata: { environment, folderId, folderName: delFolder.name, - folderPath + folderPath: secretPath } }, { @@ -304,30 +578,128 @@ export const deleteFolder = async (req: Request, res: Response) => { res.send({ message: "successfully deleted folders", folders: delFolderIds }); }; -// TODO: validate workspace +/** + * Get folders for workspace with id [workspaceId] and environment [environment] + * considering [parentFolderId] and [parentFolderPath] + * @param req + * @param res + * @returns + */ export const getFolders = async (req: Request, res: Response) => { - const { workspaceId, environment, parentFolderId, parentFolderPath } = req.query as { - workspaceId: string; - environment: string; - parentFolderId?: string; - parentFolderPath?: string; - }; + /* + #swagger.summary = 'Retrieve folders based on specific conditions' + #swagger.description = 'Fetches folders from the specified workspace and environment, optionally providing either a parentFolderId or a parentFolderPath to narrow down results' + + #swagger.security = [{ + "apiKeyAuth": [] + }] + + #swagger.parameters['workspaceId'] = { + "description": "ID of the workspace from which the folders are to be fetched", + "required": true, + "type": "string", + "in": "query" + } + + #swagger.parameters['environment'] = { + "description": "Environment where the folder is located", + "required": true, + "type": "string", + "in": "query" + } + + #swagger.parameters['parentFolderId'] = { + "description": "ID of the parent folder", + "required": false, + "type": "string", + "in": "query" + } + + #swagger.parameters['parentFolderPath'] = { + "description": "Path of the parent folder, like /folder1/folder2", + "required": false, + "type": "string", + "in": "query" + } + + #swagger.responses[200] = { + content: { + "application/json": { + "schema": { + "type": "object", + "properties": { + "folders": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string", + "example": "someFolderId" + }, + "name": { + "type": "string", + "example": "someFolderName" + } + } + }, + "description": "List of folders" + }, + "dir": { + "type": "array", + "items": { + "type": "object", + "properties": { + "name": { + "type": "string", + "example": "parentFolderName" + }, + "id": { + "type": "string", + "example": "parentFolderId" + } + } + }, + "description": "List of directories" + } + } + } + } + } + } + + #swagger.responses[400] = { + description: "Bad Request. For instance, 'The folder doesn't exist'" + } + + #swagger.responses[401] = { + description: "Unauthorized request. For example, 'Folder Permission Denied'" + } + */ + const { + query: { workspaceId, environment, parentFolderId, parentFolderPath } + } = await validateRequest(reqValidator.GetFoldersV1, req); const folders = await Folder.findOne({ workspace: workspaceId, environment }); + + if (req.user) { + const { permission } = await getUserProjectPermissions(req.user._id, workspaceId); + const secretPath = + folders && parentFolderId + ? getFolderWithPathFromId(folders.nodes, parentFolderId).folderPath + : parentFolderPath || "/"; + + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Read, + subject(ProjectPermissionSub.Folders, { environment, secretPath }) + ); + } + if (!folders) { res.send({ folders: [], dir: [] }); return; } - if (!(req.authData.authPayload instanceof ServiceTokenData)) { - // check that user is a member of the workspace - await validateMembership({ - userId: req.user._id.toString(), - workspaceId, - acceptedRoles: [ADMIN, MEMBER] - }); - } - // if instead of parentFolderId given a path like /folder1/folder2 if (parentFolderPath) { if (req.authData.authPayload instanceof ServiceTokenData) { diff --git a/backend/src/controllers/v1/signupController.ts b/backend/src/controllers/v1/signupController.ts index ce464e214..68a4e07f2 100644 --- a/backend/src/controllers/v1/signupController.ts +++ b/backend/src/controllers/v1/signupController.ts @@ -10,6 +10,8 @@ import { getSmtpConfigured } from "../../config"; import { validateUserEmail } from "../../validation"; +import { validateRequest } from "../../helpers/validation"; +import * as reqValidator from "../../validation/auth"; /** * Signup step 1: Initialize account for user under email [email] and send a verification code @@ -19,7 +21,9 @@ import { validateUserEmail } from "../../validation"; * @returns */ export const beginEmailSignup = async (req: Request, res: Response) => { - const email: string = req.body.email; + const { + body: { email } + } = await validateRequest(reqValidator.BeginEmailSignUpV1, req); // validate that email is not disposable validateUserEmail(email); @@ -50,7 +54,9 @@ export const beginEmailSignup = async (req: Request, res: Response) => { */ export const verifyEmailSignup = async (req: Request, res: Response) => { let user; - const { email, code } = req.body; + const { + body: { email, code } + } = await validateRequest(reqValidator.VerifyEmailSignUpV1, req); // initialize user account user = await User.findOne({ email }).select("+publicKey"); diff --git a/backend/src/controllers/v1/userActionController.ts b/backend/src/controllers/v1/userActionController.ts index 9e7354ce1..9a151f8dd 100644 --- a/backend/src/controllers/v1/userActionController.ts +++ b/backend/src/controllers/v1/userActionController.ts @@ -1,5 +1,7 @@ import { Request, Response } from "express"; +import { validateRequest } from "../../helpers/validation"; import { UserAction } from "../../models"; +import * as reqValidator from "../../validation/action"; /** * Add user action [action] @@ -8,26 +10,27 @@ import { UserAction } from "../../models"; * @returns */ export const addUserAction = async (req: Request, res: Response) => { - // add/record new action [action] for user with id [req.user._id] - - const { action } = req.body; + // add/record new action [action] for user with id [req.user._id] + const { + body: { action } + } = await validateRequest(reqValidator.AddUserActionV1, req); const userAction = await UserAction.findOneAndUpdate( { user: req.user._id, - action, + action }, { user: req.user._id, action }, { new: true, - upsert: true, + upsert: true } ); - return res.status(200).send({ - message: "Successfully recorded user action", - userAction, - }); + return res.status(200).send({ + message: "Successfully recorded user action", + userAction + }); }; /** @@ -37,15 +40,17 @@ export const addUserAction = async (req: Request, res: Response) => { * @returns */ export const getUserAction = async (req: Request, res: Response) => { - // get user action [action] for user with id [req.user._id] - const action: string = req.query.action as string; + // get user action [action] for user with id [req.user._id] + const { + query: { action } + } = await validateRequest(reqValidator.GetUserActionV1, req); const userAction = await UserAction.findOne({ user: req.user._id, - action, + action }); - return res.status(200).send({ - userAction, - }); + return res.status(200).send({ + userAction + }); }; diff --git a/backend/src/controllers/v1/webhookController.ts b/backend/src/controllers/v1/webhookController.ts index 7e84aae87..73d92e7dc 100644 --- a/backend/src/controllers/v1/webhookController.ts +++ b/backend/src/controllers/v1/webhookController.ts @@ -1,16 +1,32 @@ import { Request, Response } from "express"; import { Types } from "mongoose"; import { client, getRootEncryptionKey } from "../../config"; -import { validateMembership } from "../../helpers"; import { Webhook } from "../../models"; import { getWebhookPayload, triggerWebhookRequest } from "../../services/WebhookService"; import { BadRequestError, ResourceNotFoundError } from "../../utils/errors"; import { EEAuditLogService } from "../../ee/services"; import { EventType } from "../../ee/models"; -import { ADMIN, ALGORITHM_AES_256_GCM, ENCODING_SCHEME_BASE64, MEMBER } from "../../variables"; +import { ALGORITHM_AES_256_GCM, ENCODING_SCHEME_BASE64 } from "../../variables"; +import { validateRequest } from "../../helpers/validation"; +import * as reqValidator from "../../validation/webhooks"; +import { + ProjectPermissionActions, + ProjectPermissionSub, + getUserProjectPermissions +} from "../../ee/services/ProjectRoleService"; +import { ForbiddenError } from "@casl/ability"; export const createWebhook = async (req: Request, res: Response) => { - const { webhookUrl, webhookSecretKey, environment, workspaceId, secretPath } = req.body; + const { + body: { webhookUrl, webhookSecretKey, environment, workspaceId, secretPath } + } = await validateRequest(reqValidator.CreateWebhookV1, req); + + const { permission } = await getUserProjectPermissions(req.user._id, workspaceId); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Create, + ProjectPermissionSub.Webhooks + ); + const webhook = new Webhook({ workspace: workspaceId, environment, @@ -29,7 +45,7 @@ export const createWebhook = async (req: Request, res: Response) => { } await webhook.save(); - + await EEAuditLogService.createAuditLog( req.authData, { @@ -43,7 +59,7 @@ export const createWebhook = async (req: Request, res: Response) => { } }, { - workspaceId + workspaceId: new Types.ObjectId(workspaceId) } ); @@ -54,19 +70,24 @@ export const createWebhook = async (req: Request, res: Response) => { }; export const updateWebhook = async (req: Request, res: Response) => { - const { webhookId } = req.params; - const { isDisabled } = req.body; + const { + body: { isDisabled }, + params: { webhookId } + } = await validateRequest(reqValidator.UpdateWebhookV1, req); + const webhook = await Webhook.findById(webhookId); if (!webhook) { throw BadRequestError({ message: "Webhook not found!!" }); } - // check that user is a member of the workspace - await validateMembership({ - userId: req.user._id.toString(), - workspaceId: webhook.workspace, - acceptedRoles: [ADMIN, MEMBER] - }); + const { permission } = await getUserProjectPermissions( + req.user._id, + webhook.workspace.toString() + ); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Edit, + ProjectPermissionSub.Webhooks + ); if (typeof isDisabled !== undefined) { webhook.isDisabled = isDisabled; @@ -97,19 +118,24 @@ export const updateWebhook = async (req: Request, res: Response) => { }; export const deleteWebhook = async (req: Request, res: Response) => { - const { webhookId } = req.params; + const { + params: { webhookId } + } = await validateRequest(reqValidator.DeleteWebhookV1, req); let webhook = await Webhook.findById(webhookId); if (!webhook) { throw ResourceNotFoundError({ message: "Webhook not found!!" }); } - await validateMembership({ - userId: req.user._id.toString(), - workspaceId: webhook.workspace, - acceptedRoles: [ADMIN, MEMBER] - }); - + const { permission } = await getUserProjectPermissions( + req.user._id, + webhook.workspace.toString() + ); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Delete, + ProjectPermissionSub.Webhooks + ); + webhook = await Webhook.findByIdAndDelete(webhookId); if (!webhook) { @@ -139,17 +165,23 @@ export const deleteWebhook = async (req: Request, res: Response) => { }; export const testWebhook = async (req: Request, res: Response) => { - const { webhookId } = req.params; + const { + params: { webhookId } + } = await validateRequest(reqValidator.TestWebhookV1, req); + const webhook = await Webhook.findById(webhookId); if (!webhook) { throw BadRequestError({ message: "Webhook not found!!" }); } - await validateMembership({ - userId: req.user._id.toString(), - workspaceId: webhook.workspace, - acceptedRoles: [ADMIN, MEMBER] - }); + const { permission } = await getUserProjectPermissions( + req.user._id, + webhook.workspace.toString() + ); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Read, + ProjectPermissionSub.Webhooks + ); try { await triggerWebhookRequest( @@ -182,7 +214,15 @@ export const testWebhook = async (req: Request, res: Response) => { }; export const listWebhooks = async (req: Request, res: Response) => { - const { environment, workspaceId, secretPath } = req.query; + const { + query: { environment, workspaceId, secretPath } + } = await validateRequest(reqValidator.ListWebhooksV1, req); + + const { permission } = await getUserProjectPermissions(req.user._id, workspaceId); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Read, + ProjectPermissionSub.Webhooks + ); const optionalFilters: Record = {}; if (environment) optionalFilters.environment = environment as string; diff --git a/backend/src/controllers/v1/workspaceController.ts b/backend/src/controllers/v1/workspaceController.ts index 1425c142c..cbefc98a7 100644 --- a/backend/src/controllers/v1/workspaceController.ts +++ b/backend/src/controllers/v1/workspaceController.ts @@ -5,17 +5,28 @@ import { Integration, IntegrationAuth, Membership, - MembershipOrg, + Organization, ServiceToken, - Workspace, + Workspace } from "../../models"; -import { - createWorkspace as create, - deleteWorkspace as deleteWork, -} from "../../helpers/workspace"; +import { createWorkspace as create, deleteWorkspace as deleteWork } from "../../helpers/workspace"; import { EELicenseService } from "../../ee/services"; import { addMemberships } from "../../helpers/membership"; import { ADMIN } from "../../variables"; +import { OrganizationNotFoundError } from "../../utils/errors"; +import { + OrgPermissionActions, + OrgPermissionSubjects, + getUserOrgPermissions +} from "../../ee/services/RoleService"; +import { ForbiddenError } from "@casl/ability"; +import { validateRequest } from "../../helpers/validation"; +import * as reqValidator from "../../validation"; +import { + ProjectPermissionActions, + ProjectPermissionSub, + getUserProjectPermissions +} from "../../ee/services/ProjectRoleService"; /** * Return public keys of members of workspace with id [workspaceId] @@ -24,21 +35,29 @@ import { ADMIN } from "../../variables"; * @returns */ export const getWorkspacePublicKeys = async (req: Request, res: Response) => { - const { workspaceId } = req.params; + const { + params: { workspaceId } + } = await validateRequest(reqValidator.GetWorkspacePublicKeysV1, req); + + const { permission } = await getUserProjectPermissions(req.user._id, workspaceId); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Read, + ProjectPermissionSub.Member + ); const publicKeys = ( await Membership.find({ - workspace: workspaceId, + workspace: workspaceId }).populate<{ user: IUser }>("user", "publicKey") ).map((member) => { return { publicKey: member.user.publicKey, - userId: member.user._id, + userId: member.user._id }; }); return res.status(200).send({ - publicKeys, + publicKeys }); }; @@ -49,14 +68,22 @@ export const getWorkspacePublicKeys = async (req: Request, res: Response) => { * @returns */ export const getWorkspaceMemberships = async (req: Request, res: Response) => { - const { workspaceId } = req.params; + const { + params: { workspaceId } + } = await validateRequest(reqValidator.GetWorkspaceMembershipsV1, req); + + const { permission } = await getUserProjectPermissions(req.user._id, workspaceId); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Read, + ProjectPermissionSub.Member + ); const users = await Membership.find({ - workspace: workspaceId, + workspace: workspaceId }).populate("user", "+publicKey"); return res.status(200).send({ - users, + users }); }; @@ -69,12 +96,12 @@ export const getWorkspaceMemberships = async (req: Request, res: Response) => { export const getWorkspaces = async (req: Request, res: Response) => { const workspaces = ( await Membership.find({ - user: req.user._id, + user: req.user._id }).populate("workspace") ).map((m) => m.workspace); return res.status(200).send({ - workspaces, + workspaces }); }; @@ -85,14 +112,16 @@ export const getWorkspaces = async (req: Request, res: Response) => { * @returns */ export const getWorkspace = async (req: Request, res: Response) => { - const { workspaceId } = req.params; + const { + params: { workspaceId } + } = await validateRequest(reqValidator.GetWorkspaceV1, req); const workspace = await Workspace.findOne({ - _id: workspaceId, + _id: workspaceId }); return res.status(200).send({ - workspace, + workspace }); }; @@ -104,26 +133,32 @@ export const getWorkspace = async (req: Request, res: Response) => { * @returns */ export const createWorkspace = async (req: Request, res: Response) => { - const { workspaceName, organizationId } = req.body; + const { + body: { organizationId, workspaceName } + } = await validateRequest(reqValidator.CreateWorkspaceV1, req); - // validate organization membership - const membershipOrg = await MembershipOrg.findOne({ - user: req.user._id, - organization: new Types.ObjectId(organizationId), - }); - - if (!membershipOrg) { - throw new Error("Failed to validate organization membership"); + const organization = await Organization.findById(organizationId); + if (!organization) { + throw OrganizationNotFoundError({ + message: "Failed to find organization" + }); } + const { permission } = await getUserOrgPermissions(req.user._id, organizationId); + ForbiddenError.from(permission).throwUnlessCan( + OrgPermissionActions.Create, + OrgPermissionSubjects.Workspace + ); + const plan = await EELicenseService.getPlan(new Types.ObjectId(organizationId)); - + if (plan.workspaceLimit !== null) { // case: limit imposed on number of workspaces allowed if (plan.workspacesUsed >= plan.workspaceLimit) { // case: number of workspaces used exceeds the number of workspaces allowed return res.status(400).send({ - message: "Failed to create workspace due to plan limit reached. Upgrade plan to add more workspaces.", + message: + "Failed to create workspace due to plan limit reached. Upgrade plan to add more workspaces." }); } } @@ -135,17 +170,17 @@ export const createWorkspace = async (req: Request, res: Response) => { // create workspace and add user as member const workspace = await create({ name: workspaceName, - organizationId: new Types.ObjectId(organizationId), + organizationId: new Types.ObjectId(organizationId) }); await addMemberships({ userIds: [req.user._id], workspaceId: workspace._id.toString(), - roles: [ADMIN], + roles: [ADMIN] }); return res.status(200).send({ - workspace, + workspace }); }; @@ -156,15 +191,23 @@ export const createWorkspace = async (req: Request, res: Response) => { * @returns */ export const deleteWorkspace = async (req: Request, res: Response) => { - const { workspaceId } = req.params; + const { + params: { workspaceId } + } = await validateRequest(reqValidator.DeleteWorkspaceV1, req); + + const { permission } = await getUserProjectPermissions(req.user._id, workspaceId); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Delete, + ProjectPermissionSub.Workspace + ); // delete workspace await deleteWork({ - id: workspaceId, + id: workspaceId }); return res.status(200).send({ - message: "Successfully deleted workspace", + message: "Successfully deleted workspace" }); }; @@ -175,24 +218,32 @@ export const deleteWorkspace = async (req: Request, res: Response) => { * @returns */ export const changeWorkspaceName = async (req: Request, res: Response) => { - const { workspaceId } = req.params; - const { name } = req.body; + const { + params: { workspaceId }, + body: { name } + } = await validateRequest(reqValidator.ChangeWorkspaceNameV1, req); + + const { permission } = await getUserProjectPermissions(req.user._id, workspaceId); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Edit, + ProjectPermissionSub.Workspace + ); const workspace = await Workspace.findOneAndUpdate( { - _id: workspaceId, + _id: workspaceId }, { - name, + name }, { - new: true, + new: true } ); return res.status(200).send({ message: "Successfully changed workspace name", - workspace, + workspace }); }; @@ -203,14 +254,21 @@ export const changeWorkspaceName = async (req: Request, res: Response) => { * @returns */ export const getWorkspaceIntegrations = async (req: Request, res: Response) => { - const { workspaceId } = req.params; + const { + params: { workspaceId } + } = await validateRequest(reqValidator.GetWorkspaceIntegrationsV1, req); + const { permission } = await getUserProjectPermissions(req.user._id, workspaceId); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Read, + ProjectPermissionSub.Integrations + ); const integrations = await Integration.find({ - workspace: workspaceId, + workspace: workspaceId }); return res.status(200).send({ - integrations, + integrations }); }; @@ -220,18 +278,23 @@ export const getWorkspaceIntegrations = async (req: Request, res: Response) => { * @param res * @returns */ -export const getWorkspaceIntegrationAuthorizations = async ( - req: Request, - res: Response -) => { - const { workspaceId } = req.params; +export const getWorkspaceIntegrationAuthorizations = async (req: Request, res: Response) => { + const { + params: { workspaceId } + } = await validateRequest(reqValidator.GetWorkspaceIntegrationAuthorizationsV1, req); + + const { permission } = await getUserProjectPermissions(req.user._id, workspaceId); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Read, + ProjectPermissionSub.Integrations + ); const authorizations = await IntegrationAuth.find({ - workspace: workspaceId, + workspace: workspaceId }); return res.status(200).send({ - authorizations, + authorizations }); }; @@ -241,18 +304,24 @@ export const getWorkspaceIntegrationAuthorizations = async ( * @param res * @returns */ -export const getWorkspaceServiceTokens = async ( - req: Request, - res: Response -) => { - const { workspaceId } = req.params; +export const getWorkspaceServiceTokens = async (req: Request, res: Response) => { + const { + params: { workspaceId } + } = await validateRequest(reqValidator.GetWorkspaceServiceTokensV1, req); + + const { permission } = await getUserProjectPermissions(req.user._id, workspaceId); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Read, + ProjectPermissionSub.ServiceTokens + ); + // ?? FIX. const serviceTokens = await ServiceToken.find({ user: req.user._id, - workspace: workspaceId, + workspace: workspaceId }); return res.status(200).send({ - serviceTokens, + serviceTokens }); }; diff --git a/backend/src/controllers/v2/authController.ts b/backend/src/controllers/v2/authController.ts index 8f2f4cea0..d75206c64 100644 --- a/backend/src/controllers/v2/authController.ts +++ b/backend/src/controllers/v2/authController.ts @@ -10,16 +10,11 @@ import { sendMail } from "../../helpers/nodemailer"; import { TokenService } from "../../services"; import { EELogService } from "../../ee/services"; import { BadRequestError, InternalServerError } from "../../utils/errors"; -import { - ACTION_LOGIN, - TOKEN_EMAIL_MFA, -} from "../../variables"; +import { ACTION_LOGIN, TOKEN_EMAIL_MFA } from "../../variables"; import { getUserAgentType } from "../../utils/posthog"; // TODO: move this -import { - getHttpsEnabled, - getJwtMfaLifetime, - getJwtMfaSecret, -} from "../../config"; +import { getHttpsEnabled, getJwtMfaLifetime, getJwtMfaSecret } from "../../config"; +import { validateRequest } from "../../helpers/validation"; +import * as reqValidator from "../../validation/auth"; declare module "jsonwebtoken" { export interface UserIDJwtPayload extends jwt.JwtPayload { @@ -34,13 +29,10 @@ declare module "jsonwebtoken" { * @returns */ export const login1 = async (req: Request, res: Response) => { - const { - email, - clientPublicKey, - }: { email: string; clientPublicKey: string } = req.body; + const { email, clientPublicKey }: { email: string; clientPublicKey: string } = req.body; const user = await User.findOne({ - email, + email }).select("+salt +verifier"); if (!user) throw new Error("Failed to find user"); @@ -49,25 +41,28 @@ export const login1 = async (req: Request, res: Response) => { server.init( { salt: user.salt, - verifier: user.verifier, + verifier: user.verifier }, async () => { // generate server-side public key const serverPublicKey = server.getPublicKey(); - await LoginSRPDetail.findOneAndReplace({ email: email }, { - email: email, - clientPublicKey: clientPublicKey, - serverBInt: bigintConversion.bigintToBuf(server.bInt), - }, { upsert: true, returnNewDocument: false }); + await LoginSRPDetail.findOneAndReplace( + { email: email }, + { + email: email, + clientPublicKey: clientPublicKey, + serverBInt: bigintConversion.bigintToBuf(server.bInt) + }, + { upsert: true, returnNewDocument: false } + ); return res.status(200).send({ serverPublicKey, - salt: user.salt, + salt: user.salt }); } ); - }; /** @@ -78,19 +73,22 @@ export const login1 = async (req: Request, res: Response) => { * @returns */ export const login2 = async (req: Request, res: Response) => { - if (!req.headers["user-agent"]) throw InternalServerError({ message: "User-Agent header is required" }); + if (!req.headers["user-agent"]) + throw InternalServerError({ message: "User-Agent header is required" }); const { email, clientProof } = req.body; const user = await User.findOne({ - email, - }).select("+salt +verifier +encryptionVersion +protectedKey +protectedKeyIV +protectedKeyTag +publicKey +encryptedPrivateKey +iv +tag +devices"); + email + }).select( + "+salt +verifier +encryptionVersion +protectedKey +protectedKeyIV +protectedKeyTag +publicKey +encryptedPrivateKey +iv +tag +devices" + ); if (!user) throw new Error("Failed to find user"); - const loginSRPDetail = await LoginSRPDetail.findOneAndDelete({ email: email }) + const loginSRPDetail = await LoginSRPDetail.findOneAndDelete({ email: email }); if (!loginSRPDetail) { - return BadRequestError(Error("Failed to find login details for SRP")) + return BadRequestError(Error("Failed to find login details for SRP")); } const server = new jsrp.server(); @@ -98,7 +96,7 @@ export const login2 = async (req: Request, res: Response) => { { salt: user.salt, verifier: user.verifier, - b: loginSRPDetail.serverBInt, + b: loginSRPDetail.serverBInt }, async () => { server.setClientPublicKey(loginSRPDetail.clientPublicKey); @@ -111,15 +109,15 @@ export const login2 = async (req: Request, res: Response) => { // generate temporary MFA token const token = createToken({ payload: { - userId: user._id.toString(), + userId: user._id.toString() }, expiresIn: await getJwtMfaLifetime(), - secret: await getJwtMfaSecret(), + secret: await getJwtMfaSecret() }); const code = await TokenService.createToken({ type: TOKEN_EMAIL_MFA, - email, + email }); // send MFA code [code] to [email] @@ -128,27 +126,27 @@ export const login2 = async (req: Request, res: Response) => { subjectLine: "Infisical MFA code", recipients: [email], substitutions: { - code, - }, + code + } }); return res.status(200).send({ mfaEnabled: true, - token, + token }); } await checkUserDevice({ user, ip: req.realIP, - userAgent: req.headers["user-agent"] ?? "", + userAgent: req.headers["user-agent"] ?? "" }); // issue tokens - const tokens = await issueAuthTokens({ + const tokens = await issueAuthTokens({ userId: user._id, ip: req.realIP, - userAgent: req.headers["user-agent"] ?? "", + userAgent: req.headers["user-agent"] ?? "" }); // store (refresh) token in httpOnly cookie @@ -156,7 +154,7 @@ export const login2 = async (req: Request, res: Response) => { httpOnly: true, path: "/", sameSite: "strict", - secure: await getHttpsEnabled(), + secure: await getHttpsEnabled() }); // case: user does not have MFA enabled @@ -182,36 +180,33 @@ export const login2 = async (req: Request, res: Response) => { publicKey: user.publicKey, encryptedPrivateKey: user.encryptedPrivateKey, iv: user.iv, - tag: user.tag, - } + tag: user.tag + }; - if ( - user?.protectedKey && - user?.protectedKeyIV && - user?.protectedKeyTag - ) { + if (user?.protectedKey && user?.protectedKeyIV && user?.protectedKeyTag) { response.protectedKey = user.protectedKey; - response.protectedKeyIV = user.protectedKeyIV + response.protectedKeyIV = user.protectedKeyIV; response.protectedKeyTag = user.protectedKeyTag; } const loginAction = await EELogService.createAction({ name: ACTION_LOGIN, - userId: user._id, + userId: user._id }); - loginAction && await EELogService.createLog({ - userId: user._id, - actions: [loginAction], - channel: getUserAgentType(req.headers["user-agent"]), - ipAddress: req.ip, - }); + loginAction && + (await EELogService.createLog({ + userId: user._id, + actions: [loginAction], + channel: getUserAgentType(req.headers["user-agent"]), + ipAddress: req.ip + })); return res.status(200).send(response); } return res.status(400).send({ - message: "Failed to authenticate. Try again?", + message: "Failed to authenticate. Try again?" }); } ); @@ -219,15 +214,17 @@ export const login2 = async (req: Request, res: Response) => { /** * Send MFA token to email [email] - * @param req - * @param res + * @param req + * @param res */ export const sendMfaToken = async (req: Request, res: Response) => { - const { email } = req.body; + const { + body: { email } + } = await validateRequest(reqValidator.SendMfaTokenV2, req); const code = await TokenService.createToken({ type: TOKEN_EMAIL_MFA, - email, + email }); // send MFA code [code] to [email] @@ -236,49 +233,53 @@ export const sendMfaToken = async (req: Request, res: Response) => { subjectLine: "Infisical MFA code", recipients: [email], substitutions: { - code, - }, + code + } }); return res.status(200).send({ - message: "Successfully sent new MFA code", + message: "Successfully sent new MFA code" }); -} +}; /** * Verify MFA token [mfaToken] and issue JWT and refresh tokens if the * MFA token [mfaToken] is valid - * @param req - * @param res + * @param req + * @param res */ export const verifyMfaToken = async (req: Request, res: Response) => { - const { email, mfaToken } = req.body; + const { + body: { email, mfaToken } + } = await validateRequest(reqValidator.VerifyMfaTokenV2, req); await TokenService.validateToken({ type: TOKEN_EMAIL_MFA, email, - token: mfaToken, + token: mfaToken }); const user = await User.findOne({ - email, - }).select("+salt +verifier +encryptionVersion +protectedKey +protectedKeyIV +protectedKeyTag +publicKey +encryptedPrivateKey +iv +tag +devices"); + email + }).select( + "+salt +verifier +encryptionVersion +protectedKey +protectedKeyIV +protectedKeyTag +publicKey +encryptedPrivateKey +iv +tag +devices" + ); if (!user) throw new Error("Failed to find user"); - await LoginSRPDetail.deleteOne({ userId: user.id }) + await LoginSRPDetail.deleteOne({ userId: user.id }); await checkUserDevice({ user, ip: req.realIP, - userAgent: req.headers["user-agent"] ?? "", + userAgent: req.headers["user-agent"] ?? "" }); // issue tokens - const tokens = await issueAuthTokens({ + const tokens = await issueAuthTokens({ userId: user._id, ip: req.realIP, - userAgent: req.headers["user-agent"] ?? "", + userAgent: req.headers["user-agent"] ?? "" }); // store (refresh) token in httpOnly cookie @@ -286,7 +287,7 @@ export const verifyMfaToken = async (req: Request, res: Response) => { httpOnly: true, path: "/", sameSite: "strict", - secure: await getHttpsEnabled(), + secure: await getHttpsEnabled() }); interface VerifyMfaTokenRes { @@ -319,8 +320,8 @@ export const verifyMfaToken = async (req: Request, res: Response) => { publicKey: user.publicKey as string, encryptedPrivateKey: user.encryptedPrivateKey as string, iv: user.iv as string, - tag: user.tag as string, - } + tag: user.tag as string + }; if (user?.protectedKey && user?.protectedKeyIV && user?.protectedKeyTag) { resObj.protectedKey = user.protectedKey; @@ -330,15 +331,16 @@ export const verifyMfaToken = async (req: Request, res: Response) => { const loginAction = await EELogService.createAction({ name: ACTION_LOGIN, - userId: user._id, + userId: user._id }); - loginAction && await EELogService.createLog({ - userId: user._id, - actions: [loginAction], - channel: getUserAgentType(req.headers["user-agent"]), - ipAddress: req.realIP, - }); + loginAction && + (await EELogService.createLog({ + userId: user._id, + actions: [loginAction], + channel: getUserAgentType(req.headers["user-agent"]), + ipAddress: req.realIP + })); return res.status(200).send(resObj); -} +}; diff --git a/backend/src/controllers/v2/environmentController.ts b/backend/src/controllers/v2/environmentController.ts index 4e7d39fab..72365d7da 100644 --- a/backend/src/controllers/v2/environmentController.ts +++ b/backend/src/controllers/v2/environmentController.ts @@ -6,27 +6,126 @@ import { Secret, ServiceToken, ServiceTokenData, - Workspace, + Workspace } from "../../models"; import { EventType, SecretVersion } from "../../ee/models"; import { EEAuditLogService, EELicenseService } from "../../ee/services"; import { BadRequestError, WorkspaceNotFoundError } from "../../utils/errors"; import _ from "lodash"; import { PERMISSION_READ_SECRETS, PERMISSION_WRITE_SECRETS } from "../../variables"; +import { validateRequest } from "../../helpers/validation"; +import * as reqValidator from "../../validation/environments"; +import { + ProjectPermissionActions, + ProjectPermissionSub, + getUserProjectPermissions +} from "../../ee/services/ProjectRoleService"; +import { ForbiddenError } from "@casl/ability"; /** - * Create new workspace environment named [environmentName] under workspace with id + * Create new workspace environment named [environmentName] + * with slug [environmentSlug] under workspace with id * @param req * @param res * @returns */ -export const createWorkspaceEnvironment = async ( - req: Request, - res: Response -) => { +export const createWorkspaceEnvironment = async (req: Request, res: Response) => { + /* + #swagger.summary = 'Create environment' + #swagger.description = 'Create environment' + + #swagger.security = [{ + "apiKeyAuth": [] + }] + + #swagger.parameters['workspaceId'] = { + "description": "ID of project", + "required": true, + "type": "string" + } + + /* + #swagger.summary = 'Create environment' + #swagger.description = 'Create environment' + + #swagger.security = [{ + "apiKeyAuth": [] + }] + + #swagger.parameters['workspaceId'] = { + "description": "ID of project", + "required": true, + "type": "string" + } + + #swagger.requestBody = { + content: { + "application/json": { + "schema": { + "type": "object", + "properties": { + "environmentName": { + "type": "string", + "description": "Name of the environment", + "example": "development" + }, + "environmentSlug": { + "type": "string", + "description": "Slug of the environment", + "example": "dev-environment" + } + }, + "required": ["environmentName", "environmentSlug"] + } + } + } + } + + #swagger.responses[200] = { + content: { + "application/json": { + "schema": { + "type": "object", + "properties": { + "message": { + "type": "string", + "example": "Successfully created new environment" + }, + "workspace": { + "type": "string", + "example": "someWorkspaceId" + }, + "environment": { + "type": "object", + "properties": { + "name": { + "type": "string", + "example": "someEnvironmentName" + }, + "slug": { + "type": "string", + "example": "someEnvironmentSlug" + } + } + } + }, + "description": "Response after creating a new environment" + } + } + } + } + */ + const { + params: { workspaceId }, + body: { environmentName, environmentSlug } + } = await validateRequest(reqValidator.CreateWorkspaceEnvironmentV2, req); + + const { permission } = await getUserProjectPermissions(req.user._id, workspaceId); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Create, + ProjectPermissionSub.Environments + ); - const { workspaceId } = req.params; - const { environmentName, environmentSlug } = req.body; const workspace = await Workspace.findById(workspaceId).exec(); if (!workspace) throw WorkspaceNotFoundError(); @@ -39,7 +138,8 @@ export const createWorkspaceEnvironment = async ( // case: number of environments used exceeds the number of environments allowed return res.status(400).send({ - message: "Failed to create environment due to environment limit reached. Upgrade plan to create more environments.", + message: + "Failed to create environment due to environment limit reached. Upgrade plan to create more environments." }); } } @@ -55,7 +155,7 @@ export const createWorkspaceEnvironment = async ( workspace?.environments.push({ name: environmentName, - slug: environmentSlug.toLowerCase(), + slug: environmentSlug.toLowerCase() }); await workspace.save(); @@ -80,8 +180,8 @@ export const createWorkspaceEnvironment = async ( workspace: workspaceId, environment: { name: environmentName, - slug: environmentSlug, - }, + slug: environmentSlug + } }); }; @@ -91,34 +191,46 @@ export const createWorkspaceEnvironment = async ( * @param res * @returns */ -export const reorderWorkspaceEnvironments = async ( - req: Request, - res: Response -) => { - const { workspaceId } = req.params; - const { environmentSlug, environmentName, otherEnvironmentSlug, otherEnvironmentName } = req.body; +export const reorderWorkspaceEnvironments = async (req: Request, res: Response) => { + const { + params: { workspaceId }, + body: { environmentName, environmentSlug, otherEnvironmentSlug, otherEnvironmentName } + } = await validateRequest(reqValidator.ReorderWorkspaceEnvironmentsV2, req); + + const { permission } = await getUserProjectPermissions(req.user._id, workspaceId); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Edit, + ProjectPermissionSub.Environments + ); // atomic update the env to avoid conflict const workspace = await Workspace.findById(workspaceId).exec(); if (!workspace) { - throw BadRequestError({message: "Couldn't load workspace"}); + throw BadRequestError({ message: "Couldn't load workspace" }); } - const environmentIndex = workspace.environments.findIndex((env) => env.name === environmentName && env.slug === environmentSlug) - const otherEnvironmentIndex = workspace.environments.findIndex((env) => env.name === otherEnvironmentName && env.slug === otherEnvironmentSlug) + const environmentIndex = workspace.environments.findIndex( + (env) => env.name === environmentName && env.slug === environmentSlug + ); + const otherEnvironmentIndex = workspace.environments.findIndex( + (env) => env.name === otherEnvironmentName && env.slug === otherEnvironmentSlug + ); if (environmentIndex === -1 || otherEnvironmentIndex === -1) { - throw BadRequestError({message: "environment or otherEnvironment couldn't be found"}) + throw BadRequestError({ message: "environment or otherEnvironment couldn't be found" }); } // swap the order of the environments - [workspace.environments[environmentIndex], workspace.environments[otherEnvironmentIndex]] = [workspace.environments[otherEnvironmentIndex], workspace.environments[environmentIndex]] + [workspace.environments[environmentIndex], workspace.environments[otherEnvironmentIndex]] = [ + workspace.environments[otherEnvironmentIndex], + workspace.environments[environmentIndex] + ]; - await workspace.save() + await workspace.save(); return res.status(200).send({ message: "Successfully reordered environments", - workspace: workspaceId, + workspace: workspaceId }); }; @@ -129,12 +241,91 @@ export const reorderWorkspaceEnvironments = async ( * @param res * @returns */ -export const renameWorkspaceEnvironment = async ( - req: Request, - res: Response -) => { - const { workspaceId } = req.params; - const { environmentName, environmentSlug, oldEnvironmentSlug } = req.body; +export const renameWorkspaceEnvironment = async (req: Request, res: Response) => { + /* + #swagger.summary = 'Rename workspace environment' + #swagger.description = 'Rename a specific environment within a workspace' + + #swagger.parameters['workspaceId'] = { + "description": "ID of the workspace", + "required": true, + "type": "string", + "in": "path" + } + + #swagger.requestBody = { + content: { + "application/json": { + "schema": { + "type": "object", + "properties": { + "environmentName": { + "type": "string", + "description": "New name for the environment", + "example": "Staging-Renamed" + }, + "environmentSlug": { + "type": "string", + "description": "New slug for the environment", + "example": "staging-renamed" + }, + "oldEnvironmentSlug": { + "type": "string", + "description": "Current slug of the environment to rename", + "example": "staging-old" + } + }, + "required": ["environmentName", "environmentSlug", "oldEnvironmentSlug"] + } + } + } + } + + #swagger.responses[200] = { + content: { + "application/json": { + "schema": { + "type": "object", + "properties": { + "message": { + "type": "string", + "example": "Successfully update environment" + }, + "workspace": { + "type": "string", + "example": "someWorkspaceId" + }, + "environment": { + "type": "object", + "properties": { + "name": { + "type": "string", + "example": "Staging-Renamed" + }, + "slug": { + "type": "string", + "example": "staging-renamed" + } + } + } + }, + "description": "Details of the renamed environment" + } + } + } + } + */ + const { + params: { workspaceId }, + body: { environmentName, environmentSlug, oldEnvironmentSlug } + } = await validateRequest(reqValidator.UpdateWorkspaceEnvironmentV2, req); + + const { permission } = await getUserProjectPermissions(req.user._id, workspaceId); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Edit, + ProjectPermissionSub.Environments + ); + // user should pass both new slug and env name if (!environmentSlug || !environmentName) { throw new Error("Invalid environment given."); @@ -148,16 +339,13 @@ export const renameWorkspaceEnvironment = async ( const isEnvExist = workspace.environments.some( ({ name, slug }) => - slug !== oldEnvironmentSlug && - (name === environmentName || slug === environmentSlug) + slug !== oldEnvironmentSlug && (name === environmentName || slug === environmentSlug) ); if (isEnvExist) { throw new Error("Invalid environment given"); } - const envIndex = workspace?.environments.findIndex( - ({ slug }) => slug === oldEnvironmentSlug - ); + const envIndex = workspace?.environments.findIndex(({ slug }) => slug === oldEnvironmentSlug); if (envIndex === -1) { throw new Error("Invalid environment given"); } @@ -191,7 +379,7 @@ export const renameWorkspaceEnvironment = async ( await Membership.updateMany( { workspace: workspaceId, - "deniedPermissions.environmentSlug": oldEnvironmentSlug, + "deniedPermissions.environmentSlug": oldEnvironmentSlug }, { $set: { "deniedPermissions.$[element].environmentSlug": environmentSlug } }, { arrayFilters: [{ "element.environmentSlug": oldEnvironmentSlug }] } @@ -218,8 +406,8 @@ export const renameWorkspaceEnvironment = async ( workspace: workspaceId, environment: { name: environmentName, - slug: environmentSlug, - }, + slug: environmentSlug + } }); }; @@ -229,21 +417,83 @@ export const renameWorkspaceEnvironment = async ( * @param res * @returns */ -export const deleteWorkspaceEnvironment = async ( - req: Request, - res: Response -) => { - const { workspaceId } = req.params; - const { environmentSlug } = req.body; +export const deleteWorkspaceEnvironment = async (req: Request, res: Response) => { + /* + #swagger.summary = 'Delete workspace environment' + #swagger.description = 'Delete a specific environment from a workspace' + + #swagger.security = [{ + "apiKeyAuth": [] + }] + + #swagger.parameters['workspaceId'] = { + "description": "ID of the workspace", + "required": true, + "type": "string", + "in": "path" + } + + #swagger.requestBody = { + content: { + "application/json": { + "schema": { + "type": "object", + "properties": { + "environmentSlug": { + "type": "string", + "description": "Slug of the environment to delete", + "example": "dev-environment" + } + }, + "required": ["environmentSlug"] + } + } + } + } + + #swagger.responses[200] = { + content: { + "application/json": { + "schema": { + "type": "object", + "properties": { + "message": { + "type": "string", + "example": "Successfully deleted environment" + }, + "workspace": { + "type": "string", + "example": "someWorkspaceId" + }, + "environment": { + "type": "string", + "example": "dev-environment" + } + }, + "description": "Response after deleting an environment from a workspace" + } + } + } + } +*/ + const { + params: { workspaceId }, + body: { environmentSlug } + } = await validateRequest(reqValidator.DeleteWorkspaceEnvironmentV2, req); + + const { permission } = await getUserProjectPermissions(req.user._id, workspaceId); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Delete, + ProjectPermissionSub.Environments + ); + // atomic update the env to avoid conflict const workspace = await Workspace.findById(workspaceId).exec(); if (!workspace) { throw new Error("Failed to create workspace environment"); } - const envIndex = workspace?.environments.findIndex( - ({ slug }) => slug === environmentSlug - ); + const envIndex = workspace?.environments.findIndex(({ slug }) => slug === environmentSlug); if (envIndex === -1) { throw new Error("Invalid environment given"); } @@ -256,11 +506,11 @@ export const deleteWorkspaceEnvironment = async ( // clean up await Secret.deleteMany({ workspace: workspaceId, - environment: environmentSlug, + environment: environmentSlug }); await SecretVersion.deleteMany({ workspace: workspaceId, - environment: environmentSlug, + environment: environmentSlug }); // await ServiceToken.deleteMany({ @@ -279,7 +529,7 @@ export const deleteWorkspaceEnvironment = async ( await Integration.deleteMany({ workspace: workspaceId, - environment: environmentSlug, + environment: environmentSlug }); await Membership.updateMany( { workspace: workspaceId }, @@ -305,46 +555,100 @@ export const deleteWorkspaceEnvironment = async ( return res.status(200).send({ message: "Successfully deleted environment", workspace: workspaceId, - environment: environmentSlug, + environment: environmentSlug }); }; +// TODO(akhilmhdh) after rbac this can be completely removed +export const getAllAccessibleEnvironmentsOfWorkspace = async (req: Request, res: Response) => { + /* + #swagger.summary = 'Get all accessible environments of a workspace' + #swagger.description = 'Fetch all environments that the user has access to in a specified workspace' + + #swagger.security = [{ + "apiKeyAuth": [] + }] -export const getAllAccessibleEnvironmentsOfWorkspace = async ( - req: Request, - res: Response -) => { - const { workspaceId } = req.params; - const workspacesUserIsMemberOf = await Membership.findOne({ - workspace: workspaceId, - user: req.user, - }) + #swagger.parameters['workspaceId'] = { + "description": "ID of the workspace", + "required": true, + "type": "string", + "in": "path" + } - if (!workspacesUserIsMemberOf) { - throw BadRequestError() - } + #swagger.responses[200] = { + content: { + "application/json": { + "schema": { + "type": "object", + "properties": { + "accessibleEnvironments": { + "type": "array", + "items": { + "type": "object", + "properties": { + "name": { + "type": "string", + "example": "Development" + }, + "slug": { + "type": "string", + "example": "development" + }, + "isWriteDenied": { + "type": "boolean", + "example": false + }, + "isReadDenied": { + "type": "boolean", + "example": false + } + } + } + } + }, + "description": "List of environments the user has access to in the specified workspace" + } + } + } + } + */ + const { + params: { workspaceId } + } = await validateRequest(reqValidator.GetAllAccessibileEnvironmentsOfWorkspaceV2, req); - const accessibleEnvironments: any = [] - const deniedPermission = workspacesUserIsMemberOf.deniedPermissions + const { membership: workspacesUserIsMemberOf } = await getUserProjectPermissions( + req.user._id, + workspaceId + ); - const relatedWorkspace = await Workspace.findById(workspaceId) + const accessibleEnvironments: any = []; + const deniedPermission = workspacesUserIsMemberOf.deniedPermissions; + + const relatedWorkspace = await Workspace.findById(workspaceId); if (!relatedWorkspace) { - throw BadRequestError() + throw BadRequestError(); } - relatedWorkspace.environments.forEach(environment => { - const isReadBlocked = _.some(deniedPermission, { environmentSlug: environment.slug, ability: PERMISSION_READ_SECRETS }) - const isWriteBlocked = _.some(deniedPermission, { environmentSlug: environment.slug, ability: PERMISSION_WRITE_SECRETS }) + relatedWorkspace.environments.forEach((environment) => { + const isReadBlocked = _.some(deniedPermission, { + environmentSlug: environment.slug, + ability: PERMISSION_READ_SECRETS + }); + const isWriteBlocked = _.some(deniedPermission, { + environmentSlug: environment.slug, + ability: PERMISSION_WRITE_SECRETS + }); if (isReadBlocked && isWriteBlocked) { - return + return; } else { accessibleEnvironments.push({ name: environment.name, slug: environment.slug, isWriteDenied: isWriteBlocked, - isReadDenied: isReadBlocked, - }) + isReadDenied: isReadBlocked + }); } - }) + }); - res.json({ accessibleEnvironments }) + res.json({ accessibleEnvironments }); }; diff --git a/backend/src/controllers/v2/organizationsController.ts b/backend/src/controllers/v2/organizationsController.ts index 301cac9d0..277a29af8 100644 --- a/backend/src/controllers/v2/organizationsController.ts +++ b/backend/src/controllers/v2/organizationsController.ts @@ -1,21 +1,27 @@ import { Request, Response } from "express"; import { Types } from "mongoose"; -import { - Membership, - MembershipOrg, - ServiceAccount, - Workspace, -} from "../../models"; +import { Membership, MembershipOrg, ServiceAccount, Workspace } from "../../models"; import { deleteMembershipOrg } from "../../helpers/membershipOrg"; import { updateSubscriptionOrgQuantity } from "../../helpers/organization"; +import Role from "../../ee/models/role"; +import { BadRequestError } from "../../utils/errors"; +import { CUSTOM } from "../../variables"; +import * as reqValidator from "../../validation/organization"; +import { validateRequest } from "../../helpers/validation"; +import { + OrgPermissionActions, + OrgPermissionSubjects, + getUserOrgPermissions +} from "../../ee/services/RoleService"; +import { ForbiddenError } from "@casl/ability"; /** * Return memberships for organization with id [organizationId] - * @param req - * @param res + * @param req + * @param res */ export const getOrganizationMemberships = async (req: Request, res: Response) => { - /* + /* #swagger.summary = 'Return organization memberships' #swagger.description = 'Return organization memberships' @@ -48,24 +54,32 @@ export const getOrganizationMemberships = async (req: Request, res: Response) => } } */ - const { organizationId } = req.params; + const { + params: { organizationId } + } = await validateRequest(reqValidator.GetOrgMembersv2, req); - const memberships = await MembershipOrg.find({ - organization: organizationId, - }).populate("user", "+publicKey"); - - return res.status(200).send({ - memberships, - }); -} + const { permission } = await getUserOrgPermissions(req.user._id, organizationId); + ForbiddenError.from(permission).throwUnlessCan( + OrgPermissionActions.Read, + OrgPermissionSubjects.Member + ); + + const memberships = await MembershipOrg.find({ + organization: organizationId + }).populate("user", "+publicKey"); + + return res.status(200).send({ + memberships + }); +}; /** * Update role of membership with id [membershipId] to role [role] - * @param req - * @param res + * @param req + * @param res */ export const updateOrganizationMembership = async (req: Request, res: Response) => { - /* + /* #swagger.summary = 'Update organization membership' #swagger.description = 'Update organization membership' @@ -118,31 +132,53 @@ export const updateOrganizationMembership = async (req: Request, res: Response) } } */ - const { membershipId } = req.params; - const { role } = req.body; - - const membership = await MembershipOrg.findByIdAndUpdate( - membershipId, - { - role, - }, { - new: true, - } - ); - - return res.status(200).send({ - membership, + const { + params: { organizationId, membershipId }, + body: { role } + } = await validateRequest(reqValidator.UpdateOrgMemberv2, req); + const { permission } = await getUserOrgPermissions(req.user._id, organizationId); + ForbiddenError.from(permission).throwUnlessCan( + OrgPermissionActions.Edit, + OrgPermissionSubjects.Member + ); + + const isCustomRole = !["admin", "member", "owner"].includes(role); + if (isCustomRole) { + const orgRole = await Role.findOne({ slug: role, isOrgRole: true }); + if (!orgRole) throw BadRequestError({ message: "Role not found" }); + + const membership = await MembershipOrg.findByIdAndUpdate(membershipId, { + role: CUSTOM, + customRole: orgRole }); -} + return res.status(200).send({ + membership + }); + } + + const membership = await MembershipOrg.findByIdAndUpdate( + membershipId, + { + role + }, + { + new: true + } + ); + + return res.status(200).send({ + membership + }); +}; /** * Delete organization membership with id [membershipId] - * @param req - * @param res - * @returns + * @param req + * @param res + * @returns */ export const deleteOrganizationMembership = async (req: Request, res: Response) => { - /* + /* #swagger.summary = 'Delete organization membership' #swagger.description = 'Delete organization membership' @@ -178,30 +214,37 @@ export const deleteOrganizationMembership = async (req: Request, res: Response) } } */ - const { membershipId } = req.params; - - // delete organization membership - const membership = await deleteMembershipOrg({ - membershipOrgId: membershipId, - }); + const { + params: { organizationId, membershipId } + } = await validateRequest(reqValidator.DeleteOrgMemberv2, req); + const { permission } = await getUserOrgPermissions(req.user._id, organizationId); + ForbiddenError.from(permission).throwUnlessCan( + OrgPermissionActions.Delete, + OrgPermissionSubjects.Member + ); - await updateSubscriptionOrgQuantity({ - organizationId: membership.organization.toString(), - }); + // delete organization membership + const membership = await deleteMembershipOrg({ + membershipOrgId: membershipId + }); - return res.status(200).send({ - membership, - }); -} + await updateSubscriptionOrgQuantity({ + organizationId: membership.organization.toString() + }); + + return res.status(200).send({ + membership + }); +}; /** * Return workspaces for organization with id [organizationId] that user has * access to - * @param req - * @param res + * @param req + * @param res */ export const getOrganizationWorkspaces = async (req: Request, res: Response) => { - /* + /* #swagger.summary = 'Return projects in organization that user is part of' #swagger.description = 'Return projects in organization that user is part of' @@ -234,45 +277,53 @@ export const getOrganizationWorkspaces = async (req: Request, res: Response) => } } */ - const { organizationId } = req.params; + const { + params: { organizationId } + } = await validateRequest(reqValidator.GetOrgWorkspacesv2, req); - const workspacesSet = new Set( - ( - await Workspace.find( - { - organization: organizationId, - }, - "_id" - ) - ).map((w) => w._id.toString()) - ); + const { permission } = await getUserOrgPermissions(req.user._id, organizationId); + ForbiddenError.from(permission).throwUnlessCan( + OrgPermissionActions.Read, + OrgPermissionSubjects.Workspace + ); - const workspaces = ( - await Membership.find({ - user: req.user._id, - }).populate("workspace") - ) + const workspacesSet = new Set( + ( + await Workspace.find( + { + organization: organizationId + }, + "_id" + ) + ).map((w) => w._id.toString()) + ); + + const workspaces = ( + await Membership.find({ + user: req.user._id + }).populate("workspace") + ) .filter((m) => workspacesSet.has(m.workspace._id.toString())) .map((m) => m.workspace); -return res.status(200).send({ - workspaces, - }); -} + return res.status(200).send({ + workspaces + }); +}; /** * Return service accounts for organization with id [organizationId] - * @param req - * @param res + * @param req + * @param res */ export const getOrganizationServiceAccounts = async (req: Request, res: Response) => { - const { organizationId } = req.params; - - const serviceAccounts = await ServiceAccount.find({ - organization: new Types.ObjectId(organizationId), - }); - - return res.status(200).send({ - serviceAccounts, - }); -} + const { organizationId } = req.params; + + const serviceAccounts = await ServiceAccount.find({ + organization: new Types.ObjectId(organizationId) + }); + + return res.status(200).send({ + serviceAccounts + }); +}; diff --git a/backend/src/controllers/v2/secretsController.ts b/backend/src/controllers/v2/secretsController.ts index 6c9b29203..1a01282cc 100644 --- a/backend/src/controllers/v2/secretsController.ts +++ b/backend/src/controllers/v2/secretsController.ts @@ -25,7 +25,6 @@ import { userHasWriteOnlyAbility } from "../../ee/helpers/checkMembershipPermissions"; import _ from "lodash"; -import { BatchSecret, BatchSecretRequest } from "../../types/secret"; import { getFolderByPath, getFolderIdFromServiceToken, @@ -35,6 +34,18 @@ import { import { isValidScope } from "../../helpers/secrets"; import path from "path"; import { getAllImportedSecrets } from "../../services/SecretImportService"; +import { validateRequest } from "../../helpers/validation"; +import { + BatchSecretsV2, + GetSecretsV2, + validateServiceTokenDataClientForWorkspace +} from "../../validation"; +import { + ProjectPermissionActions, + ProjectPermissionSub, + getUserProjectPermissions +} from "../../ee/services/ProjectRoleService"; +import { ForbiddenError, subject } from "@casl/ability"; /** * Peform a batch of any specified CUD secret operations @@ -46,22 +57,31 @@ export const batchSecrets = async (req: Request, res: Response) => { const channel = getUserAgentType(req.headers["user-agent"]); const postHogClient = await TelemetryService.getPostHogClient(); + const validatedData = await validateRequest(BatchSecretsV2, req); const { - workspaceId, - environment, - requests - }: { - workspaceId: string; - environment: string; - requests: BatchSecretRequest[]; - } = req.body; + body: { workspaceId, environment, requests } + } = validatedData; + let { + body: { secretPath, folderId } + } = validatedData; - let secretPath = req.body.secretPath as string; - let folderId = req.body.folderId as string; + const secretIds = requests + .filter(({ method }) => method !== "POST") + // akhilmhdh: ts is dumb + .map((el) => new Types.ObjectId((el.secret as any)._id)); - const createSecrets: BatchSecret[] = []; - const updateSecrets: BatchSecret[] = []; - const deleteSecrets: { _id: Types.ObjectId, secretName: string; }[] = []; + const oldSecrets = await Secret.find({ + _id: { + $in: secretIds + } + }); + if (oldSecrets.length != secretIds.length) { + throw BadRequestError({ message: "Failed to validate non-existent secrets" }); + } + + const createSecrets: any[] = []; + const updateSecrets: any[] = []; + const deleteSecrets: { _id: Types.ObjectId; secretName: string }[] = []; const actions: IAction[] = []; // get secret blind index salt @@ -69,31 +89,33 @@ export const batchSecrets = async (req: Request, res: Response) => { workspaceId: new Types.ObjectId(workspaceId) }); - const folders = await Folder.findOne({ workspace: workspaceId, environment }); - - if (req.authData.authPayload instanceof ServiceTokenData) { - const isValidScopeAccess = isValidScope(req.authData.authPayload, environment, secretPath); - - // in service token when not giving secretpath folderid must be root - // this is to avoid giving folderid when service tokens are used - if ((!secretPath && folderId !== "root") || (secretPath && !isValidScopeAccess)) { - throw UnauthorizedRequestError({ message: "Folder Permission Denied" }); - } - } - - if (secretPath) { + if (secretPath !== "/") { folderId = await getFolderIdFromServiceToken(workspaceId, environment, secretPath); } - if (folders && folderId !== "root") { + if (folderId !== "root") { + const folders = await Folder.findOne({ workspace: workspaceId, environment }); + if (!folders) throw BadRequestError({ message: "Folder not found" }); + const folder = searchByFolderIdWithDir(folders.nodes, folderId as string); if (!folder?.folder) throw BadRequestError({ message: "Folder not found" }); + secretPath = path.join( "/", ...folder.dir.map(({ name }) => name).filter((name) => name !== "root") ); } + if (req.authData.authPayload instanceof ServiceTokenData) { + await validateServiceTokenDataClientForWorkspace({ + serviceTokenData: req.authData.authPayload, + workspaceId: new Types.ObjectId(workspaceId), + environment, + secretPath, + requiredPermissions: [PERMISSION_WRITE_SECRETS] + }); + } + for await (const request of requests) { // do a validation @@ -110,7 +132,7 @@ export const batchSecrets = async (req: Request, res: Response) => { version: 1, user: request.secret.type === SECRET_PERSONAL ? req.user : undefined, environment, - workspace: new Types.ObjectId(workspaceId), + workspace: workspaceId, folder: folderId, secretBlindIndex, algorithm: ALGORITHM_AES_256_GCM, @@ -125,7 +147,7 @@ export const batchSecrets = async (req: Request, res: Response) => { updateSecrets.push({ ...request.secret, - _id: new Types.ObjectId(request.secret._id), + _id: request.secret._id, secretBlindIndex, folder: folderId, algorithm: ALGORITHM_AES_256_GCM, @@ -133,15 +155,39 @@ export const batchSecrets = async (req: Request, res: Response) => { }); break; case "DELETE": - deleteSecrets.push({ _id: new Types.ObjectId(request.secret._id), secretName: request.secret.secretName }); + deleteSecrets.push({ + _id: new Types.ObjectId(request.secret._id), + secretName: request.secret.secretName + }); break; } } + // not using service token using auth + if (!(req.authData.authPayload instanceof ServiceTokenData)) { + const { permission } = await getUserProjectPermissions(req.user._id, workspaceId); + if (createSecrets.length) + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Create, + subject(ProjectPermissionSub.Secrets, { environment, secretPath }) + ); + + if (updateSecrets.length) + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Edit, + subject(ProjectPermissionSub.Secrets, { environment, secretPath }) + ); + + if (deleteSecrets.length) + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Delete, + subject(ProjectPermissionSub.Secrets, { environment, secretPath }) + ); + } // handle create secrets let createdSecrets: ISecret[] = []; if (createSecrets.length > 0) { - createdSecrets = await Secret.insertMany(createSecrets); + createdSecrets = (await Secret.insertMany(createSecrets)) as any; // (EE) add secret versions for new secrets await EESecretService.addSecretVersions({ secretVersions: createdSecrets.map((n: any) => { @@ -206,7 +252,7 @@ export const batchSecrets = async (req: Request, res: Response) => { // handle update secrets let updatedSecrets: ISecret[] = []; - if (updateSecrets.length > 0 && req.secrets) { + if (updateSecrets.length > 0 && oldSecrets) { // construct object containing all secrets let listedSecretsObj: { [key: string]: { @@ -215,7 +261,7 @@ export const batchSecrets = async (req: Request, res: Response) => { }; } = {}; - listedSecretsObj = req.secrets.reduce( + listedSecretsObj = oldSecrets.reduce( (obj: any, secret: ISecret) => ({ ...obj, [secret._id.toString()]: secret @@ -227,7 +273,8 @@ export const batchSecrets = async (req: Request, res: Response) => { updateOne: { filter: { _id: new Types.ObjectId(u._id), - workspace: new Types.ObjectId(workspaceId) + workspace: new Types.ObjectId(workspaceId), + environment }, update: { $inc: { @@ -241,7 +288,6 @@ export const batchSecrets = async (req: Request, res: Response) => { } } })); - await Secret.bulkWrite(updateOperations); const secretVersions = updateSecrets.map( @@ -332,23 +378,26 @@ export const batchSecrets = async (req: Request, res: Response) => { if (deleteSecrets.length > 0) { const deleteSecretIds: Types.ObjectId[] = deleteSecrets.map((s) => s._id); - const deletedSecretsObj = (await Secret.find({ - _id: { - $in: deleteSecretIds - } - })) - .reduce( - (obj: any, secret: ISecret) => ({ - ...obj, - [secret._id.toString()]: secret - }), - {} - ); + const deletedSecretsObj = ( + await Secret.find({ + _id: { + $in: deleteSecretIds + } + }) + ).reduce( + (obj: any, secret: ISecret) => ({ + ...obj, + [secret._id.toString()]: secret + }), + {} + ); await Secret.deleteMany({ _id: { $in: deleteSecretIds - } + }, + workspace: new Types.ObjectId(workspaceId), + environment }); await EESecretService.markDeletedSecretVersions({ @@ -781,10 +830,13 @@ export const getSecrets = async (req: Request, res: Response) => { } */ - const { tagSlugs, secretPath, include_imports } = req.query; - let { folderId } = req.query; - const workspaceId = req.query.workspaceId as string; - const environment = req.query.environment as string; + const validatedData = await validateRequest(GetSecretsV2, req); + const { + query: { tagSlugs, secretPath, include_imports, workspaceId, environment } + } = validatedData; + let { + query: { folderId } + } = validatedData; const folders = await Folder.findOne({ workspace: workspaceId, environment }); @@ -926,7 +978,7 @@ export const getSecrets = async (req: Request, res: Response) => { // TODO(akhilmhdh) - secret-imp change this to org type let importedSecrets: any[] = []; - if (include_imports === "true") { + if (include_imports) { importedSecrets = await getAllImportedSecrets(workspaceId, environment, folderId as string); } @@ -970,17 +1022,17 @@ export const getSecrets = async (req: Request, res: Response) => { const postHogClient = await TelemetryService.getPostHogClient(); // reduce the number of events captured - let shouldRecordK8Event = false + let shouldRecordK8Event = false; if (req.authData.userAgent == K8_USER_AGENT_NAME) { const randomNumber = Math.random(); if (randomNumber > 0.9) { - shouldRecordK8Event = true + shouldRecordK8Event = true; } } if (postHogClient) { const shouldCapture = req.authData.userAgent !== K8_USER_AGENT_NAME || shouldRecordK8Event; - const approximateForNoneCapturedEvents = secrets.length * 10 + const approximateForNoneCapturedEvents = secrets.length * 10; if (shouldCapture) { postHogClient.capture({ @@ -1104,10 +1156,10 @@ export const updateSecrets = async (req: Request, res: Response) => { tags, ...(secretCommentCiphertext !== undefined && secretCommentIV && secretCommentTag ? { - secretCommentCiphertext, - secretCommentIV, - secretCommentTag - } + secretCommentCiphertext, + secretCommentIV, + secretCommentTag + } : {}) } } diff --git a/backend/src/controllers/v2/serviceTokenDataController.ts b/backend/src/controllers/v2/serviceTokenDataController.ts index c918e6953..b8f1b8f43 100644 --- a/backend/src/controllers/v2/serviceTokenDataController.ts +++ b/backend/src/controllers/v2/serviceTokenDataController.ts @@ -6,6 +6,15 @@ import { getSaltRounds } from "../../config"; import { BadRequestError } from "../../utils/errors"; import { ActorType, EventType } from "../../ee/models"; import { EEAuditLogService } from "../../ee/services"; +import { validateRequest } from "../../helpers/validation"; +import * as reqValidator from "../../validation/serviceTokenData"; +import { + ProjectPermissionActions, + ProjectPermissionSub, + getUserProjectPermissions +} from "../../ee/services/ProjectRoleService"; +import { ForbiddenError } from "@casl/ability"; +import { Types } from "mongoose"; /** * Return service token data associated with service token on request @@ -63,7 +72,14 @@ export const getServiceTokenData = async (req: Request, res: Response) => { export const createServiceTokenData = async (req: Request, res: Response) => { let serviceTokenData; - const { name, workspaceId, encryptedKey, iv, tag, expiresIn, permissions, scopes } = req.body; + const { + body: { workspaceId, permissions, tag, encryptedKey, scopes, name, expiresIn, iv } + } = await validateRequest(reqValidator.CreateServiceTokenV2, req); + const { permission } = await getUserProjectPermissions(req.user._id, workspaceId); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Create, + ProjectPermissionSub.ServiceTokens + ); const secret = crypto.randomBytes(16).toString("hex"); const secretHash = await bcrypt.hash(secret, await getSaltRounds()); @@ -75,7 +91,7 @@ export const createServiceTokenData = async (req: Request, res: Response) => { } let user; - + if (req.authData.actor.type === ActorType.USER) { user = req.authData.authPayload._id; } @@ -100,7 +116,7 @@ export const createServiceTokenData = async (req: Request, res: Response) => { if (!serviceTokenData) throw new Error("Failed to find service token data"); const serviceToken = `st.${serviceTokenData._id.toString()}.${secret}`; - + await EEAuditLogService.createAuditLog( req.authData, { @@ -111,7 +127,7 @@ export const createServiceTokenData = async (req: Request, res: Response) => { } }, { - workspaceId + workspaceId: new Types.ObjectId(workspaceId) } ); @@ -128,14 +144,29 @@ export const createServiceTokenData = async (req: Request, res: Response) => { * @returns */ export const deleteServiceTokenData = async (req: Request, res: Response) => { - const { serviceTokenDataId } = req.params; + const { + params: { serviceTokenDataId } + } = await validateRequest(reqValidator.DeleteServiceTokenV2, req); + + let serviceTokenData = await ServiceTokenData.findById(serviceTokenDataId); + if (!serviceTokenData) throw BadRequestError({ message: "Service token not found" }); + + const { permission } = await getUserProjectPermissions( + req.user._id, + serviceTokenData.workspace.toString() + ); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Delete, + ProjectPermissionSub.ServiceTokens + ); + + serviceTokenData = await ServiceTokenData.findByIdAndDelete(serviceTokenDataId); + + if (!serviceTokenData) + return res.status(200).send({ + message: "Failed to delete service token" + }); - const serviceTokenData = await ServiceTokenData.findByIdAndDelete(serviceTokenDataId); - - if (!serviceTokenData) return res.status(200).send({ - message: "Failed to delete service token" - }); - await EEAuditLogService.createAuditLog( req.authData, { diff --git a/backend/src/controllers/v2/tagController.ts b/backend/src/controllers/v2/tagController.ts index 4a7e68bb7..668903222 100644 --- a/backend/src/controllers/v2/tagController.ts +++ b/backend/src/controllers/v2/tagController.ts @@ -1,42 +1,58 @@ +import { ForbiddenError } from "@casl/ability"; import { Request, Response } from "express"; import { Types } from "mongoose"; -import { Membership, Secret, Tag } from "../../models"; -import { BadRequestError, UnauthorizedRequestError } from "../../utils/errors"; +import { Secret, Tag } from "../../models"; +import { BadRequestError } from "../../utils/errors"; +import { validateRequest } from "../../helpers/validation"; +import { + ProjectPermissionActions, + ProjectPermissionSub, + getUserProjectPermissions +} from "../../ee/services/ProjectRoleService"; +import * as reqValidator from "../../validation/tags"; export const createWorkspaceTag = async (req: Request, res: Response) => { - const { workspaceId } = req.params; - const { name, slug, tagColor } = req.body; - + const { + body: { name, slug }, + params: { workspaceId } + } = await validateRequest(reqValidator.CreateWorkspaceTagsV2, req); + + const { permission } = await getUserProjectPermissions(req.user._id, workspaceId); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Create, + ProjectPermissionSub.Tags + ); + const tagToCreate = { - name, - tagColor, - workspace: new Types.ObjectId(workspaceId), - slug, - user: new Types.ObjectId(req.user._id), - }; - + name, + workspace: new Types.ObjectId(workspaceId), + slug, + user: new Types.ObjectId(req.user._id) + }; + const createdTag = await new Tag(tagToCreate).save(); - + res.json(createdTag); }; export const deleteWorkspaceTag = async (req: Request, res: Response) => { - const { tagId } = req.params; + const { + params: { tagId } + } = await validateRequest(reqValidator.DeleteWorkspaceTagsV2, req); const tagFromDB = await Tag.findById(tagId); if (!tagFromDB) { throw BadRequestError(); } - // can only delete if the request user is one that belongs to the same workspace as the tag - const membership = await Membership.findOne({ - user: req.user, - workspace: tagFromDB.workspace - }); - - if (!membership) { - UnauthorizedRequestError({ message: "Failed to validate membership" }); - } + const { permission } = await getUserProjectPermissions( + req.user._id, + tagFromDB.workspace.toString() + ); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Delete, + ProjectPermissionSub.Tags + ); const result = await Tag.findByIdAndDelete(tagId); @@ -47,12 +63,19 @@ export const deleteWorkspaceTag = async (req: Request, res: Response) => { }; export const getWorkspaceTags = async (req: Request, res: Response) => { - const { workspaceId } = req.params; - - const workspaceTags = await Tag.find({ - workspace: new Types.ObjectId(workspaceId) + const { + params: { workspaceId } + } = await validateRequest(reqValidator.GetWorkspaceTagsV2, req); + const { permission } = await getUserProjectPermissions(req.user._id, workspaceId); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Read, + ProjectPermissionSub.Tags + ); + + const workspaceTags = await Tag.find({ + workspace: new Types.ObjectId(workspaceId) }); - + return res.json({ workspaceTags }); diff --git a/backend/src/controllers/v2/usersController.ts b/backend/src/controllers/v2/usersController.ts index 9e23ab8ac..0b859f749 100644 --- a/backend/src/controllers/v2/usersController.ts +++ b/backend/src/controllers/v2/usersController.ts @@ -2,23 +2,19 @@ import { Request, Response } from "express"; import { Types } from "mongoose"; import crypto from "crypto"; import bcrypt from "bcrypt"; -import { - APIKeyData, - AuthMethod, - MembershipOrg, - TokenVersion, - User -} from "../../models"; +import { APIKeyData, AuthMethod, MembershipOrg, TokenVersion, User } from "../../models"; import { getSaltRounds } from "../../config"; +import { validateRequest } from "../../helpers/validation"; +import * as reqValidator from "../../validation"; /** * Return the current user. - * @param req - * @param res - * @returns + * @param req + * @param res + * @returns */ export const getMe = async (req: Request, res: Response) => { - /* + /* #swagger.summary = "Retrieve the current user on the request" #swagger.description = "Retrieve the current user on the request" @@ -43,124 +39,117 @@ export const getMe = async (req: Request, res: Response) => { } } */ - const user = await User - .findById(req.user._id) - .select("+salt +publicKey +encryptedPrivateKey +iv +tag +encryptionVersion +protectedKey +protectedKeyIV +protectedKeyTag"); - - return res.status(200).send({ - user, - }); -} + const user = await User.findById(req.user._id).select( + "+salt +publicKey +encryptedPrivateKey +iv +tag +encryptionVersion +protectedKey +protectedKeyIV +protectedKeyTag" + ); + + return res.status(200).send({ + user + }); +}; /** * Update the current user's MFA-enabled status [isMfaEnabled]. * Note: Infisical currently only supports email-based 2FA only; this will expand to * include SMS and authenticator app modes of authentication in the future. - * @param req - * @param res - * @returns + * @param req + * @param res + * @returns */ export const updateMyMfaEnabled = async (req: Request, res: Response) => { - const { isMfaEnabled }: { isMfaEnabled: boolean } = req.body; - req.user.isMfaEnabled = isMfaEnabled; - - if (isMfaEnabled) { - // TODO: adapt this route/controller - // to work for different forms of MFA - req.user.mfaMethods = ["email"]; - } else { - req.user.mfaMethods = []; - } + const { + body: { isMfaEnabled } + } = await validateRequest(reqValidator.UpdateMyMfaEnabledV2, req); - await req.user.save(); - - const user = req.user; - - return res.status(200).send({ - user, - }); -} + req.user.isMfaEnabled = isMfaEnabled; + + if (isMfaEnabled) { + // TODO: adapt this route/controller + // to work for different forms of MFA + req.user.mfaMethods = ["email"]; + } else { + req.user.mfaMethods = []; + } + + await req.user.save(); + + const user = req.user; + + return res.status(200).send({ + user + }); +}; /** * Update name of the current user to [firstName, lastName]. - * @param req - * @param res - * @returns + * @param req + * @param res + * @returns */ export const updateName = async (req: Request, res: Response) => { - const { - firstName, - lastName - }: { - firstName: string; - lastName: string; - } = req.body; - - const user = await User.findByIdAndUpdate( - req.user._id.toString(), - { - firstName, - lastName: lastName ?? "" - }, - { - new: true - } - ); - - return res.status(200).send({ - user, - }); -} + const { + body: { lastName, firstName } + } = await validateRequest(reqValidator.UpdateNameV2, req); + + const user = await User.findByIdAndUpdate( + req.user._id.toString(), + { + firstName, + lastName: lastName ?? "" + }, + { + new: true + } + ); + + return res.status(200).send({ + user + }); +}; /** * Update auth method of the current user to [authMethods] - * @param req - * @param res - * @returns + * @param req + * @param res + * @returns */ - export const updateAuthMethods = async (req: Request, res: Response) => { - const { - authMethods - } = req.body; - - const hasSamlEnabled = req.user.authMethods - .some( - (authMethod: AuthMethod) => [ - AuthMethod.OKTA_SAML, - AuthMethod.AZURE_SAML, - AuthMethod.JUMPCLOUD_SAML - ].includes(authMethod) - ); +export const updateAuthMethods = async (req: Request, res: Response) => { + const { + body: { authMethods } + } = await validateRequest(reqValidator.UpdateAuthMethodsV2, req); - if (hasSamlEnabled) { - return res.status(400).send({ - message: "Failed to update user authentication method because SAML SSO is enforced" - }); - } + const hasSamlEnabled = req.user.authMethods.some((authMethod: AuthMethod) => + [AuthMethod.OKTA_SAML, AuthMethod.AZURE_SAML, AuthMethod.JUMPCLOUD_SAML].includes(authMethod) + ); - const user = await User.findByIdAndUpdate( - req.user._id.toString(), - { - authMethods - }, - { - new: true - } - ); - - return res.status(200).send({ - user + if (hasSamlEnabled) { + return res.status(400).send({ + message: "Failed to update user authentication method because SAML SSO is enforced" }); -} + } + const user = await User.findByIdAndUpdate( + req.user._id.toString(), + { + authMethods + }, + { + new: true + } + ); + + return res.status(200).send({ + user + }); +}; /** * Return organizations that the current user is part of. - * @param req - * @param res + * @param req + * @param res */ export const getMyOrganizations = async (req: Request, res: Response) => { - /* + /* #swagger.summary = 'Return organizations that current user is part of' #swagger.description = 'Return organizations that current user is part of' @@ -189,114 +178,121 @@ export const getMyOrganizations = async (req: Request, res: Response) => { */ const organizations = ( await MembershipOrg.find({ - user: req.user._id, + user: req.user._id }).populate("organization") ).map((m) => m.organization); - return res.status(200).send({ - organizations, - }); -} + return res.status(200).send({ + organizations + }); +}; /** * Return API keys belonging to current user. - * @param req - * @param res - * @returns + * @param req + * @param res + * @returns */ export const getMyAPIKeys = async (req: Request, res: Response) => { - const apiKeyData = await APIKeyData.find({ - user: req.user._id, - }); + const apiKeyData = await APIKeyData.find({ + user: req.user._id + }); - return res.status(200).send(apiKeyData); -} + return res.status(200).send(apiKeyData); +}; /** * Create new API key for current user. - * @param req - * @param res - * @returns + * @param req + * @param res + * @returns */ export const createAPIKey = async (req: Request, res: Response) => { - const { name, expiresIn } = req.body; + const { + body: { name, expiresIn } + } = await validateRequest(reqValidator.CreateApiKeyV2, req); - const secret = crypto.randomBytes(16).toString("hex"); - const secretHash = await bcrypt.hash(secret, await getSaltRounds()); + const secret = crypto.randomBytes(16).toString("hex"); + const secretHash = await bcrypt.hash(secret, await getSaltRounds()); - const expiresAt = new Date(); - expiresAt.setSeconds(expiresAt.getSeconds() + expiresIn); + const expiresAt = new Date(); + expiresAt.setSeconds(expiresAt.getSeconds() + expiresIn); - let apiKeyData = await new APIKeyData({ - name, - lastUsed: new Date(), - expiresAt, - user: req.user._id, - secretHash, - }).save(); + let apiKeyData = await new APIKeyData({ + name, + lastUsed: new Date(), + expiresAt, + user: req.user._id, + secretHash + }).save(); - // return api key data without sensitive data - apiKeyData = (await APIKeyData.findById(apiKeyData._id)) as any; + // return api key data without sensitive data + apiKeyData = (await APIKeyData.findById(apiKeyData._id)) as any; - if (!apiKeyData) throw new Error("Failed to find API key data"); + if (!apiKeyData) throw new Error("Failed to find API key data"); - const apiKey = `ak.${apiKeyData._id.toString()}.${secret}`; + const apiKey = `ak.${apiKeyData._id.toString()}.${secret}`; - return res.status(200).send({ - apiKey, - apiKeyData, - }); -} + return res.status(200).send({ + apiKey, + apiKeyData + }); +}; /** * Delete API key with id [apiKeyDataId] belonging to current user - * @param req - * @param res + * @param req + * @param res */ export const deleteAPIKey = async (req: Request, res: Response) => { - const { apiKeyDataId } = req.params; + const { + params: { apiKeyDataId } + } = await validateRequest(reqValidator.DeleteApiKeyV2, req); - const apiKeyData = await APIKeyData.findOneAndDelete({ - _id: new Types.ObjectId(apiKeyDataId), - user: req.user._id - }); + const apiKeyData = await APIKeyData.findOneAndDelete({ + _id: new Types.ObjectId(apiKeyDataId), + user: req.user._id + }); - return res.status(200).send({ - apiKeyData - }); -} + return res.status(200).send({ + apiKeyData + }); +}; /** * Return active sessions (TokenVersion) belonging to user - * @param req - * @param res - * @returns + * @param req + * @param res + * @returns */ export const getMySessions = async (req: Request, res: Response) => { - const tokenVersions = await TokenVersion.find({ - user: req.user._id - }); - - return res.status(200).send(tokenVersions); -} + const tokenVersions = await TokenVersion.find({ + user: req.user._id + }); + + return res.status(200).send(tokenVersions); +}; /** * Revoke all active sessions belong to user - * @param req - * @param res - * @returns + * @param req + * @param res + * @returns */ export const deleteMySessions = async (req: Request, res: Response) => { - await TokenVersion.updateMany({ - user: req.user._id, - }, { - $inc: { - refreshVersion: 1, - accessVersion: 1, - }, - }); + await TokenVersion.updateMany( + { + user: req.user._id + }, + { + $inc: { + refreshVersion: 1, + accessVersion: 1 + } + } + ); - return res.status(200).send({ - message: "Successfully revoked all sessions" - }); -} \ No newline at end of file + return res.status(200).send({ + message: "Successfully revoked all sessions" + }); +}; diff --git a/backend/src/controllers/v2/workspaceController.ts b/backend/src/controllers/v2/workspaceController.ts index f057298be..040c68f31 100644 --- a/backend/src/controllers/v2/workspaceController.ts +++ b/backend/src/controllers/v2/workspaceController.ts @@ -11,6 +11,14 @@ import { EventService, TelemetryService } from "../../services"; import { eventPushSecrets } from "../../events"; import { EEAuditLogService } from "../../ee/services"; import { EventType } from "../../ee/models"; +import { validateRequest } from "../../helpers/validation"; +import * as reqValidator from "../../validation"; +import { + ProjectPermissionActions, + ProjectPermissionSub, + getUserProjectPermissions +} from "../../ee/services/ProjectRoleService"; +import { ForbiddenError } from "@casl/ability"; interface V2PushSecret { type: string; // personal or shared @@ -181,15 +189,17 @@ export const getWorkspaceKey = async (req: Request, res: Response) => { } } */ - const { workspaceId } = req.params; - + const { + params: { workspaceId } + } = await validateRequest(reqValidator.GetWorkspaceKeyV2, req); + const key = await Key.findOne({ workspace: workspaceId, receiver: req.user._id }).populate("sender", "+publicKey"); if (!key) throw new Error("Failed to find workspace key"); - + await EEAuditLogService.createAuditLog( req.authData, { @@ -258,7 +268,15 @@ export const getWorkspaceMemberships = async (req: Request, res: Response) => { } } */ - const { workspaceId } = req.params; + const { + params: { workspaceId } + } = await validateRequest(reqValidator.GetWorkspaceMembershipsV2, req); + + const { permission } = await getUserProjectPermissions(req.user._id, workspaceId); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Read, + ProjectPermissionSub.Member + ); const memberships = await Membership.find({ workspace: workspaceId @@ -329,8 +347,16 @@ export const updateWorkspaceMembership = async (req: Request, res: Response) => } } */ - const { membershipId } = req.params; - const { role } = req.body; + const { + params: { workspaceId, membershipId }, + body: { role } + } = await validateRequest(reqValidator.UpdateWorkspaceMembershipsV2, req); + + const { permission } = await getUserProjectPermissions(req.user._id, workspaceId); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Edit, + ProjectPermissionSub.Member + ); const membership = await Membership.findByIdAndUpdate( membershipId, @@ -390,7 +416,15 @@ export const deleteWorkspaceMembership = async (req: Request, res: Response) => } } */ - const { membershipId } = req.params; + const { + params: { workspaceId, membershipId } + } = await validateRequest(reqValidator.DeleteWorkspaceMembershipsV2, req); + + const { permission } = await getUserProjectPermissions(req.user._id, workspaceId); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Delete, + ProjectPermissionSub.Member + ); const membership = await Membership.findByIdAndDelete(membershipId); @@ -413,8 +447,16 @@ export const deleteWorkspaceMembership = async (req: Request, res: Response) => * @returns */ export const toggleAutoCapitalization = async (req: Request, res: Response) => { - const { workspaceId } = req.params; - const { autoCapitalization } = req.body; + const { + params: { workspaceId }, + body: { autoCapitalization } + } = await validateRequest(reqValidator.ToggleAutoCapitalizationV2, req); + + const { permission } = await getUserProjectPermissions(req.user._id, workspaceId); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Edit, + ProjectPermissionSub.Settings + ); const workspace = await Workspace.findOneAndUpdate( { diff --git a/backend/src/controllers/v3/authController.ts b/backend/src/controllers/v3/authController.ts index 413bcefa0..1b228a39b 100644 --- a/backend/src/controllers/v3/authController.ts +++ b/backend/src/controllers/v3/authController.ts @@ -10,25 +10,20 @@ import { sendMail } from "../../helpers/nodemailer"; import { TokenService } from "../../services"; import { EELogService } from "../../ee/services"; import { BadRequestError, InternalServerError } from "../../utils/errors"; -import { - ACTION_LOGIN, - TOKEN_EMAIL_MFA, -} from "../../variables"; +import { ACTION_LOGIN, TOKEN_EMAIL_MFA } from "../../variables"; import { getUserAgentType } from "../../utils/posthog"; // TODO: move this -import { - getHttpsEnabled, - getJwtMfaLifetime, - getJwtMfaSecret, -} from "../../config"; +import { getHttpsEnabled, getJwtMfaLifetime, getJwtMfaSecret } from "../../config"; import { AuthMethod } from "../../models/user"; +import { validateRequest } from "../../helpers/validation"; +import * as reqValidator from "../../validation/auth"; declare module "jsonwebtoken" { - export interface ProviderAuthJwtPayload extends jwt.JwtPayload { - userId: string; - email: string; - authProvider: AuthMethod; - isUserCompleted: boolean, - } + export interface ProviderAuthJwtPayload extends jwt.JwtPayload { + userId: string; + email: string; + authProvider: AuthMethod; + isUserCompleted: boolean; + } } /** @@ -38,53 +33,51 @@ declare module "jsonwebtoken" { * @returns */ export const login1 = async (req: Request, res: Response) => { - const { - email, - providerAuthToken, - clientPublicKey, - }: { - email: string; - clientPublicKey: string, - providerAuthToken?: string; - } = req.body; - - const user = await User.findOne({ - email, - }).select("+salt +verifier"); + const { + body: { email, clientPublicKey, providerAuthToken } + } = await validateRequest(reqValidator.Login1V3, req); - if (!user) throw new Error("Failed to find user"); - - if (!user.authMethods.includes(AuthMethod.EMAIL)) { - await validateProviderAuthToken({ - email, - providerAuthToken, - }); - } + const user = await User.findOne({ + email + }).select("+salt +verifier"); - const server = new jsrp.server(); - server.init( + if (!user) throw new Error("Failed to find user"); + + if (!user.authMethods.includes(AuthMethod.EMAIL)) { + await validateProviderAuthToken({ + email, + providerAuthToken + }); + } + + const server = new jsrp.server(); + server.init( + { + salt: user.salt, + verifier: user.verifier + }, + async () => { + // generate server-side public key + const serverPublicKey = server.getPublicKey(); + await LoginSRPDetail.findOneAndReplace( { - salt: user.salt, - verifier: user.verifier, + email: email }, - async () => { - // generate server-side public key - const serverPublicKey = server.getPublicKey(); - await LoginSRPDetail.findOneAndReplace({ - email: email, - }, { - email, - userId: user.id, - clientPublicKey: clientPublicKey, - serverBInt: bigintConversion.bigintToBuf(server.bInt), - }, { upsert: true, returnNewDocument: false }); + { + email, + userId: user.id, + clientPublicKey: clientPublicKey, + serverBInt: bigintConversion.bigintToBuf(server.bInt) + }, + { upsert: true, returnNewDocument: false } + ); - return res.status(200).send({ - serverPublicKey, - salt: user.salt, - }); - } - ); + return res.status(200).send({ + serverPublicKey, + salt: user.salt + }); + } + ); }; /** @@ -95,150 +88,151 @@ export const login1 = async (req: Request, res: Response) => { * @returns */ export const login2 = async (req: Request, res: Response) => { - if (!req.headers["user-agent"]) throw InternalServerError({ message: "User-Agent header is required" }); + if (!req.headers["user-agent"]) + throw InternalServerError({ message: "User-Agent header is required" }); - const { email, clientProof, providerAuthToken } = req.body; + const { + body: { email, providerAuthToken, clientProof } + } = await validateRequest(reqValidator.Login2V3, req); - const user = await User.findOne({ - email, - }).select("+salt +verifier +encryptionVersion +protectedKey +protectedKeyIV +protectedKeyTag +publicKey +encryptedPrivateKey +iv +tag +devices"); + const user = await User.findOne({ + email + }).select( + "+salt +verifier +encryptionVersion +protectedKey +protectedKeyIV +protectedKeyTag +publicKey +encryptedPrivateKey +iv +tag +devices" + ); - if (!user) throw new Error("Failed to find user"); - - if (!user.authMethods.includes(AuthMethod.EMAIL)) { - await validateProviderAuthToken({ - email, - providerAuthToken, - }) - } + if (!user) throw new Error("Failed to find user"); - const loginSRPDetail = await LoginSRPDetail.findOneAndDelete({ email: email }) + if (!user.authMethods.includes(AuthMethod.EMAIL)) { + await validateProviderAuthToken({ + email, + providerAuthToken + }); + } - if (!loginSRPDetail) { - return BadRequestError(Error("Failed to find login details for SRP")) - } + const loginSRPDetail = await LoginSRPDetail.findOneAndDelete({ email: email }); - const server = new jsrp.server(); - server.init( - { - salt: user.salt, - verifier: user.verifier, - b: loginSRPDetail.serverBInt, - }, - async () => { - server.setClientPublicKey(loginSRPDetail.clientPublicKey); + if (!loginSRPDetail) { + return BadRequestError(Error("Failed to find login details for SRP")); + } - // compare server and client shared keys - if (server.checkClientProof(clientProof)) { + const server = new jsrp.server(); + server.init( + { + salt: user.salt, + verifier: user.verifier, + b: loginSRPDetail.serverBInt + }, + async () => { + server.setClientPublicKey(loginSRPDetail.clientPublicKey); - if (user.isMfaEnabled) { - // case: user has MFA enabled + // compare server and client shared keys + if (server.checkClientProof(clientProof)) { + if (user.isMfaEnabled) { + // case: user has MFA enabled - // generate temporary MFA token - const token = createToken({ - payload: { - userId: user._id.toString(), - }, - expiresIn: await getJwtMfaLifetime(), - secret: await getJwtMfaSecret(), - }); + // generate temporary MFA token + const token = createToken({ + payload: { + userId: user._id.toString() + }, + expiresIn: await getJwtMfaLifetime(), + secret: await getJwtMfaSecret() + }); - const code = await TokenService.createToken({ - type: TOKEN_EMAIL_MFA, - email, - }); + const code = await TokenService.createToken({ + type: TOKEN_EMAIL_MFA, + email + }); - // send MFA code [code] to [email] - await sendMail({ - template: "emailMfa.handlebars", - subjectLine: "Infisical MFA code", - recipients: [user.email], - substitutions: { - code, - }, - }); - - return res.status(200).send({ - mfaEnabled: true, - token, - }); - } - - await checkUserDevice({ - user, - ip: req.realIP, - userAgent: req.headers["user-agent"] ?? "", - }); - - // issue tokens - const tokens = await issueAuthTokens({ - userId: user._id, - ip: req.realIP, - userAgent: req.headers["user-agent"] ?? "", - }); - - // store (refresh) token in httpOnly cookie - res.cookie("jid", tokens.refreshToken, { - httpOnly: true, - path: "/", - sameSite: "strict", - secure: await getHttpsEnabled(), - }); - - // case: user does not have MFA enablgged - // return (access) token in response - - interface ResponseData { - mfaEnabled: boolean; - encryptionVersion: any; - protectedKey?: string; - protectedKeyIV?: string; - protectedKeyTag?: string; - token: string; - publicKey?: string; - encryptedPrivateKey?: string; - iv?: string; - tag?: string; - } - - const response: ResponseData = { - mfaEnabled: false, - encryptionVersion: user.encryptionVersion, - token: tokens.token, - publicKey: user.publicKey, - encryptedPrivateKey: user.encryptedPrivateKey, - iv: user.iv, - tag: user.tag, - } - - if ( - user?.protectedKey && - user?.protectedKeyIV && - user?.protectedKeyTag - ) { - response.protectedKey = user.protectedKey; - response.protectedKeyIV = user.protectedKeyIV - response.protectedKeyTag = user.protectedKeyTag; - } - - const loginAction = await EELogService.createAction({ - name: ACTION_LOGIN, - userId: user._id, - }); - - loginAction && await EELogService.createLog({ - userId: user._id, - actions: [loginAction], - channel: getUserAgentType(req.headers["user-agent"]), - ipAddress: req.realIP, - }); - - return res.status(200).send(response); + // send MFA code [code] to [email] + await sendMail({ + template: "emailMfa.handlebars", + subjectLine: "Infisical MFA code", + recipients: [user.email], + substitutions: { + code } + }); - return res.status(400).send({ - message: "Failed to authenticate. Try again?", - }); + return res.status(200).send({ + mfaEnabled: true, + token + }); } - ); + + await checkUserDevice({ + user, + ip: req.realIP, + userAgent: req.headers["user-agent"] ?? "" + }); + + // issue tokens + const tokens = await issueAuthTokens({ + userId: user._id, + ip: req.realIP, + userAgent: req.headers["user-agent"] ?? "" + }); + + // store (refresh) token in httpOnly cookie + res.cookie("jid", tokens.refreshToken, { + httpOnly: true, + path: "/", + sameSite: "strict", + secure: await getHttpsEnabled() + }); + + // case: user does not have MFA enablgged + // return (access) token in response + + interface ResponseData { + mfaEnabled: boolean; + encryptionVersion: any; + protectedKey?: string; + protectedKeyIV?: string; + protectedKeyTag?: string; + token: string; + publicKey?: string; + encryptedPrivateKey?: string; + iv?: string; + tag?: string; + } + + const response: ResponseData = { + mfaEnabled: false, + encryptionVersion: user.encryptionVersion, + token: tokens.token, + publicKey: user.publicKey, + encryptedPrivateKey: user.encryptedPrivateKey, + iv: user.iv, + tag: user.tag + }; + + if (user?.protectedKey && user?.protectedKeyIV && user?.protectedKeyTag) { + response.protectedKey = user.protectedKey; + response.protectedKeyIV = user.protectedKeyIV; + response.protectedKeyTag = user.protectedKeyTag; + } + + const loginAction = await EELogService.createAction({ + name: ACTION_LOGIN, + userId: user._id + }); + + loginAction && + (await EELogService.createLog({ + userId: user._id, + actions: [loginAction], + channel: getUserAgentType(req.headers["user-agent"]), + ipAddress: req.realIP + })); + + return res.status(200).send(response); + } + + return res.status(400).send({ + message: "Failed to authenticate. Try again?" + }); + } + ); }; diff --git a/backend/src/controllers/v3/secretsController.ts b/backend/src/controllers/v3/secretsController.ts index 4a57beba3..c984d37b2 100644 --- a/backend/src/controllers/v3/secretsController.ts +++ b/backend/src/controllers/v3/secretsController.ts @@ -7,10 +7,18 @@ import { containsGlobPatterns, repackageSecretToRaw } from "../../helpers/secret import { encryptSymmetric128BitHexKeyUTF8 } from "../../utils/crypto"; import { getAllImportedSecrets } from "../../services/SecretImportService"; import { Folder, IServiceTokenData } from "../../models"; -import { getFolderByPath } from "../../services/FolderService"; +import { getFolderByPath, getFolderWithPathFromId } from "../../services/FolderService"; import { BadRequestError } from "../../utils/errors"; -import { requireWorkspaceAuth } from "../../middleware"; -import { ADMIN, MEMBER, PERMISSION_READ_SECRETS } from "../../variables"; +import { validateRequest } from "../../helpers/validation"; +import * as reqValidator from "../../validation/secrets"; +import { + ProjectPermissionActions, + ProjectPermissionSub, + getUserProjectPermissions +} from "../../ee/services/ProjectRoleService"; +import { ForbiddenError, subject } from "@casl/ability"; +import { validateServiceTokenDataClientForWorkspace } from "../../validation"; +import { PERMISSION_READ_SECRETS, PERMISSION_WRITE_SECRETS } from "../../variables"; /** * Return secrets for workspace with id [workspaceId] and environment @@ -19,30 +27,49 @@ import { ADMIN, MEMBER, PERMISSION_READ_SECRETS } from "../../variables"; * @param res */ export const getSecretsRaw = async (req: Request, res: Response) => { - let workspaceId = req.query.workspaceId as string; - let environment = req.query.environment as string; - let secretPath = req.query.secretPath as string; - const folderId = req.query.folderId as string | undefined; - const includeImports = req.query.include_imports as string; + const validatedData = await validateRequest(reqValidator.GetSecretsRawV3, req); + let { + query: { secretPath, environment, workspaceId } + } = validatedData; + const { + query: { folderId, include_imports: includeImports } + } = validatedData; // if the service token has single scope, it will get all secrets for that scope by default const serviceTokenDetails: IServiceTokenData = req?.serviceTokenData; - if (serviceTokenDetails && serviceTokenDetails.scopes.length == 1 && !containsGlobPatterns(serviceTokenDetails.scopes[0].secretPath)) { + if ( + serviceTokenDetails && + serviceTokenDetails.scopes.length == 1 && + !containsGlobPatterns(serviceTokenDetails.scopes[0].secretPath) + ) { const scope = serviceTokenDetails.scopes[0]; secretPath = scope.secretPath; environment = scope.environment; workspaceId = serviceTokenDetails.workspace.toString(); - } else { - requireWorkspaceAuth({ - acceptedRoles: [ADMIN, MEMBER], - locationWorkspaceId: "query", - locationEnvironment: "query", - requiredPermissions: [PERMISSION_READ_SECRETS], - requireBlindIndicesEnabled: true, - requireE2EEOff: true - }); } + 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 (req.user?._id) { + const { permission } = await getUserProjectPermissions(req.user._id, workspaceId); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Read, + subject(ProjectPermissionSub.Secrets, { environment, secretPath }) + ); + } else { + await validateServiceTokenDataClientForWorkspace({ + serviceTokenData: req.authData.authPayload as IServiceTokenData, + workspaceId: new Types.ObjectId(workspaceId), + environment, + secretPath, + requiredPermissions: [PERMISSION_READ_SECRETS] + }); + } const secrets = await SecretService.getSecrets({ workspaceId: new Types.ObjectId(workspaceId), @@ -56,7 +83,7 @@ export const getSecretsRaw = async (req: Request, res: Response) => { workspaceId: new Types.ObjectId(workspaceId) }); - if (includeImports === "true") { + if (includeImports) { const folders = await Folder.findOne({ workspace: workspaceId, environment }); let folderId = "root"; // if folder exist get it and replace folderid with new one @@ -99,11 +126,26 @@ export const getSecretsRaw = async (req: Request, res: Response) => { * @param res */ export const getSecretByNameRaw = async (req: Request, res: Response) => { - const { secretName } = req.params; - const workspaceId = req.query.workspaceId as string; - const environment = req.query.environment as string; - const secretPath = req.query.secretPath as string; - const type = req.query.type as "shared" | "personal" | undefined; + const { + query: { secretPath, environment, workspaceId, type }, + params: { secretName } + } = await validateRequest(reqValidator.GetSecretByNameRawV3, req); + + if (req.user?._id) { + const { permission } = await getUserProjectPermissions(req.user._id, workspaceId); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Read, + subject(ProjectPermissionSub.Secrets, { environment, secretPath }) + ); + } else { + await validateServiceTokenDataClientForWorkspace({ + serviceTokenData: req.authData.authPayload as IServiceTokenData, + workspaceId: new Types.ObjectId(workspaceId), + environment, + secretPath, + requiredPermissions: [PERMISSION_READ_SECRETS] + }); + } const secret = await SecretService.getSecret({ secretName, @@ -132,8 +174,26 @@ export const getSecretByNameRaw = async (req: Request, res: Response) => { * @param res */ export const createSecretRaw = async (req: Request, res: Response) => { - const { secretName } = req.params; - const { workspaceId, environment, type, secretValue, secretComment, secretPath = "/" } = req.body; + const { + params: { secretName }, + body: { secretPath, environment, workspaceId, type, secretValue, secretComment } + } = await validateRequest(reqValidator.CreateSecretRawV3, req); + + if (req.user?._id) { + const { permission } = await getUserProjectPermissions(req.user._id, workspaceId); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Create, + subject(ProjectPermissionSub.Secrets, { environment, secretPath }) + ); + } else { + await validateServiceTokenDataClientForWorkspace({ + serviceTokenData: req.authData.authPayload as IServiceTokenData, + workspaceId: new Types.ObjectId(workspaceId), + environment, + secretPath, + requiredPermissions: [PERMISSION_WRITE_SECRETS] + }); + } const key = await BotService.getWorkspaceKeyWithBot({ workspaceId: new Types.ObjectId(workspaceId) @@ -197,8 +257,26 @@ export const createSecretRaw = async (req: Request, res: Response) => { * @param res */ export const updateSecretByNameRaw = async (req: Request, res: Response) => { - const { secretName } = req.params; - const { workspaceId, environment, type, secretValue, secretPath = "/" } = req.body; + const { + params: { secretName }, + body: { secretValue, environment, secretPath, type, workspaceId } + } = await validateRequest(reqValidator.UpdateSecretByNameRawV3, req); + + if (req.user?._id) { + const { permission } = await getUserProjectPermissions(req.user._id, workspaceId); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Edit, + subject(ProjectPermissionSub.Secrets, { environment, secretPath }) + ); + } else { + await validateServiceTokenDataClientForWorkspace({ + serviceTokenData: req.authData.authPayload as IServiceTokenData, + workspaceId: new Types.ObjectId(workspaceId), + environment, + secretPath, + requiredPermissions: [PERMISSION_WRITE_SECRETS] + }); + } const key = await BotService.getWorkspaceKeyWithBot({ workspaceId: new Types.ObjectId(workspaceId) @@ -211,7 +289,7 @@ export const updateSecretByNameRaw = async (req: Request, res: Response) => { const secret = await SecretService.updateSecret({ secretName, - workspaceId, + workspaceId: new Types.ObjectId(workspaceId), environment, type, authData: req.authData, @@ -243,12 +321,30 @@ export const updateSecretByNameRaw = async (req: Request, res: Response) => { * @param res */ export const deleteSecretByNameRaw = async (req: Request, res: Response) => { - const { secretName } = req.params; - const { workspaceId, environment, type, secretPath = "/" } = req.body; + const { + params: { secretName }, + body: { environment, secretPath, type, workspaceId } + } = await validateRequest(reqValidator.DeleteSecretByNameRawV3, req); + + if (req.user?._id) { + const { permission } = await getUserProjectPermissions(req.user._id, workspaceId); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Delete, + subject(ProjectPermissionSub.Secrets, { environment, secretPath }) + ); + } else { + await validateServiceTokenDataClientForWorkspace({ + serviceTokenData: req.authData.authPayload as IServiceTokenData, + workspaceId: new Types.ObjectId(workspaceId), + environment, + secretPath, + requiredPermissions: [PERMISSION_WRITE_SECRETS] + }); + } const { secret } = await SecretService.deleteSecret({ secretName, - workspaceId, + workspaceId: new Types.ObjectId(workspaceId), environment, type, authData: req.authData, @@ -282,11 +378,37 @@ export const deleteSecretByNameRaw = async (req: Request, res: Response) => { * @param res */ export const getSecrets = async (req: Request, res: Response) => { - const workspaceId = req.query.workspaceId as string; - const environment = req.query.environment as string; - const secretPath = req.query.secretPath as string; - const folderId = req.query.folderId as string | undefined; - const includeImports = req.query.include_imports as string; + const validatedData = await validateRequest(reqValidator.GetSecretsV3, req); + const { + query: { environment, workspaceId, include_imports: includeImports, folderId } + } = validatedData; + + let { + query: { secretPath } + } = validatedData; + + 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 (req.user?._id) { + const { permission } = await getUserProjectPermissions(req.user._id, workspaceId); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Read, + subject(ProjectPermissionSub.Secrets, { environment, secretPath }) + ); + } else { + await validateServiceTokenDataClientForWorkspace({ + serviceTokenData: req.authData.authPayload as IServiceTokenData, + workspaceId: new Types.ObjectId(workspaceId), + environment, + secretPath, + requiredPermissions: [PERMISSION_READ_SECRETS] + }); + } const secrets = await SecretService.getSecrets({ workspaceId: new Types.ObjectId(workspaceId), @@ -296,7 +418,7 @@ export const getSecrets = async (req: Request, res: Response) => { authData: req.authData }); - if (includeImports === "true") { + if (includeImports) { const folders = await Folder.findOne({ workspace: workspaceId, environment }); let folderId = "root"; // if folder exist get it and replace folderid with new one @@ -325,11 +447,26 @@ export const getSecrets = async (req: Request, res: Response) => { * @param res */ export const getSecretByName = async (req: Request, res: Response) => { - const { secretName } = req.params; - const workspaceId = req.query.workspaceId as string; - const environment = req.query.environment as string; - const secretPath = req.query.secretPath as string; - const type = req.query.type as "shared" | "personal" | undefined; + const { + query: { secretPath, environment, workspaceId, type }, + params: { secretName } + } = await validateRequest(reqValidator.GetSecretByNameV3, req); + + if (req.user?._id) { + const { permission } = await getUserProjectPermissions(req.user._id, workspaceId); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Read, + subject(ProjectPermissionSub.Secrets, { environment, secretPath }) + ); + } else { + await validateServiceTokenDataClientForWorkspace({ + serviceTokenData: req.authData.authPayload as IServiceTokenData, + workspaceId: new Types.ObjectId(workspaceId), + environment, + secretPath, + requiredPermissions: [PERMISSION_READ_SECRETS] + }); + } const secret = await SecretService.getSecret({ secretName, @@ -351,23 +488,41 @@ export const getSecretByName = async (req: Request, res: Response) => { * @param res */ export const createSecret = async (req: Request, res: Response) => { - const { secretName } = req.params; const { - workspaceId, - environment, - type, - secretKeyCiphertext, - secretKeyIV, - secretKeyTag, - secretValueCiphertext, - secretValueIV, - secretValueTag, - secretCommentCiphertext, - secretCommentIV, - secretCommentTag, - secretPath = "/", - metadata - } = req.body; + body: { + workspaceId, + secretPath, + environment, + metadata, + type, + secretKeyIV, + secretKeyTag, + secretValueIV, + secretValueTag, + secretCommentIV, + secretCommentTag, + secretKeyCiphertext, + secretValueCiphertext, + secretCommentCiphertext + }, + params: { secretName } + } = await validateRequest(reqValidator.CreateSecretV3, req); + + if (req.user?._id) { + const { permission } = await getUserProjectPermissions(req.user._id, workspaceId); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Create, + subject(ProjectPermissionSub.Secrets, { environment, secretPath }) + ); + } else { + await validateServiceTokenDataClientForWorkspace({ + serviceTokenData: req.authData.authPayload as IServiceTokenData, + workspaceId: new Types.ObjectId(workspaceId), + environment, + secretPath, + requiredPermissions: [PERMISSION_WRITE_SECRETS] + }); + } const secret = await SecretService.createSecret({ secretName, @@ -410,20 +565,38 @@ export const createSecret = async (req: Request, res: Response) => { * @param res */ export const updateSecretByName = async (req: Request, res: Response) => { - const { secretName } = req.params; const { - workspaceId, - environment, - type, - secretValueCiphertext, - secretValueIV, - secretValueTag, - secretPath = "/" - } = req.body; + body: { + secretValueCiphertext, + secretValueTag, + secretValueIV, + type, + environment, + secretPath, + workspaceId + }, + params: { secretName } + } = await validateRequest(reqValidator.UpdateSecretByNameV3, req); + + if (req.user?._id) { + const { permission } = await getUserProjectPermissions(req.user._id, workspaceId); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Edit, + subject(ProjectPermissionSub.Secrets, { environment, secretPath }) + ); + } else { + await validateServiceTokenDataClientForWorkspace({ + serviceTokenData: req.authData.authPayload as IServiceTokenData, + workspaceId: new Types.ObjectId(workspaceId), + environment, + secretPath, + requiredPermissions: [PERMISSION_WRITE_SECRETS] + }); + } const secret = await SecretService.updateSecret({ secretName, - workspaceId, + workspaceId: new Types.ObjectId(workspaceId), environment, type, authData: req.authData, @@ -452,12 +625,30 @@ export const updateSecretByName = async (req: Request, res: Response) => { * @param res */ export const deleteSecretByName = async (req: Request, res: Response) => { - const { secretName } = req.params; - const { workspaceId, environment, type, secretPath = "/" } = req.body; + const { + body: { type, environment, secretPath, workspaceId }, + params: { secretName } + } = await validateRequest(reqValidator.DeleteSecretByNameV3, req); + + if (req.user?._id) { + const { permission } = await getUserProjectPermissions(req.user._id, workspaceId); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Delete, + subject(ProjectPermissionSub.Secrets, { environment, secretPath }) + ); + } else { + await validateServiceTokenDataClientForWorkspace({ + serviceTokenData: req.authData.authPayload as IServiceTokenData, + workspaceId: new Types.ObjectId(workspaceId), + environment, + secretPath, + requiredPermissions: [PERMISSION_WRITE_SECRETS] + }); + } const { secret } = await SecretService.deleteSecret({ secretName, - workspaceId, + workspaceId: new Types.ObjectId(workspaceId), environment, type, authData: req.authData, diff --git a/backend/src/controllers/v3/signupController.ts b/backend/src/controllers/v3/signupController.ts index 449df4e11..79d661b58 100644 --- a/backend/src/controllers/v3/signupController.ts +++ b/backend/src/controllers/v3/signupController.ts @@ -3,9 +3,7 @@ import { Request, Response } from "express"; import * as Sentry from "@sentry/node"; import { MembershipOrg, User } from "../../models"; import { completeAccount } from "../../helpers/user"; -import { - initializeDefaultOrg, -} from "../../helpers/signup"; +import { initializeDefaultOrg } from "../../helpers/signup"; import { issueAuthTokens, validateProviderAuthToken } from "../../helpers/auth"; import { ACCEPTED, INVITED } from "../../variables"; import { standardRequest } from "../../config/request"; @@ -13,6 +11,8 @@ import { getHttpsEnabled, getJwtSignupSecret, getLoopsApiKey } from "../../confi import { BadRequestError } from "../../utils/errors"; import { TelemetryService } from "../../services"; import { AuthMethod } from "../../models"; +import { validateRequest } from "../../helpers/validation"; +import * as reqValidator from "../../validation/auth"; /** * Complete setting up user by adding their personal and auth information as part of the @@ -22,177 +22,173 @@ import { AuthMethod } from "../../models"; * @returns */ export const completeAccountSignup = async (req: Request, res: Response) => { - let user, token, refreshToken; - try { - const { - email, - firstName, - lastName, - protectedKey, - protectedKeyIV, - protectedKeyTag, - publicKey, - encryptedPrivateKey, - encryptedPrivateKeyIV, - encryptedPrivateKeyTag, - salt, - verifier, - organizationName, - providerAuthToken, - attributionSource, - }: { - email: string; - firstName: string; - lastName: string; - protectedKey: string; - protectedKeyIV: string; - protectedKeyTag: string; - publicKey: string; - encryptedPrivateKey: string; - encryptedPrivateKeyIV: string; - encryptedPrivateKeyTag: string; - salt: string; - verifier: string; - organizationName: string; - providerAuthToken?: string; - attributionSource?: string; - } = req.body; + let user, token; + try { + const { + body: { + email, + publicKey, + salt, + lastName, + verifier, + firstName, + protectedKey, + protectedKeyIV, + protectedKeyTag, + organizationName, + providerAuthToken, + attributionSource, + encryptedPrivateKey, + encryptedPrivateKeyIV, + encryptedPrivateKeyTag + } + } = await validateRequest(reqValidator.CompletedAccountSignupV3, req); - user = await User.findOne({ email }); + user = await User.findOne({ email }); - if (!user || (user && user?.publicKey)) { - // case 1: user doesn't exist. - // case 2: user has already completed account - return res.status(403).send({ - error: "Failed to complete account for complete user", - }); - } + if (!user || (user && user?.publicKey)) { + // case 1: user doesn't exist. + // case 2: user has already completed account + return res.status(403).send({ + error: "Failed to complete account for complete user" + }); + } - if (providerAuthToken) { - await validateProviderAuthToken({ - email, - providerAuthToken - }); - } else { - const [AUTH_TOKEN_TYPE, AUTH_TOKEN_VALUE] = <[string, string]>req.headers["authorization"]?.split(" ", 2) ?? [null, null] - if (AUTH_TOKEN_TYPE === null) { - throw BadRequestError({ message: "Missing Authorization Header in the request header." }); - } - if (AUTH_TOKEN_TYPE.toLowerCase() !== "bearer") { - throw BadRequestError({ message: `The provided authentication type '${AUTH_TOKEN_TYPE}' is not supported.` }) - } - if (AUTH_TOKEN_VALUE === null) { - throw BadRequestError({ - message: "Missing Authorization Body in the request header", - }) - } + if (providerAuthToken) { + await validateProviderAuthToken({ + email, + providerAuthToken + }); + } else { + const [AUTH_TOKEN_TYPE, AUTH_TOKEN_VALUE] = <[string, string]>( + req.headers["authorization"]?.split(" ", 2) + ) ?? [null, null]; + if (AUTH_TOKEN_TYPE === null) { + throw BadRequestError({ message: "Missing Authorization Header in the request header." }); + } + if (AUTH_TOKEN_TYPE.toLowerCase() !== "bearer") { + throw BadRequestError({ + message: `The provided authentication type '${AUTH_TOKEN_TYPE}' is not supported.` + }); + } + if (AUTH_TOKEN_VALUE === null) { + throw BadRequestError({ + message: "Missing Authorization Body in the request header" + }); + } - const decodedToken = ( - jwt.verify(AUTH_TOKEN_VALUE, await getJwtSignupSecret()) - ); + const decodedToken = ( + jwt.verify(AUTH_TOKEN_VALUE, await getJwtSignupSecret()) + ); - if (decodedToken.userId !== user.id) { - throw BadRequestError(); - } - } + if (decodedToken.userId !== user.id) { + throw BadRequestError(); + } + } - // complete setting up user's account - user = await completeAccount({ - userId: user._id.toString(), - firstName, - lastName, - encryptionVersion: 2, - protectedKey, - protectedKeyIV, - protectedKeyTag, - publicKey, - encryptedPrivateKey, - encryptedPrivateKeyIV, - encryptedPrivateKeyTag, - salt, - verifier, - }); + // complete setting up user's account + user = await completeAccount({ + userId: user._id.toString(), + firstName, + lastName, + encryptionVersion: 2, + protectedKey, + protectedKeyIV, + protectedKeyTag, + publicKey, + encryptedPrivateKey, + encryptedPrivateKeyIV, + encryptedPrivateKeyTag, + salt, + verifier + }); - if (!user) - throw new Error("Failed to complete account for non-existent user"); // ensure user is non-null + if (!user) throw new Error("Failed to complete account for non-existent user"); // ensure user is non-null - const hasSamlEnabled = user.authMethods.some((authMethod: AuthMethod) => [AuthMethod.OKTA_SAML, AuthMethod.AZURE_SAML, AuthMethod.JUMPCLOUD_SAML].includes(authMethod)); - - if (!hasSamlEnabled) { // TODO: modify this part - // initialize default organization and workspace - await initializeDefaultOrg({ - organizationName, - user, - }); - } + const hasSamlEnabled = user.authMethods.some((authMethod: AuthMethod) => + [AuthMethod.OKTA_SAML, AuthMethod.AZURE_SAML, AuthMethod.JUMPCLOUD_SAML].includes(authMethod) + ); - // update organization membership statuses that are - // invited to completed with user attached - await MembershipOrg.updateMany( - { - inviteEmail: email, - status: INVITED, - }, - { - user, - status: ACCEPTED, - } - ); + if (!hasSamlEnabled) { + // TODO: modify this part + // initialize default organization and workspace + await initializeDefaultOrg({ + organizationName, + user + }); + } - // issue tokens - const tokens = await issueAuthTokens({ - userId: user._id, - ip: req.realIP, - userAgent: req.headers["user-agent"] ?? "", - }); + // update organization membership statuses that are + // invited to completed with user attached + await MembershipOrg.updateMany( + { + inviteEmail: email, + status: INVITED + }, + { + user, + status: ACCEPTED + } + ); - token = tokens.token; + // issue tokens + const tokens = await issueAuthTokens({ + userId: user._id, + ip: req.realIP, + userAgent: req.headers["user-agent"] ?? "" + }); - // sending a welcome email to new users - if (await getLoopsApiKey()) { - await standardRequest.post("https://app.loops.so/api/v1/events/send", { - "email": email, - "eventName": "Sign Up", - "firstName": firstName, - "lastName": lastName, - }, { - headers: { - "Accept": "application/json", - "Authorization": "Bearer " + (await getLoopsApiKey()), - }, - }); - } + token = tokens.token; - // store (refresh) token in httpOnly cookie - res.cookie("jid", tokens.refreshToken, { - httpOnly: true, - path: "/", - sameSite: "strict", - secure: await getHttpsEnabled(), - }); + // sending a welcome email to new users + if (await getLoopsApiKey()) { + await standardRequest.post( + "https://app.loops.so/api/v1/events/send", + { + email: email, + eventName: "Sign Up", + firstName: firstName, + lastName: lastName + }, + { + headers: { + Accept: "application/json", + Authorization: "Bearer " + (await getLoopsApiKey()) + } + } + ); + } - const postHogClient = await TelemetryService.getPostHogClient(); - if (postHogClient) { - postHogClient.capture({ - event: "User Signed Up", - distinctId: email, - properties: { - email, - ...(attributionSource ? { attributionSource } : {}) - }, - }); - } - } catch (err) { - Sentry.setUser(null); - Sentry.captureException(err); - return res.status(400).send({ - message: "Failed to complete account setup", - }); - } + // store (refresh) token in httpOnly cookie + res.cookie("jid", tokens.refreshToken, { + httpOnly: true, + path: "/", + sameSite: "strict", + secure: await getHttpsEnabled() + }); - return res.status(200).send({ - message: "Successfully set up account", - user, - token, - }); + const postHogClient = await TelemetryService.getPostHogClient(); + if (postHogClient) { + postHogClient.capture({ + event: "User Signed Up", + distinctId: email, + properties: { + email, + ...(attributionSource ? { attributionSource } : {}) + } + }); + } + } catch (err) { + Sentry.setUser(null); + Sentry.captureException(err); + return res.status(400).send({ + message: "Failed to complete account setup" + }); + } + + return res.status(200).send({ + message: "Successfully set up account", + user, + token + }); }; diff --git a/backend/src/controllers/v3/workspacesController.ts b/backend/src/controllers/v3/workspacesController.ts index 4298adba2..8469cdb5e 100644 --- a/backend/src/controllers/v3/workspacesController.ts +++ b/backend/src/controllers/v3/workspacesController.ts @@ -1,90 +1,103 @@ import { Request, Response } from "express"; import { Types } from "mongoose"; +import { validateRequest } from "../../helpers/validation"; import { Secret } from "../../models"; -import { SecretService } from"../../services"; +import { SecretService } from "../../services"; +import { getUserProjectPermissions } from "../../ee/services/ProjectRoleService"; +import { UnauthorizedRequestError } from "../../utils/errors"; +import * as reqValidator from "../../validation/workspace"; /** * Return whether or not all secrets in workspace with id [workspaceId] * are blind-indexed - * @param req - * @param res - * @returns + * @param req + * @param res + * @returns */ export const getWorkspaceBlindIndexStatus = async (req: Request, res: Response) => { - const { workspaceId } = req.params; + const { + params: { workspaceId } + } = await validateRequest(reqValidator.GetWorkspaceBlinkIndexStatusV3, req); - const secretsWithoutBlindIndex = await Secret.countDocuments({ - workspace: new Types.ObjectId(workspaceId), - secretBlindIndex: { - $exists: false, - }, - }); + const { membership } = await getUserProjectPermissions(req.user._id, workspaceId); + if (membership.role !== "admin") + throw UnauthorizedRequestError({ message: "User must be an admin" }); - return res.status(200).send(secretsWithoutBlindIndex === 0); -} + const secretsWithoutBlindIndex = await Secret.countDocuments({ + workspace: new Types.ObjectId(workspaceId), + secretBlindIndex: { + $exists: false + } + }); + + return res.status(200).send(secretsWithoutBlindIndex === 0); +}; /** * Get all secrets for workspace with id [workspaceId] */ export const getWorkspaceSecrets = async (req: Request, res: Response) => { - const { workspaceId } = req.params; + const { + params: { workspaceId } + } = await validateRequest(reqValidator.GetWorkspaceSecretsV3, req); - const secrets = await Secret.find({ - workspace: new Types.ObjectId (workspaceId), - }); - - return res.status(200).send({ - secrets, - }); -} + const { membership } = await getUserProjectPermissions(req.user._id, workspaceId); + if (membership.role !== "admin") + throw UnauthorizedRequestError({ message: "User must be an admin" }); + + const secrets = await Secret.find({ + workspace: new Types.ObjectId(workspaceId) + }); + + return res.status(200).send({ + secrets + }); +}; /** * Update blind indices for secrets in workspace with id [workspaceId] - * @param req - * @param res + * @param req + * @param res */ export const nameWorkspaceSecrets = async (req: Request, res: Response) => { - interface SecretToUpdate { - secretName: string; - _id: string; - } + const { + params: { workspaceId }, + body: { secretsToUpdate } + } = await validateRequest(reqValidator.NameWorkspaceSecretsV3, req); - const { workspaceId } = req.params; - const { - secretsToUpdate, - }: { - secretsToUpdate: SecretToUpdate[]; - } = req.body; + const { membership } = await getUserProjectPermissions(req.user._id, workspaceId); + if (membership.role !== "admin") + throw UnauthorizedRequestError({ message: "User must be an admin" }); - // get secret blind index salt - const salt = await SecretService.getSecretBlindIndexSalt({ - workspaceId: new Types.ObjectId(workspaceId), - }); + // get secret blind index salt + const salt = await SecretService.getSecretBlindIndexSalt({ + workspaceId: new Types.ObjectId(workspaceId) + }); - // update secret blind indices - const operations = await Promise.all( - secretsToUpdate.map(async (secretToUpdate: SecretToUpdate) => { - const secretBlindIndex = await SecretService.generateSecretBlindIndexWithSalt({ - secretName: secretToUpdate.secretName, - salt, - }); - - return ({ - updateOne: { - filter: { - _id: new Types.ObjectId(secretToUpdate._id), - }, - update: { - secretBlindIndex, - }, - }, - }); - }) - ); + // update secret blind indices + const operations = await Promise.all( + secretsToUpdate.map(async (secretToUpdate) => { + const secretBlindIndex = await SecretService.generateSecretBlindIndexWithSalt({ + secretName: secretToUpdate.secretName, + salt + }); - await Secret.bulkWrite(operations); + return { + updateOne: { + filter: { + _id: new Types.ObjectId(secretToUpdate._id) + }, + update: { + secretBlindIndex + } + } + }; + }) + ); - return res.status(200).send({ - message: "Successfully named workspace secrets", - }); -} \ No newline at end of file + await Secret.bulkWrite(operations); + + return res.status(200).send({ + message: "Successfully named workspace secrets" + }); +}; diff --git a/backend/src/ee/controllers/v1/actionController.ts b/backend/src/ee/controllers/v1/actionController.ts index 484c25351..56b076026 100644 --- a/backend/src/ee/controllers/v1/actionController.ts +++ b/backend/src/ee/controllers/v1/actionController.ts @@ -1,30 +1,32 @@ import { Request, Response } from "express"; import { Action } from "../../models"; import { ActionNotFoundError } from "../../../utils/errors"; +import { validateRequest } from "../../../helpers/validation"; +import * as reqValidator from "../../../validation/action"; export const getAction = async (req: Request, res: Response) => { - let action; - try { - const { actionId } = req.params; - - action = await Action - .findById(actionId) - .populate([ - "payload.secretVersions.oldSecretVersion", - "payload.secretVersions.newSecretVersion", - ]); - - if (!action) throw ActionNotFoundError({ - message: "Failed to find action", - }); + let action; + try { + const { + params: { actionId } + } = await validateRequest(reqValidator.GetActionV1, req); - } catch (err) { - throw ActionNotFoundError({ - message: "Failed to find action", - }); - } - - return res.status(200).send({ - action, + action = await Action.findById(actionId).populate([ + "payload.secretVersions.oldSecretVersion", + "payload.secretVersions.newSecretVersion" + ]); + + if (!action) + throw ActionNotFoundError({ + message: "Failed to find action" + }); + } catch (err) { + throw ActionNotFoundError({ + message: "Failed to find action" }); -} + } + + return res.status(200).send({ + action + }); +}; diff --git a/backend/src/ee/controllers/v1/cloudProductsController.ts b/backend/src/ee/controllers/v1/cloudProductsController.ts index e66fc66e5..0cc6ab372 100644 --- a/backend/src/ee/controllers/v1/cloudProductsController.ts +++ b/backend/src/ee/controllers/v1/cloudProductsController.ts @@ -2,27 +2,31 @@ import { Request, Response } from "express"; import { EELicenseService } from "../../services"; import { getLicenseServerUrl } from "../../../config"; import { licenseServerKeyRequest } from "../../../config/request"; +import { validateRequest } from "../../../helpers/validation"; +import * as reqValidator from "../../../validation/cloudProducts"; /** * Return available cloud product information. * Note: Nicely formatted to easily construct a table from - * @param req - * @param res - * @returns + * @param req + * @param res + * @returns */ export const getCloudProducts = async (req: Request, res: Response) => { - const billingCycle = req.query["billing-cycle"] as string; + const { + query: { "billing-cycle": billingCycle } + } = await validateRequest(reqValidator.GetCloudProductsV1, req); - if (EELicenseService.instanceType === "cloud") { - const { data } = await licenseServerKeyRequest.get( - `${await getLicenseServerUrl()}/api/license-server/v1/cloud-products?billing-cycle=${billingCycle}` - ); + if (EELicenseService.instanceType === "cloud") { + const { data } = await licenseServerKeyRequest.get( + `${await getLicenseServerUrl()}/api/license-server/v1/cloud-products?billing-cycle=${billingCycle}` + ); - return res.status(200).send(data); - } - - return res.status(200).send({ - head: [], - rows: [], - }); -} + return res.status(200).send(data); + } + + return res.status(200).send({ + head: [], + rows: [] + }); +}; diff --git a/backend/src/ee/controllers/v1/index.ts b/backend/src/ee/controllers/v1/index.ts index 0c63b41c0..b57a07c9d 100644 --- a/backend/src/ee/controllers/v1/index.ts +++ b/backend/src/ee/controllers/v1/index.ts @@ -7,15 +7,17 @@ import * as workspaceController from "./workspaceController"; import * as actionController from "./actionController"; import * as membershipController from "./membershipController"; import * as cloudProductsController from "./cloudProductsController"; +import * as roleController from "./roleController"; export { - secretController, - secretSnapshotController, - organizationsController, - ssoController, - usersController, - workspaceController, - actionController, - membershipController, - cloudProductsController, -} \ No newline at end of file + secretController, + secretSnapshotController, + organizationsController, + ssoController, + usersController, + workspaceController, + actionController, + membershipController, + cloudProductsController, + roleController +}; diff --git a/backend/src/ee/controllers/v1/organizationsController.ts b/backend/src/ee/controllers/v1/organizationsController.ts index b641de985..f64603e3b 100644 --- a/backend/src/ee/controllers/v1/organizationsController.ts +++ b/backend/src/ee/controllers/v1/organizationsController.ts @@ -3,228 +3,503 @@ import { Request, Response } from "express"; import { getLicenseServerUrl } from "../../../config"; import { licenseServerKeyRequest } from "../../../config/request"; import { EELicenseService } from "../../services"; +import { validateRequest } from "../../../helpers/validation"; +import * as reqValidator from "../../../validation/organization"; +import { + OrgPermissionActions, + OrgPermissionSubjects, + getUserOrgPermissions +} from "../../services/RoleService"; +import { ForbiddenError } from "@casl/ability"; +import { Organization } from "../../../models"; +import { OrganizationNotFoundError } from "../../../utils/errors"; export const getOrganizationPlansTable = async (req: Request, res: Response) => { - const billingCycle = req.query.billingCycle as string; - - const { data } = await licenseServerKeyRequest.get( - `${await getLicenseServerUrl()}/api/license-server/v1/cloud-products?billing-cycle=${billingCycle}` - ); + const { + query: { billingCycle }, + params: { organizationId } + } = await validateRequest(reqValidator.GetOrgPlansTablev1, req); - return res.status(200).send(data); -} + const { permission } = await getUserOrgPermissions(req.user._id, organizationId); + ForbiddenError.from(permission).throwUnlessCan( + OrgPermissionActions.Read, + OrgPermissionSubjects.Billing + ); + + const { data } = await licenseServerKeyRequest.get( + `${await getLicenseServerUrl()}/api/license-server/v1/cloud-products?billing-cycle=${billingCycle}` + ); + + return res.status(200).send(data); +}; /** * Return the organization current plan's feature set */ export const getOrganizationPlan = async (req: Request, res: Response) => { - const { organizationId } = req.params; - const workspaceId = req.query.workspaceId as string; + const { + query: { workspaceId }, + params: { organizationId } + } = await validateRequest(reqValidator.GetOrgPlanv1, req); - const plan = await EELicenseService.getPlan(new Types.ObjectId(organizationId), new Types.ObjectId(workspaceId)); + const { permission } = await getUserOrgPermissions(req.user._id, organizationId); + ForbiddenError.from(permission).throwUnlessCan( + OrgPermissionActions.Read, + OrgPermissionSubjects.Billing + ); - return res.status(200).send({ - plan, - }); -} + const plan = await EELicenseService.getPlan( + new Types.ObjectId(organizationId), + new Types.ObjectId(workspaceId) + ); + + return res.status(200).send({ + plan + }); +}; /** * Return checkout url for pro trial - * @param req - * @param res - * @returns + * @param req + * @param res + * @returns */ export const startOrganizationTrial = async (req: Request, res: Response) => { - const { organizationId } = req.params; - const { success_url } = req.body; + const { + params: { organizationId }, + body: { success_url } + } = await validateRequest(reqValidator.StartOrgTrailv1, req); - const { data: { url } } = await licenseServerKeyRequest.post( - `${await getLicenseServerUrl()}/api/license-server/v1/customers/${req.organization.customerId}/session/trial`, - { - success_url - } - ); - - EELicenseService.delPlan(new Types.ObjectId(organizationId)); - - return res.status(200).send({ - url + const { permission } = await getUserOrgPermissions(req.user._id, organizationId); + ForbiddenError.from(permission).throwUnlessCan( + OrgPermissionActions.Create, + OrgPermissionSubjects.Billing + ); + ForbiddenError.from(permission).throwUnlessCan( + OrgPermissionActions.Edit, + OrgPermissionSubjects.Billing + ); + + const organization = await Organization.findById(organizationId); + if (!organization) { + throw OrganizationNotFoundError({ + message: "Failed to find organization" }); -} + } + + const { + data: { url } + } = await licenseServerKeyRequest.post( + `${await getLicenseServerUrl()}/api/license-server/v1/customers/${ + organization.customerId + }/session/trial`, + { + success_url + } + ); + + EELicenseService.delPlan(new Types.ObjectId(organizationId)); + + return res.status(200).send({ + url + }); +}; /** * Return the organization's current plan's billing info - * @param req - * @param res - * @returns + * @param req + * @param res + * @returns */ export const getOrganizationPlanBillingInfo = async (req: Request, res: Response) => { - const { data } = await licenseServerKeyRequest.get( - `${await getLicenseServerUrl()}/api/license-server/v1/customers/${req.organization.customerId}/cloud-plan/billing` - ); - - return res.status(200).send(data); -} + const { + params: { organizationId } + } = await validateRequest(reqValidator.GetOrgPlanBillingInfov1, req); + + const { permission } = await getUserOrgPermissions(req.user._id, organizationId); + ForbiddenError.from(permission).throwUnlessCan( + OrgPermissionActions.Read, + OrgPermissionSubjects.Billing + ); + + const organization = await Organization.findById(organizationId); + if (!organization) { + throw OrganizationNotFoundError({ + message: "Failed to find organization" + }); + } + + const { data } = await licenseServerKeyRequest.get( + `${await getLicenseServerUrl()}/api/license-server/v1/customers/${ + organization.customerId + }/cloud-plan/billing` + ); + + return res.status(200).send(data); +}; /** * Return the organization's current plan's feature table - * @param req - * @param res - * @returns + * @param req + * @param res + * @returns */ export const getOrganizationPlanTable = async (req: Request, res: Response) => { - const { data } = await licenseServerKeyRequest.get( - `${await getLicenseServerUrl()}/api/license-server/v1/customers/${req.organization.customerId}/cloud-plan/table` - ); + const { + params: { organizationId } + } = await validateRequest(reqValidator.GetOrgPlanTablev1, req); - return res.status(200).send(data); -} + const { permission } = await getUserOrgPermissions(req.user._id, organizationId); + ForbiddenError.from(permission).throwUnlessCan( + OrgPermissionActions.Read, + OrgPermissionSubjects.Billing + ); + + const organization = await Organization.findById(organizationId); + if (!organization) { + throw OrganizationNotFoundError({ + message: "Failed to find organization" + }); + } + + const { data } = await licenseServerKeyRequest.get( + `${await getLicenseServerUrl()}/api/license-server/v1/customers/${ + organization.customerId + }/cloud-plan/table` + ); + + return res.status(200).send(data); +}; export const getOrganizationBillingDetails = async (req: Request, res: Response) => { - const { data } = await licenseServerKeyRequest.get( - `${await getLicenseServerUrl()}/api/license-server/v1/customers/${req.organization.customerId}/billing-details` - ); + const { + params: { organizationId } + } = await validateRequest(reqValidator.GetOrgBillingDetailsv1, req); - return res.status(200).send(data); -} + const { permission } = await getUserOrgPermissions(req.user._id, organizationId); + ForbiddenError.from(permission).throwUnlessCan( + OrgPermissionActions.Read, + OrgPermissionSubjects.Billing + ); + + const organization = await Organization.findById(organizationId); + if (!organization) { + throw OrganizationNotFoundError({ + message: "Failed to find organization" + }); + } + + const { data } = await licenseServerKeyRequest.get( + `${await getLicenseServerUrl()}/api/license-server/v1/customers/${ + organization.customerId + }/billing-details` + ); + + return res.status(200).send(data); +}; export const updateOrganizationBillingDetails = async (req: Request, res: Response) => { - const { - name, - email - } = req.body; + const { + params: { organizationId }, + body: { name, email } + } = await validateRequest(reqValidator.UpdateOrgBillingDetailsv1, req); - const { data } = await licenseServerKeyRequest.patch( - `${await getLicenseServerUrl()}/api/license-server/v1/customers/${req.organization.customerId}/billing-details`, - { - ...(name ? { name } : {}), - ...(email ? { email } : {}) - } - ); + const { permission } = await getUserOrgPermissions(req.user._id, organizationId); + ForbiddenError.from(permission).throwUnlessCan( + OrgPermissionActions.Edit, + OrgPermissionSubjects.Billing + ); - return res.status(200).send(data); -} + const organization = await Organization.findById(organizationId); + if (!organization) { + throw OrganizationNotFoundError({ + message: "Failed to find organization" + }); + } + + const { data } = await licenseServerKeyRequest.patch( + `${await getLicenseServerUrl()}/api/license-server/v1/customers/${ + organization.customerId + }/billing-details`, + { + ...(name ? { name } : {}), + ...(email ? { email } : {}) + } + ); + + return res.status(200).send(data); +}; /** * Return the organization's payment methods on file */ export const getOrganizationPmtMethods = async (req: Request, res: Response) => { - const { data: { pmtMethods } } = await licenseServerKeyRequest.get( - `${await getLicenseServerUrl()}/api/license-server/v1/customers/${req.organization.customerId}/billing-details/payment-methods` - ); + const { + params: { organizationId } + } = await validateRequest(reqValidator.GetOrgPmtMethodsv1, req); - return res.status(200).send(pmtMethods); -} + const { permission } = await getUserOrgPermissions(req.user._id, organizationId); + ForbiddenError.from(permission).throwUnlessCan( + OrgPermissionActions.Read, + OrgPermissionSubjects.Billing + ); + + const organization = await Organization.findById(organizationId); + if (!organization) { + throw OrganizationNotFoundError({ + message: "Failed to find organization" + }); + } + + const { + data: { pmtMethods } + } = await licenseServerKeyRequest.get( + `${await getLicenseServerUrl()}/api/license-server/v1/customers/${ + organization.customerId + }/billing-details/payment-methods` + ); + + return res.status(200).send(pmtMethods); +}; /** * Return URL to add payment method for organization */ export const addOrganizationPmtMethod = async (req: Request, res: Response) => { - const { - success_url, - cancel_url, - } = req.body; - - const { data: { url } } = await licenseServerKeyRequest.post( - `${await getLicenseServerUrl()}/api/license-server/v1/customers/${req.organization.customerId}/billing-details/payment-methods`, - { - success_url, - cancel_url, - } - ); - - return res.status(200).send({ - url, - }); -} + const { + params: { organizationId }, + body: { success_url, cancel_url } + } = await validateRequest(reqValidator.CreateOrgPmtMethodv1, req); + + const { permission } = await getUserOrgPermissions(req.user._id, organizationId); + ForbiddenError.from(permission).throwUnlessCan( + OrgPermissionActions.Create, + OrgPermissionSubjects.Billing + ); + + const organization = await Organization.findById(organizationId); + if (!organization) { + throw OrganizationNotFoundError({ + message: "Failed to find organization" + }); + } + + const { + data: { url } + } = await licenseServerKeyRequest.post( + `${await getLicenseServerUrl()}/api/license-server/v1/customers/${ + organization.customerId + }/billing-details/payment-methods`, + { + success_url, + cancel_url + } + ); + + return res.status(200).send({ + url + }); +}; /** * Delete payment method with id [pmtMethodId] for organization - * @param req - * @param res - * @returns + * @param req + * @param res + * @returns */ export const deleteOrganizationPmtMethod = async (req: Request, res: Response) => { - const { pmtMethodId } = req.params; + const { + params: { organizationId, pmtMethodId } + } = await validateRequest(reqValidator.DelOrgPmtMethodv1, req); - const { data } = await licenseServerKeyRequest.delete( - `${await getLicenseServerUrl()}/api/license-server/v1/customers/${req.organization.customerId}/billing-details/payment-methods/${pmtMethodId}`, - ); - - return res.status(200).send(data); -} + const { permission } = await getUserOrgPermissions(req.user._id, organizationId); + ForbiddenError.from(permission).throwUnlessCan( + OrgPermissionActions.Delete, + OrgPermissionSubjects.Billing + ); + + const organization = await Organization.findById(organizationId); + if (!organization) { + throw OrganizationNotFoundError({ + message: "Failed to find organization" + }); + } + + const { data } = await licenseServerKeyRequest.delete( + `${await getLicenseServerUrl()}/api/license-server/v1/customers/${ + organization.customerId + }/billing-details/payment-methods/${pmtMethodId}` + ); + + return res.status(200).send(data); +}; /** * Return the organization's tax ids on file */ export const getOrganizationTaxIds = async (req: Request, res: Response) => { - const { data: { tax_ids } } = await licenseServerKeyRequest.get( - `${await getLicenseServerUrl()}/api/license-server/v1/customers/${req.organization.customerId}/billing-details/tax-ids` - ); + const { + params: { organizationId } + } = await validateRequest(reqValidator.GetOrgTaxIdsv1, req); - return res.status(200).send(tax_ids); -} + const { permission } = await getUserOrgPermissions(req.user._id, organizationId); + ForbiddenError.from(permission).throwUnlessCan( + OrgPermissionActions.Read, + OrgPermissionSubjects.Billing + ); + + const organization = await Organization.findById(organizationId); + if (!organization) { + throw OrganizationNotFoundError({ + message: "Failed to find organization" + }); + } + + const { + data: { tax_ids } + } = await licenseServerKeyRequest.get( + `${await getLicenseServerUrl()}/api/license-server/v1/customers/${ + organization.customerId + }/billing-details/tax-ids` + ); + + return res.status(200).send(tax_ids); +}; /** * Add tax id to organization */ export const addOrganizationTaxId = async (req: Request, res: Response) => { - const { - type, - value - } = req.body; + const { + params: { organizationId }, + body: { type, value } + } = await validateRequest(reqValidator.CreateOrgTaxId, req); - const { data } = await licenseServerKeyRequest.post( - `${await getLicenseServerUrl()}/api/license-server/v1/customers/${req.organization.customerId}/billing-details/tax-ids`, - { - type, - value - } - ); + const { permission } = await getUserOrgPermissions(req.user._id, organizationId); + ForbiddenError.from(permission).throwUnlessCan( + OrgPermissionActions.Create, + OrgPermissionSubjects.Billing + ); - return res.status(200).send(data); -} + const organization = await Organization.findById(organizationId); + if (!organization) { + throw OrganizationNotFoundError({ + message: "Failed to find organization" + }); + } + + const { data } = await licenseServerKeyRequest.post( + `${await getLicenseServerUrl()}/api/license-server/v1/customers/${ + organization.customerId + }/billing-details/tax-ids`, + { + type, + value + } + ); + + return res.status(200).send(data); +}; /** * Delete tax id with id [taxId] from organization tax ids on file - * @param req - * @param res - * @returns + * @param req + * @param res + * @returns */ export const deleteOrganizationTaxId = async (req: Request, res: Response) => { - const { taxId } = req.params; + const { + params: { organizationId, taxId } + } = await validateRequest(reqValidator.DelOrgTaxIdv1, req); - const { data } = await licenseServerKeyRequest.delete( - `${await getLicenseServerUrl()}/api/license-server/v1/customers/${req.organization.customerId}/billing-details/tax-ids/${taxId}`, - ); - - return res.status(200).send(data); -} + const { permission } = await getUserOrgPermissions(req.user._id, organizationId); + ForbiddenError.from(permission).throwUnlessCan( + OrgPermissionActions.Delete, + OrgPermissionSubjects.Billing + ); + + const organization = await Organization.findById(organizationId); + if (!organization) { + throw OrganizationNotFoundError({ + message: "Failed to find organization" + }); + } + + const { data } = await licenseServerKeyRequest.delete( + `${await getLicenseServerUrl()}/api/license-server/v1/customers/${ + organization.customerId + }/billing-details/tax-ids/${taxId}` + ); + + return res.status(200).send(data); +}; /** * Return organization's invoices on file - * @param req - * @param res - * @returns + * @param req + * @param res + * @returns */ export const getOrganizationInvoices = async (req: Request, res: Response) => { - const { data: { invoices } } = await licenseServerKeyRequest.get( - `${await getLicenseServerUrl()}/api/license-server/v1/customers/${req.organization.customerId}/invoices` - ); + const { + params: { organizationId } + } = await validateRequest(reqValidator.GetOrgInvoicesv1, req); - return res.status(200).send(invoices); -} + const { permission } = await getUserOrgPermissions(req.user._id, organizationId); + ForbiddenError.from(permission).throwUnlessCan( + OrgPermissionActions.Read, + OrgPermissionSubjects.Billing + ); + + const organization = await Organization.findById(organizationId); + if (!organization) { + throw OrganizationNotFoundError({ + message: "Failed to find organization" + }); + } + + const { + data: { invoices } + } = await licenseServerKeyRequest.get( + `${await getLicenseServerUrl()}/api/license-server/v1/customers/${ + organization.customerId + }/invoices` + ); + + return res.status(200).send(invoices); +}; /** * Return organization's licenses on file - * @param req - * @param res - * @returns + * @param req + * @param res + * @returns */ export const getOrganizationLicenses = async (req: Request, res: Response) => { - const { data: { licenses } } = await licenseServerKeyRequest.get( - `${await getLicenseServerUrl()}/api/license-server/v1/customers/${req.organization.customerId}/licenses` - ); + const { + params: { organizationId } + } = await validateRequest(reqValidator.GetOrgLicencesv1, req); - return res.status(200).send(licenses); -} \ No newline at end of file + const { permission } = await getUserOrgPermissions(req.user._id, organizationId); + ForbiddenError.from(permission).throwUnlessCan( + OrgPermissionActions.Read, + OrgPermissionSubjects.Billing + ); + + const organization = await Organization.findById(organizationId); + if (!organization) { + throw OrganizationNotFoundError({ + message: "Failed to find organization" + }); + } + + const { + data: { licenses } + } = await licenseServerKeyRequest.get( + `${await getLicenseServerUrl()}/api/license-server/v1/customers/${ + organization.customerId + }/licenses` + ); + + return res.status(200).send(licenses); +}; diff --git a/backend/src/ee/controllers/v1/roleController.ts b/backend/src/ee/controllers/v1/roleController.ts new file mode 100644 index 000000000..2c87b9ff8 --- /dev/null +++ b/backend/src/ee/controllers/v1/roleController.ts @@ -0,0 +1,247 @@ +import { Request, Response } from "express"; +import { + CreateRoleSchema, + DeleteRoleSchema, + GetRoleSchema, + GetUserPermission, + GetUserProjectPermission, + UpdateRoleSchema +} from "../../validation/role"; +import { + ProjectPermissionActions, + ProjectPermissionSub, + adminProjectPermissions, + getUserProjectPermissions, + memberProjectPermissions, + viewerProjectPermission +} from "../../services/ProjectRoleService"; +import { + OrgPermissionActions, + OrgPermissionSubjects, + adminPermissions, + getUserOrgPermissions, + memberPermissions +} from "../../services/RoleService"; +import { BadRequestError } from "../../../utils/errors"; +import Role from "../../models/role"; +import { validateRequest } from "../../../helpers/validation"; +import { packRules } from "@casl/ability/extra"; + +export const createRole = async (req: Request, res: Response) => { + const { + body: { workspaceId, name, description, slug, permissions, orgId } + } = await validateRequest(CreateRoleSchema, req); + + const isOrgRole = !workspaceId; // if workspaceid is provided then its a workspace rule + if (isOrgRole) { + const { permission } = await getUserOrgPermissions(req.user.id, orgId); + if (permission.cannot(OrgPermissionActions.Create, OrgPermissionSubjects.Role)) { + throw BadRequestError({ message: "user doesn't have the permission." }); + } + } else { + const { permission } = await getUserProjectPermissions(req.user.id, workspaceId); + if (permission.cannot(ProjectPermissionActions.Create, ProjectPermissionSub.Role)) { + throw BadRequestError({ message: "User doesn't have the permission." }); + } + } + + const existingRole = await Role.findOne({ organization: orgId, workspace: workspaceId, slug }); + if (existingRole) { + throw BadRequestError({ message: "Role already exist" }); + } + + const role = new Role({ + organization: orgId, + workspace: workspaceId, + isOrgRole, + name, + slug, + permissions, + description + }); + await role.save(); + + res.status(200).json({ + message: "Successfully created role", + data: { + role + } + }); +}; + +export const updateRole = async (req: Request, res: Response) => { + const { + params: { id }, + body: { name, description, slug, permissions, workspaceId, orgId } + } = await validateRequest(UpdateRoleSchema, req); + const isOrgRole = !workspaceId; // if workspaceid is provided then its a workspace rule + + if (isOrgRole) { + const { permission } = await getUserOrgPermissions(req.user.id, orgId); + if (permission.cannot(OrgPermissionActions.Edit, OrgPermissionSubjects.Role)) { + throw BadRequestError({ message: "User doesn't have the org permission." }); + } + } else { + const { permission } = await getUserProjectPermissions(req.user.id, workspaceId); + if (permission.cannot(ProjectPermissionActions.Edit, ProjectPermissionSub.Role)) { + throw BadRequestError({ message: "User doesn't have the workspace permission." }); + } + } + + if (slug) { + const existingRole = await Role.findOne({ + organization: orgId, + slug, + isOrgRole, + workspace: workspaceId + }); + if (existingRole && existingRole.id !== id) { + throw BadRequestError({ message: "Role already exist" }); + } + } + + const role = await Role.findByIdAndUpdate( + id, + { name, description, slug, permissions }, + { returnDocument: "after" } + ); + + if (!role) { + throw BadRequestError({ message: "Role not found" }); + } + res.status(200).json({ + message: "Successfully updated role", + data: { + role + } + }); +}; + +export const deleteRole = async (req: Request, res: Response) => { + const { + params: { id } + } = await validateRequest(DeleteRoleSchema, req); + + const role = await Role.findById(id); + if (!role) { + throw BadRequestError({ message: "Role not found" }); + } + + const isOrgRole = !role.workspace; + if (isOrgRole) { + const { permission } = await getUserOrgPermissions(req.user.id, role.organization.toString()); + if (permission.cannot(OrgPermissionActions.Delete, OrgPermissionSubjects.Role)) { + throw BadRequestError({ message: "User doesn't have the org permission." }); + } + } else { + const { permission } = await getUserProjectPermissions(req.user.id, role.workspace.toString()); + if (permission.cannot(ProjectPermissionActions.Delete, ProjectPermissionSub.Role)) { + throw BadRequestError({ message: "User doesn't have the workspace permission." }); + } + } + + await Role.findByIdAndDelete(role.id); + + res.status(200).json({ + message: "Successfully deleted role", + data: { + role + } + }); +}; + +export const getRoles = async (req: Request, res: Response) => { + const { + query: { workspaceId, orgId } + } = await validateRequest(GetRoleSchema, req); + + const isOrgRole = !workspaceId; + if (isOrgRole) { + const { permission } = await getUserOrgPermissions(req.user.id, orgId); + if (permission.cannot(OrgPermissionActions.Read, OrgPermissionSubjects.Role)) { + throw BadRequestError({ message: "User doesn't have the org permission." }); + } + } else { + const { permission } = await getUserProjectPermissions(req.user.id, workspaceId); + if (permission.cannot(ProjectPermissionActions.Read, ProjectPermissionSub.Role)) { + throw BadRequestError({ message: "User doesn't have the workspace permission." }); + } + } + + const customRoles = await Role.find({ organization: orgId, isOrgRole, workspace: workspaceId }); + // as this is shared between org and workspace switch the rule set based on it + const roles = [ + // owner is only in org level role + ...(isOrgRole + ? [ + { + _id: "owner", + name: "Owner", + slug: "owner", + description: "Complete administration access over the organization.", + permissions: adminPermissions.rules + } + ] + : []), + { + _id: "admin", + name: "Admin", + slug: "admin", + description: "Complete administration access over the organization", + permissions: isOrgRole ? adminPermissions.rules : adminProjectPermissions.rules + }, + { + _id: "member", + name: "Member", + slug: "member", + description: "Non-administrative role in an organization", + permissions: isOrgRole ? memberPermissions.rules : memberProjectPermissions.rules + }, + // viewer role only for project level + ...(isOrgRole + ? [] + : [ + { + _id: "viewer", + name: "Viewer", + slug: "viewer", + description: "Non-administrative role in an organization", + permissions: viewerProjectPermission.rules + } + ]), + ...customRoles + ]; + + res.status(200).json({ + message: "Successfully fetched role list", + data: { + roles + } + }); +}; + +export const getUserPermissions = async (req: Request, res: Response) => { + const { + params: { orgId } + } = await validateRequest(GetUserPermission, req); + const { permission } = await getUserOrgPermissions(req.user._id, orgId); + + res.status(200).json({ + data: { + permissions: packRules(permission.rules) + } + }); +}; + +export const getUserWorkspacePermissions = async (req: Request, res: Response) => { + const { + params: { workspaceId } + } = await validateRequest(GetUserProjectPermission, req); + const { permission } = await getUserProjectPermissions(req.user._id, workspaceId); + + res.status(200).json({ + data: { + permissions: packRules(permission.rules) + } + }); +}; diff --git a/backend/src/ee/controllers/v1/secretController.ts b/backend/src/ee/controllers/v1/secretController.ts index 2dcc3c2c7..42aa07f3d 100644 --- a/backend/src/ee/controllers/v1/secretController.ts +++ b/backend/src/ee/controllers/v1/secretController.ts @@ -1,7 +1,17 @@ +import { ForbiddenError, subject } from "@casl/ability"; import { Request, Response } from "express"; -import { Secret } from "../../../models"; +import { validateRequest } from "../../../helpers/validation"; +import { Folder, Secret } from "../../../models"; +import { + ProjectPermissionActions, + ProjectPermissionSub, + getUserProjectPermissions +} from "../../services/ProjectRoleService"; +import { BadRequestError } from "../../../utils/errors"; +import * as reqValidator from "../../../validation"; import { SecretVersion } from "../../models"; import { EESecretService } from "../../services"; +import { getFolderWithPathFromId } from "../../../services/FolderService"; /** * Return secret versions for secret with id [secretId] @@ -54,10 +64,21 @@ export const getSecretVersions = async (req: Request, res: Response) => { } } */ - const { secretId } = req.params; + const { + params: { secretId }, + query: { offset, limit } + } = await validateRequest(reqValidator.GetSecretVersionsV1, req); - const offset: number = parseInt(req.query.offset as string); - const limit: number = parseInt(req.query.limit as string); + const secret = await Secret.findById(secretId); + if (!secret) { + throw BadRequestError({ message: "Failed to find secret" }); + } + + const { permission } = await getUserProjectPermissions(req.user._id, secret.workspace.toString()); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Read, + ProjectPermissionSub.SecretRollback + ); const secretVersions = await SecretVersion.find({ secret: secretId @@ -126,8 +147,24 @@ export const rollbackSecretVersion = async (req: Request, res: Response) => { } } */ - const { secretId } = req.params; - const { version } = req.body; + + const { + params: { secretId }, + body: { version } + } = await validateRequest(reqValidator.RollbackSecretVersionV1, req); + + const toBeUpdatedSec = await Secret.findById(secretId); + if (!toBeUpdatedSec) { + throw BadRequestError({ message: "Failed to find secret" }); + } + const { permission } = await getUserProjectPermissions( + req.user._id, + toBeUpdatedSec.workspace.toString() + ); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Create, + ProjectPermissionSub.SecretRollback + ); // validate secret version const oldSecretVersion = await SecretVersion.findOne({ @@ -154,6 +191,15 @@ export const rollbackSecretVersion = async (req: Request, res: Response) => { keyEncoding } = oldSecretVersion; + let secretPath = "/"; + const folders = await Folder.findOne({ workspace, environment }); + if (folders) + secretPath = getFolderWithPathFromId(folders.nodes, folder || "root")?.folderPath || "/"; + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Edit, + subject(ProjectPermissionSub.Secrets, { environment: toBeUpdatedSec.environment, secretPath }) + ); + // update secret const secret = await Secret.findByIdAndUpdate( secretId, diff --git a/backend/src/ee/controllers/v1/secretSnapshotController.ts b/backend/src/ee/controllers/v1/secretSnapshotController.ts index c10188b10..e39555929 100644 --- a/backend/src/ee/controllers/v1/secretSnapshotController.ts +++ b/backend/src/ee/controllers/v1/secretSnapshotController.ts @@ -1,9 +1,13 @@ +import { ForbiddenError } from "@casl/ability"; import { Request, Response } from "express"; +import { validateRequest } from "../../../helpers/validation"; import { - ISecretVersion, - SecretSnapshot, - TFolderRootVersionSchema, -} from "../../models"; + ProjectPermissionActions, + ProjectPermissionSub, + getUserProjectPermissions +} from "../../services/ProjectRoleService"; +import * as reqValidator from "../../../validation/secretSnapshot"; +import { ISecretVersion, SecretSnapshot, TFolderRootVersionSchema } from "../../models"; /** * Return secret snapshot with id [secretSnapshotId] @@ -12,7 +16,9 @@ import { * @returns */ export const getSecretSnapshot = async (req: Request, res: Response) => { - const { secretSnapshotId } = req.params; + const { + params: { secretSnapshotId } + } = await validateRequest(reqValidator.GetSecretSnapshotV1, req); const secretSnapshot = await SecretSnapshot.findById(secretSnapshotId) .lean() @@ -20,26 +26,36 @@ export const getSecretSnapshot = async (req: Request, res: Response) => { path: "secretVersions", populate: { path: "tags", - model: "Tag", - }, + model: "Tag" + } }) .populate<{ folderVersion: TFolderRootVersionSchema }>("folderVersion"); - + if (!secretSnapshot) throw new Error("Failed to find secret snapshot"); - + + const { permission } = await getUserProjectPermissions( + req.user._id, + secretSnapshot.workspace.toString() + ); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Read, + ProjectPermissionSub.SecretRollback + ); + const folderId = secretSnapshot.folderId; // to show only the folder required secrets secretSnapshot.secretVersions = secretSnapshot.secretVersions.filter( ({ folder }) => folder === folderId ); - secretSnapshot.folderVersion = - secretSnapshot?.folderVersion?.nodes?.children?.map(({ id, name }) => ({ + secretSnapshot.folderVersion = secretSnapshot?.folderVersion?.nodes?.children?.map( + ({ id, name }) => ({ id, - name, - })) as any; + name + }) + ) as any; return res.status(200).send({ - secretSnapshot, + secretSnapshot }); }; diff --git a/backend/src/ee/controllers/v1/ssoController.ts b/backend/src/ee/controllers/v1/ssoController.ts index d75cf25d2..b7ae793ab 100644 --- a/backend/src/ee/controllers/v1/ssoController.ts +++ b/backend/src/ee/controllers/v1/ssoController.ts @@ -2,239 +2,258 @@ import { Request, Response } from "express"; import { Types } from "mongoose"; import { BotOrgService } from "../../../services"; import { SSOConfig } from "../../models"; -import { - AuthMethod, - MembershipOrg, - User -} from "../../../models"; +import { AuthMethod, MembershipOrg, User } from "../../../models"; import { getSSOConfigHelper } from "../../helpers/organizations"; import { client } from "../../../config"; import { ResourceNotFoundError } from "../../../utils/errors"; import { getSiteURL } from "../../../config"; import { EELicenseService } from "../../services"; +import * as reqValidator from "../../../validation/sso"; +import { validateRequest } from "../../../helpers/validation"; +import { + OrgPermissionActions, + OrgPermissionSubjects, + getUserOrgPermissions +} from "../../services/RoleService"; +import { ForbiddenError } from "@casl/ability"; /** * Redirect user to appropriate SSO endpoint after successful authentication * to finish inputting their master key for logging in or signing up - * @param req - * @param res - * @returns + * @param req + * @param res + * @returns */ export const redirectSSO = async (req: Request, res: Response) => { - if (req.isUserCompleted) { - return res.redirect(`${await getSiteURL()}/login/sso?token=${encodeURIComponent(req.providerAuthToken)}`); - } - - return res.redirect(`${await getSiteURL()}/signup/sso?token=${encodeURIComponent(req.providerAuthToken)}`); -} + if (req.isUserCompleted) { + return res.redirect( + `${await getSiteURL()}/login/sso?token=${encodeURIComponent(req.providerAuthToken)}` + ); + } + + return res.redirect( + `${await getSiteURL()}/signup/sso?token=${encodeURIComponent(req.providerAuthToken)}` + ); +}; /** * Return organization SAML SSO configuration - * @param req - * @param res - * @returns + * @param req + * @param res + * @returns */ export const getSSOConfig = async (req: Request, res: Response) => { - const organizationId = req.query.organizationId as string; - - const data = await getSSOConfigHelper({ - organizationId: new Types.ObjectId(organizationId) - }); + const { + query: { organizationId } + } = await validateRequest(reqValidator.GetSsoConfigv1, req); - return res.status(200).send(data); -} + const { permission } = await getUserOrgPermissions(req.user._id, organizationId); + ForbiddenError.from(permission).throwUnlessCan( + OrgPermissionActions.Read, + OrgPermissionSubjects.Sso + ); + + const data = await getSSOConfigHelper({ + organizationId: new Types.ObjectId(organizationId) + }); + + return res.status(200).send(data); +}; /** * Update organization SAML SSO configuration - * @param req - * @param res - * @returns + * @param req + * @param res + * @returns */ export const updateSSOConfig = async (req: Request, res: Response) => { + const { + body: { organizationId, authProvider, isActive, entryPoint, issuer, cert } + } = await validateRequest(reqValidator.UpdateSsoConfigv1, req); + + const { permission } = await getUserOrgPermissions(req.user._id, organizationId); + ForbiddenError.from(permission).throwUnlessCan( + OrgPermissionActions.Edit, + OrgPermissionSubjects.Sso + ); + + const plan = await EELicenseService.getPlan(new Types.ObjectId(organizationId)); + + if (!plan.samlSSO) + return res.status(400).send({ + message: + "Failed to update SAML SSO configuration due to plan restriction. Upgrade plan to update SSO configuration." + }); + + interface PatchUpdate { + authProvider?: string; + isActive?: boolean; + encryptedEntryPoint?: string; + entryPointIV?: string; + entryPointTag?: string; + encryptedIssuer?: string; + issuerIV?: string; + issuerTag?: string; + encryptedCert?: string; + certIV?: string; + certTag?: string; + } + + const update: PatchUpdate = {}; + + if (authProvider) { + update.authProvider = authProvider; + } + + if (isActive !== undefined) { + update.isActive = isActive; + } + + const key = await BotOrgService.getSymmetricKey(new Types.ObjectId(organizationId)); + + if (entryPoint) { const { - organizationId, - authProvider, - isActive, - entryPoint, - issuer, - cert, - } = req.body; + ciphertext: encryptedEntryPoint, + iv: entryPointIV, + tag: entryPointTag + } = client.encryptSymmetric(entryPoint, key); - const plan = await EELicenseService.getPlan(new Types.ObjectId(organizationId)); - - if (!plan.samlSSO) return res.status(400).send({ - message: "Failed to update SAML SSO configuration due to plan restriction. Upgrade plan to update SSO configuration." + update.encryptedEntryPoint = encryptedEntryPoint; + update.entryPointIV = entryPointIV; + update.entryPointTag = entryPointTag; + } + + if (issuer) { + const { + ciphertext: encryptedIssuer, + iv: issuerIV, + tag: issuerTag + } = client.encryptSymmetric(issuer, key); + + update.encryptedIssuer = encryptedIssuer; + update.issuerIV = issuerIV; + update.issuerTag = issuerTag; + } + + if (cert) { + const { + ciphertext: encryptedCert, + iv: certIV, + tag: certTag + } = client.encryptSymmetric(cert, key); + + update.encryptedCert = encryptedCert; + update.certIV = certIV; + update.certTag = certTag; + } + + const ssoConfig = await SSOConfig.findOneAndUpdate( + { + organization: new Types.ObjectId(organizationId) + }, + update, + { + new: true + } + ); + + if (!ssoConfig) + throw ResourceNotFoundError({ + message: "Failed to find SSO config to update" }); - - interface PatchUpdate { - authProvider?: string; - isActive?: boolean; - encryptedEntryPoint?: string; - entryPointIV?: string; - entryPointTag?: string; - encryptedIssuer?: string; - issuerIV?: string; - issuerTag?: string; - encryptedCert?: string; - certIV?: string; - certTag?: string; - } - - const update: PatchUpdate = {}; - - if (authProvider) { - update.authProvider = authProvider; - } - - if (isActive !== undefined) { - update.isActive = isActive; - } - - const key = await BotOrgService.getSymmetricKey( - new Types.ObjectId(organizationId) - ); - - if (entryPoint) { - const { - ciphertext: encryptedEntryPoint, - iv: entryPointIV, - tag: entryPointTag - } = client.encryptSymmetric(entryPoint, key); - - update.encryptedEntryPoint = encryptedEntryPoint; - update.entryPointIV = entryPointIV; - update.entryPointTag = entryPointTag; - } - if (issuer) { - const { - ciphertext: encryptedIssuer, - iv: issuerIV, - tag: issuerTag - } = client.encryptSymmetric(issuer, key); - - update.encryptedIssuer = encryptedIssuer; - update.issuerIV = issuerIV; - update.issuerTag = issuerTag; - } + if (update.isActive !== undefined) { + const membershipOrgs = await MembershipOrg.find({ + organization: new Types.ObjectId(organizationId) + }).select("user"); - if (cert) { - const { - ciphertext: encryptedCert, - iv: certIV, - tag: certTag - } = client.encryptSymmetric(cert, key); - - update.encryptedCert = encryptedCert; - update.certIV = certIV; - update.certTag = certTag; - } - - const ssoConfig = await SSOConfig.findOneAndUpdate( + if (update.isActive) { + await User.updateMany( { - organization: new Types.ObjectId(organizationId) + _id: { + $in: membershipOrgs.map((membershipOrg) => membershipOrg.user) + } }, - update, { - new: true + authMethods: [ssoConfig.authProvider] } - ); - - if (!ssoConfig) throw ResourceNotFoundError({ - message: "Failed to find SSO config to update" - }); - - if (update.isActive !== undefined) { - const membershipOrgs = await MembershipOrg.find({ - organization: new Types.ObjectId(organizationId) - }).select("user"); - - if (update.isActive) { - await User.updateMany( - { - _id: { - $in: membershipOrgs.map((membershipOrg) => membershipOrg.user) - } - }, - { - authMethods: [ssoConfig.authProvider], - } - ); - } else { - await User.updateMany( - { - _id: { - $in: membershipOrgs.map((membershipOrg) => membershipOrg.user) - } - }, - { - authMethods: [AuthMethod.EMAIL], - } - ); + ); + } else { + await User.updateMany( + { + _id: { + $in: membershipOrgs.map((membershipOrg) => membershipOrg.user) + } + }, + { + authMethods: [AuthMethod.EMAIL] } + ); } - - return res.status(200).send(ssoConfig); -} + } + + return res.status(200).send(ssoConfig); +}; /** * Create organization SAML SSO configuration - * @param req - * @param res - * @returns + * @param req + * @param res + * @returns */ export const createSSOConfig = async (req: Request, res: Response) => { - const { - organizationId, - authProvider, - isActive, - entryPoint, - issuer, - cert - } = req.body; + const { + body: { organizationId, authProvider, isActive, entryPoint, issuer, cert } + } = await validateRequest(reqValidator.CreateSsoConfigv1, req); - const plan = await EELicenseService.getPlan(new Types.ObjectId(organizationId)); - - if (!plan.samlSSO) return res.status(400).send({ - message: "Failed to create SAML SSO configuration due to plan restriction. Upgrade plan to add SSO configuration." + const { permission } = await getUserOrgPermissions(req.user._id, organizationId); + ForbiddenError.from(permission).throwUnlessCan( + OrgPermissionActions.Create, + OrgPermissionSubjects.Sso + ); + + const plan = await EELicenseService.getPlan(new Types.ObjectId(organizationId)); + + if (!plan.samlSSO) + return res.status(400).send({ + message: + "Failed to create SAML SSO configuration due to plan restriction. Upgrade plan to add SSO configuration." }); - - const key = await BotOrgService.getSymmetricKey( - new Types.ObjectId(organizationId) - ); - const { - ciphertext: encryptedEntryPoint, - iv: entryPointIV, - tag: entryPointTag - } = client.encryptSymmetric(entryPoint, key); + const key = await BotOrgService.getSymmetricKey(new Types.ObjectId(organizationId)); - const { - ciphertext: encryptedIssuer, - iv: issuerIV, - tag: issuerTag - } = client.encryptSymmetric(issuer, key); + const { + ciphertext: encryptedEntryPoint, + iv: entryPointIV, + tag: entryPointTag + } = client.encryptSymmetric(entryPoint, key); - const { - ciphertext: encryptedCert, - iv: certIV, - tag: certTag - } = client.encryptSymmetric(cert, key); - - const ssoConfig = await new SSOConfig({ - organization: new Types.ObjectId(organizationId), - authProvider, - isActive, - encryptedEntryPoint, - entryPointIV, - entryPointTag, - encryptedIssuer, - issuerIV, - issuerTag, - encryptedCert, - certIV, - certTag - }).save(); + const { + ciphertext: encryptedIssuer, + iv: issuerIV, + tag: issuerTag + } = client.encryptSymmetric(issuer, key); - return res.status(200).send(ssoConfig); -} \ No newline at end of file + const { + ciphertext: encryptedCert, + iv: certIV, + tag: certTag + } = client.encryptSymmetric(cert, key); + + const ssoConfig = await new SSOConfig({ + organization: new Types.ObjectId(organizationId), + authProvider, + isActive, + encryptedEntryPoint, + entryPointIV, + entryPointTag, + encryptedIssuer, + issuerIV, + issuerTag, + encryptedCert, + certIV, + certTag + }).save(); + + return res.status(200).send(ssoConfig); +}; diff --git a/backend/src/ee/controllers/v1/workspaceController.ts b/backend/src/ee/controllers/v1/workspaceController.ts index 1e21dd591..14ea8978d 100644 --- a/backend/src/ee/controllers/v1/workspaceController.ts +++ b/backend/src/ee/controllers/v1/workspaceController.ts @@ -22,16 +22,32 @@ import { getLatestSecretVersionIds } from "../../helpers/secretVersion"; import { searchByFolderId } from "../../../services/FolderService"; import { EEAuditLogService, EELicenseService } from "../../services"; import { extractIPDetails, isValidIpOrCidr } from "../../../utils/ip"; +import { validateRequest } from "../../../helpers/validation"; +import { + AddWorkspaceTrustedIpV1, + DeleteWorkspaceTrustedIpV1, + GetWorkspaceAuditLogActorFilterOptsV1, + GetWorkspaceAuditLogsV1, + GetWorkspaceLogsV1, + GetWorkspaceSecretSnapshotsCountV1, + GetWorkspaceSecretSnapshotsV1, + GetWorkspaceTrustedIpsV1, + RollbackWorkspaceSecretSnapshotV1, + UpdateWorkspaceTrustedIpV1 +} from "../../../validation"; +import { + ProjectPermissionActions, + ProjectPermissionSub, + getUserProjectPermissions +} from "../../services/ProjectRoleService"; +import { ForbiddenError } from "@casl/ability"; /** * Return secret snapshots for workspace with id [workspaceId] * @param req * @param res */ -export const getWorkspaceSecretSnapshots = async ( - req: Request, - res: Response -) => { +export const getWorkspaceSecretSnapshots = async (req: Request, res: Response) => { /* #swagger.summary = 'Return project secret snapshot ids' #swagger.description = 'Return project secret snapshots ids' @@ -77,23 +93,28 @@ export const getWorkspaceSecretSnapshots = async ( } } */ - const { workspaceId } = req.params; - const { environment, folderId } = req.query; + const { + params: { workspaceId }, + query: { environment, folderId, offset, limit } + } = await validateRequest(GetWorkspaceSecretSnapshotsV1, req); - const offset: number = parseInt(req.query.offset as string); - const limit: number = parseInt(req.query.limit as string); + const { permission } = await getUserProjectPermissions(req.user._id, workspaceId); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Read, + ProjectPermissionSub.SecretRollback + ); const secretSnapshots = await SecretSnapshot.find({ workspace: workspaceId, environment, - folderId: folderId || "root", + folderId: folderId || "root" }) .sort({ createdAt: -1 }) .skip(offset) .limit(limit); return res.status(200).send({ - secretSnapshots, + secretSnapshots }); }; @@ -102,21 +123,26 @@ export const getWorkspaceSecretSnapshots = async ( * @param req * @param res */ -export const getWorkspaceSecretSnapshotsCount = async ( - req: Request, - res: Response -) => { - const { workspaceId } = req.params; - const { environment, folderId } = req.query; +export const getWorkspaceSecretSnapshotsCount = async (req: Request, res: Response) => { + const { + params: { workspaceId }, + query: { environment, folderId } + } = await validateRequest(GetWorkspaceSecretSnapshotsCountV1, req); + + const { permission } = await getUserProjectPermissions(req.user._id, workspaceId); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Read, + ProjectPermissionSub.SecretRollback + ); const count = await SecretSnapshot.countDocuments({ workspace: workspaceId, environment, - folderId: folderId || "root", + folderId: folderId || "root" }); return res.status(200).send({ - count, + count }); }; @@ -126,10 +152,7 @@ export const getWorkspaceSecretSnapshotsCount = async ( * @param res * @returns */ -export const rollbackWorkspaceSecretSnapshot = async ( - req: Request, - res: Response -) => { +export const rollbackWorkspaceSecretSnapshot = async (req: Request, res: Response) => { /* #swagger.summary = 'Roll back project secrets to those captured in a secret snapshot version.' #swagger.description = 'Roll back project secrets to those captured in a secret snapshot version.' @@ -181,19 +204,27 @@ export const rollbackWorkspaceSecretSnapshot = async ( } */ - const { workspaceId } = req.params; - const { version, environment, folderId = "root" } = req.body; + const { + params: { workspaceId }, + body: { folderId, environment, version } + } = await validateRequest(RollbackWorkspaceSecretSnapshotV1, req); + + const { permission } = await getUserProjectPermissions(req.user._id, workspaceId); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Create, + ProjectPermissionSub.SecretRollback + ); // validate secret snapshot const secretSnapshot = await SecretSnapshot.findOne({ workspace: workspaceId, version, environment, - folderId: folderId, + folderId: folderId }) .populate<{ secretVersions: ISecretVersion[] }>({ path: "secretVersions", - select: "+secretBlindIndex", + select: "+secretBlindIndex" }) .populate<{ folderVersion: TFolderRootVersionSchema }>("folderVersion"); @@ -202,13 +233,13 @@ export const rollbackWorkspaceSecretSnapshot = async ( const snapshotFolderTree = secretSnapshot.folderVersion; const latestFolderTree = await Folder.findOne({ workspace: workspaceId, - environment, + environment }); const latestFolderVersion = await FolderVersion.findOne({ environment, workspace: workspaceId, - "nodes.id": folderId, + "nodes.id": folderId }).sort({ "nodes.version": -1 }); const oldSecretVersionsObj: Record = {}; @@ -222,8 +253,7 @@ export const rollbackWorkspaceSecretSnapshot = async ( // the parent node from current latest one // this will be modified according to the snapshot and latest snapshots - const newFolderTree = - latestFolderTree && searchByFolderId(latestFolderTree.nodes, folderId); + const newFolderTree = latestFolderTree && searchByFolderId(latestFolderTree.nodes, folderId); if (newFolderTree) { newFolderTree.children = snapshotFolderTree?.nodes?.children || []; @@ -252,43 +282,43 @@ export const rollbackWorkspaceSecretSnapshot = async ( workspace: new Types.ObjectId(workspaceId), environment, folderId: { - $in: Object.keys(groupByFolderId), - }, - }, + $in: Object.keys(groupByFolderId) + } + } }; const sortByFolderIdAndVersion: PipelineStage = { - $sort: { folderId: 1, version: -1 }, + $sort: { folderId: 1, version: -1 } }; const pickLatestVersionOfEachFolder = { $group: { _id: "$folderId", latestVersion: { $first: "$version" }, doc: { - $first: "$$ROOT", - }, - }, + $first: "$$ROOT" + } + } }; const populateSecVersion = { $lookup: { from: SecretVersion.collection.name, localField: "doc.secretVersions", foreignField: "_id", - as: "doc.secretVersions", - }, + as: "doc.secretVersions" + } }; const populateFolderVersion = { $lookup: { from: FolderVersion.collection.name, localField: "doc.folderVersion", foreignField: "_id", - as: "doc.folderVersion", - }, + as: "doc.folderVersion" + } }; const unwindFolderVerField = { $unwind: { path: "$doc.folderVersion", - preserveNullAndEmptyArrays: true, - }, + preserveNullAndEmptyArrays: true + } }; const latestSnapshotsByFolders: Array<{ doc: typeof secretSnapshot }> = await SecretSnapshot.aggregate([ @@ -297,7 +327,7 @@ export const rollbackWorkspaceSecretSnapshot = async ( pickLatestVersionOfEachFolder, populateSecVersion, populateFolderVersion, - unwindFolderVerField, + unwindFolderVerField ]); // recursive snapshotting each level @@ -327,7 +357,7 @@ export const rollbackWorkspaceSecretSnapshot = async ( // TODO: fix any const latestSecretVersionIds = await getLatestSecretVersionIds({ - secretIds, + secretIds }); // TODO: fix any @@ -335,32 +365,31 @@ export const rollbackWorkspaceSecretSnapshot = async ( await SecretVersion.find( { _id: { - $in: latestSecretVersionIds.map((s) => s.versionId), - }, + $in: latestSecretVersionIds.map((s) => s.versionId) + } }, "secret version" ) ).reduce( (accumulator, s) => ({ ...accumulator, - [`${s.secret.toString()}`]: s, + [`${s.secret.toString()}`]: s }), {} ); const secDelQuery: Record = { workspace: workspaceId, - environment, + environment // undefined means root thus collect all secrets }; - if (folderId !== "root" && folderIds.length) - secDelQuery.folder = { $in: folderIds }; + if (folderId !== "root" && folderIds.length) secDelQuery.folder = { $in: folderIds }; // delete existing secrets await Secret.deleteMany(secDelQuery); await Folder.deleteOne({ workspace: workspaceId, - environment, + environment }); // add secrets @@ -382,7 +411,7 @@ export const rollbackWorkspaceSecretSnapshot = async ( createdAt, algorithm, keyEncoding, - folder: secFolderId, + folder: secFolderId } = oldSecretVersionsObj[sv]; return { @@ -405,7 +434,7 @@ export const rollbackWorkspaceSecretSnapshot = async ( createdAt, algorithm, keyEncoding, - folder: secFolderId, + folder: secFolderId }; }) ); @@ -429,7 +458,7 @@ export const rollbackWorkspaceSecretSnapshot = async ( secretValueTag, algorithm, keyEncoding, - folder: secFolderId, + folder: secFolderId }) => ({ _id: new Types.ObjectId(), secret: _id, @@ -448,7 +477,7 @@ export const rollbackWorkspaceSecretSnapshot = async ( secretValueTag, algorithm, keyEncoding, - folder: secFolderId, + folder: secFolderId }) ) ); @@ -464,7 +493,7 @@ export const rollbackWorkspaceSecretSnapshot = async ( const newFolderVersion = new FolderVersion({ workspace: workspaceId, environment, - nodes: newFolderTree, + nodes: newFolderTree }); await newFolderVersion.save(); } @@ -473,13 +502,11 @@ export const rollbackWorkspaceSecretSnapshot = async ( await SecretVersion.updateMany( { secret: { - $in: Object.keys(oldSecretVersionsObj).map( - (sv) => oldSecretVersionsObj[sv].secret - ), - }, + $in: Object.keys(oldSecretVersionsObj).map((sv) => oldSecretVersionsObj[sv].secret) + } }, { - isDeleted: false, + isDeleted: false } ); @@ -487,11 +514,11 @@ export const rollbackWorkspaceSecretSnapshot = async ( await EESecretService.takeSecretSnapshot({ workspaceId: new Types.ObjectId(workspaceId), environment, - folderId, + folderId }); return res.status(200).send({ - secrets, + secrets }); }; @@ -568,13 +595,16 @@ export const getWorkspaceLogs = async (req: Request, res: Response) => { } } */ - const { workspaceId } = req.params; + const { + query: { limit, offset, userId, sortBy, actionNames }, + params: { workspaceId } + } = await validateRequest(GetWorkspaceLogsV1, req); - const offset: number = parseInt(req.query.offset as string); - const limit: number = parseInt(req.query.limit as string); - const sortBy: string = req.query.sortBy as string; - const userId: string = req.query.userId as string; - const actionNames: string = req.query.actionNames as string; + const { permission } = await getUserProjectPermissions(req.user._id, workspaceId); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Read, + ProjectPermissionSub.AuditLogs + ); const logs = await Log.find({ workspace: workspaceId, @@ -582,10 +612,10 @@ export const getWorkspaceLogs = async (req: Request, res: Response) => { ...(actionNames ? { actionNames: { - $in: actionNames.split(","), - }, + $in: actionNames.split(",") + } } - : {}), + : {}) }) .sort({ createdAt: sortBy === "recent" ? -1 : 1 }) .skip(offset) @@ -594,93 +624,109 @@ export const getWorkspaceLogs = async (req: Request, res: Response) => { .populate("user serviceAccount serviceTokenData"); return res.status(200).send({ - logs, + logs }); }; /** * Return audit logs for workspace with id [workspaceId] * @param req - * @param res + * @param res */ export const getWorkspaceAuditLogs = async (req: Request, res: Response) => { - const { workspaceId } = req.params; - const eventType = req.query.eventType; - const userAgentType = req.query.userAgentType; - const actor = req.query.actor as string | undefined; - const offset: number = parseInt(req.query.offset as string); - const limit: number = parseInt(req.query.limit as string); - - const startDate = req.query.startDate as string; - const endDate = req.query.endDate as string; - + const { + query: { limit, offset, endDate, eventType, startDate, userAgentType, actor }, + params: { workspaceId } + } = await validateRequest(GetWorkspaceAuditLogsV1, req); + + const { permission } = await getUserProjectPermissions(req.user._id, workspaceId); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Read, + ProjectPermissionSub.AuditLogs + ); + const query = { workspace: new Types.ObjectId(workspaceId), - ...(eventType ? { - "event.type": eventType - } : {}), - ...(userAgentType ? { - userAgentType - } : {}), - ...(actor ? { - "actor.type": actor.split("-", 2)[0], - ...(actor.split("-", 2)[0] === ActorType.USER ? { - "actor.metadata.userId": actor.split("-", 2)[1] - } : { - "actor.metadata.serviceId": actor.split("-", 2)[1] - }) - } : {}), - ...(startDate || endDate ? { - createdAt: { - ...(startDate && { $gte: new Date(startDate) }), - ...(endDate && { $lte: new Date(endDate) }) - } - } : {}) - } - - const auditLogs = await AuditLog.find(query) - .sort({ createdAt: -1 }) - .skip(offset) - .limit(limit); + ...(eventType + ? { + "event.type": eventType + } + : {}), + ...(userAgentType + ? { + userAgentType + } + : {}), + ...(actor + ? { + "actor.type": actor.split("-", 2)[0], + ...(actor.split("-", 2)[0] === ActorType.USER + ? { + "actor.metadata.userId": actor.split("-", 2)[1] + } + : { + "actor.metadata.serviceId": actor.split("-", 2)[1] + }) + } + : {}), + ...(startDate || endDate + ? { + createdAt: { + ...(startDate && { $gte: new Date(startDate) }), + ...(endDate && { $lte: new Date(endDate) }) + } + } + : {}) + }; + + const auditLogs = await AuditLog.find(query).sort({ createdAt: -1 }).skip(offset).limit(limit); const totalCount = await AuditLog.countDocuments(query); - + return res.status(200).send({ auditLogs, totalCount }); -} +}; /** * Return audit log actor filter options for workspace with id [workspaceId] * @param req - * @param res + * @param res */ export const getWorkspaceAuditLogActorFilterOpts = async (req: Request, res: Response) => { - const { workspaceId } = req.params; - + const { + params: { workspaceId } + } = await validateRequest(GetWorkspaceAuditLogActorFilterOptsV1, req); + + const { permission } = await getUserProjectPermissions(req.user._id, workspaceId); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Read, + ProjectPermissionSub.AuditLogs + ); + const userIds = await Membership.distinct("user", { workspace: new Types.ObjectId(workspaceId) }); - const userActors: UserActor[] = (await User.find({ - _id: { - $in: userIds - } - }) - .select("email")) - .map((user) => ({ + const userActors: UserActor[] = ( + await User.find({ + _id: { + $in: userIds + } + }).select("email") + ).map((user) => ({ type: ActorType.USER, metadata: { userId: user._id.toString(), email: user.email } })); - - const serviceActors: ServiceActor[] = (await ServiceTokenData.find({ - workspace: new Types.ObjectId(workspaceId) - }) - .select("name")) - .map((serviceTokenData) => ({ + + const serviceActors: ServiceActor[] = ( + await ServiceTokenData.find({ + workspace: new Types.ObjectId(workspaceId) + }).select("name") + ).map((serviceTokenData) => ({ type: ActorType.SERVICE, metadata: { serviceId: serviceTokenData._id.toString(), @@ -691,50 +737,65 @@ export const getWorkspaceAuditLogActorFilterOpts = async (req: Request, res: Res return res.status(200).send({ actors: [...userActors, ...serviceActors] }); -} +}; /** * Return trusted ips for workspace with id [workspaceId] * @param req - * @param res + * @param res */ export const getWorkspaceTrustedIps = async (req: Request, res: Response) => { - const { workspaceId } = req.params; + const { + params: { workspaceId } + } = await validateRequest(GetWorkspaceTrustedIpsV1, req); + + const { permission } = await getUserProjectPermissions(req.user._id, workspaceId); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Read, + ProjectPermissionSub.IpAllowList + ); const trustedIps = await TrustedIP.find({ workspace: new Types.ObjectId(workspaceId) }); - + return res.status(200).send({ trustedIps }); -} +}; /** * Add a trusted ip to workspace with id [workspaceId] - * @param req - * @param res + * @param req + * @param res */ export const addWorkspaceTrustedIp = async (req: Request, res: Response) => { - const { workspaceId } = req.params; const { - ipAddress: ip, - comment, - isActive - } = req.body; - + params: { workspaceId }, + body: { comment, isActive, ipAddress: ip } + } = await validateRequest(AddWorkspaceTrustedIpV1, req); + + const { permission } = await getUserProjectPermissions(req.user._id, workspaceId); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Create, + ProjectPermissionSub.IpAllowList + ); + const plan = await EELicenseService.getPlan(req.workspace.organization); - - if (!plan.ipAllowlisting) return res.status(400).send({ - message: "Failed to add IP access range due to plan restriction. Upgrade plan to add IP access range." - }); - + + if (!plan.ipAllowlisting) + return res.status(400).send({ + message: + "Failed to add IP access range due to plan restriction. Upgrade plan to add IP access range." + }); + const isValidIPOrCidr = isValidIpOrCidr(ip); - - if (!isValidIPOrCidr) return res.status(400).send({ - message: "The IP is not a valid IPv4, IPv6, or CIDR block" - }); - + + if (!isValidIPOrCidr) + return res.status(400).send({ + message: "The IP is not a valid IPv4, IPv6, or CIDR block" + }); + const { ipAddress, type, prefix } = extractIPDetails(ip); const trustedIp = await new TrustedIP({ @@ -743,9 +804,9 @@ export const addWorkspaceTrustedIp = async (req: Request, res: Response) => { type, prefix, isActive, - comment, + comment }).save(); - + await EEAuditLogService.createAuditLog( req.authData, { @@ -764,32 +825,40 @@ export const addWorkspaceTrustedIp = async (req: Request, res: Response) => { return res.status(200).send({ trustedIp }); -} +}; /** * Update trusted ip with id [trustedIpId] workspace with id [workspaceId] - * @param req - * @param res + * @param req + * @param res */ export const updateWorkspaceTrustedIp = async (req: Request, res: Response) => { - const { workspaceId, trustedIpId } = req.params; const { - ipAddress: ip, - comment - } = req.body; + params: { workspaceId, trustedIpId }, + body: { ipAddress: ip, comment } + } = await validateRequest(UpdateWorkspaceTrustedIpV1, req); + + const { permission } = await getUserProjectPermissions(req.user._id, workspaceId); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Edit, + ProjectPermissionSub.IpAllowList + ); const plan = await EELicenseService.getPlan(req.workspace.organization); - if (!plan.ipAllowlisting) return res.status(400).send({ - message: "Failed to update IP access range due to plan restriction. Upgrade plan to update IP access range." - }); + if (!plan.ipAllowlisting) + return res.status(400).send({ + message: + "Failed to update IP access range due to plan restriction. Upgrade plan to update IP access range." + }); const isValidIPOrCidr = isValidIpOrCidr(ip); - - if (!isValidIPOrCidr) return res.status(400).send({ - message: "The IP is not a valid IPv4, IPv6, or CIDR block" - }); - + + if (!isValidIPOrCidr) + return res.status(400).send({ + message: "The IP is not a valid IPv4, IPv6, or CIDR block" + }); + const { ipAddress, type, prefix } = extractIPDetails(ip); const updateObject: { @@ -799,33 +868,34 @@ export const updateWorkspaceTrustedIp = async (req: Request, res: Response) => { prefix?: number; $unset?: { prefix: number; - } + }; } = { ipAddress, type, comment }; - + if (prefix !== undefined) { updateObject.prefix = prefix; } else { updateObject.$unset = { prefix: 1 }; } - + const trustedIp = await TrustedIP.findOneAndUpdate( { _id: new Types.ObjectId(trustedIpId), - workspace: new Types.ObjectId(workspaceId), + workspace: new Types.ObjectId(workspaceId) }, updateObject, { new: true } ); - - if (!trustedIp) return res.status(400).send({ - message: "Failed to update trusted IP" - }); + + if (!trustedIp) + return res.status(400).send({ + message: "Failed to update trusted IP" + }); await EEAuditLogService.createAuditLog( req.authData, @@ -841,34 +911,45 @@ export const updateWorkspaceTrustedIp = async (req: Request, res: Response) => { workspaceId: trustedIp.workspace } ); - + return res.status(200).send({ trustedIp }); -} +}; /** * Delete IP access range from workspace with id [workspaceId] - * @param req - * @param res + * @param req + * @param res */ export const deleteWorkspaceTrustedIp = async (req: Request, res: Response) => { - const { workspaceId, trustedIpId } = req.params; + const { + params: { workspaceId, trustedIpId } + } = await validateRequest(DeleteWorkspaceTrustedIpV1, req); + + const { permission } = await getUserProjectPermissions(req.user._id, workspaceId); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Delete, + ProjectPermissionSub.IpAllowList + ); const plan = await EELicenseService.getPlan(req.workspace.organization); - - if (!plan.ipAllowlisting) return res.status(400).send({ - message: "Failed to delete IP access range due to plan restriction. Upgrade plan to delete IP access range." - }); - + + if (!plan.ipAllowlisting) + return res.status(400).send({ + message: + "Failed to delete IP access range due to plan restriction. Upgrade plan to delete IP access range." + }); + const trustedIp = await TrustedIP.findOneAndDelete({ _id: new Types.ObjectId(trustedIpId), workspace: new Types.ObjectId(workspaceId) }); - - if (!trustedIp) return res.status(400).send({ - message: "Failed to delete trusted IP" - }); + + if (!trustedIp) + return res.status(400).send({ + message: "Failed to delete trusted IP" + }); await EEAuditLogService.createAuditLog( req.authData, @@ -888,4 +969,4 @@ export const deleteWorkspaceTrustedIp = async (req: Request, res: Response) => { return res.status(200).send({ trustedIp }); -} \ No newline at end of file +}; diff --git a/backend/src/ee/models/role.ts b/backend/src/ee/models/role.ts new file mode 100644 index 000000000..643acedcd --- /dev/null +++ b/backend/src/ee/models/role.ts @@ -0,0 +1,55 @@ +import { Schema, Types, model } from "mongoose"; + +export interface IRole { + _id: Types.ObjectId; + name: string; + description: string; + slug: string; + permissions: Array; + workspace: Types.ObjectId; + organization: Types.ObjectId; + isOrgRole: boolean; +} + +const roleSchema = new Schema( + { + name: { + type: String, + required: true + }, + organization: { + type: Schema.Types.ObjectId, + ref: "Organization", + required: true + }, + workspace: { + type: Schema.Types.ObjectId, + ref: "Workspace" + }, + isOrgRole: { + type: Boolean, + required: true, + select: false + }, + description: { + type: String + }, + slug: { + type: String, + required: true + }, + permissions: { + type: Array, + required: true + } + }, + { + timestamps: true + } +); + +roleSchema.index({ organization: 1, workspace: 1 }); + +const Role = model("Role", roleSchema); + +export default Role; diff --git a/backend/src/ee/routes/v1/action.ts b/backend/src/ee/routes/v1/action.ts index b77a84188..4700c5a59 100644 --- a/backend/src/ee/routes/v1/action.ts +++ b/backend/src/ee/routes/v1/action.ts @@ -1,17 +1,8 @@ import express from "express"; const router = express.Router(); -import { - validateRequest, -} from "../../../middleware"; -import { param } from "express-validator"; import { actionController } from "../../controllers/v1"; // TODO: put into action controller -router.get( - "/:actionId", - param("actionId").exists().trim(), - validateRequest, - actionController.getAction -); +router.get("/:actionId", actionController.getAction); -export default router; \ No newline at end of file +export default router; diff --git a/backend/src/ee/routes/v1/cloudProducts.ts b/backend/src/ee/routes/v1/cloudProducts.ts index 81256f378..23912222b 100644 --- a/backend/src/ee/routes/v1/cloudProducts.ts +++ b/backend/src/ee/routes/v1/cloudProducts.ts @@ -1,21 +1,16 @@ import express from "express"; const router = express.Router(); -import { - requireAuth, - validateRequest, -} from "../../../middleware"; -import { query } from "express-validator"; +import { requireAuth, validateRequest } from "../../../middleware"; import { cloudProductsController } from "../../controllers/v1"; import { AuthMode } from "../../../variables"; router.get( - "/", - requireAuth({ - acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY], - }), - query("billing-cycle").exists().isIn(["monthly", "yearly"]), - validateRequest, - cloudProductsController.getCloudProducts + "/", + requireAuth({ + acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY] + }), + validateRequest, + cloudProductsController.getCloudProducts ); -export default router; \ No newline at end of file +export default router; diff --git a/backend/src/ee/routes/v1/index.ts b/backend/src/ee/routes/v1/index.ts index be4847cef..9b8d1bea3 100644 --- a/backend/src/ee/routes/v1/index.ts +++ b/backend/src/ee/routes/v1/index.ts @@ -7,15 +7,17 @@ import workspace from "./workspace"; import action from "./action"; import cloudProducts from "./cloudProducts"; import secretScanning from "./secretScanning"; +import roles from "./role"; export { - secret, - secretSnapshot, - organizations, - sso, - users, - workspace, - action, - cloudProducts, - secretScanning -} \ No newline at end of file + secret, + secretSnapshot, + organizations, + sso, + users, + workspace, + action, + cloudProducts, + secretScanning, + roles +}; diff --git a/backend/src/ee/routes/v1/organizations.ts b/backend/src/ee/routes/v1/organizations.ts index 6506d3afa..bbe5019b4 100644 --- a/backend/src/ee/routes/v1/organizations.ts +++ b/backend/src/ee/routes/v1/organizations.ts @@ -1,237 +1,127 @@ import express from "express"; const router = express.Router(); -import { - requireAuth, - requireOrganizationAuth, - validateRequest, -} from "../../../middleware"; -import { body, param, query } from "express-validator"; +import { requireAuth } from "../../../middleware"; import { organizationsController } from "../../controllers/v1"; -import { - ACCEPTED, ADMIN, AuthMode, MEMBER, OWNER -} from "../../../variables"; +import { AuthMode } from "../../../variables"; router.get( - "/:organizationId/plans/table", - requireAuth({ - acceptedAuthModes: [AuthMode.JWT], - }), - requireOrganizationAuth({ - acceptedRoles: [OWNER, ADMIN, MEMBER], - acceptedStatuses: [ACCEPTED], - }), - param("organizationId").exists().trim(), - query("billingCycle").exists().isString().isIn(["monthly", "yearly"]), - validateRequest, - organizationsController.getOrganizationPlansTable + "/:organizationId/plans/table", + requireAuth({ + acceptedAuthModes: [AuthMode.JWT] + }), + organizationsController.getOrganizationPlansTable ); router.get( - "/:organizationId/plan", - requireAuth({ - acceptedAuthModes: [AuthMode.JWT], - }), - requireOrganizationAuth({ - acceptedRoles: [OWNER, ADMIN, MEMBER], - acceptedStatuses: [ACCEPTED], - }), - param("organizationId").exists().trim(), - query("workspaceId").optional().isString(), - validateRequest, - organizationsController.getOrganizationPlan + "/:organizationId/plan", + requireAuth({ + acceptedAuthModes: [AuthMode.JWT] + }), + organizationsController.getOrganizationPlan ); router.post( - "/:organizationId/session/trial", - requireAuth({ - acceptedAuthModes: [AuthMode.JWT], - }), - requireOrganizationAuth({ - acceptedRoles: [OWNER, ADMIN, MEMBER], - acceptedStatuses: [ACCEPTED], - }), - param("organizationId").exists().trim(), - body("success_url").exists().trim(), - validateRequest, - organizationsController.startOrganizationTrial + "/:organizationId/session/trial", + requireAuth({ + acceptedAuthModes: [AuthMode.JWT] + }), + organizationsController.startOrganizationTrial ); router.get( - "/:organizationId/plan/billing", - requireAuth({ - acceptedAuthModes: [AuthMode.JWT], - }), - requireOrganizationAuth({ - acceptedRoles: [OWNER, ADMIN, MEMBER], - acceptedStatuses: [ACCEPTED], - }), - param("organizationId").exists().trim(), - query("workspaceId").optional().isString(), - validateRequest, - organizationsController.getOrganizationPlanBillingInfo + "/:organizationId/plan/billing", + requireAuth({ + acceptedAuthModes: [AuthMode.JWT] + }), + organizationsController.getOrganizationPlanBillingInfo ); router.get( - "/:organizationId/plan/table", - requireAuth({ - acceptedAuthModes: [AuthMode.JWT], - }), - requireOrganizationAuth({ - acceptedRoles: [OWNER, ADMIN, MEMBER], - acceptedStatuses: [ACCEPTED], - }), - param("organizationId").exists().trim(), - query("workspaceId").optional().isString(), - validateRequest, - organizationsController.getOrganizationPlanTable + "/:organizationId/plan/table", + requireAuth({ + acceptedAuthModes: [AuthMode.JWT] + }), + organizationsController.getOrganizationPlanTable ); router.get( - "/:organizationId/billing-details", - requireAuth({ - acceptedAuthModes: [AuthMode.JWT], - }), - requireOrganizationAuth({ - acceptedRoles: [OWNER, ADMIN, MEMBER], - acceptedStatuses: [ACCEPTED], - }), - param("organizationId").exists().trim(), - validateRequest, - organizationsController.getOrganizationBillingDetails + "/:organizationId/billing-details", + requireAuth({ + acceptedAuthModes: [AuthMode.JWT] + }), + organizationsController.getOrganizationBillingDetails ); router.patch( - "/:organizationId/billing-details", - requireAuth({ - acceptedAuthModes: [AuthMode.JWT], - }), - requireOrganizationAuth({ - acceptedRoles: [OWNER, ADMIN, MEMBER], - acceptedStatuses: [ACCEPTED], - }), - param("organizationId").exists().trim(), - body("email").optional().isString().trim(), - body("name").optional().isString().trim(), - validateRequest, - organizationsController.updateOrganizationBillingDetails + "/:organizationId/billing-details", + requireAuth({ + acceptedAuthModes: [AuthMode.JWT] + }), + organizationsController.updateOrganizationBillingDetails ); router.get( - "/:organizationId/billing-details/payment-methods", - requireAuth({ - acceptedAuthModes: [AuthMode.JWT], - }), - requireOrganizationAuth({ - acceptedRoles: [OWNER, ADMIN, MEMBER], - acceptedStatuses: [ACCEPTED], - }), - param("organizationId").exists().trim(), - validateRequest, - organizationsController.getOrganizationPmtMethods + "/:organizationId/billing-details/payment-methods", + requireAuth({ + acceptedAuthModes: [AuthMode.JWT] + }), + organizationsController.getOrganizationPmtMethods ); router.post( - "/:organizationId/billing-details/payment-methods", - requireAuth({ - acceptedAuthModes: [AuthMode.JWT], - }), - requireOrganizationAuth({ - acceptedRoles: [OWNER, ADMIN, MEMBER], - acceptedStatuses: [ACCEPTED], - }), - param("organizationId").exists().trim(), - body("success_url").exists().isString(), - body("cancel_url").exists().isString(), - validateRequest, - organizationsController.addOrganizationPmtMethod + "/:organizationId/billing-details/payment-methods", + requireAuth({ + acceptedAuthModes: [AuthMode.JWT] + }), + organizationsController.addOrganizationPmtMethod ); router.delete( - "/:organizationId/billing-details/payment-methods/:pmtMethodId", - requireAuth({ - acceptedAuthModes: [AuthMode.JWT], - }), - requireOrganizationAuth({ - acceptedRoles: [OWNER, ADMIN, MEMBER], - acceptedStatuses: [ACCEPTED], - }), - param("organizationId").exists().trim(), - param("pmtMethodId").exists().trim(), - validateRequest, - organizationsController.deleteOrganizationPmtMethod + "/:organizationId/billing-details/payment-methods/:pmtMethodId", + requireAuth({ + acceptedAuthModes: [AuthMode.JWT] + }), + organizationsController.deleteOrganizationPmtMethod ); router.get( - "/:organizationId/billing-details/tax-ids", - requireAuth({ - acceptedAuthModes: [AuthMode.JWT], - }), - requireOrganizationAuth({ - acceptedRoles: [OWNER, ADMIN, MEMBER], - acceptedStatuses: [ACCEPTED], - }), - param("organizationId").exists().trim(), - validateRequest, - organizationsController.getOrganizationTaxIds + "/:organizationId/billing-details/tax-ids", + requireAuth({ + acceptedAuthModes: [AuthMode.JWT] + }), + organizationsController.getOrganizationTaxIds ); router.post( - "/:organizationId/billing-details/tax-ids", - requireAuth({ - acceptedAuthModes: [AuthMode.JWT], - }), - requireOrganizationAuth({ - acceptedRoles: [OWNER, ADMIN, MEMBER], - acceptedStatuses: [ACCEPTED], - }), - param("organizationId").exists().trim(), - body("type").exists().isString(), - body("value").exists().isString(), - validateRequest, - organizationsController.addOrganizationTaxId + "/:organizationId/billing-details/tax-ids", + requireAuth({ + acceptedAuthModes: [AuthMode.JWT] + }), + organizationsController.addOrganizationTaxId ); router.delete( - "/:organizationId/billing-details/tax-ids/:taxId", - requireAuth({ - acceptedAuthModes: [AuthMode.JWT], - }), - requireOrganizationAuth({ - acceptedRoles: [OWNER, ADMIN, MEMBER], - acceptedStatuses: [ACCEPTED], - }), - param("organizationId").exists().trim(), - param("taxId").exists().trim(), - validateRequest, - organizationsController.deleteOrganizationTaxId + "/:organizationId/billing-details/tax-ids/:taxId", + requireAuth({ + acceptedAuthModes: [AuthMode.JWT] + }), + organizationsController.deleteOrganizationTaxId ); router.get( - "/:organizationId/invoices", - requireAuth({ - acceptedAuthModes: [AuthMode.JWT], - }), - requireOrganizationAuth({ - acceptedRoles: [OWNER, ADMIN, MEMBER], - acceptedStatuses: [ACCEPTED], - }), - param("organizationId").exists().trim(), - validateRequest, - organizationsController.getOrganizationInvoices + "/:organizationId/invoices", + requireAuth({ + acceptedAuthModes: [AuthMode.JWT] + }), + organizationsController.getOrganizationInvoices ); router.get( - "/:organizationId/licenses", - requireAuth({ - acceptedAuthModes: [AuthMode.JWT], - }), - requireOrganizationAuth({ - acceptedRoles: [OWNER, ADMIN], - acceptedStatuses: [ACCEPTED], - }), - param("organizationId").exists().trim(), - validateRequest, - organizationsController.getOrganizationLicenses + "/:organizationId/licenses", + requireAuth({ + acceptedAuthModes: [AuthMode.JWT] + }), + organizationsController.getOrganizationLicenses ); -export default router; \ No newline at end of file +export default router; diff --git a/backend/src/ee/routes/v1/role.ts b/backend/src/ee/routes/v1/role.ts new file mode 100644 index 000000000..0794b6b19 --- /dev/null +++ b/backend/src/ee/routes/v1/role.ts @@ -0,0 +1,33 @@ +import express from "express"; +import { roleController } from "../../controllers/v1"; +import { requireAuth } from "../../../middleware"; +import { AuthMode } from "../../../variables"; + +const router = express.Router(); + +router.post("/", requireAuth({ acceptedAuthModes: [AuthMode.JWT] }), roleController.createRole); + +router.patch("/:id", requireAuth({ acceptedAuthModes: [AuthMode.JWT] }), roleController.updateRole); + +router.delete( + "/:id", + requireAuth({ acceptedAuthModes: [AuthMode.JWT] }), + roleController.deleteRole +); + +router.get("/", requireAuth({ acceptedAuthModes: [AuthMode.JWT] }), roleController.getRoles); + +// get a user permissions in an org +router.get( + "/organization/:orgId/permissions", + requireAuth({ acceptedAuthModes: [AuthMode.JWT] }), + roleController.getUserPermissions +); + +router.get( + "/workspace/:workspaceId/permissions", + requireAuth({ acceptedAuthModes: [AuthMode.JWT] }), + roleController.getUserWorkspacePermissions +); + +export default router; diff --git a/backend/src/ee/routes/v1/secret.ts b/backend/src/ee/routes/v1/secret.ts index 0eb23ee80..bdbdec965 100644 --- a/backend/src/ee/routes/v1/secret.ts +++ b/backend/src/ee/routes/v1/secret.ts @@ -1,48 +1,25 @@ import express from "express"; const router = express.Router(); -import { - requireAuth, - requireSecretAuth, - validateRequest, -} from "../../../middleware"; -import { body, param, query } from "express-validator"; +import { requireAuth } from "../../../middleware"; import { secretController } from "../../controllers/v1"; import { - ADMIN, - AuthMode, - MEMBER, - PERMISSION_READ_SECRETS, - PERMISSION_WRITE_SECRETS + AuthMode } from "../../../variables"; router.get( - "/:secretId/secret-versions", - requireAuth({ - acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY], - }), - requireSecretAuth({ - acceptedRoles: [ADMIN, MEMBER], - requiredPermissions: [PERMISSION_READ_SECRETS], - }), - param("secretId").exists().trim(), - query("offset").exists().isInt(), - query("limit").exists().isInt(), - validateRequest, - secretController.getSecretVersions + "/:secretId/secret-versions", + requireAuth({ + acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY] + }), + secretController.getSecretVersions ); router.post( - "/:secretId/secret-versions/rollback", - requireAuth({ - acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY], - }), - requireSecretAuth({ - acceptedRoles: [ADMIN, MEMBER], - requiredPermissions: [PERMISSION_READ_SECRETS, PERMISSION_WRITE_SECRETS], - }), - param("secretId").exists().trim(), - body("version").exists().isInt(), - secretController.rollbackSecretVersion + "/:secretId/secret-versions/rollback", + requireAuth({ + acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY] + }), + secretController.rollbackSecretVersion ); -export default router; \ No newline at end of file +export default router; diff --git a/backend/src/ee/routes/v1/secretScanning.ts b/backend/src/ee/routes/v1/secretScanning.ts index 1cc3adc46..0afdf0545 100644 --- a/backend/src/ee/routes/v1/secretScanning.ts +++ b/backend/src/ee/routes/v1/secretScanning.ts @@ -1,81 +1,53 @@ import express from "express"; const router = express.Router(); +import { requireAuth } from "../../../middleware"; import { - requireAuth, - requireOrganizationAuth, - validateRequest, -} from "../../../middleware"; -import { body, param } from "express-validator"; -import { createInstallationSession, getCurrentOrganizationInstallationStatus, getRisksForOrganization, linkInstallationToOrganization, updateRisksStatus } from "../../../controllers/v1/secretScanningController"; -import { ACCEPTED, ADMIN, AuthMode, MEMBER, OWNER } from "../../../variables"; + createInstallationSession, + getCurrentOrganizationInstallationStatus, + getRisksForOrganization, + linkInstallationToOrganization, + updateRisksStatus +} from "../../../controllers/v1/secretScanningController"; +import { AuthMode } from "../../../variables"; router.post( "/create-installation-session/organization/:organizationId", requireAuth({ - acceptedAuthModes: [AuthMode.JWT], + acceptedAuthModes: [AuthMode.JWT] }), - param("organizationId").exists().trim(), - requireOrganizationAuth({ - acceptedRoles: [OWNER, ADMIN, MEMBER], - acceptedStatuses: [ACCEPTED], - }), - validateRequest, createInstallationSession ); router.post( "/link-installation", requireAuth({ - acceptedAuthModes: [AuthMode.JWT], + acceptedAuthModes: [AuthMode.JWT] }), - body("installationId").exists().trim(), - body("sessionId").exists().trim(), - validateRequest, linkInstallationToOrganization ); router.get( "/installation-status/organization/:organizationId", requireAuth({ - acceptedAuthModes: [AuthMode.JWT], + acceptedAuthModes: [AuthMode.JWT] }), - param("organizationId").exists().trim(), - requireOrganizationAuth({ - acceptedRoles: [OWNER, ADMIN, MEMBER], - acceptedStatuses: [ACCEPTED], - }), - validateRequest, getCurrentOrganizationInstallationStatus ); router.get( "/organization/:organizationId/risks", requireAuth({ - acceptedAuthModes: [AuthMode.JWT], + acceptedAuthModes: [AuthMode.JWT] }), - param("organizationId").exists().trim(), - requireOrganizationAuth({ - acceptedRoles: [OWNER, ADMIN, MEMBER], - acceptedStatuses: [ACCEPTED], - }), - validateRequest, getRisksForOrganization ); router.post( "/organization/:organizationId/risks/:riskId/status", requireAuth({ - acceptedAuthModes: [AuthMode.JWT], + acceptedAuthModes: [AuthMode.JWT] }), - param("organizationId").exists().trim(), - param("riskId").exists().trim(), - body("status").exists(), - requireOrganizationAuth({ - acceptedRoles: [OWNER, ADMIN, MEMBER], - acceptedStatuses: [ACCEPTED], - }), - validateRequest, updateRisksStatus ); -export default router; \ No newline at end of file +export default router; diff --git a/backend/src/ee/routes/v1/secretSnapshot.ts b/backend/src/ee/routes/v1/secretSnapshot.ts index ecfe47ca5..f8c643e60 100644 --- a/backend/src/ee/routes/v1/secretSnapshot.ts +++ b/backend/src/ee/routes/v1/secretSnapshot.ts @@ -1,27 +1,15 @@ import express from "express"; const router = express.Router(); -import { - requireSecretSnapshotAuth, -} from "../../middleware"; -import { - requireAuth, - validateRequest, -} from "../../../middleware"; -import { param } from "express-validator"; -import { ADMIN, AuthMode, MEMBER } from "../../../variables"; +import { requireAuth } from "../../../middleware"; +import { AuthMode } from "../../../variables"; import { secretSnapshotController } from "../../controllers/v1"; router.get( - "/:secretSnapshotId", - requireAuth({ - acceptedAuthModes: [AuthMode.JWT], - }), - requireSecretSnapshotAuth({ - acceptedRoles: [ADMIN, MEMBER], - }), - param("secretSnapshotId").exists().trim(), - validateRequest, - secretSnapshotController.getSecretSnapshot + "/:secretSnapshotId", + requireAuth({ + acceptedAuthModes: [AuthMode.JWT] + }), + secretSnapshotController.getSecretSnapshot ); -export default router; \ No newline at end of file +export default router; diff --git a/backend/src/ee/routes/v1/sso.ts b/backend/src/ee/routes/v1/sso.ts index 005f84a0e..564073aec 100644 --- a/backend/src/ee/routes/v1/sso.ts +++ b/backend/src/ee/routes/v1/sso.ts @@ -1,146 +1,95 @@ import express from "express"; const router = express.Router(); import passport from "passport"; -import { - AuthProvider -} from "../../models"; -import { - requireAuth, - requireOrganizationAuth, - validateRequest, -} from "../../../middleware"; -import { body, query } from "express-validator"; +import { requireAuth } from "../../../middleware"; import { ssoController } from "../../controllers/v1"; import { authLimiter } from "../../../helpers/rateLimiter"; -import { - ACCEPTED, - ADMIN, - AuthMode, - OWNER -} from "../../../variables"; +import { AuthMode } from "../../../variables"; -router.get( - "/redirect/google", - authLimiter, - (req, res, next) => { - passport.authenticate("google", { - scope: ["profile", "email"], - session: false, - ...(req.query.callback_port ? { - state: req.query.callback_port as string - } : {}) - })(req, res, next); - } -); +router.get("/redirect/google", authLimiter, (req, res, next) => { + passport.authenticate("google", { + scope: ["profile", "email"], + session: false, + ...(req.query.callback_port + ? { + state: req.query.callback_port as string + } + : {}) + })(req, res, next); +}); router.get( "/google", - passport.authenticate("google", { - failureRedirect: "/login/provider/error", - session: false + passport.authenticate("google", { + failureRedirect: "/login/provider/error", + session: false }), ssoController.redirectSSO ); -router.get( - "/redirect/github", - authLimiter, - (req, res, next) => { - passport.authenticate("github", { - session: false, - ...(req.query.callback_port ? { - state: req.query.callback_port as string - } : {}) - })(req, res, next); - } -); +router.get("/redirect/github", authLimiter, (req, res, next) => { + passport.authenticate("github", { + session: false, + ...(req.query.callback_port + ? { + state: req.query.callback_port as string + } + : {}) + })(req, res, next); +}); router.get( "/github", authLimiter, - passport.authenticate("github", { - failureRedirect: "/login/provider/error", - session: false + passport.authenticate("github", { + failureRedirect: "/login/provider/error", + session: false }), ssoController.redirectSSO ); -router.get( - "/redirect/saml2/:ssoIdentifier", - authLimiter, - (req, res, next) => { - const options = { - failureRedirect: "/", - additionalParams: { - RelayState: req.query.callback_port ?? "" - }, - }; - passport.authenticate("saml", options)(req, res, next); - } -); +router.get("/redirect/saml2/:ssoIdentifier", authLimiter, (req, res, next) => { + const options = { + failureRedirect: "/", + additionalParams: { + RelayState: req.query.callback_port ?? "" + } + }; + passport.authenticate("saml", options)(req, res, next); +}); -router.post("/saml2/:ssoIdentifier", - passport.authenticate("saml", { - failureRedirect: "/login/provider/error", - failureFlash: true, +router.post( + "/saml2/:ssoIdentifier", + passport.authenticate("saml", { + failureRedirect: "/login/provider/error", + failureFlash: true, session: false }), ssoController.redirectSSO ); router.get( - "/config", - requireAuth({ - acceptedAuthModes: [AuthMode.JWT], - }), - requireOrganizationAuth({ - acceptedRoles: [OWNER, ADMIN], - acceptedStatuses: [ACCEPTED], - locationOrganizationId: "query" - }), - query("organizationId").exists().trim(), - validateRequest, - ssoController.getSSOConfig + "/config", + requireAuth({ + acceptedAuthModes: [AuthMode.JWT] + }), + ssoController.getSSOConfig ); router.post( - "/config", - requireAuth({ - acceptedAuthModes: [AuthMode.JWT], - }), - requireOrganizationAuth({ - acceptedRoles: [OWNER, ADMIN], - acceptedStatuses: [ACCEPTED], - locationOrganizationId: "body" - }), - body("organizationId").exists().trim(), - body("authProvider").exists().isString().isIn([AuthProvider.OKTA_SAML]), - body("isActive").exists().isBoolean(), - body("entryPoint").exists().isString(), - body("issuer").exists().isString(), - body("cert").exists().isString(), - validateRequest, - ssoController.createSSOConfig + "/config", + requireAuth({ + acceptedAuthModes: [AuthMode.JWT] + }), + ssoController.createSSOConfig ); router.patch( - "/config", - requireAuth({ - acceptedAuthModes: [AuthMode.JWT], - }), - requireOrganizationAuth({ - acceptedRoles: [OWNER, ADMIN], - acceptedStatuses: [ACCEPTED], - locationOrganizationId: "body" - }), - body("organizationId").exists().trim(), - body("authProvider").optional().isString(), - body("isActive").optional().isBoolean(), - body("entryPoint").optional().isString(), - body("issuer").optional().isString(), - body("cert").optional().isString(), - validateRequest, - ssoController.updateSSOConfig + "/config", + requireAuth({ + acceptedAuthModes: [AuthMode.JWT] + }), + ssoController.updateSSOConfig ); -export default router; \ No newline at end of file +export default router; diff --git a/backend/src/ee/routes/v1/workspace.ts b/backend/src/ee/routes/v1/workspace.ts index 529ba0550..9a7054ce6 100644 --- a/backend/src/ee/routes/v1/workspace.ts +++ b/backend/src/ee/routes/v1/workspace.ts @@ -1,182 +1,85 @@ import express from "express"; const router = express.Router(); -import { - requireAuth, - requireWorkspaceAuth, - validateRequest, -} from "../../../middleware"; -import { body, param, query } from "express-validator"; -import { - ADMIN, - AuthMode, - MEMBER -} from "../../../variables"; +import { requireAuth } from "../../../middleware"; +import { AuthMode } from "../../../variables"; import { workspaceController } from "../../controllers/v1"; -import { EventType, UserAgentType } from "../../models"; router.get( "/:workspaceId/secret-snapshots", requireAuth({ - acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY], + acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY] }), - requireWorkspaceAuth({ - acceptedRoles: [ADMIN, MEMBER], - locationWorkspaceId: "params", - }), - param("workspaceId").exists().trim(), - query("environment").isString().exists().trim(), - query("folderId").default("root").isString().trim(), - query("offset").exists().isInt(), - query("limit").exists().isInt(), - validateRequest, workspaceController.getWorkspaceSecretSnapshots ); router.get( "/:workspaceId/secret-snapshots/count", requireAuth({ - acceptedAuthModes: [AuthMode.JWT], + acceptedAuthModes: [AuthMode.JWT] }), - requireWorkspaceAuth({ - acceptedRoles: [ADMIN, MEMBER], - locationWorkspaceId: "params", - }), - param("workspaceId").exists().trim(), - query("environment").isString().exists().trim(), - query("folderId").default("root").isString().trim(), - validateRequest, workspaceController.getWorkspaceSecretSnapshotsCount ); router.post( "/:workspaceId/secret-snapshots/rollback", requireAuth({ - acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY], + acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY] }), - requireWorkspaceAuth({ - acceptedRoles: [ADMIN, MEMBER], - locationWorkspaceId: "params", - }), - param("workspaceId").exists().trim(), - body("environment").isString().exists().trim(), - query("folderId").default("root").isString().exists().trim(), - body("version").exists().isInt(), - validateRequest, workspaceController.rollbackWorkspaceSecretSnapshot ); router.get( "/:workspaceId/logs", requireAuth({ - acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY], + acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY] }), - requireWorkspaceAuth({ - acceptedRoles: [ADMIN, MEMBER], - locationWorkspaceId: "params", - }), - param("workspaceId").exists().trim(), - query("offset").exists().isInt(), - query("limit").exists().isInt(), - query("sortBy"), - query("userId"), - query("actionNames"), - validateRequest, workspaceController.getWorkspaceLogs ); router.get( "/:workspaceId/audit-logs", requireAuth({ - acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY], + acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY] }), - requireWorkspaceAuth({ - acceptedRoles: [ADMIN, MEMBER], - locationWorkspaceId: "params", - }), - param("workspaceId").exists().trim(), - query("eventType").isString().isIn(Object.values(EventType)).optional({ nullable: true }), - query("userAgentType").isString().isIn(Object.values(UserAgentType)).optional({ nullable: true }), - query("actor").optional({ nullable: true }), - query("startDate").isISO8601().withMessage("Invalid start date format").optional({ nullable: true }), - query("endDate").isISO8601().withMessage("Invalid end date format").optional({ nullable: true }), - query("offset"), - query("limit"), - validateRequest, workspaceController.getWorkspaceAuditLogs ); router.get( "/:workspaceId/audit-logs/filters/actors", requireAuth({ - acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY], + acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY] }), - requireWorkspaceAuth({ - acceptedRoles: [ADMIN, MEMBER], - locationWorkspaceId: "params", - }), - param("workspaceId").exists().trim(), - validateRequest, workspaceController.getWorkspaceAuditLogActorFilterOpts ); router.get( "/:workspaceId/trusted-ips", - param("workspaceId").exists().isString().trim(), requireAuth({ - acceptedAuthModes: [AuthMode.JWT], - }), - requireWorkspaceAuth({ - acceptedRoles: [ADMIN, MEMBER], - locationWorkspaceId: "params", + acceptedAuthModes: [AuthMode.JWT] }), workspaceController.getWorkspaceTrustedIps ); router.post( "/:workspaceId/trusted-ips", - param("workspaceId").exists().isString().trim(), - body("ipAddress").exists().isString().trim(), - body("comment").default("").isString().trim(), - body("isActive").exists().isBoolean(), - validateRequest, requireAuth({ - acceptedAuthModes: [AuthMode.JWT], - }), - requireWorkspaceAuth({ - acceptedRoles: [ADMIN], - locationWorkspaceId: "params", + acceptedAuthModes: [AuthMode.JWT] }), workspaceController.addWorkspaceTrustedIp ); router.patch( "/:workspaceId/trusted-ips/:trustedIpId", - param("workspaceId").exists().isString().trim(), - param("trustedIpId").exists().isString().trim(), - body("ipAddress").isString().trim().default(""), - body("comment").default("").isString().trim(), - validateRequest, requireAuth({ - acceptedAuthModes: [AuthMode.JWT], - }), - requireWorkspaceAuth({ - acceptedRoles: [ADMIN], - locationWorkspaceId: "params", + acceptedAuthModes: [AuthMode.JWT] }), workspaceController.updateWorkspaceTrustedIp ); router.delete( "/:workspaceId/trusted-ips/:trustedIpId", - param("workspaceId").exists().isString().trim(), - param("trustedIpId").exists().isString().trim(), - validateRequest, requireAuth({ - acceptedAuthModes: [AuthMode.JWT], - }), - requireWorkspaceAuth({ - acceptedRoles: [ADMIN], - locationWorkspaceId: "params", + acceptedAuthModes: [AuthMode.JWT] }), workspaceController.deleteWorkspaceTrustedIp ); diff --git a/backend/src/ee/services/ProjectRoleService.ts b/backend/src/ee/services/ProjectRoleService.ts new file mode 100644 index 000000000..c7d1e7494 --- /dev/null +++ b/backend/src/ee/services/ProjectRoleService.ts @@ -0,0 +1,258 @@ +import { + AbilityBuilder, + ForcedSubject, + MongoAbility, + RawRuleOf, + buildMongoQueryMatcher, + createMongoAbility +} from "@casl/ability"; +import { Membership } from "../../models"; +import { IRole } from "../models/role"; +import { BadRequestError, UnauthorizedRequestError } from "../../utils/errors"; +import { FieldCondition, FieldInstruction, JsInterpreter } from "@ucast/mongo2js"; +import picomatch from "picomatch"; + +const $glob: FieldInstruction = { + type: "field", + validate(instruction, value) { + if (typeof value !== "string") { + throw new Error(`"${instruction.name}" expects value to be a string`); + } + } +}; + +const glob: JsInterpreter> = (node, object, context) => { + const secretPath = context.get(object, node.field); + const permissionSecretGlobPath = node.value; + return picomatch.isMatch(secretPath, permissionSecretGlobPath, { strictSlashes: false }); +}; + +export const conditionsMatcher = buildMongoQueryMatcher({ $glob }, { glob }); + +export enum ProjectPermissionActions { + Read = "read", + Create = "create", + Edit = "edit", + Delete = "delete" +} + +export enum ProjectPermissionSub { + Role = "role", + Member = "member", + Settings = "settings", + Integrations = "integrations", + Webhooks = "webhooks", + ServiceTokens = "service-tokens", + Environments = "environments", + Tags = "tags", + AuditLogs = "audit-logs", + IpAllowList = "ip-allowlist", + Workspace = "workspace", + Secrets = "secrets", + SecretImports = "secret-imports", + SecretRollback = "secret-rollback", + Folders = "folders" +} + +type SubjectFields = { + environment: string; + secretPath: string; +}; + +export type ProjectPermissionSet = + | [ + ProjectPermissionActions, + ProjectPermissionSub.Secrets | (ForcedSubject & SubjectFields) + ] + | [ + ProjectPermissionActions, + ProjectPermissionSub.Folders | (ForcedSubject & SubjectFields) + ] + | [ + ProjectPermissionActions, + ( + | ProjectPermissionSub.SecretImports + | (ForcedSubject & SubjectFields) + ) + ] + | [ProjectPermissionActions, ProjectPermissionSub.Role] + | [ProjectPermissionActions, ProjectPermissionSub.Tags] + | [ProjectPermissionActions, ProjectPermissionSub.Member] + | [ProjectPermissionActions, ProjectPermissionSub.Integrations] + | [ProjectPermissionActions, ProjectPermissionSub.Webhooks] + | [ProjectPermissionActions, ProjectPermissionSub.AuditLogs] + | [ProjectPermissionActions, ProjectPermissionSub.Environments] + | [ProjectPermissionActions, ProjectPermissionSub.IpAllowList] + | [ProjectPermissionActions, ProjectPermissionSub.Settings] + | [ProjectPermissionActions, ProjectPermissionSub.ServiceTokens] + | [ProjectPermissionActions.Delete, ProjectPermissionSub.Workspace] + | [ProjectPermissionActions.Edit, ProjectPermissionSub.Workspace] + | [ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback] + | [ProjectPermissionActions.Create, ProjectPermissionSub.SecretRollback]; + +const buildAdminPermission = () => { + const { can, build } = new AbilityBuilder>(createMongoAbility); + + can(ProjectPermissionActions.Read, ProjectPermissionSub.Secrets); + can(ProjectPermissionActions.Create, ProjectPermissionSub.Secrets); + can(ProjectPermissionActions.Edit, ProjectPermissionSub.Secrets); + can(ProjectPermissionActions.Delete, ProjectPermissionSub.Secrets); + + can(ProjectPermissionActions.Read, ProjectPermissionSub.Folders); + can(ProjectPermissionActions.Create, ProjectPermissionSub.Folders); + can(ProjectPermissionActions.Edit, ProjectPermissionSub.Folders); + can(ProjectPermissionActions.Delete, ProjectPermissionSub.Folders); + + can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretImports); + can(ProjectPermissionActions.Create, ProjectPermissionSub.SecretImports); + can(ProjectPermissionActions.Edit, ProjectPermissionSub.SecretImports); + can(ProjectPermissionActions.Delete, ProjectPermissionSub.SecretImports); + + can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback); + can(ProjectPermissionActions.Create, ProjectPermissionSub.SecretRollback); + + can(ProjectPermissionActions.Read, ProjectPermissionSub.Member); + can(ProjectPermissionActions.Create, ProjectPermissionSub.Member); + can(ProjectPermissionActions.Edit, ProjectPermissionSub.Member); + can(ProjectPermissionActions.Delete, ProjectPermissionSub.Member); + + can(ProjectPermissionActions.Read, ProjectPermissionSub.Role); + can(ProjectPermissionActions.Create, ProjectPermissionSub.Role); + can(ProjectPermissionActions.Edit, ProjectPermissionSub.Role); + can(ProjectPermissionActions.Delete, ProjectPermissionSub.Role); + + can(ProjectPermissionActions.Read, ProjectPermissionSub.Integrations); + can(ProjectPermissionActions.Create, ProjectPermissionSub.Integrations); + can(ProjectPermissionActions.Edit, ProjectPermissionSub.Integrations); + can(ProjectPermissionActions.Delete, ProjectPermissionSub.Integrations); + + can(ProjectPermissionActions.Read, ProjectPermissionSub.Webhooks); + can(ProjectPermissionActions.Create, ProjectPermissionSub.Webhooks); + can(ProjectPermissionActions.Edit, ProjectPermissionSub.Webhooks); + can(ProjectPermissionActions.Delete, ProjectPermissionSub.Webhooks); + + can(ProjectPermissionActions.Read, ProjectPermissionSub.ServiceTokens); + can(ProjectPermissionActions.Create, ProjectPermissionSub.ServiceTokens); + can(ProjectPermissionActions.Edit, ProjectPermissionSub.ServiceTokens); + can(ProjectPermissionActions.Delete, ProjectPermissionSub.ServiceTokens); + + can(ProjectPermissionActions.Read, ProjectPermissionSub.Settings); + can(ProjectPermissionActions.Create, ProjectPermissionSub.Settings); + can(ProjectPermissionActions.Edit, ProjectPermissionSub.Settings); + can(ProjectPermissionActions.Delete, ProjectPermissionSub.Settings); + + can(ProjectPermissionActions.Read, ProjectPermissionSub.Environments); + can(ProjectPermissionActions.Create, ProjectPermissionSub.Environments); + can(ProjectPermissionActions.Edit, ProjectPermissionSub.Environments); + can(ProjectPermissionActions.Delete, ProjectPermissionSub.Environments); + + can(ProjectPermissionActions.Read, ProjectPermissionSub.Tags); + can(ProjectPermissionActions.Create, ProjectPermissionSub.Tags); + can(ProjectPermissionActions.Edit, ProjectPermissionSub.Tags); + can(ProjectPermissionActions.Delete, ProjectPermissionSub.Tags); + + can(ProjectPermissionActions.Read, ProjectPermissionSub.AuditLogs); + can(ProjectPermissionActions.Create, ProjectPermissionSub.AuditLogs); + can(ProjectPermissionActions.Edit, ProjectPermissionSub.AuditLogs); + can(ProjectPermissionActions.Delete, ProjectPermissionSub.AuditLogs); + + can(ProjectPermissionActions.Read, ProjectPermissionSub.IpAllowList); + can(ProjectPermissionActions.Create, ProjectPermissionSub.IpAllowList); + can(ProjectPermissionActions.Edit, ProjectPermissionSub.IpAllowList); + can(ProjectPermissionActions.Delete, ProjectPermissionSub.IpAllowList); + + can(ProjectPermissionActions.Edit, ProjectPermissionSub.Workspace); + can(ProjectPermissionActions.Delete, ProjectPermissionSub.Workspace); + + return build({ conditionsMatcher }); +}; + +export const adminProjectPermissions = buildAdminPermission(); + +const buildMemberPermission = () => { + const { can, build } = new AbilityBuilder>(createMongoAbility); + + can(ProjectPermissionActions.Read, ProjectPermissionSub.Secrets); + can(ProjectPermissionActions.Create, ProjectPermissionSub.Secrets); + can(ProjectPermissionActions.Edit, ProjectPermissionSub.Secrets); + can(ProjectPermissionActions.Delete, ProjectPermissionSub.Secrets); + + can(ProjectPermissionActions.Read, ProjectPermissionSub.Folders); + can(ProjectPermissionActions.Create, ProjectPermissionSub.Folders); + can(ProjectPermissionActions.Edit, ProjectPermissionSub.Folders); + can(ProjectPermissionActions.Delete, ProjectPermissionSub.Folders); + + can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretImports); + can(ProjectPermissionActions.Create, ProjectPermissionSub.SecretImports); + can(ProjectPermissionActions.Edit, ProjectPermissionSub.SecretImports); + can(ProjectPermissionActions.Delete, ProjectPermissionSub.SecretImports); + can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback); + can(ProjectPermissionActions.Create, ProjectPermissionSub.SecretRollback); + + can(ProjectPermissionActions.Read, ProjectPermissionSub.Member); + can(ProjectPermissionActions.Read, ProjectPermissionSub.Role); + can(ProjectPermissionActions.Read, ProjectPermissionSub.Integrations); + can(ProjectPermissionActions.Read, ProjectPermissionSub.Webhooks); + can(ProjectPermissionActions.Read, ProjectPermissionSub.ServiceTokens); + can(ProjectPermissionActions.Read, ProjectPermissionSub.Settings); + can(ProjectPermissionActions.Read, ProjectPermissionSub.Environments); + can(ProjectPermissionActions.Read, ProjectPermissionSub.Tags); + can(ProjectPermissionActions.Read, ProjectPermissionSub.AuditLogs); + can(ProjectPermissionActions.Read, ProjectPermissionSub.IpAllowList); + + return build({ conditionsMatcher }); +}; + +export const memberProjectPermissions = buildMemberPermission(); + +const buildViewerPermission = () => { + const { can, build } = new AbilityBuilder>(createMongoAbility); + + can(ProjectPermissionActions.Read, ProjectPermissionSub.Secrets); + can(ProjectPermissionActions.Read, ProjectPermissionSub.Folders); + can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretImports); + can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback); + can(ProjectPermissionActions.Read, ProjectPermissionSub.Member); + can(ProjectPermissionActions.Read, ProjectPermissionSub.Role); + can(ProjectPermissionActions.Read, ProjectPermissionSub.Integrations); + can(ProjectPermissionActions.Read, ProjectPermissionSub.Webhooks); + can(ProjectPermissionActions.Read, ProjectPermissionSub.ServiceTokens); + can(ProjectPermissionActions.Read, ProjectPermissionSub.Settings); + can(ProjectPermissionActions.Read, ProjectPermissionSub.Environments); + can(ProjectPermissionActions.Read, ProjectPermissionSub.Tags); + can(ProjectPermissionActions.Read, ProjectPermissionSub.AuditLogs); + can(ProjectPermissionActions.Read, ProjectPermissionSub.IpAllowList); + + return build({ conditionsMatcher }); +}; + +export const viewerProjectPermission = buildViewerPermission(); + +export const getUserProjectPermissions = async (userId: string, workspaceId: string) => { + // TODO(akhilmhdh): speed this up by pulling from cache later + const membership = await Membership.findOne({ + user: userId, + workspace: workspaceId + }) + .populate<{ + customRole: IRole & { permissions: RawRuleOf>[] }; + }>("customRole") + .exec(); + + if (!membership || (membership.role === "custom" && !membership.customRole)) { + throw UnauthorizedRequestError({ message: "User doesn't belong to organization" }); + } + + if (membership.role === "admin") return { permission: adminProjectPermissions, membership }; + if (membership.role === "member") return { permission: memberProjectPermissions, membership }; + if (membership.role === "viewer") return { permission: viewerProjectPermission, membership }; + + if (membership.role === "custom") { + const permission = createMongoAbility(membership.customRole.permissions, { + conditionsMatcher + }); + return { permission, membership }; + } + + throw BadRequestError({ message: "User role not found" }); +}; diff --git a/backend/src/ee/services/RoleService.ts b/backend/src/ee/services/RoleService.ts new file mode 100644 index 000000000..df5ebf29a --- /dev/null +++ b/backend/src/ee/services/RoleService.ts @@ -0,0 +1,130 @@ +import { AbilityBuilder, MongoAbility, RawRuleOf, createMongoAbility } from "@casl/ability"; +import { MembershipOrg } from "../../models"; +import { IRole } from "../models/role"; +import { BadRequestError, UnauthorizedRequestError } from "../../utils/errors"; +import { ACCEPTED } from "../../variables"; +import { conditionsMatcher } from "./ProjectRoleService"; + +export enum OrgPermissionActions { + Read = "read", + Create = "create", + Edit = "edit", + Delete = "delete" +} + +export enum OrgPermissionSubjects { + Workspace = "workspace", + Role = "role", + Member = "member", + Settings = "settings", + IncidentAccount = "incident-contact", + Sso = "sso", + Billing = "billing", + SecretScanning = "secret-scanning" +} + +export type OrgPermissionSet = + | [OrgPermissionActions.Read, OrgPermissionSubjects.Workspace] + | [OrgPermissionActions.Create, OrgPermissionSubjects.Workspace] + | [OrgPermissionActions, OrgPermissionSubjects.Role] + | [OrgPermissionActions, OrgPermissionSubjects.Member] + | [OrgPermissionActions, OrgPermissionSubjects.Settings] + | [OrgPermissionActions, OrgPermissionSubjects.IncidentAccount] + | [OrgPermissionActions, OrgPermissionSubjects.Sso] + | [OrgPermissionActions, OrgPermissionSubjects.SecretScanning] + | [OrgPermissionActions, OrgPermissionSubjects.Billing]; + +const buildAdminPermission = () => { + const { can, build } = new AbilityBuilder>(createMongoAbility); + // ws permissions + can(OrgPermissionActions.Read, OrgPermissionSubjects.Workspace); + can(OrgPermissionActions.Create, OrgPermissionSubjects.Workspace); + // role permission + can(OrgPermissionActions.Read, OrgPermissionSubjects.Role); + can(OrgPermissionActions.Create, OrgPermissionSubjects.Role); + can(OrgPermissionActions.Edit, OrgPermissionSubjects.Role); + can(OrgPermissionActions.Delete, OrgPermissionSubjects.Role); + + can(OrgPermissionActions.Read, OrgPermissionSubjects.Member); + can(OrgPermissionActions.Create, OrgPermissionSubjects.Member); + can(OrgPermissionActions.Edit, OrgPermissionSubjects.Member); + can(OrgPermissionActions.Delete, OrgPermissionSubjects.Member); + + can(OrgPermissionActions.Read, OrgPermissionSubjects.SecretScanning); + can(OrgPermissionActions.Create, OrgPermissionSubjects.SecretScanning); + can(OrgPermissionActions.Edit, OrgPermissionSubjects.SecretScanning); + can(OrgPermissionActions.Delete, OrgPermissionSubjects.SecretScanning); + + can(OrgPermissionActions.Read, OrgPermissionSubjects.Settings); + can(OrgPermissionActions.Create, OrgPermissionSubjects.Settings); + can(OrgPermissionActions.Edit, OrgPermissionSubjects.Settings); + can(OrgPermissionActions.Delete, OrgPermissionSubjects.Settings); + + can(OrgPermissionActions.Read, OrgPermissionSubjects.IncidentAccount); + can(OrgPermissionActions.Create, OrgPermissionSubjects.IncidentAccount); + can(OrgPermissionActions.Edit, OrgPermissionSubjects.IncidentAccount); + can(OrgPermissionActions.Delete, OrgPermissionSubjects.IncidentAccount); + + can(OrgPermissionActions.Read, OrgPermissionSubjects.Sso); + can(OrgPermissionActions.Create, OrgPermissionSubjects.Sso); + can(OrgPermissionActions.Edit, OrgPermissionSubjects.Sso); + can(OrgPermissionActions.Delete, OrgPermissionSubjects.Sso); + + can(OrgPermissionActions.Read, OrgPermissionSubjects.Billing); + can(OrgPermissionActions.Create, OrgPermissionSubjects.Billing); + can(OrgPermissionActions.Edit, OrgPermissionSubjects.Billing); + can(OrgPermissionActions.Delete, OrgPermissionSubjects.Billing); + + return build({ conditionsMatcher }); +}; + +export const adminPermissions = buildAdminPermission(); + +const buildMemberPermission = () => { + const { can, build } = new AbilityBuilder>(createMongoAbility); + + can(OrgPermissionActions.Read, OrgPermissionSubjects.Workspace); + can(OrgPermissionActions.Create, OrgPermissionSubjects.Workspace); + can(OrgPermissionActions.Read, OrgPermissionSubjects.Member); + can(OrgPermissionActions.Read, OrgPermissionSubjects.Role); + can(OrgPermissionActions.Read, OrgPermissionSubjects.Settings); + can(OrgPermissionActions.Read, OrgPermissionSubjects.Billing); + can(OrgPermissionActions.Read, OrgPermissionSubjects.Sso); + can(OrgPermissionActions.Read, OrgPermissionSubjects.IncidentAccount); + can(OrgPermissionActions.Read, OrgPermissionSubjects.SecretScanning); + + return build({ conditionsMatcher }); +}; + +export const memberPermissions = buildMemberPermission(); + +export const getUserOrgPermissions = async (userId: string, orgId: string) => { + // TODO(akhilmhdh): speed this up by pulling from cache later + const membership = await MembershipOrg.findOne({ + user: userId, + organization: orgId, + status: ACCEPTED + }) + .populate<{ customRole: IRole & { permissions: RawRuleOf>[] } }>( + "customRole" + ) + .exec(); + + if (!membership || (membership.role === "custom" && !membership.customRole)) { + throw UnauthorizedRequestError({ message: "User doesn't belong to organization" }); + } + + if (membership.role === "admin" || membership.role === "owner") + return { permission: adminPermissions, membership }; + + if (membership.role === "member") return { permission: memberPermissions, membership }; + + if (membership.role === "custom") { + const permission = createMongoAbility(membership.customRole.permissions, { + conditionsMatcher + }); + return { permission, membership }; + } + + throw BadRequestError({ message: "User role not found" }); +}; diff --git a/backend/src/ee/validation/role.ts b/backend/src/ee/validation/role.ts new file mode 100644 index 000000000..e3ecafe59 --- /dev/null +++ b/backend/src/ee/validation/role.ts @@ -0,0 +1,68 @@ +import { z } from "zod"; + +export const CreateRoleSchema = z.object({ + body: z.object({ + slug: z.string().trim(), + name: z.string().trim(), + description: z.string().trim().optional(), + workspaceId: z.string().trim().optional(), + orgId: z.string().trim(), + permissions: z + .object({ + subject: z.string().trim(), + action: z.string().trim(), + conditions: z + .record(z.union([z.string().trim(), z.number(), z.object({ $glob: z.string() })])) + .optional() + }) + .array() + }) +}); + +export const UpdateRoleSchema = z.object({ + params: z.object({ + id: z.string().trim() + }), + body: z.object({ + slug: z.string().trim().optional(), + name: z.string().trim().optional(), + description: z.string().trim().optional(), + workspaceId: z.string().trim().optional(), + orgId: z.string().trim(), + permissions: z + .object({ + subject: z.string().trim(), + action: z.string().trim(), + conditions: z + .record(z.union([z.string().trim(), z.number(), z.object({ $glob: z.string() })])) + .optional() + }) + .array() + .optional() + }) +}); + +export const DeleteRoleSchema = z.object({ + params: z.object({ + id: z.string().trim() + }) +}); + +export const GetRoleSchema = z.object({ + query: z.object({ + workspaceId: z.string().trim().optional(), + orgId: z.string().trim() + }) +}); + +export const GetUserPermission = z.object({ + params: z.object({ + orgId: z.string().trim() + }) +}); + +export const GetUserProjectPermission = z.object({ + params: z.object({ + workspaceId: z.string().trim() + }) +}); diff --git a/backend/src/helpers/integration.ts b/backend/src/helpers/integration.ts index 485c19d7e..2775d79c6 100644 --- a/backend/src/helpers/integration.ts +++ b/backend/src/helpers/integration.ts @@ -160,7 +160,7 @@ export const getIntegrationAuthAccessHelper = async ({ let accessId; let accessToken; const integrationAuth = await IntegrationAuth.findById(integrationAuthId).select( - "workspace integration +accessCiphertext +accessIV +accessTag +accessExpiresAt + refreshCiphertext +accessIdCiphertext +accessIdIV +accessIdTag" + "workspace integration +accessCiphertext +accessIV +accessTag +accessExpiresAt +refreshCiphertext +refreshIV +refreshTag +accessIdCiphertext +accessIdIV +accessIdTag" ); if (!integrationAuth) @@ -209,6 +209,7 @@ export const getIntegrationAuthAccessHelper = async ({ if (!accessToken) throw InternalServerError(); return { + integrationAuth, accessId, accessToken }; diff --git a/backend/src/helpers/membership.ts b/backend/src/helpers/membership.ts index d2fcf5b17..7700c21ee 100644 --- a/backend/src/helpers/membership.ts +++ b/backend/src/helpers/membership.ts @@ -11,29 +11,29 @@ import { BadRequestError, MembershipNotFoundError } from "../utils/errors"; * @returns {Membership} membership - membership of user with id [userId] for workspace with id [workspaceId] */ export const validateMembership = async ({ - userId, - workspaceId, - acceptedRoles, + userId, + workspaceId, + acceptedRoles }: { userId: Types.ObjectId | string; workspaceId: Types.ObjectId | string; - acceptedRoles?: Array<"admin" | "member">; + acceptedRoles?: Array<"admin" | "member" | "custom" | "viewer">; }) => { const membership = await Membership.findOne({ user: userId, - workspace: workspaceId, + workspace: workspaceId }).populate("workspace"); if (!membership) { throw MembershipNotFoundError({ - message: "Failed to find workspace membership", + message: "Failed to find workspace membership" }); } if (acceptedRoles) { if (!acceptedRoles.includes(membership.role)) { throw BadRequestError({ - message: "Failed authorization for membership role", + message: "Failed authorization for membership role" }); } } @@ -47,7 +47,7 @@ export const validateMembership = async ({ * @return {Object} membership - membership */ export const findMembership = async (queryObj: any) => { - const membership = await Membership.findOne(queryObj); + const membership = await Membership.findOne(queryObj); return membership; }; @@ -60,9 +60,9 @@ export const findMembership = async (queryObj: any) => { * @param {String[]} obj.roles - roles of users. */ export const addMemberships = async ({ - userIds, - workspaceId, - roles, + userIds, + workspaceId, + roles }: { userIds: string[]; workspaceId: string; @@ -74,15 +74,15 @@ export const addMemberships = async ({ filter: { user: userId, workspace: workspaceId, - role: roles[idx], + role: roles[idx] }, update: { user: userId, workspace: workspaceId, - role: roles[idx], + role: roles[idx] }, - upsert: true, - }, + upsert: true + } }; }); await Membership.bulkWrite(operations as any); @@ -94,8 +94,8 @@ export const addMemberships = async ({ * @param {String} obj.membershipId - id of membership to delete */ export const deleteMembership = async ({ membershipId }: { membershipId: string }) => { - const deletedMembership = await Membership.findOneAndDelete({ - _id: membershipId, + const deletedMembership = await Membership.findOneAndDelete({ + _id: membershipId }); // delete keys associated with the membership @@ -103,9 +103,9 @@ export const deleteMembership = async ({ membershipId }: { membershipId: string // case: membership had a registered user await Key.deleteMany({ receiver: deletedMembership.user, - workspace: deletedMembership.workspace, + workspace: deletedMembership.workspace }); } - return deletedMembership; + return deletedMembership; }; diff --git a/backend/src/helpers/membershipOrg.ts b/backend/src/helpers/membershipOrg.ts index 3ed5be088..46f5fbf56 100644 --- a/backend/src/helpers/membershipOrg.ts +++ b/backend/src/helpers/membershipOrg.ts @@ -1,14 +1,6 @@ import { Types } from "mongoose"; -import { - Key, - Membership, - MembershipOrg, - Workspace, -} from "../models"; -import { - MembershipOrgNotFoundError, - UnauthorizedRequestError, -} from "../utils/errors"; +import { Key, Membership, MembershipOrg, Workspace } from "../models"; +import { MembershipOrgNotFoundError, UnauthorizedRequestError } from "../utils/errors"; /** * Validate that user with id [userId] is a member of organization with id [organizationId] @@ -19,39 +11,43 @@ import { * @param {String[]} obj.acceptedRoles */ export const validateMembershipOrg = async ({ - userId, - organizationId, - acceptedRoles, - acceptedStatuses, + userId, + organizationId, + acceptedRoles, + acceptedStatuses }: { - userId: Types.ObjectId; - organizationId: Types.ObjectId; - acceptedRoles?: Array<"owner" | "admin" | "member">; - acceptedStatuses?: Array<"invited" | "accepted">; + userId: Types.ObjectId; + organizationId: Types.ObjectId; + acceptedRoles?: Array<"owner" | "admin" | "member" | "custom">; + acceptedStatuses?: Array<"invited" | "accepted">; }) => { - const membershipOrg = await MembershipOrg.findOne({ - user: userId, - organization: organizationId, - }); - - if (!membershipOrg) { - throw MembershipOrgNotFoundError({ message: "Failed to find organization membership" }); - } - - if (acceptedRoles) { - if (!acceptedRoles.includes(membershipOrg.role)) { - throw UnauthorizedRequestError({ message: "Failed to validate organization membership role" }); - } - } - - if (acceptedStatuses) { - if (!acceptedStatuses.includes(membershipOrg.status)) { - throw UnauthorizedRequestError({ message: "Failed to validate organization membership status" }); - } - } - - return membershipOrg; -} + const membershipOrg = await MembershipOrg.findOne({ + user: userId, + organization: organizationId + }); + + if (!membershipOrg) { + throw MembershipOrgNotFoundError({ message: "Failed to find organization membership" }); + } + + if (acceptedRoles) { + if (!acceptedRoles.includes(membershipOrg.role)) { + throw UnauthorizedRequestError({ + message: "Failed to validate organization membership role" + }); + } + } + + if (acceptedStatuses) { + if (!acceptedStatuses.includes(membershipOrg.status)) { + throw UnauthorizedRequestError({ + message: "Failed to validate organization membership status" + }); + } + } + + return membershipOrg; +}; /** * Return organization membership matching criteria specified in @@ -60,8 +56,8 @@ export const validateMembershipOrg = async ({ * @return {Object} membershipOrg - membership */ export const findMembershipOrg = (queryObj: any) => { - const membershipOrg = MembershipOrg.findOne(queryObj); - return membershipOrg; + const membershipOrg = MembershipOrg.findOne(queryObj); + return membershipOrg; }; /** @@ -73,15 +69,15 @@ export const findMembershipOrg = (queryObj: any) => { * @param {String[]} obj.roles - roles of users. */ export const addMembershipsOrg = async ({ - userIds, - organizationId, - roles, - statuses, + userIds, + organizationId, + roles, + statuses }: { - userIds: string[]; - organizationId: string; - roles: string[]; - statuses: string[]; + userIds: string[]; + organizationId: string; + roles: string[]; + statuses: string[]; }) => { const operations = userIds.map((userId, idx) => { return { @@ -90,16 +86,16 @@ export const addMembershipsOrg = async ({ user: userId, organization: organizationId, role: roles[idx], - status: statuses[idx], + status: statuses[idx] }, update: { user: userId, organization: organizationId, role: roles[idx], - status: statuses[idx], + status: statuses[idx] }, - upsert: true, - }, + upsert: true + } }; }); @@ -111,13 +107,9 @@ export const addMembershipsOrg = async ({ * @param {Object} obj * @param {String} obj.membershipOrgId - id of organization membership to delete */ -export const deleteMembershipOrg = async ({ - membershipOrgId, -}: { - membershipOrgId: string; -}) => { +export const deleteMembershipOrg = async ({ membershipOrgId }: { membershipOrgId: string }) => { const deletedMembershipOrg = await MembershipOrg.findOneAndDelete({ - _id: membershipOrgId, + _id: membershipOrgId }); if (!deletedMembershipOrg) throw new Error("Failed to delete organization membership"); @@ -128,24 +120,24 @@ export const deleteMembershipOrg = async ({ const workspaces = ( await Workspace.find({ - organization: deletedMembershipOrg.organization, + organization: deletedMembershipOrg.organization }) ).map((w) => w._id.toString()); await Membership.deleteMany({ user: deletedMembershipOrg.user, workspace: { - $in: workspaces, - }, + $in: workspaces + } }); await Key.deleteMany({ receiver: deletedMembershipOrg.user, workspace: { - $in: workspaces, - }, + $in: workspaces + } }); } - return deletedMembershipOrg; -}; \ No newline at end of file + return deletedMembershipOrg; +}; diff --git a/backend/src/helpers/secrets.ts b/backend/src/helpers/secrets.ts index ad6296adf..3799dc1e7 100644 --- a/backend/src/helpers/secrets.ts +++ b/backend/src/helpers/secrets.ts @@ -504,11 +504,6 @@ export const getSecretsHelper = async ({ }: GetSecretsParams) => { let secrets: ISecret[] = []; // if using service token filter towards the folderId by secretpath - if (authData.authPayload instanceof ServiceTokenData) { - if (!isValidScope(authData.authPayload, environment, secretPath)) { - throw UnauthorizedRequestError({ message: "Folder Permission Denied" }); - } - } if (!folderId) { folderId = await getFolderIdFromServiceToken(workspaceId, environment, secretPath); @@ -575,20 +570,22 @@ export const getSecretsHelper = async ({ const postHogClient = await TelemetryService.getPostHogClient(); // reduce the number of events captured - let shouldRecordK8Event = false + let shouldRecordK8Event = false; if (authData.userAgent == K8_USER_AGENT_NAME) { const randomNumber = Math.random(); if (randomNumber > 0.9) { - shouldRecordK8Event = true + shouldRecordK8Event = true; } } - const numberOfSignupSecrets = (secrets.filter((secret) => secret?.metadata?.source === "signup")).length; - const atLeastOneNonSignUpSecret = (secrets.length - numberOfSignupSecrets > 0) + const numberOfSignupSecrets = secrets.filter( + (secret) => secret?.metadata?.source === "signup" + ).length; + const atLeastOneNonSignUpSecret = secrets.length - numberOfSignupSecrets > 0; if (postHogClient && atLeastOneNonSignUpSecret) { const shouldCapture = authData.userAgent !== K8_USER_AGENT_NAME || shouldRecordK8Event; - const approximateForNoneCapturedEvents = secrets.length * 10 + const approximateForNoneCapturedEvents = secrets.length * 10; if (shouldCapture) { postHogClient.capture({ @@ -633,11 +630,7 @@ export const getSecretHelper = async ({ }); let secret: ISecret | null = null; // if using service token filter towards the folderId by secretpath - if (authData.authPayload instanceof ServiceTokenData) { - if (!isValidScope(authData.authPayload, environment, secretPath)) { - throw UnauthorizedRequestError({ message: "Folder Permission Denied" }); - } - } + const folderId = await getFolderIdFromServiceToken(workspaceId, environment, secretPath); // try getting personal secret first (if exists) @@ -751,12 +744,6 @@ export const updateSecretHelper = async ({ }); let secret: ISecret | null = null; - // if using service token filter towards the folderId by secretpath - if (authData.authPayload instanceof ServiceTokenData) { - if (!isValidScope(authData.authPayload, environment, secretPath)) { - throw UnauthorizedRequestError({ message: "Folder Permission Denied" }); - } - } const folderId = await getFolderIdFromServiceToken(workspaceId, environment, secretPath); if (type === SECRET_SHARED) { @@ -916,12 +903,6 @@ export const deleteSecretHelper = async ({ workspaceId: new Types.ObjectId(workspaceId) }); - // if using service token filter towards the folderId by secretpath - if (authData.authPayload instanceof ServiceTokenData) { - if (!isValidScope(authData.authPayload, environment, secretPath)) { - throw UnauthorizedRequestError({ message: "Folder Permission Denied" }); - } - } const folderId = await getFolderIdFromServiceToken(workspaceId, environment, secretPath); let secrets: ISecret[] = []; diff --git a/backend/src/helpers/user.ts b/backend/src/helpers/user.ts index 59030ff8c..728c7d089 100644 --- a/backend/src/helpers/user.ts +++ b/backend/src/helpers/user.ts @@ -1,7 +1,4 @@ -import { - IUser, - User, -} from "../models"; +import { IUser, User } from "../models"; import { sendMail } from "./nodemailer"; /** @@ -12,10 +9,10 @@ import { sendMail } from "./nodemailer"; */ export const setupAccount = async ({ email }: { email: string }) => { const user = await new User({ - email, + email }).save(); - return user; + return user; }; /** @@ -37,36 +34,36 @@ export const setupAccount = async ({ email }: { email: string }) => { * @returns {Object} user - the completed user */ export const completeAccount = async ({ - userId, - firstName, - lastName, - encryptionVersion, - protectedKey, - protectedKeyIV, - protectedKeyTag, - publicKey, - encryptedPrivateKey, - encryptedPrivateKeyIV, - encryptedPrivateKeyTag, - salt, - verifier, + userId, + firstName, + lastName, + encryptionVersion, + protectedKey, + protectedKeyIV, + protectedKeyTag, + publicKey, + encryptedPrivateKey, + encryptedPrivateKeyIV, + encryptedPrivateKeyTag, + salt, + verifier }: { - userId: string; - firstName: string; - lastName: string; - encryptionVersion: number; - protectedKey: string; - protectedKeyIV: string; - protectedKeyTag: string; - publicKey: string; - encryptedPrivateKey: string; - encryptedPrivateKeyIV: string; - encryptedPrivateKeyTag: string; - salt: string; - verifier: string; + userId: string; + firstName: string; + lastName?: string; + encryptionVersion: number; + protectedKey: string; + protectedKeyIV: string; + protectedKeyTag: string; + publicKey: string; + encryptedPrivateKey: string; + encryptedPrivateKeyIV: string; + encryptedPrivateKeyTag: string; + salt: string; + verifier: string; }) => { const options = { - new: true, + new: true }; const user = await User.findByIdAndUpdate( userId, @@ -82,12 +79,12 @@ export const completeAccount = async ({ iv: encryptedPrivateKeyIV, tag: encryptedPrivateKeyTag, salt, - verifier, + verifier }, options ); - return user; + return user; }; /** @@ -98,38 +95,42 @@ export const completeAccount = async ({ * @param {String} obj.userAgent - login user-agent */ export const checkUserDevice = async ({ - user, - ip, - userAgent, + user, + ip, + userAgent }: { - user: IUser; - ip: string; - userAgent: string; + user: IUser; + ip: string; + userAgent: string; }) => { - const isDeviceSeen = user.devices.some((device) => device.ip === ip && device.userAgent === userAgent); - - if (!isDeviceSeen) { - // case: unseen login ip detected for user - // -> notify user about the sign-in from new ip - - user.devices = user.devices.concat([{ - ip: String(ip), - userAgent, - }]); - - await user.save(); + const isDeviceSeen = user.devices.some( + (device) => device.ip === ip && device.userAgent === userAgent + ); - // send MFA code [code] to [email] - await sendMail({ - template: "newDevice.handlebars", - subjectLine: "Successful login from new device", - recipients: [user.email], - substitutions: { - email: user.email, - timestamp: new Date().toString(), - ip, - userAgent, - }, - }); - } -} \ No newline at end of file + if (!isDeviceSeen) { + // case: unseen login ip detected for user + // -> notify user about the sign-in from new ip + + user.devices = user.devices.concat([ + { + ip: String(ip), + userAgent + } + ]); + + await user.save(); + + // send MFA code [code] to [email] + await sendMail({ + template: "newDevice.handlebars", + subjectLine: "Successful login from new device", + recipients: [user.email], + substitutions: { + email: user.email, + timestamp: new Date().toString(), + ip, + userAgent + } + }); + } +}; diff --git a/backend/src/helpers/validation.ts b/backend/src/helpers/validation.ts new file mode 100644 index 000000000..f552eb69b --- /dev/null +++ b/backend/src/helpers/validation.ts @@ -0,0 +1,17 @@ +import type { Request } from "express"; +import { AnyZodObject, ZodError, z } from "zod"; +import { BadRequestError } from "../utils/errors"; + +export async function validateRequest( + schema: T, + req: Request +): Promise> { + try { + return schema.parseAsync(req); + } catch (error) { + if (error instanceof ZodError) { + throw BadRequestError({ message: error.message }); + } + return BadRequestError({ message: JSON.stringify(error) }); + } +} diff --git a/backend/src/index.ts b/backend/src/index.ts index 098da2aad..aa4440a55 100644 --- a/backend/src/index.ts +++ b/backend/src/index.ts @@ -24,6 +24,7 @@ import { secretSnapshot as eeSecretSnapshotRouter, users as eeUsersRouter, workspace as eeWorkspaceRouter, + roles as v1RoleRouter, secretScanning as v1SecretScanningRouter } from "./ee/routes/v1"; import { @@ -37,7 +38,7 @@ import { membership as v1MembershipRouter, organization as v1OrganizationRouter, password as v1PasswordRouter, - secretImport as v1SecretImportRouter, + secretImps as v1SecretImpsRouter, secret as v1SecretRouter, secretsFolder as v1SecretsFolder, serviceToken as v1ServiceTokenRouter, @@ -137,6 +138,7 @@ const main = async () => { app.use((req, res, next) => { // default to IP address provided by Cloudflare + // #swagger.ignore = true const cfIp = req.headers["cf-connecting-ip"]; req.realIP = Array.isArray(cfIp) ? cfIp[0] : (cfIp as string) || req.ip; next(); @@ -152,7 +154,7 @@ const main = async () => { app.use("/api/v1/sso", eeSSORouter); app.use("/api/v1/cloud-products", eeCloudProductsRouter); - // v1 routes (default) + // v1 routes app.use("/api/v1/signup", v1SignupRouter); app.use("/api/v1/auth", v1AuthRouter); app.use("/api/v1/bot", v1BotRouter); @@ -161,7 +163,7 @@ const main = async () => { app.use("/api/v1/organization", v1OrganizationRouter); app.use("/api/v1/workspace", v1WorkspaceRouter); app.use("/api/v1/membership-org", v1MembershipOrgRouter); - app.use("/api/v1/membership", v1MembershipRouter); + app.use("/api/v1/membership", v1MembershipRouter); // app.use("/api/v1/key", v1KeyRouter); app.use("/api/v1/invite-org", v1InviteOrgRouter); app.use("/api/v1/secret", v1SecretRouter); // deprecate @@ -172,7 +174,8 @@ const main = async () => { app.use("/api/v1/folders", v1SecretsFolder); app.use("/api/v1/secret-scanning", v1SecretScanningRouter); app.use("/api/v1/webhooks", v1WebhooksRouter); - app.use("/api/v1/secret-imports", v1SecretImportRouter); + app.use("/api/v1/secret-imports", v1SecretImpsRouter); + app.use("/api/v1/roles", v1RoleRouter); // v2 routes (improvements) app.use("/api/v2/signup", v2SignupRouter); diff --git a/backend/src/integrations/sync.ts b/backend/src/integrations/sync.ts index 797d83f9a..6605b231f 100644 --- a/backend/src/integrations/sync.ts +++ b/backend/src/integrations/sync.ts @@ -2015,7 +2015,6 @@ const syncSecretsCheckly = async ({ secrets: Record; accessToken: string; }) => { - const getSecretsRes = ( await standardRequest.get(`${INTEGRATION_CHECKLY_API_URL}/v1/variables`, { headers: { @@ -2893,4 +2892,4 @@ const syncSecretsNorthflank = async ({ ); }; -export { syncSecrets }; +export { syncSecrets }; \ No newline at end of file diff --git a/backend/src/middleware/index.ts b/backend/src/middleware/index.ts index bc2cbc969..b8c88bd26 100644 --- a/backend/src/middleware/index.ts +++ b/backend/src/middleware/index.ts @@ -14,6 +14,9 @@ import requireServiceAccountAuth from "./requireServiceAccountAuth"; import requireServiceAccountWorkspacePermissionAuth from "./requireServiceAccountWorkspacePermissionAuth"; import requireSecretAuth from "./requireSecretAuth"; import requireSecretsAuth from "./requireSecretsAuth"; +import requireBlindIndicesEnabled from "./requireBlindIndicesEnabled"; +import requireE2EEOff from "./requireE2EEOff"; +import requireIPAllowlistCheck from "./requireIPAllowlistCheck"; import validateRequest from "./validateRequest"; export { @@ -33,5 +36,8 @@ export { requireServiceAccountWorkspacePermissionAuth, requireSecretAuth, requireSecretsAuth, + requireBlindIndicesEnabled, + requireE2EEOff, + requireIPAllowlistCheck, validateRequest, }; diff --git a/backend/src/middleware/requireBlindIndicesEnabled.ts b/backend/src/middleware/requireBlindIndicesEnabled.ts new file mode 100644 index 000000000..b1288dd2b --- /dev/null +++ b/backend/src/middleware/requireBlindIndicesEnabled.ts @@ -0,0 +1,34 @@ +import { NextFunction, Request, Response } from "express"; +import { Types } from "mongoose"; +import { SecretBlindIndexData } from "../models"; +import { UnauthorizedRequestError } from "../utils/errors"; + +type req = "params" | "body" | "query"; + +/** + * Validate if workspace with [workspaceId] has blind indices enabled + * @param {Object} obj + * @param {String} obj.locationWorkspaceId - location of [workspaceId] on request (e.g. params, body) for parsing + * @returns + */ +const requireBlindIndicesEnabled = ({ + locationWorkspaceId +}: { + locationWorkspaceId: req; +}) => { + return async (req: Request, res: Response, next: NextFunction) => { + const workspaceId = req[locationWorkspaceId]?.workspaceId; + + const secretBlindIndexData = await SecretBlindIndexData.exists({ + workspace: new Types.ObjectId(workspaceId) + }); + + if (!secretBlindIndexData) throw UnauthorizedRequestError({ + message: "Failed workspace authorization due to blind indices not being enabled" + }); + + return next(); + } +} + +export default requireBlindIndicesEnabled; \ No newline at end of file diff --git a/backend/src/middleware/requireE2EEOff.ts b/backend/src/middleware/requireE2EEOff.ts new file mode 100644 index 000000000..a9e5a735b --- /dev/null +++ b/backend/src/middleware/requireE2EEOff.ts @@ -0,0 +1,31 @@ +import { NextFunction, Request, Response } from "express"; +import { BadRequestError } from "../utils/errors"; +import { BotService } from "../services"; + +type req = "params" | "body" | "query"; + +/** + * Validate if workspace with [workspaceId] has E2EE off/disabled + * @param {Object} obj + * @param {String} obj.locationWorkspaceId - location of [workspaceId] on request (e.g. params, body) for parsing + * @returns + */ +const requireE2EEOff = ({ + locationWorkspaceId +}: { + locationWorkspaceId: req; +}) => { + return async (req: Request, _: Response, next: NextFunction) => { + const workspaceId = req[locationWorkspaceId]?.workspaceId; + + const isWorkspaceE2EE = await BotService.getIsWorkspaceE2EE(workspaceId); + + if (isWorkspaceE2EE) throw BadRequestError({ + message: "Failed workspace authorization due to end-to-end encryption not being disabled" + }); + + return next(); + } +} + +export default requireE2EEOff; \ No newline at end of file diff --git a/backend/src/middleware/requireIPAllowlistCheck.ts b/backend/src/middleware/requireIPAllowlistCheck.ts new file mode 100644 index 000000000..411c802a9 --- /dev/null +++ b/backend/src/middleware/requireIPAllowlistCheck.ts @@ -0,0 +1,55 @@ +import net from "net"; +import { NextFunction, Request, Response } from "express"; +import { UnauthorizedRequestError } from "../utils/errors"; +import { extractIPDetails } from "../utils/ip"; +import { ActorType, TrustedIP } from "../ee/models"; + +type req = "params" | "body" | "query"; + +/** + * Validate if workspace with [workspaceId] has E2EE off/disabled + * @param {Object} obj + * @param {String} obj.locationWorkspaceId - location of [workspaceId] on request (e.g. params, body) for parsing + * @returns + */ +const requireIPAllowlistCheck = ({ + locationWorkspaceId +}: { + locationWorkspaceId: req; +}) => { + return async (req: Request, _: Response, next: NextFunction) => { + const workspaceId = req[locationWorkspaceId]?.workspaceId; + + if (req.authData.actor.type === ActorType.SERVICE) { + const trustedIps = await TrustedIP.find({ + workspace: workspaceId + }); + + if (trustedIps.length > 0) { + // case: check the IP address of the inbound request against trusted IPs + + const blockList = new net.BlockList(); + + for (const trustedIp of trustedIps) { + if (trustedIp.prefix !== undefined) { + blockList.addSubnet(trustedIp.ipAddress, trustedIp.prefix, trustedIp.type); + } else { + blockList.addAddress(trustedIp.ipAddress, trustedIp.type); + } + } + + const { type } = extractIPDetails(req.authData.ipAddress); + const check = blockList.check(req.authData.ipAddress, type); + + if (!check) + throw UnauthorizedRequestError({ + message: "Failed workspace authorization" + }); + } + } + + return next(); + } +} + +export default requireIPAllowlistCheck; \ No newline at end of file diff --git a/backend/src/middleware/requireWorkspaceAuth.ts b/backend/src/middleware/requireWorkspaceAuth.ts index f6f7405a9..bbf829565 100644 --- a/backend/src/middleware/requireWorkspaceAuth.ts +++ b/backend/src/middleware/requireWorkspaceAuth.ts @@ -9,24 +9,18 @@ type req = "params" | "body" | "query"; * on request params. * @param {Object} obj * @param {String[]} obj.acceptedRoles - accepted workspace roles for JWT auth - * @param {String[]} obj.location - location of [workspaceId] on request (e.g. params, body) for parsing + * @param {String} obj.locationWorkspaceId - location of [workspaceId] on request (e.g. params, body) for parsing */ const requireWorkspaceAuth = ({ acceptedRoles, locationWorkspaceId, locationEnvironment = undefined, requiredPermissions = [], - requireBlindIndicesEnabled = false, - requireE2EEOff = false, - checkIPAllowlist = false }: { acceptedRoles: Array<"admin" | "member">; locationWorkspaceId: req; locationEnvironment?: req | undefined; requiredPermissions?: string[]; - requireBlindIndicesEnabled?: boolean; - requireE2EEOff?: boolean; - checkIPAllowlist?: boolean; }) => { return async (req: Request, res: Response, next: NextFunction) => { const workspaceId = req[locationWorkspaceId]?.workspaceId; @@ -38,10 +32,7 @@ const requireWorkspaceAuth = ({ workspaceId: new Types.ObjectId(workspaceId), environment, acceptedRoles, - requiredPermissions, - requireBlindIndicesEnabled, - requireE2EEOff, - checkIPAllowlist + requiredPermissions }); if (membership) { diff --git a/backend/src/models/membership.ts b/backend/src/models/membership.ts index 6c32ff64f..22a3819e2 100644 --- a/backend/src/models/membership.ts +++ b/backend/src/models/membership.ts @@ -1,55 +1,60 @@ import { Schema, Types, model } from "mongoose"; -import { ADMIN, MEMBER } from "../variables"; +import { ADMIN, CUSTOM, MEMBER, VIEWER } from "../variables"; export interface IMembershipPermission { - environmentSlug: string, - ability: string + environmentSlug: string; + ability: string; } export interface IMembership { - _id: Types.ObjectId; - user: Types.ObjectId; - inviteEmail?: string; - workspace: Types.ObjectId; - role: "admin" | "member"; - deniedPermissions: IMembershipPermission[] + _id: Types.ObjectId; + user: Types.ObjectId; + inviteEmail?: string; + workspace: Types.ObjectId; + role: "admin" | "member" | "viewer" | "custom"; + customRole: Types.ObjectId; + deniedPermissions: IMembershipPermission[]; } const membershipSchema = new Schema( - { - user: { - type: Schema.Types.ObjectId, - ref: "User", - }, - inviteEmail: { - type: String, - }, - workspace: { - type: Schema.Types.ObjectId, - ref: "Workspace", - required: true, - }, - deniedPermissions: { - type: [ - { - environmentSlug: String, - ability: { - type: String, - enum: ["read", "write"], - }, - }, - ], - default: [], - }, - role: { - type: String, - enum: [ADMIN, MEMBER], - required: true, - }, - }, - { - timestamps: true, - } + { + user: { + type: Schema.Types.ObjectId, + ref: "User" + }, + inviteEmail: { + type: String + }, + workspace: { + type: Schema.Types.ObjectId, + ref: "Workspace", + required: true + }, + deniedPermissions: { + type: [ + { + environmentSlug: String, + ability: { + type: String, + enum: ["read", "write"] + } + } + ], + default: [] + }, + role: { + type: String, + enum: [ADMIN, MEMBER, VIEWER, CUSTOM], + required: true + }, + customRole: { + type: Schema.Types.ObjectId, + ref: "Role" + } + }, + { + timestamps: true + } ); export const Membership = model("Membership", membershipSchema); \ No newline at end of file diff --git a/backend/src/models/membershipOrg.ts b/backend/src/models/membershipOrg.ts index b45f9cfe8..530797379 100644 --- a/backend/src/models/membershipOrg.ts +++ b/backend/src/models/membershipOrg.ts @@ -1,45 +1,47 @@ import { Document, Schema, Types, model } from "mongoose"; -import { ACCEPTED, ADMIN, INVITED, MEMBER, OWNER } from "../variables"; +import { ACCEPTED, ADMIN, CUSTOM, INVITED, MEMBER, OWNER } from "../variables"; export interface IMembershipOrg extends Document { - _id: Types.ObjectId; - user: Types.ObjectId; - inviteEmail: string; - organization: Types.ObjectId; - role: "owner" | "admin" | "member"; - status: "invited" | "accepted"; + _id: Types.ObjectId; + user: Types.ObjectId; + inviteEmail: string; + organization: Types.ObjectId; + role: "owner" | "admin" | "member" | "custom"; + customRole: Types.ObjectId; + status: "invited" | "accepted"; } const membershipOrgSchema = new Schema( - { - user: { - type: Schema.Types.ObjectId, - ref: "User", - }, - inviteEmail: { - type: String, - }, - organization: { - type: Schema.Types.ObjectId, - ref: "Organization", - }, - role: { - type: String, - enum: [OWNER, ADMIN, MEMBER], - required: true, - }, - status: { - type: String, - enum: [INVITED, ACCEPTED], - required: true, - }, - }, - { - timestamps: true, - } + { + user: { + type: Schema.Types.ObjectId, + ref: "User" + }, + inviteEmail: { + type: String + }, + organization: { + type: Schema.Types.ObjectId, + ref: "Organization" + }, + role: { + type: String, + enum: [OWNER, ADMIN, MEMBER, CUSTOM], + required: true + }, + status: { + type: String, + enum: [INVITED, ACCEPTED], + required: true + }, + customRole: { + type: Schema.Types.ObjectId, + ref: "Role" + } + }, + { + timestamps: true + } ); -export const MembershipOrg = model( - "MembershipOrg", - membershipOrgSchema -); \ No newline at end of file +export const MembershipOrg = model("MembershipOrg", membershipOrgSchema); diff --git a/backend/src/routes/v1/auth.ts b/backend/src/routes/v1/auth.ts index d9c0d6120..a3037f311 100644 --- a/backend/src/routes/v1/auth.ts +++ b/backend/src/routes/v1/auth.ts @@ -1,6 +1,5 @@ import express from "express"; const router = express.Router(); -import { body } from "express-validator"; import { requireAuth, validateRequest } from "../../middleware"; import { authController } from "../../controllers/v1"; import { authLimiter } from "../../helpers/rateLimiter"; @@ -12,9 +11,6 @@ router.post( // TODO endpoint: deprecate (moved to api/v3/auth/login1) "/login1", authLimiter, - body("email").exists().trim().notEmpty().toLowerCase(), - body("clientPublicKey").exists().trim().notEmpty(), - validateRequest, authController.login1 ); @@ -22,9 +18,6 @@ router.post( // TODO endpoint: deprecate (moved to api/v3/auth/login2) "/login2", authLimiter, - body("email").exists().trim().notEmpty().toLowerCase(), - body("clientProof").exists().trim().notEmpty(), - validateRequest, authController.login2 ); diff --git a/backend/src/routes/v1/bot.ts b/backend/src/routes/v1/bot.ts index e50d35625..5e76288cd 100644 --- a/backend/src/routes/v1/bot.ts +++ b/backend/src/routes/v1/bot.ts @@ -1,41 +1,25 @@ import express from "express"; const router = express.Router(); -import { body, param } from "express-validator"; import { - requireAuth, - requireBotAuth, - requireWorkspaceAuth, - validateRequest, + requireAuth } from "../../middleware"; import { botController } from "../../controllers/v1"; -import { ADMIN, AuthMode, MEMBER } from "../../variables"; +import { AuthMode } from "../../variables"; router.get( - "/:workspaceId", - requireAuth({ - acceptedAuthModes: [AuthMode.JWT], - }), - requireWorkspaceAuth({ - acceptedRoles: [ADMIN, MEMBER], - locationWorkspaceId: "params", - }), - param("workspaceId").exists().trim().notEmpty(), - validateRequest, - botController.getBotByWorkspaceId + "/:workspaceId", + requireAuth({ + acceptedAuthModes: [AuthMode.JWT] + }), + botController.getBotByWorkspaceId ); router.patch( - "/:botId/active", - requireAuth({ - acceptedAuthModes: [AuthMode.JWT], - }), - requireBotAuth({ - acceptedRoles: [ADMIN, MEMBER], - }), - body("isActive").exists().isBoolean(), - body("botKey"), - validateRequest, - botController.setBotActiveState + "/:botId/active", + requireAuth({ + acceptedAuthModes: [AuthMode.JWT] + }), + botController.setBotActiveState ); -export default router; \ No newline at end of file +export default router; diff --git a/backend/src/routes/v1/index.ts b/backend/src/routes/v1/index.ts index 08298a1c6..46e74d58c 100644 --- a/backend/src/routes/v1/index.ts +++ b/backend/src/routes/v1/index.ts @@ -16,7 +16,7 @@ import integration from "./integration"; import integrationAuth from "./integrationAuth"; import secretsFolder from "./secretsFolder"; import webhooks from "./webhook"; -import secretImport from "./secretImport"; +import secretImps from "./secretImps"; export { signup, @@ -37,5 +37,5 @@ export { integrationAuth, secretsFolder, webhooks, - secretImport + secretImps }; diff --git a/backend/src/routes/v1/integration.ts b/backend/src/routes/v1/integration.ts index 22aa901ea..6dda7527b 100644 --- a/backend/src/routes/v1/integration.ts +++ b/backend/src/routes/v1/integration.ts @@ -1,45 +1,14 @@ import express from "express"; const router = express.Router(); -import { - requireAuth, - requireIntegrationAuth, - requireIntegrationAuthorizationAuth, - requireWorkspaceAuth, - validateRequest, -} from "../../middleware"; -import { - ADMIN, - AuthMode, - MEMBER -} from "../../variables"; -import { body, param } from "express-validator"; +import { requireAuth } from "../../middleware"; +import { AuthMode } from "../../variables"; import { integrationController } from "../../controllers/v1"; router.post( "/", requireAuth({ - acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY], + acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY] }), - requireIntegrationAuthorizationAuth({ - acceptedRoles: [ADMIN, MEMBER], - location: "body", - }), - body("integrationAuthId").exists().isString().trim(), - body("isActive").exists().isBoolean(), - body("app").optional().isString().trim(), - body("appId").optional().isString().trim(), - body("secretPath").default("/").isString().trim(), - body("sourceEnvironment").trim(), - body("targetEnvironment").trim(), - body("targetEnvironmentId").trim(), - body("targetService").trim(), - body("targetServiceId").trim(), - body("owner").trim(), - body("path").trim(), - body("region").trim(), - body("metadata").optional().isObject().withMessage("Metadata should be an object"), - body("metadata.secretSuffix").optional().isString().withMessage("Suffix should be a string"), - validateRequest, integrationController.createIntegration ); @@ -48,18 +17,6 @@ router.patch( requireAuth({ acceptedAuthModes: [AuthMode.JWT] }), - requireIntegrationAuth({ - acceptedRoles: [ADMIN, MEMBER], - }), - param("integrationId").exists().trim(), - body("isActive").exists().isBoolean(), - body("app").exists().trim(), - body("secretPath").default("/").isString().trim(), - body("environment").exists().trim(), - body("appId").exists(), - body("targetEnvironment").exists(), - body("owner").exists(), - validateRequest, integrationController.updateIntegration ); @@ -68,11 +25,6 @@ router.delete( requireAuth({ acceptedAuthModes: [AuthMode.JWT] }), - requireIntegrationAuth({ - acceptedRoles: [ADMIN, MEMBER], - }), - param("integrationId").exists().trim(), - validateRequest, integrationController.deleteIntegration ); @@ -81,13 +33,6 @@ router.post( requireAuth({ acceptedAuthModes: [AuthMode.JWT] }), - requireWorkspaceAuth({ - acceptedRoles: [ADMIN, MEMBER], - locationWorkspaceId: "body", - }), - body("environment").isString().exists().trim(), - body("workspaceId").exists().trim(), - validateRequest, integrationController.manualSync ); diff --git a/backend/src/routes/v1/integrationAuth.ts b/backend/src/routes/v1/integrationAuth.ts index 73278a962..cfb27ee71 100644 --- a/backend/src/routes/v1/integrationAuth.ts +++ b/backend/src/routes/v1/integrationAuth.ts @@ -1,202 +1,111 @@ import express from "express"; const router = express.Router(); -import { body, param, query } from "express-validator"; -import { - requireAuth, - requireIntegrationAuthorizationAuth, - requireWorkspaceAuth, - validateRequest, -} from "../../middleware"; -import { - ADMIN, - AuthMode, - MEMBER -} from "../../variables"; +import { requireAuth } from "../../middleware"; +import { AuthMode } from "../../variables"; import { integrationAuthController } from "../../controllers/v1"; router.get( - "/integration-options", - requireAuth({ - acceptedAuthModes: [AuthMode.JWT], - }), - integrationAuthController.getIntegrationOptions + "/integration-options", + requireAuth({ + acceptedAuthModes: [AuthMode.JWT] + }), + integrationAuthController.getIntegrationOptions ); router.get( - "/:integrationAuthId", - requireAuth({ - acceptedAuthModes: [AuthMode.JWT], - }), - requireIntegrationAuthorizationAuth({ - acceptedRoles: [ADMIN, MEMBER], - attachAccessToken: false - }), - param("integrationAuthId"), - validateRequest, - integrationAuthController.getIntegrationAuth + "/:integrationAuthId", + requireAuth({ + acceptedAuthModes: [AuthMode.JWT] + }), + integrationAuthController.getIntegrationAuth ); router.post( - "/oauth-token", - requireAuth({ - acceptedAuthModes: [AuthMode.JWT], - }), - requireWorkspaceAuth({ - acceptedRoles: [ADMIN, MEMBER], - locationWorkspaceId: "body", - }), - body("workspaceId").exists().trim().notEmpty(), - body("code").exists().trim().notEmpty(), - body("integration").exists().trim().notEmpty(), - body("url").optional().isString().trim(), - validateRequest, - integrationAuthController.oAuthExchange + "/oauth-token", + requireAuth({ + acceptedAuthModes: [AuthMode.JWT] + }), + integrationAuthController.oAuthExchange ); router.post( - "/access-token", - body("workspaceId").exists().trim().notEmpty(), - body("refreshToken").optional().isString().trim().notEmpty(), - body("accessId").optional().isString().trim(), - body("accessToken").optional().isString().trim().notEmpty(), - body("url").trim(), - body("namespace").trim(), - body("integration").exists().trim().notEmpty(), - validateRequest, - requireAuth({ - acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY], - }), - requireWorkspaceAuth({ - acceptedRoles: [ADMIN, MEMBER], - locationWorkspaceId: "body", - }), - integrationAuthController.saveIntegrationToken + "/access-token", + requireAuth({ + acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY] + }), + integrationAuthController.saveIntegrationToken ); router.get( - "/:integrationAuthId/apps", - requireAuth({ - acceptedAuthModes: [AuthMode.JWT], - }), - requireIntegrationAuthorizationAuth({ - acceptedRoles: [ADMIN, MEMBER], - }), - param("integrationAuthId"), - query("teamId"), - query("workspaceSlug"), - validateRequest, - integrationAuthController.getIntegrationAuthApps + "/:integrationAuthId/apps", + requireAuth({ + acceptedAuthModes: [AuthMode.JWT] + }), + integrationAuthController.getIntegrationAuthApps ); router.get( - "/:integrationAuthId/teams", - requireAuth({ - acceptedAuthModes: [AuthMode.JWT], - }), - requireIntegrationAuthorizationAuth({ - acceptedRoles: [ADMIN, MEMBER], - }), - param("integrationAuthId"), - validateRequest, - integrationAuthController.getIntegrationAuthTeams + "/:integrationAuthId/teams", + requireAuth({ + acceptedAuthModes: [AuthMode.JWT] + }), + integrationAuthController.getIntegrationAuthTeams ); router.get( - "/:integrationAuthId/vercel/branches", - requireAuth({ - acceptedAuthModes: [AuthMode.JWT], - }), - requireIntegrationAuthorizationAuth({ - acceptedRoles: [ADMIN, MEMBER], - }), - param("integrationAuthId").exists().isString(), - query("appId").exists().isString(), - query("teamId").optional().isString(), - validateRequest, - integrationAuthController.getIntegrationAuthVercelBranches + "/:integrationAuthId/vercel/branches", + requireAuth({ + acceptedAuthModes: [AuthMode.JWT] + }), + integrationAuthController.getIntegrationAuthVercelBranches ); router.get( - "/:integrationAuthId/railway/environments", - requireAuth({ - acceptedAuthModes: [AuthMode.JWT], - }), - requireIntegrationAuthorizationAuth({ - acceptedRoles: [ADMIN, MEMBER], - }), - param("integrationAuthId").exists().isString(), - query("appId").exists().isString(), - validateRequest, - integrationAuthController.getIntegrationAuthRailwayEnvironments + "/:integrationAuthId/railway/environments", + requireAuth({ + acceptedAuthModes: [AuthMode.JWT] + }), + integrationAuthController.getIntegrationAuthRailwayEnvironments ); router.get( - "/:integrationAuthId/railway/services", - requireAuth({ - acceptedAuthModes: [AuthMode.JWT], - }), - requireIntegrationAuthorizationAuth({ - acceptedRoles: [ADMIN, MEMBER], - }), - param("integrationAuthId").exists().isString(), - query("appId").exists().isString(), - validateRequest, - integrationAuthController.getIntegrationAuthRailwayServices + "/:integrationAuthId/railway/services", + requireAuth({ + acceptedAuthModes: [AuthMode.JWT] + }), + integrationAuthController.getIntegrationAuthRailwayServices ); router.get( - "/:integrationAuthId/bitbucket/workspaces", - requireAuth({ - acceptedAuthModes: [AuthMode.JWT], - }), - requireIntegrationAuthorizationAuth({ - acceptedRoles: [ADMIN, MEMBER], - }), - param("integrationAuthId").exists().isString(), - validateRequest, - integrationAuthController.getIntegrationAuthBitBucketWorkspaces + "/:integrationAuthId/bitbucket/workspaces", + requireAuth({ + acceptedAuthModes: [AuthMode.JWT] + }), + integrationAuthController.getIntegrationAuthBitBucketWorkspaces ); router.get( - "/:integrationAuthId/northflank/secret-groups", - requireAuth({ - acceptedAuthModes: [AuthMode.JWT], - }), - requireIntegrationAuthorizationAuth({ - acceptedRoles: [ADMIN, MEMBER], - }), - param("integrationAuthId").exists().isString(), - query("appId").exists().isString(), - validateRequest, - integrationAuthController.getIntegrationAuthNorthflankSecretGroups + "/:integrationAuthId/northflank/secret-groups", + requireAuth({ + acceptedAuthModes: [AuthMode.JWT] + }), + integrationAuthController.getIntegrationAuthNorthflankSecretGroups ); router.get( - "/:integrationAuthId/teamcity/build-configs", - requireAuth({ - acceptedAuthModes: [AuthMode.JWT], - }), - requireIntegrationAuthorizationAuth({ - acceptedRoles: [ADMIN, MEMBER], - }), - param("integrationAuthId").exists().isString(), - query("appId").exists().isString(), - validateRequest, - integrationAuthController.getIntegrationAuthTeamCityBuildConfigs + "/:integrationAuthId/teamcity/build-configs", + requireAuth({ + acceptedAuthModes: [AuthMode.JWT] + }), + integrationAuthController.getIntegrationAuthTeamCityBuildConfigs ); router.delete( - "/:integrationAuthId", - requireAuth({ - acceptedAuthModes: [AuthMode.JWT], - }), - requireIntegrationAuthorizationAuth({ - acceptedRoles: [ADMIN, MEMBER], - attachAccessToken: false, - }), - param("integrationAuthId"), - validateRequest, - integrationAuthController.deleteIntegrationAuth + "/:integrationAuthId", + requireAuth({ + acceptedAuthModes: [AuthMode.JWT] + }), + integrationAuthController.deleteIntegrationAuth ); export default router; diff --git a/backend/src/routes/v1/inviteOrg.ts b/backend/src/routes/v1/inviteOrg.ts index f79ce61d1..0fa4ffbfe 100644 --- a/backend/src/routes/v1/inviteOrg.ts +++ b/backend/src/routes/v1/inviteOrg.ts @@ -8,23 +8,20 @@ import { AuthMode } from "../../variables"; // TODO endpoint: consider moving these endpoints to be under /organization to be more RESTful router.post( - "/signup", - requireAuth({ - acceptedAuthModes: [AuthMode.JWT], - }), - body("inviteeEmail").exists().trim().notEmpty().isEmail(), - body("organizationId").exists().trim().notEmpty(), - validateRequest, - membershipOrgController.inviteUserToOrganization + "/signup", + requireAuth({ + acceptedAuthModes: [AuthMode.JWT] + }), + membershipOrgController.inviteUserToOrganization ); router.post( - "/verify", - body("email").exists().trim().notEmpty(), - body("organizationId").exists().trim().notEmpty(), - body("code").exists().trim().notEmpty(), - validateRequest, - membershipOrgController.verifyUserToOrganization + "/verify", + body("email").exists().trim().notEmpty(), + body("organizationId").exists().trim().notEmpty(), + body("code").exists().trim().notEmpty(), + validateRequest, + membershipOrgController.verifyUserToOrganization ); export default router; diff --git a/backend/src/routes/v1/key.ts b/backend/src/routes/v1/key.ts index a72b508b9..be2c8d929 100644 --- a/backend/src/routes/v1/key.ts +++ b/backend/src/routes/v1/key.ts @@ -1,43 +1,26 @@ import express from "express"; const router = express.Router(); -import { - requireAuth, - requireWorkspaceAuth, - validateRequest, -} from "../../middleware"; -import { body, param } from "express-validator"; -import { ADMIN, AuthMode, MEMBER } from "../../variables"; +import { requireAuth } from "../../middleware"; +import { AuthMode } from "../../variables"; import { keyController } from "../../controllers/v1"; // TODO endpoint: consider moving these endpoints to be under /workspaces to be more RESTful router.post( - "/:workspaceId", - requireAuth({ - acceptedAuthModes: [AuthMode.JWT], - }), - requireWorkspaceAuth({ - acceptedRoles: [ADMIN, MEMBER], - locationWorkspaceId: "params", - }), - param("workspaceId").exists().trim(), - body("key").exists(), - validateRequest, - keyController.uploadKey + "/:workspaceId", + requireAuth({ + acceptedAuthModes: [AuthMode.JWT] + }), + keyController.uploadKey ); -router.get( // TODO endpoint: deprecate (note: move frontend to v2/workspace/key or something) - "/:workspaceId/latest", - requireAuth({ - acceptedAuthModes: [AuthMode.JWT], - }), - requireWorkspaceAuth({ - acceptedRoles: [ADMIN, MEMBER], - locationWorkspaceId: "params", - }), - param("workspaceId"), - validateRequest, - keyController.getLatestKey +router.get( + // TODO endpoint: deprecate (note: move frontend to v2/workspace/key or something) + "/:workspaceId/latest", + requireAuth({ + acceptedAuthModes: [AuthMode.JWT] + }), + keyController.getLatestKey ); export default router; diff --git a/backend/src/routes/v1/membership.ts b/backend/src/routes/v1/membership.ts index cf38c7cbc..54b3b7c9e 100644 --- a/backend/src/routes/v1/membership.ts +++ b/backend/src/routes/v1/membership.ts @@ -1,53 +1,37 @@ import express from "express"; const router = express.Router(); -import { body, param } from "express-validator"; -import { requireAuth, validateRequest } from "../../middleware"; +import { requireAuth } from "../../middleware"; import { membershipController } from "../../controllers/v1"; -import { membershipController as EEMembershipControllers } from "../../ee/controllers/v1"; import { AuthMode } from "../../variables"; // note: ALL DEPRECIATED (moved to api/v2/workspace/:workspaceId/memberships/:membershipId) // TODO endpoint: consider moving these endpoints to be under /workspace to be more RESTful -router.get( // TODO endpoint: deprecate - used for old CLI (deprecate) - "/:workspaceId/connect", - requireAuth({ - acceptedAuthModes: [AuthMode.JWT], - }), - param("workspaceId").exists().trim(), - validateRequest, - membershipController.validateMembership +router.get( + // TODO endpoint: deprecate - used for old CLI (deprecate) + "/:workspaceId/connect", + requireAuth({ + acceptedAuthModes: [AuthMode.JWT] + }), + membershipController.validateMembership ); -router.delete( // TODO endpoint: check dashboard - "/:membershipId", - requireAuth({ - acceptedAuthModes: [AuthMode.JWT], - }), - param("membershipId").exists().trim(), - validateRequest, - membershipController.deleteMembership +router.delete( + // TODO endpoint: check dashboard + "/:membershipId", + requireAuth({ + acceptedAuthModes: [AuthMode.JWT] + }), + membershipController.deleteMembership ); -router.post( // TODO endpoint: check dashboard - "/:membershipId/change-role", - requireAuth({ - acceptedAuthModes: [AuthMode.JWT], - }), - body("role").exists().trim(), - validateRequest, - membershipController.changeMembershipRole -); - -router.post( // TODO endpoint: check dashboard - "/:membershipId/deny-permissions", - requireAuth({ - acceptedAuthModes: [AuthMode.JWT], - }), - param("membershipId").isMongoId().exists().trim(), - body("permissions").isArray().exists(), - validateRequest, - EEMembershipControllers.denyMembershipPermissions +router.post( + // TODO endpoint: check dashboard + "/:membershipId/change-role", + requireAuth({ + acceptedAuthModes: [AuthMode.JWT] + }), + membershipController.changeMembershipRole ); export default router; diff --git a/backend/src/routes/v1/membershipOrg.ts b/backend/src/routes/v1/membershipOrg.ts index 34899072b..1c7c47f88 100644 --- a/backend/src/routes/v1/membershipOrg.ts +++ b/backend/src/routes/v1/membershipOrg.ts @@ -5,24 +5,23 @@ import { requireAuth, validateRequest } from "../../middleware"; import { membershipOrgController } from "../../controllers/v1"; import { AuthMode } from "../../variables"; -router.post( // TODO endpoint: check dashboard - "/membershipOrg/:membershipOrgId/change-role", - requireAuth({ - acceptedAuthModes: [AuthMode.JWT], - }), - param("membershipOrgId"), - validateRequest, - membershipOrgController.changeMembershipOrgRole +router.post( + // TODO endpoint: check dashboard + "/membershipOrg/:membershipOrgId/change-role", + requireAuth({ + acceptedAuthModes: [AuthMode.JWT] + }), + param("membershipOrgId"), + validateRequest, + membershipOrgController.changeMembershipOrgRole ); router.delete( - "/:membershipOrgId", // TODO endpoint: check dashboard - requireAuth({ - acceptedAuthModes: [AuthMode.JWT], - }), - param("membershipOrgId").exists().trim(), - validateRequest, - membershipOrgController.deleteMembershipOrg + "/:membershipOrgId", // TODO endpoint: check dashboard + requireAuth({ + acceptedAuthModes: [AuthMode.JWT] + }), + membershipOrgController.deleteMembershipOrg ); export default router; diff --git a/backend/src/routes/v1/organization.ts b/backend/src/routes/v1/organization.ts index 78672093d..011d5fed6 100644 --- a/backend/src/routes/v1/organization.ts +++ b/backend/src/routes/v1/organization.ts @@ -1,166 +1,99 @@ import express from "express"; const router = express.Router(); -import { body, param } from "express-validator"; -import { - requireAuth, - requireOrganizationAuth, - validateRequest, -} from "../../middleware"; -import { - ACCEPTED, - ADMIN, - AuthMode, - MEMBER, - OWNER -} from "../../variables"; +import { requireAuth } from "../../middleware"; +import { AuthMode } from "../../variables"; import { organizationController } from "../../controllers/v1"; -router.get( // TODO endpoint: deprecate (moved to api/v2/users/me/organizations) - "/", - requireAuth({ - acceptedAuthModes: [AuthMode.JWT], - }), - organizationController.getOrganizations +router.get( + // TODO endpoint: deprecate (moved to api/v2/users/me/organizations) + "/", + requireAuth({ + acceptedAuthModes: [AuthMode.JWT] + }), + organizationController.getOrganizations ); -router.post( // not used on frontend - "/", - requireAuth({ - acceptedAuthModes: [AuthMode.JWT], - }), - body("organizationName").exists().trim().notEmpty(), - validateRequest, - organizationController.createOrganization +router.post( + // not used on frontend + "/", + requireAuth({ + acceptedAuthModes: [AuthMode.JWT] + }), + organizationController.createOrganization ); router.get( - "/:organizationId", - requireAuth({ - acceptedAuthModes: [AuthMode.JWT], - }), - requireOrganizationAuth({ - acceptedRoles: [OWNER, ADMIN, MEMBER], - acceptedStatuses: [ACCEPTED], - }), - param("organizationId").exists().trim(), - validateRequest, - organizationController.getOrganization + "/:organizationId", + requireAuth({ + acceptedAuthModes: [AuthMode.JWT] + }), + organizationController.getOrganization ); -router.get( // TODO endpoint: deprecate (moved to api/v2/organizations/:organizationId/memberships) - "/:organizationId/users", - requireAuth({ - acceptedAuthModes: [AuthMode.JWT], - }), - requireOrganizationAuth({ - acceptedRoles: [OWNER, ADMIN, MEMBER], - acceptedStatuses: [ACCEPTED], - }), - param("organizationId").exists().trim(), - validateRequest, - organizationController.getOrganizationMembers +router.get( + // TODO endpoint: deprecate (moved to api/v2/organizations/:organizationId/memberships) + "/:organizationId/users", + requireAuth({ + acceptedAuthModes: [AuthMode.JWT] + }), + organizationController.getOrganizationMembers ); -router.get( // TODO endpoint: move to /v2/users/me/organizations/:organizationId/workspaces - "/:organizationId/my-workspaces", // deprecated (moved to api/v2/organizations/:organizationId/workspaces) - requireAuth({ - acceptedAuthModes: [AuthMode.JWT], - }), - requireOrganizationAuth({ - acceptedRoles: [OWNER, ADMIN, MEMBER], - acceptedStatuses: [ACCEPTED], - }), - param("organizationId").exists().trim(), - validateRequest, - organizationController.getOrganizationWorkspaces +router.get( + // TODO endpoint: move to /v2/users/me/organizations/:organizationId/workspaces + "/:organizationId/my-workspaces", // deprecated (moved to api/v2/organizations/:organizationId/workspaces) + requireAuth({ + acceptedAuthModes: [AuthMode.JWT] + }), + organizationController.getOrganizationWorkspaces ); router.patch( - "/:organizationId/name", - requireAuth({ - acceptedAuthModes: [AuthMode.JWT], - }), - requireOrganizationAuth({ - acceptedRoles: [OWNER, ADMIN, MEMBER], - acceptedStatuses: [ACCEPTED], - }), - param("organizationId").exists().trim(), - body("name").exists().trim().notEmpty(), - validateRequest, - organizationController.changeOrganizationName + "/:organizationId/name", + requireAuth({ + acceptedAuthModes: [AuthMode.JWT] + }), + organizationController.changeOrganizationName ); router.get( - "/:organizationId/incidentContactOrg", - requireAuth({ - acceptedAuthModes: [AuthMode.JWT], - }), - requireOrganizationAuth({ - acceptedRoles: [OWNER, ADMIN, MEMBER], - acceptedStatuses: [ACCEPTED], - }), - param("organizationId").exists().trim(), - validateRequest, - organizationController.getOrganizationIncidentContacts + "/:organizationId/incidentContactOrg", + requireAuth({ + acceptedAuthModes: [AuthMode.JWT] + }), + organizationController.getOrganizationIncidentContacts ); router.post( - "/:organizationId/incidentContactOrg", - requireAuth({ - acceptedAuthModes: [AuthMode.JWT], - }), - requireOrganizationAuth({ - acceptedRoles: [OWNER, ADMIN, MEMBER], - acceptedStatuses: [ACCEPTED], - }), - param("organizationId").exists().trim(), - body("email").exists().trim().notEmpty(), - validateRequest, - organizationController.addOrganizationIncidentContact + "/:organizationId/incidentContactOrg", + requireAuth({ + acceptedAuthModes: [AuthMode.JWT] + }), + organizationController.addOrganizationIncidentContact ); router.delete( - "/:organizationId/incidentContactOrg", - requireAuth({ - acceptedAuthModes: [AuthMode.JWT], - }), - requireOrganizationAuth({ - acceptedRoles: [OWNER, ADMIN, MEMBER], - acceptedStatuses: [ACCEPTED], - }), - param("organizationId").exists().trim(), - body("email").exists().trim().notEmpty(), - validateRequest, - organizationController.deleteOrganizationIncidentContact + "/:organizationId/incidentContactOrg", + requireAuth({ + acceptedAuthModes: [AuthMode.JWT] + }), + organizationController.deleteOrganizationIncidentContact ); router.post( - "/:organizationId/customer-portal-session", // TODO endpoint: move to EE - requireAuth({ - acceptedAuthModes: [AuthMode.JWT], - }), - requireOrganizationAuth({ - acceptedRoles: [OWNER, ADMIN, MEMBER], - acceptedStatuses: [ACCEPTED], - }), - param("organizationId").exists().trim(), - validateRequest, - organizationController.createOrganizationPortalSession + "/:organizationId/customer-portal-session", // TODO endpoint: move to EE + requireAuth({ + acceptedAuthModes: [AuthMode.JWT] + }), + organizationController.createOrganizationPortalSession ); router.get( - "/:organizationId/workspace-memberships", - requireAuth({ - acceptedAuthModes: [AuthMode.JWT] - }), - requireOrganizationAuth({ - acceptedRoles: [OWNER, ADMIN, MEMBER], - acceptedStatuses: [ACCEPTED], - }), - param("organizationId").exists().trim(), - validateRequest, - organizationController.getOrganizationMembersAndTheirWorkspaces + "/:organizationId/workspace-memberships", + requireAuth({ + acceptedAuthModes: [AuthMode.JWT] + }), + organizationController.getOrganizationMembersAndTheirWorkspaces ); - export default router; diff --git a/backend/src/routes/v1/password.ts b/backend/src/routes/v1/password.ts index 3bccc0934..aec995cee 100644 --- a/backend/src/routes/v1/password.ts +++ b/backend/src/routes/v1/password.ts @@ -1,93 +1,51 @@ import express from "express"; const router = express.Router(); -import { body } from "express-validator"; -import { requireAuth, requireSignupAuth, validateRequest } from "../../middleware"; +import { requireAuth, requireSignupAuth } from "../../middleware"; import { passwordController } from "../../controllers/v1"; import { passwordLimiter } from "../../helpers/rateLimiter"; import { AuthMode } from "../../variables"; router.post( - "/srp1", - requireAuth({ - acceptedAuthModes: [AuthMode.JWT], - }), - body("clientPublicKey").exists().isString().trim().notEmpty(), - validateRequest, - passwordController.srp1 + "/srp1", + requireAuth({ + acceptedAuthModes: [AuthMode.JWT] + }), + passwordController.srp1 ); router.post( - "/change-password", - passwordLimiter, - requireAuth({ - acceptedAuthModes: [AuthMode.JWT], - }), - body("clientProof").exists().trim().notEmpty(), - body("protectedKey").exists().isString().trim().notEmpty(), - body("protectedKeyIV").exists().isString().trim().notEmpty(), - body("protectedKeyTag").exists().isString().trim().notEmpty(), - body("encryptedPrivateKey").exists().isString().trim().notEmpty(), // private key encrypted under new pwd - body("encryptedPrivateKeyIV").exists().isString().trim().notEmpty(), // new iv for private key - body("encryptedPrivateKeyTag").exists().isString().trim().notEmpty(), // new tag for private key - body("salt").exists().isString().trim().notEmpty(), // part of new pwd - body("verifier").exists().isString().trim().notEmpty(), // part of new pwd - validateRequest, - passwordController.changePassword + "/change-password", + passwordLimiter, + requireAuth({ + acceptedAuthModes: [AuthMode.JWT] + }), + passwordController.changePassword ); -router.post( - "/email/password-reset", - passwordLimiter, - body("email").exists().isString().trim().notEmpty().isEmail(), - validateRequest, - passwordController.emailPasswordReset -); +router.post("/email/password-reset", passwordLimiter, passwordController.emailPasswordReset); router.post( - "/email/password-reset-verify", - passwordLimiter, - body("email").exists().isString().trim().notEmpty().isEmail(), - body("code").exists().isString().trim().notEmpty(), - validateRequest, - passwordController.emailPasswordResetVerify + "/email/password-reset-verify", + passwordLimiter, + passwordController.emailPasswordResetVerify ); router.get( - "/backup-private-key", - passwordLimiter, - requireSignupAuth, - passwordController.getBackupPrivateKey + "/backup-private-key", + passwordLimiter, + requireSignupAuth, + passwordController.getBackupPrivateKey ); router.post( - "/backup-private-key", - passwordLimiter, - requireAuth({ - acceptedAuthModes: [AuthMode.JWT], - }), - body("clientProof").exists().isString().trim().notEmpty(), - body("encryptedPrivateKey").exists().isString().trim().notEmpty(), // (backup) private key encrypted under a strong key - body("iv").exists().isString().trim().notEmpty(), // new iv for (backup) private key - body("tag").exists().isString().trim().notEmpty(), // new tag for (backup) private key - body("salt").exists().isString().trim().notEmpty(), // salt generated from strong key - body("verifier").exists().isString().trim().notEmpty(), // salt generated from strong key - validateRequest, - passwordController.createBackupPrivateKey + "/backup-private-key", + passwordLimiter, + requireAuth({ + acceptedAuthModes: [AuthMode.JWT] + }), + passwordController.createBackupPrivateKey ); -router.post( - "/password-reset", - requireSignupAuth, - body("protectedKey").exists().isString().trim().notEmpty(), - body("protectedKeyIV").exists().isString().trim().notEmpty(), - body("protectedKeyTag").exists().isString().trim().notEmpty(), - body("encryptedPrivateKey").exists().isString().trim().notEmpty(), // private key encrypted under new pwd - body("encryptedPrivateKeyIV").exists().isString().trim().notEmpty(), // new iv for private key - body("encryptedPrivateKeyTag").exists().isString().trim().notEmpty(), // new tag for private key - body("salt").exists().isString().trim().notEmpty(), // part of new pwd - body("verifier").exists().isString().trim().notEmpty(), // part of new pwd - validateRequest, - passwordController.resetPassword -); +router.post("/password-reset", requireSignupAuth, passwordController.resetPassword); -export default router; \ No newline at end of file +export default router; diff --git a/backend/src/routes/v1/secretImport.ts b/backend/src/routes/v1/secretImport.ts deleted file mode 100644 index 21973f57e..000000000 --- a/backend/src/routes/v1/secretImport.ts +++ /dev/null @@ -1,84 +0,0 @@ -import express from "express"; -import { body, param, query } from "express-validator"; -import { secretImportController } from "../../controllers/v1"; -import { requireAuth, requireWorkspaceAuth, validateRequest } from "../../middleware"; -import { ADMIN, AuthMode, MEMBER } from "../../variables"; -const router = express.Router(); - -router.post( - "/", - requireAuth({ - acceptedAuthModes: [AuthMode.JWT,AuthMode.SERVICE_TOKEN] - }), - requireWorkspaceAuth({ - acceptedRoles: [ADMIN, MEMBER], - locationWorkspaceId: "body" - }), - body("workspaceId").exists().isString().trim().notEmpty(), - body("environment").exists().isString().trim().notEmpty(), - body("folderId").default("root").isString().trim(), - body("secretImport").exists().isObject(), - body("secretImport.environment").isString().exists().trim(), - body("secretImport.secretPath").isString().exists().trim(), - validateRequest, - secretImportController.createSecretImport -); - -router.put( - "/:id", - requireAuth({ - acceptedAuthModes: [AuthMode.JWT,AuthMode.SERVICE_TOKEN] - }), - param("id").exists().isString().trim(), - body("secretImports").exists().isArray(), - body("secretImports.*.environment").isString().exists().trim(), - body("secretImports.*.secretPath").isString().exists().trim(), - validateRequest, - secretImportController.updateSecretImport -); - -router.delete( - "/:id", - requireAuth({ - acceptedAuthModes: [AuthMode.JWT,AuthMode.SERVICE_TOKEN] - }), - param("id").exists().isString().trim(), - body("secretImportPath").isString().exists().trim(), - body("secretImportEnv").isString().exists().trim(), - validateRequest, - secretImportController.deleteSecretImport -); - -router.get( - "/", - requireAuth({ - acceptedAuthModes: [AuthMode.JWT,AuthMode.SERVICE_TOKEN] - }), - requireWorkspaceAuth({ - acceptedRoles: [ADMIN, MEMBER], - locationWorkspaceId: "query" - }), - query("workspaceId").exists().isString().trim().notEmpty(), - query("environment").exists().isString().trim().notEmpty(), - query("folderId").default("root").isString().trim(), - validateRequest, - secretImportController.getSecretImports -); - -router.get( - "/secrets", - requireAuth({ - acceptedAuthModes: [AuthMode.JWT,AuthMode.SERVICE_TOKEN] - }), - requireWorkspaceAuth({ - acceptedRoles: [ADMIN, MEMBER], - locationWorkspaceId: "query" - }), - query("workspaceId").exists().isString().trim().notEmpty(), - query("environment").exists().isString().trim().notEmpty(), - query("folderId").default("root").isString().trim(), - validateRequest, - secretImportController.getAllSecretsFromImport -); - -export default router; diff --git a/backend/src/routes/v1/secretImps.ts b/backend/src/routes/v1/secretImps.ts new file mode 100644 index 000000000..1d2696d60 --- /dev/null +++ b/backend/src/routes/v1/secretImps.ts @@ -0,0 +1,47 @@ +import express from "express"; +const router = express.Router(); +import { requireAuth } from "../../middleware"; +import { secretImpsController } from "../../controllers/v1"; +import { AuthMode } from "../../variables"; + +router.post( + "/", + requireAuth({ + acceptedAuthModes: [AuthMode.JWT, AuthMode.SERVICE_TOKEN] + }), + secretImpsController.createSecretImp +); + +router.put( + "/:id", + requireAuth({ + acceptedAuthModes: [AuthMode.JWT, AuthMode.SERVICE_TOKEN] + }), + secretImpsController.updateSecretImport +); + +router.delete( + "/:id", + requireAuth({ + acceptedAuthModes: [AuthMode.JWT, AuthMode.SERVICE_TOKEN] + }), + secretImpsController.deleteSecretImport +); + +router.get( + "/", + requireAuth({ + acceptedAuthModes: [AuthMode.JWT, AuthMode.SERVICE_TOKEN] + }), + secretImpsController.getSecretImports +); + +router.get( + "/secrets", + requireAuth({ + acceptedAuthModes: [AuthMode.JWT, AuthMode.SERVICE_TOKEN] + }), + secretImpsController.getAllSecretsFromImport +); + +export default router; diff --git a/backend/src/routes/v1/secretsFolder.ts b/backend/src/routes/v1/secretsFolder.ts index 4580e50d3..00ca2acea 100644 --- a/backend/src/routes/v1/secretsFolder.ts +++ b/backend/src/routes/v1/secretsFolder.ts @@ -1,66 +1,43 @@ import express from "express"; -import { body, param, query } from "express-validator"; +const router = express.Router(); +import { requireAuth } from "../../middleware"; import { createFolder, deleteFolder, getFolders, updateFolderById } from "../../controllers/v1/secretsFolderController"; -import { requireAuth, requireWorkspaceAuth, validateRequest } from "../../middleware"; -import { ADMIN, AuthMode, MEMBER } from "../../variables"; -const router = express.Router(); +import { AuthMode } from "../../variables"; router.post( "/", requireAuth({ - acceptedAuthModes: [AuthMode.JWT,AuthMode.SERVICE_TOKEN] + acceptedAuthModes: [AuthMode.JWT, AuthMode.SERVICE_TOKEN] }), - requireWorkspaceAuth({ - acceptedRoles: [ADMIN, MEMBER], - locationWorkspaceId: "body" - }), - body("workspaceId").exists(), - body("environment").exists(), - body("folderName").exists(), - body("parentFolderId"), - validateRequest, createFolder ); router.patch( "/:folderId", requireAuth({ - acceptedAuthModes: [AuthMode.JWT,AuthMode.SERVICE_TOKEN] + acceptedAuthModes: [AuthMode.JWT, AuthMode.SERVICE_TOKEN] }), - body("workspaceId").exists(), - body("environment").exists(), - param("folderId").not().isIn(["root"]).exists(), - validateRequest, updateFolderById ); router.delete( "/:folderId", requireAuth({ - acceptedAuthModes: [AuthMode.JWT,AuthMode.SERVICE_TOKEN] + acceptedAuthModes: [AuthMode.JWT, AuthMode.SERVICE_TOKEN] }), - body("workspaceId").exists(), - body("environment").exists(), - param("folderId").not().isIn(["root"]).exists(), - validateRequest, deleteFolder ); router.get( "/", requireAuth({ - acceptedAuthModes: [AuthMode.JWT,AuthMode.SERVICE_TOKEN] + acceptedAuthModes: [AuthMode.JWT, AuthMode.SERVICE_TOKEN] }), - query("workspaceId").exists().isString().trim(), - query("environment").exists().isString().trim(), - query("parentFolderId").optional().isString().trim(), - query("parentFolderPath").optional().isString().trim(), - validateRequest, getFolders ); diff --git a/backend/src/routes/v1/signup.ts b/backend/src/routes/v1/signup.ts index 80d250b1a..f3b5d3adf 100644 --- a/backend/src/routes/v1/signup.ts +++ b/backend/src/routes/v1/signup.ts @@ -1,27 +1,21 @@ import express from "express"; const router = express.Router(); -import { body } from "express-validator"; -import { validateRequest } from "../../middleware"; import { signupController } from "../../controllers/v1"; import { authLimiter } from "../../helpers/rateLimiter"; // TODO: consider moving to users/v3/signup -router.post( // TODO endpoint: consider moving to v3/users/signup/mail - "/email/signup", - authLimiter, - body("email").exists().trim().notEmpty().isEmail(), - validateRequest, - signupController.beginEmailSignup +router.post( + // TODO endpoint: consider moving to v3/users/signup/mail + "/email/signup", + authLimiter, + signupController.beginEmailSignup ); router.post( - "/email/verify", // TODO endpoint: consider moving to v3/users/signup/verify - authLimiter, - body("email").exists().trim().notEmpty().isEmail(), - body("code").exists().trim().notEmpty(), - validateRequest, - signupController.verifyEmailSignup + "/email/verify", // TODO endpoint: consider moving to v3/users/signup/verify + authLimiter, + signupController.verifyEmailSignup ); -export default router; \ No newline at end of file +export default router; diff --git a/backend/src/routes/v1/userAction.ts b/backend/src/routes/v1/userAction.ts index 7fd26f783..762e1cde3 100644 --- a/backend/src/routes/v1/userAction.ts +++ b/backend/src/routes/v1/userAction.ts @@ -1,29 +1,25 @@ import express from "express"; const router = express.Router(); -import { requireAuth, validateRequest } from "../../middleware"; -import { body, query } from "express-validator"; +import { requireAuth } from "../../middleware"; import { userActionController } from "../../controllers/v1"; import { AuthMode } from "../../variables"; // note: [userAction] will be deprecated in /v2 in favor of [action] -router.post( // TODO endpoint: move this into /users/me - "/", - requireAuth({ - acceptedAuthModes: [AuthMode.JWT], - }), - body("action"), - validateRequest, - userActionController.addUserAction +router.post( + // TODO endpoint: move this into /users/me + "/", + requireAuth({ + acceptedAuthModes: [AuthMode.JWT] + }), + userActionController.addUserAction ); router.get( - "/", - requireAuth({ - acceptedAuthModes: [AuthMode.JWT], - }), - query("action"), - validateRequest, - userActionController.getUserAction + "/", + requireAuth({ + acceptedAuthModes: [AuthMode.JWT] + }), + userActionController.getUserAction ); export default router; diff --git a/backend/src/routes/v1/webhook.ts b/backend/src/routes/v1/webhook.ts index 16264d4ed..30c59a15b 100644 --- a/backend/src/routes/v1/webhook.ts +++ b/backend/src/routes/v1/webhook.ts @@ -1,74 +1,46 @@ import express from "express"; const router = express.Router(); -import { requireAuth, requireWorkspaceAuth, validateRequest } from "../../middleware"; -import { body, param, query } from "express-validator"; -import { ADMIN, AuthMode, MEMBER } from "../../variables"; +import { requireAuth } from "../../middleware"; +import { AuthMode } from "../../variables"; import { webhookController } from "../../controllers/v1"; router.post( "/", requireAuth({ - acceptedAuthModes: [AuthMode.JWT], + acceptedAuthModes: [AuthMode.JWT] }), - requireWorkspaceAuth({ - acceptedRoles: [ADMIN, MEMBER], - locationWorkspaceId: "body", - locationEnvironment: "body" - }), - body("workspaceId").exists().isString().trim(), - body("environment").exists().isString().trim(), - body("webhookUrl").exists().isString().isURL().trim(), - body("webhookSecretKey").isString().trim(), - body("secretPath").default("/").isString().trim(), - validateRequest, webhookController.createWebhook ); router.patch( "/:webhookId", requireAuth({ - acceptedAuthModes: [AuthMode.JWT], + acceptedAuthModes: [AuthMode.JWT] }), - param("webhookId").exists().isString().trim(), - body("isDisabled").default(false).isBoolean(), - validateRequest, webhookController.updateWebhook ); router.post( "/:webhookId/test", requireAuth({ - acceptedAuthModes: [AuthMode.JWT], + acceptedAuthModes: [AuthMode.JWT] }), - param("webhookId").exists().isString().trim(), - validateRequest, webhookController.testWebhook ); router.delete( "/:webhookId", requireAuth({ - acceptedAuthModes: [AuthMode.JWT], + acceptedAuthModes: [AuthMode.JWT] }), - param("webhookId").exists().isString().trim(), - validateRequest, webhookController.deleteWebhook ); router.get( "/", requireAuth({ - acceptedAuthModes: [AuthMode.JWT], + acceptedAuthModes: [AuthMode.JWT] }), - requireWorkspaceAuth({ - acceptedRoles: [ADMIN, MEMBER], - locationWorkspaceId: "query", - locationEnvironment: "query" - }), - query("workspaceId").exists().isString().trim(), - query("environment").optional().isString().trim(), - query("secretPath").optional().isString().trim(), - validateRequest, webhookController.listWebhooks ); diff --git a/backend/src/routes/v1/workspace.ts b/backend/src/routes/v1/workspace.ts index f08d92865..4dbc121b9 100644 --- a/backend/src/routes/v1/workspace.ts +++ b/backend/src/routes/v1/workspace.ts @@ -1,163 +1,95 @@ import express from "express"; const router = express.Router(); -import { body, param } from "express-validator"; -import { - requireAuth, - requireWorkspaceAuth, - validateRequest, -} from "../../middleware"; -import { - ADMIN, - AuthMode, - MEMBER -} from "../../variables"; +import { requireAuth } from "../../middleware"; +import { AuthMode } from "../../variables"; import { membershipController, workspaceController } from "../../controllers/v1"; router.get( - "/:workspaceId/keys", - requireAuth({ - acceptedAuthModes: [AuthMode.JWT], - }), - requireWorkspaceAuth({ - acceptedRoles: [ADMIN, MEMBER], - locationWorkspaceId: "params", - }), - param("workspaceId").exists().trim(), - validateRequest, - workspaceController.getWorkspacePublicKeys + "/:workspaceId/keys", + requireAuth({ + acceptedAuthModes: [AuthMode.JWT] + }), + workspaceController.getWorkspacePublicKeys ); router.get( - "/:workspaceId/users", - requireAuth({ - acceptedAuthModes: [AuthMode.JWT], - }), - requireWorkspaceAuth({ - acceptedRoles: [ADMIN, MEMBER], - locationWorkspaceId: "params", - }), - param("workspaceId").exists().trim(), - validateRequest, - workspaceController.getWorkspaceMemberships + "/:workspaceId/users", + requireAuth({ + acceptedAuthModes: [AuthMode.JWT] + }), + workspaceController.getWorkspaceMemberships ); router.get( - "/", - requireAuth({ - acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY], - }), - workspaceController.getWorkspaces + "/", + requireAuth({ + acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY] + }), + workspaceController.getWorkspaces ); router.get( - "/:workspaceId", - requireAuth({ - acceptedAuthModes: [AuthMode.JWT], - }), - requireWorkspaceAuth({ - acceptedRoles: [ADMIN, MEMBER], - locationWorkspaceId: "params", - }), - param("workspaceId").exists().trim(), - validateRequest, - workspaceController.getWorkspace + "/:workspaceId", + requireAuth({ + acceptedAuthModes: [AuthMode.JWT] + }), + workspaceController.getWorkspace ); router.post( - "/", - requireAuth({ - acceptedAuthModes: [AuthMode.JWT], - }), - body("workspaceName").exists().trim().notEmpty(), - body("organizationId").exists().trim().notEmpty(), - validateRequest, - workspaceController.createWorkspace + "/", + requireAuth({ + acceptedAuthModes: [AuthMode.JWT] + }), + workspaceController.createWorkspace ); router.delete( - "/:workspaceId", - requireAuth({ - acceptedAuthModes: [AuthMode.JWT], - }), - requireWorkspaceAuth({ - acceptedRoles: [ADMIN], - locationWorkspaceId: "params", - }), - param("workspaceId").exists().trim(), - validateRequest, - workspaceController.deleteWorkspace + "/:workspaceId", + requireAuth({ + acceptedAuthModes: [AuthMode.JWT] + }), + workspaceController.deleteWorkspace ); router.post( - "/:workspaceId/name", - requireAuth({ - acceptedAuthModes: [AuthMode.JWT], - }), - requireWorkspaceAuth({ - acceptedRoles: [ADMIN, MEMBER], - locationWorkspaceId: "params", - }), - param("workspaceId").exists().trim(), - body("name").exists().trim().notEmpty(), - validateRequest, - workspaceController.changeWorkspaceName + "/:workspaceId/name", + requireAuth({ + acceptedAuthModes: [AuthMode.JWT] + }), + workspaceController.changeWorkspaceName ); router.post( - "/:workspaceId/invite-signup", - requireAuth({ - acceptedAuthModes: [AuthMode.JWT], - }), - requireWorkspaceAuth({ - acceptedRoles: [ADMIN, MEMBER], - locationWorkspaceId: "params", - }), - param("workspaceId").exists().trim(), - body("email").exists().trim().notEmpty(), - validateRequest, - membershipController.inviteUserToWorkspace + "/:workspaceId/invite-signup", + requireAuth({ + acceptedAuthModes: [AuthMode.JWT] + }), + membershipController.inviteUserToWorkspace ); router.get( - "/:workspaceId/integrations", - requireAuth({ - acceptedAuthModes: [AuthMode.JWT], - }), - requireWorkspaceAuth({ - acceptedRoles: [ADMIN, MEMBER], - locationWorkspaceId: "params", - }), - param("workspaceId").exists().trim(), - validateRequest, - workspaceController.getWorkspaceIntegrations + "/:workspaceId/integrations", + requireAuth({ + acceptedAuthModes: [AuthMode.JWT] + }), + workspaceController.getWorkspaceIntegrations ); router.get( - "/:workspaceId/authorizations", - requireAuth({ - acceptedAuthModes: [AuthMode.JWT], - }), - requireWorkspaceAuth({ - acceptedRoles: [ADMIN, MEMBER], - locationWorkspaceId: "params", - }), - param("workspaceId").exists().trim(), - validateRequest, - workspaceController.getWorkspaceIntegrationAuthorizations + "/:workspaceId/authorizations", + requireAuth({ + acceptedAuthModes: [AuthMode.JWT] + }), + workspaceController.getWorkspaceIntegrationAuthorizations ); router.get( - "/:workspaceId/service-tokens", // TODO endpoint: deprecate - requireAuth({ - acceptedAuthModes: [AuthMode.JWT], - }), - requireWorkspaceAuth({ - acceptedRoles: [ADMIN, MEMBER], - locationWorkspaceId: "params", - }), - param("workspaceId").exists().trim(), - validateRequest, - workspaceController.getWorkspaceServiceTokens + "/:workspaceId/service-tokens", // TODO endpoint: deprecate + requireAuth({ + acceptedAuthModes: [AuthMode.JWT] + }), + workspaceController.getWorkspaceServiceTokens ); export default router; diff --git a/backend/src/routes/v2/auth.ts b/backend/src/routes/v2/auth.ts index e2d45f452..c7aa6b066 100644 --- a/backend/src/routes/v2/auth.ts +++ b/backend/src/routes/v2/auth.ts @@ -5,7 +5,8 @@ import { requireMfaAuth, validateRequest } from "../../middleware"; import { authController } from "../../controllers/v2"; import { authLimiter } from "../../helpers/rateLimiter"; -router.post( // TODO: deprecate (moved to api/v3/auth/login1) +router.post( + // TODO: deprecate (moved to api/v3/auth/login1) "/login1", authLimiter, body("email").isString().trim().notEmpty().toLowerCase(), @@ -14,7 +15,8 @@ router.post( // TODO: deprecate (moved to api/v3/auth/login1) authController.login1 ); -router.post( // TODO: deprecate (moved to api/v3/auth/login1) +router.post( + // TODO: deprecate (moved to api/v3/auth/login1) "/login2", authLimiter, body("email").isString().trim().notEmpty().toLowerCase(), @@ -23,22 +25,9 @@ router.post( // TODO: deprecate (moved to api/v3/auth/login1) authController.login2 ); -router.post( - "/mfa/send", - authLimiter, - body("email").isString().trim().notEmpty().isEmail(), - validateRequest, - authController.sendMfaToken -); +//remove above ones after depreciation +router.post("/mfa/send", authLimiter, authController.sendMfaToken); -router.post( - "/mfa/verify", - authLimiter, - requireMfaAuth, - body("email").isString().trim().notEmpty(), - body("mfaToken").isString().trim().notEmpty(), - validateRequest, - authController.verifyMfaToken -); +router.post("/mfa/verify", authLimiter, requireMfaAuth, authController.verifyMfaToken); -export default router; \ No newline at end of file +export default router; diff --git a/backend/src/routes/v2/environment.ts b/backend/src/routes/v2/environment.ts index a6d5baa13..9682a04a2 100644 --- a/backend/src/routes/v2/environment.ts +++ b/backend/src/routes/v2/environment.ts @@ -1,96 +1,47 @@ import express from "express"; const router = express.Router(); -import { body, param } from "express-validator"; import { environmentController } from "../../controllers/v2"; -import { - requireAuth, - requireWorkspaceAuth, - validateRequest, -} from "../../middleware"; -import { - ADMIN, - AuthMode, - MEMBER -} from "../../variables"; +import { requireAuth } from "../../middleware"; +import { AuthMode } from "../../variables"; router.post( "/:workspaceId/environments", requireAuth({ - acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY], + acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY] }), - requireWorkspaceAuth({ - acceptedRoles: [ADMIN, MEMBER], - locationWorkspaceId: "params", - }), - param("workspaceId").exists().trim(), - body("environmentSlug").exists().trim(), - body("environmentName").exists().trim(), - validateRequest, environmentController.createWorkspaceEnvironment ); router.put( "/:workspaceId/environments", requireAuth({ - acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY], + acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY] }), - requireWorkspaceAuth({ - acceptedRoles: [ADMIN, MEMBER], - locationWorkspaceId: "params", - }), - param("workspaceId").exists().trim(), - body("environmentSlug").exists().trim(), - body("environmentName").exists().trim(), - body("oldEnvironmentSlug").exists().trim(), - validateRequest, environmentController.renameWorkspaceEnvironment ); router.patch( "/:workspaceId/environments", requireAuth({ - acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY], + acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY] }), - requireWorkspaceAuth({ - acceptedRoles: [ADMIN, MEMBER], - locationWorkspaceId: "params", - }), - param("workspaceId").exists().trim(), - body("environmentSlug").exists().isString().trim(), - body("environmentName").exists().isString().trim(), - body("otherEnvironmentSlug").exists().isString().trim(), - body("otherEnvironmentName").exists().isString().trim(), - validateRequest, environmentController.reorderWorkspaceEnvironments ); router.delete( "/:workspaceId/environments", requireAuth({ - acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY], + acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY] }), - requireWorkspaceAuth({ - acceptedRoles: [ADMIN], - locationWorkspaceId: "params", - }), - param("workspaceId").exists().trim(), - body("environmentSlug").exists().trim(), - validateRequest, environmentController.deleteWorkspaceEnvironment ); router.get( "/:workspaceId/environments", requireAuth({ - acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY], + acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY] }), - requireWorkspaceAuth({ - acceptedRoles: [MEMBER, ADMIN], - locationWorkspaceId: "params", - }), - param("workspaceId").exists().trim(), - validateRequest, environmentController.getAllAccessibleEnvironmentsOfWorkspace ); -export default router; \ No newline at end of file +export default router; diff --git a/backend/src/routes/v2/organizations.ts b/backend/src/routes/v2/organizations.ts index 55f8606df..a12288886 100644 --- a/backend/src/routes/v2/organizations.ts +++ b/backend/src/routes/v2/organizations.ts @@ -1,102 +1,57 @@ import express from "express"; const router = express.Router(); import { - requireAuth, - requireMembershipOrgAuth, - requireOrganizationAuth, - validateRequest, + requireAuth, + requireOrganizationAuth } from "../../middleware"; -import { body, param } from "express-validator"; -import { - ACCEPTED, - ADMIN, - AuthMode, - MEMBER, - OWNER -} from "../../variables"; +import { ACCEPTED, ADMIN, AuthMode, OWNER } from "../../variables"; import { organizationsController } from "../../controllers/v2"; // TODO: /POST to create membership router.get( - "/:organizationId/memberships", - param("organizationId").exists().trim(), - validateRequest, - requireAuth({ - acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY], - }), - requireOrganizationAuth({ - acceptedRoles: [OWNER, ADMIN, MEMBER], - acceptedStatuses: [ACCEPTED], - }), - organizationsController.getOrganizationMemberships + "/:organizationId/memberships", + requireAuth({ + acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY] + }), + organizationsController.getOrganizationMemberships ); router.patch( - "/:organizationId/memberships/:membershipId", - param("organizationId").exists().trim(), - param("membershipId").exists().trim(), - body("role").exists().isString().trim().isIn([OWNER, ADMIN, MEMBER]), - validateRequest, - requireAuth({ - acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY], - }), - requireOrganizationAuth({ - acceptedRoles: [OWNER, ADMIN], - acceptedStatuses: [ACCEPTED], - }), - requireMembershipOrgAuth({ - acceptedRoles: [OWNER, ADMIN], - acceptedStatuses: [ACCEPTED], - }), - organizationsController.updateOrganizationMembership + "/:organizationId/memberships/:membershipId", + requireAuth({ + acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY] + }), + organizationsController.updateOrganizationMembership ); router.delete( - "/:organizationId/memberships/:membershipId", - param("organizationId").exists().trim(), - param("membershipId").exists().trim(), - validateRequest, - requireAuth({ - acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY], - }), - requireOrganizationAuth({ - acceptedRoles: [OWNER, ADMIN], - acceptedStatuses: [ACCEPTED], - }), - requireMembershipOrgAuth({ - acceptedRoles: [OWNER, ADMIN], - acceptedStatuses: [ACCEPTED], - }), - organizationsController.deleteOrganizationMembership + "/:organizationId/memberships/:membershipId", + requireAuth({ + acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY] + }), + organizationsController.deleteOrganizationMembership ); router.get( - "/:organizationId/workspaces", - param("organizationId").exists().trim(), - validateRequest, - requireAuth({ - acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY], - }), - requireOrganizationAuth({ - acceptedRoles: [OWNER, ADMIN], - acceptedStatuses: [ACCEPTED], - }), - organizationsController.getOrganizationWorkspaces + "/:organizationId/workspaces", + requireAuth({ + acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY] + }), + organizationsController.getOrganizationWorkspaces ); -router.get( // TODO endpoint: deprecate service accounts - "/:organizationId/service-accounts", - param("organizationId").exists().trim(), - validateRequest, - requireAuth({ - acceptedAuthModes: [AuthMode.JWT] - }), - requireOrganizationAuth({ - acceptedRoles: [OWNER, ADMIN], - acceptedStatuses: [ACCEPTED], - }), - organizationsController.getOrganizationServiceAccounts +router.get( + // TODO endpoint: deprecate service accounts + "/:organizationId/service-accounts", + requireAuth({ + acceptedAuthModes: [AuthMode.JWT] + }), + requireOrganizationAuth({ + acceptedRoles: [OWNER, ADMIN], + acceptedStatuses: [ACCEPTED] + }), + organizationsController.getOrganizationServiceAccounts ); -export default router; \ No newline at end of file +export default router; diff --git a/backend/src/routes/v2/secrets.ts b/backend/src/routes/v2/secrets.ts index cb60035a7..c175335ff 100644 --- a/backend/src/routes/v2/secrets.ts +++ b/backend/src/routes/v2/secrets.ts @@ -1,14 +1,12 @@ import express from "express"; const router = express.Router(); -import { Types } from "mongoose"; import { requireAuth, requireSecretsAuth, requireWorkspaceAuth, validateRequest } from "../../middleware"; -import { validateClientForSecrets } from "../../validation"; -import { body, query } from "express-validator"; +import { body } from "express-validator"; import { secretsController } from "../../controllers/v2"; import { ADMIN, @@ -19,44 +17,18 @@ import { SECRET_PERSONAL, SECRET_SHARED } from "../../variables"; -import { BatchSecretRequest } from "../../types/secret"; -router.post( // TODO endpoint: strongly consider deprecation in favor of a single operation experience on dashboard +router.post( + // TODO endpoint: strongly consider deprecation in favor of a single operation experience on dashboard "/batch", requireAuth({ acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN] }), - requireWorkspaceAuth({ - acceptedRoles: [ADMIN, MEMBER], - locationWorkspaceId: "body" - }), - body("workspaceId").exists().isString().trim(), - body("folderId").default("root").isString().trim(), - body("environment").exists().isString().trim(), - body("secretPath").optional().isString().trim(), - body("requests") - .exists() - .custom(async (requests: BatchSecretRequest[], { req }) => { - if (Array.isArray(requests)) { - const secretIds = requests - .map((request) => request.secret._id) - .filter((secretId) => secretId !== undefined); - - if (secretIds.length > 0) { - req.secrets = await validateClientForSecrets({ - authData: req.authData, - secretIds: secretIds.map((secretId: string) => new Types.ObjectId(secretId)), - requiredPermissions: [] - }); - } - } - return true; - }), - validateRequest, secretsController.batchSecrets ); -router.post( // TODO endpoint: deprecate (moved to POST api/v3/secrets) +router.post( + // TODO endpoint: deprecate (moved to POST api/v3/secrets) "/", body("workspaceId").exists().isString().trim(), body("environment").exists().isString().trim(), @@ -117,15 +89,9 @@ router.post( // TODO endpoint: deprecate (moved to POST api/v3/secrets) secretsController.createSecrets ); -router.get( // TODO endpoint: deprecate (moved to GET api/v3/secrets) +router.get( + // TODO endpoint: deprecate (moved to GET api/v3/secrets) "/", - query("workspaceId").exists().trim(), - query("environment").exists().trim(), - query("tagSlugs"), - query("folderId").default("root").isString().trim(), - query("secretPath").optional().isString().trim(), - query("include_imports").optional().default(false).isBoolean(), - validateRequest, requireAuth({ acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN] }), @@ -138,7 +104,8 @@ router.get( // TODO endpoint: deprecate (moved to GET api/v3/secrets) secretsController.getSecrets ); -router.patch( // TODO endpoint: deprecate (moved to PATCH api/v3/secrets) +router.patch( + // TODO endpoint: deprecate (moved to PATCH api/v3/secrets) "/", body("secrets") .exists() @@ -173,7 +140,8 @@ router.patch( // TODO endpoint: deprecate (moved to PATCH api/v3/secrets) secretsController.updateSecrets ); -router.delete( // TODO endpoint: deprecate (moved to DELETE api/v3/secrets) +router.delete( + // TODO endpoint: deprecate (moved to DELETE api/v3/secrets) "/", body("secretIds") .exists() diff --git a/backend/src/routes/v2/serviceTokenData.ts b/backend/src/routes/v2/serviceTokenData.ts index efdc905fe..611cf6003 100644 --- a/backend/src/routes/v2/serviceTokenData.ts +++ b/backend/src/routes/v2/serviceTokenData.ts @@ -1,18 +1,9 @@ import express from "express"; const router = express.Router(); import { - requireAuth, - requireServiceTokenDataAuth, - requireWorkspaceAuth, - validateRequest + requireAuth } from "../../middleware"; -import { body, param } from "express-validator"; -import { - ADMIN, - AuthMode, - MEMBER, - PERMISSION_WRITE_SECRETS -} from "../../variables"; +import { AuthMode } from "../../variables"; import { serviceTokenDataController } from "../../controllers/v2"; router.get( @@ -28,33 +19,6 @@ router.post( requireAuth({ acceptedAuthModes: [AuthMode.JWT] }), - requireWorkspaceAuth({ - acceptedRoles: [ADMIN, MEMBER], - locationWorkspaceId: "body", - locationEnvironment: "body", - requiredPermissions: [PERMISSION_WRITE_SECRETS] - }), - body("name").exists().isString().trim(), - body("workspaceId").exists().isString().trim(), - body("scopes").exists().isArray(), - body("scopes.*.environment").exists().isString().trim(), - body("scopes.*.secretPath").exists().isString().trim(), - body("encryptedKey").exists().isString().trim(), - body("iv").exists().isString().trim(), - body("tag").exists().isString().trim(), - body("expiresIn").exists().isNumeric(), // measured in ms - body("permissions") - .isArray({ min: 1 }) - .custom((value: string[]) => { - const allowedPermissions = ["read", "write"]; - const invalidValues = value.filter((v) => !allowedPermissions.includes(v)); - if (invalidValues.length > 0) { - throw new Error(`permissions contains invalid values: ${invalidValues.join(", ")}`); - } - - return true; - }), - validateRequest, serviceTokenDataController.createServiceTokenData ); @@ -63,11 +27,6 @@ router.delete( requireAuth({ acceptedAuthModes: [AuthMode.JWT] }), - requireServiceTokenDataAuth({ - acceptedRoles: [ADMIN, MEMBER] - }), - param("serviceTokenDataId").exists().trim(), - validateRequest, serviceTokenDataController.deleteServiceTokenData ); diff --git a/backend/src/routes/v2/tags.ts b/backend/src/routes/v2/tags.ts index aca1b6d8c..7926e9452 100644 --- a/backend/src/routes/v2/tags.ts +++ b/backend/src/routes/v2/tags.ts @@ -1,56 +1,30 @@ import express from "express"; const router = express.Router(); -import { body, param } from "express-validator"; import { tagController } from "../../controllers/v2"; -import { - requireAuth, - requireWorkspaceAuth, - validateRequest, -} from "../../middleware"; -import { - ADMIN, - AuthMode, - MEMBER -} from "../../variables"; +import { requireAuth } from "../../middleware"; +import { AuthMode } from "../../variables"; router.get( "/:workspaceId/tags", requireAuth({ - acceptedAuthModes: [AuthMode.JWT], + acceptedAuthModes: [AuthMode.JWT] }), - requireWorkspaceAuth({ - acceptedRoles: [MEMBER, ADMIN], - locationWorkspaceId: "params", - }), - param("workspaceId").exists().trim(), - validateRequest, tagController.getWorkspaceTags ); router.delete( "/tags/:tagId", requireAuth({ - acceptedAuthModes: [AuthMode.JWT], + acceptedAuthModes: [AuthMode.JWT] }), - param("tagId").exists().trim(), - validateRequest, tagController.deleteWorkspaceTag ); router.post( "/:workspaceId/tags", requireAuth({ - acceptedAuthModes: [AuthMode.JWT], + acceptedAuthModes: [AuthMode.JWT] }), - requireWorkspaceAuth({ - acceptedRoles: [MEMBER, ADMIN], - locationWorkspaceId: "params", - }), - param("workspaceId").exists().trim(), - body("name").exists().trim(), - body("tagColor").exists().trim(), - body("slug").exists().trim(), - validateRequest, tagController.createWorkspaceTag ); diff --git a/backend/src/routes/v2/users.ts b/backend/src/routes/v2/users.ts index c13abfcb9..c093be21c 100644 --- a/backend/src/routes/v2/users.ts +++ b/backend/src/routes/v2/users.ts @@ -1,114 +1,87 @@ import express from "express"; const router = express.Router(); -import { - requireAuth, - validateRequest, -} from "../../middleware"; -import { body, param } from "express-validator"; +import { requireAuth } from "../../middleware"; import { usersController } from "../../controllers/v2"; import { AuthMode } from "../../variables"; -import { - AuthMethod -} from "../../models"; router.get( - "/me", - requireAuth({ - acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY], - }), - usersController.getMe + "/me", + requireAuth({ + acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY] + }), + usersController.getMe ); router.patch( - "/me/mfa", - requireAuth({ - acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY], - }), - body("isMfaEnabled").exists().isBoolean(), - validateRequest, - usersController.updateMyMfaEnabled + "/me/mfa", + requireAuth({ + acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY] + }), + usersController.updateMyMfaEnabled ); router.patch( - "/me/name", - requireAuth({ - acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY], - }), - body("firstName").exists().isString(), - body("lastName").isString(), - validateRequest, - usersController.updateName + "/me/name", + requireAuth({ + acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY] + }), + usersController.updateName ); router.put( - "/me/auth-methods", - requireAuth({ - acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY], - }), - body("authMethods").exists().isArray({ - min: 1, - }).custom((authMethods: AuthMethod[]) => { - return authMethods.every(provider => [ - AuthMethod.EMAIL, - AuthMethod.GOOGLE, - AuthMethod.GITHUB - ].includes(provider)) - }), - validateRequest, - usersController.updateAuthMethods, + "/me/auth-methods", + requireAuth({ + acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY] + }), + usersController.updateAuthMethods ); router.get( - "/me/organizations", - requireAuth({ - acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY], - }), - usersController.getMyOrganizations + "/me/organizations", + requireAuth({ + acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY] + }), + usersController.getMyOrganizations ); router.get( - "/me/api-keys", - requireAuth({ - acceptedAuthModes: [AuthMode.JWT] - }), - usersController.getMyAPIKeys + "/me/api-keys", + requireAuth({ + acceptedAuthModes: [AuthMode.JWT] + }), + usersController.getMyAPIKeys ); router.post( - "/me/api-keys", - requireAuth({ - acceptedAuthModes: [AuthMode.JWT] - }), - body("name").exists().isString().trim(), - body("expiresIn").isNumeric(), - validateRequest, - usersController.createAPIKey + "/me/api-keys", + requireAuth({ + acceptedAuthModes: [AuthMode.JWT] + }), + usersController.createAPIKey ); router.delete( - "/me/api-keys/:apiKeyDataId", - requireAuth({ - acceptedAuthModes: [AuthMode.JWT] - }), - param("apiKeyDataId").exists().trim(), - validateRequest, - usersController.deleteAPIKey + "/me/api-keys/:apiKeyDataId", + requireAuth({ + acceptedAuthModes: [AuthMode.JWT] + }), + usersController.deleteAPIKey ); router.get( - "/me/sessions", - requireAuth({ - acceptedAuthModes: [AuthMode.JWT] - }), - usersController.getMySessions + "/me/sessions", + requireAuth({ + acceptedAuthModes: [AuthMode.JWT] + }), + usersController.getMySessions ); router.delete( - "/me/sessions", - requireAuth({ - acceptedAuthModes: [AuthMode.JWT] - }), - usersController.deleteMySessions + "/me/sessions", + requireAuth({ + acceptedAuthModes: [AuthMode.JWT] + }), + usersController.deleteMySessions ); -export default router; \ No newline at end of file +export default router; diff --git a/backend/src/routes/v2/workspace.ts b/backend/src/routes/v2/workspace.ts index fde41d945..304f69b34 100644 --- a/backend/src/routes/v2/workspace.ts +++ b/backend/src/routes/v2/workspace.ts @@ -1,147 +1,96 @@ import express from "express"; const router = express.Router(); import { body, param, query } from "express-validator"; -import { - requireAuth, - requireMembershipAuth, - requireWorkspaceAuth, - validateRequest, -} from "../../middleware"; -import { - ADMIN, - AuthMode, - MEMBER -} from "../../variables"; +import { requireAuth, requireWorkspaceAuth, validateRequest } from "../../middleware"; +import { ADMIN, AuthMode, MEMBER } from "../../variables"; import { workspaceController } from "../../controllers/v2"; -router.post( // TODO endpoint: deprecate (moved to POST v3/secrets) - "/:workspaceId/secrets", - requireAuth({ - acceptedAuthModes: [AuthMode.JWT], - }), - requireWorkspaceAuth({ - acceptedRoles: [ADMIN, MEMBER], - locationWorkspaceId: "params", - }), - body("secrets").exists(), - body("keys").exists(), - body("environment").exists().trim().notEmpty(), - body("channel"), - param("workspaceId").exists().trim(), - validateRequest, - workspaceController.pushWorkspaceSecrets -); - -router.get( // TODO endpoint: deprecate (moved to GET v3/secrets) - "/:workspaceId/secrets", - requireAuth({ - acceptedAuthModes: [AuthMode.JWT, AuthMode.SERVICE_TOKEN], - }), - requireWorkspaceAuth({ - acceptedRoles: [ADMIN, MEMBER], - locationWorkspaceId: "params", - }), - query("environment").exists().trim(), - query("channel"), - param("workspaceId").exists().trim(), - validateRequest, - workspaceController.pullSecrets -); - -router.get( // TODO endpoint: consider moving to v3/users/me/workspaces/:workspaceId/key - "/:workspaceId/encrypted-key", - requireAuth({ - acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY], - }), - requireWorkspaceAuth({ - acceptedRoles: [ADMIN, MEMBER], - locationWorkspaceId: "params", - }), - param("workspaceId").exists().trim(), - validateRequest, - workspaceController.getWorkspaceKey +router.post( + // TODO endpoint: deprecate (moved to POST v3/secrets) + "/:workspaceId/secrets", + requireAuth({ + acceptedAuthModes: [AuthMode.JWT] + }), + requireWorkspaceAuth({ + acceptedRoles: [ADMIN, MEMBER], + locationWorkspaceId: "params" + }), + body("secrets").exists(), + body("keys").exists(), + body("environment").exists().trim().notEmpty(), + body("channel"), + param("workspaceId").exists().trim(), + validateRequest, + workspaceController.pushWorkspaceSecrets ); router.get( - "/:workspaceId/service-token-data", - requireAuth({ - acceptedAuthModes: [AuthMode.JWT], - }), - requireWorkspaceAuth({ - acceptedRoles: [ADMIN, MEMBER], - locationWorkspaceId: "params", - }), - param("workspaceId").exists().trim(), - validateRequest, - workspaceController.getWorkspaceServiceTokenData + // TODO endpoint: deprecate (moved to GET v3/secrets) + "/:workspaceId/secrets", + requireAuth({ + acceptedAuthModes: [AuthMode.JWT, AuthMode.SERVICE_TOKEN] + }), + requireWorkspaceAuth({ + acceptedRoles: [ADMIN, MEMBER], + locationWorkspaceId: "params" + }), + query("environment").exists().trim(), + query("channel"), + param("workspaceId").exists().trim(), + validateRequest, + workspaceController.pullSecrets ); -router.get( // new - TODO: rewire dashboard to this route - "/:workspaceId/memberships", - param("workspaceId").exists().trim(), - validateRequest, - requireAuth({ - acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY], - }), - requireWorkspaceAuth({ - acceptedRoles: [ADMIN, MEMBER], - locationWorkspaceId: "params", - }), - workspaceController.getWorkspaceMemberships +router.get( + // TODO endpoint: consider moving to v3/users/me/workspaces/:workspaceId/key + "/:workspaceId/encrypted-key", + requireAuth({ + acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY] + }), + workspaceController.getWorkspaceKey ); -router.patch( // TODO - rewire dashboard to this route - "/:workspaceId/memberships/:membershipId", - param("workspaceId").exists().trim(), - param("membershipId").exists().trim(), - body("role").exists().isString().trim().isIn([ADMIN, MEMBER]), - validateRequest, - requireAuth({ - acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY], - }), - requireWorkspaceAuth({ - acceptedRoles: [ADMIN], - locationWorkspaceId: "params", - }), - requireMembershipAuth({ - acceptedRoles: [ADMIN], - locationMembershipId: "params", - }), - workspaceController.updateWorkspaceMembership +router.get( + "/:workspaceId/service-token-data", + requireAuth({ + acceptedAuthModes: [AuthMode.JWT] + }), + workspaceController.getWorkspaceServiceTokenData ); -router.delete( // TODO - rewire dashboard to this route - "/:workspaceId/memberships/:membershipId", - param("workspaceId").exists().trim(), - param("membershipId").exists().trim(), - validateRequest, - requireAuth({ - acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY], - }), - requireWorkspaceAuth({ - acceptedRoles: [ADMIN], - locationWorkspaceId: "params", - }), - requireMembershipAuth({ - acceptedRoles: [ADMIN], - locationMembershipId: "params", - }), - workspaceController.deleteWorkspaceMembership +router.get( + // new - TODO: rewire dashboard to this route + "/:workspaceId/memberships", + requireAuth({ + acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY] + }), + workspaceController.getWorkspaceMemberships ); router.patch( - "/:workspaceId/auto-capitalization", - requireAuth({ - acceptedAuthModes: [AuthMode.JWT] - }), - requireWorkspaceAuth({ - acceptedRoles: [ADMIN, MEMBER], - locationWorkspaceId: "params", - }), - param("workspaceId").exists().trim(), - body("autoCapitalization").exists().trim().notEmpty(), - validateRequest, - workspaceController.toggleAutoCapitalization + // TODO - rewire dashboard to this route + "/:workspaceId/memberships/:membershipId", + requireAuth({ + acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY] + }), + workspaceController.updateWorkspaceMembership +); + +router.delete( + // TODO - rewire dashboard to this route + "/:workspaceId/memberships/:membershipId", + requireAuth({ + acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY] + }), + workspaceController.deleteWorkspaceMembership +); + +router.patch( + "/:workspaceId/auto-capitalization", + requireAuth({ + acceptedAuthModes: [AuthMode.JWT] + }), + workspaceController.toggleAutoCapitalization ); export default router; diff --git a/backend/src/routes/v3/auth.ts b/backend/src/routes/v3/auth.ts index ba6f30335..44fdaef4b 100644 --- a/backend/src/routes/v3/auth.ts +++ b/backend/src/routes/v3/auth.ts @@ -1,29 +1,11 @@ import express from "express"; -import { body } from "express-validator"; -import { validateRequest } from "../../middleware"; import { authController } from "../../controllers/v3"; import { authLimiter } from "../../helpers/rateLimiter"; const router = express.Router(); -router.post( - "/login1", - authLimiter, - body("email").isString().trim().toLowerCase(), - body("providerAuthToken").isString().trim().optional({nullable: true}), - body("clientPublicKey").isString().trim().notEmpty(), - validateRequest, - authController.login1 -); +router.post("/login1", authLimiter, authController.login1); -router.post( - "/login2", - authLimiter, - body("email").isString().trim().toLowerCase(), - body("providerAuthToken").isString().trim().optional({nullable: true}), - body("clientProof").isString().trim().notEmpty(), - validateRequest, - authController.login2 -); +router.post("/login2", authLimiter, authController.login2); export default router; diff --git a/backend/src/routes/v3/secrets.ts b/backend/src/routes/v3/secrets.ts index 648fc57f5..1765c2e33 100644 --- a/backend/src/routes/v3/secrets.ts +++ b/backend/src/routes/v3/secrets.ts @@ -1,288 +1,130 @@ import express from "express"; const router = express.Router(); -import { requireAuth, requireWorkspaceAuth, validateRequest } from "../../middleware"; -import { body, param, query } from "express-validator"; +import { + requireAuth, + requireBlindIndicesEnabled, + requireE2EEOff +} from "../../middleware"; import { secretsController } from "../../controllers/v3"; import { - ADMIN, - AuthMode, - MEMBER, - PERMISSION_READ_SECRETS, - PERMISSION_WRITE_SECRETS, - SECRET_PERSONAL, - SECRET_SHARED + AuthMode } from "../../variables"; router.get( "/raw", - query("workspaceId").optional().isString().trim(), - query("environment").optional().isString().trim(), - query("folderId").optional().isString().trim(), - query("secretPath").default("/").isString().trim(), - query("include_imports").optional().isBoolean().default(false), - validateRequest, requireAuth({ - acceptedAuthModes: [ - AuthMode.JWT, - AuthMode.API_KEY, - AuthMode.SERVICE_TOKEN - ] + acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN] }), secretsController.getSecretsRaw ); router.get( "/raw/:secretName", - param("secretName").exists().isString().trim(), - query("workspaceId").exists().isString().trim(), - query("environment").exists().isString().trim(), - query("secretPath").default("/").isString().trim(), - query("type").optional().isIn([SECRET_SHARED, SECRET_PERSONAL]), - validateRequest, requireAuth({ - acceptedAuthModes: [ - AuthMode.JWT, - AuthMode.API_KEY, - AuthMode.SERVICE_TOKEN - ] + acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN] }), - requireWorkspaceAuth({ - acceptedRoles: [ADMIN, MEMBER], - locationWorkspaceId: "query", - locationEnvironment: "query", - requiredPermissions: [PERMISSION_READ_SECRETS], - requireBlindIndicesEnabled: true, - requireE2EEOff: true, - checkIPAllowlist: false + requireBlindIndicesEnabled({ + locationWorkspaceId: "query" + }), + requireE2EEOff({ + locationWorkspaceId: "query" }), secretsController.getSecretByNameRaw ); router.post( "/raw/:secretName", - body("workspaceId").exists().isString().trim(), - body("environment").exists().isString().trim(), - body("type").exists().isIn([SECRET_SHARED, SECRET_PERSONAL]), - body("secretValue").exists().isString().trim(), - body("secretComment").default("").isString().trim(), - body("secretPath").default("/").isString().trim(), - validateRequest, requireAuth({ - acceptedAuthModes: [ - AuthMode.JWT, - AuthMode.API_KEY, - AuthMode.SERVICE_TOKEN - ] + acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN] }), - requireWorkspaceAuth({ - acceptedRoles: [ADMIN, MEMBER], - locationWorkspaceId: "body", - locationEnvironment: "body", - requiredPermissions: [PERMISSION_WRITE_SECRETS], - requireBlindIndicesEnabled: true, - requireE2EEOff: true, - checkIPAllowlist: false + requireBlindIndicesEnabled({ + locationWorkspaceId: "body" + }), + requireE2EEOff({ + locationWorkspaceId: "body" }), secretsController.createSecretRaw ); router.patch( "/raw/:secretName", - param("secretName").exists().isString().trim(), - body("workspaceId").exists().isString().trim(), - body("environment").exists().isString().trim(), - body("type").exists().isIn([SECRET_SHARED, SECRET_PERSONAL]), - body("secretValue").exists().isString().trim(), - body("secretPath").default("/").isString().trim(), - validateRequest, requireAuth({ - acceptedAuthModes: [ - AuthMode.JWT, - AuthMode.API_KEY, - AuthMode.SERVICE_TOKEN - ] + acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN] }), - requireWorkspaceAuth({ - acceptedRoles: [ADMIN, MEMBER], - locationWorkspaceId: "body", - locationEnvironment: "body", - requiredPermissions: [PERMISSION_WRITE_SECRETS], - requireBlindIndicesEnabled: true, - requireE2EEOff: true, - checkIPAllowlist: false + requireBlindIndicesEnabled({ + locationWorkspaceId: "body" + }), + requireE2EEOff({ + locationWorkspaceId: "body" }), secretsController.updateSecretByNameRaw ); router.delete( "/raw/:secretName", - param("secretName").exists().isString().trim(), - body("workspaceId").exists().isString().trim(), - body("environment").exists().isString().trim(), - body("secretPath").default("/").isString().trim(), - body("type").exists().isIn([SECRET_SHARED, SECRET_PERSONAL]), - validateRequest, requireAuth({ - acceptedAuthModes: [ - AuthMode.JWT, - AuthMode.API_KEY, - AuthMode.SERVICE_TOKEN - ] + acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN] }), - requireWorkspaceAuth({ - acceptedRoles: [ADMIN, MEMBER], - locationWorkspaceId: "body", - locationEnvironment: "body", - requiredPermissions: [PERMISSION_WRITE_SECRETS], - requireBlindIndicesEnabled: true, - requireE2EEOff: true, - checkIPAllowlist: false + requireBlindIndicesEnabled({ + locationWorkspaceId: "body" + }), + requireE2EEOff({ + locationWorkspaceId: "body" }), secretsController.deleteSecretByNameRaw ); router.get( "/", - query("workspaceId").exists().isString().trim(), - query("environment").exists().isString().trim(), - query("folderId").optional().isString().trim(), - query("secretPath").default("/").isString().trim(), - validateRequest, requireAuth({ - acceptedAuthModes: [ - AuthMode.JWT, - AuthMode.API_KEY, - AuthMode.SERVICE_TOKEN - ] + acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN] }), - requireWorkspaceAuth({ - acceptedRoles: [ADMIN, MEMBER], - locationWorkspaceId: "query", - locationEnvironment: "query", - requiredPermissions: [PERMISSION_READ_SECRETS], - requireBlindIndicesEnabled: true, - requireE2EEOff: false, - checkIPAllowlist: false + requireBlindIndicesEnabled({ + locationWorkspaceId: "query" }), secretsController.getSecrets ); router.post( "/:secretName", - body("workspaceId").exists().isString().trim(), - body("environment").exists().isString().trim(), - body("type").exists().isIn([SECRET_SHARED, SECRET_PERSONAL]), - body("secretKeyCiphertext").exists().isString().trim(), - body("secretKeyIV").exists().isString().trim(), - body("secretKeyTag").exists().isString().trim(), - body("secretValueCiphertext").exists().isString().trim(), - body("secretValueIV").exists().isString().trim(), - body("secretValueTag").exists().isString().trim(), - body("secretCommentCiphertext").optional().isString().trim(), - body("secretCommentIV").optional().isString().trim(), - body("secretCommentTag").optional().isString().trim(), - body("secretPath").default("/").isString().trim(), - body("metadata").optional().isObject().withMessage("Metadata should be an object"), - body("metadata.source").optional().isString().withMessage("Source should be a string"), - validateRequest, requireAuth({ - acceptedAuthModes: [ - AuthMode.JWT, - AuthMode.API_KEY, - AuthMode.SERVICE_TOKEN - ] + acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN] }), - requireWorkspaceAuth({ - acceptedRoles: [ADMIN, MEMBER], - locationWorkspaceId: "body", - locationEnvironment: "body", - requiredPermissions: [PERMISSION_WRITE_SECRETS], - requireBlindIndicesEnabled: true, - requireE2EEOff: false, - checkIPAllowlist: false + requireBlindIndicesEnabled({ + locationWorkspaceId: "body" }), secretsController.createSecret ); router.get( "/:secretName", - param("secretName").exists().isString().trim(), - query("workspaceId").exists().isString().trim(), - query("environment").exists().isString().trim(), - query("secretPath").default("/").isString().trim(), - query("type").optional().isIn([SECRET_SHARED, SECRET_PERSONAL]), - validateRequest, requireAuth({ - acceptedAuthModes: [ - AuthMode.JWT, - AuthMode.API_KEY, - AuthMode.SERVICE_TOKEN - ] + acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN] }), - requireWorkspaceAuth({ - acceptedRoles: [ADMIN, MEMBER], - locationWorkspaceId: "query", - locationEnvironment: "query", - requiredPermissions: [PERMISSION_READ_SECRETS], - requireBlindIndicesEnabled: true, - checkIPAllowlist: false + requireBlindIndicesEnabled({ + locationWorkspaceId: "query" }), secretsController.getSecretByName ); router.patch( "/:secretName", - param("secretName").exists().isString().trim(), - body("workspaceId").exists().isString().trim(), - body("environment").exists().isString().trim(), - body("type").exists().isIn([SECRET_SHARED, SECRET_PERSONAL]), - body("secretValueCiphertext").exists().isString().trim(), - body("secretValueIV").exists().isString().trim(), - body("secretValueTag").exists().isString().trim(), - body("secretPath").default("/").isString().trim(), - validateRequest, requireAuth({ - acceptedAuthModes: [ - AuthMode.JWT, - AuthMode.API_KEY, - AuthMode.SERVICE_TOKEN - ] + acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN] }), - requireWorkspaceAuth({ - acceptedRoles: [ADMIN, MEMBER], - locationWorkspaceId: "body", - locationEnvironment: "body", - requiredPermissions: [PERMISSION_WRITE_SECRETS], - requireBlindIndicesEnabled: true, - requireE2EEOff: false, - checkIPAllowlist: false + requireBlindIndicesEnabled({ + locationWorkspaceId: "body" }), secretsController.updateSecretByName ); router.delete( "/:secretName", - param("secretName").exists().isString().trim(), - body("workspaceId").exists().isString().trim(), - body("environment").exists().isString().trim(), - body("secretPath").default("/").isString().trim(), - body("type").exists().isIn([SECRET_SHARED, SECRET_PERSONAL]), - validateRequest, requireAuth({ - acceptedAuthModes: [ - AuthMode.JWT, - AuthMode.API_KEY, - AuthMode.SERVICE_TOKEN - ] + acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN] }), - requireWorkspaceAuth({ - acceptedRoles: [ADMIN, MEMBER], - locationWorkspaceId: "body", - locationEnvironment: "body", - requiredPermissions: [PERMISSION_WRITE_SECRETS], - requireBlindIndicesEnabled: true, - requireE2EEOff: false, - checkIPAllowlist: false + requireBlindIndicesEnabled({ + locationWorkspaceId: "body" }), secretsController.deleteSecretByName ); diff --git a/backend/src/routes/v3/signup.ts b/backend/src/routes/v3/signup.ts index bfd0c9c4d..241f5d04c 100644 --- a/backend/src/routes/v3/signup.ts +++ b/backend/src/routes/v3/signup.ts @@ -1,30 +1,14 @@ import express from "express"; const router = express.Router(); -import { body } from "express-validator"; import { signupController } from "../../controllers/v3"; import { authLimiter } from "../../helpers/rateLimiter"; import { validateRequest } from "../../middleware"; router.post( - "/complete-account/signup", // TODO: consider moving endpoint to v3/users/new/complete-account/signup - authLimiter, - body("email").exists().isString().trim().notEmpty().isEmail(), - body("firstName").exists().isString().trim().notEmpty(), - body("lastName").exists().isString().trim().optional({nullable: true}), - body("protectedKey").exists().isString().trim().notEmpty(), - body("protectedKeyIV").exists().isString().trim().notEmpty(), - body("protectedKeyTag").exists().isString().trim().notEmpty(), - body("publicKey").exists().isString().trim().notEmpty(), - body("encryptedPrivateKey").exists().isString().trim().notEmpty(), - body("encryptedPrivateKeyIV").exists().isString().trim().notEmpty(), - body("encryptedPrivateKeyTag").exists().isString().trim().notEmpty(), - body("salt").exists().isString().trim().notEmpty(), - body("verifier").exists().isString().trim().notEmpty(), - body("organizationName").exists().isString().trim().notEmpty(), - body("providerAuthToken").isString().trim().optional({ nullable: true }), - body("attributionSource").optional().isString().trim(), - validateRequest, - signupController.completeAccountSignup, + "/complete-account/signup", // TODO: consider moving endpoint to v3/users/new/complete-account/signup + authLimiter, + validateRequest, + signupController.completeAccountSignup ); export default router; diff --git a/backend/src/routes/v3/workspaces.ts b/backend/src/routes/v3/workspaces.ts index 38695e8b9..834d54cd1 100644 --- a/backend/src/routes/v3/workspaces.ts +++ b/backend/src/routes/v3/workspaces.ts @@ -1,79 +1,37 @@ import express from "express"; const router = express.Router(); -import { - requireAuth, - requireWorkspaceAuth, - validateRequest, -} from "../../middleware"; +import { requireAuth } from "../../middleware"; import { workspacesController } from "../../controllers/v3"; -import { - ADMIN, - AuthMode -} from "../../variables"; -import { body, param } from "express-validator"; +import { AuthMode } from "../../variables"; // -- migration to blind indices endpoints router.get( - "/:workspaceId/secrets/blind-index-status", - param("workspaceId").exists().isString().trim(), - validateRequest, - requireAuth({ - acceptedAuthModes: [AuthMode.JWT], - }), - requireWorkspaceAuth({ - acceptedRoles: [ADMIN], - locationWorkspaceId: "params", - }), - workspacesController.getWorkspaceBlindIndexStatus + "/:workspaceId/secrets/blind-index-status", + requireAuth({ + acceptedAuthModes: [AuthMode.JWT] + }), + workspacesController.getWorkspaceBlindIndexStatus ); -router.get( // allow admins to get all workspace secrets (part of blind indices migration) - "/:workspaceId/secrets", - param("workspaceId").exists().isString().trim(), - validateRequest, - requireAuth({ - acceptedAuthModes: [AuthMode.JWT], - }), - requireWorkspaceAuth({ - acceptedRoles: [ADMIN], - locationWorkspaceId: "params", - }), - workspacesController.getWorkspaceSecrets +router.get( + // allow admins to get all workspace secrets (part of blind indices migration) + "/:workspaceId/secrets", + requireAuth({ + acceptedAuthModes: [AuthMode.JWT] + }), + workspacesController.getWorkspaceSecrets ); -router.post( // allow admins to name all workspace secrets (part of blind indices migration) - "/:workspaceId/secrets/names", - param("workspaceId").exists().isString().trim(), - body("secretsToUpdate") - .exists() - .isArray() - .withMessage("secretsToUpdate must be an array") - .customSanitizer((value) => { - return value.map((secret: any) => ({ - secretName: secret.secretName, - _id: secret._id, - })); - }), - body("secretsToUpdate.*.secretName") - .exists() - .isString() - .withMessage("secretName must be a string"), - body("secretsToUpdate.*._id") - .exists() - .isString() - .withMessage("secretId must be a string"), - validateRequest, - requireAuth({ - acceptedAuthModes: [AuthMode.JWT], - }), - requireWorkspaceAuth({ - acceptedRoles: [ADMIN], - locationWorkspaceId: "params", - }), - workspacesController.nameWorkspaceSecrets +router.post( + // allow admins to name all workspace secrets (part of blind indices migration) + "/:workspaceId/secrets/names", + requireAuth({ + acceptedAuthModes: [AuthMode.JWT] + }), + workspacesController.nameWorkspaceSecrets ); // -- -export default router; \ No newline at end of file +export default router; diff --git a/backend/src/types/secret/index.d.ts b/backend/src/types/secret/index.d.ts index f60fdfd4c..05016b38e 100644 --- a/backend/src/types/secret/index.d.ts +++ b/backend/src/types/secret/index.d.ts @@ -28,7 +28,13 @@ export interface BatchSecretRequest { } export interface BatchSecret { - _id: string; + version?: number; + _id?: string; + user?: string; + environment: string; + workspace?: string; + algorithm?: string; + keyEncoding?: string; type: "shared" | "personal"; secretName: string; secretBlindIndex: string; @@ -42,5 +48,5 @@ export interface BatchSecret { secretCommentIV: string; secretCommentTag: string; tags: string[]; - folder: string + folder: string; } diff --git a/backend/src/utils/setup/backfillData.ts b/backend/src/utils/setup/backfillData.ts index 9c301722a..ae6279bfc 100644 --- a/backend/src/utils/setup/backfillData.ts +++ b/backend/src/utils/setup/backfillData.ts @@ -3,13 +3,7 @@ import crypto from "crypto"; import { Types } from "mongoose"; import { encryptSymmetric128BitHexKeyUTF8 } from "../crypto"; import { EESecretService } from "../../ee/services"; -import { - IPType, - ISecretVersion, - SecretSnapshot, - SecretVersion, - TrustedIP -} from "../../ee/models"; +import { IPType, ISecretVersion, SecretSnapshot, SecretVersion, TrustedIP } from "../../ee/models"; import { AuthMethod, BackupPrivateKey, @@ -18,6 +12,7 @@ import { ISecret, Integration, IntegrationAuth, + Membership, Organization, Secret, SecretBlindIndexData, @@ -30,7 +25,9 @@ import { client, getEncryptionKey, getRootEncryptionKey } from "../../config"; import { ALGORITHM_AES_256_GCM, ENCODING_SCHEME_BASE64, - ENCODING_SCHEME_UTF8 + ENCODING_SCHEME_UTF8, + MEMBER, + VIEWER } from "../../variables"; import { InternalServerError } from "../errors"; @@ -582,7 +579,7 @@ export const backfillTrustedIps = async () => { filter: { workspace: Types.ObjectId; ipAddress: string; - }, + }; update: { workspace: Types.ObjectId; ipAddress: string; @@ -590,9 +587,9 @@ export const backfillTrustedIps = async () => { prefix: number; isActive: boolean; comment: string; - }, + }; upsert: boolean; - } + }; }[] = []; workspaceIdsToAddTrustedIp.forEach((workspaceId) => { @@ -638,7 +635,7 @@ export const backfillTrustedIps = async () => { await TrustedIP.bulkWrite(operations); console.log("Backfill: Trusted IPs complete"); } -} +}; export const backfillUserAuthMethods = async () => { await User.updateMany( @@ -655,7 +652,6 @@ export const backfillUserAuthMethods = async () => { } ); - const documentsToUpdate = await User.find({ authProvider: { $exists: true }, authMethods: { $exists: false } @@ -676,4 +672,24 @@ export const backfillUserAuthMethods = async () => { } ); } -} +}; + +export const backfillPermission = async () => { + await Membership.updateMany( + { + deniedPermissions: { + $exists: true, + $ne: [] + }, + role: MEMBER + }, + [ + { + $set: { + role: VIEWER + } + } + ] + ); + console.log("Backfill: Finishing converting old denied permission in workspace to viewers"); +}; diff --git a/backend/src/utils/setup/index.ts b/backend/src/utils/setup/index.ts index 90072eb6f..7ebdd2563 100644 --- a/backend/src/utils/setup/index.ts +++ b/backend/src/utils/setup/index.ts @@ -11,6 +11,7 @@ import { backfillBots, backfillEncryptionMetadata, backfillIntegration, + backfillPermission, backfillSecretBlindIndexData, backfillSecretFolders, backfillSecretVersions, @@ -24,12 +25,7 @@ import { reencryptBotPrivateKeys, reencryptSecretBlindIndexDataSalts } from "./reencryptData"; -import { - getMongoURL, - getNodeEnv, - getRedisUrl, - getSentryDSN -} from "../../config"; +import { getMongoURL, getNodeEnv, getRedisUrl, getSentryDSN } from "../../config"; import { initializePassport } from "../auth"; /** @@ -43,8 +39,10 @@ import { initializePassport } from "../auth"; * - Re-encrypting data */ export const setup = async () => { - if (await getRedisUrl() === undefined || await getRedisUrl() === "") { - console.error("WARNING: Redis is not yet configured. Infisical may not function as expected without it.") + if ((await getRedisUrl()) === undefined || (await getRedisUrl()) === "") { + console.error( + "WARNING: Redis is not yet configured. Infisical may not function as expected without it." + ); } await validateEncryptionKeysConfig(); @@ -86,6 +84,7 @@ export const setup = async () => { await backfillServiceTokenMultiScope(); await backfillTrustedIps(); await backfillUserAuthMethods(); + await backfillPermission(); // re-encrypt any data previously encrypted under server hex 128-bit ENCRYPTION_KEY // to base64 256-bit ROOT_ENCRYPTION_KEY diff --git a/backend/src/validation/action.ts b/backend/src/validation/action.ts new file mode 100644 index 000000000..7c76a5365 --- /dev/null +++ b/backend/src/validation/action.ts @@ -0,0 +1,19 @@ +import { z } from "zod"; + +export const GetActionV1 = z.object({ + params: z.object({ + actionId: z.string().trim() + }) +}); + +export const AddUserActionV1 = z.object({ + body: z.object({ + action: z.string().trim() + }) +}); + +export const GetUserActionV1 = z.object({ + query: z.object({ + action: z.string().trim() + }) +}); diff --git a/backend/src/validation/auth.ts b/backend/src/validation/auth.ts new file mode 100644 index 000000000..f32fc1197 --- /dev/null +++ b/backend/src/validation/auth.ts @@ -0,0 +1,134 @@ +import { z } from "zod"; + +export const BeginEmailSignUpV1 = z.object({ + body: z.object({ + email: z.string().email().trim() + }) +}); + +export const VerifyEmailSignUpV1 = z.object({ + body: z.object({ + email: z.string().email().trim(), + code: z.string().trim() + }) +}); + +export const Login1V1 = z.object({ + body: z.object({ + email: z.string().email().trim(), + clientPublicKey: z.string().trim() + }) +}); + +export const Login2V1 = z.object({ + body: z.object({ + email: z.string().email().trim(), + clientProof: z.string().trim() + }) +}); + +export const Srp1V1 = z.object({ + body: z.object({ + clientPublicKey: z.string().trim() + }) +}); + +export const ChangePasswordV1 = z.object({ + body: z.object({ + clientProof: z.string().trim(), + protectedKey: z.string().trim(), + protectedKeyIV: z.string().trim(), + protectedKeyTag: z.string().trim(), + encryptedPrivateKey: z.string().trim(), + encryptedPrivateKeyIV: z.string().trim(), + encryptedPrivateKeyTag: z.string().trim(), + salt: z.string().trim(), + verifier: z.string().trim() + }) +}); + +export const EmailPasswordResetV1 = z.object({ + body: z.object({ + email: z.string().email().trim() + }) +}); + +export const EmailPasswordResetVerifyV1 = z.object({ + body: z.object({ + email: z.string().email().trim(), + code: z.string().trim() + }) +}); + +export const CreateBackupPrivateKeyV1 = z.object({ + body: z.object({ + clientProof: z.string().trim(), + encryptedPrivateKey: z.string().trim(), + iv: z.string().trim(), + tag: z.string().trim(), + salt: z.string().trim(), + verifier: z.string().trim() + }) +}); + +export const ResetPasswordV1 = z.object({ + body: z.object({ + protectedKey: z.string().trim(), + protectedKeyIV: z.string().trim(), + protectedKeyTag: z.string().trim(), + encryptedPrivateKey: z.string().trim(), + encryptedPrivateKeyIV: z.string().trim(), + encryptedPrivateKeyTag: z.string().trim(), + salt: z.string().trim(), + verifier: z.string().trim() + }) +}); + +export const SendMfaTokenV2 = z.object({ + body: z.object({ + email: z.string().email().trim() + }) +}); + +export const VerifyMfaTokenV2 = z.object({ + body: z.object({ + email: z.string().email().trim(), + mfaToken: z.string().trim() + }) +}); + +export const Login1V3 = z.object({ + body: z.object({ + email: z.string().email().trim(), + providerAuthToken: z.string().trim().optional(), + clientPublicKey: z.string().trim() + }) +}); + +export const Login2V3 = z.object({ + body: z.object({ + email: z.string().email().trim(), + providerAuthToken: z.string().trim().optional(), + clientProof: z.string().trim() + }) +}); + +export const CompletedAccountSignupV3 = z.object({ + body: z.object({ + email: z.string().email().trim(), + firstName: z.string().trim(), + lastName: z.string().trim().optional(), + protectedKey: z.string().trim(), + protectedKeyIV: z.string().trim(), + protectedKeyTag: z.string().trim(), + publicKey: z.string().trim(), + encryptedPrivateKey: z.string().trim(), + encryptedPrivateKeyIV: z.string().trim(), + encryptedPrivateKeyTag: z.string().trim(), + salt: z.string().trim(), + verifier: z.string().trim(), + organizationName: z.string().trim(), + providerAuthToken: z.string().trim().optional().nullish(), + attributionSource: z.string().trim().optional() + }) +}); diff --git a/backend/src/validation/bot.ts b/backend/src/validation/bot.ts index 2bb6ec6dd..2980aeac1 100644 --- a/backend/src/validation/bot.ts +++ b/backend/src/validation/bot.ts @@ -1,15 +1,10 @@ import { Types } from "mongoose"; -import { - Bot, - IUser, -} from "../models"; +import { Bot, IUser } from "../models"; import { validateUserClientForWorkspace } from "./user"; -import { - BotNotFoundError, - UnauthorizedRequestError, -} from "../utils/errors"; +import { BotNotFoundError, UnauthorizedRequestError } from "../utils/errors"; import { AuthData } from "../interfaces/middleware"; import { ActorType } from "../ee/models"; +import { z } from "zod"; /** * Validate authenticated clients for bot with id [botId] based @@ -22,7 +17,7 @@ import { ActorType } from "../ee/models"; export const validateClientForBot = async ({ authData, botId, - acceptedRoles, + acceptedRoles }: { authData: AuthData; botId: Types.ObjectId; @@ -30,18 +25,39 @@ export const validateClientForBot = async ({ }) => { const bot = await Bot.findById(botId); if (!bot) throw BotNotFoundError(); - + switch (authData.actor.type) { case ActorType.USER: await validateUserClientForWorkspace({ user: authData.authPayload as IUser, workspaceId: bot.workspace, - acceptedRoles, + acceptedRoles }); return bot; case ActorType.SERVICE: throw UnauthorizedRequestError({ - message: "Failed service token authorization for bot", + message: "Failed service token authorization for bot" }); } -}; \ No newline at end of file +}; + +export const GetBotByWorkspaceIdV1 = z.object({ + params: z.object({ + workspaceId: z.string().trim() + }) +}); + +export const SetBotActiveStateV1 = z.object({ + body: z.object({ + isActive: z.boolean(), + botKey: z + .object({ + nonce: z.string().trim().optional(), + encryptedKey: z.string().trim().optional() + }) + .optional() + }), + params: z.object({ + botId: z.string().trim() + }) +}); diff --git a/backend/src/validation/cloudProducts.ts b/backend/src/validation/cloudProducts.ts new file mode 100644 index 000000000..1cfd361b3 --- /dev/null +++ b/backend/src/validation/cloudProducts.ts @@ -0,0 +1,7 @@ +import { z } from "zod"; + +export const GetCloudProductsV1 = z.object({ + query: z.object({ + "billing-cycle": z.enum(["monthly", "yearly"]) + }) +}); diff --git a/backend/src/validation/environments.ts b/backend/src/validation/environments.ts new file mode 100644 index 000000000..6cf7cf68a --- /dev/null +++ b/backend/src/validation/environments.ts @@ -0,0 +1,49 @@ +import { z } from "zod"; + +export const CreateWorkspaceEnvironmentV2 = z.object({ + params: z.object({ + workspaceId: z.string().trim() + }), + body: z.object({ + environmentSlug: z.string().trim(), + environmentName: z.string().trim() + }) +}); + +export const UpdateWorkspaceEnvironmentV2 = z.object({ + params: z.object({ + workspaceId: z.string().trim() + }), + body: z.object({ + environmentSlug: z.string().trim(), + environmentName: z.string().trim(), + oldEnvironmentSlug: z.string().trim() + }) +}); + +export const DeleteWorkspaceEnvironmentV2 = z.object({ + params: z.object({ + workspaceId: z.string().trim() + }), + body: z.object({ + environmentSlug: z.string().trim() + }) +}); + +export const GetAllAccessibileEnvironmentsOfWorkspaceV2 = z.object({ + params: z.object({ + workspaceId: z.string().trim() + }) +}); + +export const ReorderWorkspaceEnvironmentsV2 = z.object({ + params: z.object({ + workspaceId: z.string().trim() + }), + body: z.object({ + environmentSlug: z.string().trim(), + environmentName: z.string().trim(), + otherEnvironmentSlug: z.string().trim(), + otherEnvironmentName: z.string().trim() + }) +}); diff --git a/backend/src/validation/folders.ts b/backend/src/validation/folders.ts new file mode 100644 index 000000000..ecfadea1c --- /dev/null +++ b/backend/src/validation/folders.ts @@ -0,0 +1,40 @@ +import { z } from "zod"; + +export const CreateFolderV1 = z.object({ + body: z.object({ + workspaceId: z.string().trim(), + environment: z.string().trim(), + folderName: z.string().trim(), + parentFolderId: z.string().trim().optional() + }) +}); + +export const UpdateFolderV1 = z.object({ + params: z.object({ + folderId: z.string().trim() + }), + body: z.object({ + workspaceId: z.string().trim(), + environment: z.string().trim(), + name: z.string().trim() + }) +}); + +export const DeleteFolderV1 = z.object({ + params: z.object({ + folderId: z.string().trim() + }), + body: z.object({ + workspaceId: z.string().trim(), + environment: z.string().trim() + }) +}); + +export const GetFoldersV1 = z.object({ + query: z.object({ + workspaceId: z.string().trim(), + environment: z.string().trim(), + parentFolderId: z.string().trim().optional(), + parentFolderPath: z.string().trim().optional() + }) +}); diff --git a/backend/src/validation/index.ts b/backend/src/validation/index.ts index 4cc25450f..409b563c6 100644 --- a/backend/src/validation/index.ts +++ b/backend/src/validation/index.ts @@ -8,4 +8,4 @@ export * from "./membershipOrg"; export * from "./organization"; export * from "./secrets"; export * from "./serviceAccount"; -export * from "./serviceTokenData"; \ No newline at end of file +export * from "./serviceTokenData"; diff --git a/backend/src/validation/integration.ts b/backend/src/validation/integration.ts index b1143a9f7..7ec0d0875 100644 --- a/backend/src/validation/integration.ts +++ b/backend/src/validation/integration.ts @@ -1,18 +1,15 @@ import { Types } from "mongoose"; -import { - IUser, - Integration, - IntegrationAuth, -} from "../models"; +import { IUser, Integration, IntegrationAuth } from "../models"; import { validateUserClientForWorkspace } from "./user"; import { IntegrationService } from "../services"; import { - IntegrationAuthNotFoundError, - IntegrationNotFoundError, - UnauthorizedRequestError, + IntegrationAuthNotFoundError, + IntegrationNotFoundError, + UnauthorizedRequestError } from "../utils/errors"; import { AuthData } from "../interfaces/middleware"; import { ActorType } from "../ee/models"; +import { z } from "zod"; /** * Validate authenticated clients for integration with id [integrationId] based @@ -25,42 +22,90 @@ import { ActorType } from "../ee/models"; * @param {String[]} obj.requiredPermissions - required permissions as part of the endpoint */ export const validateClientForIntegration = async ({ - authData, - integrationId, - acceptedRoles, + authData, + integrationId, + acceptedRoles }: { - authData: AuthData; - integrationId: Types.ObjectId; - acceptedRoles: Array<"admin" | "member">; + authData: AuthData; + integrationId: Types.ObjectId; + acceptedRoles: Array<"admin" | "member">; }) => { - - const integration = await Integration.findById(integrationId); - if (!integration) throw IntegrationNotFoundError(); + const integration = await Integration.findById(integrationId); + if (!integration) throw IntegrationNotFoundError(); - const integrationAuth = await IntegrationAuth - .findById(integration.integrationAuth) - .select( - "+refreshCiphertext +refreshIV +refreshTag +accessCiphertext +accessIV +accessTag +accessExpiresAt" - ); - - if (!integrationAuth) throw IntegrationAuthNotFoundError(); + const integrationAuth = await IntegrationAuth.findById(integration.integrationAuth).select( + "+refreshCiphertext +refreshIV +refreshTag +accessCiphertext +accessIV +accessTag +accessExpiresAt" + ); - const accessToken = (await IntegrationService.getIntegrationAuthAccess({ - integrationAuthId: integrationAuth._id, - })).accessToken; - - switch (authData.actor.type) { - case ActorType.USER: - await validateUserClientForWorkspace({ - user: authData.authPayload as IUser, - workspaceId: integration.workspace, - acceptedRoles, - }); - - return ({ integration, accessToken }); - case ActorType.SERVICE: - throw UnauthorizedRequestError({ - message: "Failed service token authorization for integration", - }); - } -} \ No newline at end of file + if (!integrationAuth) throw IntegrationAuthNotFoundError(); + + const accessToken = ( + await IntegrationService.getIntegrationAuthAccess({ + integrationAuthId: integrationAuth._id + }) + ).accessToken; + + switch (authData.actor.type) { + case ActorType.USER: + await validateUserClientForWorkspace({ + user: authData.authPayload as IUser, + workspaceId: integration.workspace, + acceptedRoles + }); + + return { integration, accessToken }; + case ActorType.SERVICE: + throw UnauthorizedRequestError({ + message: "Failed service token authorization for integration" + }); + } +}; + +export const CreateIntegrationV1 = z.object({ + body: z.object({ + integrationAuthId: z.string().trim(), + app: z.string().trim().optional(), + isActive: z.boolean(), + appId: z.string().trim().optional(), + secretPath: z.string().trim().default("/"), + sourceEnvironment: z.string().trim(), + targetEnvironment: z.string().trim().optional(), + targetEnvironmentId: z.string().trim().optional(), + targetService: z.string().trim().optional(), + targetServiceId: z.string().trim().optional(), + owner: z.string().trim().optional(), + path: z.string().trim().optional(), + region: z.string().trim().optional(), + metadata: z.object({ + secretSuffix: z.string().optional() + }).optional() + }) +}); + +export const UpdateIntegrationV1 = z.object({ + params: z.object({ + integrationId: z.string().trim() + }), + body: z.object({ + app: z.string().trim(), + appId: z.string().trim(), + isActive: z.boolean(), + secretPath: z.string().trim().default("/"), + targetEnvironment: z.string().trim(), + owner: z.string().trim(), + environment: z.string().trim() + }) +}); + +export const DeleteIntegrationV1 = z.object({ + params: z.object({ + integrationId: z.string().trim() + }) +}); + +export const ManualSyncV1 = z.object({ + body: z.object({ + environment: z.string().trim(), + workspaceId: z.string().trim() + }) +}); diff --git a/backend/src/validation/integrationAuth.ts b/backend/src/validation/integrationAuth.ts index 676184324..18ff191af 100644 --- a/backend/src/validation/integrationAuth.ts +++ b/backend/src/validation/integrationAuth.ts @@ -1,17 +1,11 @@ import { Types } from "mongoose"; -import { - IUser, - IWorkspace, - IntegrationAuth, -} from "../models"; -import { - IntegrationAuthNotFoundError, - UnauthorizedRequestError, -} from "../utils/errors"; +import { IUser, IWorkspace, IntegrationAuth } from "../models"; +import { IntegrationAuthNotFoundError, UnauthorizedRequestError } from "../utils/errors"; import { IntegrationService } from "../services"; import { validateUserClientForWorkspace } from "./user"; import { AuthData } from "../interfaces/middleware"; import { ActorType } from "../ee/models"; +import { z } from "zod"; /** * Validate authenticated clients for integration authorization with id [integrationAuthId] based @@ -22,53 +16,147 @@ import { ActorType } from "../ee/models"; * @param {Array<'admin' | 'member'>} obj.acceptedRoles - accepted workspace roles * @param {String[]} obj.requiredPermissions - required permissions as part of the endpoint */ - const validateClientForIntegrationAuth = async ({ - authData, - integrationAuthId, - acceptedRoles, - attachAccessToken, +const validateClientForIntegrationAuth = async ({ + authData, + integrationAuthId, + acceptedRoles, + attachAccessToken }: { - authData: AuthData; - integrationAuthId: Types.ObjectId; - acceptedRoles: Array<"admin" | "member">; - attachAccessToken?: boolean; + authData: AuthData; + integrationAuthId: Types.ObjectId; + acceptedRoles: Array<"admin" | "member">; + attachAccessToken?: boolean; }) => { + const integrationAuth = await IntegrationAuth.findById(integrationAuthId) + .populate<{ workspace: IWorkspace }>("workspace") + .select( + "+refreshCiphertext +refreshIV +refreshTag +accessCiphertext +accessIV +accessTag +accessExpiresAt" + ); - const integrationAuth = await IntegrationAuth - .findById(integrationAuthId) - .populate<{ workspace: IWorkspace }>("workspace") - .select( - "+refreshCiphertext +refreshIV +refreshTag +accessCiphertext +accessIV +accessTag +accessExpiresAt" - ); - - if (!integrationAuth) throw IntegrationAuthNotFoundError(); - - let accessToken, accessId; - if (attachAccessToken) { - const access = (await IntegrationService.getIntegrationAuthAccess({ - integrationAuthId: integrationAuth._id, - })); - - accessToken = access.accessToken; - accessId = access.accessId; - } - - switch (authData.actor.type) { - case ActorType.USER: - await validateUserClientForWorkspace({ - user: authData.authPayload as IUser, - workspaceId: integrationAuth.workspace._id, - acceptedRoles, - }); + if (!integrationAuth) throw IntegrationAuthNotFoundError(); - return ({ integrationAuth, accessToken, accessId }); - case ActorType.SERVICE: - throw UnauthorizedRequestError({ - message: "Failed service token authorization for integration authorization", - }); - } -} + let accessToken, accessId; + if (attachAccessToken) { + const access = await IntegrationService.getIntegrationAuthAccess({ + integrationAuthId: integrationAuth._id + }); -export { - validateClientForIntegrationAuth, -}; \ No newline at end of file + accessToken = access.accessToken; + accessId = access.accessId; + } + + switch (authData.actor.type) { + case ActorType.USER: + await validateUserClientForWorkspace({ + user: authData.authPayload as IUser, + workspaceId: integrationAuth.workspace._id, + acceptedRoles + }); + + return { integrationAuth, accessToken, accessId }; + case ActorType.SERVICE: + throw UnauthorizedRequestError({ + message: "Failed service token authorization for integration authorization" + }); + } +}; + +export const GetIntegrationAuthV1 = z.object({ + params: z.object({ + integrationAuthId: z.string().trim() + }) +}); + +export const OauthExchangeV1 = z.object({ + body: z.object({ + workspaceId: z.string().trim(), + code: z.string().trim(), + integration: z.string().trim(), + url: z.string().trim().url().optional(), + }) +}); + +export const SaveIntegrationAccessTokenV1 = z.object({ + body: z.object({ + workspaceId: z.string().trim(), + integration: z.string().trim(), + accessId: z.string().trim().optional(), + accessToken: z.string().trim().optional(), + url: z.string().url().trim().optional(), + namespace: z.string().trim().optional(), + refreshToken:z.string().trim().optional() + }) +}); + +export const GetIntegrationAuthAppsV1 = z.object({ + params: z.object({ + integrationAuthId: z.string().trim() + }), + query: z.object({ + teamId: z.string().trim().optional(), + workspaceSlug: z.string().trim().optional() + }) +}); + +export const GetIntegrationAuthTeamsV1 = z.object({ + params: z.object({ + integrationAuthId: z.string().trim() + }) +}); + +export const GetIntegrationAuthVercelBranchesV1 = z.object({ + params: z.object({ + integrationAuthId: z.string().trim() + }), + query: z.object({ + appId: z.string().trim() + }) +}); + +export const GetIntegrationAuthRailwayEnvironmentsV1 = z.object({ + params: z.object({ + integrationAuthId: z.string().trim() + }), + query: z.object({ + appId: z.string().trim() + }) +}); + +export const GetIntegrationAuthRailwayServicesV1 = z.object({ + params: z.object({ + integrationAuthId: z.string().trim() + }), + query: z.object({ + appId: z.string().trim() + }) +}); + +export const GetIntegrationAuthBitbucketWorkspacesV1 = z.object({ + params: z.object({ + integrationAuthId: z.string().trim() + }) +}); + +export const GetIntegrationAuthNorthflankSecretGroupsV1 = z.object({ + params: z.object({ + integrationAuthId: z.string().trim() + }), + query: z.object({ + appId: z.string().trim() + }) +}); + +export const DeleteIntegrationAuthV1 = z.object({ + params: z.object({ + integrationAuthId: z.string().trim() + }) +}); + +export const GetIntegrationAuthTeamCityBuildConfigsV1 = z.object({ + params:z.object({ + appId:z.string().trim(), + integrationAuthId:z.string().trim() + }) +}) + +export { validateClientForIntegrationAuth }; diff --git a/backend/src/validation/key.ts b/backend/src/validation/key.ts new file mode 100644 index 000000000..a2d4ab92d --- /dev/null +++ b/backend/src/validation/key.ts @@ -0,0 +1,20 @@ +import { z } from "zod"; + +export const UploadKeyV1 = z.object({ + params: z.object({ + workspaceId: z.string().trim() + }), + body: z.object({ + key: z.object({ + encryptedKey: z.string().trim(), + nonce: z.string().trim(), + userId: z.string().trim() + }) + }) +}); + +export const GetLatestKeyV1 = z.object({ + params: z.object({ + workspaceId: z.string().trim() + }) +}); diff --git a/backend/src/validation/membership.ts b/backend/src/validation/membership.ts index 6c40aa0aa..233805545 100644 --- a/backend/src/validation/membership.ts +++ b/backend/src/validation/membership.ts @@ -1,16 +1,11 @@ import { Types } from "mongoose"; -import { - IServiceTokenData, - IUser, - Membership, -} from "../models"; +import { IServiceTokenData, IUser, Membership } from "../models"; import { validateUserClientForWorkspace } from "./user"; import { validateServiceTokenDataClientForWorkspace } from "./serviceTokenData"; -import { - MembershipNotFoundError, -} from "../utils/errors"; +import { MembershipNotFoundError } from "../utils/errors"; import { AuthData } from "../interfaces/middleware"; import { ActorType } from "../ee/models"; +import { z } from "zod"; /** * Validate authenticated clients for membership with id [membershipId] based @@ -22,36 +17,64 @@ import { ActorType } from "../ee/models"; * @returns {Membership} - validated membership */ export const validateClientForMembership = async ({ - authData, - membershipId, - acceptedRoles, + authData, + membershipId, + acceptedRoles }: { - authData: AuthData; - membershipId: Types.ObjectId; - acceptedRoles: Array<"admin" | "member">; + authData: AuthData; + membershipId: Types.ObjectId; + acceptedRoles: Array<"admin" | "member">; }) => { - - const membership = await Membership.findById(membershipId); - - if (!membership) throw MembershipNotFoundError({ - message: "Failed to find membership", - }); - - switch (authData.actor.type) { - case ActorType.USER: - await validateUserClientForWorkspace({ - user: authData.authPayload as IUser, - workspaceId: membership.workspace, - acceptedRoles, - }); - - return membership; - case ActorType.SERVICE: - await validateServiceTokenDataClientForWorkspace({ - serviceTokenData: authData.authPayload as IServiceTokenData, - workspaceId: new Types.ObjectId(membership.workspace), - }); - - return membership; - } -} \ No newline at end of file + const membership = await Membership.findById(membershipId); + + if (!membership) + throw MembershipNotFoundError({ + message: "Failed to find membership" + }); + + switch (authData.actor.type) { + case ActorType.USER: + await validateUserClientForWorkspace({ + user: authData.authPayload as IUser, + workspaceId: membership.workspace, + acceptedRoles + }); + + return membership; + case ActorType.SERVICE: + await validateServiceTokenDataClientForWorkspace({ + serviceTokenData: authData.authPayload as IServiceTokenData, + workspaceId: new Types.ObjectId(membership.workspace) + }); + + return membership; + } +}; + +export const ValidateMembershipV1 = z.object({ + params: z.object({ + workspaceId: z.string().trim() + }) +}); + +export const DeleteMembershipV1 = z.object({ + params: z.object({ + membershipId: z.string().trim() + }) +}); + +export const ChangeMembershipRoleV1 = z.object({ + body: z.object({ + role: z.string().trim() + }), + params: z.object({ membershipId: z.string().trim() }) +}); + +export const DenyMembershipPermissionV1 = z.object({ + params: z.object({ + membershipId: z.string().trim() + }), + body: z.object({ + permissions: z.object({}).array() + }) +}); diff --git a/backend/src/validation/membershipOrg.ts b/backend/src/validation/membershipOrg.ts index b0ada6a61..02f9cac08 100644 --- a/backend/src/validation/membershipOrg.ts +++ b/backend/src/validation/membershipOrg.ts @@ -1,16 +1,10 @@ import { Types } from "mongoose"; -import { - MembershipOrg, -} from "../models"; -import { - validateMembershipOrg, -} from "../helpers/membershipOrg"; -import { - MembershipOrgNotFoundError, - UnauthorizedRequestError, -} from "../utils/errors"; +import { MembershipOrg } from "../models"; +import { validateMembershipOrg } from "../helpers/membershipOrg"; +import { MembershipOrgNotFoundError, UnauthorizedRequestError } from "../utils/errors"; import { AuthData } from "../interfaces/middleware"; import { ActorType } from "../ee/models"; +import { z } from "zod"; /** * Validate authenticated clients for organization membership with id [membershipOrgId] based @@ -22,35 +16,57 @@ import { ActorType } from "../ee/models"; * @param {MembershipOrg} - validated organization membership */ export const validateClientForMembershipOrg = async ({ - authData, - membershipOrgId, - acceptedRoles, - acceptedStatuses, + authData, + membershipOrgId, + acceptedRoles, + acceptedStatuses }: { - authData: AuthData; - membershipOrgId: Types.ObjectId; - acceptedRoles: Array<"owner" | "admin" | "member">; - acceptedStatuses: Array<"invited" | "accepted">; + authData: AuthData; + membershipOrgId: Types.ObjectId; + acceptedRoles: Array<"owner" | "admin" | "member">; + acceptedStatuses: Array<"invited" | "accepted">; }) => { - const membershipOrg = await MembershipOrg.findById(membershipOrgId); + const membershipOrg = await MembershipOrg.findById(membershipOrgId); - if (!membershipOrg) throw MembershipOrgNotFoundError({ - message: "Failed to find organization membership ", - }); - - switch (authData.actor.type) { - case ActorType.USER: - await validateMembershipOrg({ - userId: authData.authPayload._id, - organizationId: membershipOrg.organization, - acceptedRoles, - acceptedStatuses, - }); - - return membershipOrg; - case ActorType.SERVICE: - throw UnauthorizedRequestError({ - message: "Failed service account client authorization for organization membership", - }); - } -} \ No newline at end of file + if (!membershipOrg) + throw MembershipOrgNotFoundError({ + message: "Failed to find organization membership " + }); + + switch (authData.actor.type) { + case ActorType.USER: + await validateMembershipOrg({ + userId: authData.authPayload._id, + organizationId: membershipOrg.organization, + acceptedRoles, + acceptedStatuses + }); + + return membershipOrg; + case ActorType.SERVICE: + throw UnauthorizedRequestError({ + message: "Failed service account client authorization for organization membership" + }); + } +}; + +export const DelOrgMembershipv1 = z.object({ + params: z.object({ + membershipOrgId: z.string().trim() + }) +}); + +export const InviteUserToOrgv1 = z.object({ + body: z.object({ + inviteeEmail: z.string().trim().email(), + organizationId: z.string().trim() + }) +}); + +export const VerifyUserToOrgv1 = z.object({ + body: z.object({ + email: z.string().trim().email(), + organizationId: z.string().trim(), + code: z.string().trim() + }) +}); diff --git a/backend/src/validation/organization.ts b/backend/src/validation/organization.ts index 4ca9811c5..9aa13cbb6 100644 --- a/backend/src/validation/organization.ts +++ b/backend/src/validation/organization.ts @@ -1,12 +1,7 @@ import { Types } from "mongoose"; -import { - IUser, - Organization, -} from "../models"; -import { - OrganizationNotFoundError, - UnauthorizedRequestError, -} from "../utils/errors"; +import { z } from "zod"; +import { IUser, Organization } from "../models"; +import { OrganizationNotFoundError, UnauthorizedRequestError } from "../utils/errors"; import { validateUserClientForOrganization } from "./user"; import { AuthData } from "../interfaces/middleware"; import { ActorType } from "../ee/models"; @@ -21,7 +16,7 @@ export const validateClientForOrganization = async ({ authData, organizationId, acceptedRoles, - acceptedStatuses, + acceptedStatuses }: { authData: AuthData; organizationId: Types.ObjectId; @@ -32,10 +27,10 @@ export const validateClientForOrganization = async ({ if (!organization) { throw OrganizationNotFoundError({ - message: "Failed to find organization", + message: "Failed to find organization" }); } - + let membershipOrg; switch (authData.actor.type) { case ActorType.USER: @@ -43,13 +38,170 @@ export const validateClientForOrganization = async ({ user: authData.authPayload as IUser, organization, acceptedRoles, - acceptedStatuses, + acceptedStatuses }); - return { organization, membershipOrg }; + return { organization, membershipOrg }; case ActorType.SERVICE: throw UnauthorizedRequestError({ - message: "Failed service token authorization for organization", + message: "Failed service token authorization for organization" }); } -}; \ No newline at end of file +}; + +export const GetOrgPlansTablev1 = z.object({ + query: z.object({ billingCycle: z.enum(["monthly", "yearly"]) }), + params: z.object({ organizationId: z.string().trim() }) +}); + +export const GetOrgPlanv1 = z.object({ + params: z.object({ organizationId: z.string().trim() }), + query: z.object({ workspaceId: z.string().trim().optional() }) +}); + +export const StartOrgTrailv1 = z.object({ + params: z.object({ organizationId: z.string().trim() }), + body: z.object({ success_url: z.string().trim() }) +}); + +export const GetOrgPlanBillingInfov1 = z.object({ + params: z.object({ organizationId: z.string().trim() }), + query: z.object({ workspaceId: z.string().trim().optional() }) +}); + +export const GetOrgPlanTablev1 = z.object({ + params: z.object({ organizationId: z.string().trim() }), + query: z.object({ workspaceId: z.string().trim().optional() }) +}); + +export const GetOrgBillingDetailsv1 = z.object({ + params: z.object({ organizationId: z.string().trim() }) +}); + +export const UpdateOrgBillingDetailsv1 = z.object({ + params: z.object({ organizationId: z.string().trim() }), + body: z.object({ + email: z.string().trim().email().optional(), + name: z.string().trim().optional() + }) +}); + +export const GetOrgPmtMethodsv1 = z.object({ + params: z.object({ organizationId: z.string().trim() }) +}); + +export const CreateOrgPmtMethodv1 = z.object({ + params: z.object({ organizationId: z.string().trim() }), + body: z.object({ + success_url: z.string().trim(), + cancel_url: z.string().trim() + }) +}); + +export const DelOrgPmtMethodv1 = z.object({ + params: z.object({ + organizationId: z.string().trim(), + pmtMethodId: z.string().trim() + }) +}); + +export const GetOrgTaxIdsv1 = z.object({ + params: z.object({ organizationId: z.string().trim() }) +}); + +export const CreateOrgTaxId = z.object({ + params: z.object({ organizationId: z.string().trim() }), + body: z.object({ + type: z.string().trim(), + value: z.string().trim() + }) +}); + +export const DelOrgTaxIdv1 = z.object({ + params: z.object({ + organizationId: z.string().trim(), + taxId: z.string().trim() + }) +}); + +export const GetOrgInvoicesv1 = z.object({ + params: z.object({ organizationId: z.string().trim() }) +}); + +export const GetOrgLicencesv1 = z.object({ + params: z.object({ organizationId: z.string().trim() }) +}); + +export const CreateOrgv1 = z.object({ + body: z.object({ + organizationName: z.string().trim() + }) +}); + +export const GetOrgv1 = z.object({ + params: z.object({ + organizationId: z.string().trim() + }) +}); + +export const GetOrgMembersv1 = z.object({ + params: z.object({ organizationId: z.string().trim() }) +}); + +export const GetOrgWorkspacesv1 = z.object({ + params: z.object({ organizationId: z.string().trim() }) +}); + +export const ChangeOrgNamev1 = z.object({ + params: z.object({ organizationId: z.string().trim() }), + body: z.object({ name: z.string().trim() }) +}); + +export const GetOrgIncidentContactv1 = z.object({ + params: z.object({ organizationId: z.string().trim() }) +}); + +export const CreateOrgIncideContact = z.object({ + params: z.object({ organizationId: z.string().trim() }), + body: z.object({ email: z.string().email().trim() }) +}); + +export const DelOrgIncideContact = z.object({ + params: z.object({ organizationId: z.string().trim() }), + body: z.object({ email: z.string().email().trim() }) +}); + +export const CreateOrgPortalSessionv1 = z.object({ + params: z.object({ organizationId: z.string().trim() }) +}); + +export const GetOrgMembersAndWsv1 = z.object({ + params: z.object({ organizationId: z.string().trim() }) +}); + +export const GetOrgMembersv2 = z.object({ + params: z.object({ organizationId: z.string().trim() }) +}); + +export const UpdateOrgMemberv2 = z.object({ + params: z.object({ organizationId: z.string().trim(), membershipId: z.string().trim() }), + body: z.object({ + role: z.string().trim() + }) +}); + +export const DeleteOrgMemberv2 = z.object({ + params: z.object({ organizationId: z.string().trim(), membershipId: z.string().trim() }) +}); + +export const GetOrgWorkspacesv2 = z.object({ + params: z.object({ organizationId: z.string().trim() }) +}); + +export const VerfiyUserToOrganizationV1 = z.object({ + body: z.object({ + email: z.string().trim().email(), + organizationId: z.string().trim(), + code: z.string().trim() + }) +}); diff --git a/backend/src/validation/secretImports.ts b/backend/src/validation/secretImports.ts new file mode 100644 index 000000000..92867a67b --- /dev/null +++ b/backend/src/validation/secretImports.ts @@ -0,0 +1,53 @@ +import { z } from "zod"; + +export const CreateSecretImportV1 = z.object({ + body: z.object({ + workspaceId: z.string().trim(), + environment: z.string().trim(), + folderId: z.string().trim().default("root"), + secretImport: z.object({ + environment: z.string().trim(), + secretPath: z.string().trim() + }) + }) +}); + +export const UpdateSecretImportV1 = z.object({ + params: z.object({ + id: z.string().trim() + }), + body: z.object({ + secretImports: z + .object({ + environment: z.string().trim(), + secretPath: z.string().trim() + }) + .array() + }) +}); + +export const DeleteSecretImportV1 = z.object({ + params: z.object({ + id: z.string().trim() + }), + body: z.object({ + secretImportPath: z.string().trim(), + secretImportEnv: z.string().trim() + }) +}); + +export const GetSecretImportsV1 = z.object({ + query: z.object({ + workspaceId: z.string().trim(), + environment: z.string().trim(), + folderId: z.string().trim().default("root") + }) +}); + +export const GetAllSecretsFromImportV1 = z.object({ + query: z.object({ + workspaceId: z.string().trim(), + environment: z.string().trim(), + folderId: z.string().trim().default("root") + }) +}); diff --git a/backend/src/validation/secretScanning.ts b/backend/src/validation/secretScanning.ts new file mode 100644 index 000000000..e16c67c5c --- /dev/null +++ b/backend/src/validation/secretScanning.ts @@ -0,0 +1,25 @@ +import { z } from "zod"; + +export const CreateInstalLSessionv1 = z.object({ + params: z.object({ organizationId: z.string().trim() }) +}); + +export const LinkInstallationToOrgv1 = z.object({ + body: z.object({ + installationId: z.number(), + sessionId: z.string().trim() + }) +}); + +export const GetOrgInstallStatusv1 = z.object({ + params: z.object({ organizationId: z.string().trim() }) +}); + +export const GetOrgRisksv1 = z.object({ + params: z.object({ organizationId: z.string().trim() }) +}); + +export const UpdateRiskStatusv1 = z.object({ + params: z.object({ organizationId: z.string().trim(), riskId: z.string().trim() }), + body: z.object({ status: z.string().trim() }) +}); diff --git a/backend/src/validation/secretSnapshot.ts b/backend/src/validation/secretSnapshot.ts new file mode 100644 index 000000000..b431547e8 --- /dev/null +++ b/backend/src/validation/secretSnapshot.ts @@ -0,0 +1,7 @@ +import { z } from "zod"; + +export const GetSecretSnapshotV1 = z.object({ + params: z.object({ + secretSnapshotId: z.string().trim() + }) +}); diff --git a/backend/src/validation/secrets.ts b/backend/src/validation/secrets.ts index 6c0ac3084..700a3ea5d 100644 --- a/backend/src/validation/secrets.ts +++ b/backend/src/validation/secrets.ts @@ -1,18 +1,15 @@ import { Types } from "mongoose"; -import { - ISecret, - IServiceTokenData, - IUser, - Secret, -} from "../models"; +import { ISecret, IServiceTokenData, IUser, Secret } from "../models"; import { validateUserClientForSecret, validateUserClientForSecrets } from "./user"; -import { validateServiceTokenDataClientForSecrets, validateServiceTokenDataClientForWorkspace } from "./serviceTokenData"; import { - BadRequestError, - SecretNotFoundError, -} from "../utils/errors"; + validateServiceTokenDataClientForSecrets, + validateServiceTokenDataClientForWorkspace +} from "./serviceTokenData"; +import { BadRequestError, SecretNotFoundError } from "../utils/errors"; import { AuthData } from "../interfaces/middleware"; import { ActorType } from "../ee/models"; +import { z } from "zod"; +import { SECRET_PERSONAL, SECRET_SHARED } from "../variables"; /** * Validate authenticated clients for secrets with id [secretId] based @@ -24,42 +21,43 @@ import { ActorType } from "../ee/models"; * @param {String[]} obj.requiredPermissions - required permissions as part of the endpoint */ export const validateClientForSecret = async ({ - authData, - secretId, - acceptedRoles, - requiredPermissions, + authData, + secretId, + acceptedRoles, + requiredPermissions }: { - authData: AuthData; - secretId: Types.ObjectId; - acceptedRoles: Array<"admin" | "member">; - requiredPermissions: string[]; + authData: AuthData; + secretId: Types.ObjectId; + acceptedRoles: Array<"admin" | "member">; + requiredPermissions: string[]; }) => { - const secret = await Secret.findById(secretId); + const secret = await Secret.findById(secretId); - if (!secret) throw SecretNotFoundError({ - message: "Failed to find secret", + if (!secret) + throw SecretNotFoundError({ + message: "Failed to find secret" }); - - switch (authData.actor.type) { - case ActorType.USER: - await validateUserClientForSecret({ - user: authData.authPayload as IUser, - secret, - acceptedRoles, - requiredPermissions, - }); - return secret; - case ActorType.SERVICE: - await validateServiceTokenDataClientForWorkspace({ - serviceTokenData: authData.authPayload as IServiceTokenData, - workspaceId: secret.workspace, - environment: secret.environment, - }); - - return secret; - } -} + switch (authData.actor.type) { + case ActorType.USER: + await validateUserClientForSecret({ + user: authData.authPayload as IUser, + secret, + acceptedRoles, + requiredPermissions + }); + + return secret; + case ActorType.SERVICE: + await validateServiceTokenDataClientForWorkspace({ + serviceTokenData: authData.authPayload as IServiceTokenData, + workspaceId: secret.workspace, + environment: secret.environment + }); + + return secret; + } +}; /** * Validate authenticated clients for secrets with ids [secretIds] based @@ -72,43 +70,294 @@ export const validateClientForSecret = async ({ * @param {String[]} obj.requiredPermissions - required permissions as part of the endpoint */ export const validateClientForSecrets = async ({ - authData, - secretIds, - requiredPermissions, + authData, + secretIds, + requiredPermissions }: { - authData: AuthData; - secretIds: Types.ObjectId[]; - requiredPermissions: string[]; + authData: AuthData; + secretIds: Types.ObjectId[]; + requiredPermissions: string[]; }) => { + let secrets: ISecret[] = []; - let secrets: ISecret[] = []; - - secrets = await Secret.find({ - _id: { - $in: secretIds, - }, - }); + secrets = await Secret.find({ + _id: { + $in: secretIds + } + }); - if (secrets.length != secretIds.length) { - throw BadRequestError({ message: "Failed to validate non-existent secrets" }) - } - - switch (authData.actor.type) { - case ActorType.USER: - await validateUserClientForSecrets({ - user: authData.authPayload as IUser, - secrets, - requiredPermissions, - }); - - return secrets; - case ActorType.SERVICE: - await validateServiceTokenDataClientForSecrets({ - serviceTokenData: authData.authPayload as IServiceTokenData, - secrets, - requiredPermissions, - }); - - return secrets; - } -} \ No newline at end of file + if (secrets.length != secretIds.length) { + throw BadRequestError({ message: "Failed to validate non-existent secrets" }); + } + + switch (authData.actor.type) { + case ActorType.USER: + await validateUserClientForSecrets({ + user: authData.authPayload as IUser, + secrets, + requiredPermissions + }); + + return secrets; + case ActorType.SERVICE: + await validateServiceTokenDataClientForSecrets({ + serviceTokenData: authData.authPayload as IServiceTokenData, + secrets, + requiredPermissions + }); + + return secrets; + } +}; + +export const GetSecretVersionsV1 = z.object({ + params: z.object({ + secretId: z.string().trim() + }), + query: z.object({ + offset: z.coerce.number(), + limit: z.coerce.number() + }) +}); + +export const RollbackSecretVersionV1 = z.object({ + params: z.object({ + secretId: z.string().trim() + }), + body: z.object({ + version: z.number() + }) +}); + +export const PushSecretsV1 = z.object({ + params: z.object({ + workspaceId: z.string().trim() + }), + body: z.object({ + secrets: z.object({}).array(), + keys: z.object({}).array(), + environment: z.string().trim(), + channel: z.string().trim() + }) +}); + +export const PullSecretsV1 = z.object({ + params: z.object({ + workspaceId: z.string().trim() + }), + query: z.object({ + channel: z.string().optional(), + environment: z.string().trim() + }) +}); + +export const PullSecretsServiceTokenV1 = z.object({ + params: z.object({ + workspaceId: z.string().trim() + }), + query: z.object({ + channel: z.string().optional(), + environment: z.string().trim() + }) +}); + +const batchUpdateRequestV2 = z.object({ + _id: z.string(), + folderId: z.string().trim().optional(), + type: z.enum(["shared", "personal"]), + secretName: z.string().trim(), + secretKeyCiphertext: z.string().trim(), + secretKeyIV: z.string().trim(), + secretKeyTag: z.string().trim(), + secretValueCiphertext: z.string().trim(), + secretValueIV: z.string().trim(), + secretValueTag: z.string().trim(), + secretCommentCiphertext: z.string().trim().optional(), + secretCommentIV: z.string().trim().optional(), + secretCommentTag: z.string().trim().optional(), + tags: z + .object({ + _id: z.string().trim(), + name: z.string().trim(), + slug: z.string().trim() + }) + .array() +}); + +export const BatchSecretsV2 = z.object({ + body: z.object({ + workspaceId: z.string().trim(), + folderId: z.string().trim().default("root"), + environment: z.string().trim(), + secretPath: z.string().trim().default("/"), + requests: z + .discriminatedUnion("method", [ + z.object({ + method: z.literal("POST"), + secret: batchUpdateRequestV2.omit({ _id: true }) + }), + z.object({ + method: z.literal("PATCH"), + secret: batchUpdateRequestV2 + }), + z.object({ + method: z.literal("DELETE"), + secret: z.object({ _id: z.string().trim(), secretName: z.string().trim() }) + }) + ]) + .array() + }) +}); + +export const GetSecretsV2 = z.object({ + query: z.object({ + workspaceId: z.string().trim(), + environment: z.string().trim(), + tagSlugs: z.string().trim().optional(), + folderId: z.string().trim().default("root"), + secretPath: z.string().trim().optional(), + include_imports: z + .enum(["true", "false"]) + .default("false") + .transform((value) => value === "true") + }) +}); + +export const GetSecretsRawV3 = z.object({ + query: 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") + .transform((value) => value === "true") + }) +}); + +export const GetSecretByNameRawV3 = z.object({ + params: z.object({ + secretName: z.string().trim() + }), + query: z.object({ + workspaceId: z.string().trim(), + environment: z.string().trim(), + secretPath: z.string().trim().default("/"), + type: z.enum([SECRET_SHARED, SECRET_PERSONAL]).optional() + }) +}); + +export const CreateSecretRawV3 = z.object({ + body: z.object({ + workspaceId: z.string().trim(), + environment: z.string().trim(), + secretPath: z.string().trim().default("/"), + secretValue: z.string().trim(), + secretComment: z.string().trim(), + type: z.enum([SECRET_SHARED, SECRET_PERSONAL]) + }), + params: z.object({ + secretName: z.string().trim() + }) +}); + +export const UpdateSecretByNameRawV3 = z.object({ + params: z.object({ + secretName: z.string().trim() + }), + body: z.object({ + workspaceId: z.string().trim(), + environment: z.string().trim(), + secretValue: z.string().trim(), + secretName: z.string().trim(), + secretPath: z.string().trim().default("/"), + type: z.enum([SECRET_SHARED, SECRET_PERSONAL]).default(SECRET_SHARED) + }) +}); + +export const DeleteSecretByNameRawV3 = z.object({ + params: z.object({ + secretName: z.string().trim() + }), + body: z.object({ + workspaceId: z.string().trim(), + environment: z.string().trim(), + secretPath: z.string().trim().default("/"), + type: z.enum([SECRET_SHARED, SECRET_PERSONAL]).default(SECRET_SHARED) + }) +}); + +export const GetSecretsV3 = z.object({ + query: 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") + .transform((value) => value === "true") + }) +}); + +export const GetSecretByNameV3 = z.object({ + query: z.object({ + workspaceId: z.string().trim(), + environment: z.string().trim(), + secretPath: z.string().trim().default("/"), + type: z.enum([SECRET_SHARED, SECRET_PERSONAL]).optional() + }), + params: z.object({ + secretName: z.string().trim() + }) +}); + +export const CreateSecretV3 = z.object({ + body: z.object({ + workspaceId: z.string().trim(), + environment: z.string().trim(), + type: z.enum([SECRET_SHARED, SECRET_PERSONAL]), + secretPath: z.string().trim().default("/"), + secretKeyCiphertext: z.string().trim(), + secretKeyIV: z.string().trim(), + secretKeyTag: z.string().trim(), + secretValueCiphertext: z.string().trim(), + secretValueIV: z.string().trim(), + secretValueTag: z.string().trim(), + secretCommentCiphertext: z.string().trim().optional(), + secretCommentIV: z.string().trim().optional(), + secretCommentTag: z.string().trim().optional(), + metadata: z.record(z.string()).optional() + }), + params: z.object({ + secretName: z.string().trim() + }) +}); + +export const UpdateSecretByNameV3 = z.object({ + body: z.object({ + workspaceId: z.string().trim(), + environment: z.string().trim(), + type: z.enum([SECRET_SHARED, SECRET_PERSONAL]), + secretPath: z.string().trim().default("/"), + secretValueCiphertext: z.string().trim(), + secretValueIV: z.string().trim(), + secretValueTag: z.string().trim() + }), + params: z.object({ + secretName: z.string() + }) +}); + +export const DeleteSecretByNameV3 = z.object({ + body: z.object({ + workspaceId: z.string().trim(), + environment: z.string().trim(), + type: z.enum([SECRET_SHARED, SECRET_PERSONAL]), + secretPath: z.string().trim().default("/") + }), + params: z.object({ + secretName: z.string() + }) +}); diff --git a/backend/src/validation/serviceTokenData.ts b/backend/src/validation/serviceTokenData.ts index fe8e3bd0b..1379b8f97 100644 --- a/backend/src/validation/serviceTokenData.ts +++ b/backend/src/validation/serviceTokenData.ts @@ -1,14 +1,11 @@ import { Types } from "mongoose"; -import { - ISecret, - IServiceTokenData, - IUser, - ServiceTokenData, -} from "../models"; +import { ISecret, IServiceTokenData, IUser, ServiceTokenData } from "../models"; import { ServiceTokenDataNotFoundError, UnauthorizedRequestError } from "../utils/errors"; import { validateUserClientForWorkspace } from "./user"; import { ActorType } from "../ee/models"; import { AuthData } from "../interfaces/middleware"; +import { z } from "zod"; +import { isValidScope } from "../helpers"; /** * Validate authenticated clients for service token with id [serviceTokenId] based @@ -31,10 +28,11 @@ export const validateClientForServiceTokenData = async ({ .select("+encryptedKey +iv +tag") .populate<{ user: IUser }>("user"); - if (!serviceTokenData) throw ServiceTokenDataNotFoundError({ - message: "Failed to find service token data" - }); - + if (!serviceTokenData) + throw ServiceTokenDataNotFoundError({ + message: "Failed to find service token data" + }); + switch (authData.actor.type) { case ActorType.USER: await validateUserClientForWorkspace({ @@ -65,11 +63,13 @@ export const validateServiceTokenDataClientForWorkspace = async ({ serviceTokenData, workspaceId, environment, + secretPath = "/", requiredPermissions }: { serviceTokenData: IServiceTokenData; workspaceId: Types.ObjectId; environment?: string; + secretPath?: string; requiredPermissions?: string[]; }) => { if (!serviceTokenData.workspace.equals(workspaceId)) { @@ -81,7 +81,6 @@ export const validateServiceTokenDataClientForWorkspace = async ({ if (environment) { // case: environment is specified - if (!serviceTokenData.scopes.find(({ environment: tkEnv }) => tkEnv === environment)) { // case: invalid environment passed throw UnauthorizedRequestError({ @@ -89,6 +88,10 @@ export const validateServiceTokenDataClientForWorkspace = async ({ }); } + if (!isValidScope(serviceTokenData, environment, secretPath)) { + throw UnauthorizedRequestError({ message: "Folder Permission Denied" }); + } + requiredPermissions?.forEach((permission) => { if (!serviceTokenData.permissions.includes(permission)) { throw UnauthorizedRequestError({ @@ -140,3 +143,28 @@ export const validateServiceTokenDataClientForSecrets = async ({ }); }); }; + +export const CreateServiceTokenV2 = z.object({ + body: z.object({ + name: z.string().trim(), + workspaceId: z.string().trim(), + scopes: z + .object({ + environment: z.string().trim(), + secretPath: z.string().trim() + }) + .array() + .min(1), + encryptedKey: z.string().trim(), + iv: z.string().trim(), + tag: z.string().trim(), + expiresIn: z.number(), + permissions: z.enum(["read", "write"]).array() + }) +}); + +export const DeleteServiceTokenV2 = z.object({ + params: z.object({ + serviceTokenDataId: z.string().trim() + }) +}); diff --git a/backend/src/validation/sso.ts b/backend/src/validation/sso.ts new file mode 100644 index 000000000..275ae611e --- /dev/null +++ b/backend/src/validation/sso.ts @@ -0,0 +1,28 @@ +import { z } from "zod"; +import { AuthProvider } from "../ee/models"; + +export const GetSsoConfigv1 = z.object({ + query: z.object({ organizationId: z.string().trim() }) +}); + +export const CreateSsoConfigv1 = z.object({ + body: z.object({ + organizationId: z.string().trim(), + authProvider: z.nativeEnum(AuthProvider), + isActive: z.boolean(), + entryPoint: z.string().trim(), + issuer: z.string().trim(), + cert: z.string().trim() + }) +}); + +export const UpdateSsoConfigv1 = z.object({ + body: z.object({ + organizationId: z.string().trim(), + authProvider: z.nativeEnum(AuthProvider).optional(), + isActive: z.boolean().optional(), + entryPoint: z.string().trim().optional(), + issuer: z.string().trim().optional(), + cert: z.string().trim().optional() + }) +}); diff --git a/backend/src/validation/tags.ts b/backend/src/validation/tags.ts new file mode 100644 index 000000000..0631e9a9a --- /dev/null +++ b/backend/src/validation/tags.ts @@ -0,0 +1,23 @@ +import { z } from "zod"; + +export const GetWorkspaceTagsV2 = z.object({ + params: z.object({ + workspaceId: z.string().trim() + }) +}); + +export const DeleteWorkspaceTagsV2 = z.object({ + params: z.object({ + tagId: z.string().trim() + }) +}); + +export const CreateWorkspaceTagsV2 = z.object({ + params: z.object({ + workspaceId: z.string().trim() + }), + body: z.object({ + name: z.string().trim(), + slug: z.string().trim() + }) +}); diff --git a/backend/src/validation/user.ts b/backend/src/validation/user.ts index f1edad3bf..9f1176f96 100644 --- a/backend/src/validation/user.ts +++ b/backend/src/validation/user.ts @@ -1,39 +1,30 @@ import fs from "fs"; import path from "path"; import { Types } from "mongoose"; -import { - IOrganization, - ISecret, - IServiceAccount, - IUser, - Membership, -} from "../models"; +import { IOrganization, ISecret, IServiceAccount, IUser, Membership } from "../models"; import { validateMembership } from "../helpers/membership"; import _ from "lodash"; import { BadRequestError, UnauthorizedRequestError, ValidationError } from "../utils/errors"; -import { - validateMembershipOrg, -} from "../helpers/membershipOrg"; -import { - PERMISSION_READ_SECRETS, - PERMISSION_WRITE_SECRETS, -} from "../variables"; +import { validateMembershipOrg } from "../helpers/membershipOrg"; +import { PERMISSION_READ_SECRETS, PERMISSION_WRITE_SECRETS } from "../variables"; +import { AuthMethod } from "../models"; +import { z } from "zod"; /** * Validate that email [email] is not disposable * @param email - email to validate */ export const validateUserEmail = (email: string) => { - const emailDomain = email.split("@")[1]; - const disposableEmails = fs.readFileSync( - path.resolve(__dirname, "../data/" + "disposable_emails.txt"), - "utf8" - ).split("\n"); - - if (disposableEmails.includes(emailDomain)) throw ValidationError({ - message: "Failed to validate email as non-disposable", - }); -} + const emailDomain = email.split("@")[1]; + const disposableEmails = fs + .readFileSync(path.resolve(__dirname, "../data/" + "disposable_emails.txt"), "utf8") + .split("\n"); + + if (disposableEmails.includes(emailDomain)) + throw ValidationError({ + message: "Failed to validate email as non-disposable" + }); +}; /** * Validate that user (client) can access workspace @@ -46,48 +37,53 @@ export const validateUserEmail = (email: string) => { * @param {String[]} requiredPermissions - required permissions as part of the endpoint */ export const validateUserClientForWorkspace = async ({ - user, - workspaceId, - environment, - acceptedRoles, - requiredPermissions, + user, + workspaceId, + environment, + acceptedRoles, + requiredPermissions }: { - user: IUser; - workspaceId: Types.ObjectId; - environment?: string; - acceptedRoles: Array<"admin" | "member">; - requiredPermissions?: string[]; + user: IUser; + workspaceId: Types.ObjectId; + environment?: string; + acceptedRoles: Array<"admin" | "member">; + requiredPermissions?: string[]; }) => { - - // validate user membership in workspace - const membership = await validateMembership({ - userId: user._id, - workspaceId, - acceptedRoles, - }); - - let runningIsDisallowed = false; - requiredPermissions?.forEach((requiredPermission: string) => { - switch (requiredPermission) { - case PERMISSION_READ_SECRETS: - runningIsDisallowed = _.some(membership.deniedPermissions, { environmentSlug: environment, ability: PERMISSION_READ_SECRETS }); - break; - case PERMISSION_WRITE_SECRETS: - runningIsDisallowed = _.some(membership.deniedPermissions, { environmentSlug: environment, ability: PERMISSION_WRITE_SECRETS }); - break; - default: - break; - } - - if (runningIsDisallowed) { - throw UnauthorizedRequestError({ - message: `Failed permissions authorization for workspace environment action : ${requiredPermission}`, - }); - } - }); - - return membership; -} + // validate user membership in workspace + const membership = await validateMembership({ + userId: user._id, + workspaceId, + acceptedRoles + }); + + let runningIsDisallowed = false; + requiredPermissions?.forEach((requiredPermission: string) => { + switch (requiredPermission) { + case PERMISSION_READ_SECRETS: + runningIsDisallowed = _.some(membership.deniedPermissions, { + environmentSlug: environment, + ability: PERMISSION_READ_SECRETS + }); + break; + case PERMISSION_WRITE_SECRETS: + runningIsDisallowed = _.some(membership.deniedPermissions, { + environmentSlug: environment, + ability: PERMISSION_WRITE_SECRETS + }); + break; + default: + break; + } + + if (runningIsDisallowed) { + throw UnauthorizedRequestError({ + message: `Failed permissions authorization for workspace environment action : ${requiredPermission}` + }); + } + }); + + return membership; +}; /** * Validate that user (client) can access secret [secret] @@ -98,32 +94,35 @@ export const validateUserClientForWorkspace = async ({ * @param {String[]} requiredPermissions - required permissions as part of the endpoint */ export const validateUserClientForSecret = async ({ - user, - secret, - acceptedRoles, - requiredPermissions, + user, + secret, + acceptedRoles, + requiredPermissions }: { - user: IUser; - secret: ISecret; - acceptedRoles?: Array<"admin" | "member">; - requiredPermissions?: string[]; + user: IUser; + secret: ISecret; + acceptedRoles?: Array<"admin" | "member">; + requiredPermissions?: string[]; }) => { - const membership = await validateMembership({ - userId: user._id, - workspaceId: secret.workspace, - acceptedRoles, - }); - - if (requiredPermissions?.includes(PERMISSION_WRITE_SECRETS)) { - const isDisallowed = _.some(membership.deniedPermissions, { environmentSlug: secret.environment, ability: PERMISSION_WRITE_SECRETS }); + const membership = await validateMembership({ + userId: user._id, + workspaceId: secret.workspace, + acceptedRoles + }); - if (isDisallowed) { - throw UnauthorizedRequestError({ - message: "You do not have the required permissions to perform this action", - }); - } - } -} + if (requiredPermissions?.includes(PERMISSION_WRITE_SECRETS)) { + const isDisallowed = _.some(membership.deniedPermissions, { + environmentSlug: secret.environment, + ability: PERMISSION_WRITE_SECRETS + }); + + if (isDisallowed) { + throw UnauthorizedRequestError({ + message: "You do not have the required permissions to perform this action" + }); + } + } +}; /** * Validate that user (client) can access secrets [secrets] @@ -134,41 +133,44 @@ export const validateUserClientForSecret = async ({ * @param {String[]} requiredPermissions - required permissions as part of the endpoint */ export const validateUserClientForSecrets = async ({ - user, - secrets, - requiredPermissions, + user, + secrets, + requiredPermissions }: { - user: IUser; - secrets: ISecret[]; - requiredPermissions?: string[]; + user: IUser; + secrets: ISecret[]; + requiredPermissions?: string[]; }) => { - - // TODO: add acceptedRoles? + // TODO: add acceptedRoles? - const userMemberships = await Membership.find({ user: user._id }) - const userMembershipById = _.keyBy(userMemberships, "workspace"); - const workspaceIdsSet = new Set(userMemberships.map((m) => m.workspace.toString())); + const userMemberships = await Membership.find({ user: user._id }); + const userMembershipById = _.keyBy(userMemberships, "workspace"); + const workspaceIdsSet = new Set(userMemberships.map((m) => m.workspace.toString())); - // for each secret check if the secret belongs to a workspace the user is a member of - secrets.forEach((secret: ISecret) => { - if (!workspaceIdsSet.has(secret.workspace.toString())) { - throw BadRequestError({ - message: "Failed authorization for the secret", - }); - } + // for each secret check if the secret belongs to a workspace the user is a member of + secrets.forEach((secret: ISecret) => { + if (!workspaceIdsSet.has(secret.workspace.toString())) { + throw BadRequestError({ + message: "Failed authorization for the secret" + }); + } - if (requiredPermissions?.includes(PERMISSION_WRITE_SECRETS)) { - const deniedMembershipPermissions = userMembershipById[secret.workspace.toString()].deniedPermissions; - const isDisallowed = _.some(deniedMembershipPermissions, { environmentSlug: secret.environment, ability: PERMISSION_WRITE_SECRETS }); + if (requiredPermissions?.includes(PERMISSION_WRITE_SECRETS)) { + const deniedMembershipPermissions = + userMembershipById[secret.workspace.toString()].deniedPermissions; + const isDisallowed = _.some(deniedMembershipPermissions, { + environmentSlug: secret.environment, + ability: PERMISSION_WRITE_SECRETS + }); - if (isDisallowed) { - throw UnauthorizedRequestError({ - message: "You do not have the required permissions to perform this action", - }); - } - } - }); -} + if (isDisallowed) { + throw UnauthorizedRequestError({ + message: "You do not have the required permissions to perform this action" + }); + } + } + }); +}; /** * Validate that user (client) can access service account [serviceAccount] @@ -179,25 +181,25 @@ export const validateUserClientForSecrets = async ({ * @param {String[]} requiredPermissions - required permissions as part of the endpoint */ export const validateUserClientForServiceAccount = async ({ - user, - serviceAccount, - requiredPermissions, + user, + serviceAccount, + requiredPermissions }: { - user: IUser; - serviceAccount: IServiceAccount; - requiredPermissions?: string[]; + user: IUser; + serviceAccount: IServiceAccount; + requiredPermissions?: string[]; }) => { - if (!serviceAccount.user.equals(user._id)) { - // case: user who created service account is not the - // same user that is on the request - await validateMembershipOrg({ - userId: user._id, - organizationId: serviceAccount.organization, - acceptedRoles: [], - acceptedStatuses: [], - }); - } -} + if (!serviceAccount.user.equals(user._id)) { + // case: user who created service account is not the + // same user that is on the request + await validateMembershipOrg({ + userId: user._id, + organizationId: serviceAccount.organization, + acceptedRoles: [], + acceptedStatuses: [] + }); + } +}; /** * Validate that user (client) can access organization [organization] @@ -206,22 +208,54 @@ export const validateUserClientForServiceAccount = async ({ * @param {Organization} obj.organization - organization to validate against */ export const validateUserClientForOrganization = async ({ - user, - organization, - acceptedRoles, - acceptedStatuses, + user, + organization, + acceptedRoles, + acceptedStatuses }: { - user: IUser; - organization: IOrganization; - acceptedRoles: Array<"owner" | "admin" | "member">; - acceptedStatuses: Array<"invited" | "accepted">; + user: IUser; + organization: IOrganization; + acceptedRoles: Array<"owner" | "admin" | "member">; + acceptedStatuses: Array<"invited" | "accepted">; }) => { - const membershipOrg = await validateMembershipOrg({ - userId: user._id, - organizationId: organization._id, - acceptedRoles, - acceptedStatuses, - }); - - return membershipOrg; -} \ No newline at end of file + const membershipOrg = await validateMembershipOrg({ + userId: user._id, + organizationId: organization._id, + acceptedRoles, + acceptedStatuses + }); + + return membershipOrg; +}; + +export const UpdateMyMfaEnabledV2 = z.object({ + body: z.object({ + isMfaEnabled: z.boolean() + }) +}); + +export const UpdateNameV2 = z.object({ + body: z.object({ + firstName: z.string().trim(), + lastName: z.string().trim() + }) +}); + +export const UpdateAuthMethodsV2 = z.object({ + body: z.object({ + authMethods: z.nativeEnum(AuthMethod).array().min(1) + }) +}); + +export const CreateApiKeyV2 = z.object({ + body: z.object({ + name: z.string().trim(), + expiresIn: z.number() + }) +}); + +export const DeleteApiKeyV2 = z.object({ + params: z.object({ + apiKeyDataId: z.string().trim() + }) +}); diff --git a/backend/src/validation/webhooks.ts b/backend/src/validation/webhooks.ts new file mode 100644 index 000000000..907f90633 --- /dev/null +++ b/backend/src/validation/webhooks.ts @@ -0,0 +1,40 @@ +import { z } from "zod"; + +export const CreateWebhookV1 = z.object({ + body: z.object({ + workspaceId: z.string().trim(), + environment: z.string().trim(), + webhookUrl: z.string().url().trim(), + webhookSecretKey: z.string().trim().optional(), + secretPath: z.string().trim().default("/") + }) +}); + +export const UpdateWebhookV1 = z.object({ + params: z.object({ + webhookId: z.string().trim() + }), + body: z.object({ + isDisabled: z.boolean().default(false) + }) +}); + +export const TestWebhookV1 = z.object({ + params: z.object({ + webhookId: z.string().trim() + }) +}); + +export const DeleteWebhookV1 = z.object({ + params: z.object({ + webhookId: z.string().trim() + }) +}); + +export const ListWebhooksV1 = z.object({ + query: z.object({ + workspaceId: z.string().trim(), + environment: z.string().trim().optional(), + secretPath: z.string().trim().optional() + }) +}); diff --git a/backend/src/validation/workspace.ts b/backend/src/validation/workspace.ts index 3be8d0dad..44044d923 100644 --- a/backend/src/validation/workspace.ts +++ b/backend/src/validation/workspace.ts @@ -1,25 +1,12 @@ -import net from "net"; import { Types } from "mongoose"; -import { - IServiceTokenData, - IUser, - SecretBlindIndexData, - Workspace, -} from "../models"; -import { - ActorType, - TrustedIP -} from "../ee/models"; +import { IServiceTokenData, IUser, Workspace } from "../models"; +import { ActorType } from "../ee/models"; import { validateUserClientForWorkspace } from "./user"; import { validateServiceTokenDataClientForWorkspace } from "./serviceTokenData"; -import { - BadRequestError, - UnauthorizedRequestError, - WorkspaceNotFoundError, -} from "../utils/errors"; -import { BotService } from "../services"; +import { WorkspaceNotFoundError } from "../utils/errors"; import { AuthData } from "../interfaces/middleware"; -import { extractIPDetails } from "../utils/ip"; +import { z } from "zod"; +import { EventType, UserAgentType } from "../ee/models"; /** * Validate authenticated clients for workspace with id [workspaceId] based @@ -32,106 +19,283 @@ import { extractIPDetails } from "../utils/ip"; * @param {String[]} obj.requiredPermissions - required permissions as part of the endpoint */ export const validateClientForWorkspace = async ({ - authData, - workspaceId, - environment, - acceptedRoles, - requiredPermissions, - requireBlindIndicesEnabled, - requireE2EEOff, - checkIPAllowlist + authData, + workspaceId, + environment, + acceptedRoles, + requiredPermissions }: { - authData: AuthData; - workspaceId: Types.ObjectId; - environment?: string; - acceptedRoles: Array<"admin" | "member">; - requiredPermissions?: string[]; - requireBlindIndicesEnabled: boolean; - requireE2EEOff: boolean; - checkIPAllowlist: boolean; + authData: AuthData; + workspaceId: Types.ObjectId; + environment?: string; + acceptedRoles: Array<"admin" | "member">; + requiredPermissions?: string[]; }) => { - const workspace = await Workspace.findById(workspaceId); + const workspace = await Workspace.findById(workspaceId); - if (!workspace) throw WorkspaceNotFoundError({ - message: "Failed to find workspace", - }); + if (!workspace) throw WorkspaceNotFoundError({ + message: "Failed to find workspace" + }); - if (requireBlindIndicesEnabled) { - // case: blind indices are not enabled for secrets in this workspace - // (i.e. workspace was created before blind indices were introduced - // and no admin has enabled it) - - const secretBlindIndexData = await SecretBlindIndexData.exists({ - workspace: new Types.ObjectId(workspaceId), - }); - - if (!secretBlindIndexData) throw UnauthorizedRequestError({ - message: "Failed workspace authorization due to blind indices not being enabled", - }); - } - - if (requireE2EEOff) { - const isWorkspaceE2EE = await BotService.getIsWorkspaceE2EE(workspaceId); - - if (isWorkspaceE2EE) throw BadRequestError({ - message: "Failed workspace authorization due to end-to-end encryption not being disabled", - }); - } - - let membership; - switch (authData.actor.type) { - case ActorType.USER: - membership = await validateUserClientForWorkspace({ - user: authData.authPayload as IUser, - workspaceId, - environment, - acceptedRoles, - requiredPermissions, - }); - - return ({ membership, workspace }); - case ActorType.SERVICE: - if (checkIPAllowlist) { - const trustedIps = await TrustedIP.find({ - workspace: workspaceId - }); - - if (trustedIps.length > 0) { - // case: check the IP address of the inbound request against trusted IPs + let membership; + switch (authData.actor.type) { + case ActorType.USER: + membership = await validateUserClientForWorkspace({ + user: authData.authPayload as IUser, + workspaceId, + environment, + acceptedRoles, + requiredPermissions + }); - const blockList = new net.BlockList(); - - for (const trustedIp of trustedIps) { - if (trustedIp.prefix !== undefined) { - blockList.addSubnet( - trustedIp.ipAddress, - trustedIp.prefix, - trustedIp.type - ); - } else { - blockList.addAddress( - trustedIp.ipAddress, - trustedIp.type - ); - } - } - - const { type } = extractIPDetails(authData.ipAddress); - const check = blockList.check(authData.ipAddress, type); - - if (!check) throw UnauthorizedRequestError({ - message: "Failed workspace authorization" - }); - } - } + return { membership, workspace }; + case ActorType.SERVICE: + await validateServiceTokenDataClientForWorkspace({ + serviceTokenData: authData.authPayload as IServiceTokenData, + workspaceId, + environment, + requiredPermissions + }); - await validateServiceTokenDataClientForWorkspace({ - serviceTokenData: authData.authPayload as IServiceTokenData, - workspaceId, - environment, - requiredPermissions, - }); - - return {}; - } -} + return {}; + } +}; + +export const GetWorkspaceSecretSnapshotsV1 = z.object({ + params: z.object({ + workspaceId: z.string().trim() + }), + query: z.object({ + environment: z.string().trim(), + folderId: z.string().trim().default("root"), + offset: z.coerce.number(), + limit: z.coerce.number() + }) +}); + +export const GetWorkspaceSecretSnapshotsCountV1 = z.object({ + params: z.object({ + workspaceId: z.string().trim() + }), + query: z.object({ + environment: z.string().trim(), + folderId: z.string().trim().default("root") + }) +}); + +export const RollbackWorkspaceSecretSnapshotV1 = z.object({ + params: z.object({ + workspaceId: z.string().trim() + }), + body: z.object({ + environment: z.string().trim(), + folderId: z.string().trim().default("root"), + version: z.number() + }) +}); + +export const GetWorkspaceLogsV1 = z.object({ + params: z.object({ + workspaceId: z.string().trim() + }), + query: z.object({ + offset: z.coerce.number(), + limit: z.coerce.number(), + sortBy: z.string().trim().optional(), + userId: z.string().trim().optional(), + actionNames: z.string().trim().optional() + }) +}); + +export const GetWorkspaceAuditLogsV1 = z.object({ + params: z.object({ + workspaceId: z.string().trim() + }), + query: z.object({ + eventType: z.nativeEnum(EventType).nullable().optional(), + userAgentType: z.nativeEnum(UserAgentType).nullable().optional(), + startDate: z.string().datetime().nullable().optional(), + endDate: z.string().datetime().nullable().optional(), + offset: z.coerce.number(), + limit: z.coerce.number(), + actor: z.string().nullish().optional() + }) +}); + +export const GetWorkspaceAuditLogActorFilterOptsV1 = z.object({ + params: z.object({ + workspaceId: z.string().trim() + }) +}); + +export const GetWorkspaceTrustedIpsV1 = z.object({ + params: z.object({ + workspaceId: z.string().trim() + }) +}); + +export const AddWorkspaceTrustedIpV1 = z.object({ + params: z.object({ + workspaceId: z.string().trim() + }), + body: z.object({ + ipAddress: z.string().trim(), + comment: z.string().trim().default(""), + isActive: z.boolean() + }) +}); + +export const UpdateWorkspaceTrustedIpV1 = z.object({ + params: z.object({ + workspaceId: z.string().trim(), + trustedIpId: z.string().trim() + }), + body: z.object({ + ipAddress: z.string().trim(), + comment: z.string().trim().default("") + }) +}); + +export const DeleteWorkspaceTrustedIpV1 = z.object({ + params: z.object({ + workspaceId: z.string().trim(), + trustedIpId: z.string().trim() + }) +}); + +export const GetWorkspacePublicKeysV1 = z.object({ + params: z.object({ + workspaceId: z.string().trim() + }) +}); + +export const GetWorkspaceMembershipsV1 = z.object({ + params: z.object({ + workspaceId: z.string().trim() + }) +}); + +export const GetWorkspaceV1 = z.object({ + params: z.object({ + workspaceId: z.string().trim() + }) +}); + +export const CreateWorkspaceV1 = z.object({ + body: z.object({ + workspaceName: z.string().trim(), + organizationId: z.string().trim() + }) +}); + +export const DeleteWorkspaceV1 = z.object({ + params: z.object({ + workspaceId: z.string().trim() + }) +}); + +export const ChangeWorkspaceNameV1 = z.object({ + params: z.object({ + workspaceId: z.string().trim() + }), + body: z.object({ + name: z.string().trim() + }) +}); + +export const InviteUserToWorkspaceV1 = z.object({ + params: z.object({ + workspaceId: z.string().trim() + }), + body: z.object({ + email: z.string().trim() + }) +}); + +export const GetWorkspaceIntegrationsV1 = z.object({ + params: z.object({ + workspaceId: z.string().trim() + }) +}); + +export const GetWorkspaceIntegrationAuthorizationsV1 = z.object({ + params: z.object({ + workspaceId: z.string().trim() + }) +}); + +export const GetWorkspaceServiceTokensV1 = z.object({ + params: z.object({ + workspaceId: z.string().trim() + }) +}); + +export const GetWorkspaceServiceTokenDataV2 = z.object({ + params: z.object({ + workspaceId: z.string().trim() + }) +}); + +export const GetWorkspaceKeyV2 = z.object({ + params: z.object({ + workspaceId: z.string().trim() + }) +}); + +export const GetWorkspaceMembershipsV2 = z.object({ + params: z.object({ + workspaceId: z.string().trim() + }) +}); + +export const UpdateWorkspaceMembershipsV2 = z.object({ + params: z.object({ + workspaceId: z.string().trim(), + membershipId: z.string().trim() + }), + body: z.object({ + role: z.string().trim() + }) +}); + +export const DeleteWorkspaceMembershipsV2 = z.object({ + params: z.object({ + workspaceId: z.string().trim(), + membershipId: z.string().trim() + }) +}); + +export const ToggleAutoCapitalizationV2 = z.object({ + params: z.object({ + workspaceId: z.string().trim() + }), + body: z.object({ + autoCapitalization: z.string().trim() + }) +}); + +export const GetWorkspaceBlinkIndexStatusV3 = z.object({ + params: z.object({ + workspaceId: z.string().trim() + }) +}); + +export const GetWorkspaceSecretsV3 = z.object({ + params: z.object({ + workspaceId: z.string().trim() + }) +}); + +export const NameWorkspaceSecretsV3 = z.object({ + params: z.object({ + workspaceId: z.string().trim() + }), + body: z.object({ + secretsToUpdate: z + .object({ + secretName: z.string().trim(), + _id: z.string().trim() + }) + .array() + }) +}); diff --git a/backend/src/variables/organization.ts b/backend/src/variables/organization.ts index 4f5620236..bccabc6e6 100644 --- a/backend/src/variables/organization.ts +++ b/backend/src/variables/organization.ts @@ -2,9 +2,11 @@ export const OWNER = "owner"; export const ADMIN = "admin"; export const MEMBER = "member"; +export const VIEWER = "viewer"; +export const CUSTOM = "custom"; // membership statuses export const INVITED = "invited"; // -- organization -export const ACCEPTED = "accepted"; \ No newline at end of file +export const ACCEPTED = "accepted"; diff --git a/backend/swagger/index.ts b/backend/swagger/index.ts index 35ed20790..707abbfde 100644 --- a/backend/swagger/index.ts +++ b/backend/swagger/index.ts @@ -223,7 +223,8 @@ const generateOpenAPISpec = async () => { const endpointsFiles = ["../src/index.ts"]; const spec = await swaggerAutogen(outputJSONFile, endpointsFiles, doc); + await fs.writeFile(outputYAMLFile, yaml.dump(spec.data)); } -generateOpenAPISpec(); +generateOpenAPISpec(); \ No newline at end of file diff --git a/docs/api-reference/endpoints/folders/create.mdx b/docs/api-reference/endpoints/folders/create.mdx new file mode 100644 index 000000000..397f43cb5 --- /dev/null +++ b/docs/api-reference/endpoints/folders/create.mdx @@ -0,0 +1,4 @@ +--- +title: "Create" +openapi: "POST /api/v1/folders/" +--- \ No newline at end of file diff --git a/docs/api-reference/endpoints/folders/delete.mdx b/docs/api-reference/endpoints/folders/delete.mdx new file mode 100644 index 000000000..2147443e5 --- /dev/null +++ b/docs/api-reference/endpoints/folders/delete.mdx @@ -0,0 +1,4 @@ +--- +title: "Delete" +openapi: "DELETE /api/v1/folders/{folderId}" +--- \ No newline at end of file diff --git a/docs/api-reference/endpoints/folders/list.mdx b/docs/api-reference/endpoints/folders/list.mdx new file mode 100644 index 000000000..c467c5975 --- /dev/null +++ b/docs/api-reference/endpoints/folders/list.mdx @@ -0,0 +1,4 @@ +--- +title: "List" +openapi: "GET /api/v1/folders/" +--- \ No newline at end of file diff --git a/docs/api-reference/endpoints/folders/update.mdx b/docs/api-reference/endpoints/folders/update.mdx new file mode 100644 index 000000000..fe8dd6cd8 --- /dev/null +++ b/docs/api-reference/endpoints/folders/update.mdx @@ -0,0 +1,4 @@ +--- +title: "Update" +openapi: "PATCH /api/v1/folders/{folderId}" +--- \ No newline at end of file diff --git a/docs/api-reference/endpoints/secret-imports/create.mdx b/docs/api-reference/endpoints/secret-imports/create.mdx new file mode 100644 index 000000000..2c823e528 --- /dev/null +++ b/docs/api-reference/endpoints/secret-imports/create.mdx @@ -0,0 +1,4 @@ +--- +title: "Create" +openapi: "POST /api/v1/secret-imports/" +--- \ No newline at end of file diff --git a/docs/api-reference/endpoints/secret-imports/delete.mdx b/docs/api-reference/endpoints/secret-imports/delete.mdx new file mode 100644 index 000000000..c7da4f6d0 --- /dev/null +++ b/docs/api-reference/endpoints/secret-imports/delete.mdx @@ -0,0 +1,4 @@ +--- +title: "Delete" +openapi: "DELETE /api/v1/secret-imports/{id}" +--- \ No newline at end of file diff --git a/docs/api-reference/endpoints/secret-imports/list.mdx b/docs/api-reference/endpoints/secret-imports/list.mdx new file mode 100644 index 000000000..2de41b5d7 --- /dev/null +++ b/docs/api-reference/endpoints/secret-imports/list.mdx @@ -0,0 +1,4 @@ +--- +title: "List" +openapi: "GET /api/v1/secret-imports/" +--- \ No newline at end of file diff --git a/docs/api-reference/endpoints/secret-imports/update.mdx b/docs/api-reference/endpoints/secret-imports/update.mdx new file mode 100644 index 000000000..76c8a8feb --- /dev/null +++ b/docs/api-reference/endpoints/secret-imports/update.mdx @@ -0,0 +1,4 @@ +--- +title: "Update" +openapi: "PUT /api/v1/secret-imports/{id}" +--- \ No newline at end of file diff --git a/docs/api-reference/endpoints/secrets/create.mdx b/docs/api-reference/endpoints/secrets/create.mdx index 85d8ed350..9f6b65875 100644 --- a/docs/api-reference/endpoints/secrets/create.mdx +++ b/docs/api-reference/endpoints/secrets/create.mdx @@ -5,7 +5,6 @@ openapi: "POST /api/v3/secrets/{secretName}" Using this route requires understanding Infisical's system and cryptography. - It may be helpful to read through the - [introduction](/api-reference/overview/introduction) and [guide for creating - secrets](/api-reference/overview/examples/create-secret). + You should consult the [examples](https://infisical.com/docs/api-reference/overview/examples/note) for how to use + this endpoint. diff --git a/docs/api-reference/endpoints/secrets/delete.mdx b/docs/api-reference/endpoints/secrets/delete.mdx index d4084a465..0ab8e1309 100644 --- a/docs/api-reference/endpoints/secrets/delete.mdx +++ b/docs/api-reference/endpoints/secrets/delete.mdx @@ -1,4 +1,4 @@ --- title: "Delete" openapi: "DELETE /api/v3/secrets/{secretName}" ---- +--- \ No newline at end of file diff --git a/docs/api-reference/endpoints/secrets/list.mdx b/docs/api-reference/endpoints/secrets/list.mdx index eaa4597fb..377c6db86 100644 --- a/docs/api-reference/endpoints/secrets/list.mdx +++ b/docs/api-reference/endpoints/secrets/list.mdx @@ -5,7 +5,7 @@ openapi: "GET /api/v3/secrets/" Using this route requires understanding Infisical's system and cryptography. - It may be helpful to read through the - [introduction](/api-reference/overview/introduction) and [guide for retrieving - secrets](/api-reference/overview/examples/retrieve-secret). + You should consult the [examples](https://infisical.com/docs/api-reference/overview/examples/note) for how to use + this endpoint. + diff --git a/docs/api-reference/endpoints/secrets/read.mdx b/docs/api-reference/endpoints/secrets/read.mdx index 6dbbcd726..e472efccd 100644 --- a/docs/api-reference/endpoints/secrets/read.mdx +++ b/docs/api-reference/endpoints/secrets/read.mdx @@ -5,7 +5,6 @@ openapi: "GET /api/v3/secrets/{secretName}" Using this route requires understanding Infisical's system and cryptography. - It may be helpful to read through the - [introduction](/api-reference/overview/introduction) and [guide for retrieving - secrets](/api-reference/overview/examples/retrieve-secret). + You should consult the [examples](https://infisical.com/docs/api-reference/overview/examples/note) for how to use + this endpoint. diff --git a/docs/api-reference/endpoints/secrets/update.mdx b/docs/api-reference/endpoints/secrets/update.mdx index 594d8201c..c0bdcb94f 100644 --- a/docs/api-reference/endpoints/secrets/update.mdx +++ b/docs/api-reference/endpoints/secrets/update.mdx @@ -5,7 +5,7 @@ openapi: "PATCH /api/v3/secrets/{secretName}" Using this route requires understanding Infisical's system and cryptography. - It may be helpful to read through the - [introduction](/api-reference/overview/introduction) and [guide for updating - secrets](/api-reference/overview/examples/update-secret). + You should consult the [examples](https://infisical.com/docs/api-reference/overview/examples/note) for how to use + this endpoint. + diff --git a/docs/images/email-resend-create-domain.png b/docs/images/email-resend-create-domain.png new file mode 100644 index 000000000..9a469e87a Binary files /dev/null and b/docs/images/email-resend-create-domain.png differ diff --git a/docs/images/email-resend-create-key.png b/docs/images/email-resend-create-key.png new file mode 100644 index 000000000..b0b4265a5 Binary files /dev/null and b/docs/images/email-resend-create-key.png differ diff --git a/docs/images/email-resend-smtp-settings.png b/docs/images/email-resend-smtp-settings.png new file mode 100644 index 000000000..df678259e Binary files /dev/null and b/docs/images/email-resend-smtp-settings.png differ diff --git a/docs/images/internals/architecture.png b/docs/images/internals/architecture.png new file mode 100644 index 000000000..4f955b93a Binary files /dev/null and b/docs/images/internals/architecture.png differ diff --git a/docs/internals/components.mdx b/docs/internals/components.mdx new file mode 100644 index 000000000..3ec9adb77 --- /dev/null +++ b/docs/internals/components.mdx @@ -0,0 +1,30 @@ +--- +title: "Components" +description: "Infisical's components span multiple clients, an API, and a storage backend" +--- + +## Infisical API + +The Infisical API (sometimes referred to as the **backend**) contains the core platform logic. + +## Storage backend + +Infisical relies on a storage backend to store data including users and secrets. + +Currently, the only supported storage backend is [MongoDB](https://www.mongodb.com) but we plan to add support for other options including PostgreSQL in Q1 2024. + +## Redis + +Infisical uses [Redis](https://redis.com) to enable more complex workflows including a queuing system to manage long running asynchronous tasks, cron jobs, as well as reliable cache for frequently used resources. + +## Infisical Web UI + +The Web UI is the browser-based portal that connects to the Infisical API. + +## Infisical clients + +Clients are any application or infrastructure that connecting to the Infisical API using one of the below methods: +- Public API: Making API requests directly to the Infisical API. +- Client SDK: A platform-specific library with method abstractions for working with secrets. Currently, there are two official SDKs: [Node SDK](https://github.com/Infisical/infisical-node) and [Python SDK](https://github.com/Infisical/infisical-python). +- CLI: A terminal-based interface for interacting with the Infisical API. +- Kubernetes Operator: This operator retrieves secrets from Infisical and securely store \ No newline at end of file diff --git a/docs/internals/flows.mdx b/docs/internals/flows.mdx new file mode 100644 index 000000000..e18a37a31 --- /dev/null +++ b/docs/internals/flows.mdx @@ -0,0 +1,97 @@ +--- +title: "Flows" +description: "Infisical's core flows have strong cryptographic underpinnings" +--- + +## Signup + +When a user signs up for an account using email/password, they verify their email by correctly entering the 6-digit OTP code sent to it. + +After this procedure, the user creates a password that is checked against strict requirements to ensure that it has sufficient entropy; this is critical because passwords have both authentication-related and cryptographic implications in Infisical. In accordance to the [secure remote password protocol (SRP)](https://en.wikipedia.org/wiki/Secure_Remote_Password_protocol), the password is used to generate a salt and X; this is kept handy on the client side. + +Next, a few user-associated symmetric keys are generated for subsequent use: + +- The password is transformed into a 256-bit symmetric key, called the generated key, using the [Argon2id](https://en.wikipedia.org/wiki/Argon2) key derivation function. +- A 256-bit symmetric key, called the protected key, is generated. +- A public-private key pair is generated. + +The symmetric keys are used in sequence to encrypt the user’s private key: + +- The protected key is used to encrypt the private key. +- The generated key is used to encrypt the protected key. + +Finally, the encrypted private key, the protected key, salt, and X are sent to the Infisical API to be stored in the storage backend. Note that the top-level secret used to secure the user’s account and private key is their password. Therefore, it must be unknown to the Infisical API and strong by nature. + +## Login + +When a user logs in, they enter their password to authenticate with Infisical via SRP. If successful, the encrypted protected key and encrypted private key are returned to the client side. + +The password is then used in reverse sequence to decrypt the private key: + +- The password is transformed back into the generated key. +- The generated key is used to decrypt the encrypted protected key. +- The protected key is used to decrypt the encrypted private key. + +The private key is stored on the client side and kept handy. + +## Single sign-on + +When a SSO authentication method like Google, GitHub, or SAML SSO is used to login or signup to Infisical, the process is identical to logging in with email/password except that it is contingent on first successfully logging in via the authentication provider. This means, for example, a user with Google SSO enabled must first log in with Google and then enter their password for Infisical to complete logging into the platform. + +This approach implies that the user’s password assumes only the role of a master decryption key or secret. It also ensures that the authentication provider does not know this top-level secret, keeping the platform zero-knowledge as intended. + +## Account recovery + +When a user signs up for Infisical, they are issued a backup PDF containing a symmetric key that can be used to recover their account by decrypting a copy of that user’s private key; using the backup PDF is the only way to recover a user’s account in the event of a lockout - this is intentional by design of Infisical’s zero-knowledge architecture. + +We strongly encourage all users to download, print, and keep their backup PDFs in a secure location. + +## Secrets + +In Infisical, secrets belong to environments in projects, and projects belong to organizations. Each project can be thought of as a vault and has its own symmetric key, called the project key. The project key is used to encrypt the secrets contained in that project. + +Similar to each user’s private key, the project key is sensitive and must remain unknown to the server to preserve the zero-knowledge aspect of Infisical; knowledge of the project key would allow the server to decrypt the secrets of that project which would be undesirable if the server is compromised. + +In order to preserve the zero-knowledge aspect of Infisical, each project key is encrypted on the client side before being sent to the server. More specifically, for each project, we make copies of its project key for each member of that project; each copy is encrypted under that member’s public key and only then sent off to the server for storage. A few relevant sequences: + +- The initial member of a project generates its project key, encrypts it under their public key, and uploads it to the server for storage. +- When a new member is added to the project, an existing member of the project (e.g. the initial member) fetches their copy of the project key, decrypts that copy, encrypts it under the public key of the new member, and uploads it to the server for storage. +- When a member is removed from a project, their copy of the project key is hard deleted from the storage backend. + +When dealing with secrets, this implies a specific sequence of decryption/encryption steps to fetch and create/update them. Assuming that we’re dealing with the Infisical Web UI, let’s start with fetching secrets which happens after the user logs in and selects a project: + +- The user fetches encrypted secrets back to the client side. +- The user also fetches the encrypted project key, encrypted under their public key, for these secrets. +- The encrypted project key is decrypted by the user’s private key which is kept handy on the client side. +- The project key is finally used to decrypt the secrets belonging to the project. +- The secrets are displayed to the user in the Infisical Web UI. + +Similarly, when a user creates/updates a secret, the reverse sequence is performed: + +- The user fetches the encrypted project key, encrypted under the user’s public key. +- The project key is decrypted by the user’s private key which is kept handy on the client side. +- The user encrypts the new/updated secret under the project key. +- The user sends the new/updated secret to the server for storage. + +These sequences are performed across various Infisical clients including the web UI, CLI, SDKs, and K8s operators when dealing with the Infisical API. They are also relevant in the implementations of Infisical’s versioning features like secret versions and snapshots. + +## Native integrations + +Previously, we mentioned that Infisical is zero-knowledge; this is partly true because Infisical can be used this way. Under certain circumstances, however, a user can explicitly share their copy of the project key with the server to enable more advanced features like native integrations. + +The way a project key is shared with Infisical is via an abstraction that we call a bot. Each project has a bot with a public-private key pair generated on the server; the private key of each bot is symmetrically encrypted by the root encryption key of the server. This implies a few things: + +- The server may partake in the sharing of project keys via its own public-private keys bound to each project bot. +- The server root encryption key must be kept secure. + +With that, let’s discuss native integrations. A native integrations is a connection between Infisical and a target platform like GitHub, GitLab, or Vercel that allows secrets to be synced from Infisical to the target platform using its API. Since native integrations require secrets to be sent over in plaintext, they require the server to have access to the secrets. The sequence for how integrations are implemented is fairly simple: + +- A user explicitly shares copy of the project key with the server via the Infisical Web UI. In this step, the user fetches the public key of the bot assigned to that project, encrypts the project key under that public key, and sends it back to the server. +- The user selects a target platform to integrate with their project and enters details such as the source environment within the project to send secrets from as well as the project and environment in the target platform to sync secrets to. +- The user creates the integration, triggering the first sync wherein Infisical decrypts the project’s key, uses it to decrypt the secrets of that project, and sends the secrets to the target platform. +- Finally, on any subsequent mutations applied to the source environment of an active integration, Infisical automatically triggers a re-sync to the target platform. This keeps Infisical as a ground source-of-truth for a team’s secrets. + +## Resources + +- For in depth details, consult the code. +- To get started with Infisical, try out the [Getting Started](https://infisical.com/docs/documentation/getting-started/introduction) overview. \ No newline at end of file diff --git a/docs/internals/overview.mdx b/docs/internals/overview.mdx new file mode 100644 index 000000000..d4ec9a564 --- /dev/null +++ b/docs/internals/overview.mdx @@ -0,0 +1,37 @@ +--- +title: "Overview" +description: "How Infisical works under the hood" +--- + +This section covers the internals of Infisical including its technical underpinnings, architecture, and security properties. + + + Knowledge of this section is recommended but not required to use Infisical. However, if you're operating Infisical, we recommend understanding the internals. + + +## Learn More + + + + Learn about the fundamental parts of Infisical + + + Find out more about the structure of core user flows in Infisical + + + Read about most common security-related topics and questions + + + Learn best practices for utilizing Infisical sevrice tokens + + diff --git a/docs/internals/security.mdx b/docs/internals/security.mdx new file mode 100644 index 000000000..1da0bc73d --- /dev/null +++ b/docs/internals/security.mdx @@ -0,0 +1,175 @@ +--- +title: "Security" +description: "Infisical's security model includes many considerations and initiatives" +--- + +Given that Infisical is a secret management platform that manages sensitive data, the Infisical security model is very important. +The goal of Infisical's security model is to ensure the security and integrity of all of its managed data as well as all associated operations. + +This means that data at rest and in transit must be secure from eavesdropping or tampering. All clients must be authenticated and authorized to access data. Additionally, all interactions must be auditable and traced uniquely back to their source. + +## Threat model + +Infisical’s threat model spans communication, storage, response mechanisms, failover strategies, and more. + +- Eavesdropping on communications: Infisical ensures end-to-end encryption for all client interactions with the Infisical API. +- Tampering with data (at rest or in transit): Infisical implements data integrity checks to detect tampering. If inconsistencies are found, Infisical aborts transactions and raises alerts. +- Unauthorized access (lacking authentication/authorization): Infisical mandates rigorous authentication and authorization checks for all inbound requests; it also offers multi-factor authentication and role-based access controls. +- Actions without accountability: Infisical logs all project-level events, including policy updates, queries/mutations applied to secrets, and more. Every event is timestamped and information about actor, source (i.e. IP address, user-agent, etc.), and relevant metadata is included. +- Breach of data storage confidentiality: Infisical encrypts all stored secrets using proven cryptographic techniques such as AES-256-GCM for symmetric encryption. +- Loss of service availability or secret data due to failures: Infisical leverages the robust container orchestration capabilities of Kubernetes and the inherent high availability features of Bitnami MongoDB to ensure resilience and fault tolerance. By deploying multiple replicas of Infisical application on Kubernetes, operations can continue even if a single instance fails. +- Unrecognized suspicious activities: Infisical monitors for any anomalous activities such as authentication attempts from previously unseen sources. +- Unidentified system vulnerabilities: Infisical undergoes penetration tests and vulnerability assessments twice a year; we act on findings to bolster the system's defense mechanisms. + +That said, Infisical does not consider the following as part of its threat model: + +- Uncontrolled access to the storage mechanism: An attacker with unfettered access to the storage system can manipulate data in unpredictable ways, including erasing or tampering with stored secrets. Furthermore, the attacker could potentially implement state rollbacks to favor their objectives. +- Disclosure of secret presence: If an adversary gains read access to the storage backend, they might discern the existence of certain secrets, even if the actual contents remain encrypted and concealed. +- Runtime memory intrusion: An attacker with capabilities to probe the memory state of a live instance of Infisical can potentially compromise data confidentiality. +- Vulnerabilities in affiliated systems: Some functionality may rely on third-party services and dependencies. Security lapses in these dependencies can indirectly jeopardize the confidentiality or integrity of the secrets. +- Breaches via compromised clients: If a system or application accessing Infisical is compromised, and its credentials to the platform are exposed, an attacker might gain access at the privilege level of that compromised entity. +- Configuration tampering by administrators: Any configuration data, whether supplied through admin interfaces or configuration files, needs scrutiny. If an attacker can manipulate these configurations, it poses risks to data confidentiality and integrity. +- Physical access to deployment infrastructure: An attacker with physical access to the servers or infrastructure where Infisical is deployed can potentially compromise the system in ways that are challenging to guard against, such as direct hardware tampering or booting from malicious media. +- Social engineering attacks on personnel: Attacks that target personnel, tricking them into divulging sensitive information or performing compromising actions, fall outside the platform's direct defensive purview. + +It's essential to note that while these points fall outside the platform's direct threat model, they still form crucial considerations for an overarching security strategy. + +## External threat overview + +Infisical's architecture consists of various systems: + +- Infisical API +- Storage backend +- Redis +- Infisical Web UI +- Infisical clients + +The Infisical API requires that the Infisical Web UI and all Infisical clients are authenticated and authorized for every inbound request. If using [Infisical Cloud](https://app.infisical.com), all traffic is routed through [Cloudflare](https://www.cloudflare.com) which enforces TLS and requires a minimum of TLS 1.2. + +The Infisical API is untrusted by design when dealing with secrets. All secrets are encrypted/decrypted on the client-side before reaching the Infisical API by default; granting Infisical access to secrets afterward is optional and up to your organization. + +The storage backend used by Infisical is also untrusted by design. All sensitive data is encrypted either symmetrically with AES-256-GCM or asymmetrically with x25519-xsalsa20-poly1305 prior to entering the storage backend, depending on the context either on the client-side or server-side. Moreover, Infisical communicates with the storage backend over TLS to provide an added layer of security. + +## Internal threat overview + +Within Infisical, a critical security concern is an attacker gaining access to sensitive data that they are not permitted to, especially if they already has some degree of access to the system. There are currently two authentication methods categories used by clients for where we apply robust authentication and authorization logic. + +### JWT / API Key + +This token category is used by users and included in requests made from the Infisical Web UI or elsewhere to the Infisical API. + +Each token is authenticated against the API and mapped to an existing user in Infisical. If no existing user is found for the token, the request is rejected by the API. Each token assumes the permission set of the user that it is mapped to. For example, if a user corresponding to a token is not allowed access to a certain organization or project, then the token is also not be valid for any requests concerning those specific resources. + +In the event of compromise, an attacker could use the token to impersonate the associated user and perform actions within the permission set of that user. While they could retrieve secrets for a project that the user is part of, they could not, however, decrypt secrets if the project follows Infisical's default zero-knowlege architecture. In any case, it would be critical for the user to invalidate this token and change their password immediately to prevent further unintended actions and consequences. + +### Service token + +This token category is provisioned by users for applications and infrastructure to perform secret operations against the Infisical API. + +Each token is scoped to a project in Infisical and configurable with an expiration date and permission set (also known as **scopes**) for specific environment(s) and path(s) within them. For example, you may provision an application a service token to authenticate against the Infisical API and retrieve secrets from some `/environment-variables` path in the production environment of a project. If the token is tried for another project, environment, or path outside of its permission set, then it is rejected by the API. + +It should also be noted that projects in Infisical can be configured to restrict service token access to specific IP addresses or CIDR ranges; this can be useful for limiting access to traffic coming from corporate networks. + +In the event of compromise, an attacker could use a service token to access the secrets that it is provisioned for. It would be critical here for project administrator(s) to revoke the token immediately to prevent further unintended access to resources; it would also be advisable currently to transfer secrets to a new project where a new project key is created on the client-side. + +## Cryptography + +Infisical uses AES-256-GCM for symmetric encryption and x25519-xsalsa20-poly1305 for asymmetric encryption operations; asymmetric algorithms are implemented with the [TweetNaCl.js](https://tweetnacl.js.org/#/) library which has been well-audited and recommended for use by cybersecurity firm Cure53. Lastly, the secure remote password (SRP) implementation uses [jsrp](https://github.com/alax/jsrp) package for user authentication. + +By default, Infisical employs a zero-knowledge-first approach to securely storing and sharing secrets. + +- Each secret belongs to a project and is symmetrically encrypted by that project's unique key. Each member of a project is shared a copy of the project key, encrypted under their public key, when they are first invited to join the project. +Since these encryption operations occur on the client-side, the Infisical API is not able to view the value of any secret and the default zero-knowledge property of Infisical is retained; as you'd expect, it follows that decryption operations also occur on the client-side. +- An exception to the zero-knowledge property occurs when a member of a project explicitly shares that project's unique key with Infisical. It is often necessary to share the project key with Infisical in order to use features like native integrations and secret rotation that wouldn't be possible to offer otherwise. + + +## Infrastructure + +### High availability + +Infisical leverages the robust container orchestration capabilities of Kubernetes and the inherent high availability features of the storage backend (i.e. Bitnami MongoDB) to ensure resilience and fault tolerance. + +- Kubernetes: By deploying multiple replicas of Infisical application on Kubernetes, operations continue even if a single instance fails. Kubernetes Services facilitate load balancing, effectively distributing traffic across your application’s instances and ensuring optimal performance. +- Storage backend: Bitnami MongoDB supports replica sets, which provide data redundancy and automatic failover for the underlying database. +- If using [Infisical Cloud](https://app.infisical.com), data is stored in a Mongo Atlas cluster with storage autoscaling and cluster tier autoscaling enabled; as you'd expect, the cluster sits on a dedicated node. + +Together, Kubernetes’ self-healing mechanisms and Bitnami MongoDB’s failover capabilities work to create a highly available and fault-tolerant application capable of recovering gracefully from unexpected failures. + +### Snapshots + +A snapshot is a complete copy of data in the storage backend at a point in time. + +If using [Infisical Cloud](https://app.infisical.com), snapshots of MongoDB databases are taken regularly; this can be enabled on your own storage backend as well. + +### Offline usage + +Many teams and organizations use the [Infisical CLI](https://infisical.com/docs/cli/overview) to fetch and inject secrets back from Infisical into their applications and infrastructure locally; the CLI has offline fallback capabiltiies. + +If you have previously retrieved secrets for a specific project and environment, the `run/secret` command will utilize the saved secrets, even when offline, on subsequent fetch attempts to ensure that you always have access to secrets. + +## Platform + +### Web application + +Infisical utilizes the latest HTTP security headers and employs a strict Content-Security-Policy to mitigate XSS. + +JWT tokens are stored in browser memory and appended to outbound requests requiring authentication; refresh tokens are stored in `HttpOnly` cookies and included in future requests to `/api/token` for JWT token renewal. + +### User authentication + +Infisical supports several authentication methods including email/password, Google SSO, GitHub SSO, and SAML 2.0 (Okta, Azure, JumpCloud); Infisical also currently offers email-based 2FA with authenticator app methods coming in Q1 2024. + +Infisical uses the [secure remote password protocol](https://en.wikipedia.org/wiki/Secure_Remote_Password_protocol#:~:text=The%20SRP%20protocol%20has%20a,the%20user%20to%20the%20server), commonly found in other zero-knowledge platform architectures, for authentication. +Put simply, the protocol enables Infisical to validate a user's knowledge of their password without ever seeing it by constructing a mutual secret; we use this protocol because each user's password is used to seed the generation of a master encryption/decryption key via KDF for that user which the platform +should not see. + +Lastly, Infisical enforces strong password requirements according to the guidance set forth in [NIST Special Publication 800–63B](https://pages.nist.gov/800-63-3/sp800-63b.html#appA). Since passwords in Infisical also has cryptographic implications, Infisical validates each password on client-side to meet minimum length and entropy requirements; Infisical also considers each password against the [Have I Been Pwned (HIBP) API](https://haveibeenpwned.com), which checks the password against around 700M breached passwords, in a privacy-preserving way. + + + Since Infisical's unique zero-knowledge architecture requires a master decryption key for every user account, users with Google SSO, GitHub SSO, or SAML 2.0 enabled must still enter a secret after the + authentication step to access their secrets in Infisical. In practice, this implies stronger security since users must successfully authenticate with a single sign-on provider and provide a master decryption key + to access the platform. + + We strongly encourage users to generate and store their passwords / master decryption key in a password manager, such as 1Password, Bitwarden, or Dashlane. + + +## Role-based access control (RBAC) + +Infisical's RBAC feature enables organization owners and administrators to manage fine-grained access policies for members of their organization in Infisical; with RBAC, administrators can define custom roles with permission sets to be conveniently assigned to other members. + +For example, you can define a role provisioning access to secrets in a specific project and environment in it with read-only permissions; the role can be assigned to members of an organization in Infisical. + +### Audit logging + +Infisical's audit logging feature spans 25+ events, tracking everything from permissioning changes to queries and mutations applied to secrets, for security and compliance teams at enterprises to monitor information access in the event of any suspicious activity or incident review. Every event is timestamped and information about actor, source (i.e. IP address, user-agent, etc.), and relevant metadata is included. + +### IP allowlisting + +Infisical's IP allowlisting feature can be configured to restrict client access to specific IP addresses or CIDR ranges. This applies to any client using service tokens and can be useful, for example, for limiting access to traffic coming from corporate networks. + +By default, each project is initialized with the `0.0.0.0/0` entry, representing all possible IPv4 addresses. For enhanced security, we strongly recommend replacing the default entry with your client IPs to tighten access to your secrets. + +## Penetration testing + +Infisical hires external third parties to perform regular security assessment and penetration testing of the platform. + +Most recently, Infisical commissioned cybersecurity firm [Oneleet](https://www.oneleet.com) to perform a full-coverage, gray box penetration test against the platform's entire attack surface to identify vulnerabilities according to industry standards (OWASP, ASVS, WSTG, TOP-10, etc.). + +Please email security@infisical.com to request any reports including a letter of attestation for the conducted penetration test. + +## Employee data access + +Whether or not Infisical or your employees can access data in the Infisical instance and/or storage backend depends on many factors how you use Infisical: + +- Infisical Self-Hosted: Self-hosting Infisical is common amongst organizations that prefer to keep data on their own infrastructure usually to adhere to strict regulatory and compliance requirements. In this option, organizations retain full control over their data and therefore govern the data access policy of their Infisical instance and storage backend. +- Infisical Cloud: Using Infisical's managed service, [Infisical Cloud](https://app.infisical.com) means delegating data oversight and management to Infisical. Under our policy controls, employees are only granted access to parts of infrastructure according to principle of least privilege; this is especially relevent to customer data can only be accessed currently by executive management of Infisical. Moreover, any changes to sensitive customer data is prohibited without explicit customer approval. + +It should be noted that, even on Infisical Cloud, it is physically impossible for employees of Infisical to view the values of secrets if users have not explicitly granted Infisical access to their project (i.e. opted out of zero-knowledge). + +Please email security@infisical.com if you have any specific inquiries about employee data access policies. + +## Get in touch + +If you have any concerns about Infisical or believe you have uncovered a vulnerability, please get in touch via the e-mail address security@infisical.com. In the message, try to provide a description of the issue and ideally a way of reproducing it. The security team will get back to you as soon as possible. + +Note that this security address should be used for undisclosed vulnerabilities. Please report any security problems to us before disclosing it publicly. \ No newline at end of file diff --git a/docs/internals/service-tokens.mdx b/docs/internals/service-tokens.mdx new file mode 100644 index 000000000..a04eccee7 --- /dev/null +++ b/docs/internals/service-tokens.mdx @@ -0,0 +1,49 @@ +--- +title: "Service tokens" +description: "Understanding service tokens and their best practices" +--- +​ +Many clients use service tokens to authenticate and read/write secrets from/to Infisical; they can be created in your project settings. + +## Anatomy + +A service token in Infisical consists of the token itself, a `string`, and a corresponding document in the storage backend containing its +properties and metadata. + +### Database model + +The storage backend model for a token contains the following information: + +- ID: The token identifier. +- Expiration: The date at which point the token is invalid. +- Project: The project that the token is part of. +- Scopes: The project environments and paths that the token has access to. +- Encrypted project key: An encrypted copy of the project key. + +### Token + +A service token itself consist of two parts used for authentication and decryption, separated by the delimiter `.`. + +Consider the token `st.abc.def.ghi`. Here, `st.abc.def` can be used to authenticate with the API, by including it in the `Authorization` header under `Bearer st.abc.def`, and retrieve (encrypted) secrets as well as a project key back. Meanwhile, `ghi`, a hex-string, can be used to decrypt the project key used to decrypt the secrets. + +Note that when using service tokens via select client methods like SDK or CLI, cryptographic operations are abstracted for you that is the token is parsed and encryption/decryption operations are handled. If using service tokens with the REST API and end-to-end encryption enabled, then you will have to handle the encryption/decryption operations yourself. +​ +## Recommendations + +### Issuance + +When creating a new service token, it’s important to consider the principle of least privilege(PoLP) when setting its scope and expiration date. For example, if the client using the token only requires access to a staging environment, then you should scope the token to that environment only; you can further scope tokens to path(s) within environment(s) if you happen to use path-based secret storage. Likewise, if the client does not intend to access secrets indefinitely, then you may consider setting a finite lifetime for the token such as 6 months or 1 year from now. Finally, you should consider carefully whether or not your client requires the ability to read and/or write secrets from/to Infisical. + +### Network access + +We recommend configuring the IP whitelist settings of each project to allow either single IP addresses or CIDR-notated range of addresses to read/write secrets to Infisical. With this feature, you can specify the IP range of your client servers to restrict access to your project in Infisical. + +### Storage + +Since service tokens grant access to your secrets, we recommend storing them securely across your development cycle whether it be in a .env file in local development or as an environment variable of your deployment platform. + +### Rotation + +We recommend periodically rotating the service token, even in the absence of compromise. Since service tokens are capable of decrypting project keys used to decrypt secrets, all of which use AES-256-GCM encryption, they should be rotated before approximately 2^32 encryptions have been performed; this follows the guidance set forth by [NIST publication 800-38D](https://csrc.nist.gov/pubs/sp/800/38/d/final). + +Note that Infisical keeps track of the number of times that service tokens are used and will alert you when you have reached 90% of the recommended capacity. \ No newline at end of file diff --git a/docs/mint.json b/docs/mint.json index 05bb4ee98..8c39483ea 100644 --- a/docs/mint.json +++ b/docs/mint.json @@ -42,9 +42,9 @@ }, "anchors": [ { - "name": "Security", - "icon": "shield-halved", - "url": "security" + "name": "Internals", + "icon": "sitemap", + "url": "internals" }, { "name": "SDKs", @@ -331,6 +331,15 @@ "api-reference/endpoints/environments/delete" ] }, + { + "group": "Folders", + "pages": [ + "api-reference/endpoints/folders/list", + "api-reference/endpoints/folders/create", + "api-reference/endpoints/folders/update", + "api-reference/endpoints/folders/delete" + ] + }, { "group": "Secrets", "pages": [ @@ -343,12 +352,31 @@ "api-reference/endpoints/secrets/rollback-version" ] }, + { + "group": "Secret imports", + "pages": [ + "api-reference/endpoints/secret-imports/list", + "api-reference/endpoints/secret-imports/create", + "api-reference/endpoints/secret-imports/update", + "api-reference/endpoints/secret-imports/delete" + ] + }, { "group": "Service Tokens", "pages": ["api-reference/endpoints/service-tokens/get"] } ] }, + { + "group": "Internals", + "pages": [ + "internals/overview", + "internals/components", + "internals/flows", + "internals/security", + "internals/service-tokens" + ] + }, { "group": "Security", "pages": [ diff --git a/docs/security/mechanics.mdx b/docs/security/mechanics.mdx index d1c483d2c..06d5fedd9 100644 --- a/docs/security/mechanics.mdx +++ b/docs/security/mechanics.mdx @@ -11,7 +11,7 @@ The 256-bit key is used to encrypt the private key; the 256-bit key itself is th The encrypted private key, protected key, user identifier information, and SRP details are forwarded to the server. -Once authenticated via SRP, a user is issued a JWT and refresh token. The JWT token is stored in browser memory under a write-only class `SecurityClient` that appends the token to all future outbound requests requiring authentication. The refresh token is stored in an `HttpOnly` cookie and included in future requests to `/api/token` for JWT token renewal. This design side-steps potential XSS attacks on local storage. +Once authenticated via SRP, a user is issued a JWT and refresh token. The JWT token is stored in browser memory and is appended to all future outbound requests requiring authentication. The refresh token is stored in an `HttpOnly` cookie and included in future requests to `/api/token` for JWT token renewal. This design side-steps potential XSS attacks on local storage. Infisical authenticates users using the SRP protocol. With SRP, the server can diff --git a/docs/security/overview.mdx b/docs/security/overview.mdx index bddff608b..bd0ed0878 100644 --- a/docs/security/overview.mdx +++ b/docs/security/overview.mdx @@ -7,7 +7,9 @@ description: "Infisical's security statement." Infisical uses end-to-end encryption (E2EE) whenever possible to securely store and share secret values. It uses secure remote password (SRP) to handle authentication and public-key cryptography for secret sharing and syncing; secrets are symmetrically encrypted by keys decryptable only by members of the project. -Infisical uses AES256-GCM for symmetric encryption and x25519-xsalsa20-poly1305 for asymmetric encryption operations mentioned in this brief; key generation and asymmetric algorithms are implemented with the [TweetNaCl.js](https://tweetnacl.js.org/#/) library which has been well-audited and recommended for use by cybersecurity firm Cure53. Lastly, the secure remote password (SRP) implementation uses [jsrp](https://github.com/alax/jsrp) package for user authentication. As part of our commitment to user privacy and security, we aim to conduct formal security and compliance audits in the following year. +Infisical uses AES256-GCM for symmetric encryption and x25519-xsalsa20-poly1305 for asymmetric encryption operations mentioned in this brief; key generation and asymmetric algorithms are implemented with the [TweetNaCl.js](https://tweetnacl.js.org/#/) library which has been well-audited and recommended for use by cybersecurity firm Cure53. Lastly, the secure remote password (SRP) implementation uses [jsrp](https://github.com/alax/jsrp) package for user authentication. + +As part of our commitment to user privacy and security, we undergo penetration tests twice a year and are working to achieve SOC 2 (Type II) compliance in Fall 2023. ## Scope @@ -24,7 +26,8 @@ In subsequent sections, we refer: As a secrets manager, we are deeply committed to enforcing the privacy and security of all users and data on the platform but acknowledge that it is virtually impossible to guarantee perfect security; unfortunately, even the most secure systems have vulnerabilities. -As part of our commitment, we do our best to maintain platform privacy and security, notify users if anything goes wrong, and rectify adverse situations immediately if anything happens. As Infisical grows, we will be adding more opt-in security measures to ensure better data protection and maintain trust within the growing community. With that, let’s make the most simple and secure secrets management system out there! +As part of our commitment, we do our best to maintain platform privacy and security, notify users if anything goes wrong, and rectify adverse situations immediately if anything happens. +We are continuously adding more opt-in security measures to ensure better data protection and maintain trust within the growing community. With that, let’s make the most simple and secure secrets management system out there! Best, diff --git a/docs/self-hosting/configuration/email.mdx b/docs/self-hosting/configuration/email.mdx index 160e4f205..566265a2d 100644 --- a/docs/self-hosting/configuration/email.mdx +++ b/docs/self-hosting/configuration/email.mdx @@ -25,6 +25,36 @@ If you choose to setup email service, you need to configure the following SMTP [ Below you will find details on how to configure common email providers: + +1. Create an account on [Resend](https://resend.com). +2. Add a [Domain](https://resend.com/domains). + +![adding resend domain](../../images/email-resend-create-domain.png) + +3. Create an [API Key](https://resend.com/api-keys). + +![creating resend api key](../../images/email-resend-create-key.png) + +4. Go to the [SMTP page](https://resend.com/settings/smtp) and copy the values. + +![go to resend smtp settings](../../images/email-resend-smtp-settings.png) + +5. With the API Key, you can now set your SMTP environment variables variables: + +``` +SMTP_HOST=smtp.resend.com +SMTP_USERNAME=resend +SMTP_PASSWORD=YOUR_API_KEY +SMTP_PORT=587 +SMTP_SECURE=true +SMTP_FROM_ADDRESS=hey@example.com # your email address being used to send out emails +SMTP_FROM_NAME=Infisical +``` + + Remember that you will need to restart Infisical for this to work properly. + + + 1. Create an account and configure [SendGrid](https://sendgrid.com) to send emails. diff --git a/docs/spec.yaml b/docs/spec.yaml index 5cf129be3..40379087a 100644 --- a/docs/spec.yaml +++ b/docs/spec.yaml @@ -45,8 +45,8 @@ paths: items: $ref: '#/components/schemas/SecretVersion' description: Secret versions - '400': - description: Bad Request + security: + - apiKeyAuth: [] /api/v1/secret/{secretId}/secret-versions/rollback: post: summary: Roll back secret to a version. @@ -70,8 +70,8 @@ paths: type: object $ref: '#/components/schemas/Secret' description: Secret rolled back to - '400': - description: Bad Request + security: + - apiKeyAuth: [] requestBody: required: true content: @@ -94,8 +94,12 @@ paths: responses: '200': description: OK - '400': - description: Bad Request + /api/v1/users/me/ip: + get: + description: '' + responses: + '200': + description: OK /api/v1/workspace/{workspaceId}/secret-snapshots: get: summary: Return project secret snapshot ids @@ -132,8 +136,8 @@ paths: items: $ref: '#/components/schemas/SecretSnapshot' description: Project secret snapshots - '400': - description: Bad Request + security: + - apiKeyAuth: [] /api/v1/workspace/{workspaceId}/secret-snapshots/count: get: description: '' @@ -146,8 +150,6 @@ paths: responses: '200': description: OK - '400': - description: Bad Request /api/v1/workspace/{workspaceId}/secret-snapshots/rollback: post: summary: >- @@ -176,8 +178,8 @@ paths: items: $ref: '#/components/schemas/Secret' description: Secrets rolled back to - '400': - description: Bad Request + security: + - apiKeyAuth: [] requestBody: required: true content: @@ -245,6 +247,92 @@ paths: items: $ref: '#/components/schemas/Log' description: Project logs + security: + - apiKeyAuth: [] + /api/v1/workspace/{workspaceId}/audit-logs: + get: + description: '' + parameters: + - name: workspaceId + in: path + required: true + schema: + type: string + responses: + '200': + description: OK + /api/v1/workspace/{workspaceId}/audit-logs/filters/actors: + get: + description: '' + parameters: + - name: workspaceId + in: path + required: true + schema: + type: string + responses: + '200': + description: OK + /api/v1/workspace/{workspaceId}/trusted-ips: + get: + description: '' + parameters: + - name: workspaceId + in: path + required: true + schema: + type: string + responses: + '200': + description: OK + post: + description: '' + parameters: + - name: workspaceId + in: path + required: true + schema: + type: string + responses: + '200': + description: OK + '400': + description: Bad Request + /api/v1/workspace/{workspaceId}/trusted-ips/{trustedIpId}: + patch: + description: '' + parameters: + - name: workspaceId + in: path + required: true + schema: + type: string + - name: trustedIpId + in: path + required: true + schema: + type: string + responses: + '200': + description: OK + '400': + description: Bad Request + delete: + description: '' + parameters: + - name: workspaceId + in: path + required: true + schema: + type: string + - name: trustedIpId + in: path + required: true + schema: + type: string + responses: + '200': + description: OK '400': description: Bad Request /api/v1/action/{actionId}: @@ -259,74 +347,309 @@ paths: responses: '200': description: OK + /api/v1/organizations/{organizationId}/plans/table: + get: + description: '' + parameters: + - name: organizationId + in: path + required: true + schema: + type: string + responses: + '200': + description: OK + /api/v1/organizations/{organizationId}/plan: + get: + description: '' + parameters: + - name: organizationId + in: path + required: true + schema: + type: string + responses: + '200': + description: OK + /api/v1/organizations/{organizationId}/session/trial: + post: + description: '' + parameters: + - name: organizationId + in: path + required: true + schema: + type: string + responses: + '200': + description: OK + /api/v1/organizations/{organizationId}/plan/billing: + get: + description: '' + parameters: + - name: organizationId + in: path + required: true + schema: + type: string + responses: + '200': + description: OK + /api/v1/organizations/{organizationId}/plan/table: + get: + description: '' + parameters: + - name: organizationId + in: path + required: true + schema: + type: string + responses: + '200': + description: OK + /api/v1/organizations/{organizationId}/billing-details: + get: + description: '' + parameters: + - name: organizationId + in: path + required: true + schema: + type: string + responses: + '200': + description: OK + patch: + description: '' + parameters: + - name: organizationId + in: path + required: true + schema: + type: string + responses: + '200': + description: OK + /api/v1/organizations/{organizationId}/billing-details/payment-methods: + get: + description: '' + parameters: + - name: organizationId + in: path + required: true + schema: + type: string + responses: + '200': + description: OK + post: + description: '' + parameters: + - name: organizationId + in: path + required: true + schema: + type: string + responses: + '200': + description: OK + /api/v1/organizations/{organizationId}/billing-details/payment-methods/{pmtMethodId}: + delete: + description: '' + parameters: + - name: organizationId + in: path + required: true + schema: + type: string + - name: pmtMethodId + in: path + required: true + schema: + type: string + responses: + '200': + description: OK + /api/v1/organizations/{organizationId}/billing-details/tax-ids: + get: + description: '' + parameters: + - name: organizationId + in: path + required: true + schema: + type: string + responses: + '200': + description: OK + post: + description: '' + parameters: + - name: organizationId + in: path + required: true + schema: + type: string + responses: + '200': + description: OK + /api/v1/organizations/{organizationId}/billing-details/tax-ids/{taxId}: + delete: + description: '' + parameters: + - name: organizationId + in: path + required: true + schema: + type: string + - name: taxId + in: path + required: true + schema: + type: string + responses: + '200': + description: OK + /api/v1/organizations/{organizationId}/invoices: + get: + description: '' + parameters: + - name: organizationId + in: path + required: true + schema: + type: string + responses: + '200': + description: OK + /api/v1/organizations/{organizationId}/licenses: + get: + description: '' + parameters: + - name: organizationId + in: path + required: true + schema: + type: string + responses: + '200': + description: OK + /api/v1/sso/redirect/google: + get: + description: '' + parameters: + - name: callback_port + in: query + schema: + type: string + responses: + default: + description: '' + /api/v1/sso/google: + get: + description: '' + responses: + default: + description: '' + /api/v1/sso/redirect/github: + get: + description: '' + parameters: + - name: callback_port + in: query + schema: + type: string + responses: + default: + description: '' + /api/v1/sso/github: + get: + description: '' + responses: + default: + description: '' + /api/v1/sso/redirect/saml2/{ssoIdentifier}: + get: + description: '' + parameters: + - name: ssoIdentifier + in: path + required: true + schema: + type: string + - name: callback_port + in: query + schema: + type: string + responses: + default: + description: '' + /api/v1/sso/saml2/{ssoIdentifier}: + post: + description: '' + parameters: + - name: ssoIdentifier + in: path + required: true + schema: + type: string + responses: + default: + description: '' + /api/v1/sso/config: + get: + description: '' + responses: + '200': + description: OK + post: + description: '' + responses: + '200': + description: OK + '400': + description: Bad Request + patch: + description: '' + responses: + '200': + description: OK + '400': + description: Bad Request + /api/v1/cloud-products/: + get: + description: '' + responses: + '200': + description: OK /api/v1/signup/email/signup: post: description: '' - parameters: [] responses: '200': description: OK - '400': - description: Bad Request '403': description: Forbidden - requestBody: - content: - application/json: - schema: - type: object - properties: - email: - example: any /api/v1/signup/email/verify: post: description: '' - parameters: [] responses: '200': description: OK - '400': - description: Bad Request '403': description: Forbidden - requestBody: - content: - application/json: - schema: - type: object - properties: - email: - example: any - code: - example: any /api/v1/auth/token: post: description: '' - parameters: [] responses: '200': description: OK - '400': - description: Bad Request /api/v1/auth/login1: post: description: '' - parameters: [] responses: '200': description: OK - '400': - description: Bad Request - requestBody: - content: - application/json: - schema: - type: object - properties: - email: - example: any - clientPublicKey: - example: any /api/v1/auth/login2: post: description: '' @@ -340,16 +663,6 @@ paths: description: OK '400': description: Bad Request - requestBody: - content: - application/json: - schema: - type: object - properties: - email: - example: any - clientProof: - example: any /api/v1/auth/logout: post: description: '' @@ -361,12 +674,15 @@ paths: responses: '200': description: OK - '400': - description: Bad Request /api/v1/auth/checkAuth: post: description: '' - parameters: [] + responses: + '200': + description: OK + /api/v1/auth/sessions: + delete: + description: '' responses: '200': description: OK @@ -382,8 +698,6 @@ paths: responses: '200': description: OK - '400': - description: Bad Request /api/v1/bot/{botId}/active: patch: description: '' @@ -398,77 +712,34 @@ paths: description: OK '400': description: Bad Request - requestBody: - content: - application/json: - schema: - type: object - properties: - isActive: - example: any - botKey: - example: any /api/v1/user/: get: description: '' - parameters: [] responses: '200': description: OK /api/v1/user-action/: post: description: '' - parameters: [] responses: '200': description: OK - '400': - description: Bad Request - requestBody: - content: - application/json: - schema: - type: object - properties: - action: - example: any get: description: '' - parameters: - - name: action - in: query - schema: - type: string responses: '200': description: OK - '400': - description: Bad Request /api/v1/organization/: get: description: '' - parameters: [] responses: '200': description: OK - '400': - description: Bad Request post: description: '' - parameters: [] responses: '200': description: OK - '400': - description: Bad Request - requestBody: - content: - application/json: - schema: - type: object - properties: - organizationName: - example: any /api/v1/organization/{organizationId}: get: description: '' @@ -481,8 +752,6 @@ paths: responses: '200': description: OK - '400': - description: Bad Request /api/v1/organization/{organizationId}/users: get: description: '' @@ -495,8 +764,6 @@ paths: responses: '200': description: OK - '400': - description: Bad Request /api/v1/organization/{organizationId}/my-workspaces: get: description: '' @@ -509,8 +776,6 @@ paths: responses: '200': description: OK - '400': - description: Bad Request /api/v1/organization/{organizationId}/name: patch: description: '' @@ -523,16 +788,6 @@ paths: responses: '200': description: OK - '400': - description: Bad Request - requestBody: - content: - application/json: - schema: - type: object - properties: - name: - example: any /api/v1/organization/{organizationId}/incidentContactOrg: get: description: '' @@ -545,8 +800,6 @@ paths: responses: '200': description: OK - '400': - description: Bad Request post: description: '' parameters: @@ -558,16 +811,6 @@ paths: responses: '200': description: OK - '400': - description: Bad Request - requestBody: - content: - application/json: - schema: - type: object - properties: - email: - example: any delete: description: '' parameters: @@ -579,16 +822,6 @@ paths: responses: '200': description: OK - '400': - description: Bad Request - requestBody: - content: - application/json: - schema: - type: object - properties: - email: - example: any /api/v1/organization/{organizationId}/customer-portal-session: post: description: '' @@ -601,22 +834,6 @@ paths: responses: '200': description: OK - '400': - description: Bad Request - /api/v1/organization/{organizationId}/subscriptions: - get: - description: '' - parameters: - - name: organizationId - in: path - required: true - schema: - type: string - responses: - '200': - description: OK - '400': - description: Bad Request /api/v1/organization/{organizationId}/workspace-memberships: get: description: '' @@ -641,8 +858,6 @@ paths: responses: '200': description: OK - '400': - description: Bad Request /api/v1/workspace/{workspaceId}/users: get: description: '' @@ -655,35 +870,19 @@ paths: responses: '200': description: OK - '400': - description: Bad Request /api/v1/workspace/: get: description: '' - parameters: [] responses: '200': description: OK - '400': - description: Bad Request post: description: '' - parameters: [] responses: '200': description: OK '400': description: Bad Request - requestBody: - content: - application/json: - schema: - type: object - properties: - workspaceName: - example: any - organizationId: - example: any /api/v1/workspace/{workspaceId}: get: description: '' @@ -696,8 +895,6 @@ paths: responses: '200': description: OK - '400': - description: Bad Request delete: description: '' parameters: @@ -709,8 +906,6 @@ paths: responses: '200': description: OK - '400': - description: Bad Request /api/v1/workspace/{workspaceId}/name: post: description: '' @@ -723,16 +918,6 @@ paths: responses: '200': description: OK - '400': - description: Bad Request - requestBody: - content: - application/json: - schema: - type: object - properties: - name: - example: any /api/v1/workspace/{workspaceId}/invite-signup: post: description: '' @@ -745,16 +930,6 @@ paths: responses: '200': description: OK - '400': - description: Bad Request - requestBody: - content: - application/json: - schema: - type: object - properties: - email: - example: any /api/v1/workspace/{workspaceId}/integrations: get: description: '' @@ -767,8 +942,6 @@ paths: responses: '200': description: OK - '400': - description: Bad Request /api/v1/workspace/{workspaceId}/authorizations: get: description: '' @@ -781,8 +954,6 @@ paths: responses: '200': description: OK - '400': - description: Bad Request /api/v1/workspace/{workspaceId}/service-tokens: get: description: '' @@ -795,8 +966,6 @@ paths: responses: '200': description: OK - '400': - description: Bad Request /api/v1/membership-org/membershipOrg/{membershipOrgId}/change-role: post: description: '' @@ -819,8 +988,8 @@ paths: schema: type: string responses: - '400': - description: Bad Request + default: + description: '' /api/v1/membership/{workspaceId}/connect: get: description: '' @@ -833,8 +1002,6 @@ paths: responses: '200': description: OK - '400': - description: Bad Request /api/v1/membership/{membershipId}: delete: description: '' @@ -847,8 +1014,6 @@ paths: responses: '200': description: OK - '400': - description: Bad Request /api/v1/membership/{membershipId}/change-role: post: description: '' @@ -861,36 +1026,6 @@ paths: responses: '200': description: OK - '400': - description: Bad Request - requestBody: - content: - application/json: - schema: - type: object - properties: - role: - example: any - /api/v1/membership/{membershipId}/deny-permissions: - post: - description: '' - parameters: - - name: membershipId - in: path - required: true - schema: - type: string - responses: - '200': - description: OK - requestBody: - content: - application/json: - schema: - type: object - properties: - permissions: - example: any /api/v1/key/{workspaceId}: post: description: '' @@ -903,16 +1038,6 @@ paths: responses: '200': description: OK - '400': - description: Bad Request - requestBody: - content: - application/json: - schema: - type: object - properties: - key: - example: any /api/v1/key/{workspaceId}/latest: get: description: '' @@ -925,8 +1050,6 @@ paths: responses: '200': description: OK - '400': - description: Bad Request /api/v1/invite-org/signup: post: description: '' @@ -940,35 +1063,12 @@ paths: description: OK '400': description: Bad Request - requestBody: - content: - application/json: - schema: - type: object - properties: - organizationId: - example: any - inviteeEmail: - example: any /api/v1/invite-org/verify: post: description: '' - parameters: [] responses: '200': description: OK - '400': - description: Bad Request - requestBody: - content: - application/json: - schema: - type: object - properties: - email: - example: any - code: - example: any /api/v1/secret/{workspaceId}: post: description: '' @@ -981,8 +1081,6 @@ paths: responses: '200': description: OK - '400': - description: Bad Request requestBody: content: application/json: @@ -1016,8 +1114,6 @@ paths: responses: '200': description: OK - '400': - description: Bad Request /api/v1/secret/{workspaceId}/service-token: get: description: '' @@ -1038,18 +1134,14 @@ paths: responses: '200': description: OK - '400': - description: Bad Request /api/v1/service-token/: get: description: '' - parameters: [] responses: '200': description: OK post: description: '' - parameters: [] responses: '200': description: OK @@ -1078,211 +1170,56 @@ paths: /api/v1/password/srp1: post: description: '' - parameters: [] responses: '200': description: OK - '400': - description: Bad Request - requestBody: - content: - application/json: - schema: - type: object - properties: - clientPublicKey: - example: any /api/v1/password/change-password: post: description: '' - parameters: [] responses: '200': description: OK '400': description: Bad Request - requestBody: - content: - application/json: - schema: - type: object - properties: - clientProof: - example: any - protectedKey: - example: any - protectedKeyIV: - example: any - protectedKeyTag: - example: any - encryptedPrivateKey: - example: any - encryptedPrivateKeyIV: - example: any - encryptedPrivateKeyTag: - example: any - salt: - example: any - verifier: - example: any /api/v1/password/email/password-reset: post: description: '' - parameters: [] responses: '200': description: OK - '400': - description: Bad Request - '403': - description: Forbidden - requestBody: - content: - application/json: - schema: - type: object - properties: - email: - example: any /api/v1/password/email/password-reset-verify: post: description: '' - parameters: [] responses: '200': description: OK - '400': - description: Bad Request '403': description: Forbidden - requestBody: - content: - application/json: - schema: - type: object - properties: - email: - example: any - code: - example: any /api/v1/password/backup-private-key: get: description: '' - parameters: [] responses: '200': description: OK - '400': - description: Bad Request post: description: '' - parameters: [] responses: '200': description: OK '400': description: Bad Request - requestBody: - content: - application/json: - schema: - type: object - properties: - clientProof: - example: any - encryptedPrivateKey: - example: any - iv: - example: any - tag: - example: any - salt: - example: any - verifier: - example: any /api/v1/password/password-reset: post: description: '' - parameters: [] responses: '200': description: OK - '400': - description: Bad Request - requestBody: - content: - application/json: - schema: - type: object - properties: - protectedKey: - example: any - protectedKeyIV: - example: any - protectedKeyTag: - example: any - encryptedPrivateKey: - example: any - encryptedPrivateKeyIV: - example: any - encryptedPrivateKeyTag: - example: any - salt: - example: any - verifier: - example: any - /api/v1/stripe/webhook: - post: - description: '' - parameters: - - name: stripe-signature - in: header - schema: - type: string - responses: - '200': - description: OK - '400': - description: Bad Request /api/v1/integration/: post: description: '' - parameters: [] responses: '200': description: OK - '400': - description: Bad Request - requestBody: - content: - application/json: - schema: - type: object - properties: - integrationAuthId: - example: any - app: - example: any - appId: - example: any - isActive: - example: any - sourceEnvironment: - example: any - targetEnvironment: - example: any - targetEnvironmentId: - example: any - targetService: - example: any - targetServiceId: - example: any - owner: - example: any - path: - example: any - region: - example: any /api/v1/integration/{integrationId}: patch: description: '' @@ -1295,26 +1232,6 @@ paths: responses: '200': description: OK - '400': - description: Bad Request - requestBody: - content: - application/json: - schema: - type: object - properties: - environment: - example: any - isActive: - example: any - app: - example: any - appId: - example: any - targetEnvironment: - example: any - owner: - example: any delete: description: '' parameters: @@ -1326,12 +1243,15 @@ paths: responses: '200': description: OK - '400': - description: Bad Request + /api/v1/integration/manual-sync: + post: + description: '' + responses: + '200': + description: OK /api/v1/integration-auth/integration-options: get: description: '' - parameters: [] responses: '200': description: OK @@ -1365,47 +1285,15 @@ paths: /api/v1/integration-auth/oauth-token: post: description: '' - parameters: [] responses: '200': description: OK - '400': - description: Bad Request - requestBody: - content: - application/json: - schema: - type: object - properties: - workspaceId: - example: any - code: - example: any - integration: - example: any /api/v1/integration-auth/access-token: post: description: '' - parameters: [] responses: '200': description: OK - '400': - description: Bad Request - requestBody: - content: - application/json: - schema: - type: object - properties: - workspaceId: - example: any - accessId: - example: any - accessToken: - example: any - integration: - example: any /api/v1/integration-auth/{integrationAuthId}/apps: get: description: '' @@ -1415,15 +1303,9 @@ paths: required: true schema: type: string - - name: teamId - in: query - schema: - type: string responses: '200': description: OK - '400': - description: Bad Request /api/v1/integration-auth/{integrationAuthId}/teams: get: description: '' @@ -1445,10 +1327,6 @@ paths: required: true schema: type: string - - name: appId - in: query - schema: - type: string responses: '200': description: OK @@ -1461,10 +1339,6 @@ paths: required: true schema: type: string - - name: appId - in: query - schema: - type: string responses: '200': description: OK @@ -1477,22 +1351,671 @@ paths: required: true schema: type: string - - name: appId + responses: + '200': + description: OK + /api/v1/integration-auth/{integrationAuthId}/bitbucket/workspaces: + get: + description: '' + parameters: + - name: integrationAuthId + in: path + required: true + schema: + type: string + responses: + '200': + description: OK + /api/v1/integration-auth/{integrationAuthId}/northflank/secret-groups: + get: + description: '' + parameters: + - name: integrationAuthId + in: path + required: true + schema: + type: string + responses: + '200': + description: OK + /api/v1/integration-auth/{integrationAuthId}/teamcity/build-configs: + get: + description: '' + parameters: + - name: integrationAuthId + in: path + required: true + schema: + type: string + responses: + '200': + description: OK + /api/v1/folders/: + post: + summary: Create a folder + description: Create a new folder in a specified workspace and environment + responses: + '200': + description: OK + content: + application/json: + schema: + type: object + properties: + folder: + type: object + properties: + id: + type: string + example: someFolderId + name: + type: string + example: my_folder + description: Details of the created folder + '400': + description: >- + Bad Request. For example, 'Folder name cannot contain spaces. Only + underscore and dashes' + '401': + description: Unauthorized request. For example, 'Folder Permission Denied' + security: + - apiKeyAuth: [] + requestBody: + content: + application/json: + schema: + type: object + properties: + workspaceId: + type: string + description: ID of the workspace where the folder will be created + example: someWorkspaceId + environment: + type: string + description: Environment where the folder will reside + example: production + folderName: + type: string + description: Name of the folder to be created + example: my_folder + parentFolderId: + type: string + description: >- + ID of the parent folder under which this folder will be + created. If not specified, it will be created at the root + level. + example: someParentFolderId + required: + - workspaceId + - environment + - folderName + get: + summary: Retrieve folders based on specific conditions + description: >- + Fetches folders from the specified workspace and environment, optionally + providing either a parentFolderId or a parentFolderPath to narrow down + results + parameters: + - name: workspaceId + description: ID of the workspace from which the folders are to be fetched + required: true in: query schema: type: string + - name: environment + description: Environment where the folder is located + required: true + in: query + schema: + type: string + - name: parentFolderId + description: ID of the parent folder + required: false + in: query + schema: + type: string + - name: parentFolderPath + description: Path of the parent folder, like /folder1/folder2 + required: false + in: query + schema: + type: string + responses: + '200': + description: OK + content: + application/json: + schema: + type: object + properties: + folders: + type: array + items: + type: object + properties: + id: + type: string + example: someFolderId + name: + type: string + example: someFolderName + description: List of folders + dir: + type: array + items: + type: object + properties: + name: + type: string + example: parentFolderName + id: + type: string + example: parentFolderId + description: List of directories + '400': + description: Bad Request. For instance, 'The folder doesn't exist' + '401': + description: Unauthorized request. For example, 'Folder Permission Denied' + security: + - apiKeyAuth: [] + /api/v1/folders/{folderId}: + patch: + summary: Update a folder by ID + description: >- + Update the name of a folder in a specified workspace and environment by + its ID + parameters: + - name: folderId + in: path + required: true + schema: + type: string + description: ID of the folder to be updated + responses: + '200': + description: OK + content: + application/json: + schema: + type: object + properties: + message: + type: string + example: Successfully updated folder + folder: + type: object + properties: + name: + type: string + example: updated_folder_name + id: + type: string + example: someFolderId + description: Details of the updated folder + '400': + description: >- + Bad Request. Reasons can include 'The folder doesn't exist' or + 'Folder name cannot contain spaces. Only underscore and dashes' + '401': + description: Unauthorized request. For example, 'Folder Permission Denied' + security: + - apiKeyAuth: [] + requestBody: + content: + application/json: + schema: + type: object + properties: + workspaceId: + type: string + description: ID of the workspace where the folder is located + example: someWorkspaceId + environment: + type: string + description: Environment where the folder is located + example: production + name: + type: string + description: New name for the folder + example: updated_folder_name + required: + - workspaceId + - environment + - name + delete: + summary: Delete a folder by ID + description: >- + Delete the specified folder from a specified workspace and environment + using its ID + parameters: + - name: folderId + in: path + required: true + schema: + type: string + description: ID of the folder to be deleted + responses: + '200': + description: OK + content: + application/json: + schema: + type: object + properties: + message: + type: string + example: successfully deleted folders + folders: + type: array + items: + type: object + properties: + id: + type: string + example: someFolderId + name: + type: string + example: someFolderName + description: List of IDs and names of the deleted folders + '400': + description: Bad Request. Reasons can include 'The folder doesn't exist' + '401': + description: Unauthorized request. For example, 'Folder Permission Denied' + security: + - apiKeyAuth: [] + requestBody: + content: + application/json: + schema: + type: object + properties: + workspaceId: + type: string + description: ID of the workspace where the folder is located + example: someWorkspaceId + environment: + type: string + description: Environment where the folder is located + example: production + required: + - workspaceId + - environment + /api/v1/secret-scanning/create-installation-session/organization/{organizationId}: + post: + description: '' + parameters: + - name: organizationId + in: path + required: true + schema: + type: string + responses: + '200': + description: OK + /api/v1/secret-scanning/link-installation: + post: + description: '' + responses: + '200': + description: OK + /api/v1/secret-scanning/installation-status/organization/{organizationId}: + get: + description: '' + parameters: + - name: organizationId + in: path + required: true + schema: + type: string + responses: + '200': + description: OK + /api/v1/secret-scanning/organization/{organizationId}/risks: + get: + description: '' + parameters: + - name: organizationId + in: path + required: true + schema: + type: string + responses: + '200': + description: OK + /api/v1/secret-scanning/organization/{organizationId}/risks/{riskId}/status: + post: + description: '' + parameters: + - name: organizationId + in: path + required: true + schema: + type: string + - name: riskId + in: path + required: true + schema: + type: string + responses: + '200': + description: OK + /api/v1/webhooks/: + post: + description: '' + responses: + '200': + description: OK + get: + description: '' + responses: + '200': + description: OK + /api/v1/webhooks/{webhookId}: + patch: + description: '' + parameters: + - name: webhookId + in: path + required: true + schema: + type: string + responses: + '200': + description: OK + delete: + description: '' + parameters: + - name: webhookId + in: path + required: true + schema: + type: string + responses: + '200': + description: OK + /api/v1/webhooks/{webhookId}/test: + post: + description: '' + parameters: + - name: webhookId + in: path + required: true + schema: + type: string + responses: + '200': + description: OK + '400': + description: Bad Request + /api/v1/secret-imports/: + post: + summary: Create secret import + description: Create a new secret import for a specified workspace and environment + responses: + '200': + description: OK + content: + application/json: + schema: + type: object + properties: + message: + type: string + example: successfully created secret import + description: Confirmation of secret import creation + '400': + description: Bad Request. For example, 'Secret import already exist' + '401': + description: Unauthorized request. For example, 'Folder Permission Denied' + '404': + description: Resource Not Found. For example, 'Failed to find folder' + requestBody: + content: + application/json: + schema: + type: object + properties: + workspaceId: + type: string + description: ID of the workspace where the secret import will be created + example: someWorkspaceId + environment: + type: string + description: Environment to import to + example: production + folderId: + type: string + description: Folder ID. Use root for the root folder. + example: my_folder + secretImport: + type: object + properties: + environment: + type: string + description: Import from environment + example: development + secretPath: + type: string + description: Import from secret path + example: /user/oauth + required: + - workspaceId + - environment + - folderName + get: + summary: Retrieve secret imports + description: >- + Fetches the secret imports based on the workspaceId, environment, and + folderId + parameters: + - name: workspaceId + in: query + description: ID of the workspace of secret imports to get + required: true + example: workspace12345 + schema: + type: string + - name: environment + in: query + description: Environment of secret imports to get + required: true + example: production + schema: + type: string + - name: folderId + in: query + description: 'ID of the folder containing the secret imports. Default: root' + required: false + example: folder12345 + schema: + type: string + responses: + '200': + description: Successfully retrieved secret import + content: + application/json: + schema: + type: object + properties: + secretImport: + type: object + description: Details of a secret import + '401': + description: Unauthorized access due to invalid token or scope + '403': + description: Forbidden access due to insufficient permissions + /api/v1/secret-imports/{id}: + put: + summary: Update a secret import + description: >- + Updates an existing secret import based on the provided ID and new + import details + parameters: + - name: id + in: path + required: true + schema: + type: string + description: ID of the secret import to be updated + example: import12345 + responses: + '200': + description: Successfully updated the secret import + content: + application/json: + schema: + type: object + properties: + message: + type: string + example: successfully updated secret import + '400': + description: Bad Request - Import not found + '401': + description: Unauthorized access due to invalid token or scope + '403': + description: Forbidden access due to insufficient permissions + requestBody: + content: + application/json: + schema: + type: object + properties: + secretImports: + type: array + description: List of new secret imports + items: + type: object + properties: + environment: + type: string + description: Environment of the secret import + example: production + secretPath: + type: string + description: Path of the secret import + example: /path/to/secret + required: + - environment + - secretPath + required: + - secretImports + delete: + summary: Delete secret import + description: Delete secret import + parameters: + - name: id + in: path + required: true + schema: + type: string + description: ID of the secret import + example: 12345abcde + responses: + '200': + description: OK + content: + application/json: + schema: + type: object + properties: + message: + type: string + example: successfully delete secret import + description: Confirmation of secret import deletion + requestBody: + content: + application/json: + schema: + type: object + properties: + secretImportEnv: + type: string + description: Import from environment + example: someWorkspaceId + secretImportPath: + type: string + description: Import from secret path + example: production + required: + - id + - secretImportEnv + - secretImportPath + /api/v1/secret-imports/secrets: + get: + description: '' + responses: + '200': + description: OK + /api/v1/roles/: + post: + description: '' + responses: + '200': + description: OK + get: + description: '' + responses: + '200': + description: OK + /api/v1/roles/{id}: + patch: + description: '' + parameters: + - name: id + in: path + required: true + schema: + type: string + responses: + '200': + description: OK + delete: + description: '' + parameters: + - name: id + in: path + required: true + schema: + type: string + responses: + '200': + description: OK + /api/v1/roles/organization/{orgId}/permissions: + get: + description: '' + parameters: + - name: orgId + in: path + required: true + schema: + type: string + responses: + '200': + description: OK + /api/v1/roles/workspace/{workspaceId}/permissions: + get: + description: '' + parameters: + - name: workspaceId + in: path + required: true + schema: + type: string responses: '200': description: OK /api/v2/signup/complete-account/signup: post: description: '' - parameters: [] + parameters: + - name: user-agent + in: header + schema: + type: string responses: '200': description: OK - '400': - description: Bad Request '403': description: Forbidden requestBody: @@ -1530,12 +2053,14 @@ paths: /api/v2/signup/complete-account/invite: post: description: '' - parameters: [] + parameters: + - name: user-agent + in: header + schema: + type: string responses: '200': description: OK - '400': - description: Bad Request '403': description: Forbidden requestBody: @@ -1571,12 +2096,9 @@ paths: /api/v2/auth/login1: post: description: '' - parameters: [] responses: '200': description: OK - '400': - description: Bad Request requestBody: content: application/json: @@ -1613,20 +2135,9 @@ paths: /api/v2/auth/mfa/send: post: description: '' - parameters: [] responses: '200': description: OK - '400': - description: Bad Request - requestBody: - content: - application/json: - schema: - type: object - properties: - email: - example: any /api/v2/auth/mfa/verify: post: description: '' @@ -1638,21 +2149,10 @@ paths: responses: '200': description: OK - requestBody: - content: - application/json: - schema: - type: object - properties: - email: - example: any - mfaToken: - example: any /api/v2/users/me: get: summary: Retrieve the current user on the request description: Retrieve the current user on the request - parameters: [] responses: '200': description: OK @@ -1665,30 +2165,32 @@ paths: type: object $ref: '#/components/schemas/CurrentUser' description: Current user on request - '400': - description: Bad Request + security: + - apiKeyAuth: [] /api/v2/users/me/mfa: patch: description: '' - parameters: [] + responses: + '200': + description: OK + /api/v2/users/me/name: + patch: + description: '' + responses: + '200': + description: OK + /api/v2/users/me/auth-methods: + put: + description: '' responses: '200': description: OK '400': description: Bad Request - requestBody: - content: - application/json: - schema: - type: object - properties: - isMfaEnabled: - example: any /api/v2/users/me/organizations: get: summary: Return organizations that current user is part of description: Return organizations that current user is part of - parameters: [] responses: '200': description: OK @@ -1702,8 +2204,42 @@ paths: items: $ref: '#/components/schemas/Organization' description: Organizations that user is part of - '400': - description: Bad Request + security: + - apiKeyAuth: [] + /api/v2/users/me/api-keys: + get: + description: '' + responses: + '200': + description: OK + post: + description: '' + responses: + '200': + description: OK + /api/v2/users/me/api-keys/{apiKeyDataId}: + delete: + description: '' + parameters: + - name: apiKeyDataId + in: path + required: true + schema: + type: string + responses: + '200': + description: OK + /api/v2/users/me/sessions: + get: + description: '' + responses: + '200': + description: OK + delete: + description: '' + responses: + '200': + description: OK /api/v2/organizations/{organizationId}/memberships: get: summary: Return organization memberships @@ -1728,8 +2264,8 @@ paths: items: $ref: '#/components/schemas/MembershipOrg' description: Memberships of organization - '400': - description: Bad Request + security: + - apiKeyAuth: [] /api/v2/organizations/{organizationId}/memberships/{membershipId}: patch: summary: Update organization membership @@ -1758,8 +2294,8 @@ paths: membership: $ref: '#/components/schemas/MembershipOrg' description: Updated organization membership - '400': - description: Bad Request + security: + - apiKeyAuth: [] requestBody: required: true content: @@ -1799,8 +2335,8 @@ paths: membership: $ref: '#/components/schemas/MembershipOrg' description: Deleted organization membership - '400': - description: Bad Request + security: + - apiKeyAuth: [] /api/v2/organizations/{organizationId}/workspaces: get: summary: Return projects in organization that user is part of @@ -1825,6 +2361,8 @@ paths: items: $ref: '#/components/schemas/Project' description: Projects of organization + security: + - apiKeyAuth: [] /api/v2/organizations/{organizationId}/service-accounts: get: description: '' @@ -1839,18 +2377,43 @@ paths: description: OK /api/v2/workspace/{workspaceId}/environments: post: - description: '' + summary: Create environment + description: Create environment parameters: - name: workspaceId in: path required: true schema: type: string + description: ID of project responses: '200': description: OK + content: + application/json: + schema: + type: object + properties: + message: + type: string + example: Successfully created new environment + workspace: + type: string + example: someWorkspaceId + environment: + type: object + properties: + name: + type: string + example: someEnvironmentName + slug: + type: string + example: someEnvironmentSlug + description: Response after creating a new environment '400': description: Bad Request + security: + - apiKeyAuth: [] requestBody: content: application/json: @@ -1858,22 +2421,50 @@ paths: type: object properties: environmentName: - example: any + type: string + description: Name of the environment + example: development environmentSlug: - example: any + type: string + description: Slug of the environment + example: dev-environment + required: + - environmentName + - environmentSlug put: - description: '' + summary: Rename workspace environment + description: Rename a specific environment within a workspace parameters: - name: workspaceId in: path required: true schema: type: string + description: ID of the workspace responses: '200': description: OK - '400': - description: Bad Request + content: + application/json: + schema: + type: object + properties: + message: + type: string + example: Successfully update environment + workspace: + type: string + example: someWorkspaceId + environment: + type: object + properties: + name: + type: string + example: Staging-Renamed + slug: + type: string + example: staging-renamed + description: Details of the renamed environment requestBody: content: application/json: @@ -1881,12 +2472,22 @@ paths: type: object properties: environmentName: - example: any + type: string + description: New name for the environment + example: Staging-Renamed environmentSlug: - example: any + type: string + description: New slug for the environment + example: staging-renamed oldEnvironmentSlug: - example: any - delete: + type: string + description: Current slug of the environment to rename + example: staging-old + required: + - environmentName + - environmentSlug + - oldEnvironmentSlug + patch: description: '' parameters: - name: workspaceId @@ -1897,8 +2498,36 @@ paths: responses: '200': description: OK - '400': - description: Bad Request + delete: + summary: Delete workspace environment + description: Delete a specific environment from a workspace + parameters: + - name: workspaceId + in: path + required: true + schema: + type: string + description: ID of the workspace + responses: + '200': + description: OK + content: + application/json: + schema: + type: object + properties: + message: + type: string + example: Successfully deleted environment + workspace: + type: string + example: someWorkspaceId + environment: + type: string + example: dev-environment + description: Response after deleting an environment from a workspace + security: + - apiKeyAuth: [] requestBody: content: application/json: @@ -1906,18 +2535,53 @@ paths: type: object properties: environmentSlug: - example: any + type: string + description: Slug of the environment to delete + example: dev-environment + required: + - environmentSlug get: - description: '' + summary: Get all accessible environments of a workspace + description: >- + Fetch all environments that the user has access to in a specified + workspace parameters: - name: workspaceId in: path required: true schema: type: string + description: ID of the workspace responses: '200': description: OK + content: + application/json: + schema: + type: object + properties: + accessibleEnvironments: + type: array + items: + type: object + properties: + name: + type: string + example: Development + slug: + type: string + example: development + isWriteDenied: + type: boolean + example: false + isReadDenied: + type: boolean + example: false + description: >- + List of environments the user has access to in the specified + workspace + security: + - apiKeyAuth: [] /api/v2/workspace/{workspaceId}/tags: get: description: '' @@ -1941,18 +2605,6 @@ paths: responses: '200': description: OK - requestBody: - content: - application/json: - schema: - type: object - properties: - name: - example: any - tagColor: - example: any - slug: - example: any /api/v2/workspace/tags/{tagId}: delete: description: '' @@ -1977,8 +2629,6 @@ paths: responses: '200': description: OK - '400': - description: Bad Request requestBody: content: application/json: @@ -2012,8 +2662,6 @@ paths: responses: '200': description: OK - '400': - description: Bad Request /api/v2/workspace/{workspaceId}/encrypted-key: get: summary: Return encrypted project key @@ -2035,8 +2683,8 @@ paths: items: $ref: '#/components/schemas/ProjectKey' description: Encrypted project key for the given project - '400': - description: Bad Request + security: + - apiKeyAuth: [] /api/v2/workspace/{workspaceId}/service-token-data: get: description: '' @@ -2049,8 +2697,6 @@ paths: responses: '200': description: OK - '400': - description: Bad Request /api/v2/workspace/{workspaceId}/memberships: get: summary: Return project memberships @@ -2075,8 +2721,8 @@ paths: items: $ref: '#/components/schemas/Membership' description: Memberships of project - '400': - description: Bad Request + security: + - apiKeyAuth: [] /api/v2/workspace/{workspaceId}/memberships/{membershipId}: patch: summary: Update project membership @@ -2105,8 +2751,8 @@ paths: membership: $ref: '#/components/schemas/Membership' description: Updated membership - '400': - description: Bad Request + security: + - apiKeyAuth: [] requestBody: required: true content: @@ -2144,8 +2790,8 @@ paths: membership: $ref: '#/components/schemas/Membership' description: Deleted membership - '400': - description: Bad Request + security: + - apiKeyAuth: [] /api/v2/workspace/{workspaceId}/auto-capitalization: patch: description: '' @@ -2158,16 +2804,6 @@ paths: responses: '200': description: OK - '400': - description: Bad Request - requestBody: - content: - application/json: - schema: - type: object - properties: - autoCapitalization: - example: any /api/v2/secret/batch-create/workspace/{workspaceId}/environment/{environment}: post: description: '' @@ -2343,18 +2979,6 @@ paths: responses: '200': description: OK - requestBody: - content: - application/json: - schema: - type: object - properties: - workspaceId: - example: any - environment: - example: any - requests: - example: any /api/v2/secrets/: post: summary: Create new secret(s) @@ -2379,6 +3003,8 @@ paths: description: >- Newly-created secrets for the given project and environment + security: + - apiKeyAuth: [] requestBody: required: true content: @@ -2415,10 +3041,6 @@ paths: in: header schema: type: string - - name: content - in: query - schema: - type: string responses: '200': description: OK @@ -2432,10 +3054,11 @@ paths: items: $ref: '#/components/schemas/Secret' description: Secrets for the given project and environment + security: + - apiKeyAuth: [] patch: summary: Update secret(s) description: Update secret(s) - parameters: [] responses: '200': description: OK @@ -2449,6 +3072,8 @@ paths: items: $ref: '#/components/schemas/Secret' description: Updated secrets + security: + - apiKeyAuth: [] requestBody: required: true content: @@ -2480,6 +3105,8 @@ paths: items: $ref: '#/components/schemas/Secret' description: Deleted secrets + security: + - apiKeyAuth: [] requestBody: required: true content: @@ -2494,7 +3121,6 @@ paths: get: summary: Return Infisical Token data description: Return Infisical Token data - parameters: [] responses: '200': description: OK @@ -2511,32 +3137,9 @@ paths: - bearerAuth: [] post: description: '' - parameters: [] responses: '200': description: OK - requestBody: - content: - application/json: - schema: - type: object - properties: - name: - example: any - workspaceId: - example: any - environment: - example: any - encryptedKey: - example: any - iv: - example: any - tag: - example: any - expiresIn: - example: any - permissions: - example: any /api/v2/service-token/{serviceTokenDataId}: delete: description: '' @@ -2549,48 +3152,58 @@ paths: responses: '200': description: OK - /api/v2/service-accounts/me: - get: - description: '' - parameters: [] - responses: - '200': - description: OK - /api/v2/service-accounts/{serviceAccountId}: - get: - description: '' - parameters: - - name: serviceAccountId - in: path - required: true - schema: - type: string - responses: - '200': - description: OK - delete: - description: '' - parameters: - - name: serviceAccountId - in: path - required: true - schema: - type: string - responses: - '200': - description: OK - /api/v2/service-accounts/: + /api/v3/auth/login1: post: description: '' - parameters: [] responses: '200': description: OK - /api/v2/service-accounts/{serviceAccountId}/name: + /api/v3/auth/login2: + post: + description: '' + parameters: + - name: user-agent + in: header + schema: + type: string + responses: + '200': + description: OK + '400': + description: Bad Request + /api/v3/secrets/raw: + get: + description: '' + responses: + '200': + description: OK + /api/v3/secrets/raw/{secretName}: + get: + description: '' + parameters: + - name: secretName + in: path + required: true + schema: + type: string + responses: + '200': + description: OK + post: + description: '' + parameters: + - name: secretName + in: path + required: true + schema: + type: string + responses: + '200': + description: OK patch: description: '' parameters: - - name: serviceAccountId + - name: secretName in: path required: true schema: @@ -2598,67 +3211,10 @@ paths: responses: '200': description: OK - requestBody: - content: - application/json: - schema: - type: object - properties: - name: - example: any - /api/v2/service-accounts/{serviceAccountId}/permissions/workspace: - get: - description: '' - parameters: - - name: serviceAccountId - in: path - required: true - schema: - type: string - responses: - '200': - description: OK - post: - description: '' - parameters: - - name: serviceAccountId - in: path - required: true - schema: - type: string - responses: - '200': - description: OK - '400': - description: Bad Request - requestBody: - content: - application/json: - schema: - type: object - properties: - environment: - example: any - workspaceId: - example: any - read: - example: any - write: - example: any - encryptedKey: - example: any - nonce: - example: any - /api/v2/service-accounts/{serviceAccountId}/permissions/workspace/{serviceAccountWorkspacePermissionId}: delete: description: '' parameters: - - name: serviceAccountId - in: path - required: true - schema: - type: string - - name: serviceAccountWorkspacePermissionId + - name: secretName in: path required: true schema: @@ -2666,75 +3222,9 @@ paths: responses: '200': description: OK - /api/v2/service-accounts/{serviceAccountId}/keys: - get: - description: '' - parameters: - - name: serviceAccountId - in: path - required: true - schema: - type: string - - name: workspaceId - in: query - schema: - type: string - responses: - '200': - description: OK - /api/v2/api-key/: - get: - description: '' - parameters: [] - responses: - '200': - description: OK - '400': - description: Bad Request - post: - description: '' - parameters: [] - responses: - '200': - description: OK - '400': - description: Bad Request - requestBody: - content: - application/json: - schema: - type: object - properties: - name: - example: any - expiresIn: - example: any - /api/v2/api-key/{apiKeyDataId}: - delete: - description: '' - parameters: - - name: apiKeyDataId - in: path - required: true - schema: - type: string - responses: - '200': - description: OK - '400': - description: Bad Request /api/v3/secrets/: get: description: '' - parameters: - - name: workspaceId - in: query - schema: - type: string - - name: environment - in: query - schema: - type: string responses: '200': description: OK @@ -2750,36 +3240,6 @@ paths: responses: '200': description: OK - requestBody: - content: - application/json: - schema: - type: object - properties: - workspaceId: - example: any - environment: - example: any - type: - example: any - secretKeyCiphertext: - example: any - secretKeyIV: - example: any - secretKeyTag: - example: any - secretValueCiphertext: - example: any - secretValueIV: - example: any - secretValueTag: - example: any - secretCommentCiphertext: - example: any - secretCommentIV: - example: any - secretCommentTag: - example: any get: description: '' parameters: @@ -2788,18 +3248,6 @@ paths: required: true schema: type: string - - name: workspaceId - in: query - schema: - type: string - - name: environment - in: query - schema: - type: string - - name: type - in: query - schema: - type: string responses: '200': description: OK @@ -2814,24 +3262,6 @@ paths: responses: '200': description: OK - requestBody: - content: - application/json: - schema: - type: object - properties: - workspaceId: - example: any - environment: - example: any - type: - example: any - secretValueCiphertext: - example: any - secretValueIV: - example: any - secretValueTag: - example: any delete: description: '' parameters: @@ -2843,18 +3273,6 @@ paths: responses: '200': description: OK - requestBody: - content: - application/json: - schema: - type: object - properties: - workspaceId: - example: any - environment: - example: any - type: - example: any /api/v3/workspaces/{workspaceId}/secrets/blind-index-status: get: description: '' @@ -2891,18 +3309,28 @@ paths: responses: '200': description: OK - requestBody: - content: - application/json: - schema: - type: object - properties: - secretsToUpdate: - example: any + /api/v3/signup/complete-account/signup: + post: + description: '' + parameters: + - name: authorization + in: header + schema: + type: string + - name: user-agent + in: header + schema: + type: string + responses: + '200': + description: OK + '400': + description: Bad Request + '403': + description: Forbidden /api/status: get: description: '' - parameters: [] responses: '200': description: OK diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 990b03377..e6847b42c 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -5,6 +5,8 @@ "packages": { "": { "dependencies": { + "@casl/ability": "^6.5.0", + "@casl/react": "^3.1.0", "@dnd-kit/core": "^6.0.8", "@dnd-kit/modifiers": "^6.0.1", "@dnd-kit/sortable": "^7.0.2", @@ -19,7 +21,7 @@ "@headlessui/react": "^1.7.7", "@hookform/resolvers": "^2.9.10", "@octokit/rest": "^19.0.7", - "@radix-ui/react-accordion": "^1.1.0", + "@radix-ui/react-accordion": "^1.1.2", "@radix-ui/react-alert-dialog": "^1.0.2", "@radix-ui/react-checkbox": "^1.0.1", "@radix-ui/react-dialog": "^1.0.2", @@ -39,6 +41,7 @@ "@stripe/stripe-js": "^1.46.0", "@tanstack/react-query": "^4.23.0", "@types/argon2-browser": "^1.18.1", + "@ucast/mongo2js": "^1.3.4", "add": "^2.0.6", "argon2-browser": "^1.18.0", "axios": "^0.27.2", @@ -63,6 +66,7 @@ "markdown-it": "^13.0.1", "next": "^12.3.4", "nprogress": "^0.2.0", + "picomatch": "^2.3.1", "posthog-js": "^1.58.0", "query-string": "^7.1.3", "react": "^17.0.2", @@ -88,7 +92,8 @@ "uuid": "^8.3.2", "uuidv4": "^6.2.13", "yaml": "^2.2.2", - "yup": "^0.32.11" + "yup": "^0.32.11", + "zod": "^3.22.0" }, "devDependencies": { "@storybook/addon-essentials": "^7.0.23", @@ -103,6 +108,7 @@ "@tailwindcss/typography": "^0.5.4", "@types/jsrp": "^0.2.4", "@types/node": "^18.11.9", + "@types/picomatch": "^2.3.0", "@types/react": "^18.0.26", "@types/sanitize-html": "^2.9.0", "@typescript-eslint/eslint-plugin": "^5.48.1", @@ -2458,6 +2464,26 @@ "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", "dev": true }, + "node_modules/@casl/ability": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@casl/ability/-/ability-6.5.0.tgz", + "integrity": "sha512-3guc94ugr5ylZQIpJTLz0CDfwNi0mxKVECj1vJUPAvs+Lwunh/dcuUjwzc4MHM9D8JOYX0XUZMEPedpB3vIbOw==", + "dependencies": { + "@ucast/mongo2js": "^1.3.0" + }, + "funding": { + "url": "https://github.com/stalniy/casl/blob/master/BACKERS.md" + } + }, + "node_modules/@casl/react": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@casl/react/-/react-3.1.0.tgz", + "integrity": "sha512-p4Xmex1Slxz/G0cBtZik+xyOkeOynBUe0UrMFTai6aYkYOb4NyUy3w+9rtnedjcuKijiow2HKJQjnSurLxdc/g==", + "peerDependencies": { + "@casl/ability": "^3.0.0 || ^4.0.0 || ^5.1.0 || ^6.0.0", + "react": "^16.0.0 || ^17.0.0 || ^18.0.0" + } + }, "node_modules/@colors/colors": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz", @@ -4235,24 +4261,230 @@ } }, "node_modules/@radix-ui/react-accordion": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-accordion/-/react-accordion-1.1.1.tgz", - "integrity": "sha512-TQtyyRubYe8DD6DYCovNLTjd2D+TFrNCpr99T5M3cYUbR7BsRxWsxfInjbQ1nHsdy2uPTcnJS5npyXPVfP0piw==", + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-accordion/-/react-accordion-1.1.2.tgz", + "integrity": "sha512-fDG7jcoNKVjSK6yfmuAs0EnPDro0WMXIhMtXdTBWqEioVW206ku+4Lw07e+13lUkFkpoEQ2PdeMIAGpdqEAmDg==", "dependencies": { "@babel/runtime": "^7.13.10", - "@radix-ui/primitive": "1.0.0", - "@radix-ui/react-collapsible": "1.0.2", - "@radix-ui/react-collection": "1.0.2", - "@radix-ui/react-compose-refs": "1.0.0", - "@radix-ui/react-context": "1.0.0", - "@radix-ui/react-direction": "1.0.0", - "@radix-ui/react-id": "1.0.0", - "@radix-ui/react-primitive": "1.0.2", - "@radix-ui/react-use-controllable-state": "1.0.0" + "@radix-ui/primitive": "1.0.1", + "@radix-ui/react-collapsible": "1.0.3", + "@radix-ui/react-collection": "1.0.3", + "@radix-ui/react-compose-refs": "1.0.1", + "@radix-ui/react-context": "1.0.1", + "@radix-ui/react-direction": "1.0.1", + "@radix-ui/react-id": "1.0.1", + "@radix-ui/react-primitive": "1.0.3", + "@radix-ui/react-use-controllable-state": "1.0.1" }, "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0", "react-dom": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-accordion/node_modules/@radix-ui/primitive": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.0.1.tgz", + "integrity": "sha512-yQ8oGX2GVsEYMWGxcovu1uGWPCxV5BFfeeYxqPmuAzUyLT9qmaMXSAhXpb0WrspIeqYzdJpkh2vHModJPgRIaw==", + "dependencies": { + "@babel/runtime": "^7.13.10" + } + }, + "node_modules/@radix-ui/react-accordion/node_modules/@radix-ui/react-collection": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.0.3.tgz", + "integrity": "sha512-3SzW+0PW7yBBoQlT8wNcGtaxaD0XSu0uLUFgrtHY08Acx05TaHaOmVLR73c0j/cqpDy53KBMO7s0dx2wmOIDIA==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-compose-refs": "1.0.1", + "@radix-ui/react-context": "1.0.1", + "@radix-ui/react-primitive": "1.0.3", + "@radix-ui/react-slot": "1.0.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-accordion/node_modules/@radix-ui/react-compose-refs": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.0.1.tgz", + "integrity": "sha512-fDSBgd44FKHa1FRMU59qBMPFcl2PZE+2nmqunj+BWFyYYjnhIDWL2ItDs3rrbJDQOtzt5nIebLCQc4QRfz6LJw==", + "dependencies": { + "@babel/runtime": "^7.13.10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-accordion/node_modules/@radix-ui/react-context": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.0.1.tgz", + "integrity": "sha512-ebbrdFoYTcuZ0v4wG5tedGnp9tzcV8awzsxYph7gXUyvnNLuTIcCk1q17JEbnVhXAKG9oX3KtchwiMIAYp9NLg==", + "dependencies": { + "@babel/runtime": "^7.13.10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-accordion/node_modules/@radix-ui/react-direction": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.0.1.tgz", + "integrity": "sha512-RXcvnXgyvYvBEOhCBuddKecVkoMiI10Jcm5cTI7abJRAHYfFxeu+FBQs/DvdxSYucxR5mna0dNsL6QFlds5TMA==", + "dependencies": { + "@babel/runtime": "^7.13.10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-accordion/node_modules/@radix-ui/react-id": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.0.1.tgz", + "integrity": "sha512-tI7sT/kqYp8p96yGWY1OAnLHrqDgzHefRBKQ2YAkBS5ja7QLcZ9Z/uY7bEjPUatf8RomoXM8/1sMj1IJaE5UzQ==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-use-layout-effect": "1.0.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-accordion/node_modules/@radix-ui/react-primitive": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-1.0.3.tgz", + "integrity": "sha512-yi58uVyoAcK/Nq1inRY56ZSjKypBNKTa/1mcL8qdl6oJeEaDbOldlzrGn7P6Q3Id5d+SYNGc5AJgc4vGhjs5+g==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-slot": "1.0.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-accordion/node_modules/@radix-ui/react-slot": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.0.2.tgz", + "integrity": "sha512-YeTpuq4deV+6DusvVUW4ivBgnkHwECUu0BiN43L5UCDFgdhsRUWAghhTF5MbvNTPzmiFOx90asDSUjWuCNapwg==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-compose-refs": "1.0.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-accordion/node_modules/@radix-ui/react-use-callback-ref": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.0.1.tgz", + "integrity": "sha512-D94LjX4Sp0xJFVaoQOd3OO9k7tpBYNOXdVhkltUbGv2Qb9OXdrg/CpsjlZv7ia14Sylv398LswWBVVu5nqKzAQ==", + "dependencies": { + "@babel/runtime": "^7.13.10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-accordion/node_modules/@radix-ui/react-use-controllable-state": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.0.1.tgz", + "integrity": "sha512-Svl5GY5FQeN758fWKrjM6Qb7asvXeiZltlT4U2gVfl8Gx5UAv2sMR0LWo8yhsIZh2oQ0eFdZ59aoOOMV7b47VA==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-use-callback-ref": "1.0.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-accordion/node_modules/@radix-ui/react-use-layout-effect": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.0.1.tgz", + "integrity": "sha512-v/5RegiJWYdoCvMnITBkNNx6bCj20fiaJnWtRkU18yITptraXjffz5Qbn05uOiQnOvi+dbkznkoaMltz1GnszQ==", + "dependencies": { + "@babel/runtime": "^7.13.10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } } }, "node_modules/@radix-ui/react-alert-dialog": { @@ -4307,23 +4539,210 @@ } }, "node_modules/@radix-ui/react-collapsible": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-collapsible/-/react-collapsible-1.0.2.tgz", - "integrity": "sha512-QNiDT6Au8jUU0K1WV+HEd4loH7C5CKQjeXxskwqyiyAkyCmW7qlQM5vSSJCIoQC+OVPyhgafSmGudRP8Qm1/gA==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-collapsible/-/react-collapsible-1.0.3.tgz", + "integrity": "sha512-UBmVDkmR6IvDsloHVN+3rtx4Mi5TFvylYXpluuv0f37dtaz3H99bp8No0LGXRigVpl3UAT4l9j6bIchh42S/Gg==", "dependencies": { "@babel/runtime": "^7.13.10", - "@radix-ui/primitive": "1.0.0", - "@radix-ui/react-compose-refs": "1.0.0", - "@radix-ui/react-context": "1.0.0", - "@radix-ui/react-id": "1.0.0", - "@radix-ui/react-presence": "1.0.0", - "@radix-ui/react-primitive": "1.0.2", - "@radix-ui/react-use-controllable-state": "1.0.0", - "@radix-ui/react-use-layout-effect": "1.0.0" + "@radix-ui/primitive": "1.0.1", + "@radix-ui/react-compose-refs": "1.0.1", + "@radix-ui/react-context": "1.0.1", + "@radix-ui/react-id": "1.0.1", + "@radix-ui/react-presence": "1.0.1", + "@radix-ui/react-primitive": "1.0.3", + "@radix-ui/react-use-controllable-state": "1.0.1", + "@radix-ui/react-use-layout-effect": "1.0.1" }, "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0", "react-dom": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-collapsible/node_modules/@radix-ui/primitive": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.0.1.tgz", + "integrity": "sha512-yQ8oGX2GVsEYMWGxcovu1uGWPCxV5BFfeeYxqPmuAzUyLT9qmaMXSAhXpb0WrspIeqYzdJpkh2vHModJPgRIaw==", + "dependencies": { + "@babel/runtime": "^7.13.10" + } + }, + "node_modules/@radix-ui/react-collapsible/node_modules/@radix-ui/react-compose-refs": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.0.1.tgz", + "integrity": "sha512-fDSBgd44FKHa1FRMU59qBMPFcl2PZE+2nmqunj+BWFyYYjnhIDWL2ItDs3rrbJDQOtzt5nIebLCQc4QRfz6LJw==", + "dependencies": { + "@babel/runtime": "^7.13.10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-collapsible/node_modules/@radix-ui/react-context": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.0.1.tgz", + "integrity": "sha512-ebbrdFoYTcuZ0v4wG5tedGnp9tzcV8awzsxYph7gXUyvnNLuTIcCk1q17JEbnVhXAKG9oX3KtchwiMIAYp9NLg==", + "dependencies": { + "@babel/runtime": "^7.13.10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-collapsible/node_modules/@radix-ui/react-id": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.0.1.tgz", + "integrity": "sha512-tI7sT/kqYp8p96yGWY1OAnLHrqDgzHefRBKQ2YAkBS5ja7QLcZ9Z/uY7bEjPUatf8RomoXM8/1sMj1IJaE5UzQ==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-use-layout-effect": "1.0.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-collapsible/node_modules/@radix-ui/react-presence": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.0.1.tgz", + "integrity": "sha512-UXLW4UAbIY5ZjcvzjfRFo5gxva8QirC9hF7wRE4U5gz+TP0DbRk+//qyuAQ1McDxBt1xNMBTaciFGvEmJvAZCg==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-compose-refs": "1.0.1", + "@radix-ui/react-use-layout-effect": "1.0.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-collapsible/node_modules/@radix-ui/react-primitive": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-1.0.3.tgz", + "integrity": "sha512-yi58uVyoAcK/Nq1inRY56ZSjKypBNKTa/1mcL8qdl6oJeEaDbOldlzrGn7P6Q3Id5d+SYNGc5AJgc4vGhjs5+g==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-slot": "1.0.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-collapsible/node_modules/@radix-ui/react-slot": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.0.2.tgz", + "integrity": "sha512-YeTpuq4deV+6DusvVUW4ivBgnkHwECUu0BiN43L5UCDFgdhsRUWAghhTF5MbvNTPzmiFOx90asDSUjWuCNapwg==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-compose-refs": "1.0.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-collapsible/node_modules/@radix-ui/react-use-callback-ref": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.0.1.tgz", + "integrity": "sha512-D94LjX4Sp0xJFVaoQOd3OO9k7tpBYNOXdVhkltUbGv2Qb9OXdrg/CpsjlZv7ia14Sylv398LswWBVVu5nqKzAQ==", + "dependencies": { + "@babel/runtime": "^7.13.10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-collapsible/node_modules/@radix-ui/react-use-controllable-state": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.0.1.tgz", + "integrity": "sha512-Svl5GY5FQeN758fWKrjM6Qb7asvXeiZltlT4U2gVfl8Gx5UAv2sMR0LWo8yhsIZh2oQ0eFdZ59aoOOMV7b47VA==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-use-callback-ref": "1.0.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-collapsible/node_modules/@radix-ui/react-use-layout-effect": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.0.1.tgz", + "integrity": "sha512-v/5RegiJWYdoCvMnITBkNNx6bCj20fiaJnWtRkU18yITptraXjffz5Qbn05uOiQnOvi+dbkznkoaMltz1GnszQ==", + "dependencies": { + "@babel/runtime": "^7.13.10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } } }, "node_modules/@radix-ui/react-collection": { @@ -7975,6 +8394,12 @@ "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.0.tgz", "integrity": "sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA==" }, + "node_modules/@types/picomatch": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@types/picomatch/-/picomatch-2.3.0.tgz", + "integrity": "sha512-O397rnSS9iQI4OirieAtsDqvCj4+3eY1J+EPdNTKuHuRWIfUoGyzX294o8C4KJYaLqgSrd2o60c5EqCU8Zv02g==", + "dev": true + }, "node_modules/@types/pretty-hrtime": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@types/pretty-hrtime/-/pretty-hrtime-1.0.1.tgz", @@ -8457,6 +8882,37 @@ "url": "https://opencollective.com/typescript-eslint" } }, + "node_modules/@ucast/core": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/@ucast/core/-/core-1.10.2.tgz", + "integrity": "sha512-ons5CwXZ/51wrUPfoduC+cO7AS1/wRb0ybpQJ9RrssossDxVy4t49QxWoWgfBDvVKsz9VXzBk9z0wqTdZ+Cq8g==" + }, + "node_modules/@ucast/js": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@ucast/js/-/js-3.0.3.tgz", + "integrity": "sha512-jBBqt57T5WagkAjqfCIIE5UYVdaXYgGkOFYv2+kjq2AVpZ2RIbwCo/TujJpDlwTVluUI+WpnRpoGU2tSGlEvFQ==", + "dependencies": { + "@ucast/core": "^1.0.0" + } + }, + "node_modules/@ucast/mongo": { + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/@ucast/mongo/-/mongo-2.4.3.tgz", + "integrity": "sha512-XcI8LclrHWP83H+7H2anGCEeDq0n+12FU2mXCTz6/Tva9/9ddK/iacvvhCyW6cijAAOILmt0tWplRyRhVyZLsA==", + "dependencies": { + "@ucast/core": "^1.4.1" + } + }, + "node_modules/@ucast/mongo2js": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@ucast/mongo2js/-/mongo2js-1.3.4.tgz", + "integrity": "sha512-ahazOr1HtelA5AC1KZ9x0UwPMqqimvfmtSm/PRRSeKKeE5G2SCqTgwiNzO7i9jS8zA3dzXpKVPpXMkcYLnyItA==", + "dependencies": { + "@ucast/core": "^1.6.1", + "@ucast/js": "^3.0.0", + "@ucast/mongo": "^2.4.0" + } + }, "node_modules/@webassemblyjs/ast": { "version": "1.11.6", "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.11.6.tgz", @@ -23188,6 +23644,14 @@ "engines": { "node": ">=10" } + }, + "node_modules/zod": { + "version": "3.22.0", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.22.0.tgz", + "integrity": "sha512-y5KZY/ssf5n7hCGDGGtcJO/EBJEm5Pa+QQvFBeyMOtnFYOSflalxIFFvdaYevPhePcmcKC4aTbFkCcXN7D0O8Q==", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } } }, "dependencies": { @@ -24852,6 +25316,20 @@ "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", "dev": true }, + "@casl/ability": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@casl/ability/-/ability-6.5.0.tgz", + "integrity": "sha512-3guc94ugr5ylZQIpJTLz0CDfwNi0mxKVECj1vJUPAvs+Lwunh/dcuUjwzc4MHM9D8JOYX0XUZMEPedpB3vIbOw==", + "requires": { + "@ucast/mongo2js": "^1.3.0" + } + }, + "@casl/react": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@casl/react/-/react-3.1.0.tgz", + "integrity": "sha512-p4Xmex1Slxz/G0cBtZik+xyOkeOynBUe0UrMFTai6aYkYOb4NyUy3w+9rtnedjcuKijiow2HKJQjnSurLxdc/g==", + "requires": {} + }, "@colors/colors": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz", @@ -26029,20 +26507,118 @@ } }, "@radix-ui/react-accordion": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-accordion/-/react-accordion-1.1.1.tgz", - "integrity": "sha512-TQtyyRubYe8DD6DYCovNLTjd2D+TFrNCpr99T5M3cYUbR7BsRxWsxfInjbQ1nHsdy2uPTcnJS5npyXPVfP0piw==", + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-accordion/-/react-accordion-1.1.2.tgz", + "integrity": "sha512-fDG7jcoNKVjSK6yfmuAs0EnPDro0WMXIhMtXdTBWqEioVW206ku+4Lw07e+13lUkFkpoEQ2PdeMIAGpdqEAmDg==", "requires": { "@babel/runtime": "^7.13.10", - "@radix-ui/primitive": "1.0.0", - "@radix-ui/react-collapsible": "1.0.2", - "@radix-ui/react-collection": "1.0.2", - "@radix-ui/react-compose-refs": "1.0.0", - "@radix-ui/react-context": "1.0.0", - "@radix-ui/react-direction": "1.0.0", - "@radix-ui/react-id": "1.0.0", - "@radix-ui/react-primitive": "1.0.2", - "@radix-ui/react-use-controllable-state": "1.0.0" + "@radix-ui/primitive": "1.0.1", + "@radix-ui/react-collapsible": "1.0.3", + "@radix-ui/react-collection": "1.0.3", + "@radix-ui/react-compose-refs": "1.0.1", + "@radix-ui/react-context": "1.0.1", + "@radix-ui/react-direction": "1.0.1", + "@radix-ui/react-id": "1.0.1", + "@radix-ui/react-primitive": "1.0.3", + "@radix-ui/react-use-controllable-state": "1.0.1" + }, + "dependencies": { + "@radix-ui/primitive": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.0.1.tgz", + "integrity": "sha512-yQ8oGX2GVsEYMWGxcovu1uGWPCxV5BFfeeYxqPmuAzUyLT9qmaMXSAhXpb0WrspIeqYzdJpkh2vHModJPgRIaw==", + "requires": { + "@babel/runtime": "^7.13.10" + } + }, + "@radix-ui/react-collection": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.0.3.tgz", + "integrity": "sha512-3SzW+0PW7yBBoQlT8wNcGtaxaD0XSu0uLUFgrtHY08Acx05TaHaOmVLR73c0j/cqpDy53KBMO7s0dx2wmOIDIA==", + "requires": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-compose-refs": "1.0.1", + "@radix-ui/react-context": "1.0.1", + "@radix-ui/react-primitive": "1.0.3", + "@radix-ui/react-slot": "1.0.2" + } + }, + "@radix-ui/react-compose-refs": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.0.1.tgz", + "integrity": "sha512-fDSBgd44FKHa1FRMU59qBMPFcl2PZE+2nmqunj+BWFyYYjnhIDWL2ItDs3rrbJDQOtzt5nIebLCQc4QRfz6LJw==", + "requires": { + "@babel/runtime": "^7.13.10" + } + }, + "@radix-ui/react-context": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.0.1.tgz", + "integrity": "sha512-ebbrdFoYTcuZ0v4wG5tedGnp9tzcV8awzsxYph7gXUyvnNLuTIcCk1q17JEbnVhXAKG9oX3KtchwiMIAYp9NLg==", + "requires": { + "@babel/runtime": "^7.13.10" + } + }, + "@radix-ui/react-direction": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.0.1.tgz", + "integrity": "sha512-RXcvnXgyvYvBEOhCBuddKecVkoMiI10Jcm5cTI7abJRAHYfFxeu+FBQs/DvdxSYucxR5mna0dNsL6QFlds5TMA==", + "requires": { + "@babel/runtime": "^7.13.10" + } + }, + "@radix-ui/react-id": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.0.1.tgz", + "integrity": "sha512-tI7sT/kqYp8p96yGWY1OAnLHrqDgzHefRBKQ2YAkBS5ja7QLcZ9Z/uY7bEjPUatf8RomoXM8/1sMj1IJaE5UzQ==", + "requires": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-use-layout-effect": "1.0.1" + } + }, + "@radix-ui/react-primitive": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-1.0.3.tgz", + "integrity": "sha512-yi58uVyoAcK/Nq1inRY56ZSjKypBNKTa/1mcL8qdl6oJeEaDbOldlzrGn7P6Q3Id5d+SYNGc5AJgc4vGhjs5+g==", + "requires": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-slot": "1.0.2" + } + }, + "@radix-ui/react-slot": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.0.2.tgz", + "integrity": "sha512-YeTpuq4deV+6DusvVUW4ivBgnkHwECUu0BiN43L5UCDFgdhsRUWAghhTF5MbvNTPzmiFOx90asDSUjWuCNapwg==", + "requires": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-compose-refs": "1.0.1" + } + }, + "@radix-ui/react-use-callback-ref": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.0.1.tgz", + "integrity": "sha512-D94LjX4Sp0xJFVaoQOd3OO9k7tpBYNOXdVhkltUbGv2Qb9OXdrg/CpsjlZv7ia14Sylv398LswWBVVu5nqKzAQ==", + "requires": { + "@babel/runtime": "^7.13.10" + } + }, + "@radix-ui/react-use-controllable-state": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.0.1.tgz", + "integrity": "sha512-Svl5GY5FQeN758fWKrjM6Qb7asvXeiZltlT4U2gVfl8Gx5UAv2sMR0LWo8yhsIZh2oQ0eFdZ59aoOOMV7b47VA==", + "requires": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-use-callback-ref": "1.0.1" + } + }, + "@radix-ui/react-use-layout-effect": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.0.1.tgz", + "integrity": "sha512-v/5RegiJWYdoCvMnITBkNNx6bCj20fiaJnWtRkU18yITptraXjffz5Qbn05uOiQnOvi+dbkznkoaMltz1GnszQ==", + "requires": { + "@babel/runtime": "^7.13.10" + } + } } }, "@radix-ui/react-alert-dialog": { @@ -26085,19 +26661,107 @@ } }, "@radix-ui/react-collapsible": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-collapsible/-/react-collapsible-1.0.2.tgz", - "integrity": "sha512-QNiDT6Au8jUU0K1WV+HEd4loH7C5CKQjeXxskwqyiyAkyCmW7qlQM5vSSJCIoQC+OVPyhgafSmGudRP8Qm1/gA==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-collapsible/-/react-collapsible-1.0.3.tgz", + "integrity": "sha512-UBmVDkmR6IvDsloHVN+3rtx4Mi5TFvylYXpluuv0f37dtaz3H99bp8No0LGXRigVpl3UAT4l9j6bIchh42S/Gg==", "requires": { "@babel/runtime": "^7.13.10", - "@radix-ui/primitive": "1.0.0", - "@radix-ui/react-compose-refs": "1.0.0", - "@radix-ui/react-context": "1.0.0", - "@radix-ui/react-id": "1.0.0", - "@radix-ui/react-presence": "1.0.0", - "@radix-ui/react-primitive": "1.0.2", - "@radix-ui/react-use-controllable-state": "1.0.0", - "@radix-ui/react-use-layout-effect": "1.0.0" + "@radix-ui/primitive": "1.0.1", + "@radix-ui/react-compose-refs": "1.0.1", + "@radix-ui/react-context": "1.0.1", + "@radix-ui/react-id": "1.0.1", + "@radix-ui/react-presence": "1.0.1", + "@radix-ui/react-primitive": "1.0.3", + "@radix-ui/react-use-controllable-state": "1.0.1", + "@radix-ui/react-use-layout-effect": "1.0.1" + }, + "dependencies": { + "@radix-ui/primitive": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.0.1.tgz", + "integrity": "sha512-yQ8oGX2GVsEYMWGxcovu1uGWPCxV5BFfeeYxqPmuAzUyLT9qmaMXSAhXpb0WrspIeqYzdJpkh2vHModJPgRIaw==", + "requires": { + "@babel/runtime": "^7.13.10" + } + }, + "@radix-ui/react-compose-refs": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.0.1.tgz", + "integrity": "sha512-fDSBgd44FKHa1FRMU59qBMPFcl2PZE+2nmqunj+BWFyYYjnhIDWL2ItDs3rrbJDQOtzt5nIebLCQc4QRfz6LJw==", + "requires": { + "@babel/runtime": "^7.13.10" + } + }, + "@radix-ui/react-context": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.0.1.tgz", + "integrity": "sha512-ebbrdFoYTcuZ0v4wG5tedGnp9tzcV8awzsxYph7gXUyvnNLuTIcCk1q17JEbnVhXAKG9oX3KtchwiMIAYp9NLg==", + "requires": { + "@babel/runtime": "^7.13.10" + } + }, + "@radix-ui/react-id": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.0.1.tgz", + "integrity": "sha512-tI7sT/kqYp8p96yGWY1OAnLHrqDgzHefRBKQ2YAkBS5ja7QLcZ9Z/uY7bEjPUatf8RomoXM8/1sMj1IJaE5UzQ==", + "requires": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-use-layout-effect": "1.0.1" + } + }, + "@radix-ui/react-presence": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.0.1.tgz", + "integrity": "sha512-UXLW4UAbIY5ZjcvzjfRFo5gxva8QirC9hF7wRE4U5gz+TP0DbRk+//qyuAQ1McDxBt1xNMBTaciFGvEmJvAZCg==", + "requires": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-compose-refs": "1.0.1", + "@radix-ui/react-use-layout-effect": "1.0.1" + } + }, + "@radix-ui/react-primitive": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-1.0.3.tgz", + "integrity": "sha512-yi58uVyoAcK/Nq1inRY56ZSjKypBNKTa/1mcL8qdl6oJeEaDbOldlzrGn7P6Q3Id5d+SYNGc5AJgc4vGhjs5+g==", + "requires": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-slot": "1.0.2" + } + }, + "@radix-ui/react-slot": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.0.2.tgz", + "integrity": "sha512-YeTpuq4deV+6DusvVUW4ivBgnkHwECUu0BiN43L5UCDFgdhsRUWAghhTF5MbvNTPzmiFOx90asDSUjWuCNapwg==", + "requires": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-compose-refs": "1.0.1" + } + }, + "@radix-ui/react-use-callback-ref": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.0.1.tgz", + "integrity": "sha512-D94LjX4Sp0xJFVaoQOd3OO9k7tpBYNOXdVhkltUbGv2Qb9OXdrg/CpsjlZv7ia14Sylv398LswWBVVu5nqKzAQ==", + "requires": { + "@babel/runtime": "^7.13.10" + } + }, + "@radix-ui/react-use-controllable-state": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.0.1.tgz", + "integrity": "sha512-Svl5GY5FQeN758fWKrjM6Qb7asvXeiZltlT4U2gVfl8Gx5UAv2sMR0LWo8yhsIZh2oQ0eFdZ59aoOOMV7b47VA==", + "requires": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-use-callback-ref": "1.0.1" + } + }, + "@radix-ui/react-use-layout-effect": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.0.1.tgz", + "integrity": "sha512-v/5RegiJWYdoCvMnITBkNNx6bCj20fiaJnWtRkU18yITptraXjffz5Qbn05uOiQnOvi+dbkznkoaMltz1GnszQ==", + "requires": { + "@babel/runtime": "^7.13.10" + } + } } }, "@radix-ui/react-collection": { @@ -28759,6 +29423,12 @@ "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.0.tgz", "integrity": "sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA==" }, + "@types/picomatch": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@types/picomatch/-/picomatch-2.3.0.tgz", + "integrity": "sha512-O397rnSS9iQI4OirieAtsDqvCj4+3eY1J+EPdNTKuHuRWIfUoGyzX294o8C4KJYaLqgSrd2o60c5EqCU8Zv02g==", + "dev": true + }, "@types/pretty-hrtime": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@types/pretty-hrtime/-/pretty-hrtime-1.0.1.tgz", @@ -29108,6 +29778,37 @@ "eslint-visitor-keys": "^3.3.0" } }, + "@ucast/core": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/@ucast/core/-/core-1.10.2.tgz", + "integrity": "sha512-ons5CwXZ/51wrUPfoduC+cO7AS1/wRb0ybpQJ9RrssossDxVy4t49QxWoWgfBDvVKsz9VXzBk9z0wqTdZ+Cq8g==" + }, + "@ucast/js": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@ucast/js/-/js-3.0.3.tgz", + "integrity": "sha512-jBBqt57T5WagkAjqfCIIE5UYVdaXYgGkOFYv2+kjq2AVpZ2RIbwCo/TujJpDlwTVluUI+WpnRpoGU2tSGlEvFQ==", + "requires": { + "@ucast/core": "^1.0.0" + } + }, + "@ucast/mongo": { + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/@ucast/mongo/-/mongo-2.4.3.tgz", + "integrity": "sha512-XcI8LclrHWP83H+7H2anGCEeDq0n+12FU2mXCTz6/Tva9/9ddK/iacvvhCyW6cijAAOILmt0tWplRyRhVyZLsA==", + "requires": { + "@ucast/core": "^1.4.1" + } + }, + "@ucast/mongo2js": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@ucast/mongo2js/-/mongo2js-1.3.4.tgz", + "integrity": "sha512-ahazOr1HtelA5AC1KZ9x0UwPMqqimvfmtSm/PRRSeKKeE5G2SCqTgwiNzO7i9jS8zA3dzXpKVPpXMkcYLnyItA==", + "requires": { + "@ucast/core": "^1.6.1", + "@ucast/js": "^3.0.0", + "@ucast/mongo": "^2.4.0" + } + }, "@webassemblyjs/ast": { "version": "1.11.6", "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.11.6.tgz", @@ -40082,6 +40783,11 @@ "property-expr": "^2.0.4", "toposort": "^2.0.2" } + }, + "zod": { + "version": "3.22.0", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.22.0.tgz", + "integrity": "sha512-y5KZY/ssf5n7hCGDGGtcJO/EBJEm5Pa+QQvFBeyMOtnFYOSflalxIFFvdaYevPhePcmcKC4aTbFkCcXN7D0O8Q==" } } } diff --git a/frontend/package.json b/frontend/package.json index fab8b7033..d954da2a8 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -13,6 +13,8 @@ "build-storybook": "storybook build" }, "dependencies": { + "@casl/ability": "^6.5.0", + "@casl/react": "^3.1.0", "@dnd-kit/core": "^6.0.8", "@dnd-kit/modifiers": "^6.0.1", "@dnd-kit/sortable": "^7.0.2", @@ -27,7 +29,7 @@ "@headlessui/react": "^1.7.7", "@hookform/resolvers": "^2.9.10", "@octokit/rest": "^19.0.7", - "@radix-ui/react-accordion": "^1.1.0", + "@radix-ui/react-accordion": "^1.1.2", "@radix-ui/react-alert-dialog": "^1.0.2", "@radix-ui/react-checkbox": "^1.0.1", "@radix-ui/react-dialog": "^1.0.2", @@ -47,6 +49,7 @@ "@stripe/stripe-js": "^1.46.0", "@tanstack/react-query": "^4.23.0", "@types/argon2-browser": "^1.18.1", + "@ucast/mongo2js": "^1.3.4", "add": "^2.0.6", "argon2-browser": "^1.18.0", "axios": "^0.27.2", @@ -71,6 +74,7 @@ "markdown-it": "^13.0.1", "next": "^12.3.4", "nprogress": "^0.2.0", + "picomatch": "^2.3.1", "posthog-js": "^1.58.0", "query-string": "^7.1.3", "react": "^17.0.2", @@ -96,7 +100,8 @@ "uuid": "^8.3.2", "uuidv4": "^6.2.13", "yaml": "^2.2.2", - "yup": "^0.32.11" + "yup": "^0.32.11", + "zod": "^3.22.0" }, "devDependencies": { "@storybook/addon-essentials": "^7.0.23", @@ -111,6 +116,7 @@ "@tailwindcss/typography": "^0.5.4", "@types/jsrp": "^0.2.4", "@types/node": "^18.11.9", + "@types/picomatch": "^2.3.0", "@types/react": "^18.0.26", "@types/sanitize-html": "^2.9.0", "@typescript-eslint/eslint-plugin": "^5.48.1", diff --git a/frontend/src/components/AddTagPopoverContent/AddTagPopoverContent.tsx b/frontend/src/components/AddTagPopoverContent/AddTagPopoverContent.tsx index a8b965269..77ca8c24f 100644 --- a/frontend/src/components/AddTagPopoverContent/AddTagPopoverContent.tsx +++ b/frontend/src/components/AddTagPopoverContent/AddTagPopoverContent.tsx @@ -1,78 +1,92 @@ - import { faPlus } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { Checkbox, PopoverContent } from "@app/components/v2"; +import { Button, Checkbox, PopoverContent } from "@app/components/v2"; +import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context"; import { WsTag } from "../../hooks/api/tags/types"; +import { ProjectPermissionCan } from "../permissions"; interface Props { - wsTags: WsTag[] | undefined; - secKey: string; - selectedTagIds: Record; - handleSelectTag: (wsTag: WsTag) => void; - handleTagOnMouseEnter: (wsTag: WsTag) => void; - handleTagOnMouseLeave: () => void; - checkIfTagIsVisible: (wsTag: WsTag) => boolean; - handleOnCreateTagOpen: () => void + wsTags: WsTag[] | undefined; + secKey: string; + selectedTagIds: Record; + handleSelectTag: (wsTag: WsTag) => void; + handleTagOnMouseEnter: (wsTag: WsTag) => void; + handleTagOnMouseLeave: () => void; + checkIfTagIsVisible: (wsTag: WsTag) => boolean; + handleOnCreateTagOpen: () => void; } const AddTagPopoverContent = ({ - wsTags, - secKey, - selectedTagIds, - handleSelectTag, - handleTagOnMouseEnter, - handleTagOnMouseLeave, - checkIfTagIsVisible, - handleOnCreateTagOpen + wsTags, + secKey, + selectedTagIds, + handleSelectTag, + handleTagOnMouseEnter, + handleTagOnMouseLeave, + checkIfTagIsVisible, + handleOnCreateTagOpen }: Props) => { - return ( - -
- Add tags to {secKey || "this secret"} + return ( + +
+ Add tags to {secKey || "this secret"} +
+
+
+ {wsTags?.map((wsTag: WsTag) => ( +
handleSelectTag(wsTag)} + onMouseEnter={() => handleTagOnMouseEnter(wsTag)} + onMouseLeave={() => handleTagOnMouseLeave()} + tabIndex={0} + role="button" + onKeyDown={() => {}} + > + {(checkIfTagIsVisible(wsTag) || selectedTagIds?.[wsTag.slug]) && ( + + )} +
+
+ {" "} +
+ {wsTag.slug}
-
-
- {wsTags?.map((wsTag: WsTag) => ( -
handleSelectTag(wsTag)} - onMouseEnter={() => handleTagOnMouseEnter(wsTag)} - onMouseLeave={() => handleTagOnMouseLeave()} - tabIndex={0} role="button" - onKeyDown={() => { }}> - { +
+ ))} + + {(isAllowed) => ( + + )} + +
+ + ); +}; - (checkIfTagIsVisible(wsTag) || selectedTagIds?.[wsTag.slug]) && - } -
-
- - {wsTag.slug} - -
-
- ))} -
handleOnCreateTagOpen()} - tabIndex={0} role="button" - onKeyDown={() => { }}> - - Add new tag -
-
- - ) -} - -export default AddTagPopoverContent \ No newline at end of file +export default AddTagPopoverContent; diff --git a/frontend/src/components/permissions/OrgPermissionCan.tsx b/frontend/src/components/permissions/OrgPermissionCan.tsx new file mode 100644 index 000000000..4366110a9 --- /dev/null +++ b/frontend/src/components/permissions/OrgPermissionCan.tsx @@ -0,0 +1,49 @@ +import { FunctionComponent, ReactNode } from "react"; +import { BoundCanProps, Can } from "@casl/react"; + +import { TOrgPermission, useOrgPermission } from "@app/context/OrgPermissionContext"; + +import { Tooltip } from "../v2"; + +type Props = { + label?: ReactNode; + // this prop is used when there exist already a tooltip as helper text for users + // so when permission is allowed same tooltip will be reused to show helpertext + renderTooltip?: boolean; + allowedLabel?: string; +} & BoundCanProps; + +export const OrgPermissionCan: FunctionComponent = ({ + label = "Access restricted", + children, + passThrough = true, + renderTooltip, + allowedLabel, + ...props +}) => { + const permission = useOrgPermission(); + + return ( + + {(isAllowed, ability) => { + // akhilmhdh: This is set as type due to error in casl react type. + const finalChild = + typeof children === "function" + ? children(isAllowed, ability as TOrgPermission) + : children; + + if (!isAllowed && passThrough) { + return {finalChild}; + } + + if (isAllowed && renderTooltip) { + return {finalChild}; + } + + if (!isAllowed) return null; + + return finalChild; + }} + + ); +}; diff --git a/frontend/src/components/permissions/PermissionDeniedBanner.tsx b/frontend/src/components/permissions/PermissionDeniedBanner.tsx new file mode 100644 index 000000000..40e17577f --- /dev/null +++ b/frontend/src/components/permissions/PermissionDeniedBanner.tsx @@ -0,0 +1,40 @@ +import { ReactNode } from "react"; +import { faLock } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { twMerge } from "tailwind-merge"; + +type Props = { + containerClassName?: string; + className?: string; + children?: ReactNode; +}; + +export const PermissionDeniedBanner = ({ containerClassName, className, children }: Props) => { + return ( +
+
+
+ +
+
+
Access Restricted
+ {children || ( +
+ Your role has limited permissions, please
contact your administrator to gain access +
+ )} +
+
+
+ ); +}; diff --git a/frontend/src/components/permissions/ProjectPermissionCan.tsx b/frontend/src/components/permissions/ProjectPermissionCan.tsx new file mode 100644 index 000000000..aa64c33d7 --- /dev/null +++ b/frontend/src/components/permissions/ProjectPermissionCan.tsx @@ -0,0 +1,52 @@ +import { FunctionComponent, ReactNode } from "react"; +import { BoundCanProps, Can } from "@casl/react"; + +import { TProjectPermission, useProjectPermission } from "@app/context/ProjectPermissionContext"; + +import { Tooltip } from "../v2"; + +type Props = { + label?: ReactNode; + // this prop is used when there exist already a tooltip as helper text for users + // so when permission is allowed same tooltip will be reused to show helpertext + renderTooltip?: boolean; + allowedLabel?: string; + // BUG(akhilmhdh): As a workaround for now i put any but this should be TProjectPermission + // For some reason when i put TProjectPermission in a wrapper component it just wont work causes a weird ts error + // tried a lot combinations + // REF: https://github.com/stalniy/casl/blob/ac081a34f56366a7eaaed05d21689d27041ef005/packages/casl-react/src/factory.ts#L15 +} & BoundCanProps; + +export const ProjectPermissionCan: FunctionComponent = ({ + label = "Access restricted", + children, + passThrough = true, + renderTooltip, + allowedLabel, + ...props +}) => { + const permission = useProjectPermission(); + return ( + + {(isAllowed, ability) => { + // akhilmhdh: This is set as type due to error in casl react type. + const finalChild = + typeof children === "function" + ? children(isAllowed, ability as TProjectPermission) + : children; + + if (!isAllowed && passThrough) { + return {finalChild}; + } + + if (isAllowed && renderTooltip) { + return {finalChild}; + } + + if (!isAllowed) return null; + + return finalChild; + }} + + ); +}; diff --git a/frontend/src/components/permissions/index.tsx b/frontend/src/components/permissions/index.tsx new file mode 100644 index 000000000..8d523c311 --- /dev/null +++ b/frontend/src/components/permissions/index.tsx @@ -0,0 +1,3 @@ +export { OrgPermissionCan } from "./OrgPermissionCan"; +export { PermissionDeniedBanner } from "./PermissionDeniedBanner"; +export { ProjectPermissionCan } from "./ProjectPermissionCan"; diff --git a/frontend/src/components/v2/Accordion/Accordion.tsx b/frontend/src/components/v2/Accordion/Accordion.tsx new file mode 100644 index 000000000..20ec5bacd --- /dev/null +++ b/frontend/src/components/v2/Accordion/Accordion.tsx @@ -0,0 +1,77 @@ +import { forwardRef } from "react"; +import { faChevronDown } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import * as AccordionPrimitive from "@radix-ui/react-accordion"; +import { twMerge } from "tailwind-merge"; + +export const AccordionItem = forwardRef( + ({ children, className, ...props }, forwardedRef) => ( + + {children} + + ) +); +AccordionItem.displayName = "AccordionItem"; + +export const AccordionTrigger = forwardRef< + HTMLButtonElement, + AccordionPrimitive.AccordionTriggerProps +>(({ children, className, ...props }, forwardedRef) => ( + + + {children} + + + +)); + +AccordionTrigger.displayName = "AccordionTrigger"; + +export const AccordionContent = forwardRef< + HTMLDivElement, + AccordionPrimitive.AccordionContentProps +>(({ children, className, ...props }, forwardedRef) => ( + +
{children}
+
+)); + +AccordionContent.displayName = "AccordionContent"; + +// ref: https://www.radix-ui.com/primitives/docs/components/accordion#root +export const Accordion = ({ + children, + ...props +}: AccordionPrimitive.AccordionSingleProps | AccordionPrimitive.AccordionMultipleProps) => ( + + {children} + +); diff --git a/frontend/src/components/v2/Accordion/index.tsx b/frontend/src/components/v2/Accordion/index.tsx new file mode 100644 index 000000000..831904d54 --- /dev/null +++ b/frontend/src/components/v2/Accordion/index.tsx @@ -0,0 +1 @@ +export { Accordion, AccordionContent, AccordionItem,AccordionTrigger } from "./Accordion"; diff --git a/frontend/src/components/v2/Checkbox/Checkbox.tsx b/frontend/src/components/v2/Checkbox/Checkbox.tsx index 64ec0a54a..8c79a5ff2 100644 --- a/frontend/src/components/v2/Checkbox/Checkbox.tsx +++ b/frontend/src/components/v2/Checkbox/Checkbox.tsx @@ -45,7 +45,7 @@ export const Checkbox = ({ -
{typeof label === "string" ? ( - + ) : ( label )} diff --git a/frontend/src/components/v2/IconButton/IconButton.tsx b/frontend/src/components/v2/IconButton/IconButton.tsx index 56e15c9af..2f6a2837a 100644 --- a/frontend/src/components/v2/IconButton/IconButton.tsx +++ b/frontend/src/components/v2/IconButton/IconButton.tsx @@ -57,7 +57,8 @@ const iconButtonVariants = cva( { colorSchema: "primary", variant: "outline_bg", - className: "bg-mineshaft-700 border border-mineshaft-600 hover:bg-primary/[0.15] hover:border-primary/60 text-bunker-200 hover:text-bunker-100 duration-100" + className: + "bg-mineshaft-700 border border-mineshaft-600 hover:bg-primary/[0.15] hover:border-primary/60 text-bunker-200 hover:text-bunker-100 duration-100" }, { colorSchema: "danger", diff --git a/frontend/src/components/v2/Table/Table.tsx b/frontend/src/components/v2/Table/Table.tsx index eaebda330..82cfffd21 100644 --- a/frontend/src/components/v2/Table/Table.tsx +++ b/frontend/src/components/v2/Table/Table.tsx @@ -79,7 +79,7 @@ export const Tr = ({ }: TrProps): JSX.Element => ( ; export const Td = ({ children, className, ...props }: TdProps): JSX.Element => ( - + {children} ); diff --git a/frontend/src/components/v2/Tabs/Tabs.tsx b/frontend/src/components/v2/Tabs/Tabs.tsx index cc7e755f6..98bb649bd 100644 --- a/frontend/src/components/v2/Tabs/Tabs.tsx +++ b/frontend/src/components/v2/Tabs/Tabs.tsx @@ -1,142 +1,46 @@ -import { useState } from "react"; -import { faArrowUpRightFromSquare, faCheck, faClipboard } from "@fortawesome/free-solid-svg-icons"; -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import * as Tabs from "@radix-ui/react-tabs"; +import * as TabsPrimitive from "@radix-ui/react-tabs"; +import { twMerge } from "tailwind-merge"; +export type TabsProps = TabsPrimitive.TabsProps; -export type TabsProps = Tabs.TabsProps & {}; +export const Tabs = ({ className, children, ...props }: TabsProps) => ( + + {children} + +); -function copyToClipboard(id: string, setState: (value: boolean) => void) { - // Get the text field - const copyText = document.getElementById(id) as HTMLInputElement; +export type TabListProps = TabsPrimitive.TabsListProps; - // Select the text field - copyText.select(); - copyText.setSelectionRange(0, 99999); // For mobile devices - - // Copy the text inside the text field - navigator.clipboard.writeText(copyText.value); - - setState(true); - setTimeout(() => setState(false), 2000); - // Alert the copied text - // alert("Copied the text: " + copyText.value); -} - -const CodeItem = ({isCopied, setIsCopied, textExplanation, code, id}: {isCopied: boolean, setIsCopied: (value: boolean) => void, textExplanation: string, code: string, id: string}) => { - return <> -

- {textExplanation} -

-
- - -
- -} - -export const TabsObject = () => { - const [downloadCodeCopied, setDownloadCodeCopied] = useState(false); - const [downloadCode2Copied, setDownloadCode2Copied] = useState(false); - const [loginCodeCopied, setLoginCodeCopied] = useState(false); - const [initCodeCopied, setInitCodeCopied] = useState(false); - const [runCodeCopied, setRunCodeCopied] = useState(false); - - return ( + - - - MacOS - - - Windows - - {/* - Arch Linux - */} -
- Other Platforms - - - - - - - -

You can find example of start commands for different frameworks - here - .

-
- - -
- - -
- - - -

You can find example of start commands for different frameworks - here - .

-
- -}; \ No newline at end of file + {children} + +); + +export type TabProps = TabsPrimitive.TabsTriggerProps; + +export const Tab = ({ className, children, ...props }: TabProps) => ( + + {children} + +); + +export type TabPanelProps = TabsPrimitive.TabsContentProps; + +export const TabPanel = ({ className, children, ...props }: TabPanelProps) => ( + + {children} + +); diff --git a/frontend/src/components/v2/Tabs/index.tsx b/frontend/src/components/v2/Tabs/index.tsx index 0f76dfecc..4e2246aaf 100644 --- a/frontend/src/components/v2/Tabs/index.tsx +++ b/frontend/src/components/v2/Tabs/index.tsx @@ -1,4 +1,2 @@ -export type { - TabsProps -} from "./Tabs"; -export { TabsObject } from "./Tabs"; +export type { TabListProps,TabPanelProps, TabProps, TabsProps } from "./Tabs"; +export { Tab, TabList, TabPanel, Tabs } from "./Tabs"; diff --git a/frontend/src/components/v2/Tooltip/Tooltip.tsx b/frontend/src/components/v2/Tooltip/Tooltip.tsx index f6f697f38..9d1285aa0 100644 --- a/frontend/src/components/v2/Tooltip/Tooltip.tsx +++ b/frontend/src/components/v2/Tooltip/Tooltip.tsx @@ -10,6 +10,7 @@ export type TooltipProps = Omit void; defaultOpen?: boolean; + position?: "top" | "bottom" | "left" | "right"; }; export const Tooltip = ({ @@ -20,6 +21,7 @@ export const Tooltip = ({ defaultOpen, className, asChild = true, + position = "top", ...props }: TooltipProps) => ( {children} (null); + +export const OrgPermissionProvider = ({ children }: Props): JSX.Element => { + const { currentOrg } = useOrganization(); + const orgId = currentOrg?._id || ""; + const { data: permission, isLoading } = useGetUserOrgPermissions({ orgId }); + + if (isLoading) { + return ( +
+ infisical loading indicator +
+ ); + } + + if (!permission) { + return ( +
+ Failed to load user permissions +
+ ); + } + + return ( + {children} + ); +}; + +export const useOrgPermission = () => { + const ctx = useContext(OrgPermissionContext); + if (!ctx) { + throw new Error("useOrgPermission to be used within "); + } + + return ctx; +}; diff --git a/frontend/src/context/OrgPermissionContext/index.tsx b/frontend/src/context/OrgPermissionContext/index.tsx new file mode 100644 index 000000000..730fe55b1 --- /dev/null +++ b/frontend/src/context/OrgPermissionContext/index.tsx @@ -0,0 +1,3 @@ +export { OrgPermissionProvider, useOrgPermission } from "./OrgPermissionContext"; +export type { TOrgPermission } from "./types"; +export { OrgPermissionActions, OrgPermissionSubjects } from "./types"; diff --git a/frontend/src/context/OrgPermissionContext/types.ts b/frontend/src/context/OrgPermissionContext/types.ts new file mode 100644 index 000000000..a4f1ab5ef --- /dev/null +++ b/frontend/src/context/OrgPermissionContext/types.ts @@ -0,0 +1,32 @@ +import { MongoAbility } from "@casl/ability"; + +export enum OrgPermissionActions { + Read = "read", + Create = "create", + Edit = "edit", + Delete = "delete" +} + +export enum OrgPermissionSubjects { + Workspace = "workspace", + Role = "role", + Member = "member", + Settings = "settings", + IncidentAccount = "incident-contact", + Sso = "sso", + Billing = "billing", + SecretScanning = "secret-scanning" +} + +export type OrgPermissionSet = + | [OrgPermissionActions.Create, OrgPermissionSubjects.Workspace] + | [OrgPermissionActions.Read, OrgPermissionSubjects.Workspace] + | [OrgPermissionActions, OrgPermissionSubjects.Role] + | [OrgPermissionActions, OrgPermissionSubjects.Member] + | [OrgPermissionActions, OrgPermissionSubjects.Settings] + | [OrgPermissionActions, OrgPermissionSubjects.IncidentAccount] + | [OrgPermissionActions, OrgPermissionSubjects.Sso] + | [OrgPermissionActions, OrgPermissionSubjects.SecretScanning] + | [OrgPermissionActions, OrgPermissionSubjects.Billing]; + +export type TOrgPermission = MongoAbility; diff --git a/frontend/src/context/ProjectPermissionContext/ProjectPermissionContext.tsx b/frontend/src/context/ProjectPermissionContext/ProjectPermissionContext.tsx new file mode 100644 index 000000000..732466cf6 --- /dev/null +++ b/frontend/src/context/ProjectPermissionContext/ProjectPermissionContext.tsx @@ -0,0 +1,54 @@ +import { createContext, ReactNode, useContext } from "react"; + +import { useGetUserProjectPermissions } from "@app/hooks/api"; + +import { useWorkspace } from "../WorkspaceContext"; +import { TProjectPermission } from "./types"; + +type Props = { + children: ReactNode; +}; + +const ProjectPermissionContext = createContext(null); + +export const ProjectPermissionProvider = ({ children }: Props): JSX.Element => { + const { currentWorkspace, isLoading: isWsLoading } = useWorkspace(); + const workspaceId = currentWorkspace?._id || ""; + const { data: permission, isLoading } = useGetUserProjectPermissions({ workspaceId }); + + if (!permission && currentWorkspace) { + return ( +
+ Failed to load user permissions +
+ ); + } + + if ((isLoading && currentWorkspace) || isWsLoading) { + return ( +
+ infisical loading indicator +
+ ); + } + + return ( + + {children} + + ); +}; + +export const useProjectPermission = () => { + const ctx = useContext(ProjectPermissionContext); + if (!ctx) { + throw new Error("useProjectPermission to be used within "); + } + + return ctx; +}; diff --git a/frontend/src/context/ProjectPermissionContext/index.tsx b/frontend/src/context/ProjectPermissionContext/index.tsx new file mode 100644 index 000000000..68da4a459 --- /dev/null +++ b/frontend/src/context/ProjectPermissionContext/index.tsx @@ -0,0 +1,3 @@ +export { ProjectPermissionProvider, useProjectPermission } from "./ProjectPermissionContext"; +export type { ProjectPermissionSet, TProjectPermission } from "./types"; +export { ProjectPermissionActions,ProjectPermissionSub } from "./types"; diff --git a/frontend/src/context/ProjectPermissionContext/types.ts b/frontend/src/context/ProjectPermissionContext/types.ts new file mode 100644 index 000000000..ca1ca13ae --- /dev/null +++ b/frontend/src/context/ProjectPermissionContext/types.ts @@ -0,0 +1,64 @@ +import { ForcedSubject, MongoAbility } from "@casl/ability"; + +export enum ProjectPermissionActions { + Read = "read", + Create = "create", + Edit = "edit", + Delete = "delete" +} + +export enum ProjectPermissionSub { + Role = "role", + Member = "member", + Settings = "settings", + Integrations = "integrations", + Webhooks = "webhooks", + ServiceTokens = "service-tokens", + Environments = "environments", + Tags = "tags", + AuditLogs = "audit-logs", + IpAllowList = "ip-allowlist", + Workspace = "workspace", + Secrets = "secrets", + SecretImports = "secret-imports", + SecretRollback = "secret-rollback", + Folders = "folders" +} + +type SubjectFields = { + environment: string; + secretPath: string; +}; + +export type ProjectPermissionSet = + | [ + ProjectPermissionActions, + ProjectPermissionSub.Secrets | (ForcedSubject & SubjectFields) + ] + | [ + ProjectPermissionActions, + ProjectPermissionSub.Folders | (ForcedSubject & SubjectFields) + ] + | [ + ProjectPermissionActions, + ( + | ProjectPermissionSub.SecretImports + | (ForcedSubject & SubjectFields) + ) + ] + | [ProjectPermissionActions, ProjectPermissionSub.Role] + | [ProjectPermissionActions, ProjectPermissionSub.Tags] + | [ProjectPermissionActions, ProjectPermissionSub.Member] + | [ProjectPermissionActions, ProjectPermissionSub.Integrations] + | [ProjectPermissionActions, ProjectPermissionSub.Webhooks] + | [ProjectPermissionActions, ProjectPermissionSub.AuditLogs] + | [ProjectPermissionActions, ProjectPermissionSub.Environments] + | [ProjectPermissionActions, ProjectPermissionSub.IpAllowList] + | [ProjectPermissionActions, ProjectPermissionSub.Settings] + | [ProjectPermissionActions, ProjectPermissionSub.ServiceTokens] + | [ProjectPermissionActions.Delete, ProjectPermissionSub.Workspace] + | [ProjectPermissionActions.Edit, ProjectPermissionSub.Workspace] + | [ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback] + | [ProjectPermissionActions.Create, ProjectPermissionSub.SecretRollback]; + +export type TProjectPermission = MongoAbility; diff --git a/frontend/src/context/index.tsx b/frontend/src/context/index.tsx index 7eb61268a..35c39f533 100644 --- a/frontend/src/context/index.tsx +++ b/frontend/src/context/index.tsx @@ -1,5 +1,19 @@ export { AuthProvider } from "./AuthContext"; export { OrgProvider, useOrganization } from "./OrganizationContext"; +export type { TOrgPermission } from "./OrgPermissionContext"; +export { + OrgPermissionActions, + OrgPermissionProvider, + OrgPermissionSubjects, + useOrgPermission +} from "./OrgPermissionContext"; +export type { TProjectPermission } from "./ProjectPermissionContext"; +export { + ProjectPermissionActions, + ProjectPermissionProvider, + ProjectPermissionSub, + useProjectPermission +} from "./ProjectPermissionContext"; export { SubscriptionProvider, useSubscription } from "./SubscriptionContext"; export { UserProvider, useUser } from "./UserContext"; export { useWorkspace, WorkspaceProvider } from "./WorkspaceContext"; diff --git a/frontend/src/hoc/index.tsx b/frontend/src/hoc/index.tsx new file mode 100644 index 000000000..4efe5f1f9 --- /dev/null +++ b/frontend/src/hoc/index.tsx @@ -0,0 +1,2 @@ +export { withPermission } from "./withPermission"; +export { withProjectPermission } from "./withProjectPermission"; diff --git a/frontend/src/hoc/withPermission/index.tsx b/frontend/src/hoc/withPermission/index.tsx new file mode 100644 index 000000000..d5dfb8dc0 --- /dev/null +++ b/frontend/src/hoc/withPermission/index.tsx @@ -0,0 +1 @@ +export { withPermission } from "./withPermission"; diff --git a/frontend/src/hoc/withPermission/withPermission.tsx b/frontend/src/hoc/withPermission/withPermission.tsx new file mode 100644 index 000000000..870c54f81 --- /dev/null +++ b/frontend/src/hoc/withPermission/withPermission.tsx @@ -0,0 +1,62 @@ +import { ComponentType } from "react"; +import { Abilities, AbilityTuple, Generics, SubjectType } from "@casl/ability"; +import { faLock } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { twMerge } from "tailwind-merge"; + +import { TOrgPermission, useOrgPermission } from "@app/context"; + +type Props = (T extends AbilityTuple + ? { + action: T[0]; + subject: Extract; + } + : { + action: string; + subject: string; + }) & { className?: string; containerClassName?: string }; + +export const withPermission = ( + Component: ComponentType, + { action, subject, className, containerClassName }: Props["abilities"]> +) => { + const HOC = (hocProps: T) => { + const permission = useOrgPermission(); + + // akhilmhdh: Set as any due to casl/react ts type bug + // REASON: casl due to its type checking can't seem to union even if union intersection is applied + if (permission.cannot(action as any, subject)) { + return ( +
+
+
+ +
+
+
Access Restricted
+
+ Your role has limited permissions, please
You do not have permission to this + page. +
+
+
+
+ ); + } + + return ; + }; + + HOC.displayName = "WithPermission"; + return HOC; +}; diff --git a/frontend/src/hoc/withProjectPermission/index.tsx b/frontend/src/hoc/withProjectPermission/index.tsx new file mode 100644 index 000000000..34171ed28 --- /dev/null +++ b/frontend/src/hoc/withProjectPermission/index.tsx @@ -0,0 +1 @@ +export { withProjectPermission } from "./withProjectPermission"; diff --git a/frontend/src/hoc/withProjectPermission/withProjectPermission.tsx b/frontend/src/hoc/withProjectPermission/withProjectPermission.tsx new file mode 100644 index 000000000..911703973 --- /dev/null +++ b/frontend/src/hoc/withProjectPermission/withProjectPermission.tsx @@ -0,0 +1,62 @@ +import { ComponentType } from "react"; +import { Abilities, AbilityTuple, Generics, SubjectType } from "@casl/ability"; +import { faLock } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { twMerge } from "tailwind-merge"; + +import { TProjectPermission, useProjectPermission } from "@app/context"; + +type Props = (T extends AbilityTuple + ? { + action: T[0]; + subject: Extract; + } + : { + action: string; + subject: string; + }) & { className?: string; containerClassName?: string }; + +export const withProjectPermission = ( + Component: ComponentType, + { action, subject, className, containerClassName }: Props["abilities"]> +) => { + const HOC = (hocProps: T) => { + const permission = useProjectPermission(); + + // akhilmhdh: Set as any due to casl/react ts type bug + // REASON: casl due to its type checking can't seem to union even if union intersection is applied + if (permission.cannot(action as any, subject)) { + return ( +
+
+
+ +
+
+
Permission Denied
+
+ You do not have permission to this page.
Kindly contact your organization + administrator +
+
+
+
+ ); + } + + return ; + }; + + HOC.displayName = "WithProjectPermission"; + return HOC; +}; diff --git a/frontend/src/hooks/api/index.tsx b/frontend/src/hooks/api/index.tsx index 4e5cb66f4..f3a7ab4ca 100644 --- a/frontend/src/hooks/api/index.tsx +++ b/frontend/src/hooks/api/index.tsx @@ -6,6 +6,7 @@ export * from "./integrationAuth"; export * from "./integrations"; export * from "./keys"; export * from "./organization"; +export * from "./roles"; export * from "./secretFolders"; export * from "./secretImports"; export * from "./secrets"; diff --git a/frontend/src/hooks/api/integrationAuth/queries.tsx b/frontend/src/hooks/api/integrationAuth/queries.tsx index 84d22ce5b..37e8df768 100644 --- a/frontend/src/hooks/api/integrationAuth/queries.tsx +++ b/frontend/src/hooks/api/integrationAuth/queries.tsx @@ -408,8 +408,8 @@ export const useSaveIntegrationAccessToken = () => { refreshToken?: string; accessId?: string; accessToken?: string; - url: string | null; - namespace: string | null; + url?: string; + namespace?: string; }) => { const { data: { integrationAuth } } = await apiRequest.post("/api/v1/integration-auth/access-token", { workspaceId, diff --git a/frontend/src/hooks/api/integrations/queries.tsx b/frontend/src/hooks/api/integrations/queries.tsx index 214f7de93..8857da834 100644 --- a/frontend/src/hooks/api/integrations/queries.tsx +++ b/frontend/src/hooks/api/integrations/queries.tsx @@ -49,13 +49,13 @@ export const useCreateIntegration = () => { app?: string; appId?: string; sourceEnvironment: string; - targetEnvironment: string | null; - targetEnvironmentId: string | null; - targetService: string | null; - targetServiceId: string | null; - owner: string | null; - path: string | null; - region: string | null; + targetEnvironment?: string; + targetEnvironmentId?: string; + targetService?: string; + targetServiceId?: string; + owner?: string; + path?: string; + region?: string; metadata?: { secretSuffix?: string; } diff --git a/frontend/src/hooks/api/roles/index.tsx b/frontend/src/hooks/api/roles/index.tsx new file mode 100644 index 000000000..ff7314db8 --- /dev/null +++ b/frontend/src/hooks/api/roles/index.tsx @@ -0,0 +1,2 @@ +export { useCreateRole, useDeleteRole, useUpdateRole } from "./mutation"; +export { useGetRoles, useGetUserOrgPermissions,useGetUserProjectPermissions } from "./queries"; diff --git a/frontend/src/hooks/api/roles/mutation.tsx b/frontend/src/hooks/api/roles/mutation.tsx new file mode 100644 index 000000000..e5fda67b1 --- /dev/null +++ b/frontend/src/hooks/api/roles/mutation.tsx @@ -0,0 +1,42 @@ +import { useMutation, useQueryClient } from "@tanstack/react-query"; + +import { apiRequest } from "@app/config/request"; + +import { roleQueryKeys } from "./queries"; +import { TCreateRoleDTO, TDeleteRoleDTO, TUpdateRoleDTO } from "./types"; + +export const useCreateRole = () => { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: (dto: TCreateRoleDTO) => apiRequest.post("/api/v1/roles", dto), + onSuccess: (_, { orgId, workspaceId }) => { + queryClient.invalidateQueries(roleQueryKeys.getRoles({ orgId, workspaceId })); + } + }); +}; + +export const useUpdateRole = () => { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: ({ id, ...dto }: TUpdateRoleDTO) => apiRequest.patch(`/api/v1/roles/${id}`, dto), + onSuccess: (_, { orgId, workspaceId }) => { + queryClient.invalidateQueries(roleQueryKeys.getRoles({ orgId, workspaceId })); + } + }); +}; + +export const useDeleteRole = () => { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: ({ orgId, id }: TDeleteRoleDTO) => + apiRequest.delete(`/api/v1/roles/${id}`, { + data: { orgId } + }), + onSuccess: (_, { orgId, workspaceId }) => { + queryClient.invalidateQueries(roleQueryKeys.getRoles({ orgId, workspaceId })); + } + }); +}; diff --git a/frontend/src/hooks/api/roles/queries.tsx b/frontend/src/hooks/api/roles/queries.tsx new file mode 100644 index 000000000..8af9fe6f3 --- /dev/null +++ b/frontend/src/hooks/api/roles/queries.tsx @@ -0,0 +1,103 @@ +import { buildMongoQueryMatcher, createMongoAbility, MongoAbility, RawRuleOf } from "@casl/ability"; +import { PackRule, unpackRules } from "@casl/ability/extra"; +import { useQuery } from "@tanstack/react-query"; +import { FieldCondition, FieldInstruction, JsInterpreter } from "@ucast/mongo2js"; +import picomatch from "picomatch"; + +import { apiRequest } from "@app/config/request"; +import { OrgPermissionSet } from "@app/context/OrgPermissionContext/types"; +import { ProjectPermissionSet } from "@app/context/ProjectPermissionContext/types"; + +import { + TGetRolesDTO, + TGetUserOrgPermissionsDTO, + TGetUserProjectPermissionDTO, + TRole +} from "./types"; + +const $glob: FieldInstruction = { + type: "field", + validate(instruction, value) { + if (typeof value !== "string") { + throw new Error(`"${instruction.name}" expects value to be a string`); + } + } +}; + +const glob: JsInterpreter> = (node, object, context) => { + const secretPath = context.get(object, node.field); + const permissionSecretGlobPath = node.value; + if (!secretPath) return false; + return picomatch.isMatch(secretPath, permissionSecretGlobPath, { strictSlashes: false }); +}; + +const conditionsMatcher = buildMongoQueryMatcher({ $glob }, { glob }); + +export const roleQueryKeys = { + getRoles: ({ orgId, workspaceId }: TGetRolesDTO) => ["roles", { orgId, workspaceId }] as const, + getUserOrgPermissions: ({ orgId }: TGetUserOrgPermissionsDTO) => + ["user-permissions", { orgId }] as const, + getUserProjectPermissions: ({ workspaceId }: TGetUserProjectPermissionDTO) => + ["user-project-permissions", { workspaceId }] as const +}; + +const getRoles = async ({ orgId, workspaceId }: TGetRolesDTO) => { + const { data } = await apiRequest.get<{ data: { roles: TRole[] } }>( + "/api/v1/roles", + { + params: { + workspaceId, + orgId + } + } + ); + + return data.data.roles; +}; + +export const useGetRoles = ({ orgId, workspaceId }: TGetRolesDTO) => + useQuery({ + queryKey: roleQueryKeys.getRoles({ orgId, workspaceId }), + queryFn: () => getRoles({ orgId, workspaceId }), + enabled: Boolean(orgId) + }); + +const getUserOrgPermissions = async ({ orgId }: TGetUserOrgPermissionsDTO) => { + const { data } = await apiRequest.get<{ + data: { permissions: PackRule>>[] }; + }>(`/api/v1/roles/organization/${orgId}/permissions`, {}); + + return data.data.permissions; +}; + +export const useGetUserOrgPermissions = ({ orgId }: TGetUserOrgPermissionsDTO) => + useQuery({ + queryKey: roleQueryKeys.getUserOrgPermissions({ orgId }), + queryFn: () => getUserOrgPermissions({ orgId }), + enabled: Boolean(orgId), + select: (data) => { + const rule = unpackRules>>(data); + const ability = createMongoAbility(rule, { conditionsMatcher }); + return ability; + } + }); + +const getUserProjectPermissions = async ({ workspaceId }: TGetUserProjectPermissionDTO) => { + const { data } = await apiRequest.get<{ + data: { permissions: PackRule>>[] }; + }>(`/api/v1/roles/workspace/${workspaceId}/permissions`, {}); + + return data.data.permissions; +}; + +export const useGetUserProjectPermissions = ({ workspaceId }: TGetUserProjectPermissionDTO) => + useQuery({ + queryKey: roleQueryKeys.getUserProjectPermissions({ workspaceId }), + queryFn: () => getUserProjectPermissions({ workspaceId }), + enabled: Boolean(workspaceId), + select: (data) => { + const rule = unpackRules>>(data); + const ability = createMongoAbility(rule, { conditionsMatcher }); + return ability; + } + }); diff --git a/frontend/src/hooks/api/roles/types.ts b/frontend/src/hooks/api/roles/types.ts new file mode 100644 index 000000000..8cf578fa6 --- /dev/null +++ b/frontend/src/hooks/api/roles/types.ts @@ -0,0 +1,57 @@ +export type TGetRolesDTO = { + orgId: string; + workspaceId?: string; +}; + +export type TRole = { + _id: string; + organization: string; + workspace: T; + name: string; + description: string; + slug: string; + permissions: T extends string ? TProjectPermission[] : TPermission[]; + createdAt: string; + updatedAt: string; +}; + +export type TPermission = { + conditions?: Record; + action: string; + subject: string; +}; + +export type TProjectPermission = { + conditions?: Record; + action: string; + subject: string; +}; + +export type TCreateRoleDTO = { + orgId: string; + workspaceId?: T; + name: string; + description?: string; + slug: string; + permissions: T extends string ? TProjectPermission[] : TPermission[]; +}; + +export type TUpdateRoleDTO = { + orgId: string; + id: string; + workspaceId?: T; +} & Partial, "orgId" | "workspaceId">>; + +export type TDeleteRoleDTO = { + orgId: string; + id: string; + workspaceId?: string; +}; + +export type TGetUserOrgPermissionsDTO = { + orgId: string; +}; + +export type TGetUserProjectPermissionDTO = { + workspaceId: string; +}; diff --git a/frontend/src/hooks/api/users/queries.tsx b/frontend/src/hooks/api/users/queries.tsx index 90314a1e4..c940c8d61 100644 --- a/frontend/src/hooks/api/users/queries.tsx +++ b/frontend/src/hooks/api/users/queries.tsx @@ -8,6 +8,7 @@ import { apiRequest } from "@app/config/request"; import { setAuthToken } from "@app/reactQuery"; import { useUploadWsKey } from "../keys/queries"; +import { workspaceKeys } from "../workspace/queries"; import { AddUserToOrgDTO, AddUserToWsDTO, @@ -55,27 +56,27 @@ export const useRenameUser = () => { return useMutation<{}, {}, RenameUserDTO>({ mutationFn: ({ newName }) => - apiRequest.patch("/api/v2/users/me/name", { firstName: newName?.split(" ")[0], lastName: newName?.split(" ").slice(1).join(" ") }), + apiRequest.patch("/api/v2/users/me/name", { + firstName: newName?.split(" ")[0], + lastName: newName?.split(" ").slice(1).join(" ") + }), onSuccess: () => { queryClient.invalidateQueries(userKeys.getUser); } }); }; - export const useUpdateUserAuthMethods = () => { const queryClient = useQueryClient(); return useMutation({ - mutationFn: async ({ - authMethods - }: { - authMethods: AuthMethod[]; - }) => { - const { data: { user } } = await apiRequest.put("/api/v2/users/me/auth-methods", { + mutationFn: async ({ authMethods }: { authMethods: AuthMethod[] }) => { + const { + data: { user } + } = await apiRequest.put("/api/v2/users/me/auth-methods", { authMethods }); - + return user; }, onSuccess: () => { @@ -108,6 +109,7 @@ export const useGetOrgUsers = (orgId: string) => // mutation export const useAddUserToWs = () => { const uploadWsKey = useUploadWsKey(); + const queryClient = useQueryClient(); return useMutation<{ data: AddUserToWsRes }, {}, AddUserToWsDTO>({ mutationFn: ({ email, workspaceId }) => @@ -136,18 +138,20 @@ export const useAddUserToWs = () => { userId: data.invitee._id, workspaceId }); + + queryClient.invalidateQueries(workspaceKeys.getWorkspaceUsers(workspaceId)); } }); }; export const useAddUserToOrg = () => { const queryClient = useQueryClient(); - type Response = { + type Response = { data: { - message: string, - completeInviteLink: string | undefined - } - } + message: string; + completeInviteLink: string | undefined; + }; + }; return useMutation({ mutationFn: (dto) => { @@ -164,7 +168,7 @@ export const useDeleteOrgMembership = () => { return useMutation<{}, {}, DeletOrgMembershipDTO>({ mutationFn: ({ membershipId, orgId }) => { - return apiRequest.delete(`/api/v2/organizations/${orgId}/memberships/${membershipId}`) + return apiRequest.delete(`/api/v2/organizations/${orgId}/memberships/${membershipId}`); }, onSuccess: (_, { orgId }) => { queryClient.invalidateQueries(userKeys.getOrgUsers(orgId)); @@ -177,9 +181,12 @@ export const useUpdateOrgUserRole = () => { return useMutation<{}, {}, UpdateOrgUserRoleDTO>({ mutationFn: ({ organizationId, membershipId, role }) => { - return apiRequest.patch(`/api/v2/organizations/${organizationId}/memberships/${membershipId}`, { - role - }); + return apiRequest.patch( + `/api/v2/organizations/${organizationId}/memberships/${membershipId}`, + { + role + } + ); }, onSuccess: (_, { organizationId }) => { queryClient.invalidateQueries(userKeys.getOrgUsers(organizationId)); @@ -218,64 +225,49 @@ export const useLogoutUser = () => }); export const useGetMyIp = () => { - return useQuery({ + return useQuery({ queryKey: userKeys.myIp, queryFn: async () => { - const { data } = await apiRequest.get<{ ip: string; }>( - "/api/v1/users/me/ip" - ); + const { data } = await apiRequest.get<{ ip: string }>("/api/v1/users/me/ip"); return data.ip; }, enabled: true - }); -} + }); +}; export const useGetMyAPIKeys = () => { return useQuery({ queryKey: userKeys.myAPIKeys, queryFn: async () => { - const { data } = await apiRequest.get( - "/api/v2/users/me/api-keys" - ); + const { data } = await apiRequest.get("/api/v2/users/me/api-keys"); return data; }, enabled: true }); -} +}; export const useCreateAPIKey = () => { const queryClient = useQueryClient(); return useMutation({ - mutationFn: async ({ - name, - expiresIn - }: { - name: string; - expiresIn: number; - }) => { - const { data } = await apiRequest.post( - "/api/v2/users/me/api-keys", - { - name, - expiresIn - } - ); - + mutationFn: async ({ name, expiresIn }: { name: string; expiresIn: number }) => { + const { data } = await apiRequest.post("/api/v2/users/me/api-keys", { + name, + expiresIn + }); + return data; }, onSuccess() { queryClient.invalidateQueries(userKeys.myAPIKeys); } }); -} +}; export const useDeleteAPIKey = () => { const queryClient = useQueryClient(); return useMutation({ mutationFn: async (apiKeyDataId: string) => { - const { data } = await apiRequest.delete( - `/api/v2/users/me/api-keys/${apiKeyDataId}` - ); + const { data } = await apiRequest.delete(`/api/v2/users/me/api-keys/${apiKeyDataId}`); return data; }, @@ -283,29 +275,25 @@ export const useDeleteAPIKey = () => { queryClient.invalidateQueries(userKeys.myAPIKeys); } }); -} +}; export const useGetMySessions = () => { return useQuery({ queryKey: userKeys.mySessions, queryFn: async () => { - const { data } = await apiRequest.get( - "/api/v2/users/me/sessions" - ); + const { data } = await apiRequest.get("/api/v2/users/me/sessions"); return data; }, enabled: true }); -} +}; export const useRevokeMySessions = () => { const queryClient = useQueryClient(); return useMutation({ mutationFn: async () => { - const { data } = await apiRequest.delete( - "/api/v2/users/me/sessions" - ); + const { data } = await apiRequest.delete("/api/v2/users/me/sessions"); return data; }, @@ -313,22 +301,17 @@ export const useRevokeMySessions = () => { queryClient.invalidateQueries(userKeys.mySessions); } }); -} +}; export const useUpdateMfaEnabled = () => { const queryClient = useQueryClient(); return useMutation({ - mutationFn: async ({ - isMfaEnabled - }: { - isMfaEnabled: boolean; - }) => { - const { data: { user } } = await apiRequest.patch( - "/api/v2/users/me/mfa", - { - isMfaEnabled - } - ); + mutationFn: async ({ isMfaEnabled }: { isMfaEnabled: boolean }) => { + const { + data: { user } + } = await apiRequest.patch("/api/v2/users/me/mfa", { + isMfaEnabled + }); return user; }, @@ -336,15 +319,15 @@ export const useUpdateMfaEnabled = () => { queryClient.invalidateQueries(userKeys.getUser); } }); -} +}; export const fetchMyOrganizationProjects = async (orgId: string) => { - const { data: { workspaces } } = await apiRequest.get( - `/api/v1/organization/${orgId}/my-workspaces` - ); + const { + data: { workspaces } + } = await apiRequest.get(`/api/v1/organization/${orgId}/my-workspaces`); return workspaces; -} +}; export const useGetMyOrganizationProjects = (orgId: string) => { return useQuery({ @@ -354,4 +337,4 @@ export const useGetMyOrganizationProjects = (orgId: string) => { }, enabled: true }); -} \ No newline at end of file +}; diff --git a/frontend/src/hooks/api/users/types.ts b/frontend/src/hooks/api/users/types.ts index 8e0d19646..07f497c07 100644 --- a/frontend/src/hooks/api/users/types.ts +++ b/frontend/src/hooks/api/users/types.ts @@ -1,12 +1,12 @@ import { UserWsKeyPair } from "../keys/types"; export enum AuthMethod { - EMAIL = "email", - GOOGLE = "google", + EMAIL = "email", + GOOGLE = "google", GITHUB = "github", - OKTA_SAML = "okta-saml", - AZURE_SAML = "azure-saml", - JUMPCLOUD_SAML = "jumpcloud-saml" + OKTA_SAML = "okta-saml", + AZURE_SAML = "azure-saml", + JUMPCLOUD_SAML = "jumpcloud-saml" } export type User = { @@ -42,11 +42,14 @@ export type OrgUser = { }; inviteEmail: string; organization: string; - role: "owner" | "admin" | "member"; + role: "owner" | "admin" | "member" | "custom"; status: "invited" | "accepted" | "verified" | "completed"; deniedPermissions: any[]; + customRole: string; }; +export type TWorkspaceUser = OrgUser; + export type AddUserToWsDTO = { workspaceId: string; email: string; @@ -76,7 +79,7 @@ export type AddUserToOrgDTO = { export type CreateAPIKeyRes = { apiKey: string; apiKeyData: APIKeyData; -} +}; export type RenameUserDTO = { newName: string; @@ -89,7 +92,7 @@ export type APIKeyData = { lastUsed: string; createdAt: string; expiresAt: string; -} +}; export type TokenVersion = { _id: string; @@ -99,4 +102,4 @@ export type TokenVersion = { lastUsed: string; createdAt: string; updatedAt: string; -} \ No newline at end of file +}; diff --git a/frontend/src/hooks/api/workspace/queries.tsx b/frontend/src/hooks/api/workspace/queries.tsx index 12e571ad9..f9e40d2db 100644 --- a/frontend/src/hooks/api/workspace/queries.tsx +++ b/frontend/src/hooks/api/workspace/queries.tsx @@ -5,6 +5,7 @@ import { apiRequest } from "@app/config/request"; import { IntegrationAuth } from "../integrationAuth/types"; import { TIntegration } from "../integrations/types"; import { EncryptedSecret } from "../secrets/types"; +import { TWorkspaceUser } from "../users/types"; import { CreateEnvironmentDTO, CreateWorkspaceDTO, @@ -173,16 +174,17 @@ export const createWorkspace = ({ workspaceName }: CreateWorkspaceDTO): Promise<{ data: { workspace: Workspace } }> => { return apiRequest.post("/api/v1/workspace", { workspaceName, organizationId }); -} +}; export const useCreateWorkspace = () => { const queryClient = useQueryClient(); return useMutation<{ data: { workspace: Workspace } }, {}, CreateWorkspaceDTO>({ - mutationFn: async ({ organizationId, workspaceName }) => createWorkspace({ - organizationId, - workspaceName - }), + mutationFn: async ({ organizationId, workspaceName }) => + createWorkspace({ + organizationId, + workspaceName + }), onSuccess: () => { queryClient.invalidateQueries(workspaceKeys.getAllUserWorkspace); } @@ -296,32 +298,30 @@ export const useGetWorkspaceUsers = (workspaceId: string) => { return useQuery({ queryKey: workspaceKeys.getWorkspaceUsers(workspaceId), queryFn: async () => { - const { data: { users } } = await apiRequest.get( + const { + data: { users } + } = await apiRequest.get<{ users: TWorkspaceUser[] }>( `/api/v1/workspace/${workspaceId}/users` ); return users; }, enabled: true }); -} +}; export const useAddUserToWorkspace = () => { const queryClient = useQueryClient(); return useMutation({ - mutationFn: async ({ - email, - workspaceId - }: { - email: string; - workspaceId: string; - }) => { - const { data: { invitee, latestKey } } = await apiRequest.post(`/api/v1/workspace/${workspaceId}/invite-signup`, { email }); - - return ({ + mutationFn: async ({ email, workspaceId }: { email: string; workspaceId: string }) => { + const { + data: { invitee, latestKey } + } = await apiRequest.post(`/api/v1/workspace/${workspaceId}/invite-signup`, { email }); + + return { invitee, latestKey - }); + }; }, onSuccess: (_, dto) => { queryClient.invalidateQueries(workspaceKeys.getWorkspaceUsers(dto.workspaceId)); @@ -334,7 +334,9 @@ export const useDeleteUserFromWorkspace = () => { return useMutation({ mutationFn: async (membershipId: string) => { - const { data: { deletedMembership } } = await apiRequest.delete(`/api/v1/membership/${membershipId}`); + const { + data: { deletedMembership } + } = await apiRequest.delete(`/api/v1/membership/${membershipId}`); return deletedMembership; }, onSuccess: (res) => { @@ -346,14 +348,10 @@ export const useDeleteUserFromWorkspace = () => { export const useUpdateUserWorkspaceRole = () => { const queryClient = useQueryClient(); return useMutation({ - mutationFn: async ({ - membershipId, - role - }: { - membershipId: string; - role: string; - }) => { - const { data: { membership } } = await apiRequest.post(`/api/v1/membership/${membershipId}/change-role`, { + mutationFn: async ({ membershipId, role }: { membershipId: string; role: string }) => { + const { + data: { membership } + } = await apiRequest.post(`/api/v1/membership/${membershipId}/change-role`, { role }); return membership; diff --git a/frontend/src/layouts/AppLayout/AppLayout.tsx b/frontend/src/layouts/AppLayout/AppLayout.tsx index 525524b5a..d03161f30 100644 --- a/frontend/src/layouts/AppLayout/AppLayout.tsx +++ b/frontend/src/layouts/AppLayout/AppLayout.tsx @@ -31,6 +31,7 @@ import { DropdownMenuTrigger } from "@radix-ui/react-dropdown-menu"; import * as yup from "yup"; import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider"; +import { OrgPermissionCan } from "@app/components/permissions"; import onboardingCheck from "@app/components/utilities/checks/OnboardingCheck"; import { tempLocalStorage } from "@app/components/utilities/checks/tempLocalStorage"; import { encryptAssymmetric } from "@app/components/utilities/cryptography/crypto"; @@ -50,7 +51,14 @@ import { SelectItem, UpgradePlanModal } from "@app/components/v2"; -import { useOrganization, useSubscription, useUser, useWorkspace } from "@app/context"; +import { + OrgPermissionActions, + OrgPermissionSubjects, + useOrganization, + useSubscription, + useUser, + useWorkspace +} from "@app/context"; import { usePopUp } from "@app/hooks"; import { fetchOrgUsers, @@ -395,22 +403,30 @@ export const AppLayout = ({ children }: LayoutProps) => {

- + {(isAllowed) => ( + + )} +
@@ -723,16 +739,26 @@ export const AppLayout = ({ children }: LayoutProps) => { ( - - Add all members of my organization to this project - + {(isAllowed) => ( +
+ + Add all members of my organization to this project + +
+ )} + )} />
diff --git a/frontend/src/pages/_app.tsx b/frontend/src/pages/_app.tsx index 56fb8c9f7..45a1cbe71 100644 --- a/frontend/src/pages/_app.tsx +++ b/frontend/src/pages/_app.tsx @@ -17,7 +17,9 @@ import { TooltipProvider } from "@app/components/v2"; import { publicPaths } from "@app/const"; import { AuthProvider, + OrgPermissionProvider, OrgProvider, + ProjectPermissionProvider, SubscriptionProvider, UserProvider, WorkspaceProvider @@ -95,17 +97,21 @@ const App = ({ Component, pageProps, ...appProps }: NextAppProp): JSX.Element => - - - - - - - - - - - + + + + + + + + + + + + + + + diff --git a/frontend/src/pages/integrations/aws-parameter-store/authorize.tsx b/frontend/src/pages/integrations/aws-parameter-store/authorize.tsx index d71273fa5..80952191c 100644 --- a/frontend/src/pages/integrations/aws-parameter-store/authorize.tsx +++ b/frontend/src/pages/integrations/aws-parameter-store/authorize.tsx @@ -39,9 +39,7 @@ export default function AWSParameterStoreAuthorizeIntegrationPage() { workspaceId: localStorage.getItem("projectData.id"), integration: "aws-parameter-store", accessId: accessKey, - accessToken: accessSecretKey, - url: null, - namespace: null + accessToken: accessSecretKey }); setAccessKey(""); diff --git a/frontend/src/pages/integrations/aws-parameter-store/create.tsx b/frontend/src/pages/integrations/aws-parameter-store/create.tsx index 60d8849e4..a3759d17b 100644 --- a/frontend/src/pages/integrations/aws-parameter-store/create.tsx +++ b/frontend/src/pages/integrations/aws-parameter-store/create.tsx @@ -98,11 +98,6 @@ export default function AWSParameterStoreCreateIntegrationPage() { integrationAuthId: integrationAuth?._id, isActive: true, sourceEnvironment: selectedSourceEnvironment, - targetEnvironment: null, - targetEnvironmentId: null, - targetService: null, - targetServiceId: null, - owner: null, path, region: selectedAWSRegion, secretPath diff --git a/frontend/src/pages/integrations/aws-secret-manager/authorize.tsx b/frontend/src/pages/integrations/aws-secret-manager/authorize.tsx index 11da85224..f150a5a28 100644 --- a/frontend/src/pages/integrations/aws-secret-manager/authorize.tsx +++ b/frontend/src/pages/integrations/aws-secret-manager/authorize.tsx @@ -37,9 +37,7 @@ export default function AWSSecretManagerCreateIntegrationPage() { workspaceId: localStorage.getItem("projectData.id"), integration: "aws-secret-manager", accessId: accessKey, - accessToken: accessSecretKey, - url: null, - namespace: null + accessToken: accessSecretKey }); setAccessKey(""); diff --git a/frontend/src/pages/integrations/aws-secret-manager/create.tsx b/frontend/src/pages/integrations/aws-secret-manager/create.tsx index fb6fdf90d..99b89b0fb 100644 --- a/frontend/src/pages/integrations/aws-secret-manager/create.tsx +++ b/frontend/src/pages/integrations/aws-secret-manager/create.tsx @@ -98,12 +98,6 @@ export default function AWSSecretManagerCreateIntegrationPage() { isActive: true, app: targetSecretName.trim(), sourceEnvironment: selectedSourceEnvironment, - targetEnvironment: null, - targetEnvironmentId: null, - targetService: null, - targetServiceId: null, - owner: null, - path: null, region: selectedAWSRegion, secretPath }); diff --git a/frontend/src/pages/integrations/azure-key-vault/create.tsx b/frontend/src/pages/integrations/azure-key-vault/create.tsx index d6b843923..0797ecc03 100644 --- a/frontend/src/pages/integrations/azure-key-vault/create.tsx +++ b/frontend/src/pages/integrations/azure-key-vault/create.tsx @@ -61,13 +61,6 @@ export default function AzureKeyVaultCreateIntegrationPage() { isActive: true, app: vaultBaseUrl, sourceEnvironment: selectedSourceEnvironment, - targetEnvironment: null, - targetEnvironmentId: null, - targetService: null, - targetServiceId: null, - owner: null, - path: null, - region: null, secretPath }); setIsLoading(false); diff --git a/frontend/src/pages/integrations/bitbucket/create.tsx b/frontend/src/pages/integrations/bitbucket/create.tsx index 1901360af..f696ea592 100644 --- a/frontend/src/pages/integrations/bitbucket/create.tsx +++ b/frontend/src/pages/integrations/bitbucket/create.tsx @@ -91,11 +91,6 @@ export default function BitBucketCreateIntegrationPage() { sourceEnvironment: selectedSourceEnvironment, targetEnvironment: targetEnvironment.name, targetEnvironmentId: targetEnvironment.slug, - targetService: null, - targetServiceId: null, - owner: null, - path: null, - region: null, secretPath }); diff --git a/frontend/src/pages/integrations/checkly/authorize.tsx b/frontend/src/pages/integrations/checkly/authorize.tsx index b99e8dfe4..a84c9fe28 100644 --- a/frontend/src/pages/integrations/checkly/authorize.tsx +++ b/frontend/src/pages/integrations/checkly/authorize.tsx @@ -28,9 +28,7 @@ export default function ChecklyCreateIntegrationPage() { const integrationAuth = await mutateAsync({ workspaceId: localStorage.getItem("projectData.id"), integration: "checkly", - accessToken, - url: null, - namespace: null + accessToken }); setIsLoading(false); diff --git a/frontend/src/pages/integrations/checkly/create.tsx b/frontend/src/pages/integrations/checkly/create.tsx index 6b3febfb8..97db4c118 100644 --- a/frontend/src/pages/integrations/checkly/create.tsx +++ b/frontend/src/pages/integrations/checkly/create.tsx @@ -72,13 +72,6 @@ export default function ChecklyCreateIntegrationPage() { app: targetApp, appId: targetAppId, sourceEnvironment: selectedSourceEnvironment, - targetEnvironment: null, - targetEnvironmentId: null, - targetService: null, - targetServiceId: null, - owner: null, - path: null, - region: null, secretPath, metadata: { secretSuffix diff --git a/frontend/src/pages/integrations/circleci/authorize.tsx b/frontend/src/pages/integrations/circleci/authorize.tsx index d5c741a18..592056bbe 100644 --- a/frontend/src/pages/integrations/circleci/authorize.tsx +++ b/frontend/src/pages/integrations/circleci/authorize.tsx @@ -28,9 +28,7 @@ export default function CircleCICreateIntegrationPage() { const integrationAuth = await mutateAsync({ workspaceId: localStorage.getItem("projectData.id"), integration: "circleci", - accessToken: apiKey, - url: null, - namespace:null + accessToken: apiKey }); setIsLoading(false); diff --git a/frontend/src/pages/integrations/circleci/create.tsx b/frontend/src/pages/integrations/circleci/create.tsx index 5620b10c8..dda2fd089 100644 --- a/frontend/src/pages/integrations/circleci/create.tsx +++ b/frontend/src/pages/integrations/circleci/create.tsx @@ -68,13 +68,6 @@ export default function CircleCICreateIntegrationPage() { app: targetApp, appId: integrationAuthApps?.find((integrationAuthApp) => integrationAuthApp.name === targetApp)?.appId, sourceEnvironment: selectedSourceEnvironment, - targetEnvironment: null, - targetEnvironmentId: null, - targetService: null, - targetServiceId: null, - owner: null, - path: null, - region: null, secretPath }); diff --git a/frontend/src/pages/integrations/cloud-66/authorize.tsx b/frontend/src/pages/integrations/cloud-66/authorize.tsx index 4f7d3b010..70c115337 100644 --- a/frontend/src/pages/integrations/cloud-66/authorize.tsx +++ b/frontend/src/pages/integrations/cloud-66/authorize.tsx @@ -28,9 +28,7 @@ export default function Cloud66CreateIntegrationPage() { const integrationAuth = await mutateAsync({ workspaceId: localStorage.getItem("projectData.id"), integration: "cloud-66", - accessToken: apiKey, - url: null, - namespace: null + accessToken: apiKey }); setIsLoading(false); diff --git a/frontend/src/pages/integrations/cloud-66/create.tsx b/frontend/src/pages/integrations/cloud-66/create.tsx index 5b51e3d99..5313f82cb 100644 --- a/frontend/src/pages/integrations/cloud-66/create.tsx +++ b/frontend/src/pages/integrations/cloud-66/create.tsx @@ -66,13 +66,6 @@ export default function Cloud66CreateIntegrationPage() { app: targetApp, appId: integrationAuthApps?.find((integrationAuthApp) => integrationAuthApp.name === targetApp)?.appId, sourceEnvironment: selectedSourceEnvironment, - targetEnvironment: null, - targetEnvironmentId: null, - targetService: null, - targetServiceId: null, - owner: null, - path: null, - region: null, secretPath }); diff --git a/frontend/src/pages/integrations/cloudflare-pages/authorize.tsx b/frontend/src/pages/integrations/cloudflare-pages/authorize.tsx index d63a3de43..a16756c79 100644 --- a/frontend/src/pages/integrations/cloudflare-pages/authorize.tsx +++ b/frontend/src/pages/integrations/cloudflare-pages/authorize.tsx @@ -33,9 +33,7 @@ export default function CloudflarePagesIntegrationPage() { workspaceId: localStorage.getItem("projectData.id"), integration: "cloudflare-pages", accessId: accountId, - accessToken: accessKey, - url: null, - namespace: null + accessToken: accessKey }); setAccessKey(""); diff --git a/frontend/src/pages/integrations/cloudflare-pages/create.tsx b/frontend/src/pages/integrations/cloudflare-pages/create.tsx index 49ea9b191..0f4d171a4 100644 --- a/frontend/src/pages/integrations/cloudflare-pages/create.tsx +++ b/frontend/src/pages/integrations/cloudflare-pages/create.tsx @@ -64,12 +64,6 @@ export default function CloudflarePagesIntegrationPage() { appId: targetAppId, sourceEnvironment: selectedSourceEnvironment, targetEnvironment, - targetEnvironmentId: null, - targetService: null, - targetServiceId: null, - owner: null, - path: null, - region: null, secretPath: "/", }); diff --git a/frontend/src/pages/integrations/codefresh/authorize.tsx b/frontend/src/pages/integrations/codefresh/authorize.tsx index e74b8e1ce..b6f96c1a2 100644 --- a/frontend/src/pages/integrations/codefresh/authorize.tsx +++ b/frontend/src/pages/integrations/codefresh/authorize.tsx @@ -28,9 +28,7 @@ export default function CodefreshCreateIntegrationPage() { const integrationAuth = await mutateAsync({ workspaceId: localStorage.getItem("projectData.id"), integration: "codefresh", - accessToken: apiKey, - url: null, - namespace: null + accessToken: apiKey }); setIsLoading(false); diff --git a/frontend/src/pages/integrations/codefresh/create.tsx b/frontend/src/pages/integrations/codefresh/create.tsx index c4d4a0e5a..4ce9a4c53 100644 --- a/frontend/src/pages/integrations/codefresh/create.tsx +++ b/frontend/src/pages/integrations/codefresh/create.tsx @@ -66,13 +66,6 @@ export default function CodefreshCreateIntegrationPage() { app: targetApp, appId: integrationAuthApps?.find((integrationAuthApp) => integrationAuthApp.name === targetApp)?.appId, sourceEnvironment: selectedSourceEnvironment, - targetEnvironment: null, - targetEnvironmentId: null, - targetService: null, - targetServiceId: null, - owner: null, - path: null, - region: null, secretPath }); diff --git a/frontend/src/pages/integrations/digital-ocean-app-platform/authorize.tsx b/frontend/src/pages/integrations/digital-ocean-app-platform/authorize.tsx index 8e84cdef6..ec4652b9e 100644 --- a/frontend/src/pages/integrations/digital-ocean-app-platform/authorize.tsx +++ b/frontend/src/pages/integrations/digital-ocean-app-platform/authorize.tsx @@ -28,9 +28,7 @@ export default function DigitalOceanAppPlatformCreateIntegrationPage() { const integrationAuth = await mutateAsync({ workspaceId: localStorage.getItem("projectData.id"), integration: "digital-ocean-app-platform", - accessToken: apiKey, - url: null, - namespace: null + accessToken: apiKey }); setIsLoading(false); diff --git a/frontend/src/pages/integrations/digital-ocean-app-platform/create.tsx b/frontend/src/pages/integrations/digital-ocean-app-platform/create.tsx index f7f1ec633..3dddaf831 100644 --- a/frontend/src/pages/integrations/digital-ocean-app-platform/create.tsx +++ b/frontend/src/pages/integrations/digital-ocean-app-platform/create.tsx @@ -66,13 +66,6 @@ export default function DigitalOceanAppPlatformCreateIntegrationPage() { app: targetApp, appId: integrationAuthApps?.find((integrationAuthApp) => integrationAuthApp.name === targetApp)?.appId, sourceEnvironment: selectedSourceEnvironment, - targetEnvironment: null, - targetEnvironmentId: null, - targetService: null, - targetServiceId: null, - owner: null, - path: null, - region: null, secretPath }); diff --git a/frontend/src/pages/integrations/flyio/authorize.tsx b/frontend/src/pages/integrations/flyio/authorize.tsx index 2d7735427..2af0fa2ab 100644 --- a/frontend/src/pages/integrations/flyio/authorize.tsx +++ b/frontend/src/pages/integrations/flyio/authorize.tsx @@ -28,9 +28,7 @@ export default function FlyioCreateIntegrationPage() { const integrationAuth = await mutateAsync({ workspaceId: localStorage.getItem("projectData.id"), integration: "flyio", - accessToken, - url: null, - namespace: null + accessToken }); setIsLoading(false); diff --git a/frontend/src/pages/integrations/flyio/create.tsx b/frontend/src/pages/integrations/flyio/create.tsx index 082ce81a4..8a49d4c06 100644 --- a/frontend/src/pages/integrations/flyio/create.tsx +++ b/frontend/src/pages/integrations/flyio/create.tsx @@ -68,13 +68,6 @@ export default function FlyioCreateIntegrationPage() { isActive: true, app: targetApp, sourceEnvironment: selectedSourceEnvironment, - targetEnvironment: null, - targetEnvironmentId: null, - targetService: null, - targetServiceId: null, - owner: null, - path: null, - region: null, secretPath }); diff --git a/frontend/src/pages/integrations/gcp-secret-manager/authorize.tsx b/frontend/src/pages/integrations/gcp-secret-manager/authorize.tsx index 746980601..c49513b75 100644 --- a/frontend/src/pages/integrations/gcp-secret-manager/authorize.tsx +++ b/frontend/src/pages/integrations/gcp-secret-manager/authorize.tsx @@ -34,9 +34,7 @@ export default function GCPSecretManagerAuthorizeIntegrationPage() { const integrationAuth = await mutateAsync({ workspaceId: localStorage.getItem("projectData.id"), integration: "gcp-secret-manager", - refreshToken: accessToken, - url: null, - namespace: null + refreshToken: accessToken }); setIsLoading(false); diff --git a/frontend/src/pages/integrations/gcp-secret-manager/create.tsx b/frontend/src/pages/integrations/gcp-secret-manager/create.tsx index f90d5f158..a452b42cc 100644 --- a/frontend/src/pages/integrations/gcp-secret-manager/create.tsx +++ b/frontend/src/pages/integrations/gcp-secret-manager/create.tsx @@ -67,13 +67,6 @@ export default function GCPSecretManagerCreateIntegrationPage() { app: integrationAuthApps?.find((integrationAuthApp) => integrationAuthApp.appId === targetAppId)?.name, appId: targetAppId, sourceEnvironment: selectedSourceEnvironment, - targetEnvironment: null, - targetEnvironmentId: null, - targetService: null, - targetServiceId: null, - owner: null, - path: null, - region: null, secretPath }); @@ -83,7 +76,7 @@ export default function GCPSecretManagerCreateIntegrationPage() { console.error(err); } }; - + return integrationAuth && workspace && selectedSourceEnvironment && diff --git a/frontend/src/pages/integrations/gcp-secret-manager/pat/create.tsx b/frontend/src/pages/integrations/gcp-secret-manager/pat/create.tsx index f21a37304..3bc98b615 100644 --- a/frontend/src/pages/integrations/gcp-secret-manager/pat/create.tsx +++ b/frontend/src/pages/integrations/gcp-secret-manager/pat/create.tsx @@ -63,13 +63,6 @@ export default function GCPSecretManagerCreateIntegrationPage() { app: integrationAuthApps?.find((integrationAuthApp) => integrationAuthApp.appId === targetAppId)?.name, appId: targetAppId, sourceEnvironment: selectedSourceEnvironment, - targetEnvironment: null, - targetEnvironmentId: null, - targetService: null, - targetServiceId: null, - owner: null, - path: null, - region: null, secretPath }); diff --git a/frontend/src/pages/integrations/github/create.tsx b/frontend/src/pages/integrations/github/create.tsx index c99c60342..4a319ceb8 100644 --- a/frontend/src/pages/integrations/github/create.tsx +++ b/frontend/src/pages/integrations/github/create.tsx @@ -72,13 +72,7 @@ export default function GitHubCreateIntegrationPage() { isActive: true, app: targetApp.name, sourceEnvironment: selectedSourceEnvironment, - targetEnvironment: null, - targetEnvironmentId: null, - targetService: null, - targetServiceId: null, owner: targetApp.owner, - path: null, - region: null, secretPath }); diff --git a/frontend/src/pages/integrations/gitlab/create.tsx b/frontend/src/pages/integrations/gitlab/create.tsx index 6b4c5e769..66e68303e 100644 --- a/frontend/src/pages/integrations/gitlab/create.tsx +++ b/frontend/src/pages/integrations/gitlab/create.tsx @@ -98,12 +98,6 @@ export default function GitLabCreateIntegrationPage() { appId: String(targetAppId), sourceEnvironment: selectedSourceEnvironment, targetEnvironment: targetEnvironment === "" ? "*" : targetEnvironment, - targetEnvironmentId: null, - targetService: null, - targetServiceId: null, - owner: null, - path: null, - region: null, secretPath }); diff --git a/frontend/src/pages/integrations/hashicorp-vault/create.tsx b/frontend/src/pages/integrations/hashicorp-vault/create.tsx index 0134c66bc..e4c560361 100644 --- a/frontend/src/pages/integrations/hashicorp-vault/create.tsx +++ b/frontend/src/pages/integrations/hashicorp-vault/create.tsx @@ -66,13 +66,7 @@ export default function HashiCorpVaultCreateIntegrationPage() { isActive: true, app: vaultEnginePath, sourceEnvironment: selectedSourceEnvironment, - targetEnvironment: null, - targetEnvironmentId: null, - targetService: null, - targetServiceId: null, - owner: null, path: vaultSecretPath, - region: null, secretPath }); diff --git a/frontend/src/pages/integrations/heroku/create.tsx b/frontend/src/pages/integrations/heroku/create.tsx index cedd6e44c..d73eb408b 100644 --- a/frontend/src/pages/integrations/heroku/create.tsx +++ b/frontend/src/pages/integrations/heroku/create.tsx @@ -66,13 +66,6 @@ export default function HerokuCreateIntegrationPage() { isActive: true, app: targetApp, sourceEnvironment: selectedSourceEnvironment, - targetEnvironment: null, - targetEnvironmentId: null, - targetService: null, - targetServiceId: null, - owner: null, - path: null, - region: null, secretPath }); diff --git a/frontend/src/pages/integrations/laravel-forge/authorize.tsx b/frontend/src/pages/integrations/laravel-forge/authorize.tsx index 98c5569c1..cf8d7020e 100644 --- a/frontend/src/pages/integrations/laravel-forge/authorize.tsx +++ b/frontend/src/pages/integrations/laravel-forge/authorize.tsx @@ -38,9 +38,7 @@ export default function LaravelForgeCreateIntegrationPage() { workspaceId: localStorage.getItem("projectData.id"), integration: "laravel-forge", accessId: serverId, - accessToken: apiKey, - url: null, - namespace: null + accessToken: apiKey }); setIsLoading(false); diff --git a/frontend/src/pages/integrations/laravel-forge/create.tsx b/frontend/src/pages/integrations/laravel-forge/create.tsx index 08ea2d6b5..91ff2a2d0 100644 --- a/frontend/src/pages/integrations/laravel-forge/create.tsx +++ b/frontend/src/pages/integrations/laravel-forge/create.tsx @@ -64,15 +64,8 @@ export default function LaravelForgeCreateIntegrationPage() { integrationAuthId: integrationAuth?._id, isActive: true, app: targetApp, - appId: integrationAuthApps?.find((integrationAuthApp) => integrationAuthApp.name === targetApp)?.appId, + appId: String(integrationAuthApps?.find((integrationAuthApp) => integrationAuthApp.name === targetApp)?.appId), sourceEnvironment: selectedSourceEnvironment, - targetEnvironment: null, - targetEnvironmentId: null, - targetService: null, - targetServiceId: null, - owner: null, - path: null, - region: null, secretPath }); diff --git a/frontend/src/pages/integrations/netlify/create.tsx b/frontend/src/pages/integrations/netlify/create.tsx index c649ad5ed..eff335f97 100644 --- a/frontend/src/pages/integrations/netlify/create.tsx +++ b/frontend/src/pages/integrations/netlify/create.tsx @@ -76,12 +76,6 @@ export default function NetlifyCreateIntegrationPage() { appId: integrationAuthApps?.find((integrationAuthApp) => integrationAuthApp.name === targetApp)?.appId, sourceEnvironment: selectedSourceEnvironment, targetEnvironment, - targetEnvironmentId: null, - targetService: null, - targetServiceId: null, - owner: null, - path: null, - region: null, secretPath }); diff --git a/frontend/src/pages/integrations/northflank/authorize.tsx b/frontend/src/pages/integrations/northflank/authorize.tsx index 809f0a63f..ad1ba9b1e 100644 --- a/frontend/src/pages/integrations/northflank/authorize.tsx +++ b/frontend/src/pages/integrations/northflank/authorize.tsx @@ -28,9 +28,7 @@ export default function NorthflankCreateIntegrationPage() { const integrationAuth = await mutateAsync({ workspaceId: localStorage.getItem("projectData.id"), integration: "northflank", - accessToken: apiKey, - url: null, - namespace: null + accessToken: apiKey }); setIsLoading(false); diff --git a/frontend/src/pages/integrations/northflank/create.tsx b/frontend/src/pages/integrations/northflank/create.tsx index 9869a9030..055c937e1 100644 --- a/frontend/src/pages/integrations/northflank/create.tsx +++ b/frontend/src/pages/integrations/northflank/create.tsx @@ -29,7 +29,7 @@ export default function NorthflankCreateIntegrationPage() { const [selectedSourceEnvironment, setSelectedSourceEnvironment] = useState(""); const [secretPath, setSecretPath] = useState("/"); const [targetAppId, setTargetAppId] = useState(""); - const [targetSecretGroupId, setTargetSecretGroupId] = useState(null); + const [targetSecretGroupId, setTargetSecretGroupId] = useState(""); const [isLoading, setIsLoading] = useState(false); @@ -88,13 +88,7 @@ export default function NorthflankCreateIntegrationPage() { app: integrationAuthApps?.find((integrationAuthApp) => integrationAuthApp.appId === targetAppId)?.name, appId: targetAppId, sourceEnvironment: selectedSourceEnvironment, - targetEnvironment: null, - targetEnvironmentId: null, - targetService: null, targetServiceId: targetSecretGroupId, - owner: null, - path: null, - region: null, secretPath }); @@ -160,7 +154,7 @@ export default function NorthflankCreateIntegrationPage() { )} - {targetSecretGroupId && integrationAuthSecretGroups && ( + {targetSecretGroupId !== "" && integrationAuthSecretGroups && ( + +
+ + ); +}; + +const TabsObject = () => { + const [downloadCodeCopied, setDownloadCodeCopied] = useState(false); + const [downloadCode2Copied, setDownloadCode2Copied] = useState(false); + const [loginCodeCopied, setLoginCodeCopied] = useState(false); + const [initCodeCopied, setInitCodeCopied] = useState(false); + const [runCodeCopied, setRunCodeCopied] = useState(false); + + return ( + + + + MacOS + + + Windows + + {/* + Arch Linux + */} + + Other Platforms + + + + + + + +

+ You can find example of start commands for different frameworks{" "} + + here + + .{" "} +

+
+ + +
+ + +
+ + + +

+ You can find example of start commands for different frameworks{" "} + + here + + .{" "} +

+
+
+ ); +}; + const LearningItem = ({ text, subText, @@ -92,9 +315,7 @@ const LearningItem = ({ tabIndex={0} onClick={async () => { if (userAction && userAction !== "first_time_secrets_pushed") { - await registerUserAction.mutateAsync( - userAction - ); + await registerUserAction.mutateAsync(userAction); } }} className={`group relative flex h-[5.5rem] w-full items-center justify-between overflow-hidden rounded-md border ${ @@ -237,410 +458,446 @@ type TAddProjectFormData = yup.InferType; // #TODO: Update all the workspaceIds -export default function Organization() { - const { t } = useTranslation(); +const OrganizationPage = withPermission( + () => { + const { t } = useTranslation(); - const router = useRouter(); + const router = useRouter(); - const { workspaces, isLoading: isWorkspaceLoading } = useWorkspace(); - const orgWorkspaces = - workspaces?.filter( - (workspace) => workspace.organization === localStorage.getItem("orgData.id") - ) || []; - const currentOrg = String(router.query.id); - const { createNotification } = useNotificationContext(); - const addWsUser = useAddUserToWs(); + const { workspaces, isLoading: isWorkspaceLoading } = useWorkspace(); + const orgWorkspaces = + workspaces?.filter( + (workspace) => workspace.organization === localStorage.getItem("orgData.id") + ) || []; + const currentOrg = String(router.query.id); + const { createNotification } = useNotificationContext(); + const addWsUser = useAddUserToWs(); - const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([ - "addNewWs", - "upgradePlan" - ] as const); - const { - control, - formState: { isSubmitting }, - reset, - handleSubmit - } = useForm({ - resolver: yupResolver(formSchema) - }); - - const [hasUserClickedSlack, setHasUserClickedSlack] = useState(false); - const [hasUserClickedIntro, setHasUserClickedIntro] = useState(false); - const [hasUserPushedSecrets, setHasUserPushedSecrets] = useState(false); - const [usersInOrg, setUsersInOrg] = useState(false); - const [searchFilter, setSearchFilter] = useState(""); - const createWs = useCreateWorkspace(); - const { user } = useUser(); - const uploadWsKey = useUploadWsKey(); - const { data: serverDetails } = useFetchServerStatus(); - - - const onCreateProject = async ({ name, addMembers }: TAddProjectFormData) => { - // type check - if (!currentOrg) return; - try { - const { - data: { - workspace: { _id: newWorkspaceId } - } - } = await createWs.mutateAsync({ - organizationId: currentOrg, - workspaceName: name - }); - - const randomBytes = crypto.randomBytes(16).toString("hex"); - const PRIVATE_KEY = String(localStorage.getItem("PRIVATE_KEY")); - const { ciphertext, nonce } = encryptAssymmetric({ - plaintext: randomBytes, - publicKey: user.publicKey, - privateKey: PRIVATE_KEY - }); - - await uploadWsKey.mutateAsync({ - encryptedKey: ciphertext, - nonce, - userId: user?._id, - workspaceId: newWorkspaceId - }); - - if (addMembers) { - // not using hooks because need at this point only - const orgUsers = await fetchOrgUsers(currentOrg); - orgUsers.forEach(({ status, user: orgUser }) => { - // skip if status of org user is not accepted - // this orgUser is the person who created the ws - if (status !== "accepted" || user.email === orgUser.email) return; - addWsUser.mutate({ email: orgUser.email, workspaceId: newWorkspaceId }); - }); - } - createNotification({ text: "Workspace created", type: "success" }); - handlePopUpClose("addNewWs"); - router.push(`/project/${newWorkspaceId}/secrets/overview`); - } catch (err) { - console.error(err); - createNotification({ text: "Failed to create workspace", type: "error" }); - } - }; - - const { subscription } = useSubscription(); - - const isAddingProjectsAllowed = subscription?.workspaceLimit - ? subscription.workspacesUsed < subscription.workspaceLimit - : true; - - useEffect(() => { - onboardingCheck({ - setHasUserClickedIntro, - setHasUserClickedSlack, - setHasUserPushedSecrets, - setUsersInOrg + const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([ + "addNewWs", + "upgradePlan" + ] as const); + const { + control, + formState: { isSubmitting }, + reset, + handleSubmit + } = useForm({ + resolver: yupResolver(formSchema) }); - }, []); - const isWorkspaceEmpty = !isWorkspaceLoading && orgWorkspaces?.length === 0; + const [hasUserClickedSlack, setHasUserClickedSlack] = useState(false); + const [hasUserClickedIntro, setHasUserClickedIntro] = useState(false); + const [hasUserPushedSecrets, setHasUserPushedSecrets] = useState(false); + const [usersInOrg, setUsersInOrg] = useState(false); + const [searchFilter, setSearchFilter] = useState(""); + const createWs = useCreateWorkspace(); + const { user } = useUser(); + const uploadWsKey = useUploadWsKey(); + const { data: serverDetails } = useFetchServerStatus(); - return ( -
- - {t("common.head-title", { title: t("settings.members.title") })} - - - {!serverDetails?.redisConfigured &&
-

Announcements

-
- - Attention: Updated versions of Infisical now require Redis for full functionality. Learn how to configure it - - - here - - . + const onCreateProject = async ({ name, addMembers }: TAddProjectFormData) => { + // type check + if (!currentOrg) return; + try { + const { + data: { + workspace: { _id: newWorkspaceId } + } + } = await createWs.mutateAsync({ + organizationId: currentOrg, + workspaceName: name + }); + + const randomBytes = crypto.randomBytes(16).toString("hex"); + const PRIVATE_KEY = String(localStorage.getItem("PRIVATE_KEY")); + const { ciphertext, nonce } = encryptAssymmetric({ + plaintext: randomBytes, + publicKey: user.publicKey, + privateKey: PRIVATE_KEY + }); + + await uploadWsKey.mutateAsync({ + encryptedKey: ciphertext, + nonce, + userId: user?._id, + workspaceId: newWorkspaceId + }); + + if (addMembers) { + // not using hooks because need at this point only + const orgUsers = await fetchOrgUsers(currentOrg); + orgUsers.forEach(({ status, user: orgUser }) => { + // skip if status of org user is not accepted + // this orgUser is the person who created the ws + if (status !== "accepted" || user.email === orgUser.email) return; + addWsUser.mutate({ email: orgUser.email, workspaceId: newWorkspaceId }); + }); + } + createNotification({ text: "Workspace created", type: "success" }); + handlePopUpClose("addNewWs"); + router.push(`/project/${newWorkspaceId}/secrets/overview`); + } catch (err) { + console.error(err); + createNotification({ text: "Failed to create workspace", type: "error" }); + } + }; + + const { subscription } = useSubscription(); + + const isAddingProjectsAllowed = subscription?.workspaceLimit + ? subscription.workspacesUsed < subscription.workspaceLimit + : true; + + useEffect(() => { + onboardingCheck({ + setHasUserClickedIntro, + setHasUserClickedSlack, + setHasUserPushedSecrets, + setUsersInOrg + }); + }, []); + + const isWorkspaceEmpty = !isWorkspaceLoading && orgWorkspaces?.length === 0; + + return ( +
+ + {t("common.head-title", { title: t("settings.members.title") })} + + + {!serverDetails?.redisConfigured && ( +
+

Announcements

+
+ + Attention: Updated versions of Infisical now require Redis for full functionality. + Learn how to configure it + + + here + + + . +
-
} -
-

Projects

-
- setSearchFilter(e.target.value)} - leftIcon={} - /> - -
-
- {isWorkspaceLoading && - Array.apply(0, Array(3)).map((_x, i) => ( -
-
- -
-
- -
-
- -
-
- ))} - {orgWorkspaces - .filter((ws) => ws?.name?.toLowerCase().includes(searchFilter.toLowerCase())) - .map((workspace) => ( -
-
{workspace.name}
-
- {workspace.environments?.length || 0} environments -
- + )} + +
+
+ {isWorkspaceLoading && + Array.apply(0, Array(3)).map((_x, i) => ( +
+
+ +
+
+ +
+
+ +
+
+ ))} + {orgWorkspaces + .filter((ws) => ws?.name?.toLowerCase().includes(searchFilter.toLowerCase())) + .map((workspace) => ( +
+
{workspace.name}
+
+ {workspace.environments?.length || 0} environments +
+ +
+ ))} +
+ {isWorkspaceEmpty && ( +
+ +
+ You are not part of any projects in this organization yet. When you are, they will + appear here. +
+
+ Create a new project, or ask other organization members to give you necessary + permissions. +
+
+ )} +
+ {new Date().getTime() - new Date(user?.createdAt).getTime() < 30 * 24 * 60 * 60 * 1000 && ( +
+

Onboarding Guide

+
+ + {orgWorkspaces.length !== 0 && ( + <> + + + + )} +
+ +
+
+ {orgWorkspaces.length !== 0 && ( +
+
+
+ + {false && ( +
+ +
+ )} +
+
Inject secrets locally
+
+ Replace .env files with a more secure and efficient alternative. +
+
+
+
+ About 2 min +
+
+ + {false &&
} +
+ )} + {orgWorkspaces.length !== 0 && ( + + )} +
+ )} +
+

Explore More

+
+ {features.map((feature) => ( +
+
{feature.name}
+
+ {feature.description} +
+
+
+ Setup time: 20 min +
+ + Learn more{" "} -
- + +
))} -
- {isWorkspaceEmpty && ( -
- -
- You are not part of any projects in this organization yet. When you are, they will - appear here. -
-
- Create a new project, or ask other organization members to give you necessary - permissions. -
- )} -
- {new Date().getTime() - new Date(user?.createdAt).getTime() < 30 * 24 * 60 * 60 * 1000 && ( -
-

Onboarding Guide

-
- - {orgWorkspaces.length !== 0 && ( - <> - - - - )} -
- -
-
- {orgWorkspaces.length !== 0 && ( -
-
-
- - {false && ( -
- -
- )} -
-
Inject secrets locally
-
- Replace .env files with a more secure and efficient alternative. -
-
-
-
- About 2 min -
-
- - {false &&
} -
- )} - {orgWorkspaces.length !== 0 && ( - - )}
- )} -
-

Explore More

-
{ + handlePopUpToggle("addNewWs", isModalOpen); + reset(); + }} > - {features.map((feature) => ( -
-
{feature.name}
-
- {feature.description} -
-
-
Setup time: 20 min
- - Learn more{" "} - - -
-
- ))} -
-
- { - handlePopUpToggle("addNewWs", isModalOpen); - reset(); - }} - > - -
- ( - - - - )} - /> -
+ + ( - ( + - Add all members of my organization to this project - + + )} /> -
-
- - -
- -
-
- handlePopUpToggle("upgradePlan", isOpen)} - text="You have exceeded the number of projects allowed on the free plan." - /> - {/* */} -
- ); -} +
+ ( + + {(isAllowed) => ( +
+ + Add all members of my organization to this project + +
+ )} +
+ )} + /> +
+
+ + +
+ + + + handlePopUpToggle("upgradePlan", isOpen)} + text="You have exceeded the number of projects allowed on the free plan." + /> + {/* */} +
+ ); + }, + { + action: OrgPermissionActions.Read, + subject: OrgPermissionSubjects.Workspace + } +); -Organization.requireAuth = true; +Object.assign(OrganizationPage, { requireAuth: true }); + +export default OrganizationPage; diff --git a/frontend/src/pages/org/[id]/secret-scanning/index.tsx b/frontend/src/pages/org/[id]/secret-scanning/index.tsx index e0f98243a..5dc5ffe1e 100644 --- a/frontend/src/pages/org/[id]/secret-scanning/index.tsx +++ b/frontend/src/pages/org/[id]/secret-scanning/index.tsx @@ -1,89 +1,133 @@ import { useEffect, useState } from "react"; import Head from "next/head"; -import { useRouter } from "next/router" +import { useRouter } from "next/router"; +import { OrgPermissionCan } from "@app/components/permissions"; import { Button } from "@app/components/v2"; +import { OrgPermissionActions, OrgPermissionSubjects } from "@app/context"; +import { withPermission } from "@app/hoc"; import { SecretScanningLogsTable } from "@app/views/SecretScanning/components"; import createNewIntegrationSession from "../../../api/secret-scanning/createSecretScanningSession"; import getInstallationStatus from "../../../api/secret-scanning/getInstallationStatus"; import linkGitAppInstallationWithOrganization from "../../../api/secret-scanning/linkGitAppInstallationWithOrganization"; -export default function SecretScanning() { - const router = useRouter() - const queryParams = router.query - const [integrationEnabled, setIntegrationStatus] = useState(false) +const SecretScanning = withPermission( + () => { + const router = useRouter(); + const queryParams = router.query; + const [integrationEnabled, setIntegrationStatus] = useState(false); - useEffect(()=>{ - const linkInstallation = async () => { - if (typeof queryParams.state === "string" && typeof queryParams.installation_id === "string"){ - try { - const isLinked = await linkGitAppInstallationWithOrganization(queryParams.installation_id as string, queryParams.state as string) - if (isLinked){ - router.reload() + useEffect(() => { + const linkInstallation = async () => { + if ( + typeof queryParams.state === "string" && + typeof queryParams.installation_id === "string" + ) { + try { + const isLinked = await linkGitAppInstallationWithOrganization( + queryParams.installation_id as string, + queryParams.state as string + ); + if (isLinked) { + router.reload(); + } + + console.log("installation verification complete"); + } catch (e) { + console.log("app installation is stale, start new session", e); } - - console.log("installation verification complete") - }catch (e){ - console.log("app installation is stale, start new session", e) } - } - } + }; - const fetchInstallationStatus = async () => { - const status = await getInstallationStatus(String(localStorage.getItem("orgData.id"))) - setIntegrationStatus(status) - } + const fetchInstallationStatus = async () => { + const status = await getInstallationStatus(String(localStorage.getItem("orgData.id"))); + setIntegrationStatus(status); + }; - fetchInstallationStatus() - linkInstallation() - },[queryParams.state, queryParams.installation_id]) + fetchInstallationStatus(); + linkInstallation(); + }, [queryParams.state, queryParams.installation_id]); - const generateNewIntegrationSession = async () => { - const session = await createNewIntegrationSession(String(localStorage.getItem("orgData.id"))) - router.push(`https://github.com/apps/infisical-radar/installations/new?state=${session.sessionId}`) - } + const generateNewIntegrationSession = async () => { + const session = await createNewIntegrationSession(String(localStorage.getItem("orgData.id"))); + router.push( + `https://github.com/apps/infisical-radar/installations/new?state=${session.sessionId}` + ); + }; - return ( -
- - Secret scanning - - - -
-
-
Secret Scanning
-
Automatically monitor your GitHub activity and prevent secret leaks
-
-
-
Secret Scanning Status: {integrationEnabled ?

Enabled

:

Not enabled

}
-
{integrationEnabled ?

Your GitHub organization is connected to Infisical, and is being continuously monitored for secret leaks.

:

Connect your GitHub organization to Infisical.

}
+ return ( +
+ + Secret scanning + + + +
+
+
Secret Scanning
+
+ Automatically monitor your GitHub activity and prevent secret leaks
- {integrationEnabled ? ( -
-
-
-
+
+
+
+ Secret Scanning Status:{" "} + {integrationEnabled ? ( +

Enabled

+ ) : ( +

Not enabled

+ )} +
+
+ {integrationEnabled ? ( +

+ Your GitHub organization is connected to Infisical, and is being continuously + monitored for secret leaks. +

+ ) : ( +

+ Connect your GitHub organization to Infisical. +

+ )} +
- ) : ( -
- -
- )} + {integrationEnabled ? ( +
+
+
+
+
+ ) : ( +
+ + {(isAllowed) => ( + + )} + +
+ )} +
+
-
-
- ); -} + ); + }, + { action: OrgPermissionActions.Read, subject: OrgPermissionSubjects.SecretScanning } +); -SecretScanning.requireAuth = true; +Object.assign(SecretScanning, { requireAuth: true }); + +export default SecretScanning; diff --git a/frontend/src/pages/project/[id]/logs/index.tsx b/frontend/src/pages/project/[id]/logs/index.tsx index 0c44abbef..cc6f79457 100644 --- a/frontend/src/pages/project/[id]/logs/index.tsx +++ b/frontend/src/pages/project/[id]/logs/index.tsx @@ -6,8 +6,9 @@ import { useRouter } from "next/router"; import Button from "@app/components/basic/buttons/Button"; import EventFilter from "@app/components/basic/EventFilter"; import { UpgradePlanModal } from "@app/components/v2"; -import { useSubscription } from "@app/context"; +import { ProjectPermissionActions, ProjectPermissionSub, useSubscription } from "@app/context"; import ActivitySideBar from "@app/ee/components/ActivitySideBar"; +import { withProjectPermission } from "@app/hoc"; import { usePopUp } from "@app/hooks/usePopUp"; import getProjectLogs from "../../../../ee/api/secrets/GetProjectLogs"; @@ -23,10 +24,10 @@ interface LogData { }; serviceAccount?: { string: string; - }, + }; serviceTokenData?: { name: string; - } + }; actions: { _id: string; name: string; @@ -60,67 +61,33 @@ interface LogDataPoint { /** * This is the tab that includes all of the user activity logs */ -export default function Activity() { - const router = useRouter(); - const [eventChosen, setEventChosen] = useState(""); - const [logsData, setLogsData] = useState([]); - const [isLoading, setIsLoading] = useState(false); - const [currentOffset, setCurrentOffset] = useState(0); - const currentLimit = 10; - const [currentSidebarAction, toggleSidebar] = useState(); - const { t } = useTranslation(); - const { subscription } = useSubscription(); - const { popUp, handlePopUpOpen, handlePopUpClose } = usePopUp([ - "upgradePlan" - ] as const); +const Activity = withProjectPermission( + () => { + const router = useRouter(); + const [eventChosen, setEventChosen] = useState(""); + const [logsData, setLogsData] = useState([]); + const [isLoading, setIsLoading] = useState(false); + const [currentOffset, setCurrentOffset] = useState(0); + const currentLimit = 10; + const [currentSidebarAction, toggleSidebar] = useState(); + const { t } = useTranslation(); + const { subscription } = useSubscription(); + const { popUp, handlePopUpOpen, handlePopUpClose } = usePopUp(["upgradePlan"] as const); - // this use effect updates the data in case of a new filter being added - useEffect(() => { - setCurrentOffset(0); - const getLogData = async () => { - setIsLoading(true); - const tempLogsData = await getProjectLogs({ - workspaceId: String(router.query.id), - offset: 0, - limit: currentLimit, - userId: "", - actionNames: eventChosen - }); + // this use effect updates the data in case of a new filter being added + useEffect(() => { + setCurrentOffset(0); + const getLogData = async () => { + setIsLoading(true); + const tempLogsData = await getProjectLogs({ + workspaceId: String(router.query.id), + offset: 0, + limit: currentLimit, + userId: "", + actionNames: eventChosen + }); - setLogsData( - tempLogsData.map((log: LogData) => ({ - _id: log._id, - channel: log.channel, - createdAt: log.createdAt, - ipAddress: log.ipAddress, - user: log?.user?.email, - serviceAccount: log?.serviceAccount, - serviceTokenData: log?.serviceTokenData, - payload: log.actions.map((action) => ({ - _id: action._id, - name: action.name, - secretVersions: action.payload.secretVersions - })) - })) - ); - setIsLoading(false); - }; - getLogData(); - }, [eventChosen]); - - // this use effect adds more data in case 'View More' button is clicked - useEffect(() => { - const getLogData = async () => { - setIsLoading(true); - const tempLogsData = await getProjectLogs({ - workspaceId: String(router.query.id), - offset: currentOffset, - limit: currentLimit, - userId: "", - actionNames: eventChosen - }); - setLogsData( - logsData.concat( + setLogsData( tempLogsData.map((log: LogData) => ({ _id: log._id, channel: log.channel, @@ -135,63 +102,103 @@ export default function Activity() { secretVersions: action.payload.secretVersions })) })) - ) - ); - setIsLoading(false); + ); + setIsLoading(false); + }; + getLogData(); + }, [eventChosen]); + + // this use effect adds more data in case 'View More' button is clicked + useEffect(() => { + const getLogData = async () => { + setIsLoading(true); + const tempLogsData = await getProjectLogs({ + workspaceId: String(router.query.id), + offset: currentOffset, + limit: currentLimit, + userId: "", + actionNames: eventChosen + }); + setLogsData( + logsData.concat( + tempLogsData.map((log: LogData) => ({ + _id: log._id, + channel: log.channel, + createdAt: log.createdAt, + ipAddress: log.ipAddress, + user: log?.user?.email, + serviceAccount: log?.serviceAccount, + serviceTokenData: log?.serviceTokenData, + payload: log.actions.map((action) => ({ + _id: action._id, + name: action.name, + secretVersions: action.payload.secretVersions + })) + })) + ) + ); + setIsLoading(false); + }; + getLogData(); + }, [currentLimit, currentOffset]); + + const loadMoreLogs = () => { + if (subscription?.auditLogs === false) { + handlePopUpOpen("upgradePlan"); + } else { + setCurrentOffset(currentOffset + currentLimit); + } }; - getLogData(); - }, [currentLimit, currentOffset]); - const loadMoreLogs = () => { - if (subscription?.auditLogs === false) { - handlePopUpOpen("upgradePlan"); - } else { - setCurrentOffset(currentOffset + currentLimit); - } - }; - - return ( -
- - Audit Logs - - - - {currentSidebarAction && ( - - )} -
-
-

{t("activity.title")}

+ return ( +
+ + Audit Logs + + + + {currentSidebarAction && ( + + )} +
+
+

{t("activity.title")}

+
+

{t("activity.subtitle")}

-

{t("activity.subtitle")}

-
-
- -
- -
-
-
+ +
+
+
+
+ {subscription && ( + handlePopUpClose("upgradePlan")} + text={ + subscription.slug === null + ? "You can see more logs under an Enterprise license" + : "You can see more logs if you switch to Infisical's Business/Professional Plan." + } /> -
+ )}
- {subscription && ( - handlePopUpClose("upgradePlan")} - text={subscription.slug === null ? "You can see more logs under an Enterprise license" : "You can see more logs if you switch to Infisical's Business/Professional Plan."} - /> - )} -
- ); -} + ); + }, + { action: ProjectPermissionActions.Read, subject: ProjectPermissionSub.AuditLogs } +); -Activity.requireAuth = true; +Object.assign(Activity, { requireAuth: true }); +export default Activity; diff --git a/frontend/src/pages/project/[id]/members/index.tsx b/frontend/src/pages/project/[id]/members/index.tsx index 997606b1d..4bcbb833e 100644 --- a/frontend/src/pages/project/[id]/members/index.tsx +++ b/frontend/src/pages/project/[id]/members/index.tsx @@ -1,225 +1,21 @@ -import { useEffect, useState } from "react"; +/* eslint-disable @typescript-eslint/no-unused-vars */ import { useTranslation } from "react-i18next"; import Head from "next/head"; -import Image from "next/image"; -import { useRouter } from "next/router"; -import { faMagnifyingGlass, faPlus } from "@fortawesome/free-solid-svg-icons"; -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import Button from "@app/components/basic/buttons/Button"; -import AddProjectMemberDialog from "@app/components/basic/dialog/AddProjectMemberDialog"; -import ProjectUsersTable from "@app/components/basic/table/ProjectUsersTable"; -import guidGenerator from "@app/components/utilities/randomId"; -import { Input } from "@app/components/v2"; -import { useOrganization } from "@app/context"; -import { - useAddUserToWorkspace, - useGetOrgUsers, - useGetUser, - useGetWorkspaceUsers} from "@app/hooks/api"; -import { uploadWsKey } from "@app/hooks/api/keys/queries"; - -import { - decryptAssymmetric, - encryptAssymmetric -} from "../../../../components/utilities/cryptography/crypto"; - -interface UserProps { - firstName: string; - lastName: string; - email: string; - _id: string; - publicKey: string; -} - -interface MembershipProps { - deniedPermissions: any[]; - user: UserProps; - inviteEmail: string; - role: string; - status: string; - _id: string; -} - -// #TODO: Update all the workspaceIds - -export default function Users() { - const router = useRouter(); - const workspaceId = router.query.id as string; - - const { data: user } = useGetUser(); - const { currentOrg } = useOrganization(); - const { data: orgUsers } = useGetOrgUsers(currentOrg?._id ?? ""); - - const { data: workspaceUsers } = useGetWorkspaceUsers(workspaceId); - const { mutateAsync: addUserToWorkspaceMutateAsync } = useAddUserToWorkspace(); - - const [isAddOpen, setIsAddOpen] = useState(false); - // let [isDeleteOpen, setIsDeleteOpen] = useState(false); - // let [userIdToBeDeleted, setUserIdToBeDeleted] = useState(false); - const [email, setEmail] = useState(""); - const [personalEmail, setPersonalEmail] = useState(""); - const [searchUsers, setSearchUsers] = useState(""); +import { MembersPage } from "@app/views/Project/MembersPage"; +export default function WorkspaceMemberSettings() { const { t } = useTranslation(); - - const [userList, setUserList] = useState([]); - const [isUserListLoading, setIsUserListLoading] = useState(true); - const [orgUserList, setOrgUserList] = useState([]); - - useEffect(() => { - if (user && workspaceUsers && orgUsers) { - (async () => { - setPersonalEmail(user.email); - - const tempUserList = workspaceUsers.map((membership: MembershipProps) => ({ - key: guidGenerator(), - firstName: membership.user?.firstName, - lastName: membership.user?.lastName, - email: membership.user?.email === null ? membership.inviteEmail : membership.user?.email, - role: membership?.role, - status: membership?.status, - userId: membership.user?._id, - membershipId: membership._id, - deniedPermissions: membership.deniedPermissions, - publicKey: membership.user?.publicKey - })); - setUserList(tempUserList); - - setIsUserListLoading(false); - - setOrgUserList(orgUsers); - setEmail( - orgUsers - ?.filter((membership: MembershipProps) => membership.status === "accepted") - .map((membership: MembershipProps) => membership.user.email) - .filter( - (usEmail: string) => - !tempUserList?.map((user1: UserProps) => user1.email).includes(usEmail) - )[0] - ); - })(); - } - }, [user, workspaceUsers, orgUsers]); - - const closeAddModal = () => { - setIsAddOpen(false); - }; - - const openAddModal = () => { - setIsAddOpen(true); - }; - - // function closeDeleteModal() { - // setIsDeleteOpen(false); - // } - - // function deleteMembership(userId) { - // deleteUserFromWorkspace(userId, router.query.id) - // } - - // function openDeleteModal() { - // setIsDeleteOpen(true); - // } - - const submitAddModal = async () => { - const result = await addUserToWorkspaceMutateAsync({ - email, - workspaceId - }); - - if (result?.invitee && result?.latestKey) { - const PRIVATE_KEY = localStorage.getItem("PRIVATE_KEY") as string; - - // assymmetrically decrypt symmetric key with local private key - const key = decryptAssymmetric({ - ciphertext: result.latestKey.encryptedKey, - nonce: result.latestKey.nonce, - publicKey: result.latestKey.sender.publicKey, - privateKey: PRIVATE_KEY - }); - - const { ciphertext, nonce } = encryptAssymmetric({ - plaintext: key, - publicKey: result.invitee.publicKey, - privateKey: PRIVATE_KEY - }); - - await uploadWsKey({ - workspaceId, - userId: result.invitee._id, - encryptedKey: ciphertext, - nonce - }); - } - setEmail(""); - setIsAddOpen(false); - }; - - return userList ? ( -
+ return ( + <> {t("common.head-title", { title: t("settings.members.title") })} -
-

{t("settings.members.title")}

-
- membership.status === "accepted") - .map((membership: MembershipProps) => membership.user.email) - .filter( - (orgEmail) => !userList?.map((user1: UserProps) => user1.email).includes(orgEmail) - )} - setEmail={setEmail} - /> - {/* */} -
-
- setSearchUsers(e.target.value)} - leftIcon={} - /> -
-
-
-
-
- -
-
- ) : ( -
- loading animation -
+ + ); } -Users.requireAuth = true; +WorkspaceMemberSettings.requireAuth = true; diff --git a/frontend/src/styles/globals.css b/frontend/src/styles/globals.css index 6d5070fe0..a895d1b87 100644 --- a/frontend/src/styles/globals.css +++ b/frontend/src/styles/globals.css @@ -12,7 +12,7 @@ } .rdp-day_today { - @apply bg-primary/50 hover:bg-primary-600 text-mineshaft-500; + @apply bg-primary/50 text-mineshaft-500 hover:bg-primary-600; } @tailwind utilities; @@ -108,7 +108,13 @@ } } .tags-conic-bg { - background: conic-gradient(rgb(235, 87, 87), rgb(242, 201, 76), rgb(76, 183, 130), rgb(78, 167, 252), rgb(250, 96, 122)); + background: conic-gradient( + rgb(235, 87, 87), + rgb(242, 201, 76), + rgb(76, 183, 130), + rgb(78, 167, 252), + rgb(250, 96, 122) + ); } .show-tags { diff --git a/frontend/src/views/DashboardPage/DashboardPage.tsx b/frontend/src/views/DashboardPage/DashboardPage.tsx index faaaeb49c..81cbedf03 100644 --- a/frontend/src/views/DashboardPage/DashboardPage.tsx +++ b/frontend/src/views/DashboardPage/DashboardPage.tsx @@ -2,6 +2,7 @@ import { useCallback, useEffect, useRef, useState } from "react"; import { FormProvider, useFieldArray, useForm } from "react-hook-form"; import { useTranslation } from "react-i18next"; import { useRouter } from "next/router"; +import { subject } from "@casl/ability"; import { closestCenter, DndContext, @@ -39,6 +40,7 @@ import { useQueryClient } from "@tanstack/react-query"; import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider"; import NavHeader from "@app/components/navigation/NavHeader"; +import { PermissionDeniedBanner, ProjectPermissionCan } from "@app/components/permissions"; import { Button, DeleteActionModal, @@ -55,7 +57,14 @@ import { UpgradePlanModal } from "@app/components/v2"; import { leaveConfirmDefaultMessage } from "@app/const"; -import { useOrganization, useSubscription, useWorkspace } from "@app/context"; +import { + ProjectPermissionActions, + ProjectPermissionSub, + useOrganization, + useProjectPermission, + useSubscription, + useWorkspace +} from "@app/context"; import { useLeaveConfirm, usePopUp, useToggle } from "@app/hooks"; import { useBatchSecretsOp, @@ -71,7 +80,6 @@ import { useGetSecretVersion, useGetSnapshotSecrets, useGetUserAction, - useGetUserWsEnvironments, useGetUserWsKey, useGetWorkspaceSecretSnapshots, useGetWsSnapshotCount, @@ -82,7 +90,6 @@ import { useUpdateSecretImport } from "@app/hooks/api"; import { secretKeys } from "@app/hooks/api/secrets/queries"; -import { WorkspaceEnv } from "@app/hooks/api/types"; import { CompareSecret } from "./components/CompareSecret"; import { CreateTagModal } from "./components/CreateTagModal"; @@ -129,7 +136,8 @@ export const DashboardPage = () => { const router = useRouter(); const { createNotification } = useNotificationContext(); const queryClient = useQueryClient(); - const envQuery = router.query.env as string; + const environment = router.query.env as string; + const permission = useProjectPermission(); const secretContainer = useRef(null); const { popUp, handlePopUpOpen, handlePopUpToggle, handlePopUpClose } = usePopUp([ @@ -147,9 +155,8 @@ export const DashboardPage = () => { const [isSecretValueHidden, setIsSecretValueHidden] = useToggle(true); const [searchFilter, setSearchFilter] = useState(""); const [snapshotId, setSnaphotId] = useState(null); - const [selectedEnv, setSelectedEnv] = useState(null); const [sortDir, setSortDir] = useState<"asc" | "desc">("asc"); - const deletedSecretIds = useRef<{ id: string; secretName: string; }[]>([]); + const deletedSecretIds = useRef<{ id: string; secretName: string }[]>([]); const { hasUnsavedChanges, setHasUnsavedChanges } = useLeaveConfirm({ initialValue: false }); const folderId = router.query.folderId as string; @@ -158,7 +165,6 @@ export const DashboardPage = () => { const { currentWorkspace, isLoading } = useWorkspace(); const { currentOrg } = useOrganization(); const workspaceId = currentWorkspace?._id as string; - const selectedEnvSlug = selectedEnv?.slug || ""; const { data: latestFileKey } = useGetUserWsKey(workspaceId); @@ -172,17 +178,6 @@ export const DashboardPage = () => { const { data: userAction } = useGetUserAction(USER_ACTION_PUSH); const hasUserPushed = Boolean(userAction); - const { data: wsEnv, isLoading: isEnvListLoading } = useGetUserWsEnvironments({ - workspaceId, - onSuccess: (data) => { - // get an env with one of the access available - const env = data.find(({ isReadDenied, isWriteDenied }) => !isWriteDenied || !isReadDenied); - if (env && data?.map((wsenv) => wsenv.slug).includes(envQuery)) { - setSelectedEnv(data?.filter((dp) => dp.slug === envQuery)[0]); - } - } - }); - const { data: secretVersion } = useGetSecretVersion({ limit: 10, offset: 0, @@ -192,7 +187,7 @@ export const DashboardPage = () => { const { data: secrets, isLoading: isSecretsLoading } = useGetProjectSecrets({ workspaceId, - env: selectedEnvSlug, + env: environment, decryptFileKey: latestFileKey!, isPaused: Boolean(snapshotId), folderId @@ -200,7 +195,7 @@ export const DashboardPage = () => { const { data: folderData, isLoading: isFoldersLoading } = useGetProjectFolders({ workspaceId: workspaceId || "", - environment: selectedEnvSlug, + environment, parentFolderId: folderId, isPaused: isRollbackMode, sortDir @@ -213,7 +208,7 @@ export const DashboardPage = () => { isFetchingNextPage } = useGetWorkspaceSecretSnapshots({ workspaceId, - environment: selectedEnvSlug, + environment, folder: folderId, limit: 10 }); @@ -224,13 +219,13 @@ export const DashboardPage = () => { isFetching: isSnapshotChanging } = useGetSnapshotSecrets({ snapshotId: snapshotId || "", - env: selectedEnvSlug, + env: environment, decryptFileKey: latestFileKey! }); const { data: snapshotCount, isLoading: isLoadingSnapshotCount } = useGetWsSnapshotCount( workspaceId, - selectedEnvSlug, + environment, folderId ); @@ -247,17 +242,38 @@ export const DashboardPage = () => { const { data: secretImportCfg, isFetching: isSecretImportCfgFetching } = useGetSecretImports( workspaceId, - selectedEnvSlug, + environment, folderId ); const { data: importedSecrets } = useGetImportedSecrets({ workspaceId, decryptFileKey: latestFileKey!, - environment: selectedEnvSlug, + environment, folderId }); + const secretPath = `/${(folderData?.dir || []) + ?.filter(({ name }) => name !== "root") + ?.map(({ name }) => name) + .join("/")}`; + + const userAvailableEnvs = currentWorkspace?.environments?.filter( + ({ slug }) => + permission.can( + ProjectPermissionActions.Read, + subject(ProjectPermissionSub.Secrets, { environment: slug, secretPath }) + ) || + permission.can( + ProjectPermissionActions.Read, + subject(ProjectPermissionSub.Folders, { environment: slug, secretPath }) + ) || + permission.can( + ProjectPermissionActions.Read, + subject(ProjectPermissionSub.SecretImports, { environment: slug, secretPath }) + ) + ); + // This is for dnd-kit. As react-query state mutation async // This will act as a placeholder to avoid a glitching animation on dropping items const [items, setItems] = useState< @@ -297,7 +313,6 @@ export const DashboardPage = () => { resolver: yupResolver(schema) }); - const { register, control, @@ -308,10 +323,35 @@ export const DashboardPage = () => { reset } = method; const { fields, prepend, append, remove } = useFieldArray({ control, name: "secrets" }); - const isReadOnly = selectedEnv?.isWriteDenied; - const isAddOnly = selectedEnv?.isReadDenied && !selectedEnv?.isWriteDenied; - const canDoRollback = !isReadOnly && !isAddOnly; - const isSubmitDisabled = isReadOnly || (!isRollbackMode && !isDirty) || isAddOnly || isSubmitting; + + const isReadOnly = isFoldersLoading + ? true + : permission.can( + ProjectPermissionActions.Read, + subject(ProjectPermissionSub.Secrets, { environment, secretPath }) + ) && + permission.cannot( + ProjectPermissionActions.Edit, + subject(ProjectPermissionSub.Secrets, { environment, secretPath }) + ) && + permission.cannot( + ProjectPermissionActions.Create, + subject(ProjectPermissionSub.Secrets, { environment, secretPath }) + ) && + permission.cannot( + ProjectPermissionActions.Delete, + subject(ProjectPermissionSub.Secrets, { environment, secretPath }) + ); + + const canDoRollback = !isReadOnly; + const isSubmitDisabled = + isReadOnly || + // not in rollback mode and no form has changed + (!isRollbackMode && !isDirty) || + // in rollback mode and don't have permission to do it + (isRollbackMode && + permission.cannot(ProjectPermissionActions.Create, ProjectPermissionSub.SecretRollback)) || + isSubmitting; useEffect(() => { if (!isSnapshotChanging && Boolean(snapshotId)) { @@ -397,12 +437,12 @@ export const DashboardPage = () => { await performSecretRollback({ workspaceId, version: snapshotSecret.version, - environment: selectedEnvSlug, + environment, folderId }); setValue("isSnapshotMode", false); setSnaphotId(null); - queryClient.invalidateQueries(secretKeys.getProjectSecret(workspaceId, selectedEnvSlug)); + queryClient.invalidateQueries(secretKeys.getProjectSecret(workspaceId, environment)); createNotification({ text: "Successfully rollback secrets", type: "success" @@ -428,18 +468,15 @@ export const DashboardPage = () => { } // just closing this if save is triggered from drawer handlePopUpClose("secretDetails"); - // when add only mode remove rest of things not created - const sec = isAddOnly ? userSec.filter(({ _id }) => !_id) : userSec; // encrypt and format the secrets to batch api format // requests = [ {method:"", secret:""} ] const batchedSecret = transformSecretsToBatchSecretReq( deletedSecretIds.current, latestFileKey, - sec, + userSec, secrets?.secrets ); // type check - if (!selectedEnv?.slug) return; if (batchedSecret.length === 0) { reset(); return; @@ -449,7 +486,7 @@ export const DashboardPage = () => { requests: batchedSecret, workspaceId, folderId, - environment: selectedEnv?.slug + environment }); createNotification({ text: "Successfully saved changes", @@ -477,8 +514,7 @@ export const DashboardPage = () => { // eslint-disable-next-line no-alert if (!window.confirm(leaveConfirmDefaultMessage)) return; } - const env = wsEnv?.find((el) => el.slug === slug); - if (env) setSelectedEnv(env); + const query: Record = { ...router.query, env: slug }; delete query.folderId; router.push({ @@ -494,24 +530,29 @@ export const DashboardPage = () => { secretsFromImport.push({ key: el.key, value: el.value, comment: el.comment }); }); }); - downloadSecret(getValues("secrets"), secretsFromImport, selectedEnv?.slug); + downloadSecret(getValues("secrets"), secretsFromImport, environment); }; // record all deleted ids // This will make final deletion easier - const onSecretDelete = useCallback((index: number, secretName: string, id?: string, overrideId?: string) => { - if (id) deletedSecretIds.current.push({ - id, - secretName - }); - if (overrideId) deletedSecretIds.current.push({ - id: overrideId, - secretName - }); - remove(index); - // just the case if this is called from drawer - handlePopUpClose("secretDetails"); - }, []); + const onSecretDelete = useCallback( + (index: number, secretName: string, id?: string, overrideId?: string) => { + if (id) + deletedSecretIds.current.push({ + id, + secretName + }); + if (overrideId) + deletedSecretIds.current.push({ + id: overrideId, + secretName + }); + remove(index); + // just the case if this is called from drawer + handlePopUpClose("secretDetails"); + }, + [] + ); const onCreateWsTag = useCallback( async (tagName: string, tagColor: string) => { @@ -545,12 +586,12 @@ export const DashboardPage = () => { pathname: router.pathname, query: { id: workspaceId, - env: envQuery, + env: environment, folderId: id } }); }, - [envQuery, workspaceId] + [environment, workspaceId] ); const isEditFolder = Boolean(popUp?.folderForm?.data); @@ -560,7 +601,7 @@ export const DashboardPage = () => { try { await createFolder({ workspaceId, - environment: selectedEnv?.slug || "", + environment, folderName: name, parentFolderId: folderId }); @@ -585,7 +626,7 @@ export const DashboardPage = () => { await updateFolder({ folderId: id, workspaceId, - environment: selectedEnv?.slug || "", + environment, name }); createNotification({ @@ -601,7 +642,7 @@ export const DashboardPage = () => { }); } }, - [selectedEnv?.slug, (popUp?.folderForm?.data as TDeleteFolderForm)?.id] + [environment, (popUp?.folderForm?.data as TDeleteFolderForm)?.id] ); const handleFolderDelete = useCallback(async () => { @@ -609,7 +650,7 @@ export const DashboardPage = () => { try { deleteFolder({ workspaceId, - environment: selectedEnv?.slug || "", + environment, folderId: id }); createNotification({ @@ -624,18 +665,18 @@ export const DashboardPage = () => { type: "error" }); } - }, [selectedEnv?.slug, (popUp?.deleteFolder?.data as TDeleteFolderForm)?.id]); + }, [(popUp?.deleteFolder?.data as TDeleteFolderForm)?.id]); // SECRET IMPORT SECTION - const handleSecretImportCreate = async (env: string, secretPath: string) => { + const handleSecretImportCreate = async (env: string, secPath: string) => { try { await createSecretImport({ workspaceId, - environment: selectedEnv?.slug || "", + environment, folderId, secretImport: { environment: env, - secretPath + secretPath: secPath } }); createNotification({ @@ -659,7 +700,7 @@ export const DashboardPage = () => { if (secretImportCfg?._id) { await deleteSecretImport({ workspaceId, - environment: selectedEnvSlug, + environment, folderId, id: secretImportCfg?._id, secretImportEnv: importEnv, @@ -689,7 +730,7 @@ export const DashboardPage = () => { setItems(newImportOrder); updateSecretImportSync({ workspaceId, - environment: selectedEnvSlug, + environment, folderId, id: secretImportCfg?._id || "", secretImports: newImportOrder.map((el) => ({ @@ -734,7 +775,7 @@ export const DashboardPage = () => { const isSecretImportEmpty = !secretImportCfg?.imports?.length; const isEmptyPage = isFoldersEmpty && isSecretEmpty && isSecretImportEmpty; - if (isSecretsLoading || isEnvListLoading) { + if (isSecretsLoading) { return (
loading animation @@ -742,10 +783,6 @@ export const DashboardPage = () => { ); } - const userAvailableEnvs = wsEnv?.filter( - ({ isReadDenied, isWriteDenied }) => !isReadDenied || !isWriteDenied - ); - return (
@@ -753,7 +790,9 @@ export const DashboardPage = () => {
envir.slug === envQuery)[0].name || ""} + currentEnv={ + userAvailableEnvs?.filter((envir) => envir.slug === environment)?.[0]?.name || "" + } isFolderMode folders={folderData?.dir} isProjectRelated @@ -816,57 +855,83 @@ export const DashboardPage = () => {
-
- - handlePopUpOpen("secretSnapshots")} - > - - - -
-
- -
+ handlePopUpOpen("upgradePlan"); + }} + leftIcon={} + isLoading={isLoadingSnapshotCount} + isDisabled={!canDoRollback || !isAllowed} + className="h-10" + > + {snapshotCount} Commits + +
+ )} + {!isReadOnly && !isRollbackMode && (
- + {(isAllowed) => ( + + )} +
@@ -876,28 +941,45 @@ export const DashboardPage = () => {
- + {(isAllowed) => ( + + )} +
- + {(isAllowed) => ( + + )} +
@@ -917,17 +999,25 @@ export const DashboardPage = () => { Go back )} - + +
{ { searchTerm={searchFilter} /> - {fields.map(({ id, _id }, index) => ( - - ))} + {permission.can( + ProjectPermissionActions.Read, + subject(ProjectPermissionSub.Secrets, { environment, secretPath }) + ) ? ( + fields.map(({ id, _id }, index) => ( + + )) + ) : ( + + + + + + )} {!isReadOnly && !isRollbackMode && ( - + {(isAllowed) => ( + + )} + )} @@ -1014,6 +1131,8 @@ export const DashboardPage = () => { onSelectSnapshot={setSnaphotId} /> { onEnvCompare={(key) => handlePopUpOpen("compareSecrets", key)} /> - ; search?: string; + environment: string; + secretPath: string; onFolderUpdate: (folderId: string, name: string) => void; onFolderDelete: (folderId: string, name: string) => void; onFolderOpen: (folderId: string) => void; @@ -18,7 +23,9 @@ export const FolderSection = memo( onFolderDelete: handleFolderDelete, onFolderOpen: handleFolderOpen, search = "", - folders = [] + folders = [], + environment, + secretPath }: Props) => { return ( <> @@ -47,32 +54,48 @@ export const FolderSection = memo( {name}
-
- - handleFolderUpdate(id, name)} - ariaLabel="expand" - > - - - -
-
- - handleFolderDelete(id, name)} - > - - - -
+ + {(isAllowed) => ( +
+ + handleFolderUpdate(id, name)} + ariaLabel="expand" + > + + + +
+ )} +
+ + {(isAllowed) => ( +
+ + handleFolderDelete(id, name)} + > + + + +
+ )} +
diff --git a/frontend/src/views/DashboardPage/components/SecretDetailDrawer/SecretDetailDrawer.tsx b/frontend/src/views/DashboardPage/components/SecretDetailDrawer/SecretDetailDrawer.tsx index 0f8446fcc..ef85c4896 100644 --- a/frontend/src/views/DashboardPage/components/SecretDetailDrawer/SecretDetailDrawer.tsx +++ b/frontend/src/views/DashboardPage/components/SecretDetailDrawer/SecretDetailDrawer.tsx @@ -1,7 +1,9 @@ import { useFormContext, useWatch } from "react-hook-form"; +import { subject } from "@casl/ability"; import { faCircle, faCircleDot, faShuffle } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { ProjectPermissionCan } from "@app/components/permissions"; import { Button, Drawer, @@ -14,6 +16,7 @@ import { Switch, TextArea } from "@app/components/v2"; +import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context"; import { useToggle } from "@app/hooks"; import { FormData, SecretActionType } from "../../DashboardPage.utils"; @@ -21,6 +24,8 @@ import { GenRandomNumber } from "./GenRandomNumber"; type Props = { isDrawerOpen: boolean; + environment: string; + secretPath: string; onOpenChange: (isOpen: boolean) => void; index: number; isReadOnly?: boolean; @@ -39,7 +44,9 @@ export const SecretDetailDrawer = ({ isReadOnly, onSecretDelete, onSave, - onEnvCompare + onEnvCompare, + environment, + secretPath }: Props): JSX.Element => { const [canRevealSecVal, setCanRevealSecVal] = useToggle(); const [canRevealSecOverride, setCanRevealSecOverride] = useToggle(); @@ -85,20 +92,34 @@ export const SecretDetailDrawer = ({
- - + {(isAllowed) => ( + + )} + + + {(isAllowed) => ( + + )} +
} diff --git a/frontend/src/views/DashboardPage/components/SecretDropzone/SecretDropzone.tsx b/frontend/src/views/DashboardPage/components/SecretDropzone/SecretDropzone.tsx index 1c992df25..df022f4bb 100644 --- a/frontend/src/views/DashboardPage/components/SecretDropzone/SecretDropzone.tsx +++ b/frontend/src/views/DashboardPage/components/SecretDropzone/SecretDropzone.tsx @@ -1,6 +1,7 @@ import { ChangeEvent, DragEvent, useEffect, useState } from "react"; import { Controller, useForm } from "react-hook-form"; import { useTranslation } from "react-i18next"; +import { subject } from "@casl/ability"; import { faSquareCheck } from "@fortawesome/free-regular-svg-icons"; import { faClone, @@ -15,6 +16,7 @@ import { twMerge } from "tailwind-merge"; import * as yup from "yup"; import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider"; +import { ProjectPermissionCan } from "@app/components/permissions"; // TODO:(akhilmhdh) convert all the util functions like this into a lib folder grouped by functionality import { parseDotEnv } from "@app/components/utilities/parseDotEnv"; import { @@ -32,6 +34,7 @@ import { Skeleton, Tooltip } from "@app/components/v2"; +import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context"; import { useDebounce, usePopUp, useToggle } from "@app/hooks"; import { useGetProjectSecrets } from "@app/hooks/api"; import { UserWsKeyPair } from "@app/hooks/api/types"; @@ -76,6 +79,8 @@ type Props = { environments?: { name: string; slug: string }[]; workspaceId: string; decryptFileKey: UserWsKeyPair; + environment: string; + secretPath: string; }; export const SecretDropzone = ({ @@ -84,7 +89,9 @@ export const SecretDropzone = ({ onAddNewSecret, environments = [], workspaceId, - decryptFileKey + decryptFileKey, + environment, + secretPath }: Props): JSX.Element => { const { t } = useTranslation(); const [isDragActive, setDragActive] = useToggle(); @@ -107,16 +114,16 @@ export const SecretDropzone = ({ defaultValues: { secretPath: "/", environment: environments?.[0]?.slug } }); - const secretPath = watch("secretPath"); + const envCopySecPath = watch("secretPath"); const selectedEnvSlug = watch("environment"); - const debouncedSecretPath = useDebounce(secretPath); + const debouncedEnvCopySecretPath = useDebounce(envCopySecPath); const { data: secrets, isLoading: isSecretsLoading } = useGetProjectSecrets({ workspaceId, env: selectedEnvSlug, - secretPath: debouncedSecretPath, + secretPath: debouncedEnvCopySecretPath, isPaused: - !(Boolean(workspaceId) && Boolean(selectedEnvSlug) && Boolean(debouncedSecretPath)) && + !(Boolean(workspaceId) && Boolean(selectedEnvSlug) && Boolean(debouncedEnvCopySecretPath)) && !popUp.importSecEnv.isOpen, decryptFileKey }); @@ -124,7 +131,7 @@ export const SecretDropzone = ({ useEffect(() => { setValue("secrets", {}); setSearchFilter(""); - }, [debouncedSecretPath]); + }, [debouncedEnvCopySecretPath]); const handleDrag = (e: DragEvent) => { e.preventDefault(); @@ -175,7 +182,7 @@ export const SecretDropzone = ({ e.dataTransfer.dropEffect = "copy"; setDragActive.off(); - parseFile(e.dataTransfer.files[0], e.dataTransfer?.files?.[0]?.type === "application/json"); + parseFile(e.dataTransfer.files[0]); }; const handleFileUpload = (e: ChangeEvent) => { @@ -234,13 +241,21 @@ export const SecretDropzone = ({

{t(isSmaller ? "common.drop-zone-keys" : "common.drop-zone")}

- + + {(isAllowed) => ( + + )} +
- +
+ + {(isAllowed) => ( + + )} + +
{!isSmaller && ( - + + {(isAllowed) => ( + + )} + )}
diff --git a/frontend/src/views/DashboardPage/components/SecretImportSection/SecretImportItem.tsx b/frontend/src/views/DashboardPage/components/SecretImportSection/SecretImportItem.tsx index 31752aa02..6d9f23e47 100644 --- a/frontend/src/views/DashboardPage/components/SecretImportSection/SecretImportItem.tsx +++ b/frontend/src/views/DashboardPage/components/SecretImportSection/SecretImportItem.tsx @@ -1,4 +1,5 @@ import { useEffect } from "react"; +import { subject } from "@casl/ability"; import { useSortable } from "@dnd-kit/sortable"; import { faFileImport, @@ -9,12 +10,15 @@ import { } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { ProjectPermissionCan } from "@app/components/permissions"; import { EmptyState, IconButton, SecretInput, TableContainer, Tooltip } from "@app/components/v2"; -import { useWorkspace } from "@app/context"; -import { useToggle } from "@app/hooks/useToggle"; +import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context"; +import { useToggle } from "@app/hooks"; type Props = { onDelete: (environment: string, secretPath: string) => void; + environment: string; + secretPath: string; importedEnv: string; importedSecPath: string; importedSecrets: { key: string; value: string; overriden: { env: string; secretPath: string } }[]; @@ -39,7 +43,9 @@ export const SecretImportItem = ({ importedSecPath, onDelete, importedSecrets = [], - searchTerm = "" + searchTerm = "", + secretPath, + environment }: Props) => { const [isExpanded, setIsExpanded] = useToggle(); const { attributes, listeners, transform, transition, setNodeRef, isDragging } = useSortable({ @@ -49,7 +55,9 @@ export const SecretImportItem = ({ const rowEnv = currentWorkspace?.environments?.find(({ slug }) => slug === importedEnv); useEffect(() => { - const filteredSecrets = importedSecrets.filter(secret => secret.key.toUpperCase().includes(searchTerm.toUpperCase())) + const filteredSecrets = importedSecrets.filter((secret) => + secret.key.toUpperCase().includes(searchTerm.toUpperCase()) + ); if (filteredSecrets.length > 0 && searchTerm) { setIsExpanded.on(); @@ -58,7 +66,6 @@ export const SecretImportItem = ({ } }, [searchTerm]); - useEffect(() => { if (isDragging) { setIsExpanded.off(); @@ -78,7 +85,11 @@ export const SecretImportItem = ({ className="group flex cursor-default flex-row items-center hover:bg-mineshaft-700" onClick={() => setIsExpanded.toggle()} > - + @@ -106,28 +117,39 @@ export const SecretImportItem = ({
-
- - { - evt.stopPropagation(); - onDelete(importedEnv, importedSecPath); - }} - > - - - -
+ + {(isAllowed) => ( +
+ + { + evt.stopPropagation(); + onDelete(importedEnv, importedSecPath); + }} + > + + + +
+ )} +
{isExpanded && !isDragging && ( - +
@@ -146,19 +168,26 @@ export const SecretImportItem = ({ )} - {importedSecrets.filter(secret => secret.key.toUpperCase().includes(searchTerm.toUpperCase())).map(({ key, value, overriden }, index) => ( - - - - - - ))} + {importedSecrets + .filter((secret) => + secret.key.toUpperCase().includes(searchTerm.toUpperCase()) + ) + .map(({ key, value, overriden }, index) => ( + + + + + + ))}
- {key} - - - - -
+ {key} + + + + +
diff --git a/frontend/src/views/DashboardPage/components/SecretImportSection/SecretImportSection.tsx b/frontend/src/views/DashboardPage/components/SecretImportSection/SecretImportSection.tsx index 9d796d079..db12e84a4 100644 --- a/frontend/src/views/DashboardPage/components/SecretImportSection/SecretImportSection.tsx +++ b/frontend/src/views/DashboardPage/components/SecretImportSection/SecretImportSection.tsx @@ -59,13 +59,23 @@ export const computeImportedSecretRows = ( type Props = { secrets?: DecryptedSecret[]; importedSecrets?: TImportedSecrets; + environment: string; + secretPath: string; onSecretImportDelete: (env: string, secPath: string) => void; items: { id: string; environment: string; secretPath: string }[]; searchTerm: string; }; export const SecretImportSection = memo( - ({ secrets = [], importedSecrets = [], onSecretImportDelete, items = [], searchTerm = "" }: Props) => { + ({ + secrets = [], + environment, + secretPath, + importedSecrets = [], + onSecretImportDelete, + items = [], + searchTerm = "" + }: Props) => { const { currentWorkspace } = useWorkspace(); const environments = currentWorkspace?.environments || []; @@ -82,6 +92,8 @@ export const SecretImportSection = memo( secrets, environments )} + secretPath={secretPath} + environment={environment} onDelete={onSecretImportDelete} importedSecPath={impSecPath} searchTerm={searchTerm} diff --git a/frontend/src/views/DashboardPage/components/SecretInputRow/SecretInputRow.tsx b/frontend/src/views/DashboardPage/components/SecretInputRow/SecretInputRow.tsx index 6c089d695..c31c6df2e 100644 --- a/frontend/src/views/DashboardPage/components/SecretInputRow/SecretInputRow.tsx +++ b/frontend/src/views/DashboardPage/components/SecretInputRow/SecretInputRow.tsx @@ -8,6 +8,7 @@ import { UseFormSetValue, useWatch } from "react-hook-form"; +import { subject } from "@casl/ability"; import { faCheck, faCodeBranch, @@ -22,26 +23,34 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { cx } from "cva"; import { twMerge } from "tailwind-merge"; +// TODO:(akhilmhdh): Refactor this +import AddTagPopoverContent from "@app/components/AddTagPopoverContent/AddTagPopoverContent"; +import { ProjectPermissionCan } from "@app/components/permissions"; import { + FormControl, HoverCard, HoverCardContent, HoverCardTrigger, IconButton, Input, Popover, + PopoverContent, PopoverTrigger, SecretInput, Tag, + TextArea, Tooltip } from "@app/components/v2"; +import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context"; import { useToggle } from "@app/hooks"; import { WsTag } from "@app/hooks/api/types"; -import AddTagPopoverContent from "../../../../components/AddTagPopoverContent/AddTagPopoverContent"; import { FormData, SecretActionType } from "../../DashboardPage.utils"; type Props = { index: number; + environment: string; + secretPath: string; // backend generated unique id secUniqId?: string; // permission and external state's that decided to hide or show @@ -69,6 +78,8 @@ type Props = { export const SecretInputRow = memo( ({ index, + secretPath, + environment, isSecretValueHidden, onRowExpand, isReadOnly, @@ -79,7 +90,7 @@ export const SecretInputRow = memo( onSecretDelete, searchTerm, control, - // register, + register, setValue, isKeyError, keyError, @@ -217,7 +228,6 @@ export const SecretInputRow = memo(
{index + 1}
-
- - - - - + + {(isAllowed) => ( + + + + )} +
{!isAddOnly && (
- - -
- -
-
-
+ + {(isAllowed) => ( + +
+ +
+
+ )} +
)} - -
- - - + + +
+ - - - - onSelectTag(wsTag)} - handleTagOnMouseEnter={(wsTag: WsTag) => handleTagOnMouseEnter(wsTag)} - handleTagOnMouseLeave={() => handleTagOnMouseLeave()} - checkIfTagIsVisible={(wsTag: WsTag) => checkIfTagIsVisible(wsTag)} - handleOnCreateTagOpen={() => onCreateTagOpen()} - /> - -
- + {(isAllowed) => ( + + + + )} + +
+ + + +