Merge branch 'main' into gitlab-sso

This commit is contained in:
Andrew Atimapre
2023-09-27 12:57:53 +01:00
committed by GitHub
439 changed files with 27747 additions and 15919 deletions
+1 -1
View File
@@ -8,7 +8,7 @@ assignees: ''
--- ---
### Feature description ### Feature description
A clear and concise description of what the the feature should be. A clear and concise description of what the feature should be.
### Why would it be useful? ### Why would it be useful?
Why would this feature be useful for Infisical users? Why would this feature be useful for Infisical users?
@@ -17,9 +17,9 @@ jobs:
- name: 📦 Install dependencies to test all dependencies - name: 📦 Install dependencies to test all dependencies
run: npm ci --only-production run: npm ci --only-production
working-directory: backend working-directory: backend
- name: 🧪 Run tests # - name: 🧪 Run tests
run: npm run test:ci # run: npm run test:ci
working-directory: backend # working-directory: backend
- name: Save commit hashes for tag - name: Save commit hashes for tag
id: commit id: commit
uses: pr-mpt/actions-commit-hash@v2 uses: pr-mpt/actions-commit-hash@v2
+3 -3
View File
@@ -11,9 +11,9 @@ jobs:
- name: 📦 Install dependencies to test all dependencies - name: 📦 Install dependencies to test all dependencies
run: npm ci --only-production run: npm ci --only-production
working-directory: backend working-directory: backend
- name: 🧪 Run tests # - name: 🧪 Run tests
run: npm run test:ci # run: npm run test:ci
working-directory: backend # working-directory: backend
- name: Save commit hashes for tag - name: Save commit hashes for tag
id: commit id: commit
uses: pr-mpt/actions-commit-hash@v2 uses: pr-mpt/actions-commit-hash@v2
+3
View File
@@ -57,3 +57,6 @@ yarn-error.log*
# Infisical init # Infisical init
.infisical.json .infisical.json
# Editor specific
.vscode/*
+1 -1
View File
@@ -1 +1 @@
.github/resources/docker-compose.be-test.yml:generic-api-key:16 .github/resources/docker-compose.be-test.yml:generic-api-key:16
+1 -1
View File
@@ -34,7 +34,7 @@
<img src="https://img.shields.io/github/commit-activity/m/infisical/infisical" alt="git commit activity" /> <img src="https://img.shields.io/github/commit-activity/m/infisical/infisical" alt="git commit activity" />
</a> </a>
<a href="https://cloudsmith.io/~infisical/repos/"> <a href="https://cloudsmith.io/~infisical/repos/">
<img src="https://img.shields.io/badge/Downloads-821.8k-orange" alt="Cloudsmith downloads" /> <img src="https://img.shields.io/badge/Downloads-1.38M-orange" alt="Cloudsmith downloads" />
</a> </a>
<a href="https://infisical.com/slack"> <a href="https://infisical.com/slack">
<img src="https://img.shields.io/badge/chat-on%20Slack-blueviolet" alt="Slack community channel" /> <img src="https://img.shields.io/badge/chat-on%20Slack-blueviolet" alt="Slack community channel" />
+3 -3
View File
@@ -17,17 +17,17 @@ WORKDIR /app
ENV npm_config_cache /home/node/.npm ENV npm_config_cache /home/node/.npm
COPY package*.json ./ COPY package*.json ./
RUN npm ci --only-production RUN npm ci --only-production && npm cache clean --force
COPY --from=build /app . COPY --from=build /app .
RUN apk add --no-cache bash curl && curl -1sLf \ RUN apk add --no-cache bash curl && curl -1sLf \
'https://dl.cloudsmith.io/public/infisical/infisical-cli/setup.alpine.sh' | bash \ 'https://dl.cloudsmith.io/public/infisical/infisical-cli/setup.alpine.sh' | bash \
&& apk add infisical=0.8.1 && apk add infisical=0.8.1 && apk add --no-cache git
HEALTHCHECK --interval=10s --timeout=3s --start-period=10s \ HEALTHCHECK --interval=10s --timeout=3s --start-period=10s \
CMD node healthcheck.js CMD node healthcheck.js
EXPOSE 4000 EXPOSE 4000
CMD ["npm", "run", "start"] CMD ["node", "build/index.js"]
+197 -83
View File
@@ -10,6 +10,8 @@
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"@aws-sdk/client-secrets-manager": "^3.319.0", "@aws-sdk/client-secrets-manager": "^3.319.0",
"@casl/ability": "^6.5.0",
"@casl/mongoose": "^7.2.1",
"@godaddy/terminus": "^4.12.0", "@godaddy/terminus": "^4.12.0",
"@node-saml/passport-saml": "^4.0.4", "@node-saml/passport-saml": "^4.0.4",
"@octokit/rest": "^19.0.5", "@octokit/rest": "^19.0.5",
@@ -17,6 +19,7 @@
"@sentry/tracing": "^7.48.0", "@sentry/tracing": "^7.48.0",
"@types/crypto-js": "^4.1.1", "@types/crypto-js": "^4.1.1",
"@types/libsodium-wrappers": "^0.7.10", "@types/libsodium-wrappers": "^0.7.10",
"@ucast/mongo2js": "^1.3.4",
"argon2": "^0.30.3", "argon2": "^0.30.3",
"aws-sdk": "^2.1364.0", "aws-sdk": "^2.1364.0",
"axios": "^1.3.5", "axios": "^1.3.5",
@@ -34,6 +37,7 @@
"handlebars": "^4.7.7", "handlebars": "^4.7.7",
"helmet": "^5.1.1", "helmet": "^5.1.1",
"infisical-node": "^1.2.1", "infisical-node": "^1.2.1",
"ioredis": "^5.3.2",
"js-yaml": "^4.1.0", "js-yaml": "^4.1.0",
"jsonwebtoken": "^9.0.0", "jsonwebtoken": "^9.0.0",
"jsrp": "^0.2.4", "jsrp": "^0.2.4",
@@ -53,14 +57,14 @@
"query-string": "^7.1.3", "query-string": "^7.1.3",
"rate-limit-mongo": "^2.3.2", "rate-limit-mongo": "^2.3.2",
"rimraf": "^3.0.2", "rimraf": "^3.0.2",
"swagger-autogen": "^2.22.0",
"swagger-ui-express": "^4.6.2", "swagger-ui-express": "^4.6.2",
"tweetnacl": "^1.0.3", "tweetnacl": "^1.0.3",
"tweetnacl-util": "^0.15.1", "tweetnacl-util": "^0.15.1",
"typescript": "^4.9.3", "typescript": "^4.9.3",
"utility-types": "^3.10.0", "utility-types": "^3.10.0",
"winston": "^3.8.2", "winston": "^3.8.2",
"winston-loki": "^6.0.7" "winston-loki": "^6.0.6",
"zod": "^3.21.4"
}, },
"devDependencies": { "devDependencies": {
"@jest/globals": "^29.3.1", "@jest/globals": "^29.3.1",
@@ -93,6 +97,7 @@
"npm": "^8.19.3", "npm": "^8.19.3",
"smee-client": "^1.2.3", "smee-client": "^1.2.3",
"supertest": "^6.3.3", "supertest": "^6.3.3",
"swagger-autogen": "^2.23.5",
"ts-jest": "^29.0.3", "ts-jest": "^29.0.3",
"ts-node": "^10.9.1" "ts-node": "^10.9.1"
} }
@@ -3340,6 +3345,26 @@
"integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==",
"dev": true "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": { "node_modules/@colors/colors": {
"version": "1.5.0", "version": "1.5.0",
"resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz", "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz",
@@ -3485,8 +3510,7 @@
"node_modules/@ioredis/commands": { "node_modules/@ioredis/commands": {
"version": "1.2.0", "version": "1.2.0",
"resolved": "https://registry.npmjs.org/@ioredis/commands/-/commands-1.2.0.tgz", "resolved": "https://registry.npmjs.org/@ioredis/commands/-/commands-1.2.0.tgz",
"integrity": "sha512-Sx1pU8EM64o2BrqNpEO1CNLtKQwyhuXuqyfH7oGKCk+1a33d2r5saW8zNwm3j6BTExtjrv2BxTgzzkMwts6vGg==", "integrity": "sha512-Sx1pU8EM64o2BrqNpEO1CNLtKQwyhuXuqyfH7oGKCk+1a33d2r5saW8zNwm3j6BTExtjrv2BxTgzzkMwts6vGg=="
"dev": true
}, },
"node_modules/@istanbuljs/load-nyc-config": { "node_modules/@istanbuljs/load-nyc-config": {
"version": "1.1.0", "version": "1.1.0",
@@ -6175,6 +6199,37 @@
"url": "https://opencollective.com/typescript-eslint" "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": { "node_modules/@xmldom/xmldom": {
"version": "0.8.10", "version": "0.8.10",
"resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.10.tgz", "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.10.tgz",
@@ -6977,39 +7032,6 @@
"node": ">=12" "node": ">=12"
} }
}, },
"node_modules/bull/node_modules/denque": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz",
"integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==",
"dev": true,
"engines": {
"node": ">=0.10"
}
},
"node_modules/bull/node_modules/ioredis": {
"version": "5.3.2",
"resolved": "https://registry.npmjs.org/ioredis/-/ioredis-5.3.2.tgz",
"integrity": "sha512-1DKMMzlIHM02eBBVOFQ1+AolGjs6+xEcM4PDL7NqOS6szq7H9jSaEkIUH6/a5Hl241LzW6JLSiAbNvTQjUupUA==",
"dev": true,
"dependencies": {
"@ioredis/commands": "^1.1.1",
"cluster-key-slot": "^1.1.0",
"debug": "^4.3.4",
"denque": "^2.1.0",
"lodash.defaults": "^4.2.0",
"lodash.isarguments": "^3.1.0",
"redis-errors": "^1.2.0",
"redis-parser": "^3.0.0",
"standard-as-callback": "^2.1.0"
},
"engines": {
"node": ">=12.22.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/ioredis"
}
},
"node_modules/bytes": { "node_modules/bytes": {
"version": "3.1.2", "version": "3.1.2",
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
@@ -9017,30 +9039,36 @@
} }
}, },
"node_modules/ioredis": { "node_modules/ioredis": {
"version": "4.28.5", "version": "5.3.2",
"resolved": "https://registry.npmjs.org/ioredis/-/ioredis-4.28.5.tgz", "resolved": "https://registry.npmjs.org/ioredis/-/ioredis-5.3.2.tgz",
"integrity": "sha512-3GYo0GJtLqgNXj4YhrisLaNNvWSNwSS2wS4OELGfGxH8I69+XfNdnmV1AyN+ZqMh0i7eX+SWjrwFKDBDgfBC1A==", "integrity": "sha512-1DKMMzlIHM02eBBVOFQ1+AolGjs6+xEcM4PDL7NqOS6szq7H9jSaEkIUH6/a5Hl241LzW6JLSiAbNvTQjUupUA==",
"dependencies": { "dependencies": {
"@ioredis/commands": "^1.1.1",
"cluster-key-slot": "^1.1.0", "cluster-key-slot": "^1.1.0",
"debug": "^4.3.1", "debug": "^4.3.4",
"denque": "^1.1.0", "denque": "^2.1.0",
"lodash.defaults": "^4.2.0", "lodash.defaults": "^4.2.0",
"lodash.flatten": "^4.4.0",
"lodash.isarguments": "^3.1.0", "lodash.isarguments": "^3.1.0",
"p-map": "^2.1.0",
"redis-commands": "1.7.0",
"redis-errors": "^1.2.0", "redis-errors": "^1.2.0",
"redis-parser": "^3.0.0", "redis-parser": "^3.0.0",
"standard-as-callback": "^2.1.0" "standard-as-callback": "^2.1.0"
}, },
"engines": { "engines": {
"node": ">=6" "node": ">=12.22.0"
}, },
"funding": { "funding": {
"type": "opencollective", "type": "opencollective",
"url": "https://opencollective.com/ioredis" "url": "https://opencollective.com/ioredis"
} }
}, },
"node_modules/ioredis/node_modules/denque": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz",
"integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==",
"engines": {
"node": ">=0.10"
}
},
"node_modules/ip": { "node_modules/ip": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/ip/-/ip-2.0.0.tgz", "resolved": "https://registry.npmjs.org/ip/-/ip-2.0.0.tgz",
@@ -9969,6 +9997,7 @@
"version": "2.2.3", "version": "2.2.3",
"resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", "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,
"bin": { "bin": {
"json5": "lib/cli.js" "json5": "lib/cli.js"
}, },
@@ -14401,6 +14430,31 @@
"node": ">=10" "node": ">=10"
} }
}, },
"node_modules/probot/node_modules/ioredis": {
"version": "4.28.5",
"resolved": "https://registry.npmjs.org/ioredis/-/ioredis-4.28.5.tgz",
"integrity": "sha512-3GYo0GJtLqgNXj4YhrisLaNNvWSNwSS2wS4OELGfGxH8I69+XfNdnmV1AyN+ZqMh0i7eX+SWjrwFKDBDgfBC1A==",
"dependencies": {
"cluster-key-slot": "^1.1.0",
"debug": "^4.3.1",
"denque": "^1.1.0",
"lodash.defaults": "^4.2.0",
"lodash.flatten": "^4.4.0",
"lodash.isarguments": "^3.1.0",
"p-map": "^2.1.0",
"redis-commands": "1.7.0",
"redis-errors": "^1.2.0",
"redis-parser": "^3.0.0",
"standard-as-callback": "^2.1.0"
},
"engines": {
"node": ">=6"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/ioredis"
}
},
"node_modules/probot/node_modules/js-yaml": { "node_modules/probot/node_modules/js-yaml": {
"version": "3.14.1", "version": "3.14.1",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz",
@@ -15516,6 +15570,7 @@
"version": "2.23.5", "version": "2.23.5",
"resolved": "https://registry.npmjs.org/swagger-autogen/-/swagger-autogen-2.23.5.tgz", "resolved": "https://registry.npmjs.org/swagger-autogen/-/swagger-autogen-2.23.5.tgz",
"integrity": "sha512-4Tl2+XhZMyHoBYkABnScHtQE0lKPKUD3NBt09mClrI6UKOUYljKlYw1xiFVwsHCTGR2hAXmhT4PpgjruCtt1ZA==", "integrity": "sha512-4Tl2+XhZMyHoBYkABnScHtQE0lKPKUD3NBt09mClrI6UKOUYljKlYw1xiFVwsHCTGR2hAXmhT4PpgjruCtt1ZA==",
"dev": true,
"dependencies": { "dependencies": {
"acorn": "^7.4.1", "acorn": "^7.4.1",
"deepmerge": "^4.2.2", "deepmerge": "^4.2.2",
@@ -15527,6 +15582,7 @@
"version": "7.4.1", "version": "7.4.1",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz", "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz",
"integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==", "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==",
"dev": true,
"bin": { "bin": {
"acorn": "bin/acorn" "acorn": "bin/acorn"
}, },
@@ -16638,6 +16694,14 @@
"funding": { "funding": {
"url": "https://github.com/sponsors/sindresorhus" "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": { "dependencies": {
@@ -19336,6 +19400,20 @@
"integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==",
"dev": true "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": { "@colors/colors": {
"version": "1.5.0", "version": "1.5.0",
"resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz", "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz",
@@ -19449,8 +19527,7 @@
"@ioredis/commands": { "@ioredis/commands": {
"version": "1.2.0", "version": "1.2.0",
"resolved": "https://registry.npmjs.org/@ioredis/commands/-/commands-1.2.0.tgz", "resolved": "https://registry.npmjs.org/@ioredis/commands/-/commands-1.2.0.tgz",
"integrity": "sha512-Sx1pU8EM64o2BrqNpEO1CNLtKQwyhuXuqyfH7oGKCk+1a33d2r5saW8zNwm3j6BTExtjrv2BxTgzzkMwts6vGg==", "integrity": "sha512-Sx1pU8EM64o2BrqNpEO1CNLtKQwyhuXuqyfH7oGKCk+1a33d2r5saW8zNwm3j6BTExtjrv2BxTgzzkMwts6vGg=="
"dev": true
}, },
"@istanbuljs/load-nyc-config": { "@istanbuljs/load-nyc-config": {
"version": "1.1.0", "version": "1.1.0",
@@ -21587,6 +21664,37 @@
"eslint-visitor-keys": "^3.3.0" "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": { "@xmldom/xmldom": {
"version": "0.8.10", "version": "0.8.10",
"resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.10.tgz", "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.10.tgz",
@@ -22203,31 +22311,6 @@
"msgpackr": "^1.5.2", "msgpackr": "^1.5.2",
"semver": "^7.3.2", "semver": "^7.3.2",
"uuid": "^8.3.0" "uuid": "^8.3.0"
},
"dependencies": {
"denque": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz",
"integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==",
"dev": true
},
"ioredis": {
"version": "5.3.2",
"resolved": "https://registry.npmjs.org/ioredis/-/ioredis-5.3.2.tgz",
"integrity": "sha512-1DKMMzlIHM02eBBVOFQ1+AolGjs6+xEcM4PDL7NqOS6szq7H9jSaEkIUH6/a5Hl241LzW6JLSiAbNvTQjUupUA==",
"dev": true,
"requires": {
"@ioredis/commands": "^1.1.1",
"cluster-key-slot": "^1.1.0",
"debug": "^4.3.4",
"denque": "^2.1.0",
"lodash.defaults": "^4.2.0",
"lodash.isarguments": "^3.1.0",
"redis-errors": "^1.2.0",
"redis-parser": "^3.0.0",
"standard-as-callback": "^2.1.0"
}
}
} }
}, },
"bytes": { "bytes": {
@@ -23710,21 +23793,26 @@
"dev": true "dev": true
}, },
"ioredis": { "ioredis": {
"version": "4.28.5", "version": "5.3.2",
"resolved": "https://registry.npmjs.org/ioredis/-/ioredis-4.28.5.tgz", "resolved": "https://registry.npmjs.org/ioredis/-/ioredis-5.3.2.tgz",
"integrity": "sha512-3GYo0GJtLqgNXj4YhrisLaNNvWSNwSS2wS4OELGfGxH8I69+XfNdnmV1AyN+ZqMh0i7eX+SWjrwFKDBDgfBC1A==", "integrity": "sha512-1DKMMzlIHM02eBBVOFQ1+AolGjs6+xEcM4PDL7NqOS6szq7H9jSaEkIUH6/a5Hl241LzW6JLSiAbNvTQjUupUA==",
"requires": { "requires": {
"@ioredis/commands": "^1.1.1",
"cluster-key-slot": "^1.1.0", "cluster-key-slot": "^1.1.0",
"debug": "^4.3.1", "debug": "^4.3.4",
"denque": "^1.1.0", "denque": "^2.1.0",
"lodash.defaults": "^4.2.0", "lodash.defaults": "^4.2.0",
"lodash.flatten": "^4.4.0",
"lodash.isarguments": "^3.1.0", "lodash.isarguments": "^3.1.0",
"p-map": "^2.1.0",
"redis-commands": "1.7.0",
"redis-errors": "^1.2.0", "redis-errors": "^1.2.0",
"redis-parser": "^3.0.0", "redis-parser": "^3.0.0",
"standard-as-callback": "^2.1.0" "standard-as-callback": "^2.1.0"
},
"dependencies": {
"denque": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz",
"integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw=="
}
} }
}, },
"ip": { "ip": {
@@ -24425,7 +24513,8 @@
"json5": { "json5": {
"version": "2.2.3", "version": "2.2.3",
"resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", "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": { "jsonwebtoken": {
"version": "9.0.1", "version": "9.0.1",
@@ -27699,6 +27788,24 @@
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-8.6.0.tgz", "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-8.6.0.tgz",
"integrity": "sha512-IrPdXQsk2BbzvCBGBOTmmSH5SodmqZNt4ERAZDmW4CT+tL8VtvinqywuANaFu4bOMWki16nqf0e4oC0QIaDr/g==" "integrity": "sha512-IrPdXQsk2BbzvCBGBOTmmSH5SodmqZNt4ERAZDmW4CT+tL8VtvinqywuANaFu4bOMWki16nqf0e4oC0QIaDr/g=="
}, },
"ioredis": {
"version": "4.28.5",
"resolved": "https://registry.npmjs.org/ioredis/-/ioredis-4.28.5.tgz",
"integrity": "sha512-3GYo0GJtLqgNXj4YhrisLaNNvWSNwSS2wS4OELGfGxH8I69+XfNdnmV1AyN+ZqMh0i7eX+SWjrwFKDBDgfBC1A==",
"requires": {
"cluster-key-slot": "^1.1.0",
"debug": "^4.3.1",
"denque": "^1.1.0",
"lodash.defaults": "^4.2.0",
"lodash.flatten": "^4.4.0",
"lodash.isarguments": "^3.1.0",
"p-map": "^2.1.0",
"redis-commands": "1.7.0",
"redis-errors": "^1.2.0",
"redis-parser": "^3.0.0",
"standard-as-callback": "^2.1.0"
}
},
"js-yaml": { "js-yaml": {
"version": "3.14.1", "version": "3.14.1",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz",
@@ -28526,6 +28633,7 @@
"version": "2.23.5", "version": "2.23.5",
"resolved": "https://registry.npmjs.org/swagger-autogen/-/swagger-autogen-2.23.5.tgz", "resolved": "https://registry.npmjs.org/swagger-autogen/-/swagger-autogen-2.23.5.tgz",
"integrity": "sha512-4Tl2+XhZMyHoBYkABnScHtQE0lKPKUD3NBt09mClrI6UKOUYljKlYw1xiFVwsHCTGR2hAXmhT4PpgjruCtt1ZA==", "integrity": "sha512-4Tl2+XhZMyHoBYkABnScHtQE0lKPKUD3NBt09mClrI6UKOUYljKlYw1xiFVwsHCTGR2hAXmhT4PpgjruCtt1ZA==",
"dev": true,
"requires": { "requires": {
"acorn": "^7.4.1", "acorn": "^7.4.1",
"deepmerge": "^4.2.2", "deepmerge": "^4.2.2",
@@ -28536,7 +28644,8 @@
"acorn": { "acorn": {
"version": "7.4.1", "version": "7.4.1",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz", "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz",
"integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==" "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==",
"dev": true
} }
} }
}, },
@@ -29293,6 +29402,11 @@
"resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
"integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==",
"dev": true "dev": true
},
"zod": {
"version": "3.21.4",
"resolved": "https://registry.npmjs.org/zod/-/zod-3.21.4.tgz",
"integrity": "sha512-m46AKbrzKVzOzs/DZgVnG5H55N1sv1M8qZU3A8RIKbs3mrACDNeIOeilDymVb2HdmP8uwshOCF4uJ8uM9rCqJw=="
} }
} }
} }
+7 -2
View File
@@ -1,6 +1,8 @@
{ {
"dependencies": { "dependencies": {
"@aws-sdk/client-secrets-manager": "^3.319.0", "@aws-sdk/client-secrets-manager": "^3.319.0",
"@casl/ability": "^6.5.0",
"@casl/mongoose": "^7.2.1",
"@godaddy/terminus": "^4.12.0", "@godaddy/terminus": "^4.12.0",
"@node-saml/passport-saml": "^4.0.4", "@node-saml/passport-saml": "^4.0.4",
"@octokit/rest": "^19.0.5", "@octokit/rest": "^19.0.5",
@@ -8,6 +10,7 @@
"@sentry/tracing": "^7.48.0", "@sentry/tracing": "^7.48.0",
"@types/crypto-js": "^4.1.1", "@types/crypto-js": "^4.1.1",
"@types/libsodium-wrappers": "^0.7.10", "@types/libsodium-wrappers": "^0.7.10",
"@ucast/mongo2js": "^1.3.4",
"argon2": "^0.30.3", "argon2": "^0.30.3",
"aws-sdk": "^2.1364.0", "aws-sdk": "^2.1364.0",
"axios": "^1.3.5", "axios": "^1.3.5",
@@ -25,6 +28,7 @@
"handlebars": "^4.7.7", "handlebars": "^4.7.7",
"helmet": "^5.1.1", "helmet": "^5.1.1",
"infisical-node": "^1.2.1", "infisical-node": "^1.2.1",
"ioredis": "^5.3.2",
"js-yaml": "^4.1.0", "js-yaml": "^4.1.0",
"jsonwebtoken": "^9.0.0", "jsonwebtoken": "^9.0.0",
"jsrp": "^0.2.4", "jsrp": "^0.2.4",
@@ -44,14 +48,14 @@
"query-string": "^7.1.3", "query-string": "^7.1.3",
"rate-limit-mongo": "^2.3.2", "rate-limit-mongo": "^2.3.2",
"rimraf": "^3.0.2", "rimraf": "^3.0.2",
"swagger-autogen": "^2.22.0",
"swagger-ui-express": "^4.6.2", "swagger-ui-express": "^4.6.2",
"tweetnacl": "^1.0.3", "tweetnacl": "^1.0.3",
"tweetnacl-util": "^0.15.1", "tweetnacl-util": "^0.15.1",
"typescript": "^4.9.3", "typescript": "^4.9.3",
"utility-types": "^3.10.0", "utility-types": "^3.10.0",
"winston": "^3.8.2", "winston": "^3.8.2",
"winston-loki": "^6.0.7" "winston-loki": "^6.0.6",
"zod": "^3.21.4"
}, },
"name": "infisical-api", "name": "infisical-api",
"version": "1.0.0", "version": "1.0.0",
@@ -111,6 +115,7 @@
"npm": "^8.19.3", "npm": "^8.19.3",
"smee-client": "^1.2.3", "smee-client": "^1.2.3",
"supertest": "^6.3.3", "supertest": "^6.3.3",
"swagger-autogen": "^2.23.5",
"ts-jest": "^29.0.3", "ts-jest": "^29.0.3",
"ts-node": "^10.9.1" "ts-node": "^10.9.1"
}, },
+1975 -1412
View File
File diff suppressed because it is too large Load Diff
+9 -2
View File
@@ -17,6 +17,8 @@ import {
getJwtRefreshSecret getJwtRefreshSecret
} from "../../config"; } from "../../config";
import { ActorType } from "../../ee/models"; import { ActorType } from "../../ee/models";
import { validateRequest } from "../../helpers/validation";
import * as reqValidator from "../../validation/auth";
declare module "jsonwebtoken" { declare module "jsonwebtoken" {
export interface UserIDJwtPayload extends jwt.JwtPayload { export interface UserIDJwtPayload extends jwt.JwtPayload {
@@ -32,7 +34,9 @@ declare module "jsonwebtoken" {
* @returns * @returns
*/ */
export const login1 = async (req: Request, res: Response) => { 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({ const user = await User.findOne({
email email
@@ -76,7 +80,10 @@ export const login1 = async (req: Request, res: Response) => {
* @returns * @returns
*/ */
export const login2 = async (req: Request, res: Response) => { 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({ const user = await User.findOne({
email email
}).select("+salt +verifier +publicKey +encryptedPrivateKey +iv +tag"); }).select("+salt +verifier +publicKey +encryptedPrivateKey +iv +tag");
+91 -52
View File
@@ -2,10 +2,19 @@ import { Request, Response } from "express";
import { Types } from "mongoose"; import { Types } from "mongoose";
import { Bot, BotKey } from "../../models"; import { Bot, BotKey } from "../../models";
import { createBot } from "../../helpers/bot"; 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 { interface BotKey {
encryptedKey: string; encryptedKey: string;
nonce: string; nonce: string;
} }
/** /**
@@ -16,23 +25,30 @@ interface BotKey {
* @returns * @returns
*/ */
export const getBotByWorkspaceId = async (req: Request, res: Response) => { 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({ let bot = await Bot.findOne({
workspace: workspaceId, workspace: workspaceId
}); });
if (!bot) { if (!bot) {
// case: bot doesn't exist for workspace with id [workspaceId] // case: bot doesn't exist for workspace with id [workspaceId]
// -> create a new bot and return it // -> create a new bot and return it
bot = await createBot({ bot = await createBot({
name: "Infisical Bot", name: "Infisical Bot",
workspaceId: new Types.ObjectId(workspaceId), workspaceId: new Types.ObjectId(workspaceId)
}); });
} }
return res.status(200).send({ return res.status(200).send({
bot, bot
}); });
}; };
@@ -43,46 +59,69 @@ export const getBotByWorkspaceId = async (req: Request, res: Response) => {
* @returns * @returns
*/ */
export const setBotActiveState = async (req: Request, res: Response) => { export const setBotActiveState = async (req: Request, res: Response) => {
const { isActive, botKey }: { isActive: boolean, botKey: BotKey } = req.body; const {
body: { botKey, isActive },
if (isActive) { params: { botId }
// bot state set to active -> share workspace key with bot } = await validateRequest(reqValidator.SetBotActiveStateV1, req);
if (!botKey?.encryptedKey || !botKey?.nonce) {
return res.status(400).send({ const bot = await Bot.findById(botId);
message: "Failed to set bot state to active - missing bot key", if (!bot) {
}); throw BadRequestError({ message: "Bot not found" });
} }
const userId = req.user._id;
await BotKey.findOneAndUpdate({
workspace: req.bot.workspace, const { permission } = await getUserProjectPermissions(userId, bot.workspace.toString());
}, { ForbiddenError.from(permission).throwUnlessCan(
encryptedKey: botKey.encryptedKey, ProjectPermissionActions.Edit,
nonce: botKey.nonce, ProjectPermissionSub.Integrations
sender: req.user._id, );
bot: req.bot._id,
workspace: req.bot.workspace, if (isActive) {
}, { // bot state set to active -> share workspace key with bot
upsert: true, if (!botKey?.encryptedKey || !botKey?.nonce) {
new: true, return res.status(400).send({
}); message: "Failed to set bot state to active - missing bot key"
} else { });
// case: bot state set to inactive -> delete bot's workspace key
await BotKey.deleteOne({
bot: req.bot._id,
});
} }
const bot = await Bot.findOneAndUpdate({ await BotKey.findOneAndUpdate(
_id: req.bot._id, {
}, { workspace: bot.workspace
isActive, },
}, { {
new: true, encryptedKey: botKey.encryptedKey,
}); nonce: botKey.nonce,
sender: userId,
if (!bot) throw new Error("Failed to update bot active state"); bot: bot._id,
workspace: bot.workspace
return res.status(200).send({ },
bot, {
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
});
}; };
+2 -2
View File
@@ -15,7 +15,7 @@ import * as userController from "./userController";
import * as workspaceController from "./workspaceController"; import * as workspaceController from "./workspaceController";
import * as secretScanningController from "./secretScanningController"; import * as secretScanningController from "./secretScanningController";
import * as webhookController from "./webhookController"; import * as webhookController from "./webhookController";
import * as secretImportController from "./secretImportController"; import * as secretImpsController from "./secretImpsController";
export { export {
authController, authController,
@@ -35,5 +35,5 @@ export {
workspaceController, workspaceController,
secretScanningController, secretScanningController,
webhookController, webhookController,
secretImportController secretImpsController
}; };
File diff suppressed because it is too large Load Diff
@@ -1,6 +1,6 @@
import { Request, Response } from "express"; import { Request, Response } from "express";
import { Types } from "mongoose"; import { Types } from "mongoose";
import { Folder, Integration } from "../../models"; import { Folder, IWorkspace, Integration, IntegrationAuth } from "../../models";
import { EventService } from "../../services"; import { EventService } from "../../services";
import { eventStartIntegration } from "../../events"; import { eventStartIntegration } from "../../events";
import { getFolderByPath } from "../../services/FolderService"; import { getFolderByPath } from "../../services/FolderService";
@@ -8,6 +8,14 @@ import { BadRequestError } from "../../utils/errors";
import { EEAuditLogService } from "../../ee/services"; import { EEAuditLogService } from "../../ee/services";
import { EventType } from "../../ee/models"; import { EventType } from "../../ee/models";
import { syncSecretsToActiveIntegrationsQueue } from "../../queues/integrations/syncSecretsToThirdPartyServices"; 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 * Create/initialize an (empty) integration for integration authorization
@@ -17,24 +25,44 @@ import { syncSecretsToActiveIntegrationsQueue } from "../../queues/integrations/
*/ */
export const createIntegration = async (req: Request, res: Response) => { export const createIntegration = async (req: Request, res: Response) => {
const { const {
integrationAuthId, body: {
app, isActive,
appId, sourceEnvironment,
isActive, secretPath,
sourceEnvironment, app,
targetEnvironment, path,
targetEnvironmentId, appId,
targetService, owner,
targetServiceId, region,
owner, scope,
path, targetService,
region, targetServiceId,
secretPath, integrationAuthId,
metadata targetEnvironment,
} = req.body; 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({ const folders = await Folder.findOne({
workspace: req.integrationAuth.workspace._id, workspace: integrationAuth.workspace._id,
environment: sourceEnvironment environment: sourceEnvironment
}); });
@@ -42,7 +70,7 @@ export const createIntegration = async (req: Request, res: Response) => {
const folder = getFolderByPath(folders.nodes, secretPath); const folder = getFolderByPath(folders.nodes, secretPath);
if (!folder) { if (!folder) {
throw BadRequestError({ throw BadRequestError({
message: "Path for service token does not exist" message: "Folder path doesn't exist"
}); });
} }
} }
@@ -51,7 +79,7 @@ export const createIntegration = async (req: Request, res: Response) => {
// initialize new integration after saving integration access token // initialize new integration after saving integration access token
const integration = await new Integration({ const integration = await new Integration({
workspace: req.integrationAuth.workspace._id, workspace: integrationAuth.workspace._id,
environment: sourceEnvironment, environment: sourceEnvironment,
isActive, isActive,
app, app,
@@ -63,8 +91,9 @@ export const createIntegration = async (req: Request, res: Response) => {
owner, owner,
path, path,
region, region,
scope,
secretPath, secretPath,
integration: req.integrationAuth.integration, integration: integrationAuth.integration,
integrationAuth: new Types.ObjectId(integrationAuthId), integrationAuth: new Types.ObjectId(integrationAuthId),
metadata metadata
}).save(); }).save();
@@ -120,17 +149,32 @@ export const updateIntegration = async (req: Request, res: Response) => {
// integration has the correct fields populated in [Integration] // integration has the correct fields populated in [Integration]
const { const {
environment, body: {
isActive, environment,
app, isActive,
appId, app,
targetEnvironment, appId,
owner, // github-specific integration param targetEnvironment,
secretPath owner, // github-specific integration param
} = req.body; 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({ const folders = await Folder.findOne({
workspace: req.integration.workspace, workspace: integration.workspace,
environment environment
}); });
@@ -143,9 +187,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, environment,
@@ -161,18 +205,18 @@ export const updateIntegration = async (req: Request, res: Response) => {
} }
); );
if (integration) { if (updatedIntegration) {
// trigger event - push secrets // trigger event - push secrets
EventService.handleEvent({ EventService.handleEvent({
event: eventStartIntegration({ event: eventStartIntegration({
workspaceId: integration.workspace, workspaceId: updatedIntegration.workspace,
environment environment
}) })
}); });
} }
return res.status(200).send({ return res.status(200).send({
integration integration: updatedIntegration
}); });
}; };
@@ -183,13 +227,27 @@ export const updateIntegration = async (req: Request, res: Response) => {
* @returns * @returns
*/ */
export const deleteIntegration = async (req: Request, res: Response) => { 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 _id: integrationId
}); });
if (!integration) throw new Error("Failed to find integration"); if (!deletedIntegration) throw new Error("Failed to find integration");
await EEAuditLogService.createAuditLog( await EEAuditLogService.createAuditLog(
req.authData, req.authData,
@@ -221,14 +279,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) => { 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({ syncSecretsToActiveIntegrationsQueue({
workspaceId, workspaceId,
environment environment
}) });
res.status(200).send() res.status(200).send();
}; };
+33 -15
View File
@@ -4,6 +4,14 @@ import { Key } from "../../models";
import { findMembership } from "../../helpers/membership"; import { findMembership } from "../../helpers/membership";
import { EventType } from "../../ee/models"; import { EventType } from "../../ee/models";
import { EEAuditLogService } from "../../ee/services"; 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 * Add (encrypted) copy of workspace key for workspace with id [workspaceId] for user with
@@ -13,13 +21,21 @@ import { EEAuditLogService } from "../../ee/services";
* @returns * @returns
*/ */
export const uploadKey = async (req: Request, res: Response) => { export const uploadKey = async (req: Request, res: Response) => {
const { workspaceId } = req.params; const {
const { key } = req.body; 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 // validate membership of receiver
const receiverMembership = await findMembership({ const receiverMembership = await findMembership({
user: key.userId, user: key.userId,
workspace: workspaceId, workspace: workspaceId
}); });
if (!receiverMembership) { if (!receiverMembership) {
@@ -31,12 +47,12 @@ export const uploadKey = async (req: Request, res: Response) => {
nonce: key.nonce, nonce: key.nonce,
sender: req.user._id, sender: req.user._id,
receiver: key.userId, receiver: key.userId,
workspace: workspaceId, workspace: workspaceId
}).save(); }).save();
return res.status(200).send({ return res.status(200).send({
message: "Successfully uploaded key to workspace", message: "Successfully uploaded key to workspace"
}); });
}; };
/** /**
@@ -46,21 +62,23 @@ export const uploadKey = async (req: Request, res: Response) => {
* @returns * @returns
*/ */
export const getLatestKey = async (req: Request, res: Response) => { export const getLatestKey = async (req: Request, res: Response) => {
const { workspaceId } = req.params; const {
params: { workspaceId }
} = await validateRequest(reqValidator.GetLatestKeyV1, req);
// get latest key // get latest key
const latestKey = await Key.find({ const latestKey = await Key.find({
workspace: workspaceId, workspace: workspaceId,
receiver: req.user._id, receiver: req.user._id
}) })
.sort({ createdAt: -1 }) .sort({ createdAt: -1 })
.limit(1) .limit(1)
.populate("sender", "+publicKey"); .populate("sender", "+publicKey");
const resObj: any = {}; const resObj: any = {};
if (latestKey.length > 0) { if (latestKey.length > 0) {
resObj["latestKey"] = latestKey[0]; resObj["latestKey"] = latestKey[0];
await EEAuditLogService.createAuditLog( await EEAuditLogService.createAuditLog(
req.authData, req.authData,
{ {
@@ -73,7 +91,7 @@ export const getLatestKey = async (req: Request, res: Response) => {
workspaceId: new Types.ObjectId(workspaceId) workspaceId: new Types.ObjectId(workspaceId)
} }
); );
} }
return res.status(200).send(resObj); return res.status(200).send(resObj);
}; };
@@ -1,12 +1,23 @@
import { Request, Response } from "express"; import { Request, Response } from "express";
import { Types } from "mongoose"; 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 { EventType } from "../../ee/models";
import { deleteMembership as deleteMember, findMembership } from "../../helpers/membership"; import { deleteMembership as deleteMember, findMembership } from "../../helpers/membership";
import { sendMail } from "../../helpers/nodemailer"; import { sendMail } from "../../helpers/nodemailer";
import { ACCEPTED, ADMIN, MEMBER } from "../../variables"; import { ACCEPTED, ADMIN, CUSTOM, MEMBER, VIEWER } from "../../variables";
import { getSiteURL } from "../../config"; import { getSiteURL } from "../../config";
import { EEAuditLogService } from "../../ee/services"; 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] * Check that user is a member of workspace with id [workspaceId]
@@ -15,7 +26,10 @@ import { EEAuditLogService } from "../../ee/services";
* @returns * @returns
*/ */
export const validateMembership = async (req: Request, res: Response) => { export const validateMembership = async (req: Request, res: Response) => {
const { workspaceId } = req.params; const {
params: { workspaceId }
} = await validateRequest(reqValidator.ValidateMembershipV1, req);
// validate membership // validate membership
const membership = await findMembership({ const membership = await findMembership({
user: req.user._id, user: req.user._id,
@@ -38,8 +52,10 @@ export const validateMembership = async (req: Request, res: Response) => {
* @returns * @returns
*/ */
export const deleteMembership = async (req: Request, res: Response) => { 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 // check if membership to delete exists
const membershipToDelete = await Membership.findOne({ const membershipToDelete = await Membership.findOne({
_id: membershipId _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"); throw new Error("Failed to delete workspace membership that doesn't exist");
} }
// check if user is a member and admin of the workspace const { permission } = await getUserProjectPermissions(
// whose membership we wish to delete req.user._id,
const membership = await Membership.findOne({ membershipToDelete.workspace.toString()
user: req.user._id, );
workspace: membershipToDelete.workspace ForbiddenError.from(permission).throwUnlessCan(
}); ProjectPermissionActions.Delete,
ProjectPermissionSub.Member
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");
}
// delete workspace membership // delete workspace membership
const deletedMembership = await deleteMember({ const deletedMembership = await deleteMember({
membershipId: membershipToDelete._id.toString() membershipId: membershipToDelete._id.toString()
}); });
await EEAuditLogService.createAuditLog( await EEAuditLogService.createAuditLog(
req.authData, req.authData,
{ {
@@ -80,7 +89,7 @@ export const deleteMembership = async (req: Request, res: Response) => {
} }
}, },
{ {
workspaceId: membership.workspace workspaceId: membershipToDelete.workspace
} }
); );
@@ -96,43 +105,61 @@ export const deleteMembership = async (req: Request, res: Response) => {
* @returns * @returns
*/ */
export const changeMembershipRole = async (req: Request, res: Response) => { export const changeMembershipRole = async (req: Request, res: Response) => {
const { membershipId } = req.params; const {
const { role } = req.body; body: { role },
params: { membershipId }
if (![ADMIN, MEMBER].includes(role)) { } = await validateRequest(reqValidator.ChangeMembershipRoleV1, req);
throw new Error("Failed to validate role");
}
// validate target membership // validate target membership
const membershipToChangeRole = await Membership const membershipToChangeRole = await Membership.findById(membershipId).populate<{ user: IUser }>(
.findById(membershipId) "user"
.populate<{ user: IUser }>("user"); );
if (!membershipToChangeRole) { if (!membershipToChangeRole) {
throw new Error("Failed to find membership to change role"); throw new Error("Failed to find membership to change role");
} }
// check if user is a member and admin of target membership's const { permission } = await getUserProjectPermissions(
// workspace req.user._id,
const membership = await findMembership({ membershipToChangeRole.workspace.toString()
user: req.user._id, );
workspace: membershipToChangeRole.workspace ForbiddenError.from(permission).throwUnlessCan(
}); ProjectPermissionActions.Edit,
ProjectPermissionSub.Member
);
if (!membership) { const isCustomRole = ![ADMIN, MEMBER, VIEWER].includes(role);
throw new Error("Failed to validate membership"); 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) { const membership = await Membership.findByIdAndUpdate(
// user is not an admin member of the workspace membershipId,
throw new Error("Insufficient role for changing member roles"); {
} $set: {
role
const oldRole = membershipToChangeRole.role; },
$unset: {
customRole: 1
}
},
{
new: true
}
);
membershipToChangeRole.role = role;
await membershipToChangeRole.save();
await EEAuditLogService.createAuditLog( await EEAuditLogService.createAuditLog(
req.authData, req.authData,
{ {
@@ -140,8 +167,8 @@ export const changeMembershipRole = async (req: Request, res: Response) => {
metadata: { metadata: {
userId: membershipToChangeRole.user._id.toString(), userId: membershipToChangeRole.user._id.toString(),
email: membershipToChangeRole.user.email, email: membershipToChangeRole.user.email,
oldRole, oldRole: membershipToChangeRole.role,
newRole: membershipToChangeRole.role newRole: role
} }
}, },
{ {
@@ -150,7 +177,7 @@ export const changeMembershipRole = async (req: Request, res: Response) => {
); );
return res.status(200).send({ return res.status(200).send({
membership: membershipToChangeRole membership
}); });
}; };
@@ -161,8 +188,15 @@ export const changeMembershipRole = async (req: Request, res: Response) => {
* @returns * @returns
*/ */
export const inviteUserToWorkspace = async (req: Request, res: Response) => { export const inviteUserToWorkspace = async (req: Request, res: Response) => {
const { workspaceId } = req.params; const {
const { email }: { email: string } = req.body; 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({ const invitee = await User.findOne({
email email
@@ -179,11 +213,13 @@ export const inviteUserToWorkspace = async (req: Request, res: Response) => {
if (inviteeMembership) throw new Error("Failed to add existing member of workspace"); 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 // validate invitee's organization membership - ensure that only
// (accepted) organization members can be added to the workspace // (accepted) organization members can be added to the workspace
const membershipOrg = await MembershipOrg.findOne({ const membershipOrg = await MembershipOrg.findOne({
user: invitee._id, user: invitee._id,
organization: req.membership.workspace.organization, organization: workspace.organization,
status: ACCEPTED status: ACCEPTED
}); });
@@ -211,7 +247,7 @@ export const inviteUserToWorkspace = async (req: Request, res: Response) => {
substitutions: { substitutions: {
inviterFirstName: req.user.firstName, inviterFirstName: req.user.firstName,
inviterEmail: req.user.email, inviterEmail: req.user.email,
workspaceName: req.membership.workspace.name, workspaceName: workspace.name,
callback_url: (await getSiteURL()) + "/login" callback_url: (await getSiteURL()) + "/login"
} }
}); });
@@ -8,14 +8,8 @@ import { updateSubscriptionOrgQuantity } from "../../helpers/organization";
import { sendMail } from "../../helpers/nodemailer"; import { sendMail } from "../../helpers/nodemailer";
import { TokenService } from "../../services"; import { TokenService } from "../../services";
import { EELicenseService } from "../../ee/services"; import { EELicenseService } from "../../ee/services";
import { import { ACCEPTED, INVITED, MEMBER, TOKEN_EMAIL_ORG_INVITATION } from "../../variables";
ACCEPTED, import * as reqValidator from "../../validation/membershipOrg";
ADMIN,
INVITED,
MEMBER,
OWNER,
TOKEN_EMAIL_ORG_INVITATION
} from "../../variables";
import { import {
getJwtSignupLifetime, getJwtSignupLifetime,
getJwtSignupSecret, getJwtSignupSecret,
@@ -23,6 +17,13 @@ import {
getSmtpConfigured getSmtpConfigured
} from "../../config"; } from "../../config";
import { validateUserEmail } from "../../validation"; 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 * Delete organization membership with id [membershipOrgId] from organization
@@ -31,7 +32,9 @@ import { validateUserEmail } from "../../validation";
* @returns * @returns
*/ */
export const deleteMembershipOrg = async (req: Request, _res: Response) => { 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 // check if organization membership to delete exists
const membershipOrgToDelete = await MembershipOrg.findOne({ 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"); throw new Error("Failed to delete organization membership that doesn't exist");
} }
// check if user is a member and admin of the organization const { permission, membership: membershipOrg } = await getUserOrgPermissions(
// whose membership we wish to delete req.user._id,
const membershipOrg = await MembershipOrg.findOne({ membershipOrgToDelete.organization.toString()
user: req.user._id, );
organization: membershipOrgToDelete.organization ForbiddenError.from(permission).throwUnlessCan(
}); OrgPermissionActions.Delete,
OrgPermissionSubjects.Member
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");
}
// delete organization membership // delete organization membership
await deleteMemberFromOrg({ await deleteMemberFromOrg({
@@ -96,22 +92,20 @@ export const changeMembershipOrgRole = async (req: Request, res: Response) => {
*/ */
export const inviteUserToOrganization = async (req: Request, res: Response) => { export const inviteUserToOrganization = async (req: Request, res: Response) => {
let inviteeMembershipOrg, completeInviteLink; 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 host = req.headers.host;
const siteUrl = `${req.protocol}://${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 plan = await EELicenseService.getPlan(new Types.ObjectId(organizationId));
const ssoConfig = await SSOConfig.findOne({ const ssoConfig = await SSOConfig.findOne({
organization: new Types.ObjectId(organizationId) organization: new Types.ObjectId(organizationId)
}); });
@@ -119,9 +113,8 @@ export const inviteUserToOrganization = async (req: Request, res: Response) => {
if (ssoConfig && ssoConfig.isActive) { if (ssoConfig && ssoConfig.isActive) {
// case: SAML SSO is enabled for the organization // case: SAML SSO is enabled for the organization
return res.status(400).send({ return res.status(400).send({
message: message: "Failed to invite member due to SAML SSO configured for organization"
"Failed to invite member due to SAML SSO configured for organization" });
});
} }
if (plan.memberLimit !== null) { if (plan.memberLimit !== null) {
@@ -231,7 +224,10 @@ export const inviteUserToOrganization = async (req: Request, res: Response) => {
*/ */
export const verifyUserToOrganization = async (req: Request, res: Response) => { export const verifyUserToOrganization = async (req: Request, res: Response) => {
let user; 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"); user = await User.findOne({ email }).select("+publicKey");
@@ -1,28 +1,37 @@
import { Request, Response } from "express"; import { Request, Response } from "express";
import { import {
IncidentContactOrg, IncidentContactOrg,
Membership, Membership,
MembershipOrg, MembershipOrg,
Organization, Organization,
Workspace, Workspace
} from "../../models"; } from "../../models";
import { createOrganization as create } from "../../helpers/organization"; import { createOrganization as create } from "../../helpers/organization";
import { addMembershipsOrg } from "../../helpers/membershipOrg"; import { addMembershipsOrg } from "../../helpers/membershipOrg";
import { ACCEPTED, OWNER } from "../../variables"; import { ACCEPTED, ADMIN } from "../../variables";
import { getLicenseServerUrl, getSiteURL } from "../../config"; import { getLicenseServerUrl, getSiteURL } from "../../config";
import { licenseServerKeyRequest } from "../../config/request"; 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) => { export const getOrganizations = async (req: Request, res: Response) => {
const organizations = ( const organizations = (
await MembershipOrg.find({ await MembershipOrg.find({
user: req.user._id, user: req.user._id,
status: ACCEPTED, status: ACCEPTED
}).populate("organization") }).populate("organization")
).map((m) => m.organization); ).map((m) => m.organization);
return res.status(200).send({ return res.status(200).send({
organizations, organizations
}); });
}; };
/** /**
@@ -33,28 +42,26 @@ export const getOrganizations = async (req: Request, res: Response) => {
* @returns * @returns
*/ */
export const createOrganization = async (req: Request, res: Response) => { export const createOrganization = async (req: Request, res: Response) => {
const { organizationName } = req.body; const {
body: { organizationName }
if (organizationName.length < 1) { } = await validateRequest(reqValidator.CreateOrgv1, req);
throw new Error("Organization names must be at least 1-character long");
}
// create organization and add user as member // create organization and add user as member
const organization = await create({ const organization = await create({
email: req.user.email, email: req.user.email,
name: organizationName, name: organizationName
}); });
await addMembershipsOrg({ await addMembershipsOrg({
userIds: [req.user._id.toString()], userIds: [req.user._id.toString()],
organizationId: organization._id.toString(), organizationId: organization._id.toString(),
roles: [OWNER], roles: [ADMIN],
statuses: [ACCEPTED], statuses: [ACCEPTED]
}); });
return res.status(200).send({ return res.status(200).send({
organization, organization
}); });
}; };
/** /**
@@ -64,10 +71,23 @@ export const createOrganization = async (req: Request, res: Response) => {
* @returns * @returns
*/ */
export const getOrganization = async (req: Request, res: Response) => { export const getOrganization = async (req: Request, res: Response) => {
const organization = req.organization const {
return res.status(200).send({ params: { organizationId }
organization, } = 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 * @returns
*/ */
export const getOrganizationMembers = async (req: Request, res: Response) => { 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({ const users = await MembershipOrg.find({
organization: organizationId, organization: organizationId
}).populate("user", "+publicKey"); }).populate("user", "+publicKey");
return res.status(200).send({ return res.status(200).send({
users, users
}); });
}; };
/** /**
@@ -94,17 +122,22 @@ export const getOrganizationMembers = async (req: Request, res: Response) => {
* @param res * @param res
* @returns * @returns
*/ */
export const getOrganizationWorkspaces = async ( export const getOrganizationWorkspaces = async (req: Request, res: Response) => {
req: Request, const {
res: Response params: { organizationId }
) => { } = await validateRequest(reqValidator.GetOrgWorkspacesv1, req);
const { organizationId } = req.params;
const { permission } = await getUserOrgPermissions(req.user._id, organizationId);
ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionActions.Read,
OrgPermissionSubjects.Workspace
);
const workspacesSet = new Set( const workspacesSet = new Set(
( (
await Workspace.find( await Workspace.find(
{ {
organization: organizationId, organization: organizationId
}, },
"_id" "_id"
) )
@@ -113,15 +146,15 @@ export const getOrganizationWorkspaces = async (
const workspaces = ( const workspaces = (
await Membership.find({ await Membership.find({
user: req.user._id, user: req.user._id
}).populate("workspace") }).populate("workspace")
) )
.filter((m) => workspacesSet.has(m.workspace._id.toString())) .filter((m) => workspacesSet.has(m.workspace._id.toString()))
.map((m) => m.workspace); .map((m) => m.workspace);
return res.status(200).send({ return res.status(200).send({
workspaces, workspaces
}); });
}; };
/** /**
@@ -131,25 +164,33 @@ export const getOrganizationWorkspaces = async (
* @returns * @returns
*/ */
export const changeOrganizationName = async (req: Request, res: Response) => { export const changeOrganizationName = async (req: Request, res: Response) => {
const { organizationId } = req.params; const {
const { name } = req.body; 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( const organization = await Organization.findOneAndUpdate(
{ {
_id: organizationId, _id: organizationId
}, },
{ {
name, name
}, },
{ {
new: true, new: true
} }
); );
return res.status(200).send({ return res.status(200).send({
message: "Successfully changed organization name", message: "Successfully changed organization name",
organization, organization
}); });
}; };
/** /**
@@ -158,19 +199,24 @@ export const changeOrganizationName = async (req: Request, res: Response) => {
* @param res * @param res
* @returns * @returns
*/ */
export const getOrganizationIncidentContacts = async ( export const getOrganizationIncidentContacts = async (req: Request, res: Response) => {
req: Request, const {
res: Response params: { organizationId }
) => { } = await validateRequest(reqValidator.GetOrgIncidentContactv1, req);
const { organizationId } = req.params;
const { permission } = await getUserOrgPermissions(req.user._id, organizationId);
ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionActions.Read,
OrgPermissionSubjects.IncidentAccount
);
const incidentContactsOrg = await IncidentContactOrg.find({ const incidentContactsOrg = await IncidentContactOrg.find({
organization: organizationId, organization: organizationId
}); });
return res.status(200).send({ return res.status(200).send({
incidentContactsOrg, incidentContactsOrg
}); });
}; };
/** /**
@@ -179,12 +225,17 @@ export const getOrganizationIncidentContacts = async (
* @param res * @param res
* @returns * @returns
*/ */
export const addOrganizationIncidentContact = async ( export const addOrganizationIncidentContact = async (req: Request, res: Response) => {
req: Request, const {
res: Response params: { organizationId },
) => { body: { email }
const { organizationId } = req.params; } = await validateRequest(reqValidator.CreateOrgIncideContact, req);
const { email } = req.body;
const { permission } = await getUserOrgPermissions(req.user._id, organizationId);
ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionActions.Create,
OrgPermissionSubjects.IncidentAccount
);
const incidentContactOrg = await IncidentContactOrg.findOneAndUpdate( const incidentContactOrg = await IncidentContactOrg.findOneAndUpdate(
{ email, organization: organizationId }, { email, organization: organizationId },
@@ -192,9 +243,9 @@ export const addOrganizationIncidentContact = async (
{ upsert: true, new: true } { upsert: true, new: true }
); );
return res.status(200).send({ return res.status(200).send({
incidentContactOrg, incidentContactOrg
}); });
}; };
/** /**
@@ -203,22 +254,27 @@ export const addOrganizationIncidentContact = async (
* @param res * @param res
* @returns * @returns
*/ */
export const deleteOrganizationIncidentContact = async ( export const deleteOrganizationIncidentContact = async (req: Request, res: Response) => {
req: Request, const {
res: Response params: { organizationId },
) => { body: { email }
const { organizationId } = req.params; } = await validateRequest(reqValidator.DelOrgIncideContact, req);
const { email } = req.body;
const { permission } = await getUserOrgPermissions(req.user._id, organizationId);
ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionActions.Delete,
OrgPermissionSubjects.IncidentAccount
);
const incidentContactOrg = await IncidentContactOrg.findOneAndDelete({ const incidentContactOrg = await IncidentContactOrg.findOneAndDelete({
email, email,
organization: organizationId, organization: organizationId
}); });
return res.status(200).send({ return res.status(200).send({
message: "Successfully deleted organization incident contact", message: "Successfully deleted organization incident contact",
incidentContactOrg, incidentContactOrg
}); });
}; };
/** /**
@@ -228,19 +284,41 @@ export const deleteOrganizationIncidentContact = async (
* @param res * @param res
* @returns * @returns
*/ */
export const createOrganizationPortalSession = async ( export const createOrganizationPortalSession = async (req: Request, res: Response) => {
req: Request, const {
res: Response params: { organizationId }
) => { } = await validateRequest(reqValidator.GetOrgPlanBillingInfov1, req);
const { data: { pmtMethods } } = await licenseServerKeyRequest.get(
`${await getLicenseServerUrl()}/api/license-server/v1/customers/${req.organization.customerId}/billing-details/payment-methods`, 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) { if (pmtMethods.length < 1) {
// case: organization has no payment method on file // case: organization has no payment method on file
// -> redirect to add payment method portal // -> redirect to add payment method portal
const { data: { url } } = await licenseServerKeyRequest.post( const {
`${await getLicenseServerUrl()}/api/license-server/v1/customers/${req.organization.customerId}/billing-details/payment-methods`, data: { url }
} = await licenseServerKeyRequest.post(
`${await getLicenseServerUrl()}/api/license-server/v1/customers/${
organization.customerId
}/billing-details/payment-methods`,
{ {
success_url: (await getSiteURL()) + "/dashboard", success_url: (await getSiteURL()) + "/dashboard",
cancel_url: (await getSiteURL()) + "/dashboard" cancel_url: (await getSiteURL()) + "/dashboard"
@@ -250,8 +328,12 @@ export const createOrganizationPortalSession = async (
} else { } else {
// case: organization has payment method on file // case: organization has payment method on file
// -> redirect to billing portal // -> redirect to billing portal
const { data: { url } } = await licenseServerKeyRequest.post( const {
`${await getLicenseServerUrl()}/api/license-server/v1/customers/${req.organization.customerId}/billing-details/billing-portal`, data: { url }
} = await licenseServerKeyRequest.post(
`${await getLicenseServerUrl()}/api/license-server/v1/customers/${
organization.customerId
}/billing-details/billing-portal`,
{ {
return_url: (await getSiteURL()) + "/dashboard" return_url: (await getSiteURL()) + "/dashboard"
} }
@@ -266,36 +348,43 @@ export const createOrganizationPortalSession = async (
* @param res * @param res
* @returns * @returns
*/ */
export const getOrganizationMembersAndTheirWorkspaces = async ( export const getOrganizationMembersAndTheirWorkspaces = async (req: Request, res: Response) => {
req: Request, const {
res: Response params: { organizationId }
) => { } = await validateRequest(reqValidator.GetOrgMembersv1, req);
const { organizationId } = req.params;
const workspacesSet = ( const { permission } = await getUserOrgPermissions(req.user._id, organizationId);
await Workspace.find( ForbiddenError.from(permission).throwUnlessCan(
{ OrgPermissionActions.Read,
organization: organizationId, OrgPermissionSubjects.Member
}, );
"_id" ForbiddenError.from(permission).throwUnlessCan(
) OrgPermissionActions.Read,
).map((w) => w._id.toString()); OrgPermissionSubjects.Workspace
);
const memberships = ( const workspacesSet = (
await Membership.find({ await Workspace.find(
workspace: { $in: workspacesSet }, {
}).populate("workspace") organization: organizationId
); },
const userToWorkspaceIds: any = {}; "_id"
)
).map((w) => w._id.toString());
memberships.forEach(membership => { const memberships = await Membership.find({
const user = membership.user.toString(); workspace: { $in: workspacesSet }
if (userToWorkspaceIds[user]) { }).populate("workspace");
userToWorkspaceIds[user].push(membership.workspace); const userToWorkspaceIds: any = {};
} else {
userToWorkspaceIds[user] = [membership.workspace];
}
});
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);
}; };
@@ -14,6 +14,8 @@ import {
getSiteURL getSiteURL
} from "../../config"; } from "../../config";
import { ActorType } from "../../ee/models"; 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] * Password reset step 1: Send email verification link to email [email]
@@ -23,7 +25,9 @@ import { ActorType } from "../../ee/models";
* @returns * @returns
*/ */
export const emailPasswordReset = async (req: Request, res: Response) => { 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"); const user = await User.findOne({ email }).select("+publicKey");
if (!user || !user?.publicKey) { if (!user || !user?.publicKey) {
@@ -62,7 +66,9 @@ export const emailPasswordReset = async (req: Request, res: Response) => {
* @returns * @returns
*/ */
export const emailPasswordResetVerify = async (req: Request, res: Response) => { 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"); const user = await User.findOne({ email }).select("+publicKey");
if (!user || !user?.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) => { export const srp1 = async (req: Request, res: Response) => {
// return salt, serverPublicKey as part of first step of SRP protocol // 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({ const user = await User.findOne({
email: req.user.email email: req.user.email
}).select("+salt +verifier"); }).select("+salt +verifier");
@@ -149,16 +157,18 @@ export const srp1 = async (req: Request, res: Response) => {
*/ */
export const changePassword = async (req: Request, res: Response) => { export const changePassword = async (req: Request, res: Response) => {
const { const {
clientProof, body: {
protectedKey, clientProof,
protectedKeyIV, protectedKey,
protectedKeyTag, protectedKeyIV,
encryptedPrivateKey, protectedKeyTag,
encryptedPrivateKeyIV, encryptedPrivateKey,
encryptedPrivateKeyTag, encryptedPrivateKeyIV,
salt, encryptedPrivateKeyTag,
verifier salt,
} = req.body; verifier
}
} = await validateRequest(reqValidator.ChangePasswordV1, req);
const user = await User.findOne({ const user = await User.findOne({
email: req.user.email email: req.user.email
@@ -208,10 +218,7 @@ export const changePassword = async (req: Request, res: Response) => {
} }
); );
if ( if (req.authData.actor.type === ActorType.USER && req.authData.tokenVersionId) {
req.authData.actor.type === ActorType.USER &&
req.authData.tokenVersionId
) {
await clearTokens(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 // create/change backup private key
// requires verifying [clientProof] as part of second step of SRP protocol // requires verifying [clientProof] as part of second step of SRP protocol
// as initiated in /srp1 // as initiated in /srp1
const {
const { clientProof, encryptedPrivateKey, iv, tag, salt, verifier } = req.body; body: { clientProof, encryptedPrivateKey, salt, verifier, iv, tag }
} = await validateRequest(reqValidator.CreateBackupPrivateKeyV1, req);
const user = await User.findOne({ const user = await User.findOne({
email: req.user.email email: req.user.email
}).select("+salt +verifier"); }).select("+salt +verifier");
@@ -325,15 +333,17 @@ export const getBackupPrivateKey = async (req: Request, res: Response) => {
export const resetPassword = async (req: Request, res: Response) => { export const resetPassword = async (req: Request, res: Response) => {
const { const {
protectedKey, body: {
protectedKeyIV, encryptedPrivateKey,
protectedKeyTag, protectedKeyTag,
encryptedPrivateKey, protectedKey,
encryptedPrivateKeyIV, protectedKeyIV,
encryptedPrivateKeyTag, salt,
salt, verifier,
verifier encryptedPrivateKeyIV,
} = req.body; encryptedPrivateKeyTag
}
} = await validateRequest(reqValidator.ResetPasswordV1, req);
await User.findByIdAndUpdate( await User.findByIdAndUpdate(
req.user._id.toString(), req.user._id.toString(),
@@ -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 });
};
@@ -0,0 +1,706 @@
import { Request, Response } from "express";
import { isValidScope } from "../../helpers";
import { Folder, IServiceTokenData, 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
let isValidScopeAccess = isValidScope(req.authData.authPayload, environment, secretPath);
if (!isValidScopeAccess) {
throw UnauthorizedRequestError({ message: "Folder Permission Denied" });
}
isValidScopeAccess = isValidScope(
req.authData.authPayload,
secretImport.environment,
secretImport.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.Secrets, { environment, secretPath })
);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read,
subject(ProjectPermissionSub.Secrets, {
environment: secretImport.environment,
secretPath: secretImport.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.Secrets, {
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.Secrets, {
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.Secrets, {
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;
}
let permissionCheckFn: (env: string, secPath: string) => boolean; // used to pass as callback function to import secret
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" });
}
permissionCheckFn = (env: string, secPath: string) =>
isValidScope(req.authData.authPayload as IServiceTokenData, env, secPath);
} else {
const { permission } = await getUserProjectPermissions(
req.user._id,
importSecDoc.workspace.toString()
);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read,
subject(ProjectPermissionSub.Secrets, {
environment: importSecDoc.environment,
secretPath
})
);
permissionCheckFn = (env: string, secPath: string) =>
permission.can(
ProjectPermissionActions.Read,
subject(ProjectPermissionSub.Secrets, {
environment: env,
secretPath: secPath
})
);
}
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,
permissionCheckFn
);
return res.status(200).json({ secrets });
};
@@ -2,17 +2,49 @@ import { Request, Response } from "express";
import GitAppInstallationSession from "../../ee/models/gitAppInstallationSession"; import GitAppInstallationSession from "../../ee/models/gitAppInstallationSession";
import crypto from "crypto"; import crypto from "crypto";
import { Types } from "mongoose"; import { Types } from "mongoose";
import { UnauthorizedRequestError } from "../../utils/errors"; import { OrganizationNotFoundError, UnauthorizedRequestError } from "../../utils/errors";
import GitAppOrganizationInstallation from "../../ee/models/gitAppOrganizationInstallation"; import GitAppOrganizationInstallation from "../../ee/models/gitAppOrganizationInstallation";
import { MembershipOrg } from "../../models"; import { scanGithubFullRepoForSecretLeaks } from "../../queues/secret-scanning/githubScanFullRepository";
import GitRisks, { STATUS_RESOLVED_FALSE_POSITIVE, STATUS_RESOLVED_NOT_REVOKED, STATUS_RESOLVED_REVOKED } from "../../ee/models/gitRisks"; import { getSecretScanningGitAppId, getSecretScanningPrivateKey } from "../../config";
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) => { export const createInstallationSession = async (req: Request, res: Response) => {
const sessionId = crypto.randomBytes(16).toString("hex"); 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( await GitAppInstallationSession.findByIdAndUpdate(
req.organization, organization,
{ {
organization: new Types.ObjectId(req.organization), organization: organization.id,
sessionId: sessionId, sessionId: sessionId,
user: new Types.ObjectId(req.user._id) user: new Types.ObjectId(req.user._id)
}, },
@@ -21,71 +53,128 @@ export const createInstallationSession = async (req: Request, res: Response) =>
res.send({ res.send({
sessionId: sessionId sessionId: sessionId
}) });
} };
export const linkInstallationToOrganization = async (req: Request, res: Response) => { 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) { if (!installationSession) {
throw UnauthorizedRequestError() throw UnauthorizedRequestError();
} }
const userMembership = await MembershipOrg.find({ user: req.user._id, organization: installationSession.organization }) const { permission } = await getUserOrgPermissions(
if (!userMembership) { req.user._id,
throw UnauthorizedRequestError() 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 octokit = new ProbotOctokit({
auth: {
appId: await getSecretScanningGitAppId(),
privateKey: await getSecretScanningPrivateKey(),
installationId: installationId.toString()
}
});
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 }
});
} }
res.json(installationLink);
const installationLink = await GitAppOrganizationInstallation.findOneAndUpdate({ };
organizationId: installationSession.organization,
}, {
installationId: installationId,
organizationId: installationSession.organization,
user: installationSession.user
}, {
upsert: true
}).lean()
res.json(installationLink)
}
export const getCurrentOrganizationInstallationStatus = async (req: Request, res: Response) => { export const getCurrentOrganizationInstallationStatus = async (req: Request, res: Response) => {
const { organizationId } = req.params const { organizationId } = req.params;
try { try {
const appInstallation = await GitAppOrganizationInstallation.findOne({ organizationId: organizationId }).lean() const appInstallation = await GitAppOrganizationInstallation.findOne({
organizationId: organizationId
}).lean();
if (!appInstallation) { if (!appInstallation) {
res.json({ res.json({
appInstallationComplete: false appInstallationComplete: false
}) });
} }
res.json({ res.json({
appInstallationComplete: true appInstallationComplete: true
}) });
} catch { } catch {
res.json({ res.json({
appInstallationComplete: false appInstallationComplete: false
}) });
} }
} };
export const getRisksForOrganization = async (req: Request, res: Response) => { export const getRisksForOrganization = async (req: Request, res: Response) => {
const { organizationId } = req.params const {
const risks = await GitRisks.find({ organization: organizationId }).sort({ createdAt: -1 }).lean() 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({ res.json({
risks: risks risks: risks
}) });
} };
export const updateRisksStatus = async (req: Request, res: Response) => { export const updateRisksStatus = async (req: Request, res: Response) => {
const { riskId } = req.params const {
const { status } = req.body params: { organizationId, riskId },
const isRiskResolved = status == STATUS_RESOLVED_FALSE_POSITIVE || status == STATUS_RESOLVED_REVOKED || status == STATUS_RESOLVED_NOT_REVOKED ? true : false 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, { const risk = await GitRisks.findByIdAndUpdate(riskId, {
status: status, status: status,
isResolved: isRiskResolved isResolved: isRiskResolved
}).lean() }).lean();
res.json(risk) res.json(risk);
} };
@@ -1,10 +1,12 @@
import { ForbiddenError, subject } from "@casl/ability";
import { Request, Response } from "express"; import { Request, Response } from "express";
import { Types } from "mongoose"; import { Types } from "mongoose";
import { EventType, FolderVersion } from "../../ee/models"; import { EventType, FolderVersion } from "../../ee/models";
import { EEAuditLogService, EESecretService } from "../../ee/services"; import { EEAuditLogService, EESecretService } from "../../ee/services";
import { validateMembership } from "../../helpers/membership";
import { isValidScope } from "../../helpers/secrets"; 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 { import {
appendFolder, appendFolder,
deleteFolderById, deleteFolderById,
@@ -15,12 +17,99 @@ import {
getParentFromFolderId, getParentFromFolderId,
validateFolderName validateFolderName
} from "../../services/FolderService"; } from "../../services/FolderService";
import {
ProjectPermissionActions,
ProjectPermissionSub,
getUserProjectPermissions
} from "../../ee/services/ProjectRoleService";
import { BadRequestError, UnauthorizedRequestError } from "../../utils/errors"; 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) => { 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)) { if (!validateFolderName(folderName)) {
throw BadRequestError({ throw BadRequestError({
message: "Folder name cannot contain spaces. Only underscore and dashes" message: "Folder name cannot contain spaces. Only underscore and dashes"
@@ -32,8 +121,20 @@ export const createFolder = async (req: Request, res: Response) => {
environment environment
}).lean(); }).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.Secrets, { environment, secretPath })
);
}
// space has no folders initialized // space has no folders initialized
if (!folders) { if (!folders) {
if (req.authData.authPayload instanceof ServiceTokenData) { if (req.authData.authPayload instanceof ServiceTokenData) {
// root check // root check
@@ -62,10 +163,10 @@ export const createFolder = async (req: Request, res: Response) => {
}); });
await folderVersion.save(); await folderVersion.save();
await EESecretService.takeSecretSnapshot({ await EESecretService.takeSecretSnapshot({
workspaceId, workspaceId: new Types.ObjectId(workspaceId),
environment environment
}); });
await EEAuditLogService.createAuditLog( await EEAuditLogService.createAuditLog(
req.authData, req.authData,
{ {
@@ -86,9 +187,9 @@ export const createFolder = async (req: Request, res: Response) => {
} }
const folder = appendFolder(folders.nodes, { folderName, parentFolderId }); const folder = appendFolder(folders.nodes, { folderName, parentFolderId });
await Folder.findByIdAndUpdate(folders._id, folders); await Folder.findByIdAndUpdate(folders._id, folders);
const { folder: parentFolder, folderPath: parentFolderPath } = getFolderWithPathFromId( const { folder: parentFolder, folderPath: parentFolderPath } = getFolderWithPathFromId(
folders.nodes, folders.nodes,
parentFolderId || "root" parentFolderId || "root"
@@ -116,13 +217,13 @@ export const createFolder = async (req: Request, res: Response) => {
await folderVersion.save(); await folderVersion.save();
await EESecretService.takeSecretSnapshot({ await EESecretService.takeSecretSnapshot({
workspaceId, workspaceId: new Types.ObjectId(workspaceId),
environment, environment,
folderId: parentFolderId folderId: parentFolderId
}); });
const {folderPath} = getFolderWithPathFromId(folders.nodes, folder.id); const { folderPath } = getFolderWithPathFromId(folders.nodes, folder.id);
await EEAuditLogService.createAuditLog( await EEAuditLogService.createAuditLog(
req.authData, req.authData,
{ {
@@ -142,9 +243,99 @@ export const createFolder = async (req: Request, res: Response) => {
return res.json({ folder }); return res.json({ folder });
}; };
/**
* Update folder with id [folderId]
* @param req
* @param res
* @returns
*/
export const updateFolderById = async (req: Request, res: Response) => { 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)) { if (!validateFolderName(name)) {
throw BadRequestError({ throw BadRequestError({
message: "Folder name cannot contain spaces. Only underscore and dashes" 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" }); 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); const parentFolder = getParentFromFolderId(folders.nodes, folderId);
if (!parentFolder) { if (!parentFolder) {
throw BadRequestError({ message: "The folder doesn't exist" }); 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.Secrets, { environment, secretPath })
);
}
const folder = parentFolder.children.find(({ id }) => id === folderId); const folder = parentFolder.children.find(({ id }) => id === folderId);
if (!folder) { if (!folder) {
throw BadRequestError({ message: "The folder doesn't exist" }); throw BadRequestError({ message: "The folder doesn't exist" });
} }
@@ -197,13 +388,13 @@ export const updateFolderById = async (req: Request, res: Response) => {
await folderVersion.save(); await folderVersion.save();
await EESecretService.takeSecretSnapshot({ await EESecretService.takeSecretSnapshot({
workspaceId, workspaceId: new Types.ObjectId(workspaceId),
environment, environment,
folderId: parentFolder.id folderId: parentFolder.id
}); });
const {folderPath} = getFolderWithPathFromId(folders.nodes, folder.id); const { folderPath } = getFolderWithPathFromId(folders.nodes, folder.id);
await EEAuditLogService.createAuditLog( await EEAuditLogService.createAuditLog(
req.authData, 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) => { 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 }); const folders = await Folder.findOne({ workspace: workspaceId, environment });
if (!folders) { if (!folders) {
throw BadRequestError({ message: "The folder doesn't exist" }); 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); const delOp = deleteFolderById(folders.nodes, folderId);
if (!delOp) { if (!delOp) {
throw BadRequestError({ message: "The folder doesn't exist" }); throw BadRequestError({ message: "The folder doesn't exist" });
} }
const { deletedNode: delFolder, parent: parentFolder } = delOp; const { deletedNode: delFolder, parent: parentFolder } = delOp;
const { folderPath: secretPath } = getFolderWithPathFromId(folders.nodes, parentFolder.id);
if (req.authData.authPayload instanceof ServiceTokenData) { if (req.authData.authPayload instanceof ServiceTokenData) {
const { folderPath: secretPath } = getFolderWithPathFromId(folders.nodes, parentFolder.id);
const isValidScopeAccess = isValidScope(req.authData.authPayload, environment, secretPath); const isValidScopeAccess = isValidScope(req.authData.authPayload, environment, secretPath);
if (!isValidScopeAccess) { if (!isValidScopeAccess) {
throw UnauthorizedRequestError({ message: "Folder Permission Denied" }); 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.Secrets, { environment, secretPath })
);
} }
parentFolder.version += 1; parentFolder.version += 1;
@@ -280,7 +554,7 @@ export const deleteFolder = async (req: Request, res: Response) => {
} }
await EESecretService.takeSecretSnapshot({ await EESecretService.takeSecretSnapshot({
workspaceId, workspaceId: new Types.ObjectId(workspaceId),
environment, environment,
folderId: parentFolder.id folderId: parentFolder.id
}); });
@@ -288,12 +562,12 @@ export const deleteFolder = async (req: Request, res: Response) => {
await EEAuditLogService.createAuditLog( await EEAuditLogService.createAuditLog(
req.authData, req.authData,
{ {
type: EventType.DELETE_FOLDER , type: EventType.DELETE_FOLDER,
metadata: { metadata: {
environment, environment,
folderId, folderId,
folderName: delFolder.name, folderName: delFolder.name,
folderPath folderPath: secretPath
} }
}, },
{ {
@@ -304,30 +578,117 @@ export const deleteFolder = async (req: Request, res: Response) => {
res.send({ message: "successfully deleted folders", folders: delFolderIds }); 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) => { export const getFolders = async (req: Request, res: Response) => {
const { workspaceId, environment, parentFolderId, parentFolderPath } = req.query as { /*
workspaceId: string; #swagger.summary = 'Retrieve folders based on specific conditions'
environment: string; #swagger.description = 'Fetches folders from the specified workspace and environment, optionally providing either a parentFolderId or a parentFolderPath to narrow down results'
parentFolderId?: string;
parentFolderPath?: string; #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 }); const folders = await Folder.findOne({ workspace: workspaceId, environment });
if (req.user) await getUserProjectPermissions(req.user._id, workspaceId);
if (!folders) { if (!folders) {
res.send({ folders: [], dir: [] }); res.send({ folders: [], dir: [] });
return; 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 instead of parentFolderId given a path like /folder1/folder2
if (parentFolderPath) { if (parentFolderPath) {
if (req.authData.authPayload instanceof ServiceTokenData) { if (req.authData.authPayload instanceof ServiceTokenData) {
@@ -10,6 +10,8 @@ import {
getSmtpConfigured getSmtpConfigured
} from "../../config"; } from "../../config";
import { validateUserEmail } from "../../validation"; 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 * Signup step 1: Initialize account for user under email [email] and send a verification code
@@ -19,7 +21,9 @@ import { validateUserEmail } from "../../validation";
* @returns * @returns
*/ */
export const beginEmailSignup = async (req: Request, res: Response) => { 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 // validate that email is not disposable
validateUserEmail(email); validateUserEmail(email);
@@ -50,7 +54,9 @@ export const beginEmailSignup = async (req: Request, res: Response) => {
*/ */
export const verifyEmailSignup = async (req: Request, res: Response) => { export const verifyEmailSignup = async (req: Request, res: Response) => {
let user; let user;
const { email, code } = req.body; const {
body: { email, code }
} = await validateRequest(reqValidator.VerifyEmailSignUpV1, req);
// initialize user account // initialize user account
user = await User.findOne({ email }).select("+publicKey"); user = await User.findOne({ email }).select("+publicKey");
@@ -1,5 +1,7 @@
import { Request, Response } from "express"; import { Request, Response } from "express";
import { validateRequest } from "../../helpers/validation";
import { UserAction } from "../../models"; import { UserAction } from "../../models";
import * as reqValidator from "../../validation/action";
/** /**
* Add user action [action] * Add user action [action]
@@ -8,26 +10,27 @@ import { UserAction } from "../../models";
* @returns * @returns
*/ */
export const addUserAction = async (req: Request, res: Response) => { export const addUserAction = async (req: Request, res: Response) => {
// add/record new action [action] for user with id [req.user._id] // add/record new action [action] for user with id [req.user._id]
const {
const { action } = req.body; body: { action }
} = await validateRequest(reqValidator.AddUserActionV1, req);
const userAction = await UserAction.findOneAndUpdate( const userAction = await UserAction.findOneAndUpdate(
{ {
user: req.user._id, user: req.user._id,
action, action
}, },
{ user: req.user._id, action }, { user: req.user._id, action },
{ {
new: true, new: true,
upsert: true, upsert: true
} }
); );
return res.status(200).send({ return res.status(200).send({
message: "Successfully recorded user action", message: "Successfully recorded user action",
userAction, userAction
}); });
}; };
/** /**
@@ -37,15 +40,17 @@ export const addUserAction = async (req: Request, res: Response) => {
* @returns * @returns
*/ */
export const getUserAction = async (req: Request, res: Response) => { export const getUserAction = async (req: Request, res: Response) => {
// get user action [action] for user with id [req.user._id] // get user action [action] for user with id [req.user._id]
const action: string = req.query.action as string; const {
query: { action }
} = await validateRequest(reqValidator.GetUserActionV1, req);
const userAction = await UserAction.findOne({ const userAction = await UserAction.findOne({
user: req.user._id, user: req.user._id,
action, action
}); });
return res.status(200).send({ return res.status(200).send({
userAction, userAction
}); });
}; };
+67 -27
View File
@@ -1,16 +1,32 @@
import { Request, Response } from "express"; import { Request, Response } from "express";
import { Types } from "mongoose"; import { Types } from "mongoose";
import { client, getRootEncryptionKey } from "../../config"; import { client, getRootEncryptionKey } from "../../config";
import { validateMembership } from "../../helpers";
import { Webhook } from "../../models"; import { Webhook } from "../../models";
import { getWebhookPayload, triggerWebhookRequest } from "../../services/WebhookService"; import { getWebhookPayload, triggerWebhookRequest } from "../../services/WebhookService";
import { BadRequestError, ResourceNotFoundError } from "../../utils/errors"; import { BadRequestError, ResourceNotFoundError } from "../../utils/errors";
import { EEAuditLogService } from "../../ee/services"; import { EEAuditLogService } from "../../ee/services";
import { EventType } from "../../ee/models"; 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) => { 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({ const webhook = new Webhook({
workspace: workspaceId, workspace: workspaceId,
environment, environment,
@@ -29,7 +45,7 @@ export const createWebhook = async (req: Request, res: Response) => {
} }
await webhook.save(); await webhook.save();
await EEAuditLogService.createAuditLog( await EEAuditLogService.createAuditLog(
req.authData, 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) => { export const updateWebhook = async (req: Request, res: Response) => {
const { webhookId } = req.params; const {
const { isDisabled } = req.body; body: { isDisabled },
params: { webhookId }
} = await validateRequest(reqValidator.UpdateWebhookV1, req);
const webhook = await Webhook.findById(webhookId); const webhook = await Webhook.findById(webhookId);
if (!webhook) { if (!webhook) {
throw BadRequestError({ message: "Webhook not found!!" }); throw BadRequestError({ message: "Webhook not found!!" });
} }
// check that user is a member of the workspace const { permission } = await getUserProjectPermissions(
await validateMembership({ req.user._id,
userId: req.user._id.toString(), webhook.workspace.toString()
workspaceId: webhook.workspace, );
acceptedRoles: [ADMIN, MEMBER] ForbiddenError.from(permission).throwUnlessCan(
}); ProjectPermissionActions.Edit,
ProjectPermissionSub.Webhooks
);
if (typeof isDisabled !== undefined) { if (typeof isDisabled !== undefined) {
webhook.isDisabled = isDisabled; webhook.isDisabled = isDisabled;
@@ -97,19 +118,24 @@ export const updateWebhook = async (req: Request, res: Response) => {
}; };
export const deleteWebhook = 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); let webhook = await Webhook.findById(webhookId);
if (!webhook) { if (!webhook) {
throw ResourceNotFoundError({ message: "Webhook not found!!" }); throw ResourceNotFoundError({ message: "Webhook not found!!" });
} }
await validateMembership({ const { permission } = await getUserProjectPermissions(
userId: req.user._id.toString(), req.user._id,
workspaceId: webhook.workspace, webhook.workspace.toString()
acceptedRoles: [ADMIN, MEMBER] );
}); ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Delete,
ProjectPermissionSub.Webhooks
);
webhook = await Webhook.findByIdAndDelete(webhookId); webhook = await Webhook.findByIdAndDelete(webhookId);
if (!webhook) { if (!webhook) {
@@ -139,17 +165,23 @@ export const deleteWebhook = async (req: Request, res: Response) => {
}; };
export const testWebhook = 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); const webhook = await Webhook.findById(webhookId);
if (!webhook) { if (!webhook) {
throw BadRequestError({ message: "Webhook not found!!" }); throw BadRequestError({ message: "Webhook not found!!" });
} }
await validateMembership({ const { permission } = await getUserProjectPermissions(
userId: req.user._id.toString(), req.user._id,
workspaceId: webhook.workspace, webhook.workspace.toString()
acceptedRoles: [ADMIN, MEMBER] );
}); ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read,
ProjectPermissionSub.Webhooks
);
try { try {
await triggerWebhookRequest( await triggerWebhookRequest(
@@ -182,7 +214,15 @@ export const testWebhook = async (req: Request, res: Response) => {
}; };
export const listWebhooks = 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<string, string> = {}; const optionalFilters: Record<string, string> = {};
if (environment) optionalFilters.environment = environment as string; if (environment) optionalFilters.environment = environment as string;
+127 -58
View File
@@ -5,17 +5,28 @@ import {
Integration, Integration,
IntegrationAuth, IntegrationAuth,
Membership, Membership,
MembershipOrg, Organization,
ServiceToken, ServiceToken,
Workspace, Workspace
} from "../../models"; } from "../../models";
import { import { createWorkspace as create, deleteWorkspace as deleteWork } from "../../helpers/workspace";
createWorkspace as create,
deleteWorkspace as deleteWork,
} from "../../helpers/workspace";
import { EELicenseService } from "../../ee/services"; import { EELicenseService } from "../../ee/services";
import { addMemberships } from "../../helpers/membership"; import { addMemberships } from "../../helpers/membership";
import { ADMIN } from "../../variables"; 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] * Return public keys of members of workspace with id [workspaceId]
@@ -24,21 +35,29 @@ import { ADMIN } from "../../variables";
* @returns * @returns
*/ */
export const getWorkspacePublicKeys = async (req: Request, res: Response) => { 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 = ( const publicKeys = (
await Membership.find({ await Membership.find({
workspace: workspaceId, workspace: workspaceId
}).populate<{ user: IUser }>("user", "publicKey") }).populate<{ user: IUser }>("user", "publicKey")
).map((member) => { ).map((member) => {
return { return {
publicKey: member.user.publicKey, publicKey: member.user.publicKey,
userId: member.user._id, userId: member.user._id
}; };
}); });
return res.status(200).send({ return res.status(200).send({
publicKeys, publicKeys
}); });
}; };
@@ -49,14 +68,22 @@ export const getWorkspacePublicKeys = async (req: Request, res: Response) => {
* @returns * @returns
*/ */
export const getWorkspaceMemberships = async (req: Request, res: Response) => { 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({ const users = await Membership.find({
workspace: workspaceId, workspace: workspaceId
}).populate("user", "+publicKey"); }).populate("user", "+publicKey");
return res.status(200).send({ 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) => { export const getWorkspaces = async (req: Request, res: Response) => {
const workspaces = ( const workspaces = (
await Membership.find({ await Membership.find({
user: req.user._id, user: req.user._id
}).populate("workspace") }).populate("workspace")
).map((m) => m.workspace); ).map((m) => m.workspace);
return res.status(200).send({ return res.status(200).send({
workspaces, workspaces
}); });
}; };
@@ -85,14 +112,16 @@ export const getWorkspaces = async (req: Request, res: Response) => {
* @returns * @returns
*/ */
export const getWorkspace = async (req: Request, res: Response) => { 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({ const workspace = await Workspace.findOne({
_id: workspaceId, _id: workspaceId
}); });
return res.status(200).send({ return res.status(200).send({
workspace, workspace
}); });
}; };
@@ -104,26 +133,32 @@ export const getWorkspace = async (req: Request, res: Response) => {
* @returns * @returns
*/ */
export const createWorkspace = async (req: Request, res: Response) => { 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 organization = await Organization.findById(organizationId);
const membershipOrg = await MembershipOrg.findOne({ if (!organization) {
user: req.user._id, throw OrganizationNotFoundError({
organization: new Types.ObjectId(organizationId), message: "Failed to find organization"
}); });
if (!membershipOrg) {
throw new Error("Failed to validate organization membership");
} }
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)); const plan = await EELicenseService.getPlan(new Types.ObjectId(organizationId));
if (plan.workspaceLimit !== null) { if (plan.workspaceLimit !== null) {
// case: limit imposed on number of workspaces allowed // case: limit imposed on number of workspaces allowed
if (plan.workspacesUsed >= plan.workspaceLimit) { if (plan.workspacesUsed >= plan.workspaceLimit) {
// case: number of workspaces used exceeds the number of workspaces allowed // case: number of workspaces used exceeds the number of workspaces allowed
return res.status(400).send({ 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 // create workspace and add user as member
const workspace = await create({ const workspace = await create({
name: workspaceName, name: workspaceName,
organizationId: new Types.ObjectId(organizationId), organizationId: new Types.ObjectId(organizationId)
}); });
await addMemberships({ await addMemberships({
userIds: [req.user._id], userIds: [req.user._id],
workspaceId: workspace._id.toString(), workspaceId: workspace._id.toString(),
roles: [ADMIN], roles: [ADMIN]
}); });
return res.status(200).send({ return res.status(200).send({
workspace, workspace
}); });
}; };
@@ -156,15 +191,23 @@ export const createWorkspace = async (req: Request, res: Response) => {
* @returns * @returns
*/ */
export const deleteWorkspace = async (req: Request, res: Response) => { 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 // delete workspace
await deleteWork({ await deleteWork({
id: workspaceId, id: workspaceId
}); });
return res.status(200).send({ 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 * @returns
*/ */
export const changeWorkspaceName = async (req: Request, res: Response) => { export const changeWorkspaceName = async (req: Request, res: Response) => {
const { workspaceId } = req.params; const {
const { name } = req.body; 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( const workspace = await Workspace.findOneAndUpdate(
{ {
_id: workspaceId, _id: workspaceId
}, },
{ {
name, name
}, },
{ {
new: true, new: true
} }
); );
return res.status(200).send({ return res.status(200).send({
message: "Successfully changed workspace name", message: "Successfully changed workspace name",
workspace, workspace
}); });
}; };
@@ -203,14 +254,21 @@ export const changeWorkspaceName = async (req: Request, res: Response) => {
* @returns * @returns
*/ */
export const getWorkspaceIntegrations = async (req: Request, res: Response) => { 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({ const integrations = await Integration.find({
workspace: workspaceId, workspace: workspaceId
}); });
return res.status(200).send({ return res.status(200).send({
integrations, integrations
}); });
}; };
@@ -220,18 +278,23 @@ export const getWorkspaceIntegrations = async (req: Request, res: Response) => {
* @param res * @param res
* @returns * @returns
*/ */
export const getWorkspaceIntegrationAuthorizations = async ( export const getWorkspaceIntegrationAuthorizations = async (req: Request, res: Response) => {
req: Request, const {
res: Response params: { workspaceId }
) => { } = await validateRequest(reqValidator.GetWorkspaceIntegrationAuthorizationsV1, req);
const { workspaceId } = req.params;
const { permission } = await getUserProjectPermissions(req.user._id, workspaceId);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read,
ProjectPermissionSub.Integrations
);
const authorizations = await IntegrationAuth.find({ const authorizations = await IntegrationAuth.find({
workspace: workspaceId, workspace: workspaceId
}); });
return res.status(200).send({ return res.status(200).send({
authorizations, authorizations
}); });
}; };
@@ -241,18 +304,24 @@ export const getWorkspaceIntegrationAuthorizations = async (
* @param res * @param res
* @returns * @returns
*/ */
export const getWorkspaceServiceTokens = async ( export const getWorkspaceServiceTokens = async (req: Request, res: Response) => {
req: Request, const {
res: Response params: { workspaceId }
) => { } = await validateRequest(reqValidator.GetWorkspaceServiceTokensV1, req);
const { workspaceId } = req.params;
const { permission } = await getUserProjectPermissions(req.user._id, workspaceId);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read,
ProjectPermissionSub.ServiceTokens
);
// ?? FIX. // ?? FIX.
const serviceTokens = await ServiceToken.find({ const serviceTokens = await ServiceToken.find({
user: req.user._id, user: req.user._id,
workspace: workspaceId, workspace: workspaceId
}); });
return res.status(200).send({ return res.status(200).send({
serviceTokens, serviceTokens
}); });
}; };
+85 -83
View File
@@ -10,16 +10,11 @@ import { sendMail } from "../../helpers/nodemailer";
import { TokenService } from "../../services"; import { TokenService } from "../../services";
import { EELogService } from "../../ee/services"; import { EELogService } from "../../ee/services";
import { BadRequestError, InternalServerError } from "../../utils/errors"; import { BadRequestError, InternalServerError } from "../../utils/errors";
import { import { ACTION_LOGIN, TOKEN_EMAIL_MFA } from "../../variables";
ACTION_LOGIN,
TOKEN_EMAIL_MFA,
} from "../../variables";
import { getUserAgentType } from "../../utils/posthog"; // TODO: move this import { getUserAgentType } from "../../utils/posthog"; // TODO: move this
import { import { getHttpsEnabled, getJwtMfaLifetime, getJwtMfaSecret } from "../../config";
getHttpsEnabled, import { validateRequest } from "../../helpers/validation";
getJwtMfaLifetime, import * as reqValidator from "../../validation/auth";
getJwtMfaSecret,
} from "../../config";
declare module "jsonwebtoken" { declare module "jsonwebtoken" {
export interface UserIDJwtPayload extends jwt.JwtPayload { export interface UserIDJwtPayload extends jwt.JwtPayload {
@@ -34,13 +29,10 @@ declare module "jsonwebtoken" {
* @returns * @returns
*/ */
export const login1 = async (req: Request, res: Response) => { export const login1 = async (req: Request, res: Response) => {
const { const { email, clientPublicKey }: { email: string; clientPublicKey: string } = req.body;
email,
clientPublicKey,
}: { email: string; clientPublicKey: string } = req.body;
const user = await User.findOne({ const user = await User.findOne({
email, email
}).select("+salt +verifier"); }).select("+salt +verifier");
if (!user) throw new Error("Failed to find user"); if (!user) throw new Error("Failed to find user");
@@ -49,25 +41,28 @@ export const login1 = async (req: Request, res: Response) => {
server.init( server.init(
{ {
salt: user.salt, salt: user.salt,
verifier: user.verifier, verifier: user.verifier
}, },
async () => { async () => {
// generate server-side public key // generate server-side public key
const serverPublicKey = server.getPublicKey(); const serverPublicKey = server.getPublicKey();
await LoginSRPDetail.findOneAndReplace({ email: email }, { await LoginSRPDetail.findOneAndReplace(
email: email, { email: email },
clientPublicKey: clientPublicKey, {
serverBInt: bigintConversion.bigintToBuf(server.bInt), email: email,
}, { upsert: true, returnNewDocument: false }); clientPublicKey: clientPublicKey,
serverBInt: bigintConversion.bigintToBuf(server.bInt)
},
{ upsert: true, returnNewDocument: false }
);
return res.status(200).send({ return res.status(200).send({
serverPublicKey, serverPublicKey,
salt: user.salt, salt: user.salt
}); });
} }
); );
}; };
/** /**
@@ -78,19 +73,22 @@ export const login1 = async (req: Request, res: Response) => {
* @returns * @returns
*/ */
export const login2 = async (req: Request, res: Response) => { 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 { email, clientProof } = req.body;
const user = await User.findOne({ const user = await User.findOne({
email, email
}).select("+salt +verifier +encryptionVersion +protectedKey +protectedKeyIV +protectedKeyTag +publicKey +encryptedPrivateKey +iv +tag +devices"); }).select(
"+salt +verifier +encryptionVersion +protectedKey +protectedKeyIV +protectedKeyTag +publicKey +encryptedPrivateKey +iv +tag +devices"
);
if (!user) throw new Error("Failed to find user"); 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) { 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(); const server = new jsrp.server();
@@ -98,7 +96,7 @@ export const login2 = async (req: Request, res: Response) => {
{ {
salt: user.salt, salt: user.salt,
verifier: user.verifier, verifier: user.verifier,
b: loginSRPDetail.serverBInt, b: loginSRPDetail.serverBInt
}, },
async () => { async () => {
server.setClientPublicKey(loginSRPDetail.clientPublicKey); server.setClientPublicKey(loginSRPDetail.clientPublicKey);
@@ -111,15 +109,15 @@ export const login2 = async (req: Request, res: Response) => {
// generate temporary MFA token // generate temporary MFA token
const token = createToken({ const token = createToken({
payload: { payload: {
userId: user._id.toString(), userId: user._id.toString()
}, },
expiresIn: await getJwtMfaLifetime(), expiresIn: await getJwtMfaLifetime(),
secret: await getJwtMfaSecret(), secret: await getJwtMfaSecret()
}); });
const code = await TokenService.createToken({ const code = await TokenService.createToken({
type: TOKEN_EMAIL_MFA, type: TOKEN_EMAIL_MFA,
email, email
}); });
// send MFA code [code] to [email] // send MFA code [code] to [email]
@@ -128,27 +126,27 @@ export const login2 = async (req: Request, res: Response) => {
subjectLine: "Infisical MFA code", subjectLine: "Infisical MFA code",
recipients: [email], recipients: [email],
substitutions: { substitutions: {
code, code
}, }
}); });
return res.status(200).send({ return res.status(200).send({
mfaEnabled: true, mfaEnabled: true,
token, token
}); });
} }
await checkUserDevice({ await checkUserDevice({
user, user,
ip: req.realIP, ip: req.realIP,
userAgent: req.headers["user-agent"] ?? "", userAgent: req.headers["user-agent"] ?? ""
}); });
// issue tokens // issue tokens
const tokens = await issueAuthTokens({ const tokens = await issueAuthTokens({
userId: user._id, userId: user._id,
ip: req.realIP, ip: req.realIP,
userAgent: req.headers["user-agent"] ?? "", userAgent: req.headers["user-agent"] ?? ""
}); });
// store (refresh) token in httpOnly cookie // store (refresh) token in httpOnly cookie
@@ -156,7 +154,7 @@ export const login2 = async (req: Request, res: Response) => {
httpOnly: true, httpOnly: true,
path: "/", path: "/",
sameSite: "strict", sameSite: "strict",
secure: await getHttpsEnabled(), secure: await getHttpsEnabled()
}); });
// case: user does not have MFA enabled // case: user does not have MFA enabled
@@ -182,36 +180,33 @@ export const login2 = async (req: Request, res: Response) => {
publicKey: user.publicKey, publicKey: user.publicKey,
encryptedPrivateKey: user.encryptedPrivateKey, encryptedPrivateKey: user.encryptedPrivateKey,
iv: user.iv, iv: user.iv,
tag: user.tag, tag: user.tag
} };
if ( if (user?.protectedKey && user?.protectedKeyIV && user?.protectedKeyTag) {
user?.protectedKey &&
user?.protectedKeyIV &&
user?.protectedKeyTag
) {
response.protectedKey = user.protectedKey; response.protectedKey = user.protectedKey;
response.protectedKeyIV = user.protectedKeyIV response.protectedKeyIV = user.protectedKeyIV;
response.protectedKeyTag = user.protectedKeyTag; response.protectedKeyTag = user.protectedKeyTag;
} }
const loginAction = await EELogService.createAction({ const loginAction = await EELogService.createAction({
name: ACTION_LOGIN, name: ACTION_LOGIN,
userId: user._id, userId: user._id
}); });
loginAction && await EELogService.createLog({ loginAction &&
userId: user._id, (await EELogService.createLog({
actions: [loginAction], userId: user._id,
channel: getUserAgentType(req.headers["user-agent"]), actions: [loginAction],
ipAddress: req.ip, channel: getUserAgentType(req.headers["user-agent"]),
}); ipAddress: req.ip
}));
return res.status(200).send(response); return res.status(200).send(response);
} }
return res.status(400).send({ 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] * Send MFA token to email [email]
* @param req * @param req
* @param res * @param res
*/ */
export const sendMfaToken = async (req: Request, res: Response) => { 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({ const code = await TokenService.createToken({
type: TOKEN_EMAIL_MFA, type: TOKEN_EMAIL_MFA,
email, email
}); });
// send MFA code [code] to [email] // send MFA code [code] to [email]
@@ -236,49 +233,53 @@ export const sendMfaToken = async (req: Request, res: Response) => {
subjectLine: "Infisical MFA code", subjectLine: "Infisical MFA code",
recipients: [email], recipients: [email],
substitutions: { substitutions: {
code, code
}, }
}); });
return res.status(200).send({ 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 * Verify MFA token [mfaToken] and issue JWT and refresh tokens if the
* MFA token [mfaToken] is valid * MFA token [mfaToken] is valid
* @param req * @param req
* @param res * @param res
*/ */
export const verifyMfaToken = async (req: Request, res: Response) => { 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({ await TokenService.validateToken({
type: TOKEN_EMAIL_MFA, type: TOKEN_EMAIL_MFA,
email, email,
token: mfaToken, token: mfaToken
}); });
const user = await User.findOne({ const user = await User.findOne({
email, email
}).select("+salt +verifier +encryptionVersion +protectedKey +protectedKeyIV +protectedKeyTag +publicKey +encryptedPrivateKey +iv +tag +devices"); }).select(
"+salt +verifier +encryptionVersion +protectedKey +protectedKeyIV +protectedKeyTag +publicKey +encryptedPrivateKey +iv +tag +devices"
);
if (!user) throw new Error("Failed to find user"); if (!user) throw new Error("Failed to find user");
await LoginSRPDetail.deleteOne({ userId: user.id }) await LoginSRPDetail.deleteOne({ userId: user.id });
await checkUserDevice({ await checkUserDevice({
user, user,
ip: req.realIP, ip: req.realIP,
userAgent: req.headers["user-agent"] ?? "", userAgent: req.headers["user-agent"] ?? ""
}); });
// issue tokens // issue tokens
const tokens = await issueAuthTokens({ const tokens = await issueAuthTokens({
userId: user._id, userId: user._id,
ip: req.realIP, ip: req.realIP,
userAgent: req.headers["user-agent"] ?? "", userAgent: req.headers["user-agent"] ?? ""
}); });
// store (refresh) token in httpOnly cookie // store (refresh) token in httpOnly cookie
@@ -286,7 +287,7 @@ export const verifyMfaToken = async (req: Request, res: Response) => {
httpOnly: true, httpOnly: true,
path: "/", path: "/",
sameSite: "strict", sameSite: "strict",
secure: await getHttpsEnabled(), secure: await getHttpsEnabled()
}); });
interface VerifyMfaTokenRes { interface VerifyMfaTokenRes {
@@ -319,8 +320,8 @@ export const verifyMfaToken = async (req: Request, res: Response) => {
publicKey: user.publicKey as string, publicKey: user.publicKey as string,
encryptedPrivateKey: user.encryptedPrivateKey as string, encryptedPrivateKey: user.encryptedPrivateKey as string,
iv: user.iv as string, iv: user.iv as string,
tag: user.tag as string, tag: user.tag as string
} };
if (user?.protectedKey && user?.protectedKeyIV && user?.protectedKeyTag) { if (user?.protectedKey && user?.protectedKeyIV && user?.protectedKeyTag) {
resObj.protectedKey = user.protectedKey; resObj.protectedKey = user.protectedKey;
@@ -330,15 +331,16 @@ export const verifyMfaToken = async (req: Request, res: Response) => {
const loginAction = await EELogService.createAction({ const loginAction = await EELogService.createAction({
name: ACTION_LOGIN, name: ACTION_LOGIN,
userId: user._id, userId: user._id
}); });
loginAction && await EELogService.createLog({ loginAction &&
userId: user._id, (await EELogService.createLog({
actions: [loginAction], userId: user._id,
channel: getUserAgentType(req.headers["user-agent"]), actions: [loginAction],
ipAddress: req.realIP, channel: getUserAgentType(req.headers["user-agent"]),
}); ipAddress: req.realIP
}));
return res.status(200).send(resObj); return res.status(200).send(resObj);
} };
@@ -6,27 +6,126 @@ import {
Secret, Secret,
ServiceToken, ServiceToken,
ServiceTokenData, ServiceTokenData,
Workspace, Workspace
} from "../../models"; } from "../../models";
import { EventType, SecretVersion } from "../../ee/models"; import { EventType, SecretVersion } from "../../ee/models";
import { EEAuditLogService, EELicenseService } from "../../ee/services"; import { EEAuditLogService, EELicenseService } from "../../ee/services";
import { BadRequestError, WorkspaceNotFoundError } from "../../utils/errors"; import { BadRequestError, WorkspaceNotFoundError } from "../../utils/errors";
import _ from "lodash"; import _ from "lodash";
import { PERMISSION_READ_SECRETS, PERMISSION_WRITE_SECRETS } from "../../variables"; 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 req
* @param res * @param res
* @returns * @returns
*/ */
export const createWorkspaceEnvironment = async ( export const createWorkspaceEnvironment = async (req: Request, res: Response) => {
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(); const workspace = await Workspace.findById(workspaceId).exec();
if (!workspace) throw WorkspaceNotFoundError(); if (!workspace) throw WorkspaceNotFoundError();
@@ -39,7 +138,8 @@ export const createWorkspaceEnvironment = async (
// case: number of environments used exceeds the number of environments allowed // case: number of environments used exceeds the number of environments allowed
return res.status(400).send({ 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({ workspace?.environments.push({
name: environmentName, name: environmentName,
slug: environmentSlug.toLowerCase(), slug: environmentSlug.toLowerCase()
}); });
await workspace.save(); await workspace.save();
@@ -80,8 +180,8 @@ export const createWorkspaceEnvironment = async (
workspace: workspaceId, workspace: workspaceId,
environment: { environment: {
name: environmentName, name: environmentName,
slug: environmentSlug, slug: environmentSlug
}, }
}); });
}; };
@@ -91,34 +191,46 @@ export const createWorkspaceEnvironment = async (
* @param res * @param res
* @returns * @returns
*/ */
export const reorderWorkspaceEnvironments = async ( export const reorderWorkspaceEnvironments = async (req: Request, res: Response) => {
req: Request, const {
res: Response params: { workspaceId },
) => { body: { environmentName, environmentSlug, otherEnvironmentSlug, otherEnvironmentName }
const { workspaceId } = req.params; } = await validateRequest(reqValidator.ReorderWorkspaceEnvironmentsV2, req);
const { environmentSlug, environmentName, otherEnvironmentSlug, otherEnvironmentName } = req.body;
const { permission } = await getUserProjectPermissions(req.user._id, workspaceId);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Edit,
ProjectPermissionSub.Environments
);
// atomic update the env to avoid conflict // atomic update the env to avoid conflict
const workspace = await Workspace.findById(workspaceId).exec(); const workspace = await Workspace.findById(workspaceId).exec();
if (!workspace) { 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 environmentIndex = workspace.environments.findIndex(
const otherEnvironmentIndex = workspace.environments.findIndex((env) => env.name === otherEnvironmentName && env.slug === otherEnvironmentSlug) (env) => env.name === environmentName && env.slug === environmentSlug
);
const otherEnvironmentIndex = workspace.environments.findIndex(
(env) => env.name === otherEnvironmentName && env.slug === otherEnvironmentSlug
);
if (environmentIndex === -1 || otherEnvironmentIndex === -1) { 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 // 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({ return res.status(200).send({
message: "Successfully reordered environments", message: "Successfully reordered environments",
workspace: workspaceId, workspace: workspaceId
}); });
}; };
@@ -129,12 +241,91 @@ export const reorderWorkspaceEnvironments = async (
* @param res * @param res
* @returns * @returns
*/ */
export const renameWorkspaceEnvironment = async ( export const renameWorkspaceEnvironment = async (req: Request, res: Response) => {
req: Request, /*
res: Response #swagger.summary = 'Rename workspace environment'
) => { #swagger.description = 'Rename a specific environment within a workspace'
const { workspaceId } = req.params;
const { environmentName, environmentSlug, oldEnvironmentSlug } = req.body; #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 // user should pass both new slug and env name
if (!environmentSlug || !environmentName) { if (!environmentSlug || !environmentName) {
throw new Error("Invalid environment given."); throw new Error("Invalid environment given.");
@@ -148,16 +339,13 @@ export const renameWorkspaceEnvironment = async (
const isEnvExist = workspace.environments.some( const isEnvExist = workspace.environments.some(
({ name, slug }) => ({ name, slug }) =>
slug !== oldEnvironmentSlug && slug !== oldEnvironmentSlug && (name === environmentName || slug === environmentSlug)
(name === environmentName || slug === environmentSlug)
); );
if (isEnvExist) { if (isEnvExist) {
throw new Error("Invalid environment given"); throw new Error("Invalid environment given");
} }
const envIndex = workspace?.environments.findIndex( const envIndex = workspace?.environments.findIndex(({ slug }) => slug === oldEnvironmentSlug);
({ slug }) => slug === oldEnvironmentSlug
);
if (envIndex === -1) { if (envIndex === -1) {
throw new Error("Invalid environment given"); throw new Error("Invalid environment given");
} }
@@ -191,7 +379,7 @@ export const renameWorkspaceEnvironment = async (
await Membership.updateMany( await Membership.updateMany(
{ {
workspace: workspaceId, workspace: workspaceId,
"deniedPermissions.environmentSlug": oldEnvironmentSlug, "deniedPermissions.environmentSlug": oldEnvironmentSlug
}, },
{ $set: { "deniedPermissions.$[element].environmentSlug": environmentSlug } }, { $set: { "deniedPermissions.$[element].environmentSlug": environmentSlug } },
{ arrayFilters: [{ "element.environmentSlug": oldEnvironmentSlug }] } { arrayFilters: [{ "element.environmentSlug": oldEnvironmentSlug }] }
@@ -218,8 +406,8 @@ export const renameWorkspaceEnvironment = async (
workspace: workspaceId, workspace: workspaceId,
environment: { environment: {
name: environmentName, name: environmentName,
slug: environmentSlug, slug: environmentSlug
}, }
}); });
}; };
@@ -229,21 +417,83 @@ export const renameWorkspaceEnvironment = async (
* @param res * @param res
* @returns * @returns
*/ */
export const deleteWorkspaceEnvironment = async ( export const deleteWorkspaceEnvironment = async (req: Request, res: Response) => {
req: Request, /*
res: Response #swagger.summary = 'Delete workspace environment'
) => { #swagger.description = 'Delete a specific environment from a workspace'
const { workspaceId } = req.params;
const { environmentSlug } = req.body; #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 // atomic update the env to avoid conflict
const workspace = await Workspace.findById(workspaceId).exec(); const workspace = await Workspace.findById(workspaceId).exec();
if (!workspace) { if (!workspace) {
throw new Error("Failed to create workspace environment"); throw new Error("Failed to create workspace environment");
} }
const envIndex = workspace?.environments.findIndex( const envIndex = workspace?.environments.findIndex(({ slug }) => slug === environmentSlug);
({ slug }) => slug === environmentSlug
);
if (envIndex === -1) { if (envIndex === -1) {
throw new Error("Invalid environment given"); throw new Error("Invalid environment given");
} }
@@ -256,11 +506,11 @@ export const deleteWorkspaceEnvironment = async (
// clean up // clean up
await Secret.deleteMany({ await Secret.deleteMany({
workspace: workspaceId, workspace: workspaceId,
environment: environmentSlug, environment: environmentSlug
}); });
await SecretVersion.deleteMany({ await SecretVersion.deleteMany({
workspace: workspaceId, workspace: workspaceId,
environment: environmentSlug, environment: environmentSlug
}); });
// await ServiceToken.deleteMany({ // await ServiceToken.deleteMany({
@@ -279,7 +529,7 @@ export const deleteWorkspaceEnvironment = async (
await Integration.deleteMany({ await Integration.deleteMany({
workspace: workspaceId, workspace: workspaceId,
environment: environmentSlug, environment: environmentSlug
}); });
await Membership.updateMany( await Membership.updateMany(
{ workspace: workspaceId }, { workspace: workspaceId },
@@ -305,46 +555,100 @@ export const deleteWorkspaceEnvironment = async (
return res.status(200).send({ return res.status(200).send({
message: "Successfully deleted environment", message: "Successfully deleted environment",
workspace: workspaceId, 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 ( #swagger.parameters['workspaceId'] = {
req: Request, "description": "ID of the workspace",
res: Response "required": true,
) => { "type": "string",
const { workspaceId } = req.params; "in": "path"
const workspacesUserIsMemberOf = await Membership.findOne({ }
workspace: workspaceId,
user: req.user,
})
if (!workspacesUserIsMemberOf) { #swagger.responses[200] = {
throw BadRequestError() 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 { membership: workspacesUserIsMemberOf } = await getUserProjectPermissions(
const deniedPermission = workspacesUserIsMemberOf.deniedPermissions 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) { if (!relatedWorkspace) {
throw BadRequestError() throw BadRequestError();
} }
relatedWorkspace.environments.forEach(environment => { relatedWorkspace.environments.forEach((environment) => {
const isReadBlocked = _.some(deniedPermission, { environmentSlug: environment.slug, ability: PERMISSION_READ_SECRETS }) const isReadBlocked = _.some(deniedPermission, {
const isWriteBlocked = _.some(deniedPermission, { environmentSlug: environment.slug, ability: PERMISSION_WRITE_SECRETS }) environmentSlug: environment.slug,
ability: PERMISSION_READ_SECRETS
});
const isWriteBlocked = _.some(deniedPermission, {
environmentSlug: environment.slug,
ability: PERMISSION_WRITE_SECRETS
});
if (isReadBlocked && isWriteBlocked) { if (isReadBlocked && isWriteBlocked) {
return return;
} else { } else {
accessibleEnvironments.push({ accessibleEnvironments.push({
name: environment.name, name: environment.name,
slug: environment.slug, slug: environment.slug,
isWriteDenied: isWriteBlocked, isWriteDenied: isWriteBlocked,
isReadDenied: isReadBlocked, isReadDenied: isReadBlocked
}) });
} }
}) });
res.json({ accessibleEnvironments }) res.json({ accessibleEnvironments });
}; };
@@ -1,21 +1,27 @@
import { Request, Response } from "express"; import { Request, Response } from "express";
import { Types } from "mongoose"; import { Types } from "mongoose";
import { import { Membership, MembershipOrg, ServiceAccount, Workspace } from "../../models";
Membership,
MembershipOrg,
ServiceAccount,
Workspace,
} from "../../models";
import { deleteMembershipOrg } from "../../helpers/membershipOrg"; import { deleteMembershipOrg } from "../../helpers/membershipOrg";
import { updateSubscriptionOrgQuantity } from "../../helpers/organization"; 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] * Return memberships for organization with id [organizationId]
* @param req * @param req
* @param res * @param res
*/ */
export const getOrganizationMemberships = async (req: Request, res: Response) => { export const getOrganizationMemberships = async (req: Request, res: Response) => {
/* /*
#swagger.summary = 'Return organization memberships' #swagger.summary = 'Return organization memberships'
#swagger.description = '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({ const { permission } = await getUserOrgPermissions(req.user._id, organizationId);
organization: organizationId, ForbiddenError.from(permission).throwUnlessCan(
}).populate("user", "+publicKey"); OrgPermissionActions.Read,
OrgPermissionSubjects.Member
return res.status(200).send({ );
memberships,
}); 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] * Update role of membership with id [membershipId] to role [role]
* @param req * @param req
* @param res * @param res
*/ */
export const updateOrganizationMembership = async (req: Request, res: Response) => { export const updateOrganizationMembership = async (req: Request, res: Response) => {
/* /*
#swagger.summary = 'Update organization membership' #swagger.summary = 'Update organization membership'
#swagger.description = 'Update organization membership' #swagger.description = 'Update organization membership'
@@ -118,31 +132,58 @@ export const updateOrganizationMembership = async (req: Request, res: Response)
} }
} }
*/ */
const { membershipId } = req.params; const {
const { role } = req.body; params: { organizationId, membershipId },
body: { role }
const membership = await MembershipOrg.findByIdAndUpdate( } = await validateRequest(reqValidator.UpdateOrgMemberv2, req);
membershipId, const { permission } = await getUserOrgPermissions(req.user._id, organizationId);
{ ForbiddenError.from(permission).throwUnlessCan(
role, OrgPermissionActions.Edit,
}, { OrgPermissionSubjects.Member
new: true, );
}
); const isCustomRole = !["admin", "member", "owner"].includes(role);
if (isCustomRole) {
return res.status(200).send({ const orgRole = await Role.findOne({ slug: role, isOrgRole: true });
membership, 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,
{
$set: {
role
},
$unset: {
customRole: 1
}
},
{
new: true
}
);
return res.status(200).send({
membership
});
};
/** /**
* Delete organization membership with id [membershipId] * Delete organization membership with id [membershipId]
* @param req * @param req
* @param res * @param res
* @returns * @returns
*/ */
export const deleteOrganizationMembership = async (req: Request, res: Response) => { export const deleteOrganizationMembership = async (req: Request, res: Response) => {
/* /*
#swagger.summary = 'Delete organization membership' #swagger.summary = 'Delete organization membership'
#swagger.description = 'Delete organization membership' #swagger.description = 'Delete organization membership'
@@ -178,30 +219,37 @@ export const deleteOrganizationMembership = async (req: Request, res: Response)
} }
} }
*/ */
const { membershipId } = req.params; const {
params: { organizationId, membershipId }
// delete organization membership } = await validateRequest(reqValidator.DeleteOrgMemberv2, req);
const membership = await deleteMembershipOrg({ const { permission } = await getUserOrgPermissions(req.user._id, organizationId);
membershipOrgId: membershipId, ForbiddenError.from(permission).throwUnlessCan(
}); OrgPermissionActions.Delete,
OrgPermissionSubjects.Member
);
await updateSubscriptionOrgQuantity({ // delete organization membership
organizationId: membership.organization.toString(), const membership = await deleteMembershipOrg({
}); membershipOrgId: membershipId
});
return res.status(200).send({ await updateSubscriptionOrgQuantity({
membership, organizationId: membership.organization.toString()
}); });
}
return res.status(200).send({
membership
});
};
/** /**
* Return workspaces for organization with id [organizationId] that user has * Return workspaces for organization with id [organizationId] that user has
* access to * access to
* @param req * @param req
* @param res * @param res
*/ */
export const getOrganizationWorkspaces = async (req: Request, res: Response) => { export const getOrganizationWorkspaces = async (req: Request, res: Response) => {
/* /*
#swagger.summary = 'Return projects in organization that user is part of' #swagger.summary = 'Return projects in organization that user is part of'
#swagger.description = 'Return projects in organization that user is part of' #swagger.description = 'Return projects in organization that user is part of'
@@ -234,45 +282,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( const { permission } = await getUserOrgPermissions(req.user._id, organizationId);
( ForbiddenError.from(permission).throwUnlessCan(
await Workspace.find( OrgPermissionActions.Read,
{ OrgPermissionSubjects.Workspace
organization: organizationId, );
},
"_id"
)
).map((w) => w._id.toString())
);
const workspaces = ( const workspacesSet = new Set(
await Membership.find({ (
user: req.user._id, await Workspace.find(
}).populate("workspace") {
) 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())) .filter((m) => workspacesSet.has(m.workspace._id.toString()))
.map((m) => m.workspace); .map((m) => m.workspace);
return res.status(200).send({ return res.status(200).send({
workspaces, workspaces
}); });
} };
/** /**
* Return service accounts for organization with id [organizationId] * Return service accounts for organization with id [organizationId]
* @param req * @param req
* @param res * @param res
*/ */
export const getOrganizationServiceAccounts = async (req: Request, res: Response) => { export const getOrganizationServiceAccounts = async (req: Request, res: Response) => {
const { organizationId } = req.params; const { organizationId } = req.params;
const serviceAccounts = await ServiceAccount.find({ const serviceAccounts = await ServiceAccount.find({
organization: new Types.ObjectId(organizationId), organization: new Types.ObjectId(organizationId)
}); });
return res.status(200).send({ return res.status(200).send({
serviceAccounts, serviceAccounts
}); });
} };
+120 -62
View File
@@ -25,7 +25,6 @@ import {
userHasWriteOnlyAbility userHasWriteOnlyAbility
} from "../../ee/helpers/checkMembershipPermissions"; } from "../../ee/helpers/checkMembershipPermissions";
import _ from "lodash"; import _ from "lodash";
import { BatchSecret, BatchSecretRequest } from "../../types/secret";
import { import {
getFolderByPath, getFolderByPath,
getFolderIdFromServiceToken, getFolderIdFromServiceToken,
@@ -35,6 +34,18 @@ import {
import { isValidScope } from "../../helpers/secrets"; import { isValidScope } from "../../helpers/secrets";
import path from "path"; import path from "path";
import { getAllImportedSecrets } from "../../services/SecretImportService"; 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 * 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 channel = getUserAgentType(req.headers["user-agent"]);
const postHogClient = await TelemetryService.getPostHogClient(); const postHogClient = await TelemetryService.getPostHogClient();
const validatedData = await validateRequest(BatchSecretsV2, req);
const { const {
workspaceId, body: { workspaceId, environment, requests }
environment, } = validatedData;
requests let {
}: { body: { secretPath, folderId }
workspaceId: string; } = validatedData;
environment: string;
requests: BatchSecretRequest[];
} = req.body;
let secretPath = req.body.secretPath as string; const secretIds = requests
let folderId = req.body.folderId as string; .filter(({ method }) => method !== "POST")
// akhilmhdh: ts is dumb
.map((el) => new Types.ObjectId((el.secret as any)._id));
const createSecrets: BatchSecret[] = []; const oldSecrets = await Secret.find({
const updateSecrets: BatchSecret[] = []; _id: {
const deleteSecrets: { _id: Types.ObjectId, secretName: string; }[] = []; $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[] = []; const actions: IAction[] = [];
// get secret blind index salt // get secret blind index salt
@@ -69,31 +89,33 @@ export const batchSecrets = async (req: Request, res: Response) => {
workspaceId: new Types.ObjectId(workspaceId) workspaceId: new Types.ObjectId(workspaceId)
}); });
const folders = await Folder.findOne({ workspace: workspaceId, environment }); if (secretPath !== "/") {
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) {
folderId = await getFolderIdFromServiceToken(workspaceId, environment, 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); const folder = searchByFolderIdWithDir(folders.nodes, folderId as string);
if (!folder?.folder) throw BadRequestError({ message: "Folder not found" }); if (!folder?.folder) throw BadRequestError({ message: "Folder not found" });
secretPath = path.join( secretPath = path.join(
"/", "/",
...folder.dir.map(({ name }) => name).filter((name) => name !== "root") ...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) { for await (const request of requests) {
// do a validation // do a validation
@@ -110,7 +132,7 @@ export const batchSecrets = async (req: Request, res: Response) => {
version: 1, version: 1,
user: request.secret.type === SECRET_PERSONAL ? req.user : undefined, user: request.secret.type === SECRET_PERSONAL ? req.user : undefined,
environment, environment,
workspace: new Types.ObjectId(workspaceId), workspace: workspaceId,
folder: folderId, folder: folderId,
secretBlindIndex, secretBlindIndex,
algorithm: ALGORITHM_AES_256_GCM, algorithm: ALGORITHM_AES_256_GCM,
@@ -125,7 +147,7 @@ export const batchSecrets = async (req: Request, res: Response) => {
updateSecrets.push({ updateSecrets.push({
...request.secret, ...request.secret,
_id: new Types.ObjectId(request.secret._id), _id: request.secret._id,
secretBlindIndex, secretBlindIndex,
folder: folderId, folder: folderId,
algorithm: ALGORITHM_AES_256_GCM, algorithm: ALGORITHM_AES_256_GCM,
@@ -133,15 +155,39 @@ export const batchSecrets = async (req: Request, res: Response) => {
}); });
break; break;
case "DELETE": 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; 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 // handle create secrets
let createdSecrets: ISecret[] = []; let createdSecrets: ISecret[] = [];
if (createSecrets.length > 0) { if (createSecrets.length > 0) {
createdSecrets = await Secret.insertMany(createSecrets); createdSecrets = (await Secret.insertMany(createSecrets)) as any;
// (EE) add secret versions for new secrets // (EE) add secret versions for new secrets
await EESecretService.addSecretVersions({ await EESecretService.addSecretVersions({
secretVersions: createdSecrets.map((n: any) => { secretVersions: createdSecrets.map((n: any) => {
@@ -206,7 +252,7 @@ export const batchSecrets = async (req: Request, res: Response) => {
// handle update secrets // handle update secrets
let updatedSecrets: ISecret[] = []; let updatedSecrets: ISecret[] = [];
if (updateSecrets.length > 0 && req.secrets) { if (updateSecrets.length > 0 && oldSecrets) {
// construct object containing all secrets // construct object containing all secrets
let listedSecretsObj: { let listedSecretsObj: {
[key: string]: { [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: any, secret: ISecret) => ({
...obj, ...obj,
[secret._id.toString()]: secret [secret._id.toString()]: secret
@@ -227,7 +273,8 @@ export const batchSecrets = async (req: Request, res: Response) => {
updateOne: { updateOne: {
filter: { filter: {
_id: new Types.ObjectId(u._id), _id: new Types.ObjectId(u._id),
workspace: new Types.ObjectId(workspaceId) workspace: new Types.ObjectId(workspaceId),
environment
}, },
update: { update: {
$inc: { $inc: {
@@ -241,7 +288,6 @@ export const batchSecrets = async (req: Request, res: Response) => {
} }
} }
})); }));
await Secret.bulkWrite(updateOperations); await Secret.bulkWrite(updateOperations);
const secretVersions = updateSecrets.map( const secretVersions = updateSecrets.map(
@@ -332,23 +378,26 @@ export const batchSecrets = async (req: Request, res: Response) => {
if (deleteSecrets.length > 0) { if (deleteSecrets.length > 0) {
const deleteSecretIds: Types.ObjectId[] = deleteSecrets.map((s) => s._id); const deleteSecretIds: Types.ObjectId[] = deleteSecrets.map((s) => s._id);
const deletedSecretsObj = (await Secret.find({ const deletedSecretsObj = (
_id: { await Secret.find({
$in: deleteSecretIds _id: {
} $in: deleteSecretIds
})) }
.reduce( })
(obj: any, secret: ISecret) => ({ ).reduce(
...obj, (obj: any, secret: ISecret) => ({
[secret._id.toString()]: secret ...obj,
}), [secret._id.toString()]: secret
{} }),
); {}
);
await Secret.deleteMany({ await Secret.deleteMany({
_id: { _id: {
$in: deleteSecretIds $in: deleteSecretIds
} },
workspace: new Types.ObjectId(workspaceId),
environment
}); });
await EESecretService.markDeletedSecretVersions({ await EESecretService.markDeletedSecretVersions({
@@ -781,10 +830,13 @@ export const getSecrets = async (req: Request, res: Response) => {
} }
*/ */
const { tagSlugs, secretPath, include_imports } = req.query; const validatedData = await validateRequest(GetSecretsV2, req);
let { folderId } = req.query; const {
const workspaceId = req.query.workspaceId as string; query: { tagSlugs, secretPath, include_imports, workspaceId, environment }
const environment = req.query.environment as string; } = validatedData;
let {
query: { folderId }
} = validatedData;
const folders = await Folder.findOne({ workspace: workspaceId, environment }); const folders = await Folder.findOne({ workspace: workspaceId, environment });
@@ -926,8 +978,14 @@ export const getSecrets = async (req: Request, res: Response) => {
// TODO(akhilmhdh) - secret-imp change this to org type // TODO(akhilmhdh) - secret-imp change this to org type
let importedSecrets: any[] = []; let importedSecrets: any[] = [];
if (include_imports === "true") { if (include_imports) {
importedSecrets = await getAllImportedSecrets(workspaceId, environment, folderId as string); // depreciated
importedSecrets = await getAllImportedSecrets(
workspaceId,
environment,
folderId as string,
() => false
);
} }
const channel = getUserAgentType(req.headers["user-agent"]); const channel = getUserAgentType(req.headers["user-agent"]);
@@ -970,17 +1028,17 @@ export const getSecrets = async (req: Request, res: Response) => {
const postHogClient = await TelemetryService.getPostHogClient(); const postHogClient = await TelemetryService.getPostHogClient();
// reduce the number of events captured // reduce the number of events captured
let shouldRecordK8Event = false let shouldRecordK8Event = false;
if (req.authData.userAgent == K8_USER_AGENT_NAME) { if (req.authData.userAgent == K8_USER_AGENT_NAME) {
const randomNumber = Math.random(); const randomNumber = Math.random();
if (randomNumber > 0.9) { if (randomNumber > 0.9) {
shouldRecordK8Event = true shouldRecordK8Event = true;
} }
} }
if (postHogClient) { if (postHogClient) {
const shouldCapture = req.authData.userAgent !== K8_USER_AGENT_NAME || shouldRecordK8Event; const shouldCapture = req.authData.userAgent !== K8_USER_AGENT_NAME || shouldRecordK8Event;
const approximateForNoneCapturedEvents = secrets.length * 10 const approximateForNoneCapturedEvents = secrets.length * 10;
if (shouldCapture) { if (shouldCapture) {
postHogClient.capture({ postHogClient.capture({
@@ -1104,10 +1162,10 @@ export const updateSecrets = async (req: Request, res: Response) => {
tags, tags,
...(secretCommentCiphertext !== undefined && secretCommentIV && secretCommentTag ...(secretCommentCiphertext !== undefined && secretCommentIV && secretCommentTag
? { ? {
secretCommentCiphertext, secretCommentCiphertext,
secretCommentIV, secretCommentIV,
secretCommentTag secretCommentTag
} }
: {}) : {})
} }
} }
@@ -6,6 +6,15 @@ import { getSaltRounds } from "../../config";
import { BadRequestError } from "../../utils/errors"; import { BadRequestError } from "../../utils/errors";
import { ActorType, EventType } from "../../ee/models"; import { ActorType, EventType } from "../../ee/models";
import { EEAuditLogService } from "../../ee/services"; 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 * 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) => { export const createServiceTokenData = async (req: Request, res: Response) => {
let serviceTokenData; 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 secret = crypto.randomBytes(16).toString("hex");
const secretHash = await bcrypt.hash(secret, await getSaltRounds()); const secretHash = await bcrypt.hash(secret, await getSaltRounds());
@@ -75,7 +91,7 @@ export const createServiceTokenData = async (req: Request, res: Response) => {
} }
let user; let user;
if (req.authData.actor.type === ActorType.USER) { if (req.authData.actor.type === ActorType.USER) {
user = req.authData.authPayload._id; 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"); if (!serviceTokenData) throw new Error("Failed to find service token data");
const serviceToken = `st.${serviceTokenData._id.toString()}.${secret}`; const serviceToken = `st.${serviceTokenData._id.toString()}.${secret}`;
await EEAuditLogService.createAuditLog( await EEAuditLogService.createAuditLog(
req.authData, 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 * @returns
*/ */
export const deleteServiceTokenData = async (req: Request, res: Response) => { 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( await EEAuditLogService.createAuditLog(
req.authData, req.authData,
{ {
+51 -28
View File
@@ -1,42 +1,58 @@
import { ForbiddenError } from "@casl/ability";
import { Request, Response } from "express"; import { Request, Response } from "express";
import { Types } from "mongoose"; import { Types } from "mongoose";
import { Membership, Secret, Tag } from "../../models"; import { Secret, Tag } from "../../models";
import { BadRequestError, UnauthorizedRequestError } from "../../utils/errors"; 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) => { export const createWorkspaceTag = async (req: Request, res: Response) => {
const { workspaceId } = req.params; const {
const { name, slug, tagColor } = req.body; 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 = { const tagToCreate = {
name, name,
tagColor, workspace: new Types.ObjectId(workspaceId),
workspace: new Types.ObjectId(workspaceId), slug,
slug, user: new Types.ObjectId(req.user._id)
user: new Types.ObjectId(req.user._id), };
};
const createdTag = await new Tag(tagToCreate).save(); const createdTag = await new Tag(tagToCreate).save();
res.json(createdTag); res.json(createdTag);
}; };
export const deleteWorkspaceTag = async (req: Request, res: Response) => { 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); const tagFromDB = await Tag.findById(tagId);
if (!tagFromDB) { if (!tagFromDB) {
throw BadRequestError(); throw BadRequestError();
} }
// can only delete if the request user is one that belongs to the same workspace as the tag const { permission } = await getUserProjectPermissions(
const membership = await Membership.findOne({ req.user._id,
user: req.user, tagFromDB.workspace.toString()
workspace: tagFromDB.workspace );
}); ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Delete,
if (!membership) { ProjectPermissionSub.Tags
UnauthorizedRequestError({ message: "Failed to validate membership" }); );
}
const result = await Tag.findByIdAndDelete(tagId); 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) => { export const getWorkspaceTags = async (req: Request, res: Response) => {
const { workspaceId } = req.params; const {
params: { workspaceId }
const workspaceTags = await Tag.find({ } = await validateRequest(reqValidator.GetWorkspaceTagsV2, req);
workspace: new Types.ObjectId(workspaceId) 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({ return res.json({
workspaceTags workspaceTags
}); });
+171 -175
View File
@@ -2,23 +2,19 @@ import { Request, Response } from "express";
import { Types } from "mongoose"; import { Types } from "mongoose";
import crypto from "crypto"; import crypto from "crypto";
import bcrypt from "bcrypt"; import bcrypt from "bcrypt";
import { import { APIKeyData, AuthMethod, MembershipOrg, TokenVersion, User } from "../../models";
APIKeyData,
AuthMethod,
MembershipOrg,
TokenVersion,
User
} from "../../models";
import { getSaltRounds } from "../../config"; import { getSaltRounds } from "../../config";
import { validateRequest } from "../../helpers/validation";
import * as reqValidator from "../../validation";
/** /**
* Return the current user. * Return the current user.
* @param req * @param req
* @param res * @param res
* @returns * @returns
*/ */
export const getMe = async (req: Request, res: Response) => { export const getMe = async (req: Request, res: Response) => {
/* /*
#swagger.summary = "Retrieve the current user on the request" #swagger.summary = "Retrieve the current user on the request"
#swagger.description = "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 const user = await User.findById(req.user._id).select(
.findById(req.user._id) "+salt +publicKey +encryptedPrivateKey +iv +tag +encryptionVersion +protectedKey +protectedKeyIV +protectedKeyTag"
.select("+salt +publicKey +encryptedPrivateKey +iv +tag +encryptionVersion +protectedKey +protectedKeyIV +protectedKeyTag"); );
return res.status(200).send({ return res.status(200).send({
user, user
}); });
} };
/** /**
* Update the current user's MFA-enabled status [isMfaEnabled]. * Update the current user's MFA-enabled status [isMfaEnabled].
* Note: Infisical currently only supports email-based 2FA only; this will expand to * Note: Infisical currently only supports email-based 2FA only; this will expand to
* include SMS and authenticator app modes of authentication in the future. * include SMS and authenticator app modes of authentication in the future.
* @param req * @param req
* @param res * @param res
* @returns * @returns
*/ */
export const updateMyMfaEnabled = async (req: Request, res: Response) => { export const updateMyMfaEnabled = async (req: Request, res: Response) => {
const { isMfaEnabled }: { isMfaEnabled: boolean } = req.body; const {
req.user.isMfaEnabled = isMfaEnabled; body: { isMfaEnabled }
} = await validateRequest(reqValidator.UpdateMyMfaEnabledV2, req);
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(); req.user.isMfaEnabled = isMfaEnabled;
const user = req.user; if (isMfaEnabled) {
// TODO: adapt this route/controller
return res.status(200).send({ // to work for different forms of MFA
user, 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]. * Update name of the current user to [firstName, lastName].
* @param req * @param req
* @param res * @param res
* @returns * @returns
*/ */
export const updateName = async (req: Request, res: Response) => { export const updateName = async (req: Request, res: Response) => {
const { const {
firstName, body: { lastName, firstName }
lastName } = await validateRequest(reqValidator.UpdateNameV2, req);
}: {
firstName: string; const user = await User.findByIdAndUpdate(
lastName: string; req.user._id.toString(),
} = req.body; {
firstName,
const user = await User.findByIdAndUpdate( lastName: lastName ?? ""
req.user._id.toString(), },
{ {
firstName, new: true
lastName: lastName ?? "" }
}, );
{
new: true return res.status(200).send({
} user
); });
};
return res.status(200).send({
user,
});
}
/** /**
* Update auth method of the current user to [authMethods] * Update auth method of the current user to [authMethods]
* @param req * @param req
* @param res * @param res
* @returns * @returns
*/ */
export const updateAuthMethods = async (req: Request, res: Response) => { export const updateAuthMethods = async (req: Request, res: Response) => {
const { const {
authMethods body: { authMethods }
} = req.body; } = await validateRequest(reqValidator.UpdateAuthMethodsV2, req);
const hasSamlEnabled = req.user.authMethods
.some(
(authMethod: AuthMethod) => [
AuthMethod.OKTA_SAML,
AuthMethod.AZURE_SAML,
AuthMethod.JUMPCLOUD_SAML
].includes(authMethod)
);
if (hasSamlEnabled) { const hasSamlEnabled = req.user.authMethods.some((authMethod: AuthMethod) =>
return res.status(400).send({ [AuthMethod.OKTA_SAML, AuthMethod.AZURE_SAML, AuthMethod.JUMPCLOUD_SAML].includes(authMethod)
message: "Failed to update user authentication method because SAML SSO is enforced" );
});
}
const user = await User.findByIdAndUpdate( if (hasSamlEnabled) {
req.user._id.toString(), return res.status(400).send({
{ message: "Failed to update user authentication method because SAML SSO is enforced"
authMethods
},
{
new: true
}
);
return res.status(200).send({
user
}); });
} }
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. * Return organizations that the current user is part of.
* @param req * @param req
* @param res * @param res
*/ */
export const getMyOrganizations = async (req: Request, res: Response) => { export const getMyOrganizations = async (req: Request, res: Response) => {
/* /*
#swagger.summary = 'Return organizations that current user is part of' #swagger.summary = 'Return organizations that current user is part of'
#swagger.description = '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 = ( const organizations = (
await MembershipOrg.find({ await MembershipOrg.find({
user: req.user._id, user: req.user._id
}).populate("organization") }).populate("organization")
).map((m) => m.organization); ).map((m) => m.organization);
return res.status(200).send({ return res.status(200).send({
organizations, organizations
}); });
} };
/** /**
* Return API keys belonging to current user. * Return API keys belonging to current user.
* @param req * @param req
* @param res * @param res
* @returns * @returns
*/ */
export const getMyAPIKeys = async (req: Request, res: Response) => { export const getMyAPIKeys = async (req: Request, res: Response) => {
const apiKeyData = await APIKeyData.find({ const apiKeyData = await APIKeyData.find({
user: req.user._id, user: req.user._id
}); });
return res.status(200).send(apiKeyData); return res.status(200).send(apiKeyData);
} };
/** /**
* Create new API key for current user. * Create new API key for current user.
* @param req * @param req
* @param res * @param res
* @returns * @returns
*/ */
export const createAPIKey = async (req: Request, res: Response) => { 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 secret = crypto.randomBytes(16).toString("hex");
const secretHash = await bcrypt.hash(secret, await getSaltRounds()); const secretHash = await bcrypt.hash(secret, await getSaltRounds());
const expiresAt = new Date(); const expiresAt = new Date();
expiresAt.setSeconds(expiresAt.getSeconds() + expiresIn); expiresAt.setSeconds(expiresAt.getSeconds() + expiresIn);
let apiKeyData = await new APIKeyData({ let apiKeyData = await new APIKeyData({
name, name,
lastUsed: new Date(), lastUsed: new Date(),
expiresAt, expiresAt,
user: req.user._id, user: req.user._id,
secretHash, secretHash
}).save(); }).save();
// return api key data without sensitive data // return api key data without sensitive data
apiKeyData = (await APIKeyData.findById(apiKeyData._id)) as any; 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({ return res.status(200).send({
apiKey, apiKey,
apiKeyData, apiKeyData
}); });
} };
/** /**
* Delete API key with id [apiKeyDataId] belonging to current user * Delete API key with id [apiKeyDataId] belonging to current user
* @param req * @param req
* @param res * @param res
*/ */
export const deleteAPIKey = async (req: Request, res: Response) => { 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({ const apiKeyData = await APIKeyData.findOneAndDelete({
_id: new Types.ObjectId(apiKeyDataId), _id: new Types.ObjectId(apiKeyDataId),
user: req.user._id user: req.user._id
}); });
return res.status(200).send({ return res.status(200).send({
apiKeyData apiKeyData
}); });
} };
/** /**
* Return active sessions (TokenVersion) belonging to user * Return active sessions (TokenVersion) belonging to user
* @param req * @param req
* @param res * @param res
* @returns * @returns
*/ */
export const getMySessions = async (req: Request, res: Response) => { export const getMySessions = async (req: Request, res: Response) => {
const tokenVersions = await TokenVersion.find({ const tokenVersions = await TokenVersion.find({
user: req.user._id user: req.user._id
}); });
return res.status(200).send(tokenVersions); return res.status(200).send(tokenVersions);
} };
/** /**
* Revoke all active sessions belong to user * Revoke all active sessions belong to user
* @param req * @param req
* @param res * @param res
* @returns * @returns
*/ */
export const deleteMySessions = async (req: Request, res: Response) => { export const deleteMySessions = async (req: Request, res: Response) => {
await TokenVersion.updateMany({ await TokenVersion.updateMany(
user: req.user._id, {
}, { user: req.user._id
$inc: { },
refreshVersion: 1, {
accessVersion: 1, $inc: {
}, refreshVersion: 1,
}); accessVersion: 1
}
}
);
return res.status(200).send({ return res.status(200).send({
message: "Successfully revoked all sessions" message: "Successfully revoked all sessions"
}); });
} };
@@ -11,6 +11,14 @@ import { EventService, TelemetryService } from "../../services";
import { eventPushSecrets } from "../../events"; import { eventPushSecrets } from "../../events";
import { EEAuditLogService } from "../../ee/services"; import { EEAuditLogService } from "../../ee/services";
import { EventType } from "../../ee/models"; 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 { interface V2PushSecret {
type: string; // personal or shared 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({ const key = await Key.findOne({
workspace: workspaceId, workspace: workspaceId,
receiver: req.user._id receiver: req.user._id
}).populate("sender", "+publicKey"); }).populate("sender", "+publicKey");
if (!key) throw new Error("Failed to find workspace key"); if (!key) throw new Error("Failed to find workspace key");
await EEAuditLogService.createAuditLog( await EEAuditLogService.createAuditLog(
req.authData, 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({ const memberships = await Membership.find({
workspace: workspaceId workspace: workspaceId
@@ -329,8 +347,16 @@ export const updateWorkspaceMembership = async (req: Request, res: Response) =>
} }
} }
*/ */
const { membershipId } = req.params; const {
const { role } = req.body; 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( const membership = await Membership.findByIdAndUpdate(
membershipId, 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); const membership = await Membership.findByIdAndDelete(membershipId);
@@ -413,8 +447,16 @@ export const deleteWorkspaceMembership = async (req: Request, res: Response) =>
* @returns * @returns
*/ */
export const toggleAutoCapitalization = async (req: Request, res: Response) => { export const toggleAutoCapitalization = async (req: Request, res: Response) => {
const { workspaceId } = req.params; const {
const { autoCapitalization } = req.body; 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( const workspace = await Workspace.findOneAndUpdate(
{ {
+183 -189
View File
@@ -10,25 +10,20 @@ import { sendMail } from "../../helpers/nodemailer";
import { TokenService } from "../../services"; import { TokenService } from "../../services";
import { EELogService } from "../../ee/services"; import { EELogService } from "../../ee/services";
import { BadRequestError, InternalServerError } from "../../utils/errors"; import { BadRequestError, InternalServerError } from "../../utils/errors";
import { import { ACTION_LOGIN, TOKEN_EMAIL_MFA } from "../../variables";
ACTION_LOGIN,
TOKEN_EMAIL_MFA,
} from "../../variables";
import { getUserAgentType } from "../../utils/posthog"; // TODO: move this import { getUserAgentType } from "../../utils/posthog"; // TODO: move this
import { import { getHttpsEnabled, getJwtMfaLifetime, getJwtMfaSecret } from "../../config";
getHttpsEnabled,
getJwtMfaLifetime,
getJwtMfaSecret,
} from "../../config";
import { AuthMethod } from "../../models/user"; import { AuthMethod } from "../../models/user";
import { validateRequest } from "../../helpers/validation";
import * as reqValidator from "../../validation/auth";
declare module "jsonwebtoken" { declare module "jsonwebtoken" {
export interface ProviderAuthJwtPayload extends jwt.JwtPayload { export interface ProviderAuthJwtPayload extends jwt.JwtPayload {
userId: string; userId: string;
email: string; email: string;
authProvider: AuthMethod; authProvider: AuthMethod;
isUserCompleted: boolean, isUserCompleted: boolean;
} }
} }
/** /**
@@ -38,53 +33,51 @@ declare module "jsonwebtoken" {
* @returns * @returns
*/ */
export const login1 = async (req: Request, res: Response) => { export const login1 = async (req: Request, res: Response) => {
const { const {
email, body: { email, clientPublicKey, providerAuthToken }
providerAuthToken, } = await validateRequest(reqValidator.Login1V3, req);
clientPublicKey,
}: {
email: string;
clientPublicKey: string,
providerAuthToken?: string;
} = req.body;
const user = await User.findOne({
email,
}).select("+salt +verifier");
if (!user) throw new Error("Failed to find user"); const user = await User.findOne({
email
if (!user.authMethods.includes(AuthMethod.EMAIL)) { }).select("+salt +verifier");
await validateProviderAuthToken({
email,
providerAuthToken,
});
}
const server = new jsrp.server(); if (!user) throw new Error("Failed to find user");
server.init(
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, email: email
verifier: user.verifier,
}, },
async () => { {
// generate server-side public key email,
const serverPublicKey = server.getPublicKey(); userId: user.id,
await LoginSRPDetail.findOneAndReplace({ clientPublicKey: clientPublicKey,
email: email, serverBInt: bigintConversion.bigintToBuf(server.bInt)
}, { },
email, { upsert: true, returnNewDocument: false }
userId: user.id, );
clientPublicKey: clientPublicKey,
serverBInt: bigintConversion.bigintToBuf(server.bInt),
}, { upsert: true, returnNewDocument: false });
return res.status(200).send({ return res.status(200).send({
serverPublicKey, serverPublicKey,
salt: user.salt, salt: user.salt
}); });
} }
); );
}; };
/** /**
@@ -95,150 +88,151 @@ export const login1 = async (req: Request, res: Response) => {
* @returns * @returns
*/ */
export const login2 = async (req: Request, res: Response) => { 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({ const user = await User.findOne({
email, email
}).select("+salt +verifier +encryptionVersion +protectedKey +protectedKeyIV +protectedKeyTag +publicKey +encryptedPrivateKey +iv +tag +devices"); }).select(
"+salt +verifier +encryptionVersion +protectedKey +protectedKeyIV +protectedKeyTag +publicKey +encryptedPrivateKey +iv +tag +devices"
);
if (!user) throw new Error("Failed to find user"); if (!user) throw new Error("Failed to find user");
if (!user.authMethods.includes(AuthMethod.EMAIL)) {
await validateProviderAuthToken({
email,
providerAuthToken,
})
}
const loginSRPDetail = await LoginSRPDetail.findOneAndDelete({ email: email }) if (!user.authMethods.includes(AuthMethod.EMAIL)) {
await validateProviderAuthToken({
email,
providerAuthToken
});
}
if (!loginSRPDetail) { const loginSRPDetail = await LoginSRPDetail.findOneAndDelete({ email: email });
return BadRequestError(Error("Failed to find login details for SRP"))
}
const server = new jsrp.server(); if (!loginSRPDetail) {
server.init( return BadRequestError(Error("Failed to find login details for SRP"));
{ }
salt: user.salt,
verifier: user.verifier,
b: loginSRPDetail.serverBInt,
},
async () => {
server.setClientPublicKey(loginSRPDetail.clientPublicKey);
// compare server and client shared keys const server = new jsrp.server();
if (server.checkClientProof(clientProof)) { server.init(
{
salt: user.salt,
verifier: user.verifier,
b: loginSRPDetail.serverBInt
},
async () => {
server.setClientPublicKey(loginSRPDetail.clientPublicKey);
if (user.isMfaEnabled) { // compare server and client shared keys
// case: user has MFA enabled if (server.checkClientProof(clientProof)) {
if (user.isMfaEnabled) {
// case: user has MFA enabled
// generate temporary MFA token // generate temporary MFA token
const token = createToken({ const token = createToken({
payload: { payload: {
userId: user._id.toString(), userId: user._id.toString()
}, },
expiresIn: await getJwtMfaLifetime(), expiresIn: await getJwtMfaLifetime(),
secret: await getJwtMfaSecret(), secret: await getJwtMfaSecret()
}); });
const code = await TokenService.createToken({ const code = await TokenService.createToken({
type: TOKEN_EMAIL_MFA, type: TOKEN_EMAIL_MFA,
email, email
}); });
// send MFA code [code] to [email] // send MFA code [code] to [email]
await sendMail({ await sendMail({
template: "emailMfa.handlebars", template: "emailMfa.handlebars",
subjectLine: "Infisical MFA code", subjectLine: "Infisical MFA code",
recipients: [user.email], recipients: [user.email],
substitutions: { substitutions: {
code, 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);
} }
});
return res.status(400).send({ return res.status(200).send({
message: "Failed to authenticate. Try again?", 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?"
});
}
);
}; };
+305 -77
View File
@@ -3,14 +3,22 @@ import { Types } from "mongoose";
import { EventService, SecretService } from "../../services"; import { EventService, SecretService } from "../../services";
import { eventPushSecrets } from "../../events"; import { eventPushSecrets } from "../../events";
import { BotService } from "../../services"; import { BotService } from "../../services";
import { containsGlobPatterns, repackageSecretToRaw } from "../../helpers/secrets"; import { containsGlobPatterns, isValidScope, repackageSecretToRaw } from "../../helpers/secrets";
import { encryptSymmetric128BitHexKeyUTF8 } from "../../utils/crypto"; import { encryptSymmetric128BitHexKeyUTF8 } from "../../utils/crypto";
import { getAllImportedSecrets } from "../../services/SecretImportService"; import { getAllImportedSecrets } from "../../services/SecretImportService";
import { Folder, IServiceTokenData } from "../../models"; import { Folder, IServiceTokenData } from "../../models";
import { getFolderByPath } from "../../services/FolderService"; import { getFolderByPath, getFolderWithPathFromId } from "../../services/FolderService";
import { BadRequestError } from "../../utils/errors"; import { BadRequestError } from "../../utils/errors";
import { requireWorkspaceAuth } from "../../middleware"; import { validateRequest } from "../../helpers/validation";
import { ADMIN, MEMBER, PERMISSION_READ_SECRETS } from "../../variables"; 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 * Return secrets for workspace with id [workspaceId] and environment
@@ -19,30 +27,63 @@ import { ADMIN, MEMBER, PERMISSION_READ_SECRETS } from "../../variables";
* @param res * @param res
*/ */
export const getSecretsRaw = async (req: Request, res: Response) => { export const getSecretsRaw = async (req: Request, res: Response) => {
let workspaceId = req.query.workspaceId as string; const validatedData = await validateRequest(reqValidator.GetSecretsRawV3, req);
let environment = req.query.environment as string; let {
let secretPath = req.query.secretPath as string; query: { secretPath, environment, workspaceId }
const folderId = req.query.folderId as string | undefined; } = validatedData;
const includeImports = req.query.include_imports as string; const {
query: { folderId, include_imports: includeImports }
} = validatedData;
// if the service token has single scope, it will get all secrets for that scope by default // if the service token has single scope, it will get all secrets for that scope by default
const serviceTokenDetails: IServiceTokenData = req?.serviceTokenData; 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]; const scope = serviceTokenDetails.scopes[0];
secretPath = scope.secretPath; secretPath = scope.secretPath;
environment = scope.environment; environment = scope.environment;
workspaceId = serviceTokenDetails.workspace.toString(); 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 (!environment || !workspaceId)
throw BadRequestError({ message: "Missing environment or workspace id" });
let permissionCheckFn: (env: string, secPath: string) => boolean; // used to pass as callback function to import secret
if (req.user?._id) {
const { permission } = await getUserProjectPermissions(req.user._id, workspaceId);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read,
subject(ProjectPermissionSub.Secrets, { environment, secretPath })
);
permissionCheckFn = (env: string, secPath: string) =>
permission.can(
ProjectPermissionActions.Read,
subject(ProjectPermissionSub.Secrets, {
environment: env,
secretPath: secPath
})
);
} else {
await validateServiceTokenDataClientForWorkspace({
serviceTokenData: req.authData.authPayload as IServiceTokenData,
workspaceId: new Types.ObjectId(workspaceId),
environment,
secretPath,
requiredPermissions: [PERMISSION_READ_SECRETS]
});
permissionCheckFn = (env: string, secPath: string) =>
isValidScope(req.authData.authPayload as IServiceTokenData, env, secPath);
}
const secrets = await SecretService.getSecrets({ const secrets = await SecretService.getSecrets({
workspaceId: new Types.ObjectId(workspaceId), workspaceId: new Types.ObjectId(workspaceId),
@@ -56,7 +97,7 @@ export const getSecretsRaw = async (req: Request, res: Response) => {
workspaceId: new Types.ObjectId(workspaceId) workspaceId: new Types.ObjectId(workspaceId)
}); });
if (includeImports === "true") { if (includeImports) {
const folders = await Folder.findOne({ workspace: workspaceId, environment }); const folders = await Folder.findOne({ workspace: workspaceId, environment });
let folderId = "root"; let folderId = "root";
// if folder exist get it and replace folderid with new one // if folder exist get it and replace folderid with new one
@@ -67,7 +108,12 @@ export const getSecretsRaw = async (req: Request, res: Response) => {
} }
folderId = folder.id; folderId = folder.id;
} }
const importedSecrets = await getAllImportedSecrets(workspaceId, environment, folderId); const importedSecrets = await getAllImportedSecrets(
workspaceId,
environment,
folderId,
permissionCheckFn
);
return res.status(200).send({ return res.status(200).send({
secrets: secrets.map((secret) => secrets: secrets.map((secret) =>
repackageSecretToRaw({ repackageSecretToRaw({
@@ -99,11 +145,26 @@ export const getSecretsRaw = async (req: Request, res: Response) => {
* @param res * @param res
*/ */
export const getSecretByNameRaw = async (req: Request, res: Response) => { export const getSecretByNameRaw = async (req: Request, res: Response) => {
const { secretName } = req.params; const {
const workspaceId = req.query.workspaceId as string; query: { secretPath, environment, workspaceId, type, include_imports },
const environment = req.query.environment as string; params: { secretName }
const secretPath = req.query.secretPath as string; } = await validateRequest(reqValidator.GetSecretByNameRawV3, req);
const type = req.query.type as "shared" | "personal" | undefined;
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({ const secret = await SecretService.getSecret({
secretName, secretName,
@@ -111,7 +172,8 @@ export const getSecretByNameRaw = async (req: Request, res: Response) => {
environment, environment,
type, type,
secretPath, secretPath,
authData: req.authData authData: req.authData,
include_imports
}); });
const key = await BotService.getWorkspaceKeyWithBot({ const key = await BotService.getWorkspaceKeyWithBot({
@@ -132,8 +194,26 @@ export const getSecretByNameRaw = async (req: Request, res: Response) => {
* @param res * @param res
*/ */
export const createSecretRaw = async (req: Request, res: Response) => { export const createSecretRaw = async (req: Request, res: Response) => {
const { secretName } = req.params; const {
const { workspaceId, environment, type, secretValue, secretComment, secretPath = "/" } = req.body; 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({ const key = await BotService.getWorkspaceKeyWithBot({
workspaceId: new Types.ObjectId(workspaceId) workspaceId: new Types.ObjectId(workspaceId)
@@ -197,8 +277,26 @@ export const createSecretRaw = async (req: Request, res: Response) => {
* @param res * @param res
*/ */
export const updateSecretByNameRaw = async (req: Request, res: Response) => { export const updateSecretByNameRaw = async (req: Request, res: Response) => {
const { secretName } = req.params; const {
const { workspaceId, environment, type, secretValue, secretPath = "/" } = req.body; 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({ const key = await BotService.getWorkspaceKeyWithBot({
workspaceId: new Types.ObjectId(workspaceId) workspaceId: new Types.ObjectId(workspaceId)
@@ -211,7 +309,7 @@ export const updateSecretByNameRaw = async (req: Request, res: Response) => {
const secret = await SecretService.updateSecret({ const secret = await SecretService.updateSecret({
secretName, secretName,
workspaceId, workspaceId: new Types.ObjectId(workspaceId),
environment, environment,
type, type,
authData: req.authData, authData: req.authData,
@@ -243,12 +341,30 @@ export const updateSecretByNameRaw = async (req: Request, res: Response) => {
* @param res * @param res
*/ */
export const deleteSecretByNameRaw = async (req: Request, res: Response) => { export const deleteSecretByNameRaw = async (req: Request, res: Response) => {
const { secretName } = req.params; const {
const { workspaceId, environment, type, secretPath = "/" } = req.body; 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({ const { secret } = await SecretService.deleteSecret({
secretName, secretName,
workspaceId, workspaceId: new Types.ObjectId(workspaceId),
environment, environment,
type, type,
authData: req.authData, authData: req.authData,
@@ -282,11 +398,48 @@ export const deleteSecretByNameRaw = async (req: Request, res: Response) => {
* @param res * @param res
*/ */
export const getSecrets = async (req: Request, res: Response) => { export const getSecrets = async (req: Request, res: Response) => {
const workspaceId = req.query.workspaceId as string; const validatedData = await validateRequest(reqValidator.GetSecretsV3, req);
const environment = req.query.environment as string; const {
const secretPath = req.query.secretPath as string; query: { environment, workspaceId, include_imports: includeImports, folderId }
const folderId = req.query.folderId as string | undefined; } = validatedData;
const includeImports = req.query.include_imports as string;
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;
}
let permissionCheckFn: (env: string, secPath: string) => boolean; // used to pass as callback function to import secret
if (req.user?._id) {
const { permission } = await getUserProjectPermissions(req.user._id, workspaceId);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read,
subject(ProjectPermissionSub.Secrets, { environment, secretPath })
);
permissionCheckFn = (env: string, secPath: string) =>
permission.can(
ProjectPermissionActions.Read,
subject(ProjectPermissionSub.Secrets, {
environment: env,
secretPath: secPath
})
);
} else {
await validateServiceTokenDataClientForWorkspace({
serviceTokenData: req.authData.authPayload as IServiceTokenData,
workspaceId: new Types.ObjectId(workspaceId),
environment,
secretPath,
requiredPermissions: [PERMISSION_READ_SECRETS]
});
permissionCheckFn = (env: string, secPath: string) =>
isValidScope(req.authData.authPayload as IServiceTokenData, env, secPath);
}
const secrets = await SecretService.getSecrets({ const secrets = await SecretService.getSecrets({
workspaceId: new Types.ObjectId(workspaceId), workspaceId: new Types.ObjectId(workspaceId),
@@ -296,7 +449,7 @@ export const getSecrets = async (req: Request, res: Response) => {
authData: req.authData authData: req.authData
}); });
if (includeImports === "true") { if (includeImports) {
const folders = await Folder.findOne({ workspace: workspaceId, environment }); const folders = await Folder.findOne({ workspace: workspaceId, environment });
let folderId = "root"; let folderId = "root";
// if folder exist get it and replace folderid with new one // if folder exist get it and replace folderid with new one
@@ -307,7 +460,12 @@ export const getSecrets = async (req: Request, res: Response) => {
} }
folderId = folder.id; folderId = folder.id;
} }
const importedSecrets = await getAllImportedSecrets(workspaceId, environment, folderId); const importedSecrets = await getAllImportedSecrets(
workspaceId,
environment,
folderId,
permissionCheckFn
);
return res.status(200).send({ return res.status(200).send({
secrets, secrets,
imports: importedSecrets imports: importedSecrets
@@ -325,11 +483,26 @@ export const getSecrets = async (req: Request, res: Response) => {
* @param res * @param res
*/ */
export const getSecretByName = async (req: Request, res: Response) => { export const getSecretByName = async (req: Request, res: Response) => {
const { secretName } = req.params; const {
const workspaceId = req.query.workspaceId as string; query: { secretPath, environment, workspaceId, type, include_imports },
const environment = req.query.environment as string; params: { secretName }
const secretPath = req.query.secretPath as string; } = await validateRequest(reqValidator.GetSecretByNameV3, req);
const type = req.query.type as "shared" | "personal" | undefined;
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({ const secret = await SecretService.getSecret({
secretName, secretName,
@@ -337,7 +510,8 @@ export const getSecretByName = async (req: Request, res: Response) => {
environment, environment,
type, type,
secretPath, secretPath,
authData: req.authData authData: req.authData,
include_imports
}); });
return res.status(200).send({ return res.status(200).send({
@@ -351,23 +525,41 @@ export const getSecretByName = async (req: Request, res: Response) => {
* @param res * @param res
*/ */
export const createSecret = async (req: Request, res: Response) => { export const createSecret = async (req: Request, res: Response) => {
const { secretName } = req.params;
const { const {
workspaceId, body: {
environment, workspaceId,
type, secretPath,
secretKeyCiphertext, environment,
secretKeyIV, metadata,
secretKeyTag, type,
secretValueCiphertext, secretKeyIV,
secretValueIV, secretKeyTag,
secretValueTag, secretValueIV,
secretCommentCiphertext, secretValueTag,
secretCommentIV, secretCommentIV,
secretCommentTag, secretCommentTag,
secretPath = "/", secretKeyCiphertext,
metadata secretValueCiphertext,
} = req.body; 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({ const secret = await SecretService.createSecret({
secretName, secretName,
@@ -410,20 +602,38 @@ export const createSecret = async (req: Request, res: Response) => {
* @param res * @param res
*/ */
export const updateSecretByName = async (req: Request, res: Response) => { export const updateSecretByName = async (req: Request, res: Response) => {
const { secretName } = req.params;
const { const {
workspaceId, body: {
environment, secretValueCiphertext,
type, secretValueTag,
secretValueCiphertext, secretValueIV,
secretValueIV, type,
secretValueTag, environment,
secretPath = "/" secretPath,
} = req.body; 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({ const secret = await SecretService.updateSecret({
secretName, secretName,
workspaceId, workspaceId: new Types.ObjectId(workspaceId),
environment, environment,
type, type,
authData: req.authData, authData: req.authData,
@@ -452,12 +662,30 @@ export const updateSecretByName = async (req: Request, res: Response) => {
* @param res * @param res
*/ */
export const deleteSecretByName = async (req: Request, res: Response) => { export const deleteSecretByName = async (req: Request, res: Response) => {
const { secretName } = req.params; const {
const { workspaceId, environment, type, secretPath = "/" } = req.body; 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({ const { secret } = await SecretService.deleteSecret({
secretName, secretName,
workspaceId, workspaceId: new Types.ObjectId(workspaceId),
environment, environment,
type, type,
authData: req.authData, authData: req.authData,
+157 -161
View File
@@ -3,9 +3,7 @@ import { Request, Response } from "express";
import * as Sentry from "@sentry/node"; import * as Sentry from "@sentry/node";
import { MembershipOrg, User } from "../../models"; import { MembershipOrg, User } from "../../models";
import { completeAccount } from "../../helpers/user"; import { completeAccount } from "../../helpers/user";
import { import { initializeDefaultOrg } from "../../helpers/signup";
initializeDefaultOrg,
} from "../../helpers/signup";
import { issueAuthTokens, validateProviderAuthToken } from "../../helpers/auth"; import { issueAuthTokens, validateProviderAuthToken } from "../../helpers/auth";
import { ACCEPTED, INVITED } from "../../variables"; import { ACCEPTED, INVITED } from "../../variables";
import { standardRequest } from "../../config/request"; import { standardRequest } from "../../config/request";
@@ -13,6 +11,8 @@ import { getHttpsEnabled, getJwtSignupSecret, getLoopsApiKey } from "../../confi
import { BadRequestError } from "../../utils/errors"; import { BadRequestError } from "../../utils/errors";
import { TelemetryService } from "../../services"; import { TelemetryService } from "../../services";
import { AuthMethod } from "../../models"; 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 * Complete setting up user by adding their personal and auth information as part of the
@@ -22,177 +22,173 @@ import { AuthMethod } from "../../models";
* @returns * @returns
*/ */
export const completeAccountSignup = async (req: Request, res: Response) => { export const completeAccountSignup = async (req: Request, res: Response) => {
let user, token, refreshToken; let user, token;
try { try {
const { const {
email, body: {
firstName, email,
lastName, publicKey,
protectedKey, salt,
protectedKeyIV, lastName,
protectedKeyTag, verifier,
publicKey, firstName,
encryptedPrivateKey, protectedKey,
encryptedPrivateKeyIV, protectedKeyIV,
encryptedPrivateKeyTag, protectedKeyTag,
salt, organizationName,
verifier, providerAuthToken,
organizationName, attributionSource,
providerAuthToken, encryptedPrivateKey,
attributionSource, encryptedPrivateKeyIV,
}: { encryptedPrivateKeyTag
email: string; }
firstName: string; } = await validateRequest(reqValidator.CompletedAccountSignupV3, req);
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;
user = await User.findOne({ email }); user = await User.findOne({ email });
if (!user || (user && user?.publicKey)) { if (!user || (user && user?.publicKey)) {
// case 1: user doesn't exist. // case 1: user doesn't exist.
// case 2: user has already completed account // case 2: user has already completed account
return res.status(403).send({ return res.status(403).send({
error: "Failed to complete account for complete user", error: "Failed to complete account for complete user"
}); });
} }
if (providerAuthToken) { if (providerAuthToken) {
await validateProviderAuthToken({ await validateProviderAuthToken({
email, email,
providerAuthToken providerAuthToken
}); });
} else { } else {
const [AUTH_TOKEN_TYPE, AUTH_TOKEN_VALUE] = <[string, string]>req.headers["authorization"]?.split(" ", 2) ?? [null, null] const [AUTH_TOKEN_TYPE, AUTH_TOKEN_VALUE] = <[string, string]>(
if (AUTH_TOKEN_TYPE === null) { req.headers["authorization"]?.split(" ", 2)
throw BadRequestError({ message: "Missing Authorization Header in the request header." }); ) ?? [null, null];
} if (AUTH_TOKEN_TYPE === null) {
if (AUTH_TOKEN_TYPE.toLowerCase() !== "bearer") { throw BadRequestError({ message: "Missing Authorization Header in the request header." });
throw BadRequestError({ message: `The provided authentication type '${AUTH_TOKEN_TYPE}' is not supported.` }) }
} if (AUTH_TOKEN_TYPE.toLowerCase() !== "bearer") {
if (AUTH_TOKEN_VALUE === null) { throw BadRequestError({
throw BadRequestError({ message: `The provided authentication type '${AUTH_TOKEN_TYPE}' is not supported.`
message: "Missing Authorization Body in the request header", });
}) }
} if (AUTH_TOKEN_VALUE === null) {
throw BadRequestError({
message: "Missing Authorization Body in the request header"
});
}
const decodedToken = <jwt.UserIDJwtPayload>( const decodedToken = <jwt.UserIDJwtPayload>(
jwt.verify(AUTH_TOKEN_VALUE, await getJwtSignupSecret()) jwt.verify(AUTH_TOKEN_VALUE, await getJwtSignupSecret())
); );
if (decodedToken.userId !== user.id) { if (decodedToken.userId !== user.id) {
throw BadRequestError(); throw BadRequestError();
} }
} }
// complete setting up user's account // complete setting up user's account
user = await completeAccount({ user = await completeAccount({
userId: user._id.toString(), userId: user._id.toString(),
firstName, firstName,
lastName, lastName,
encryptionVersion: 2, encryptionVersion: 2,
protectedKey, protectedKey,
protectedKeyIV, protectedKeyIV,
protectedKeyTag, protectedKeyTag,
publicKey, publicKey,
encryptedPrivateKey, encryptedPrivateKey,
encryptedPrivateKeyIV, encryptedPrivateKeyIV,
encryptedPrivateKeyTag, encryptedPrivateKeyTag,
salt, salt,
verifier, verifier
}); });
if (!user) if (!user) throw new Error("Failed to complete account for non-existent user"); // ensure user is non-null
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)); 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,
});
}
// update organization membership statuses that are if (!hasSamlEnabled) {
// invited to completed with user attached // TODO: modify this part
await MembershipOrg.updateMany( // initialize default organization and workspace
{ await initializeDefaultOrg({
inviteEmail: email, organizationName,
status: INVITED, user
}, });
{ }
user,
status: ACCEPTED,
}
);
// issue tokens // update organization membership statuses that are
const tokens = await issueAuthTokens({ // invited to completed with user attached
userId: user._id, await MembershipOrg.updateMany(
ip: req.realIP, {
userAgent: req.headers["user-agent"] ?? "", 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 token = tokens.token;
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()),
},
});
}
// store (refresh) token in httpOnly cookie // sending a welcome email to new users
res.cookie("jid", tokens.refreshToken, { if (await getLoopsApiKey()) {
httpOnly: true, await standardRequest.post(
path: "/", "https://app.loops.so/api/v1/events/send",
sameSite: "strict", {
secure: await getHttpsEnabled(), email: email,
}); eventName: "Sign Up",
firstName: firstName,
lastName: lastName
},
{
headers: {
Accept: "application/json",
Authorization: "Bearer " + (await getLoopsApiKey())
}
}
);
}
const postHogClient = await TelemetryService.getPostHogClient(); // store (refresh) token in httpOnly cookie
if (postHogClient) { res.cookie("jid", tokens.refreshToken, {
postHogClient.capture({ httpOnly: true,
event: "User Signed Up", path: "/",
distinctId: email, sameSite: "strict",
properties: { secure: await getHttpsEnabled()
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({ const postHogClient = await TelemetryService.getPostHogClient();
message: "Successfully set up account", if (postHogClient) {
user, postHogClient.capture({
token, 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
});
}; };
@@ -1,90 +1,103 @@
import { Request, Response } from "express"; import { Request, Response } from "express";
import { Types } from "mongoose"; import { Types } from "mongoose";
import { validateRequest } from "../../helpers/validation";
import { Secret } from "../../models"; 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] * Return whether or not all secrets in workspace with id [workspaceId]
* are blind-indexed * are blind-indexed
* @param req * @param req
* @param res * @param res
* @returns * @returns
*/ */
export const getWorkspaceBlindIndexStatus = async (req: Request, res: Response) => { 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({ const { membership } = await getUserProjectPermissions(req.user._id, workspaceId);
workspace: new Types.ObjectId(workspaceId), if (membership.role !== "admin")
secretBlindIndex: { throw UnauthorizedRequestError({ message: "User must be an admin" });
$exists: false,
},
});
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] * Get all secrets for workspace with id [workspaceId]
*/ */
export const getWorkspaceSecrets = async (req: Request, res: Response) => { 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({ const { membership } = await getUserProjectPermissions(req.user._id, workspaceId);
workspace: new Types.ObjectId (workspaceId), if (membership.role !== "admin")
}); throw UnauthorizedRequestError({ message: "User must be an admin" });
return res.status(200).send({ const secrets = await Secret.find({
secrets, workspace: new Types.ObjectId(workspaceId)
}); });
}
return res.status(200).send({
secrets
});
};
/** /**
* Update blind indices for secrets in workspace with id [workspaceId] * Update blind indices for secrets in workspace with id [workspaceId]
* @param req * @param req
* @param res * @param res
*/ */
export const nameWorkspaceSecrets = async (req: Request, res: Response) => { export const nameWorkspaceSecrets = async (req: Request, res: Response) => {
interface SecretToUpdate { const {
secretName: string; params: { workspaceId },
_id: string; body: { secretsToUpdate }
} } = await validateRequest(reqValidator.NameWorkspaceSecretsV3, req);
const { workspaceId } = req.params; const { membership } = await getUserProjectPermissions(req.user._id, workspaceId);
const { if (membership.role !== "admin")
secretsToUpdate, throw UnauthorizedRequestError({ message: "User must be an admin" });
}: {
secretsToUpdate: SecretToUpdate[];
} = req.body;
// get secret blind index salt // get secret blind index salt
const salt = await SecretService.getSecretBlindIndexSalt({ const salt = await SecretService.getSecretBlindIndexSalt({
workspaceId: new Types.ObjectId(workspaceId), workspaceId: new Types.ObjectId(workspaceId)
}); });
// update secret blind indices // update secret blind indices
const operations = await Promise.all( const operations = await Promise.all(
secretsToUpdate.map(async (secretToUpdate: SecretToUpdate) => { secretsToUpdate.map(async (secretToUpdate) => {
const secretBlindIndex = await SecretService.generateSecretBlindIndexWithSalt({ const secretBlindIndex = await SecretService.generateSecretBlindIndexWithSalt({
secretName: secretToUpdate.secretName, secretName: secretToUpdate.secretName,
salt, salt
}); });
return ({
updateOne: {
filter: {
_id: new Types.ObjectId(secretToUpdate._id),
},
update: {
secretBlindIndex,
},
},
});
})
);
await Secret.bulkWrite(operations); return {
updateOne: {
filter: {
_id: new Types.ObjectId(secretToUpdate._id)
},
update: {
secretBlindIndex
}
}
};
})
);
return res.status(200).send({ await Secret.bulkWrite(operations);
message: "Successfully named workspace secrets",
}); return res.status(200).send({
} message: "Successfully named workspace secrets"
});
};
@@ -1,30 +1,32 @@
import { Request, Response } from "express"; import { Request, Response } from "express";
import { Action } from "../../models"; import { Action } from "../../models";
import { ActionNotFoundError } from "../../../utils/errors"; import { ActionNotFoundError } from "../../../utils/errors";
import { validateRequest } from "../../../helpers/validation";
import * as reqValidator from "../../../validation/action";
export const getAction = async (req: Request, res: Response) => { export const getAction = async (req: Request, res: Response) => {
let action; let action;
try { try {
const { actionId } = req.params; const {
params: { actionId }
action = await Action } = await validateRequest(reqValidator.GetActionV1, req);
.findById(actionId)
.populate([
"payload.secretVersions.oldSecretVersion",
"payload.secretVersions.newSecretVersion",
]);
if (!action) throw ActionNotFoundError({
message: "Failed to find action",
});
} catch (err) { action = await Action.findById(actionId).populate([
throw ActionNotFoundError({ "payload.secretVersions.oldSecretVersion",
message: "Failed to find action", "payload.secretVersions.newSecretVersion"
}); ]);
}
if (!action)
return res.status(200).send({ throw ActionNotFoundError({
action, message: "Failed to find action"
});
} catch (err) {
throw ActionNotFoundError({
message: "Failed to find action"
}); });
} }
return res.status(200).send({
action
});
};
@@ -2,27 +2,31 @@ import { Request, Response } from "express";
import { EELicenseService } from "../../services"; import { EELicenseService } from "../../services";
import { getLicenseServerUrl } from "../../../config"; import { getLicenseServerUrl } from "../../../config";
import { licenseServerKeyRequest } from "../../../config/request"; import { licenseServerKeyRequest } from "../../../config/request";
import { validateRequest } from "../../../helpers/validation";
import * as reqValidator from "../../../validation/cloudProducts";
/** /**
* Return available cloud product information. * Return available cloud product information.
* Note: Nicely formatted to easily construct a table from * Note: Nicely formatted to easily construct a table from
* @param req * @param req
* @param res * @param res
* @returns * @returns
*/ */
export const getCloudProducts = async (req: Request, res: Response) => { 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") { if (EELicenseService.instanceType === "cloud") {
const { data } = await licenseServerKeyRequest.get( const { data } = await licenseServerKeyRequest.get(
`${await getLicenseServerUrl()}/api/license-server/v1/cloud-products?billing-cycle=${billingCycle}` `${await getLicenseServerUrl()}/api/license-server/v1/cloud-products?billing-cycle=${billingCycle}`
); );
return res.status(200).send(data); return res.status(200).send(data);
} }
return res.status(200).send({ return res.status(200).send({
head: [], head: [],
rows: [], rows: []
}); });
} };
+12 -10
View File
@@ -7,15 +7,17 @@ import * as workspaceController from "./workspaceController";
import * as actionController from "./actionController"; import * as actionController from "./actionController";
import * as membershipController from "./membershipController"; import * as membershipController from "./membershipController";
import * as cloudProductsController from "./cloudProductsController"; import * as cloudProductsController from "./cloudProductsController";
import * as roleController from "./roleController";
export { export {
secretController, secretController,
secretSnapshotController, secretSnapshotController,
organizationsController, organizationsController,
ssoController, ssoController,
usersController, usersController,
workspaceController, workspaceController,
actionController, actionController,
membershipController, membershipController,
cloudProductsController, cloudProductsController,
} roleController
};
@@ -3,228 +3,503 @@ import { Request, Response } from "express";
import { getLicenseServerUrl } from "../../../config"; import { getLicenseServerUrl } from "../../../config";
import { licenseServerKeyRequest } from "../../../config/request"; import { licenseServerKeyRequest } from "../../../config/request";
import { EELicenseService } from "../../services"; 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) => { export const getOrganizationPlansTable = async (req: Request, res: Response) => {
const billingCycle = req.query.billingCycle as string; const {
query: { billingCycle },
const { data } = await licenseServerKeyRequest.get( params: { organizationId }
`${await getLicenseServerUrl()}/api/license-server/v1/cloud-products?billing-cycle=${billingCycle}` } = 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 * Return the organization current plan's feature set
*/ */
export const getOrganizationPlan = async (req: Request, res: Response) => { export const getOrganizationPlan = async (req: Request, res: Response) => {
const { organizationId } = req.params; const {
const workspaceId = req.query.workspaceId as string; 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({ const plan = await EELicenseService.getPlan(
plan, new Types.ObjectId(organizationId),
}); new Types.ObjectId(workspaceId)
} );
return res.status(200).send({
plan
});
};
/** /**
* Return checkout url for pro trial * Return checkout url for pro trial
* @param req * @param req
* @param res * @param res
* @returns * @returns
*/ */
export const startOrganizationTrial = async (req: Request, res: Response) => { export const startOrganizationTrial = async (req: Request, res: Response) => {
const { organizationId } = req.params; const {
const { success_url } = req.body; params: { organizationId },
body: { success_url }
} = await validateRequest(reqValidator.StartOrgTrailv1, req);
const { data: { url } } = await licenseServerKeyRequest.post( const { permission } = await getUserOrgPermissions(req.user._id, organizationId);
`${await getLicenseServerUrl()}/api/license-server/v1/customers/${req.organization.customerId}/session/trial`, ForbiddenError.from(permission).throwUnlessCan(
{ OrgPermissionActions.Create,
success_url OrgPermissionSubjects.Billing
} );
); ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionActions.Edit,
EELicenseService.delPlan(new Types.ObjectId(organizationId)); OrgPermissionSubjects.Billing
);
return res.status(200).send({
url 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 * Return the organization's current plan's billing info
* @param req * @param req
* @param res * @param res
* @returns * @returns
*/ */
export const getOrganizationPlanBillingInfo = async (req: Request, res: Response) => { export const getOrganizationPlanBillingInfo = async (req: Request, res: Response) => {
const { data } = await licenseServerKeyRequest.get( const {
`${await getLicenseServerUrl()}/api/license-server/v1/customers/${req.organization.customerId}/cloud-plan/billing` params: { organizationId }
); } = await validateRequest(reqValidator.GetOrgPlanBillingInfov1, 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/billing`
);
return res.status(200).send(data);
};
/** /**
* Return the organization's current plan's feature table * Return the organization's current plan's feature table
* @param req * @param req
* @param res * @param res
* @returns * @returns
*/ */
export const getOrganizationPlanTable = async (req: Request, res: Response) => { export const getOrganizationPlanTable = async (req: Request, res: Response) => {
const { data } = await licenseServerKeyRequest.get( const {
`${await getLicenseServerUrl()}/api/license-server/v1/customers/${req.organization.customerId}/cloud-plan/table` 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) => { export const getOrganizationBillingDetails = async (req: Request, res: Response) => {
const { data } = await licenseServerKeyRequest.get( const {
`${await getLicenseServerUrl()}/api/license-server/v1/customers/${req.organization.customerId}/billing-details` 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) => { export const updateOrganizationBillingDetails = async (req: Request, res: Response) => {
const { const {
name, params: { organizationId },
email body: { name, email }
} = req.body; } = await validateRequest(reqValidator.UpdateOrgBillingDetailsv1, req);
const { data } = await licenseServerKeyRequest.patch( const { permission } = await getUserOrgPermissions(req.user._id, organizationId);
`${await getLicenseServerUrl()}/api/license-server/v1/customers/${req.organization.customerId}/billing-details`, ForbiddenError.from(permission).throwUnlessCan(
{ OrgPermissionActions.Edit,
...(name ? { name } : {}), OrgPermissionSubjects.Billing
...(email ? { email } : {}) );
}
);
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 * Return the organization's payment methods on file
*/ */
export const getOrganizationPmtMethods = async (req: Request, res: Response) => { export const getOrganizationPmtMethods = async (req: Request, res: Response) => {
const { data: { pmtMethods } } = await licenseServerKeyRequest.get( const {
`${await getLicenseServerUrl()}/api/license-server/v1/customers/${req.organization.customerId}/billing-details/payment-methods` 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 * Return URL to add payment method for organization
*/ */
export const addOrganizationPmtMethod = async (req: Request, res: Response) => { export const addOrganizationPmtMethod = async (req: Request, res: Response) => {
const { const {
success_url, params: { organizationId },
cancel_url, body: { success_url, cancel_url }
} = req.body; } = await validateRequest(reqValidator.CreateOrgPmtMethodv1, req);
const { data: { url } } = await licenseServerKeyRequest.post( const { permission } = await getUserOrgPermissions(req.user._id, organizationId);
`${await getLicenseServerUrl()}/api/license-server/v1/customers/${req.organization.customerId}/billing-details/payment-methods`, ForbiddenError.from(permission).throwUnlessCan(
{ OrgPermissionActions.Create,
success_url, OrgPermissionSubjects.Billing
cancel_url, );
}
); const organization = await Organization.findById(organizationId);
if (!organization) {
return res.status(200).send({ throw OrganizationNotFoundError({
url, 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 * Delete payment method with id [pmtMethodId] for organization
* @param req * @param req
* @param res * @param res
* @returns * @returns
*/ */
export const deleteOrganizationPmtMethod = async (req: Request, res: Response) => { 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( const { permission } = await getUserOrgPermissions(req.user._id, organizationId);
`${await getLicenseServerUrl()}/api/license-server/v1/customers/${req.organization.customerId}/billing-details/payment-methods/${pmtMethodId}`, ForbiddenError.from(permission).throwUnlessCan(
); OrgPermissionActions.Delete,
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.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 * Return the organization's tax ids on file
*/ */
export const getOrganizationTaxIds = async (req: Request, res: Response) => { export const getOrganizationTaxIds = async (req: Request, res: Response) => {
const { data: { tax_ids } } = await licenseServerKeyRequest.get( const {
`${await getLicenseServerUrl()}/api/license-server/v1/customers/${req.organization.customerId}/billing-details/tax-ids` 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 * Add tax id to organization
*/ */
export const addOrganizationTaxId = async (req: Request, res: Response) => { export const addOrganizationTaxId = async (req: Request, res: Response) => {
const { const {
type, params: { organizationId },
value body: { type, value }
} = req.body; } = await validateRequest(reqValidator.CreateOrgTaxId, req);
const { data } = await licenseServerKeyRequest.post( const { permission } = await getUserOrgPermissions(req.user._id, organizationId);
`${await getLicenseServerUrl()}/api/license-server/v1/customers/${req.organization.customerId}/billing-details/tax-ids`, ForbiddenError.from(permission).throwUnlessCan(
{ OrgPermissionActions.Create,
type, OrgPermissionSubjects.Billing
value );
}
);
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 * Delete tax id with id [taxId] from organization tax ids on file
* @param req * @param req
* @param res * @param res
* @returns * @returns
*/ */
export const deleteOrganizationTaxId = async (req: Request, res: Response) => { 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( const { permission } = await getUserOrgPermissions(req.user._id, organizationId);
`${await getLicenseServerUrl()}/api/license-server/v1/customers/${req.organization.customerId}/billing-details/tax-ids/${taxId}`, ForbiddenError.from(permission).throwUnlessCan(
); OrgPermissionActions.Delete,
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.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 * Return organization's invoices on file
* @param req * @param req
* @param res * @param res
* @returns * @returns
*/ */
export const getOrganizationInvoices = async (req: Request, res: Response) => { export const getOrganizationInvoices = async (req: Request, res: Response) => {
const { data: { invoices } } = await licenseServerKeyRequest.get( const {
`${await getLicenseServerUrl()}/api/license-server/v1/customers/${req.organization.customerId}/invoices` 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 * Return organization's licenses on file
* @param req * @param req
* @param res * @param res
* @returns * @returns
*/ */
export const getOrganizationLicenses = async (req: Request, res: Response) => { export const getOrganizationLicenses = async (req: Request, res: Response) => {
const { data: { licenses } } = await licenseServerKeyRequest.get( const {
`${await getLicenseServerUrl()}/api/license-server/v1/customers/${req.organization.customerId}/licenses` params: { organizationId }
); } = await validateRequest(reqValidator.GetOrgLicencesv1, req);
return res.status(200).send(licenses); 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);
};
@@ -0,0 +1,235 @@
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 = [
{
_id: "admin",
name: "Admin",
slug: "admin",
description: "Complete administration access over the organization",
permissions: isOrgRole ? adminPermissions.rules : adminProjectPermissions.rules
},
{
_id: "member",
name: isOrgRole ? "Member" : "Developer",
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)
}
});
};
@@ -1,7 +1,17 @@
import { ForbiddenError, subject } from "@casl/ability";
import { Request, Response } from "express"; 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 { SecretVersion } from "../../models";
import { EESecretService } from "../../services"; import { EESecretService } from "../../services";
import { getFolderWithPathFromId } from "../../../services/FolderService";
/** /**
* Return secret versions for secret with id [secretId] * 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 secret = await Secret.findById(secretId);
const limit: number = parseInt(req.query.limit as string); 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({ const secretVersions = await SecretVersion.find({
secret: secretId 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 // validate secret version
const oldSecretVersion = await SecretVersion.findOne({ const oldSecretVersion = await SecretVersion.findOne({
@@ -154,6 +191,15 @@ export const rollbackSecretVersion = async (req: Request, res: Response) => {
keyEncoding keyEncoding
} = oldSecretVersion; } = 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 // update secret
const secret = await Secret.findByIdAndUpdate( const secret = await Secret.findByIdAndUpdate(
secretId, secretId,
@@ -1,9 +1,13 @@
import { ForbiddenError } from "@casl/ability";
import { Request, Response } from "express"; import { Request, Response } from "express";
import { validateRequest } from "../../../helpers/validation";
import { import {
ISecretVersion, ProjectPermissionActions,
SecretSnapshot, ProjectPermissionSub,
TFolderRootVersionSchema, getUserProjectPermissions
} from "../../models"; } from "../../services/ProjectRoleService";
import * as reqValidator from "../../../validation/secretSnapshot";
import { ISecretVersion, SecretSnapshot, TFolderRootVersionSchema } from "../../models";
/** /**
* Return secret snapshot with id [secretSnapshotId] * Return secret snapshot with id [secretSnapshotId]
@@ -12,7 +16,9 @@ import {
* @returns * @returns
*/ */
export const getSecretSnapshot = async (req: Request, res: Response) => { 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) const secretSnapshot = await SecretSnapshot.findById(secretSnapshotId)
.lean() .lean()
@@ -20,26 +26,36 @@ export const getSecretSnapshot = async (req: Request, res: Response) => {
path: "secretVersions", path: "secretVersions",
populate: { populate: {
path: "tags", path: "tags",
model: "Tag", model: "Tag"
}, }
}) })
.populate<{ folderVersion: TFolderRootVersionSchema }>("folderVersion"); .populate<{ folderVersion: TFolderRootVersionSchema }>("folderVersion");
if (!secretSnapshot) throw new Error("Failed to find secret snapshot"); 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; const folderId = secretSnapshot.folderId;
// to show only the folder required secrets // to show only the folder required secrets
secretSnapshot.secretVersions = secretSnapshot.secretVersions.filter( secretSnapshot.secretVersions = secretSnapshot.secretVersions.filter(
({ folder }) => folder === folderId ({ folder }) => folder === folderId
); );
secretSnapshot.folderVersion = secretSnapshot.folderVersion = secretSnapshot?.folderVersion?.nodes?.children?.map(
secretSnapshot?.folderVersion?.nodes?.children?.map(({ id, name }) => ({ ({ id, name }) => ({
id, id,
name, name
})) as any; })
) as any;
return res.status(200).send({ return res.status(200).send({
secretSnapshot, secretSnapshot
}); });
}; };
+211 -192
View File
@@ -2,239 +2,258 @@ import { Request, Response } from "express";
import { Types } from "mongoose"; import { Types } from "mongoose";
import { BotOrgService } from "../../../services"; import { BotOrgService } from "../../../services";
import { SSOConfig } from "../../models"; import { SSOConfig } from "../../models";
import { import { AuthMethod, MembershipOrg, User } from "../../../models";
AuthMethod,
MembershipOrg,
User
} from "../../../models";
import { getSSOConfigHelper } from "../../helpers/organizations"; import { getSSOConfigHelper } from "../../helpers/organizations";
import { client } from "../../../config"; import { client } from "../../../config";
import { ResourceNotFoundError } from "../../../utils/errors"; import { ResourceNotFoundError } from "../../../utils/errors";
import { getSiteURL } from "../../../config"; import { getSiteURL } from "../../../config";
import { EELicenseService } from "../../services"; 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 * Redirect user to appropriate SSO endpoint after successful authentication
* to finish inputting their master key for logging in or signing up * to finish inputting their master key for logging in or signing up
* @param req * @param req
* @param res * @param res
* @returns * @returns
*/ */
export const redirectSSO = async (req: Request, res: Response) => { export const redirectSSO = async (req: Request, res: Response) => {
if (req.isUserCompleted) { if (req.isUserCompleted) {
return res.redirect(`${await getSiteURL()}/login/sso?token=${encodeURIComponent(req.providerAuthToken)}`); return res.redirect(
} `${await getSiteURL()}/login/sso?token=${encodeURIComponent(req.providerAuthToken)}`
);
return res.redirect(`${await getSiteURL()}/signup/sso?token=${encodeURIComponent(req.providerAuthToken)}`); }
}
return res.redirect(
`${await getSiteURL()}/signup/sso?token=${encodeURIComponent(req.providerAuthToken)}`
);
};
/** /**
* Return organization SAML SSO configuration * Return organization SAML SSO configuration
* @param req * @param req
* @param res * @param res
* @returns * @returns
*/ */
export const getSSOConfig = async (req: Request, res: Response) => { export const getSSOConfig = async (req: Request, res: Response) => {
const organizationId = req.query.organizationId as string; const {
query: { organizationId }
const data = await getSSOConfigHelper({ } = await validateRequest(reqValidator.GetSsoConfigv1, req);
organizationId: new Types.ObjectId(organizationId)
});
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 * Update organization SAML SSO configuration
* @param req * @param req
* @param res * @param res
* @returns * @returns
*/ */
export const updateSSOConfig = async (req: Request, res: Response) => { 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 { const {
organizationId, ciphertext: encryptedEntryPoint,
authProvider, iv: entryPointIV,
isActive, tag: entryPointTag
entryPoint, } = client.encryptSymmetric(entryPoint, key);
issuer,
cert,
} = req.body;
const plan = await EELicenseService.getPlan(new Types.ObjectId(organizationId)); update.encryptedEntryPoint = encryptedEntryPoint;
update.entryPointIV = entryPointIV;
if (!plan.samlSSO) return res.status(400).send({ update.entryPointTag = entryPointTag;
message: "Failed to update SAML SSO configuration due to plan restriction. Upgrade plan to update SSO configuration." }
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) { if (update.isActive !== undefined) {
const { const membershipOrgs = await MembershipOrg.find({
ciphertext: encryptedIssuer, organization: new Types.ObjectId(organizationId)
iv: issuerIV, }).select("user");
tag: issuerTag
} = client.encryptSymmetric(issuer, key);
update.encryptedIssuer = encryptedIssuer;
update.issuerIV = issuerIV;
update.issuerTag = issuerTag;
}
if (cert) { if (update.isActive) {
const { await User.updateMany(
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) _id: {
$in: membershipOrgs.map((membershipOrg) => membershipOrg.user)
}
}, },
update,
{ {
new: true authMethods: [ssoConfig.authProvider]
} }
); );
} else {
if (!ssoConfig) throw ResourceNotFoundError({ await User.updateMany(
message: "Failed to find SSO config to update" {
}); _id: {
$in: membershipOrgs.map((membershipOrg) => membershipOrg.user)
if (update.isActive !== undefined) { }
const membershipOrgs = await MembershipOrg.find({ },
organization: new Types.ObjectId(organizationId) {
}).select("user"); authMethods: [AuthMethod.EMAIL]
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],
}
);
} }
);
} }
}
return res.status(200).send(ssoConfig);
} return res.status(200).send(ssoConfig);
};
/** /**
* Create organization SAML SSO configuration * Create organization SAML SSO configuration
* @param req * @param req
* @param res * @param res
* @returns * @returns
*/ */
export const createSSOConfig = async (req: Request, res: Response) => { export const createSSOConfig = async (req: Request, res: Response) => {
const { const {
organizationId, body: { organizationId, authProvider, isActive, entryPoint, issuer, cert }
authProvider, } = await validateRequest(reqValidator.CreateSsoConfigv1, req);
isActive,
entryPoint,
issuer,
cert
} = req.body;
const plan = await EELicenseService.getPlan(new Types.ObjectId(organizationId)); const { permission } = await getUserOrgPermissions(req.user._id, organizationId);
ForbiddenError.from(permission).throwUnlessCan(
if (!plan.samlSSO) return res.status(400).send({ OrgPermissionActions.Create,
message: "Failed to create SAML SSO configuration due to plan restriction. Upgrade plan to add SSO configuration." 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 { const key = await BotOrgService.getSymmetricKey(new Types.ObjectId(organizationId));
ciphertext: encryptedEntryPoint,
iv: entryPointIV,
tag: entryPointTag
} = client.encryptSymmetric(entryPoint, key);
const { const {
ciphertext: encryptedIssuer, ciphertext: encryptedEntryPoint,
iv: issuerIV, iv: entryPointIV,
tag: issuerTag tag: entryPointTag
} = client.encryptSymmetric(issuer, key); } = client.encryptSymmetric(entryPoint, key);
const { const {
ciphertext: encryptedCert, ciphertext: encryptedIssuer,
iv: certIV, iv: issuerIV,
tag: certTag tag: issuerTag
} = client.encryptSymmetric(cert, key); } = client.encryptSymmetric(issuer, 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); 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);
};
@@ -1,6 +1,14 @@
import { Request, Response } from "express"; import { Request, Response } from "express";
import { PipelineStage, Types } from "mongoose"; import { PipelineStage, Types } from "mongoose";
import { Folder, Membership, Secret, ServiceTokenData, TFolderSchema, User } from "../../../models"; import {
Folder,
Membership,
Secret,
ServiceTokenData,
TFolderSchema,
User,
Workspace
} from "../../../models";
import { import {
ActorType, ActorType,
AuditLog, AuditLog,
@@ -22,16 +30,33 @@ import { getLatestSecretVersionIds } from "../../helpers/secretVersion";
import { searchByFolderId } from "../../../services/FolderService"; import { searchByFolderId } from "../../../services/FolderService";
import { EEAuditLogService, EELicenseService } from "../../services"; import { EEAuditLogService, EELicenseService } from "../../services";
import { extractIPDetails, isValidIpOrCidr } from "../../../utils/ip"; 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";
import { BadRequestError } from "../../../utils/errors";
/** /**
* Return secret snapshots for workspace with id [workspaceId] * Return secret snapshots for workspace with id [workspaceId]
* @param req * @param req
* @param res * @param res
*/ */
export const getWorkspaceSecretSnapshots = async ( export const getWorkspaceSecretSnapshots = async (req: Request, res: Response) => {
req: Request,
res: Response
) => {
/* /*
#swagger.summary = 'Return project secret snapshot ids' #swagger.summary = 'Return project secret snapshot ids'
#swagger.description = 'Return project secret snapshots ids' #swagger.description = 'Return project secret snapshots ids'
@@ -77,23 +102,28 @@ export const getWorkspaceSecretSnapshots = async (
} }
} }
*/ */
const { workspaceId } = req.params; const {
const { environment, folderId } = req.query; params: { workspaceId },
query: { environment, folderId, offset, limit }
} = await validateRequest(GetWorkspaceSecretSnapshotsV1, req);
const offset: number = parseInt(req.query.offset as string); const { permission } = await getUserProjectPermissions(req.user._id, workspaceId);
const limit: number = parseInt(req.query.limit as string); ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read,
ProjectPermissionSub.SecretRollback
);
const secretSnapshots = await SecretSnapshot.find({ const secretSnapshots = await SecretSnapshot.find({
workspace: workspaceId, workspace: workspaceId,
environment, environment,
folderId: folderId || "root", folderId: folderId || "root"
}) })
.sort({ createdAt: -1 }) .sort({ createdAt: -1 })
.skip(offset) .skip(offset)
.limit(limit); .limit(limit);
return res.status(200).send({ return res.status(200).send({
secretSnapshots, secretSnapshots
}); });
}; };
@@ -102,21 +132,26 @@ export const getWorkspaceSecretSnapshots = async (
* @param req * @param req
* @param res * @param res
*/ */
export const getWorkspaceSecretSnapshotsCount = async ( export const getWorkspaceSecretSnapshotsCount = async (req: Request, res: Response) => {
req: Request, const {
res: Response params: { workspaceId },
) => { query: { environment, folderId }
const { workspaceId } = req.params; } = await validateRequest(GetWorkspaceSecretSnapshotsCountV1, req);
const { environment, folderId } = req.query;
const { permission } = await getUserProjectPermissions(req.user._id, workspaceId);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read,
ProjectPermissionSub.SecretRollback
);
const count = await SecretSnapshot.countDocuments({ const count = await SecretSnapshot.countDocuments({
workspace: workspaceId, workspace: workspaceId,
environment, environment,
folderId: folderId || "root", folderId: folderId || "root"
}); });
return res.status(200).send({ return res.status(200).send({
count, count
}); });
}; };
@@ -126,10 +161,7 @@ export const getWorkspaceSecretSnapshotsCount = async (
* @param res * @param res
* @returns * @returns
*/ */
export const rollbackWorkspaceSecretSnapshot = async ( export const rollbackWorkspaceSecretSnapshot = async (req: Request, res: Response) => {
req: Request,
res: Response
) => {
/* /*
#swagger.summary = 'Roll back project secrets to those captured in a secret snapshot version.' #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.' #swagger.description = 'Roll back project secrets to those captured in a secret snapshot version.'
@@ -181,19 +213,27 @@ export const rollbackWorkspaceSecretSnapshot = async (
} }
*/ */
const { workspaceId } = req.params; const {
const { version, environment, folderId = "root" } = req.body; 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 // validate secret snapshot
const secretSnapshot = await SecretSnapshot.findOne({ const secretSnapshot = await SecretSnapshot.findOne({
workspace: workspaceId, workspace: workspaceId,
version, version,
environment, environment,
folderId: folderId, folderId: folderId
}) })
.populate<{ secretVersions: ISecretVersion[] }>({ .populate<{ secretVersions: ISecretVersion[] }>({
path: "secretVersions", path: "secretVersions",
select: "+secretBlindIndex", select: "+secretBlindIndex"
}) })
.populate<{ folderVersion: TFolderRootVersionSchema }>("folderVersion"); .populate<{ folderVersion: TFolderRootVersionSchema }>("folderVersion");
@@ -202,13 +242,13 @@ export const rollbackWorkspaceSecretSnapshot = async (
const snapshotFolderTree = secretSnapshot.folderVersion; const snapshotFolderTree = secretSnapshot.folderVersion;
const latestFolderTree = await Folder.findOne({ const latestFolderTree = await Folder.findOne({
workspace: workspaceId, workspace: workspaceId,
environment, environment
}); });
const latestFolderVersion = await FolderVersion.findOne({ const latestFolderVersion = await FolderVersion.findOne({
environment, environment,
workspace: workspaceId, workspace: workspaceId,
"nodes.id": folderId, "nodes.id": folderId
}).sort({ "nodes.version": -1 }); }).sort({ "nodes.version": -1 });
const oldSecretVersionsObj: Record<string, ISecretVersion> = {}; const oldSecretVersionsObj: Record<string, ISecretVersion> = {};
@@ -222,8 +262,7 @@ export const rollbackWorkspaceSecretSnapshot = async (
// the parent node from current latest one // the parent node from current latest one
// this will be modified according to the snapshot and latest snapshots // this will be modified according to the snapshot and latest snapshots
const newFolderTree = const newFolderTree = latestFolderTree && searchByFolderId(latestFolderTree.nodes, folderId);
latestFolderTree && searchByFolderId(latestFolderTree.nodes, folderId);
if (newFolderTree) { if (newFolderTree) {
newFolderTree.children = snapshotFolderTree?.nodes?.children || []; newFolderTree.children = snapshotFolderTree?.nodes?.children || [];
@@ -252,43 +291,43 @@ export const rollbackWorkspaceSecretSnapshot = async (
workspace: new Types.ObjectId(workspaceId), workspace: new Types.ObjectId(workspaceId),
environment, environment,
folderId: { folderId: {
$in: Object.keys(groupByFolderId), $in: Object.keys(groupByFolderId)
}, }
}, }
}; };
const sortByFolderIdAndVersion: PipelineStage = { const sortByFolderIdAndVersion: PipelineStage = {
$sort: { folderId: 1, version: -1 }, $sort: { folderId: 1, version: -1 }
}; };
const pickLatestVersionOfEachFolder = { const pickLatestVersionOfEachFolder = {
$group: { $group: {
_id: "$folderId", _id: "$folderId",
latestVersion: { $first: "$version" }, latestVersion: { $first: "$version" },
doc: { doc: {
$first: "$$ROOT", $first: "$$ROOT"
}, }
}, }
}; };
const populateSecVersion = { const populateSecVersion = {
$lookup: { $lookup: {
from: SecretVersion.collection.name, from: SecretVersion.collection.name,
localField: "doc.secretVersions", localField: "doc.secretVersions",
foreignField: "_id", foreignField: "_id",
as: "doc.secretVersions", as: "doc.secretVersions"
}, }
}; };
const populateFolderVersion = { const populateFolderVersion = {
$lookup: { $lookup: {
from: FolderVersion.collection.name, from: FolderVersion.collection.name,
localField: "doc.folderVersion", localField: "doc.folderVersion",
foreignField: "_id", foreignField: "_id",
as: "doc.folderVersion", as: "doc.folderVersion"
}, }
}; };
const unwindFolderVerField = { const unwindFolderVerField = {
$unwind: { $unwind: {
path: "$doc.folderVersion", path: "$doc.folderVersion",
preserveNullAndEmptyArrays: true, preserveNullAndEmptyArrays: true
}, }
}; };
const latestSnapshotsByFolders: Array<{ doc: typeof secretSnapshot }> = const latestSnapshotsByFolders: Array<{ doc: typeof secretSnapshot }> =
await SecretSnapshot.aggregate([ await SecretSnapshot.aggregate([
@@ -297,7 +336,7 @@ export const rollbackWorkspaceSecretSnapshot = async (
pickLatestVersionOfEachFolder, pickLatestVersionOfEachFolder,
populateSecVersion, populateSecVersion,
populateFolderVersion, populateFolderVersion,
unwindFolderVerField, unwindFolderVerField
]); ]);
// recursive snapshotting each level // recursive snapshotting each level
@@ -327,7 +366,7 @@ export const rollbackWorkspaceSecretSnapshot = async (
// TODO: fix any // TODO: fix any
const latestSecretVersionIds = await getLatestSecretVersionIds({ const latestSecretVersionIds = await getLatestSecretVersionIds({
secretIds, secretIds
}); });
// TODO: fix any // TODO: fix any
@@ -335,32 +374,31 @@ export const rollbackWorkspaceSecretSnapshot = async (
await SecretVersion.find( await SecretVersion.find(
{ {
_id: { _id: {
$in: latestSecretVersionIds.map((s) => s.versionId), $in: latestSecretVersionIds.map((s) => s.versionId)
}, }
}, },
"secret version" "secret version"
) )
).reduce( ).reduce(
(accumulator, s) => ({ (accumulator, s) => ({
...accumulator, ...accumulator,
[`${s.secret.toString()}`]: s, [`${s.secret.toString()}`]: s
}), }),
{} {}
); );
const secDelQuery: Record<string, unknown> = { const secDelQuery: Record<string, unknown> = {
workspace: workspaceId, workspace: workspaceId,
environment, environment
// undefined means root thus collect all secrets // undefined means root thus collect all secrets
}; };
if (folderId !== "root" && folderIds.length) if (folderId !== "root" && folderIds.length) secDelQuery.folder = { $in: folderIds };
secDelQuery.folder = { $in: folderIds };
// delete existing secrets // delete existing secrets
await Secret.deleteMany(secDelQuery); await Secret.deleteMany(secDelQuery);
await Folder.deleteOne({ await Folder.deleteOne({
workspace: workspaceId, workspace: workspaceId,
environment, environment
}); });
// add secrets // add secrets
@@ -382,7 +420,7 @@ export const rollbackWorkspaceSecretSnapshot = async (
createdAt, createdAt,
algorithm, algorithm,
keyEncoding, keyEncoding,
folder: secFolderId, folder: secFolderId
} = oldSecretVersionsObj[sv]; } = oldSecretVersionsObj[sv];
return { return {
@@ -405,7 +443,7 @@ export const rollbackWorkspaceSecretSnapshot = async (
createdAt, createdAt,
algorithm, algorithm,
keyEncoding, keyEncoding,
folder: secFolderId, folder: secFolderId
}; };
}) })
); );
@@ -429,7 +467,7 @@ export const rollbackWorkspaceSecretSnapshot = async (
secretValueTag, secretValueTag,
algorithm, algorithm,
keyEncoding, keyEncoding,
folder: secFolderId, folder: secFolderId
}) => ({ }) => ({
_id: new Types.ObjectId(), _id: new Types.ObjectId(),
secret: _id, secret: _id,
@@ -448,7 +486,7 @@ export const rollbackWorkspaceSecretSnapshot = async (
secretValueTag, secretValueTag,
algorithm, algorithm,
keyEncoding, keyEncoding,
folder: secFolderId, folder: secFolderId
}) })
) )
); );
@@ -464,7 +502,7 @@ export const rollbackWorkspaceSecretSnapshot = async (
const newFolderVersion = new FolderVersion({ const newFolderVersion = new FolderVersion({
workspace: workspaceId, workspace: workspaceId,
environment, environment,
nodes: newFolderTree, nodes: newFolderTree
}); });
await newFolderVersion.save(); await newFolderVersion.save();
} }
@@ -473,13 +511,11 @@ export const rollbackWorkspaceSecretSnapshot = async (
await SecretVersion.updateMany( await SecretVersion.updateMany(
{ {
secret: { secret: {
$in: Object.keys(oldSecretVersionsObj).map( $in: Object.keys(oldSecretVersionsObj).map((sv) => oldSecretVersionsObj[sv].secret)
(sv) => oldSecretVersionsObj[sv].secret }
),
},
}, },
{ {
isDeleted: false, isDeleted: false
} }
); );
@@ -487,11 +523,11 @@ export const rollbackWorkspaceSecretSnapshot = async (
await EESecretService.takeSecretSnapshot({ await EESecretService.takeSecretSnapshot({
workspaceId: new Types.ObjectId(workspaceId), workspaceId: new Types.ObjectId(workspaceId),
environment, environment,
folderId, folderId
}); });
return res.status(200).send({ return res.status(200).send({
secrets, secrets
}); });
}; };
@@ -568,13 +604,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 { permission } = await getUserProjectPermissions(req.user._id, workspaceId);
const limit: number = parseInt(req.query.limit as string); ForbiddenError.from(permission).throwUnlessCan(
const sortBy: string = req.query.sortBy as string; ProjectPermissionActions.Read,
const userId: string = req.query.userId as string; ProjectPermissionSub.AuditLogs
const actionNames: string = req.query.actionNames as string; );
const logs = await Log.find({ const logs = await Log.find({
workspace: workspaceId, workspace: workspaceId,
@@ -582,10 +621,10 @@ export const getWorkspaceLogs = async (req: Request, res: Response) => {
...(actionNames ...(actionNames
? { ? {
actionNames: { actionNames: {
$in: actionNames.split(","), $in: actionNames.split(",")
}, }
} }
: {}), : {})
}) })
.sort({ createdAt: sortBy === "recent" ? -1 : 1 }) .sort({ createdAt: sortBy === "recent" ? -1 : 1 })
.skip(offset) .skip(offset)
@@ -594,93 +633,109 @@ export const getWorkspaceLogs = async (req: Request, res: Response) => {
.populate("user serviceAccount serviceTokenData"); .populate("user serviceAccount serviceTokenData");
return res.status(200).send({ return res.status(200).send({
logs, logs
}); });
}; };
/** /**
* Return audit logs for workspace with id [workspaceId] * Return audit logs for workspace with id [workspaceId]
* @param req * @param req
* @param res * @param res
*/ */
export const getWorkspaceAuditLogs = async (req: Request, res: Response) => { export const getWorkspaceAuditLogs = async (req: Request, res: Response) => {
const { workspaceId } = req.params; const {
const eventType = req.query.eventType; query: { limit, offset, endDate, eventType, startDate, userAgentType, actor },
const userAgentType = req.query.userAgentType; params: { workspaceId }
const actor = req.query.actor as string | undefined; } = await validateRequest(GetWorkspaceAuditLogsV1, 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(
const startDate = req.query.startDate as string; ProjectPermissionActions.Read,
const endDate = req.query.endDate as string; ProjectPermissionSub.AuditLogs
);
const query = { const query = {
workspace: new Types.ObjectId(workspaceId), workspace: new Types.ObjectId(workspaceId),
...(eventType ? { ...(eventType
"event.type": eventType ? {
} : {}), "event.type": eventType
...(userAgentType ? { }
userAgentType : {}),
} : {}), ...(userAgentType
...(actor ? { ? {
"actor.type": actor.split("-", 2)[0], userAgentType
...(actor.split("-", 2)[0] === ActorType.USER ? { }
"actor.metadata.userId": actor.split("-", 2)[1] : {}),
} : { ...(actor
"actor.metadata.serviceId": actor.split("-", 2)[1] ? {
}) "actor.type": actor.split("-", 2)[0],
} : {}), ...(actor.split("-", 2)[0] === ActorType.USER
...(startDate || endDate ? { ? {
createdAt: { "actor.metadata.userId": actor.split("-", 2)[1]
...(startDate && { $gte: new Date(startDate) }), }
...(endDate && { $lte: new Date(endDate) }) : {
} "actor.metadata.serviceId": actor.split("-", 2)[1]
} : {}) })
} }
: {}),
const auditLogs = await AuditLog.find(query) ...(startDate || endDate
.sort({ createdAt: -1 }) ? {
.skip(offset) createdAt: {
.limit(limit); ...(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); const totalCount = await AuditLog.countDocuments(query);
return res.status(200).send({ return res.status(200).send({
auditLogs, auditLogs,
totalCount totalCount
}); });
} };
/** /**
* Return audit log actor filter options for workspace with id [workspaceId] * Return audit log actor filter options for workspace with id [workspaceId]
* @param req * @param req
* @param res * @param res
*/ */
export const getWorkspaceAuditLogActorFilterOpts = async (req: Request, res: Response) => { 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", { const userIds = await Membership.distinct("user", {
workspace: new Types.ObjectId(workspaceId) workspace: new Types.ObjectId(workspaceId)
}); });
const userActors: UserActor[] = (await User.find({ const userActors: UserActor[] = (
_id: { await User.find({
$in: userIds _id: {
} $in: userIds
}) }
.select("email")) }).select("email")
.map((user) => ({ ).map((user) => ({
type: ActorType.USER, type: ActorType.USER,
metadata: { metadata: {
userId: user._id.toString(), userId: user._id.toString(),
email: user.email email: user.email
} }
})); }));
const serviceActors: ServiceActor[] = (await ServiceTokenData.find({ const serviceActors: ServiceActor[] = (
workspace: new Types.ObjectId(workspaceId) await ServiceTokenData.find({
}) workspace: new Types.ObjectId(workspaceId)
.select("name")) }).select("name")
.map((serviceTokenData) => ({ ).map((serviceTokenData) => ({
type: ActorType.SERVICE, type: ActorType.SERVICE,
metadata: { metadata: {
serviceId: serviceTokenData._id.toString(), serviceId: serviceTokenData._id.toString(),
@@ -691,50 +746,68 @@ export const getWorkspaceAuditLogActorFilterOpts = async (req: Request, res: Res
return res.status(200).send({ return res.status(200).send({
actors: [...userActors, ...serviceActors] actors: [...userActors, ...serviceActors]
}); });
} };
/** /**
* Return trusted ips for workspace with id [workspaceId] * Return trusted ips for workspace with id [workspaceId]
* @param req * @param req
* @param res * @param res
*/ */
export const getWorkspaceTrustedIps = async (req: Request, res: Response) => { 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({ const trustedIps = await TrustedIP.find({
workspace: new Types.ObjectId(workspaceId) workspace: new Types.ObjectId(workspaceId)
}); });
return res.status(200).send({ return res.status(200).send({
trustedIps trustedIps
}); });
} };
/** /**
* Add a trusted ip to workspace with id [workspaceId] * Add a trusted ip to workspace with id [workspaceId]
* @param req * @param req
* @param res * @param res
*/ */
export const addWorkspaceTrustedIp = async (req: Request, res: Response) => { export const addWorkspaceTrustedIp = async (req: Request, res: Response) => {
const { workspaceId } = req.params;
const { const {
ipAddress: ip, params: { workspaceId },
comment, body: { comment, isActive, ipAddress: ip }
isActive } = await validateRequest(AddWorkspaceTrustedIpV1, req);
} = req.body;
const { permission } = await getUserProjectPermissions(req.user._id, workspaceId);
const plan = await EELicenseService.getPlan(req.workspace.organization); ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Create,
if (!plan.ipAllowlisting) return res.status(400).send({ ProjectPermissionSub.IpAllowList
message: "Failed to add IP access range due to plan restriction. Upgrade plan to add IP access range." );
});
const workspace = await Workspace.findById(workspaceId);
if (!workspace) throw BadRequestError({ message: "Workspace not found" });
const plan = await EELicenseService.getPlan(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."
});
const isValidIPOrCidr = isValidIpOrCidr(ip); const isValidIPOrCidr = isValidIpOrCidr(ip);
if (!isValidIPOrCidr) return res.status(400).send({ if (!isValidIPOrCidr)
message: "The IP is not a valid IPv4, IPv6, or CIDR block" return res.status(400).send({
}); message: "The IP is not a valid IPv4, IPv6, or CIDR block"
});
const { ipAddress, type, prefix } = extractIPDetails(ip); const { ipAddress, type, prefix } = extractIPDetails(ip);
const trustedIp = await new TrustedIP({ const trustedIp = await new TrustedIP({
@@ -743,9 +816,9 @@ export const addWorkspaceTrustedIp = async (req: Request, res: Response) => {
type, type,
prefix, prefix,
isActive, isActive,
comment, comment
}).save(); }).save();
await EEAuditLogService.createAuditLog( await EEAuditLogService.createAuditLog(
req.authData, req.authData,
{ {
@@ -764,32 +837,43 @@ export const addWorkspaceTrustedIp = async (req: Request, res: Response) => {
return res.status(200).send({ return res.status(200).send({
trustedIp trustedIp
}); });
} };
/** /**
* Update trusted ip with id [trustedIpId] workspace with id [workspaceId] * Update trusted ip with id [trustedIpId] workspace with id [workspaceId]
* @param req * @param req
* @param res * @param res
*/ */
export const updateWorkspaceTrustedIp = async (req: Request, res: Response) => { export const updateWorkspaceTrustedIp = async (req: Request, res: Response) => {
const { workspaceId, trustedIpId } = req.params;
const { const {
ipAddress: ip, params: { workspaceId, trustedIpId },
comment body: { ipAddress: ip, comment }
} = req.body; } = await validateRequest(UpdateWorkspaceTrustedIpV1, req);
const plan = await EELicenseService.getPlan(req.workspace.organization); const { permission } = await getUserProjectPermissions(req.user._id, workspaceId);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Edit,
ProjectPermissionSub.IpAllowList
);
if (!plan.ipAllowlisting) return res.status(400).send({ const workspace = await Workspace.findById(workspaceId);
message: "Failed to update IP access range due to plan restriction. Upgrade plan to update IP access range." if (!workspace) throw BadRequestError({ message: "Workspace not found" });
});
const plan = await EELicenseService.getPlan(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."
});
const isValidIPOrCidr = isValidIpOrCidr(ip); const isValidIPOrCidr = isValidIpOrCidr(ip);
if (!isValidIPOrCidr) return res.status(400).send({ if (!isValidIPOrCidr)
message: "The IP is not a valid IPv4, IPv6, or CIDR block" return res.status(400).send({
}); message: "The IP is not a valid IPv4, IPv6, or CIDR block"
});
const { ipAddress, type, prefix } = extractIPDetails(ip); const { ipAddress, type, prefix } = extractIPDetails(ip);
const updateObject: { const updateObject: {
@@ -799,33 +883,34 @@ export const updateWorkspaceTrustedIp = async (req: Request, res: Response) => {
prefix?: number; prefix?: number;
$unset?: { $unset?: {
prefix: number; prefix: number;
} };
} = { } = {
ipAddress, ipAddress,
type, type,
comment comment
}; };
if (prefix !== undefined) { if (prefix !== undefined) {
updateObject.prefix = prefix; updateObject.prefix = prefix;
} else { } else {
updateObject.$unset = { prefix: 1 }; updateObject.$unset = { prefix: 1 };
} }
const trustedIp = await TrustedIP.findOneAndUpdate( const trustedIp = await TrustedIP.findOneAndUpdate(
{ {
_id: new Types.ObjectId(trustedIpId), _id: new Types.ObjectId(trustedIpId),
workspace: new Types.ObjectId(workspaceId), workspace: new Types.ObjectId(workspaceId)
}, },
updateObject, updateObject,
{ {
new: true new: true
} }
); );
if (!trustedIp) return res.status(400).send({ if (!trustedIp)
message: "Failed to update trusted IP" return res.status(400).send({
}); message: "Failed to update trusted IP"
});
await EEAuditLogService.createAuditLog( await EEAuditLogService.createAuditLog(
req.authData, req.authData,
@@ -841,34 +926,48 @@ export const updateWorkspaceTrustedIp = async (req: Request, res: Response) => {
workspaceId: trustedIp.workspace workspaceId: trustedIp.workspace
} }
); );
return res.status(200).send({ return res.status(200).send({
trustedIp trustedIp
}); });
} };
/** /**
* Delete IP access range from workspace with id [workspaceId] * Delete IP access range from workspace with id [workspaceId]
* @param req * @param req
* @param res * @param res
*/ */
export const deleteWorkspaceTrustedIp = async (req: Request, res: Response) => { 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 workspace = await Workspace.findById(workspaceId);
if (!workspace) throw BadRequestError({ message: "Workspace not found" });
const plan = await EELicenseService.getPlan(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."
});
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."
});
const trustedIp = await TrustedIP.findOneAndDelete({ const trustedIp = await TrustedIP.findOneAndDelete({
_id: new Types.ObjectId(trustedIpId), _id: new Types.ObjectId(trustedIpId),
workspace: new Types.ObjectId(workspaceId) workspace: new Types.ObjectId(workspaceId)
}); });
if (!trustedIp) return res.status(400).send({ if (!trustedIp)
message: "Failed to delete trusted IP" return res.status(400).send({
}); message: "Failed to delete trusted IP"
});
await EEAuditLogService.createAuditLog( await EEAuditLogService.createAuditLog(
req.authData, req.authData,
@@ -888,4 +987,4 @@ export const deleteWorkspaceTrustedIp = async (req: Request, res: Response) => {
return res.status(200).send({ return res.status(200).send({
trustedIp trustedIp
}); });
} };
+55
View File
@@ -0,0 +1,55 @@
import { Schema, Types, model } from "mongoose";
export interface IRole {
_id: Types.ObjectId;
name: string;
description: string;
slug: string;
permissions: Array<unknown>;
workspace: Types.ObjectId;
organization: Types.ObjectId;
isOrgRole: boolean;
}
const roleSchema = new Schema<IRole>(
{
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<IRole>("Role", roleSchema);
export default Role;
+2 -11
View File
@@ -1,17 +1,8 @@
import express from "express"; import express from "express";
const router = express.Router(); const router = express.Router();
import {
validateRequest,
} from "../../../middleware";
import { param } from "express-validator";
import { actionController } from "../../controllers/v1"; import { actionController } from "../../controllers/v1";
// TODO: put into action controller // TODO: put into action controller
router.get( router.get("/:actionId", actionController.getAction);
"/:actionId",
param("actionId").exists().trim(),
validateRequest,
actionController.getAction
);
export default router; export default router;
+8 -13
View File
@@ -1,21 +1,16 @@
import express from "express"; import express from "express";
const router = express.Router(); const router = express.Router();
import { import { requireAuth, validateRequest } from "../../../middleware";
requireAuth,
validateRequest,
} from "../../../middleware";
import { query } from "express-validator";
import { cloudProductsController } from "../../controllers/v1"; import { cloudProductsController } from "../../controllers/v1";
import { AuthMode } from "../../../variables"; import { AuthMode } from "../../../variables";
router.get( router.get(
"/", "/",
requireAuth({ requireAuth({
acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY], acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY]
}), }),
query("billing-cycle").exists().isIn(["monthly", "yearly"]), validateRequest,
validateRequest, cloudProductsController.getCloudProducts
cloudProductsController.getCloudProducts
); );
export default router; export default router;
+12 -10
View File
@@ -7,15 +7,17 @@ import workspace from "./workspace";
import action from "./action"; import action from "./action";
import cloudProducts from "./cloudProducts"; import cloudProducts from "./cloudProducts";
import secretScanning from "./secretScanning"; import secretScanning from "./secretScanning";
import roles from "./role";
export { export {
secret, secret,
secretSnapshot, secretSnapshot,
organizations, organizations,
sso, sso,
users, users,
workspace, workspace,
action, action,
cloudProducts, cloudProducts,
secretScanning secretScanning,
} roles
};
+78 -188
View File
@@ -1,237 +1,127 @@
import express from "express"; import express from "express";
const router = express.Router(); const router = express.Router();
import { import { requireAuth } from "../../../middleware";
requireAuth,
requireOrganizationAuth,
validateRequest,
} from "../../../middleware";
import { body, param, query } from "express-validator";
import { organizationsController } from "../../controllers/v1"; import { organizationsController } from "../../controllers/v1";
import { import { AuthMode } from "../../../variables";
ACCEPTED, ADMIN, AuthMode, MEMBER, OWNER
} from "../../../variables";
router.get( router.get(
"/:organizationId/plans/table", "/:organizationId/plans/table",
requireAuth({ requireAuth({
acceptedAuthModes: [AuthMode.JWT], acceptedAuthModes: [AuthMode.JWT]
}), }),
requireOrganizationAuth({ organizationsController.getOrganizationPlansTable
acceptedRoles: [OWNER, ADMIN, MEMBER],
acceptedStatuses: [ACCEPTED],
}),
param("organizationId").exists().trim(),
query("billingCycle").exists().isString().isIn(["monthly", "yearly"]),
validateRequest,
organizationsController.getOrganizationPlansTable
); );
router.get( router.get(
"/:organizationId/plan", "/:organizationId/plan",
requireAuth({ requireAuth({
acceptedAuthModes: [AuthMode.JWT], acceptedAuthModes: [AuthMode.JWT]
}), }),
requireOrganizationAuth({ organizationsController.getOrganizationPlan
acceptedRoles: [OWNER, ADMIN, MEMBER],
acceptedStatuses: [ACCEPTED],
}),
param("organizationId").exists().trim(),
query("workspaceId").optional().isString(),
validateRequest,
organizationsController.getOrganizationPlan
); );
router.post( router.post(
"/:organizationId/session/trial", "/:organizationId/session/trial",
requireAuth({ requireAuth({
acceptedAuthModes: [AuthMode.JWT], acceptedAuthModes: [AuthMode.JWT]
}), }),
requireOrganizationAuth({ organizationsController.startOrganizationTrial
acceptedRoles: [OWNER, ADMIN, MEMBER],
acceptedStatuses: [ACCEPTED],
}),
param("organizationId").exists().trim(),
body("success_url").exists().trim(),
validateRequest,
organizationsController.startOrganizationTrial
); );
router.get( router.get(
"/:organizationId/plan/billing", "/:organizationId/plan/billing",
requireAuth({ requireAuth({
acceptedAuthModes: [AuthMode.JWT], acceptedAuthModes: [AuthMode.JWT]
}), }),
requireOrganizationAuth({ organizationsController.getOrganizationPlanBillingInfo
acceptedRoles: [OWNER, ADMIN, MEMBER],
acceptedStatuses: [ACCEPTED],
}),
param("organizationId").exists().trim(),
query("workspaceId").optional().isString(),
validateRequest,
organizationsController.getOrganizationPlanBillingInfo
); );
router.get( router.get(
"/:organizationId/plan/table", "/:organizationId/plan/table",
requireAuth({ requireAuth({
acceptedAuthModes: [AuthMode.JWT], acceptedAuthModes: [AuthMode.JWT]
}), }),
requireOrganizationAuth({ organizationsController.getOrganizationPlanTable
acceptedRoles: [OWNER, ADMIN, MEMBER],
acceptedStatuses: [ACCEPTED],
}),
param("organizationId").exists().trim(),
query("workspaceId").optional().isString(),
validateRequest,
organizationsController.getOrganizationPlanTable
); );
router.get( router.get(
"/:organizationId/billing-details", "/:organizationId/billing-details",
requireAuth({ requireAuth({
acceptedAuthModes: [AuthMode.JWT], acceptedAuthModes: [AuthMode.JWT]
}), }),
requireOrganizationAuth({ organizationsController.getOrganizationBillingDetails
acceptedRoles: [OWNER, ADMIN, MEMBER],
acceptedStatuses: [ACCEPTED],
}),
param("organizationId").exists().trim(),
validateRequest,
organizationsController.getOrganizationBillingDetails
); );
router.patch( router.patch(
"/:organizationId/billing-details", "/:organizationId/billing-details",
requireAuth({ requireAuth({
acceptedAuthModes: [AuthMode.JWT], acceptedAuthModes: [AuthMode.JWT]
}), }),
requireOrganizationAuth({ organizationsController.updateOrganizationBillingDetails
acceptedRoles: [OWNER, ADMIN, MEMBER],
acceptedStatuses: [ACCEPTED],
}),
param("organizationId").exists().trim(),
body("email").optional().isString().trim(),
body("name").optional().isString().trim(),
validateRequest,
organizationsController.updateOrganizationBillingDetails
); );
router.get( router.get(
"/:organizationId/billing-details/payment-methods", "/:organizationId/billing-details/payment-methods",
requireAuth({ requireAuth({
acceptedAuthModes: [AuthMode.JWT], acceptedAuthModes: [AuthMode.JWT]
}), }),
requireOrganizationAuth({ organizationsController.getOrganizationPmtMethods
acceptedRoles: [OWNER, ADMIN, MEMBER],
acceptedStatuses: [ACCEPTED],
}),
param("organizationId").exists().trim(),
validateRequest,
organizationsController.getOrganizationPmtMethods
); );
router.post( router.post(
"/:organizationId/billing-details/payment-methods", "/:organizationId/billing-details/payment-methods",
requireAuth({ requireAuth({
acceptedAuthModes: [AuthMode.JWT], acceptedAuthModes: [AuthMode.JWT]
}), }),
requireOrganizationAuth({ organizationsController.addOrganizationPmtMethod
acceptedRoles: [OWNER, ADMIN, MEMBER],
acceptedStatuses: [ACCEPTED],
}),
param("organizationId").exists().trim(),
body("success_url").exists().isString(),
body("cancel_url").exists().isString(),
validateRequest,
organizationsController.addOrganizationPmtMethod
); );
router.delete( router.delete(
"/:organizationId/billing-details/payment-methods/:pmtMethodId", "/:organizationId/billing-details/payment-methods/:pmtMethodId",
requireAuth({ requireAuth({
acceptedAuthModes: [AuthMode.JWT], acceptedAuthModes: [AuthMode.JWT]
}), }),
requireOrganizationAuth({ organizationsController.deleteOrganizationPmtMethod
acceptedRoles: [OWNER, ADMIN, MEMBER],
acceptedStatuses: [ACCEPTED],
}),
param("organizationId").exists().trim(),
param("pmtMethodId").exists().trim(),
validateRequest,
organizationsController.deleteOrganizationPmtMethod
); );
router.get( router.get(
"/:organizationId/billing-details/tax-ids", "/:organizationId/billing-details/tax-ids",
requireAuth({ requireAuth({
acceptedAuthModes: [AuthMode.JWT], acceptedAuthModes: [AuthMode.JWT]
}), }),
requireOrganizationAuth({ organizationsController.getOrganizationTaxIds
acceptedRoles: [OWNER, ADMIN, MEMBER],
acceptedStatuses: [ACCEPTED],
}),
param("organizationId").exists().trim(),
validateRequest,
organizationsController.getOrganizationTaxIds
); );
router.post( router.post(
"/:organizationId/billing-details/tax-ids", "/:organizationId/billing-details/tax-ids",
requireAuth({ requireAuth({
acceptedAuthModes: [AuthMode.JWT], acceptedAuthModes: [AuthMode.JWT]
}), }),
requireOrganizationAuth({ organizationsController.addOrganizationTaxId
acceptedRoles: [OWNER, ADMIN, MEMBER],
acceptedStatuses: [ACCEPTED],
}),
param("organizationId").exists().trim(),
body("type").exists().isString(),
body("value").exists().isString(),
validateRequest,
organizationsController.addOrganizationTaxId
); );
router.delete( router.delete(
"/:organizationId/billing-details/tax-ids/:taxId", "/:organizationId/billing-details/tax-ids/:taxId",
requireAuth({ requireAuth({
acceptedAuthModes: [AuthMode.JWT], acceptedAuthModes: [AuthMode.JWT]
}), }),
requireOrganizationAuth({ organizationsController.deleteOrganizationTaxId
acceptedRoles: [OWNER, ADMIN, MEMBER],
acceptedStatuses: [ACCEPTED],
}),
param("organizationId").exists().trim(),
param("taxId").exists().trim(),
validateRequest,
organizationsController.deleteOrganizationTaxId
); );
router.get( router.get(
"/:organizationId/invoices", "/:organizationId/invoices",
requireAuth({ requireAuth({
acceptedAuthModes: [AuthMode.JWT], acceptedAuthModes: [AuthMode.JWT]
}), }),
requireOrganizationAuth({ organizationsController.getOrganizationInvoices
acceptedRoles: [OWNER, ADMIN, MEMBER],
acceptedStatuses: [ACCEPTED],
}),
param("organizationId").exists().trim(),
validateRequest,
organizationsController.getOrganizationInvoices
); );
router.get( router.get(
"/:organizationId/licenses", "/:organizationId/licenses",
requireAuth({ requireAuth({
acceptedAuthModes: [AuthMode.JWT], acceptedAuthModes: [AuthMode.JWT]
}), }),
requireOrganizationAuth({ organizationsController.getOrganizationLicenses
acceptedRoles: [OWNER, ADMIN],
acceptedStatuses: [ACCEPTED],
}),
param("organizationId").exists().trim(),
validateRequest,
organizationsController.getOrganizationLicenses
); );
export default router; export default router;
+33
View File
@@ -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;
+13 -36
View File
@@ -1,48 +1,25 @@
import express from "express"; import express from "express";
const router = express.Router(); const router = express.Router();
import { import { requireAuth } from "../../../middleware";
requireAuth,
requireSecretAuth,
validateRequest,
} from "../../../middleware";
import { body, param, query } from "express-validator";
import { secretController } from "../../controllers/v1"; import { secretController } from "../../controllers/v1";
import { import {
ADMIN, AuthMode
AuthMode,
MEMBER,
PERMISSION_READ_SECRETS,
PERMISSION_WRITE_SECRETS
} from "../../../variables"; } from "../../../variables";
router.get( router.get(
"/:secretId/secret-versions", "/:secretId/secret-versions",
requireAuth({ requireAuth({
acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY], acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY]
}), }),
requireSecretAuth({ secretController.getSecretVersions
acceptedRoles: [ADMIN, MEMBER],
requiredPermissions: [PERMISSION_READ_SECRETS],
}),
param("secretId").exists().trim(),
query("offset").exists().isInt(),
query("limit").exists().isInt(),
validateRequest,
secretController.getSecretVersions
); );
router.post( router.post(
"/:secretId/secret-versions/rollback", "/:secretId/secret-versions/rollback",
requireAuth({ requireAuth({
acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY], acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY]
}), }),
requireSecretAuth({ secretController.rollbackSecretVersion
acceptedRoles: [ADMIN, MEMBER],
requiredPermissions: [PERMISSION_READ_SECRETS, PERMISSION_WRITE_SECRETS],
}),
param("secretId").exists().trim(),
body("version").exists().isInt(),
secretController.rollbackSecretVersion
); );
export default router; export default router;
+14 -42
View File
@@ -1,81 +1,53 @@
import express from "express"; import express from "express";
const router = express.Router(); const router = express.Router();
import { requireAuth } from "../../../middleware";
import { import {
requireAuth, createInstallationSession,
requireOrganizationAuth, getCurrentOrganizationInstallationStatus,
validateRequest, getRisksForOrganization,
} from "../../../middleware"; linkInstallationToOrganization,
import { body, param } from "express-validator"; updateRisksStatus
import { createInstallationSession, getCurrentOrganizationInstallationStatus, getRisksForOrganization, linkInstallationToOrganization, updateRisksStatus } from "../../../controllers/v1/secretScanningController"; } from "../../../controllers/v1/secretScanningController";
import { ACCEPTED, ADMIN, AuthMode, MEMBER, OWNER } from "../../../variables"; import { AuthMode } from "../../../variables";
router.post( router.post(
"/create-installation-session/organization/:organizationId", "/create-installation-session/organization/:organizationId",
requireAuth({ requireAuth({
acceptedAuthModes: [AuthMode.JWT], acceptedAuthModes: [AuthMode.JWT]
}), }),
param("organizationId").exists().trim(),
requireOrganizationAuth({
acceptedRoles: [OWNER, ADMIN, MEMBER],
acceptedStatuses: [ACCEPTED],
}),
validateRequest,
createInstallationSession createInstallationSession
); );
router.post( router.post(
"/link-installation", "/link-installation",
requireAuth({ requireAuth({
acceptedAuthModes: [AuthMode.JWT], acceptedAuthModes: [AuthMode.JWT]
}), }),
body("installationId").exists().trim(),
body("sessionId").exists().trim(),
validateRequest,
linkInstallationToOrganization linkInstallationToOrganization
); );
router.get( router.get(
"/installation-status/organization/:organizationId", "/installation-status/organization/:organizationId",
requireAuth({ requireAuth({
acceptedAuthModes: [AuthMode.JWT], acceptedAuthModes: [AuthMode.JWT]
}), }),
param("organizationId").exists().trim(),
requireOrganizationAuth({
acceptedRoles: [OWNER, ADMIN, MEMBER],
acceptedStatuses: [ACCEPTED],
}),
validateRequest,
getCurrentOrganizationInstallationStatus getCurrentOrganizationInstallationStatus
); );
router.get( router.get(
"/organization/:organizationId/risks", "/organization/:organizationId/risks",
requireAuth({ requireAuth({
acceptedAuthModes: [AuthMode.JWT], acceptedAuthModes: [AuthMode.JWT]
}), }),
param("organizationId").exists().trim(),
requireOrganizationAuth({
acceptedRoles: [OWNER, ADMIN, MEMBER],
acceptedStatuses: [ACCEPTED],
}),
validateRequest,
getRisksForOrganization getRisksForOrganization
); );
router.post( router.post(
"/organization/:organizationId/risks/:riskId/status", "/organization/:organizationId/risks/:riskId/status",
requireAuth({ 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 updateRisksStatus
); );
export default router; export default router;
+8 -20
View File
@@ -1,27 +1,15 @@
import express from "express"; import express from "express";
const router = express.Router(); const router = express.Router();
import { import { requireAuth } from "../../../middleware";
requireSecretSnapshotAuth, import { AuthMode } from "../../../variables";
} from "../../middleware";
import {
requireAuth,
validateRequest,
} from "../../../middleware";
import { param } from "express-validator";
import { ADMIN, AuthMode, MEMBER } from "../../../variables";
import { secretSnapshotController } from "../../controllers/v1"; import { secretSnapshotController } from "../../controllers/v1";
router.get( router.get(
"/:secretSnapshotId", "/:secretSnapshotId",
requireAuth({ requireAuth({
acceptedAuthModes: [AuthMode.JWT], acceptedAuthModes: [AuthMode.JWT]
}), }),
requireSecretSnapshotAuth({ secretSnapshotController.getSecretSnapshot
acceptedRoles: [ADMIN, MEMBER],
}),
param("secretSnapshotId").exists().trim(),
validateRequest,
secretSnapshotController.getSecretSnapshot
); );
export default router; export default router;
+50 -97
View File
@@ -1,66 +1,49 @@
import express from "express"; import express from "express";
const router = express.Router(); const router = express.Router();
import passport from "passport"; import passport from "passport";
import { import { requireAuth } from "../../../middleware";
AuthProvider
} from "../../models";
import {
requireAuth,
requireOrganizationAuth,
validateRequest,
} from "../../../middleware";
import { body, query } from "express-validator";
import { ssoController } from "../../controllers/v1"; import { ssoController } from "../../controllers/v1";
import { authLimiter } from "../../../helpers/rateLimiter"; import { authLimiter } from "../../../helpers/rateLimiter";
import { import { AuthMode } from "../../../variables";
ACCEPTED,
ADMIN,
AuthMode,
OWNER
} from "../../../variables";
router.get( router.get("/redirect/google", authLimiter, (req, res, next) => {
"/redirect/google", passport.authenticate("google", {
authLimiter, scope: ["profile", "email"],
(req, res, next) => { session: false,
passport.authenticate("google", { ...(req.query.callback_port
scope: ["profile", "email"], ? {
session: false, state: req.query.callback_port as string
...(req.query.callback_port ? { }
state: req.query.callback_port as string : {})
} : {}) })(req, res, next);
})(req, res, next); });
}
);
router.get( router.get(
"/google", "/google",
passport.authenticate("google", { passport.authenticate("google", {
failureRedirect: "/login/provider/error", failureRedirect: "/login/provider/error",
session: false session: false
}), }),
ssoController.redirectSSO ssoController.redirectSSO
); );
router.get( router.get("/redirect/github", authLimiter, (req, res, next) => {
"/redirect/github", passport.authenticate("github", {
authLimiter, session: false,
(req, res, next) => { ...(req.query.callback_port
passport.authenticate("github", { ? {
session: false, state: req.query.callback_port as string
...(req.query.callback_port ? { }
state: req.query.callback_port as string : {})
} : {}) })(req, res, next);
})(req, res, next); });
}
);
router.get( router.get(
"/github", "/github",
authLimiter, authLimiter,
passport.authenticate("github", { passport.authenticate("github", {
failureRedirect: "/login/provider/error", failureRedirect: "/login/provider/error",
session: false session: false
}), }),
ssoController.redirectSSO ssoController.redirectSSO
); );
@@ -102,68 +85,38 @@ router.get(
} }
); );
router.post("/saml2/:ssoIdentifier", router.post(
passport.authenticate("saml", { "/saml2/:ssoIdentifier",
failureRedirect: "/login/provider/error", passport.authenticate("saml", {
failureFlash: true, failureRedirect: "/login/provider/error",
failureFlash: true,
session: false session: false
}), }),
ssoController.redirectSSO ssoController.redirectSSO
); );
router.get( router.get(
"/config", "/config",
requireAuth({ requireAuth({
acceptedAuthModes: [AuthMode.JWT], acceptedAuthModes: [AuthMode.JWT]
}), }),
requireOrganizationAuth({ ssoController.getSSOConfig
acceptedRoles: [OWNER, ADMIN],
acceptedStatuses: [ACCEPTED],
locationOrganizationId: "query"
}),
query("organizationId").exists().trim(),
validateRequest,
ssoController.getSSOConfig
); );
router.post( router.post(
"/config", "/config",
requireAuth({ requireAuth({
acceptedAuthModes: [AuthMode.JWT], acceptedAuthModes: [AuthMode.JWT]
}), }),
requireOrganizationAuth({ ssoController.createSSOConfig
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
); );
router.patch( router.patch(
"/config", "/config",
requireAuth({ requireAuth({
acceptedAuthModes: [AuthMode.JWT], acceptedAuthModes: [AuthMode.JWT]
}), }),
requireOrganizationAuth({ ssoController.updateSSOConfig
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
); );
export default router; export default router;
+12 -109
View File
@@ -1,182 +1,85 @@
import express from "express"; import express from "express";
const router = express.Router(); const router = express.Router();
import { import { requireAuth } from "../../../middleware";
requireAuth, import { AuthMode } from "../../../variables";
requireWorkspaceAuth,
validateRequest,
} from "../../../middleware";
import { body, param, query } from "express-validator";
import {
ADMIN,
AuthMode,
MEMBER
} from "../../../variables";
import { workspaceController } from "../../controllers/v1"; import { workspaceController } from "../../controllers/v1";
import { EventType, UserAgentType } from "../../models";
router.get( router.get(
"/:workspaceId/secret-snapshots", "/:workspaceId/secret-snapshots",
requireAuth({ 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 workspaceController.getWorkspaceSecretSnapshots
); );
router.get( router.get(
"/:workspaceId/secret-snapshots/count", "/:workspaceId/secret-snapshots/count",
requireAuth({ 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 workspaceController.getWorkspaceSecretSnapshotsCount
); );
router.post( router.post(
"/:workspaceId/secret-snapshots/rollback", "/:workspaceId/secret-snapshots/rollback",
requireAuth({ 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 workspaceController.rollbackWorkspaceSecretSnapshot
); );
router.get( router.get(
"/:workspaceId/logs", "/:workspaceId/logs",
requireAuth({ 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 workspaceController.getWorkspaceLogs
); );
router.get( router.get(
"/:workspaceId/audit-logs", "/:workspaceId/audit-logs",
requireAuth({ 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 workspaceController.getWorkspaceAuditLogs
); );
router.get( router.get(
"/:workspaceId/audit-logs/filters/actors", "/:workspaceId/audit-logs/filters/actors",
requireAuth({ 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 workspaceController.getWorkspaceAuditLogActorFilterOpts
); );
router.get( router.get(
"/:workspaceId/trusted-ips", "/:workspaceId/trusted-ips",
param("workspaceId").exists().isString().trim(),
requireAuth({ requireAuth({
acceptedAuthModes: [AuthMode.JWT], acceptedAuthModes: [AuthMode.JWT]
}),
requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER],
locationWorkspaceId: "params",
}), }),
workspaceController.getWorkspaceTrustedIps workspaceController.getWorkspaceTrustedIps
); );
router.post( router.post(
"/:workspaceId/trusted-ips", "/:workspaceId/trusted-ips",
param("workspaceId").exists().isString().trim(),
body("ipAddress").exists().isString().trim(),
body("comment").default("").isString().trim(),
body("isActive").exists().isBoolean(),
validateRequest,
requireAuth({ requireAuth({
acceptedAuthModes: [AuthMode.JWT], acceptedAuthModes: [AuthMode.JWT]
}),
requireWorkspaceAuth({
acceptedRoles: [ADMIN],
locationWorkspaceId: "params",
}), }),
workspaceController.addWorkspaceTrustedIp workspaceController.addWorkspaceTrustedIp
); );
router.patch( router.patch(
"/:workspaceId/trusted-ips/:trustedIpId", "/: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({ requireAuth({
acceptedAuthModes: [AuthMode.JWT], acceptedAuthModes: [AuthMode.JWT]
}),
requireWorkspaceAuth({
acceptedRoles: [ADMIN],
locationWorkspaceId: "params",
}), }),
workspaceController.updateWorkspaceTrustedIp workspaceController.updateWorkspaceTrustedIp
); );
router.delete( router.delete(
"/:workspaceId/trusted-ips/:trustedIpId", "/:workspaceId/trusted-ips/:trustedIpId",
param("workspaceId").exists().isString().trim(),
param("trustedIpId").exists().isString().trim(),
validateRequest,
requireAuth({ requireAuth({
acceptedAuthModes: [AuthMode.JWT], acceptedAuthModes: [AuthMode.JWT]
}),
requireWorkspaceAuth({
acceptedRoles: [ADMIN],
locationWorkspaceId: "params",
}), }),
workspaceController.deleteWorkspaceTrustedIp workspaceController.deleteWorkspaceTrustedIp
); );
@@ -3,7 +3,21 @@ import { mkdir, readFile, rm, writeFile } from "fs";
import { tmpdir } from "os"; import { tmpdir } from "os";
import { join } from "path" import { join } from "path"
import { SecretMatch } from "./types"; import { SecretMatch } from "./types";
import { Octokit } from "@octokit/rest";
export async function scanFullRepoContentAndGetFindings(octokit: any, installationId: number, repositoryFullName: string): Promise<SecretMatch[]> {
const tempFolder = await createTempFolder();
const findingsPath = join(tempFolder, "findings.json");
const repoPath = join(tempFolder, "repo.git")
try {
const { data: { token }} = await octokit.apps.createInstallationAccessToken({installation_id: installationId})
await cloneRepo(token, repositoryFullName, repoPath)
await runInfisicalScanOnRepo(repoPath, findingsPath);
const findingsData = await readFindingsFile(findingsPath);
return JSON.parse(findingsData);
} finally {
await deleteTempFolder(tempFolder);
}
}
export async function scanContentAndGetFindings(textContent: string): Promise<SecretMatch[]> { export async function scanContentAndGetFindings(textContent: string): Promise<SecretMatch[]> {
const tempFolder = await createTempFolder(); const tempFolder = await createTempFolder();
@@ -36,6 +50,8 @@ export function createTempFolder(): Promise<string> {
}); });
} }
export function writeTextToFile(filePath: string, content: string): Promise<void> { export function writeTextToFile(filePath: string, content: string): Promise<void> {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
writeFile(filePath, content, (err) => { writeFile(filePath, content, (err) => {
@@ -48,6 +64,33 @@ export function writeTextToFile(filePath: string, content: string): Promise<void
}); });
} }
export async function cloneRepo(installationAcccessToken: string, repositoryFullName: string, repoPath: string): Promise<void> {
const cloneUrl = `https://x-access-token:${installationAcccessToken}@github.com/${repositoryFullName}.git`;
const command = `git clone ${cloneUrl} ${repoPath} --bare`
return new Promise((resolve, reject) => {
exec(command, (error) => {
if (error) {
reject(error);
} else {
resolve();
}
});
})
}
export function runInfisicalScanOnRepo(repoPath: string, outputPath: string): Promise<void> {
return new Promise((resolve, reject) => {
const command = `cd ${repoPath} && infisical scan --exit-code=77 -r "${outputPath}"`;
exec(command, (error) => {
if (error && error.code != 77) {
reject(error);
} else {
resolve();
}
});
});
}
export function runInfisicalScan(inputPath: string, outputPath: string): Promise<void> { export function runInfisicalScan(inputPath: string, outputPath: string): Promise<void> {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const command = `cat "${inputPath}" | infisical scan --exit-code=77 --pipe -r "${outputPath}"`; const command = `cat "${inputPath}" | infisical scan --exit-code=77 --pipe -r "${outputPath}"`;
@@ -96,30 +139,4 @@ export function convertKeysToLowercase<T>(obj: T): T {
} }
return convertedObj; return convertedObj;
}
export async function getCommits(octokit: Octokit, owner: string, repo: string) {
let commits: { sha: string }[] = [];
let page = 1;
while (true) {
const response = await octokit.repos.listCommits({
owner,
repo,
per_page: 100,
page,
});
commits = commits.concat(response.data);
if (response.data.length == 0) break;
page++;
}
return commits;
}
export async function getFilesFromCommit(octokit: any, owner: string, repo: string, sha: string) {
const response = await octokit.repos.getCommit({
owner,
repo,
ref: sha,
});
} }
@@ -0,0 +1,250 @@
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<string> = {
type: "field",
validate(instruction, value) {
if (typeof value !== "string") {
throw new Error(`"${instruction.name}" expects value to be a string`);
}
}
};
const glob: JsInterpreter<FieldCondition<string>> = (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",
SecretRollback = "secret-rollback"
}
type SubjectFields = {
environment: string;
secretPath: string;
};
export type ProjectPermissionSet =
| [
ProjectPermissionActions,
ProjectPermissionSub.Secrets | (ForcedSubject<ProjectPermissionSub.Secrets> & 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<MongoAbility<ProjectPermissionSet>>(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.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<MongoAbility<ProjectPermissionSet>>(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.SecretRollback);
can(ProjectPermissionActions.Create, ProjectPermissionSub.SecretRollback);
can(ProjectPermissionActions.Read, ProjectPermissionSub.Member);
can(ProjectPermissionActions.Create, ProjectPermissionSub.Member);
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.Role);
can(ProjectPermissionActions.Read, ProjectPermissionSub.AuditLogs);
can(ProjectPermissionActions.Read, ProjectPermissionSub.IpAllowList);
return build({ conditionsMatcher });
};
export const memberProjectPermissions = buildMemberPermission();
const buildViewerPermission = () => {
const { can, build } = new AbilityBuilder<MongoAbility<ProjectPermissionSet>>(createMongoAbility);
can(ProjectPermissionActions.Read, ProjectPermissionSub.Secrets);
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<MongoAbility<ProjectPermissionSet>>[] };
}>("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<ProjectPermissionSet>(membership.customRole.permissions, {
conditionsMatcher
});
return { permission, membership };
}
throw BadRequestError({ message: "User role not found" });
};
+134
View File
@@ -0,0 +1,134 @@
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<MongoAbility<OrgPermissionSet>>(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<MongoAbility<OrgPermissionSet>>(createMongoAbility);
can(OrgPermissionActions.Read, OrgPermissionSubjects.Workspace);
can(OrgPermissionActions.Create, OrgPermissionSubjects.Workspace);
can(OrgPermissionActions.Read, OrgPermissionSubjects.Member);
can(OrgPermissionActions.Create, 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);
can(OrgPermissionActions.Create, OrgPermissionSubjects.SecretScanning);
can(OrgPermissionActions.Edit, OrgPermissionSubjects.SecretScanning);
can(OrgPermissionActions.Delete, 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<MongoAbility<OrgPermissionSet>>[] } }>(
"customRole"
)
.exec();
if (!membership || (membership.role === "custom" && !membership.customRole)) {
throw UnauthorizedRequestError({ message: "User doesn't belong to organization" });
}
if (membership.role === "admin") return { permission: adminPermissions, membership };
if (membership.role === "member") return { permission: memberPermissions, membership };
if (membership.role === "custom") {
const permission = createMongoAbility<OrgPermissionSet>(membership.customRole.permissions, {
conditionsMatcher
});
return { permission, membership };
}
throw BadRequestError({ message: "User role not found" });
};
+68
View File
@@ -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()
})
});
+2 -1
View File
@@ -134,7 +134,8 @@ export const getSecretsBotHelper = async ({
const importedSecrets = await getAllImportedSecrets( const importedSecrets = await getAllImportedSecrets(
workspaceId.toString(), workspaceId.toString(),
environment, environment,
folderId folderId,
() => true // integrations are setup to read all the ones
); );
importedSecrets.forEach(({ secrets }) => { importedSecrets.forEach(({ secrets }) => {
+58 -32
View File
@@ -1,20 +1,24 @@
import { Types } from "mongoose"; import { Types } from "mongoose";
import { Bot, IntegrationAuth } from "../models"; import { Bot, IIntegrationAuth, IntegrationAuth } from "../models";
import { exchangeCode, exchangeRefresh } from "../integrations"; import { exchangeCode, exchangeRefresh } from "../integrations";
import { BotService } from "../services"; import { BotService } from "../services";
import { import {
ALGORITHM_AES_256_GCM, ALGORITHM_AES_256_GCM,
ENCODING_SCHEME_UTF8, ENCODING_SCHEME_UTF8,
INTEGRATION_GCP_SECRET_MANAGER,
INTEGRATION_NETLIFY, INTEGRATION_NETLIFY,
INTEGRATION_VERCEL INTEGRATION_VERCEL,
} from "../variables"; } from "../variables";
import { UnauthorizedRequestError } from "../utils/errors"; import { InternalServerError, UnauthorizedRequestError } from "../utils/errors";
import { IntegrationAuthMetadata } from "../models/integrationAuth/types";
interface Update { interface Update {
workspace: string; workspace: string;
integration: string; integration: string;
url?: string;
teamId?: string; teamId?: string;
accountId?: string; accountId?: string;
metadata?: IntegrationAuthMetadata
} }
/** /**
@@ -33,12 +37,14 @@ export const handleOAuthExchangeHelper = async ({
workspaceId, workspaceId,
integration, integration,
code, code,
environment environment,
url
}: { }: {
workspaceId: string; workspaceId: string;
integration: string; integration: string;
code: string; code: string;
environment: string; environment: string;
url?: string;
}) => { }) => {
const bot = await Bot.findOne({ const bot = await Bot.findOne({
workspace: workspaceId, workspace: workspaceId,
@@ -50,13 +56,18 @@ export const handleOAuthExchangeHelper = async ({
// exchange code for access and refresh tokens // exchange code for access and refresh tokens
const res = await exchangeCode({ const res = await exchangeCode({
integration, integration,
code code,
url
}); });
const update: Update = { const update: Update = {
workspace: workspaceId, workspace: workspaceId,
integration integration
}; };
if (res.url) {
update.url = res.url;
}
switch (integration) { switch (integration) {
case INTEGRATION_VERCEL: case INTEGRATION_VERCEL:
@@ -65,6 +76,11 @@ export const handleOAuthExchangeHelper = async ({
case INTEGRATION_NETLIFY: case INTEGRATION_NETLIFY:
update.accountId = res.accountId; update.accountId = res.accountId;
break; break;
case INTEGRATION_GCP_SECRET_MANAGER:
update.metadata = {
authMethod: "oauth2"
}
break;
} }
const integrationAuth = await IntegrationAuth.findOneAndUpdate( const integrationAuth = await IntegrationAuth.findOneAndUpdate(
@@ -93,7 +109,6 @@ export const handleOAuthExchangeHelper = async ({
// set integration auth access token // set integration auth access token
await setIntegrationAuthAccessHelper({ await setIntegrationAuthAccessHelper({
integrationAuthId: integrationAuth._id.toString(), integrationAuthId: integrationAuth._id.toString(),
accessId: null,
accessToken: res.accessToken, accessToken: res.accessToken,
accessExpiresAt: res.accessExpiresAt accessExpiresAt: res.accessExpiresAt
}); });
@@ -150,7 +165,7 @@ export const getIntegrationAuthAccessHelper = async ({
let accessId; let accessId;
let accessToken; let accessToken;
const integrationAuth = await IntegrationAuth.findById(integrationAuthId).select( 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 metadata teamId url"
); );
if (!integrationAuth) if (!integrationAuth)
@@ -158,22 +173,24 @@ export const getIntegrationAuthAccessHelper = async ({
message: "Failed to locate Integration Authentication credentials" message: "Failed to locate Integration Authentication credentials"
}); });
accessToken = await BotService.decryptSymmetric({ if (integrationAuth.accessCiphertext && integrationAuth.accessIV && integrationAuth.accessTag) {
workspaceId: integrationAuth.workspace, accessToken = await BotService.decryptSymmetric({
ciphertext: integrationAuth.accessCiphertext as string, workspaceId: integrationAuth.workspace,
iv: integrationAuth.accessIV as string, ciphertext: integrationAuth.accessCiphertext as string,
tag: integrationAuth.accessTag as string iv: integrationAuth.accessIV as string,
}); tag: integrationAuth.accessTag as string
});
}
if (integrationAuth?.accessExpiresAt && integrationAuth?.refreshCiphertext) { if (integrationAuth?.refreshCiphertext) {
// there is a access token expiration date // there is a access token expiration date
// and refresh token to exchange with the OAuth2 server // and refresh token to exchange with the OAuth2 server
const refreshToken = await getIntegrationAuthRefreshHelper({
integrationAuthId
});
if (integrationAuth.accessExpiresAt < new Date()) { if (integrationAuth?.accessExpiresAt && integrationAuth.accessExpiresAt < new Date()) {
// access token is expired // access token is expired
const refreshToken = await getIntegrationAuthRefreshHelper({
integrationAuthId
});
accessToken = await exchangeRefresh({ accessToken = await exchangeRefresh({
integrationAuth, integrationAuth,
refreshToken refreshToken
@@ -194,7 +211,10 @@ export const getIntegrationAuthAccessHelper = async ({
}); });
} }
if (!accessToken) throw InternalServerError();
return { return {
integrationAuth,
accessId, accessId,
accessToken accessToken
}; };
@@ -214,7 +234,7 @@ export const setIntegrationAuthRefreshHelper = async ({
}: { }: {
integrationAuthId: string; integrationAuthId: string;
refreshToken: string; refreshToken: string;
}) => { }): Promise<IIntegrationAuth> => {
let integrationAuth = await IntegrationAuth.findById(integrationAuthId); let integrationAuth = await IntegrationAuth.findById(integrationAuthId);
if (!integrationAuth) throw new Error("Failed to find integration auth"); if (!integrationAuth) throw new Error("Failed to find integration auth");
@@ -239,6 +259,8 @@ export const setIntegrationAuthRefreshHelper = async ({
new: true new: true
} }
); );
if (!integrationAuth) throw InternalServerError();
return integrationAuth; return integrationAuth;
}; };
@@ -259,20 +281,24 @@ export const setIntegrationAuthAccessHelper = async ({
accessExpiresAt accessExpiresAt
}: { }: {
integrationAuthId: string; integrationAuthId: string;
accessId: string | null; accessId?: string;
accessToken: string; accessToken?: string;
accessExpiresAt: Date | undefined; accessExpiresAt: Date | undefined;
}) => { }) => {
let integrationAuth = await IntegrationAuth.findById(integrationAuthId); let integrationAuth = await IntegrationAuth.findById(integrationAuthId);
if (!integrationAuth) throw new Error("Failed to find integration auth"); if (!integrationAuth) throw new Error("Failed to find integration auth");
const encryptedAccessTokenObj = await BotService.encryptSymmetric({ let encryptedAccessTokenObj;
workspaceId: integrationAuth.workspace,
plaintext: accessToken
});
let encryptedAccessIdObj; let encryptedAccessIdObj;
if (accessToken) {
encryptedAccessTokenObj = await BotService.encryptSymmetric({
workspaceId: integrationAuth.workspace,
plaintext: accessToken
});
}
if (accessId) { if (accessId) {
encryptedAccessIdObj = await BotService.encryptSymmetric({ encryptedAccessIdObj = await BotService.encryptSymmetric({
workspaceId: integrationAuth.workspace, workspaceId: integrationAuth.workspace,
@@ -286,11 +312,11 @@ export const setIntegrationAuthAccessHelper = async ({
}, },
{ {
accessIdCiphertext: encryptedAccessIdObj?.ciphertext ?? undefined, accessIdCiphertext: encryptedAccessIdObj?.ciphertext ?? undefined,
accessIdIV: encryptedAccessIdObj?.iv ?? undefined, accessIdIV: encryptedAccessIdObj?.iv,
accessIdTag: encryptedAccessIdObj?.tag ?? undefined, accessIdTag: encryptedAccessIdObj?.tag,
accessCiphertext: encryptedAccessTokenObj.ciphertext, accessCiphertext: encryptedAccessTokenObj?.ciphertext,
accessIV: encryptedAccessTokenObj.iv, accessIV: encryptedAccessTokenObj?.iv,
accessTag: encryptedAccessTokenObj.tag, accessTag: encryptedAccessTokenObj?.tag,
accessExpiresAt, accessExpiresAt,
algorithm: ALGORITHM_AES_256_GCM, algorithm: ALGORITHM_AES_256_GCM,
keyEncoding: ENCODING_SCHEME_UTF8 keyEncoding: ENCODING_SCHEME_UTF8
+19 -19
View File
@@ -11,29 +11,29 @@ import { BadRequestError, MembershipNotFoundError } from "../utils/errors";
* @returns {Membership} membership - membership of user with id [userId] for workspace with id [workspaceId] * @returns {Membership} membership - membership of user with id [userId] for workspace with id [workspaceId]
*/ */
export const validateMembership = async ({ export const validateMembership = async ({
userId, userId,
workspaceId, workspaceId,
acceptedRoles, acceptedRoles
}: { }: {
userId: Types.ObjectId | string; userId: Types.ObjectId | string;
workspaceId: Types.ObjectId | string; workspaceId: Types.ObjectId | string;
acceptedRoles?: Array<"admin" | "member">; acceptedRoles?: Array<"admin" | "member" | "custom" | "viewer">;
}) => { }) => {
const membership = await Membership.findOne({ const membership = await Membership.findOne({
user: userId, user: userId,
workspace: workspaceId, workspace: workspaceId
}).populate("workspace"); }).populate("workspace");
if (!membership) { if (!membership) {
throw MembershipNotFoundError({ throw MembershipNotFoundError({
message: "Failed to find workspace membership", message: "Failed to find workspace membership"
}); });
} }
if (acceptedRoles) { if (acceptedRoles) {
if (!acceptedRoles.includes(membership.role)) { if (!acceptedRoles.includes(membership.role)) {
throw BadRequestError({ 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 * @return {Object} membership - membership
*/ */
export const findMembership = async (queryObj: any) => { export const findMembership = async (queryObj: any) => {
const membership = await Membership.findOne(queryObj); const membership = await Membership.findOne(queryObj);
return membership; return membership;
}; };
@@ -60,9 +60,9 @@ export const findMembership = async (queryObj: any) => {
* @param {String[]} obj.roles - roles of users. * @param {String[]} obj.roles - roles of users.
*/ */
export const addMemberships = async ({ export const addMemberships = async ({
userIds, userIds,
workspaceId, workspaceId,
roles, roles
}: { }: {
userIds: string[]; userIds: string[];
workspaceId: string; workspaceId: string;
@@ -74,15 +74,15 @@ export const addMemberships = async ({
filter: { filter: {
user: userId, user: userId,
workspace: workspaceId, workspace: workspaceId,
role: roles[idx], role: roles[idx]
}, },
update: { update: {
user: userId, user: userId,
workspace: workspaceId, workspace: workspaceId,
role: roles[idx], role: roles[idx]
}, },
upsert: true, upsert: true
}, }
}; };
}); });
await Membership.bulkWrite(operations as any); await Membership.bulkWrite(operations as any);
@@ -94,8 +94,8 @@ export const addMemberships = async ({
* @param {String} obj.membershipId - id of membership to delete * @param {String} obj.membershipId - id of membership to delete
*/ */
export const deleteMembership = async ({ membershipId }: { membershipId: string }) => { export const deleteMembership = async ({ membershipId }: { membershipId: string }) => {
const deletedMembership = await Membership.findOneAndDelete({ const deletedMembership = await Membership.findOneAndDelete({
_id: membershipId, _id: membershipId
}); });
// delete keys associated with the membership // delete keys associated with the membership
@@ -103,9 +103,9 @@ export const deleteMembership = async ({ membershipId }: { membershipId: string
// case: membership had a registered user // case: membership had a registered user
await Key.deleteMany({ await Key.deleteMany({
receiver: deletedMembership.user, receiver: deletedMembership.user,
workspace: deletedMembership.workspace, workspace: deletedMembership.workspace
}); });
} }
return deletedMembership; return deletedMembership;
}; };
+60 -68
View File
@@ -1,14 +1,6 @@
import { Types } from "mongoose"; import { Types } from "mongoose";
import { import { Key, Membership, MembershipOrg, Workspace } from "../models";
Key, import { MembershipOrgNotFoundError, UnauthorizedRequestError } from "../utils/errors";
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] * Validate that user with id [userId] is a member of organization with id [organizationId]
@@ -19,39 +11,43 @@ import {
* @param {String[]} obj.acceptedRoles * @param {String[]} obj.acceptedRoles
*/ */
export const validateMembershipOrg = async ({ export const validateMembershipOrg = async ({
userId, userId,
organizationId, organizationId,
acceptedRoles, acceptedRoles,
acceptedStatuses, acceptedStatuses
}: { }: {
userId: Types.ObjectId; userId: Types.ObjectId;
organizationId: Types.ObjectId; organizationId: Types.ObjectId;
acceptedRoles?: Array<"owner" | "admin" | "member">; acceptedRoles?: Array<"owner" | "admin" | "member" | "custom">;
acceptedStatuses?: Array<"invited" | "accepted">; acceptedStatuses?: Array<"invited" | "accepted">;
}) => { }) => {
const membershipOrg = await MembershipOrg.findOne({ const membershipOrg = await MembershipOrg.findOne({
user: userId, user: userId,
organization: organizationId, organization: organizationId
}); });
if (!membershipOrg) { if (!membershipOrg) {
throw MembershipOrgNotFoundError({ message: "Failed to find organization membership" }); throw MembershipOrgNotFoundError({ message: "Failed to find organization membership" });
} }
if (acceptedRoles) { if (acceptedRoles) {
if (!acceptedRoles.includes(membershipOrg.role)) { if (!acceptedRoles.includes(membershipOrg.role)) {
throw UnauthorizedRequestError({ message: "Failed to validate organization membership 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" }); if (acceptedStatuses) {
} if (!acceptedStatuses.includes(membershipOrg.status)) {
} throw UnauthorizedRequestError({
message: "Failed to validate organization membership status"
return membershipOrg; });
} }
}
return membershipOrg;
};
/** /**
* Return organization membership matching criteria specified in * Return organization membership matching criteria specified in
@@ -60,8 +56,8 @@ export const validateMembershipOrg = async ({
* @return {Object} membershipOrg - membership * @return {Object} membershipOrg - membership
*/ */
export const findMembershipOrg = (queryObj: any) => { export const findMembershipOrg = (queryObj: any) => {
const membershipOrg = MembershipOrg.findOne(queryObj); const membershipOrg = MembershipOrg.findOne(queryObj);
return membershipOrg; return membershipOrg;
}; };
/** /**
@@ -73,15 +69,15 @@ export const findMembershipOrg = (queryObj: any) => {
* @param {String[]} obj.roles - roles of users. * @param {String[]} obj.roles - roles of users.
*/ */
export const addMembershipsOrg = async ({ export const addMembershipsOrg = async ({
userIds, userIds,
organizationId, organizationId,
roles, roles,
statuses, statuses
}: { }: {
userIds: string[]; userIds: string[];
organizationId: string; organizationId: string;
roles: string[]; roles: string[];
statuses: string[]; statuses: string[];
}) => { }) => {
const operations = userIds.map((userId, idx) => { const operations = userIds.map((userId, idx) => {
return { return {
@@ -90,16 +86,16 @@ export const addMembershipsOrg = async ({
user: userId, user: userId,
organization: organizationId, organization: organizationId,
role: roles[idx], role: roles[idx],
status: statuses[idx], status: statuses[idx]
}, },
update: { update: {
user: userId, user: userId,
organization: organizationId, organization: organizationId,
role: roles[idx], 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 {Object} obj
* @param {String} obj.membershipOrgId - id of organization membership to delete * @param {String} obj.membershipOrgId - id of organization membership to delete
*/ */
export const deleteMembershipOrg = async ({ export const deleteMembershipOrg = async ({ membershipOrgId }: { membershipOrgId: string }) => {
membershipOrgId,
}: {
membershipOrgId: string;
}) => {
const deletedMembershipOrg = await MembershipOrg.findOneAndDelete({ const deletedMembershipOrg = await MembershipOrg.findOneAndDelete({
_id: membershipOrgId, _id: membershipOrgId
}); });
if (!deletedMembershipOrg) throw new Error("Failed to delete organization membership"); if (!deletedMembershipOrg) throw new Error("Failed to delete organization membership");
@@ -128,24 +120,24 @@ export const deleteMembershipOrg = async ({
const workspaces = ( const workspaces = (
await Workspace.find({ await Workspace.find({
organization: deletedMembershipOrg.organization, organization: deletedMembershipOrg.organization
}) })
).map((w) => w._id.toString()); ).map((w) => w._id.toString());
await Membership.deleteMany({ await Membership.deleteMany({
user: deletedMembershipOrg.user, user: deletedMembershipOrg.user,
workspace: { workspace: {
$in: workspaces, $in: workspaces
}, }
}); });
await Key.deleteMany({ await Key.deleteMany({
receiver: deletedMembershipOrg.user, receiver: deletedMembershipOrg.user,
workspace: { workspace: {
$in: workspaces, $in: workspaces
}, }
}); });
} }
return deletedMembershipOrg; return deletedMembershipOrg;
}; };
+17 -29
View File
@@ -48,6 +48,7 @@ import { getAuthDataPayloadIdObj, getAuthDataPayloadUserObj } from "../utils/aut
import { getFolderByPath, getFolderIdFromServiceToken } from "../services/FolderService"; import { getFolderByPath, getFolderIdFromServiceToken } from "../services/FolderService";
import picomatch from "picomatch"; import picomatch from "picomatch";
import path from "path"; import path from "path";
import { getAnImportedSecret } from "../services/SecretImportService";
export const isValidScope = ( export const isValidScope = (
authPayload: IServiceTokenData, authPayload: IServiceTokenData,
@@ -504,11 +505,6 @@ export const getSecretsHelper = async ({
}: GetSecretsParams) => { }: GetSecretsParams) => {
let secrets: ISecret[] = []; let secrets: ISecret[] = [];
// if using service token filter towards the folderId by secretpath // 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) { if (!folderId) {
folderId = await getFolderIdFromServiceToken(workspaceId, environment, secretPath); folderId = await getFolderIdFromServiceToken(workspaceId, environment, secretPath);
@@ -575,20 +571,22 @@ export const getSecretsHelper = async ({
const postHogClient = await TelemetryService.getPostHogClient(); const postHogClient = await TelemetryService.getPostHogClient();
// reduce the number of events captured // reduce the number of events captured
let shouldRecordK8Event = false let shouldRecordK8Event = false;
if (authData.userAgent == K8_USER_AGENT_NAME) { if (authData.userAgent == K8_USER_AGENT_NAME) {
const randomNumber = Math.random(); const randomNumber = Math.random();
if (randomNumber > 0.9) { if (randomNumber > 0.9) {
shouldRecordK8Event = true shouldRecordK8Event = true;
} }
} }
const numberOfSignupSecrets = (secrets.filter((secret) => secret?.metadata?.source === "signup")).length; const numberOfSignupSecrets = secrets.filter(
const atLeastOneNonSignUpSecret = (secrets.length - numberOfSignupSecrets > 0) (secret) => secret?.metadata?.source === "signup"
).length;
const atLeastOneNonSignUpSecret = secrets.length - numberOfSignupSecrets > 0;
if (postHogClient && atLeastOneNonSignUpSecret) { if (postHogClient && atLeastOneNonSignUpSecret) {
const shouldCapture = authData.userAgent !== K8_USER_AGENT_NAME || shouldRecordK8Event; const shouldCapture = authData.userAgent !== K8_USER_AGENT_NAME || shouldRecordK8Event;
const approximateForNoneCapturedEvents = secrets.length * 10 const approximateForNoneCapturedEvents = secrets.length * 10;
if (shouldCapture) { if (shouldCapture) {
postHogClient.capture({ postHogClient.capture({
@@ -625,19 +623,16 @@ export const getSecretHelper = async ({
environment, environment,
type, type,
authData, authData,
secretPath = "/" secretPath = "/",
include_imports = true
}: GetSecretParams) => { }: GetSecretParams) => {
const secretBlindIndex = await generateSecretBlindIndexHelper({ const secretBlindIndex = await generateSecretBlindIndexHelper({
secretName, secretName,
workspaceId: new Types.ObjectId(workspaceId) workspaceId: new Types.ObjectId(workspaceId)
}); });
let secret: ISecret | null = null; let secret: ISecret | null | undefined = null;
// if using service token filter towards the folderId by secretpath // 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); const folderId = await getFolderIdFromServiceToken(workspaceId, environment, secretPath);
// try getting personal secret first (if exists) // try getting personal secret first (if exists)
@@ -662,6 +657,11 @@ export const getSecretHelper = async ({
}).lean(); }).lean();
} }
if (!secret && include_imports) {
// if still no secret found search in imported secret and retreive
secret = await getAnImportedSecret(secretName, workspaceId.toString(), environment, folderId);
}
if (!secret) throw SecretNotFoundError(); if (!secret) throw SecretNotFoundError();
// (EE) create (audit) log // (EE) create (audit) log
@@ -751,12 +751,6 @@ export const updateSecretHelper = async ({
}); });
let secret: ISecret | null = null; 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); const folderId = await getFolderIdFromServiceToken(workspaceId, environment, secretPath);
if (type === SECRET_SHARED) { if (type === SECRET_SHARED) {
@@ -916,12 +910,6 @@ export const deleteSecretHelper = async ({
workspaceId: new Types.ObjectId(workspaceId) 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); const folderId = await getFolderIdFromServiceToken(workspaceId, environment, secretPath);
let secrets: ISecret[] = []; let secrets: ISecret[] = [];
+30 -36
View File
@@ -1,7 +1,7 @@
import { IUser } from "../models"; import { IUser } from "../models";
import { createOrganization } from "./organization"; import { createOrganization } from "./organization";
import { addMembershipsOrg } from "./membershipOrg"; import { addMembershipsOrg } from "./membershipOrg";
import { ACCEPTED, OWNER } from "../variables"; import { ACCEPTED, ADMIN } from "../variables";
import { sendMail } from "../helpers/nodemailer"; import { sendMail } from "../helpers/nodemailer";
import { TokenService } from "../services"; import { TokenService } from "../services";
import { TOKEN_EMAIL_CONFIRMATION } from "../variables"; import { TOKEN_EMAIL_CONFIRMATION } from "../variables";
@@ -14,10 +14,10 @@ import { TOKEN_EMAIL_CONFIRMATION } from "../variables";
* @returns {Boolean} success - whether or not operation was successful * @returns {Boolean} success - whether or not operation was successful
*/ */
export const sendEmailVerification = async ({ email }: { email: string }) => { export const sendEmailVerification = async ({ email }: { email: string }) => {
const token = await TokenService.createToken({ const token = await TokenService.createToken({
type: TOKEN_EMAIL_CONFIRMATION, type: TOKEN_EMAIL_CONFIRMATION,
email, email
}); });
// send mail // send mail
await sendMail({ await sendMail({
@@ -25,8 +25,8 @@ export const sendEmailVerification = async ({ email }: { email: string }) => {
subjectLine: "Infisical confirmation code", subjectLine: "Infisical confirmation code",
recipients: [email], recipients: [email],
substitutions: { substitutions: {
code: token, code: token
}, }
}); });
}; };
@@ -36,17 +36,11 @@ export const sendEmailVerification = async ({ email }: { email: string }) => {
* @param {String} obj.email - emai * @param {String} obj.email - emai
* @param {String} obj.code - code that was sent to [email] * @param {String} obj.code - code that was sent to [email]
*/ */
export const checkEmailVerification = async ({ export const checkEmailVerification = async ({ email, code }: { email: string; code: string }) => {
email,
code,
}: {
email: string;
code: string;
}) => {
await TokenService.validateToken({ await TokenService.validateToken({
type: TOKEN_EMAIL_CONFIRMATION, type: TOKEN_EMAIL_CONFIRMATION,
email, email,
token: code, token: code
}); });
}; };
@@ -58,27 +52,27 @@ export const checkEmailVerification = async ({
* @param {IUser} obj.user - user who we are initializing for * @param {IUser} obj.user - user who we are initializing for
*/ */
export const initializeDefaultOrg = async ({ export const initializeDefaultOrg = async ({
organizationName, organizationName,
user, user
}: { }: {
organizationName: string; organizationName: string;
user: IUser; user: IUser;
}) => { }) => {
try { try {
// create organization with user as owner and initialize a free // create organization with user as owner and initialize a free
// subscription // subscription
const organization = await createOrganization({ const organization = await createOrganization({
email: user.email, email: user.email,
name: organizationName, name: organizationName
}); });
await addMembershipsOrg({ await addMembershipsOrg({
userIds: [user._id.toString()], userIds: [user._id.toString()],
organizationId: organization._id.toString(), organizationId: organization._id.toString(),
roles: [OWNER], roles: [ADMIN],
statuses: [ACCEPTED], statuses: [ACCEPTED]
}); });
} catch (err) { } catch (err) {
throw new Error(`Failed to initialize default organization and workspace [err=${err}]`); throw new Error(`Failed to initialize default organization and workspace [err=${err}]`);
} }
}; };
+68 -67
View File
@@ -1,7 +1,4 @@
import { import { IUser, User } from "../models";
IUser,
User,
} from "../models";
import { sendMail } from "./nodemailer"; import { sendMail } from "./nodemailer";
/** /**
@@ -12,10 +9,10 @@ import { sendMail } from "./nodemailer";
*/ */
export const setupAccount = async ({ email }: { email: string }) => { export const setupAccount = async ({ email }: { email: string }) => {
const user = await new User({ const user = await new User({
email, email
}).save(); }).save();
return user; return user;
}; };
/** /**
@@ -37,36 +34,36 @@ export const setupAccount = async ({ email }: { email: string }) => {
* @returns {Object} user - the completed user * @returns {Object} user - the completed user
*/ */
export const completeAccount = async ({ export const completeAccount = async ({
userId, userId,
firstName, firstName,
lastName, lastName,
encryptionVersion, encryptionVersion,
protectedKey, protectedKey,
protectedKeyIV, protectedKeyIV,
protectedKeyTag, protectedKeyTag,
publicKey, publicKey,
encryptedPrivateKey, encryptedPrivateKey,
encryptedPrivateKeyIV, encryptedPrivateKeyIV,
encryptedPrivateKeyTag, encryptedPrivateKeyTag,
salt, salt,
verifier, verifier
}: { }: {
userId: string; userId: string;
firstName: string; firstName: string;
lastName: string; lastName?: string;
encryptionVersion: number; encryptionVersion: number;
protectedKey: string; protectedKey: string;
protectedKeyIV: string; protectedKeyIV: string;
protectedKeyTag: string; protectedKeyTag: string;
publicKey: string; publicKey: string;
encryptedPrivateKey: string; encryptedPrivateKey: string;
encryptedPrivateKeyIV: string; encryptedPrivateKeyIV: string;
encryptedPrivateKeyTag: string; encryptedPrivateKeyTag: string;
salt: string; salt: string;
verifier: string; verifier: string;
}) => { }) => {
const options = { const options = {
new: true, new: true
}; };
const user = await User.findByIdAndUpdate( const user = await User.findByIdAndUpdate(
userId, userId,
@@ -82,12 +79,12 @@ export const completeAccount = async ({
iv: encryptedPrivateKeyIV, iv: encryptedPrivateKeyIV,
tag: encryptedPrivateKeyTag, tag: encryptedPrivateKeyTag,
salt, salt,
verifier, verifier
}, },
options options
); );
return user; return user;
}; };
/** /**
@@ -98,38 +95,42 @@ export const completeAccount = async ({
* @param {String} obj.userAgent - login user-agent * @param {String} obj.userAgent - login user-agent
*/ */
export const checkUserDevice = async ({ export const checkUserDevice = async ({
user, user,
ip, ip,
userAgent, userAgent
}: { }: {
user: IUser; user: IUser;
ip: string; ip: string;
userAgent: string; userAgent: string;
}) => { }) => {
const isDeviceSeen = user.devices.some((device) => device.ip === ip && device.userAgent === userAgent); 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();
// send MFA code [code] to [email] if (!isDeviceSeen) {
await sendMail({ // case: unseen login ip detected for user
template: "newDevice.handlebars", // -> notify user about the sign-in from new ip
subjectLine: "Successful login from new device",
recipients: [user.email], user.devices = user.devices.concat([
substitutions: { {
email: user.email, ip: String(ip),
timestamp: new Date().toString(), userAgent
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
}
});
}
};
+17
View File
@@ -0,0 +1,17 @@
import type { Request } from "express";
import { AnyZodObject, ZodError, z } from "zod";
import { BadRequestError } from "../utils/errors";
export async function validateRequest<T extends AnyZodObject>(
schema: T,
req: Request
): Promise<z.infer<T>> {
try {
return schema.parseAsync(req);
} catch (error) {
if (error instanceof ZodError) {
throw BadRequestError({ message: error.message });
}
return BadRequestError({ message: JSON.stringify(error) });
}
}
+7 -4
View File
@@ -24,6 +24,7 @@ import {
secretSnapshot as eeSecretSnapshotRouter, secretSnapshot as eeSecretSnapshotRouter,
users as eeUsersRouter, users as eeUsersRouter,
workspace as eeWorkspaceRouter, workspace as eeWorkspaceRouter,
roles as v1RoleRouter,
secretScanning as v1SecretScanningRouter secretScanning as v1SecretScanningRouter
} from "./ee/routes/v1"; } from "./ee/routes/v1";
import { import {
@@ -37,7 +38,7 @@ import {
membership as v1MembershipRouter, membership as v1MembershipRouter,
organization as v1OrganizationRouter, organization as v1OrganizationRouter,
password as v1PasswordRouter, password as v1PasswordRouter,
secretImport as v1SecretImportRouter, secretImps as v1SecretImpsRouter,
secret as v1SecretRouter, secret as v1SecretRouter,
secretsFolder as v1SecretsFolder, secretsFolder as v1SecretsFolder,
serviceToken as v1ServiceTokenRouter, serviceToken as v1ServiceTokenRouter,
@@ -137,6 +138,7 @@ const main = async () => {
app.use((req, res, next) => { app.use((req, res, next) => {
// default to IP address provided by Cloudflare // default to IP address provided by Cloudflare
// #swagger.ignore = true
const cfIp = req.headers["cf-connecting-ip"]; const cfIp = req.headers["cf-connecting-ip"];
req.realIP = Array.isArray(cfIp) ? cfIp[0] : (cfIp as string) || req.ip; req.realIP = Array.isArray(cfIp) ? cfIp[0] : (cfIp as string) || req.ip;
next(); next();
@@ -152,7 +154,7 @@ const main = async () => {
app.use("/api/v1/sso", eeSSORouter); app.use("/api/v1/sso", eeSSORouter);
app.use("/api/v1/cloud-products", eeCloudProductsRouter); app.use("/api/v1/cloud-products", eeCloudProductsRouter);
// v1 routes (default) // v1 routes
app.use("/api/v1/signup", v1SignupRouter); app.use("/api/v1/signup", v1SignupRouter);
app.use("/api/v1/auth", v1AuthRouter); app.use("/api/v1/auth", v1AuthRouter);
app.use("/api/v1/bot", v1BotRouter); app.use("/api/v1/bot", v1BotRouter);
@@ -161,7 +163,7 @@ const main = async () => {
app.use("/api/v1/organization", v1OrganizationRouter); app.use("/api/v1/organization", v1OrganizationRouter);
app.use("/api/v1/workspace", v1WorkspaceRouter); app.use("/api/v1/workspace", v1WorkspaceRouter);
app.use("/api/v1/membership-org", v1MembershipOrgRouter); 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/key", v1KeyRouter);
app.use("/api/v1/invite-org", v1InviteOrgRouter); app.use("/api/v1/invite-org", v1InviteOrgRouter);
app.use("/api/v1/secret", v1SecretRouter); // deprecate app.use("/api/v1/secret", v1SecretRouter); // deprecate
@@ -172,7 +174,8 @@ const main = async () => {
app.use("/api/v1/folders", v1SecretsFolder); app.use("/api/v1/folders", v1SecretsFolder);
app.use("/api/v1/secret-scanning", v1SecretScanningRouter); app.use("/api/v1/secret-scanning", v1SecretScanningRouter);
app.use("/api/v1/webhooks", v1WebhooksRouter); 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) // v2 routes (improvements)
app.use("/api/v2/signup", v2SignupRouter); app.use("/api/v2/signup", v2SignupRouter);
+17 -5
View File
@@ -120,6 +120,7 @@ const getApps = async ({
break; break;
case INTEGRATION_GITLAB: case INTEGRATION_GITLAB:
apps = await getAppsGitlab({ apps = await getAppsGitlab({
integrationAuth,
accessToken, accessToken,
teamId, teamId,
}); });
@@ -607,6 +608,12 @@ const getAppsLaravelForge = async ({
* @returns {String} apps.name - name of Fly.io apps * @returns {String} apps.name - name of Fly.io apps
*/ */
const getAppsFlyio = async ({ accessToken }: { accessToken: string }) => { const getAppsFlyio = async ({ accessToken }: { accessToken: string }) => {
interface FlyioApp {
id: string;
name: string;
hostname: string;
}
const query = ` const query = `
query($role: String) { query($role: String) {
apps(type: "container", first: 400, role: $role) { apps(type: "container", first: 400, role: $role) {
@@ -619,7 +626,7 @@ const getAppsFlyio = async ({ accessToken }: { accessToken: string }) => {
} }
`; `;
const res = ( const res: FlyioApp[] = (
await standardRequest.post( await standardRequest.post(
INTEGRATION_FLYIO_API_URL, INTEGRATION_FLYIO_API_URL,
{ {
@@ -638,8 +645,9 @@ const getAppsFlyio = async ({ accessToken }: { accessToken: string }) => {
) )
).data.data.apps.nodes; ).data.data.apps.nodes;
const apps = res.map((a: any) => ({ const apps = res.map((a: FlyioApp) => ({
name: a.name, name: a.name,
appId: a.id
})); }));
return apps; return apps;
@@ -736,12 +744,16 @@ const getAppsTerraformCloud = async ({
* @returns {String} apps.name - name of GitLab site * @returns {String} apps.name - name of GitLab site
*/ */
const getAppsGitlab = async ({ const getAppsGitlab = async ({
integrationAuth,
accessToken, accessToken,
teamId, teamId,
}: { }: {
integrationAuth: IIntegrationAuth;
accessToken: string; accessToken: string;
teamId?: string; teamId?: string;
}) => { }) => {
const gitLabApiUrl = integrationAuth.url ? `${integrationAuth.url}/api` : INTEGRATION_GITLAB_API_URL;
const apps: App[] = []; const apps: App[] = [];
let page = 1; let page = 1;
@@ -758,7 +770,7 @@ const getAppsGitlab = async ({
}); });
const { data } = await standardRequest.get( const { data } = await standardRequest.get(
`${INTEGRATION_GITLAB_API_URL}/v4/groups/${teamId}/projects`, `${gitLabApiUrl}/v4/groups/${teamId}/projects`,
{ {
params, params,
headers: { headers: {
@@ -785,7 +797,7 @@ const getAppsGitlab = async ({
// case: fetch projects for individual in GitLab // case: fetch projects for individual in GitLab
const { id } = ( const { id } = (
await standardRequest.get(`${INTEGRATION_GITLAB_API_URL}/v4/user`, { await standardRequest.get(`${gitLabApiUrl}/v4/user`, {
headers: { headers: {
Authorization: `Bearer ${accessToken}`, Authorization: `Bearer ${accessToken}`,
"Accept-Encoding": "application/json", "Accept-Encoding": "application/json",
@@ -800,7 +812,7 @@ const getAppsGitlab = async ({
}); });
const { data } = await standardRequest.get( const { data } = await standardRequest.get(
`${INTEGRATION_GITLAB_API_URL}/v4/users/${id}/projects`, `${gitLabApiUrl}/v4/users/${id}/projects`,
{ {
params, params,
headers: { headers: {
+21 -3
View File
@@ -46,6 +46,14 @@ interface ExchangeCodeAzureResponse {
id_token: string; id_token: string;
} }
interface ExchangeCodeGCPResponse {
access_token: string;
expires_in: number;
refresh_token: string;
scope: string;
token_type: string;
}
interface ExchangeCodeHerokuResponse { interface ExchangeCodeHerokuResponse {
token_type: string; token_type: string;
access_token: string; access_token: string;
@@ -110,9 +118,11 @@ interface ExchangeCodeBitBucketResponse {
const exchangeCode = async ({ const exchangeCode = async ({
integration, integration,
code, code,
url
}: { }: {
integration: string; integration: string;
code: string; code: string;
url?: string;
}) => { }) => {
let obj = {} as any; let obj = {} as any;
@@ -150,6 +160,7 @@ const exchangeCode = async ({
case INTEGRATION_GITLAB: case INTEGRATION_GITLAB:
obj = await exchangeCodeGitlab({ obj = await exchangeCodeGitlab({
code, code,
url
}); });
break; break;
case INTEGRATION_BITBUCKET: case INTEGRATION_BITBUCKET:
@@ -174,7 +185,7 @@ const exchangeCode = async ({
const exchangeCodeGCP = async ({ code }: { code: string }) => { const exchangeCodeGCP = async ({ code }: { code: string }) => {
const accessExpiresAt = new Date(); const accessExpiresAt = new Date();
const res: ExchangeCodeAzureResponse = ( const res: ExchangeCodeGCPResponse = (
await standardRequest.post( await standardRequest.post(
INTEGRATION_GCP_TOKEN_URL, INTEGRATION_GCP_TOKEN_URL,
new URLSearchParams({ new URLSearchParams({
@@ -380,11 +391,17 @@ const exchangeCodeGithub = async ({ code }: { code: string }) => {
* @returns {String} obj2.refreshToken - refresh token for Gitlab API * @returns {String} obj2.refreshToken - refresh token for Gitlab API
* @returns {Date} obj2.accessExpiresAt - date of expiration for access token * @returns {Date} obj2.accessExpiresAt - date of expiration for access token
*/ */
const exchangeCodeGitlab = async ({ code }: { code: string }) => { const exchangeCodeGitlab = async ({
code,
url
}: {
code: string,
url?: string;
}) => {
const accessExpiresAt = new Date(); const accessExpiresAt = new Date();
const res: ExchangeCodeGitlabResponse = ( const res: ExchangeCodeGitlabResponse = (
await standardRequest.post( await standardRequest.post(
INTEGRATION_GITLAB_TOKEN_URL, url ? `${url}/oauth/token` : INTEGRATION_GITLAB_TOKEN_URL,
new URLSearchParams({ new URLSearchParams({
grant_type: "authorization_code", grant_type: "authorization_code",
code: code, code: code,
@@ -406,6 +423,7 @@ const exchangeCodeGitlab = async ({ code }: { code: string }) => {
accessToken: res.access_token, accessToken: res.access_token,
refreshToken: res.refresh_token, refreshToken: res.refresh_token,
accessExpiresAt, accessExpiresAt,
url
}; };
}; };
+108 -7
View File
@@ -1,11 +1,15 @@
import jwt from "jsonwebtoken";
import { standardRequest } from "../config/request"; import { standardRequest } from "../config/request";
import { IIntegrationAuth } from "../models"; import { IIntegrationAuth } from "../models";
import { import {
INTEGRATION_AZURE_KEY_VAULT, INTEGRATION_AZURE_KEY_VAULT,
INTEGRATION_BITBUCKET, INTEGRATION_BITBUCKET,
INTEGRATION_BITBUCKET_TOKEN_URL, INTEGRATION_BITBUCKET_TOKEN_URL,
INTEGRATION_GCP_CLOUD_PLATFORM_SCOPE,
INTEGRATION_GCP_SECRET_MANAGER,
INTEGRATION_GCP_TOKEN_URL,
INTEGRATION_GITLAB, INTEGRATION_GITLAB,
INTEGRATION_HEROKU, INTEGRATION_HEROKU
} from "../variables"; } from "../variables";
import { import {
INTEGRATION_AZURE_TOKEN_URL, INTEGRATION_AZURE_TOKEN_URL,
@@ -16,9 +20,11 @@ import { IntegrationService } from "../services";
import { import {
getClientIdAzure, getClientIdAzure,
getClientIdBitBucket, getClientIdBitBucket,
getClientIdGCPSecretManager,
getClientIdGitLab, getClientIdGitLab,
getClientSecretAzure, getClientSecretAzure,
getClientSecretBitBucket, getClientSecretBitBucket,
getClientSecretGCPSecretManager,
getClientSecretGitLab, getClientSecretGitLab,
getClientSecretHeroku, getClientSecretHeroku,
getSiteURL, getSiteURL,
@@ -59,6 +65,19 @@ interface RefreshTokenBitBucketResponse {
state: string; state: string;
} }
interface ServiceAccountAccessTokenGCPSecretManagerResponse {
access_token: string;
expires_in: number;
token_type: string;
}
interface RefreshTokenGCPSecretManagerResponse {
access_token: string;
expires_in: number;
scope: string;
token_type: string;
}
/** /**
* Return new access token by exchanging refresh token [refreshToken] for integration * Return new access token by exchanging refresh token [refreshToken] for integration
* named [integration] * named [integration]
@@ -93,6 +112,7 @@ const exchangeRefresh = async ({
break; break;
case INTEGRATION_GITLAB: case INTEGRATION_GITLAB:
tokenDetails = await exchangeRefreshGitLab({ tokenDetails = await exchangeRefreshGitLab({
integrationAuth,
refreshToken, refreshToken,
}); });
break; break;
@@ -101,18 +121,23 @@ const exchangeRefresh = async ({
refreshToken, refreshToken,
}); });
break; break;
case INTEGRATION_GCP_SECRET_MANAGER:
tokenDetails = await exchangeRefreshGCPSecretManager({
integrationAuth,
refreshToken,
});
break;
default: default:
throw new Error("Failed to exchange token for incompatible integration"); throw new Error("Failed to exchange token for incompatible integration");
} }
if ( if (
tokenDetails?.accessToken && tokenDetails.accessToken &&
tokenDetails?.refreshToken && tokenDetails.refreshToken &&
tokenDetails?.accessExpiresAt tokenDetails.accessExpiresAt
) { ) {
await IntegrationService.setIntegrationAuthAccess({ await IntegrationService.setIntegrationAuthAccess({
integrationAuthId: integrationAuth._id.toString(), integrationAuthId: integrationAuth._id.toString(),
accessId: null,
accessToken: tokenDetails.accessToken, accessToken: tokenDetails.accessToken,
accessExpiresAt: tokenDetails.accessExpiresAt, accessExpiresAt: tokenDetails.accessExpiresAt,
}); });
@@ -202,17 +227,21 @@ const exchangeRefreshHeroku = async ({
* @returns * @returns
*/ */
const exchangeRefreshGitLab = async ({ const exchangeRefreshGitLab = async ({
integrationAuth,
refreshToken, refreshToken,
}: { }: {
integrationAuth: IIntegrationAuth;
refreshToken: string; refreshToken: string;
}) => { }) => {
const accessExpiresAt = new Date(); const accessExpiresAt = new Date();
const url = integrationAuth.url;
const { const {
data, data,
}: { }: {
data: RefreshTokenGitLabResponse; data: RefreshTokenGitLabResponse;
} = await standardRequest.post( } = await standardRequest.post(
INTEGRATION_GITLAB_TOKEN_URL, url ? `${url}/oauth/token` : INTEGRATION_GITLAB_TOKEN_URL,
new URLSearchParams({ new URLSearchParams({
grant_type: "refresh_token", grant_type: "refresh_token",
refresh_token: refreshToken, refresh_token: refreshToken,
@@ -278,4 +307,76 @@ const exchangeRefreshBitBucket = async ({
}; };
}; };
export { exchangeRefresh }; /**
* Return new access token by exchanging refresh token [refreshToken] for the
* GCP Secret Manager integration
* @param {Object} obj
* @param {String} obj.refreshToken - refresh token to use to get new access token for GCP Secret Manager
* @returns
*/
const exchangeRefreshGCPSecretManager = async ({
integrationAuth,
refreshToken,
}: {
integrationAuth: IIntegrationAuth;
refreshToken: string;
}) => {
const accessExpiresAt = new Date();
if (integrationAuth.metadata?.authMethod === "serviceAccount") {
const serviceAccount = JSON.parse(refreshToken);
const payload = {
iss: serviceAccount.client_email,
aud: serviceAccount.token_uri,
scope: INTEGRATION_GCP_CLOUD_PLATFORM_SCOPE,
iat: Math.floor(Date.now() / 1000),
exp: Math.floor(Date.now() / 1000) + 3600,
};
const token = jwt.sign(payload, serviceAccount.private_key, { algorithm: "RS256" });
const { data }: { data: ServiceAccountAccessTokenGCPSecretManagerResponse } = await standardRequest.post(
INTEGRATION_GCP_TOKEN_URL,
new URLSearchParams({
grant_type: "urn:ietf:params:oauth:grant-type:jwt-bearer",
assertion: token
}).toString(),
{
headers: {
"Content-Type": "application/x-www-form-urlencoded"
}
}
);
accessExpiresAt.setSeconds(accessExpiresAt.getSeconds() + data.expires_in);
return {
accessToken: data.access_token,
refreshToken,
accessExpiresAt
};
}
const { data }: { data: RefreshTokenGCPSecretManagerResponse } = (
await standardRequest.post(
INTEGRATION_GCP_TOKEN_URL,
new URLSearchParams({
client_id: await getClientIdGCPSecretManager(),
client_secret: await getClientSecretGCPSecretManager(),
refresh_token: refreshToken,
grant_type: "refresh_token",
} as any)
)
);
accessExpiresAt.setSeconds(accessExpiresAt.getSeconds() + data.expires_in);
return {
accessToken: data.access_token,
refreshToken,
accessExpiresAt,
};
};
export { exchangeRefresh };
+173 -25
View File
@@ -40,6 +40,8 @@ import {
INTEGRATION_NETLIFY_API_URL, INTEGRATION_NETLIFY_API_URL,
INTEGRATION_NORTHFLANK, INTEGRATION_NORTHFLANK,
INTEGRATION_NORTHFLANK_API_URL, INTEGRATION_NORTHFLANK_API_URL,
INTEGRATION_QOVERY,
INTEGRATION_QOVERY_API_URL,
INTEGRATION_RAILWAY, INTEGRATION_RAILWAY,
INTEGRATION_RAILWAY_API_URL, INTEGRATION_RAILWAY_API_URL,
INTEGRATION_RENDER, INTEGRATION_RENDER,
@@ -156,6 +158,7 @@ const syncSecrets = async ({
break; break;
case INTEGRATION_GITLAB: case INTEGRATION_GITLAB:
await syncSecretsGitLab({ await syncSecretsGitLab({
integrationAuth,
integration, integration,
secrets, secrets,
accessToken accessToken
@@ -218,6 +221,13 @@ const syncSecrets = async ({
accessToken accessToken
}); });
break; break;
case INTEGRATION_QOVERY:
await syncSecretsQovery({
integration,
secrets,
accessToken
});
break;
case INTEGRATION_TERRAFORM_CLOUD: case INTEGRATION_TERRAFORM_CLOUD:
await syncSecretsTerraformCloud({ await syncSecretsTerraformCloud({
integration, integration,
@@ -327,15 +337,19 @@ const syncSecretsGCPSecretManager = async ({
const pageSize = 100; const pageSize = 100;
let pageToken: string | undefined; let pageToken: string | undefined;
let hasMorePages = true; let hasMorePages = true;
const filterParam = integration.metadata.secretGCPLabel
? `?filter=labels.${integration.metadata.secretGCPLabel.labelName}=${integration.metadata.secretGCPLabel.labelValue}`
: "";
while (hasMorePages) { while (hasMorePages) {
const params = new URLSearchParams({ const params = new URLSearchParams({
pageSize: String(pageSize), pageSize: String(pageSize),
...(pageToken ? { pageToken } : {}) ...(pageToken ? { pageToken } : {})
}); });
const res: GCPSMListSecretsRes = (await standardRequest.get( const res: GCPSMListSecretsRes = (await standardRequest.get(
`${INTEGRATION_GCP_SECRET_MANAGER_URL}/v1beta1/projects/${integration.appId}/secrets`, `${INTEGRATION_GCP_SECRET_MANAGER_URL}/v1/projects/${integration.appId}/secrets${filterParam}`,
{ {
params, params,
headers: { headers: {
@@ -346,7 +360,24 @@ const syncSecretsGCPSecretManager = async ({
)).data; )).data;
if (res.secrets) { if (res.secrets) {
gcpSecrets = gcpSecrets.concat(res.secrets); const filteredSecrets = res.secrets?.filter((gcpSecret) => {
const arr = gcpSecret.name.split("/");
const key = arr[arr.length - 1];
let isValid = true;
if (integration.metadata.secretPrefix && !key.startsWith(integration.metadata.secretPrefix)) {
isValid = false;
}
if (integration.metadata.secretSuffix && !key.endsWith(integration.metadata.secretSuffix)) {
isValid = false;
}
return isValid;
});
gcpSecrets = gcpSecrets.concat(filteredSecrets);
} }
if (!res.nextPageToken) { if (!res.nextPageToken) {
@@ -370,7 +401,7 @@ const syncSecretsGCPSecretManager = async ({
const key = arr[arr.length - 1]; const key = arr[arr.length - 1];
const secretLatest: GCPLatestSecretVersionAccess = (await standardRequest.get( const secretLatest: GCPLatestSecretVersionAccess = (await standardRequest.get(
`${INTEGRATION_GCP_SECRET_MANAGER_URL}/v1beta1/projects/${integration.appId}/secrets/${key}/versions/latest:access`, `${INTEGRATION_GCP_SECRET_MANAGER_URL}/v1/projects/${integration.appId}/secrets/${key}/versions/latest:access`,
{ {
headers: { headers: {
Authorization: `Bearer ${accessToken}`, Authorization: `Bearer ${accessToken}`,
@@ -378,6 +409,7 @@ const syncSecretsGCPSecretManager = async ({
} }
} }
)).data; )).data;
res[key] = Buffer.from(secretLatest.payload.data, "base64").toString("utf-8"); res[key] = Buffer.from(secretLatest.payload.data, "base64").toString("utf-8");
} }
@@ -386,11 +418,16 @@ const syncSecretsGCPSecretManager = async ({
if (!(key in res)) { if (!(key in res)) {
// case: create secret // case: create secret
await standardRequest.post( await standardRequest.post(
`${INTEGRATION_GCP_SECRET_MANAGER_URL}/v1beta1/projects/${integration.appId}/secrets`, `${INTEGRATION_GCP_SECRET_MANAGER_URL}/v1/projects/${integration.appId}/secrets`,
{ {
replication: { replication: {
automatic: {} automatic: {}
} },
...(integration.metadata.secretGCPLabel ? {
labels: {
[integration.metadata.secretGCPLabel.labelName]: integration.metadata.secretGCPLabel.labelValue
}
} : {})
}, },
{ {
params: { params: {
@@ -404,7 +441,7 @@ const syncSecretsGCPSecretManager = async ({
); );
await standardRequest.post( await standardRequest.post(
`${INTEGRATION_GCP_SECRET_MANAGER_URL}/v1beta1/projects/${integration.appId}/secrets/${key}:addVersion`, `${INTEGRATION_GCP_SECRET_MANAGER_URL}/v1/projects/${integration.appId}/secrets/${key}:addVersion`,
{ {
payload: { payload: {
data: Buffer.from(secrets[key].value).toString("base64") data: Buffer.from(secrets[key].value).toString("base64")
@@ -424,7 +461,7 @@ const syncSecretsGCPSecretManager = async ({
if (!(key in secrets)) { if (!(key in secrets)) {
// case: delete secret // case: delete secret
await standardRequest.delete( await standardRequest.delete(
`${INTEGRATION_GCP_SECRET_MANAGER_URL}/v1beta1/projects/${integration.appId}/secrets/${key}`, `${INTEGRATION_GCP_SECRET_MANAGER_URL}/v1/projects/${integration.appId}/secrets/${key}`,
{ {
headers: { headers: {
Authorization: `Bearer ${accessToken}`, Authorization: `Bearer ${accessToken}`,
@@ -436,7 +473,7 @@ const syncSecretsGCPSecretManager = async ({
// case: update secret // case: update secret
if (secrets[key].value !== res[key]) { if (secrets[key].value !== res[key]) {
await standardRequest.post( await standardRequest.post(
`${INTEGRATION_GCP_SECRET_MANAGER_URL}/v1beta1/projects/${integration.appId}/secrets/${key}:addVersion`, `${INTEGRATION_GCP_SECRET_MANAGER_URL}/v1/projects/${integration.appId}/secrets/${key}:addVersion`,
{ {
payload: { payload: {
data: Buffer.from(secrets[key].value).toString("base64") data: Buffer.from(secrets[key].value).toString("base64")
@@ -755,12 +792,12 @@ const syncSecretsAWSParameterStore = async ({
}; };
/** /**
* Sync/push [secrets] to AWS secret manager * Sync/push [secrets] to AWS Secrets Manager
* @param {Object} obj * @param {Object} obj
* @param {IIntegration} obj.integration - integration details * @param {IIntegration} obj.integration - integration details
* @param {Object} obj.secrets - secrets to push to integration (object where keys are secret keys and values are secret values) * @param {Object} obj.secrets - secrets to push to integration (object where keys are secret keys and values are secret values)
* @param {String} obj.accessId - access id for AWS secret manager integration * @param {String} obj.accessId - access id for AWS Secrets Manager integration
* @param {String} obj.accessToken - access token for AWS secret manager integration * @param {String} obj.accessToken - access token for AWS Secrets Manager integration
*/ */
const syncSecretsAWSSecretManager = async ({ const syncSecretsAWSSecretManager = async ({
integration, integration,
@@ -913,7 +950,11 @@ const syncSecretsVercel = async ({
? { ? {
teamId: integrationAuth.teamId teamId: integrationAuth.teamId
} }
: {}) : {}),
...(integration?.path
? {
gitBranch: integration?.path
} : {})
}; };
const vercelSecrets: VercelSecret[] = ( const vercelSecrets: VercelSecret[] = (
@@ -932,7 +973,7 @@ const syncSecretsVercel = async ({
if ( if (
integration.targetEnvironment === "preview" && integration.targetEnvironment === "preview" &&
integration.path && secret.gitBranch &&
integration.path !== secret.gitBranch integration.path !== secret.gitBranch
) { ) {
// case: secret on preview environment does not have same target git branch // case: secret on preview environment does not have same target git branch
@@ -941,7 +982,7 @@ const syncSecretsVercel = async ({
return true; return true;
}); });
const res: { [key: string]: VercelSecret } = {}; const res: { [key: string]: VercelSecret } = {};
for await (const vercelSecret of vercelSecrets) { for await (const vercelSecret of vercelSecrets) {
@@ -1813,10 +1854,12 @@ const syncSecretsTravisCI = async ({
* @param {String} obj.accessToken - access token for GitLab integration * @param {String} obj.accessToken - access token for GitLab integration
*/ */
const syncSecretsGitLab = async ({ const syncSecretsGitLab = async ({
integrationAuth,
integration, integration,
secrets, secrets,
accessToken accessToken
}: { }: {
integrationAuth: IIntegrationAuth;
integration: IIntegration; integration: IIntegration;
secrets: Record<string, { value: string; comment?: string }>; secrets: Record<string, { value: string; comment?: string }>;
accessToken: string; accessToken: string;
@@ -1826,9 +1869,10 @@ const syncSecretsGitLab = async ({
value: string; value: string;
environment_scope: string; environment_scope: string;
} }
const gitLabApiUrl = integrationAuth.url ? `${integrationAuth.url}/api` : INTEGRATION_GITLAB_API_URL;
const getAllEnvVariables = async (integrationAppId: string, accessToken: string) => { const getAllEnvVariables = async (integrationAppId: string, accessToken: string) => {
const gitLabApiUrl = `${INTEGRATION_GITLAB_API_URL}/v4/projects/${integrationAppId}/variables`;
const headers = { const headers = {
Authorization: `Bearer ${accessToken}`, Authorization: `Bearer ${accessToken}`,
"Accept-Encoding": "application/json", "Accept-Encoding": "application/json",
@@ -1836,7 +1880,7 @@ const syncSecretsGitLab = async ({
}; };
let allEnvVariables: GitLabSecret[] = []; let allEnvVariables: GitLabSecret[] = [];
let url: string | null = `${gitLabApiUrl}?per_page=100`; let url: string | null = `${gitLabApiUrl}/v4/projects/${integrationAppId}/variables?per_page=100`;
while (url) { while (url) {
const response: any = await standardRequest.get(url, { headers }); const response: any = await standardRequest.get(url, { headers });
@@ -1856,15 +1900,29 @@ const syncSecretsGitLab = async ({
}; };
const allEnvVariables = await getAllEnvVariables(integration?.appId, accessToken); const allEnvVariables = await getAllEnvVariables(integration?.appId, accessToken);
const getSecretsRes: GitLabSecret[] = allEnvVariables.filter( const getSecretsRes: GitLabSecret[] = allEnvVariables
(secret: GitLabSecret) => secret.environment_scope === integration.targetEnvironment .filter(
); (secret: GitLabSecret) => secret.environment_scope === integration.targetEnvironment
)
.filter((gitLabSecret) => {
let isValid = true;
if (integration.metadata.secretPrefix && !gitLabSecret.key.startsWith(integration.metadata.secretPrefix)) {
isValid = false;
}
if (integration.metadata.secretSuffix && !gitLabSecret.key.endsWith(integration.metadata.secretSuffix)) {
isValid = false;
}
return isValid;
});
for await (const key of Object.keys(secrets)) { for await (const key of Object.keys(secrets)) {
const existingSecret = getSecretsRes.find((s: any) => s.key == key); const existingSecret = getSecretsRes.find((s: any) => s.key == key);
if (!existingSecret) { if (!existingSecret) {
await standardRequest.post( await standardRequest.post(
`${INTEGRATION_GITLAB_API_URL}/v4/projects/${integration?.appId}/variables`, `${gitLabApiUrl}/v4/projects/${integration?.appId}/variables`,
{ {
key: key, key: key,
value: secrets[key].value, value: secrets[key].value,
@@ -1885,7 +1943,7 @@ const syncSecretsGitLab = async ({
// update secret // update secret
if (secrets[key].value !== existingSecret.value) { if (secrets[key].value !== existingSecret.value) {
await standardRequest.put( await standardRequest.put(
`${INTEGRATION_GITLAB_API_URL}/v4/projects/${integration?.appId}/variables/${existingSecret.key}?filter[environment_scope]=${integration.targetEnvironment}`, `${gitLabApiUrl}/v4/projects/${integration?.appId}/variables/${existingSecret.key}?filter[environment_scope]=${integration.targetEnvironment}`,
{ {
...existingSecret, ...existingSecret,
value: secrets[existingSecret.key].value value: secrets[existingSecret.key].value
@@ -1906,7 +1964,7 @@ const syncSecretsGitLab = async ({
for await (const sec of getSecretsRes) { for await (const sec of getSecretsRes) {
if (!(sec.key in secrets)) { if (!(sec.key in secrets)) {
await standardRequest.delete( await standardRequest.delete(
`${INTEGRATION_GITLAB_API_URL}/v4/projects/${integration?.appId}/variables/${sec.key}?filter[environment_scope]=${integration.targetEnvironment}`, `${gitLabApiUrl}/v4/projects/${integration?.appId}/variables/${sec.key}?filter[environment_scope]=${integration.targetEnvironment}`,
{ {
headers: { headers: {
Authorization: `Bearer ${accessToken}` Authorization: `Bearer ${accessToken}`
@@ -2008,7 +2066,6 @@ const syncSecretsCheckly = async ({
secrets: Record<string, { value: string; comment?: string }>; secrets: Record<string, { value: string; comment?: string }>;
accessToken: string; accessToken: string;
}) => { }) => {
const getSecretsRes = ( const getSecretsRes = (
await standardRequest.get(`${INTEGRATION_CHECKLY_API_URL}/v1/variables`, { await standardRequest.get(`${INTEGRATION_CHECKLY_API_URL}/v1/variables`, {
headers: { headers: {
@@ -2082,6 +2139,97 @@ const syncSecretsCheckly = async ({
} }
}; };
/**
* Sync/push [secrets] to Qovery app
* @param {Object} obj
* @param {IIntegration} obj.integration - integration details
* @param {Object} obj.secrets - secrets to push to integration (object where keys are secret keys and values are secret values)
* @param {String} obj.accessToken - access token for Qovery integration
*/
const syncSecretsQovery = async ({
integration,
secrets,
accessToken
}: {
integration: IIntegration;
secrets: Record<string, { value: string; comment?: string }>;
accessToken: string;
}) => {
const getSecretsRes = (
await standardRequest.get(`${INTEGRATION_QOVERY_API_URL}/${integration.scope}/${integration.appId}/environmentVariable`, {
headers: {
Authorization: `Token ${accessToken}`,
"Accept-Encoding": "application/json"
}
})
).data.results.reduce(
(obj: any, secret: any) => ({
...obj,
[secret.key]: {"id": secret.id, "value": secret.value}
}),
{}
);
// add secrets
for await (const key of Object.keys(secrets)) {
if (!(key in getSecretsRes)) {
// case: secret does not exist in qovery
// -> add secret
await standardRequest.post(
`${INTEGRATION_QOVERY_API_URL}/${integration.scope}/${integration.appId}/environmentVariable`,
{
key,
value: secrets[key].value
},
{
headers: {
Authorization: `Token ${accessToken}`,
Accept: "application/json",
"Content-Type": "application/json"
}
}
);
} else {
// case: secret exists in qovery
// -> update/set secret
if (secrets[key].value !== getSecretsRes[key].value) {
await standardRequest.put(
`${INTEGRATION_QOVERY_API_URL}/${integration.scope}/${integration.appId}/environmentVariable/${getSecretsRes[key].id}`,
{
key,
value: secrets[key].value
},
{
headers: {
Authorization: `Token ${accessToken}`,
"Content-Type": "application/json",
Accept: "application/json"
}
}
);
}
}
}
// This one is dangerous because there might be a lot of qovery-specific secrets
// for await (const key of Object.keys(getSecretsRes)) {
// if (!(key in secrets)) {
// console.log(3)
// // delete secret
// await standardRequest.delete(`${INTEGRATION_QOVERY_API_URL}/application/${integration.appId}/environmentVariable/${getSecretsRes[key].id}`, {
// headers: {
// Authorization: `Token ${accessToken}`,
// Accept: "application/json",
// "X-Qovery-Account": integration.appId
// }
// });
// }
// }
};
/** /**
* Sync/push [secrets] to Terraform Cloud project with id [integration.appId] * Sync/push [secrets] to Terraform Cloud project with id [integration.appId]
* @param {Object} obj * @param {Object} obj
@@ -2886,4 +3034,4 @@ const syncSecretsNorthflank = async ({
); );
}; };
export { syncSecrets }; export { syncSecrets };
+6 -1
View File
@@ -34,6 +34,7 @@ const getTeams = async ({
switch (integrationAuth.integration) { switch (integrationAuth.integration) {
case INTEGRATION_GITLAB: case INTEGRATION_GITLAB:
teams = await getTeamsGitLab({ teams = await getTeamsGitLab({
integrationAuth,
accessToken, accessToken,
}); });
break; break;
@@ -51,13 +52,17 @@ const getTeams = async ({
* @returns {String} teams.teamId - id of team * @returns {String} teams.teamId - id of team
*/ */
const getTeamsGitLab = async ({ const getTeamsGitLab = async ({
integrationAuth,
accessToken, accessToken,
}: { }: {
integrationAuth: IIntegrationAuth;
accessToken: string; accessToken: string;
}) => { }) => {
const gitLabApiUrl = integrationAuth.url ? `${integrationAuth.url}/api` : INTEGRATION_GITLAB_API_URL;
let teams: Team[] = []; let teams: Team[] = [];
const res = (await standardRequest.get( const res = (await standardRequest.get(
`${INTEGRATION_GITLAB_API_URL}/v4/groups`, `${gitLabApiUrl}/v4/groups`,
{ {
headers: { headers: {
Authorization: `Bearer ${accessToken}`, Authorization: `Bearer ${accessToken}`,
@@ -19,7 +19,7 @@ export interface CreateSecretParams {
secretPath: string; secretPath: string;
metadata?: { metadata?: {
source?: string; source?: string;
} };
} }
export interface GetSecretsParams { export interface GetSecretsParams {
@@ -37,6 +37,7 @@ export interface GetSecretParams {
environment: string; environment: string;
type?: "shared" | "personal"; type?: "shared" | "personal";
authData: AuthData; authData: AuthData;
include_imports?: boolean;
} }
export interface UpdateSecretParams { export interface UpdateSecretParams {
+6
View File
@@ -14,6 +14,9 @@ import requireServiceAccountAuth from "./requireServiceAccountAuth";
import requireServiceAccountWorkspacePermissionAuth from "./requireServiceAccountWorkspacePermissionAuth"; import requireServiceAccountWorkspacePermissionAuth from "./requireServiceAccountWorkspacePermissionAuth";
import requireSecretAuth from "./requireSecretAuth"; import requireSecretAuth from "./requireSecretAuth";
import requireSecretsAuth from "./requireSecretsAuth"; import requireSecretsAuth from "./requireSecretsAuth";
import requireBlindIndicesEnabled from "./requireBlindIndicesEnabled";
import requireE2EEOff from "./requireE2EEOff";
import requireIPAllowlistCheck from "./requireIPAllowlistCheck";
import validateRequest from "./validateRequest"; import validateRequest from "./validateRequest";
export { export {
@@ -33,5 +36,8 @@ export {
requireServiceAccountWorkspacePermissionAuth, requireServiceAccountWorkspacePermissionAuth,
requireSecretAuth, requireSecretAuth,
requireSecretsAuth, requireSecretsAuth,
requireBlindIndicesEnabled,
requireE2EEOff,
requireIPAllowlistCheck,
validateRequest, validateRequest,
}; };
@@ -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;
+31
View File
@@ -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;
@@ -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;
+2 -11
View File
@@ -9,24 +9,18 @@ type req = "params" | "body" | "query";
* on request params. * on request params.
* @param {Object} obj * @param {Object} obj
* @param {String[]} obj.acceptedRoles - accepted workspace roles for JWT auth * @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 = ({ const requireWorkspaceAuth = ({
acceptedRoles, acceptedRoles,
locationWorkspaceId, locationWorkspaceId,
locationEnvironment = undefined, locationEnvironment = undefined,
requiredPermissions = [], requiredPermissions = [],
requireBlindIndicesEnabled = false,
requireE2EEOff = false,
checkIPAllowlist = false
}: { }: {
acceptedRoles: Array<"admin" | "member">; acceptedRoles: Array<"admin" | "member">;
locationWorkspaceId: req; locationWorkspaceId: req;
locationEnvironment?: req | undefined; locationEnvironment?: req | undefined;
requiredPermissions?: string[]; requiredPermissions?: string[];
requireBlindIndicesEnabled?: boolean;
requireE2EEOff?: boolean;
checkIPAllowlist?: boolean;
}) => { }) => {
return async (req: Request, res: Response, next: NextFunction) => { return async (req: Request, res: Response, next: NextFunction) => {
const workspaceId = req[locationWorkspaceId]?.workspaceId; const workspaceId = req[locationWorkspaceId]?.workspaceId;
@@ -38,10 +32,7 @@ const requireWorkspaceAuth = ({
workspaceId: new Types.ObjectId(workspaceId), workspaceId: new Types.ObjectId(workspaceId),
environment, environment,
acceptedRoles, acceptedRoles,
requiredPermissions, requiredPermissions
requireBlindIndicesEnabled,
requireE2EEOff,
checkIPAllowlist
}); });
if (membership) { if (membership) {
+13 -1
View File
@@ -18,6 +18,7 @@ import {
INTEGRATION_LARAVELFORGE, INTEGRATION_LARAVELFORGE,
INTEGRATION_NETLIFY, INTEGRATION_NETLIFY,
INTEGRATION_NORTHFLANK, INTEGRATION_NORTHFLANK,
INTEGRATION_QOVERY,
INTEGRATION_RAILWAY, INTEGRATION_RAILWAY,
INTEGRATION_RENDER, INTEGRATION_RENDER,
INTEGRATION_SUPABASE, INTEGRATION_SUPABASE,
@@ -45,6 +46,7 @@ export interface IIntegration {
targetServiceId: string; targetServiceId: string;
path: string; path: string;
region: string; region: string;
scope: string;
secretPath: string; secretPath: string;
integration: integration:
| "azure-key-vault" | "azure-key-vault"
@@ -63,6 +65,7 @@ export interface IIntegration {
| "travisci" | "travisci"
| "supabase" | "supabase"
| "checkly" | "checkly"
| "qovery"
| "terraform-cloud" | "terraform-cloud"
| "teamcity" | "teamcity"
| "hashicorp-vault" | "hashicorp-vault"
@@ -119,11 +122,13 @@ const integrationSchema = new Schema<IIntegration>(
}, },
targetService: { targetService: {
// railway-specific service // railway-specific service
// qovery-specific project
type: String, type: String,
default: null, default: null,
}, },
targetServiceId: { targetServiceId: {
// railway-specific service // railway-specific service
// qovery specific project
type: String, type: String,
default: null, default: null,
}, },
@@ -143,6 +148,11 @@ const integrationSchema = new Schema<IIntegration>(
type: String, type: String,
default: null, default: null,
}, },
scope: {
// qovery-specific scope
type: String,
default: null
},
integration: { integration: {
type: String, type: String,
enum: [ enum: [
@@ -162,6 +172,7 @@ const integrationSchema = new Schema<IIntegration>(
INTEGRATION_TRAVISCI, INTEGRATION_TRAVISCI,
INTEGRATION_SUPABASE, INTEGRATION_SUPABASE,
INTEGRATION_CHECKLY, INTEGRATION_CHECKLY,
INTEGRATION_QOVERY,
INTEGRATION_TERRAFORM_CLOUD, INTEGRATION_TERRAFORM_CLOUD,
INTEGRATION_TEAMCITY, INTEGRATION_TEAMCITY,
INTEGRATION_HASHICORP_VAULT, INTEGRATION_HASHICORP_VAULT,
@@ -187,7 +198,8 @@ const integrationSchema = new Schema<IIntegration>(
default: "/", default: "/",
}, },
metadata: { metadata: {
type: Schema.Types.Mixed type: Schema.Types.Mixed,
default: {}
} }
}, },
{ {
+5
View File
@@ -1,3 +1,8 @@
export type Metadata = { export type Metadata = {
secretPrefix?: string;
secretSuffix?: string; secretSuffix?: string;
secretGCPLabel?: {
labelName: string;
labelValue: string;
}
} }
-199
View File
@@ -1,199 +0,0 @@
import {
ALGORITHM_AES_256_GCM,
ENCODING_SCHEME_BASE64,
ENCODING_SCHEME_UTF8,
INTEGRATION_AWS_PARAMETER_STORE,
INTEGRATION_AWS_SECRET_MANAGER,
INTEGRATION_AZURE_KEY_VAULT,
INTEGRATION_BITBUCKET,
INTEGRATION_CIRCLECI,
INTEGRATION_CLOUDFLARE_PAGES,
INTEGRATION_CLOUD_66,
INTEGRATION_CODEFRESH,
INTEGRATION_DIGITAL_OCEAN_APP_PLATFORM,
INTEGRATION_FLYIO,
INTEGRATION_GCP_SECRET_MANAGER,
INTEGRATION_GITHUB,
INTEGRATION_GITLAB,
INTEGRATION_HASHICORP_VAULT,
INTEGRATION_HEROKU,
INTEGRATION_LARAVELFORGE,
INTEGRATION_NETLIFY,
INTEGRATION_NORTHFLANK,
INTEGRATION_RAILWAY,
INTEGRATION_RENDER,
INTEGRATION_SUPABASE,
INTEGRATION_TEAMCITY,
INTEGRATION_TERRAFORM_CLOUD,
INTEGRATION_TRAVISCI,
INTEGRATION_VERCEL,
INTEGRATION_WINDMILL
} from "../variables";
import { Document, Schema, Types, model } from "mongoose";
export interface IIntegrationAuth extends Document {
_id: Types.ObjectId;
workspace: Types.ObjectId;
integration:
| "heroku"
| "vercel"
| "netlify"
| "github"
| "gitlab"
| "render"
| "railway"
| "flyio"
| "azure-key-vault"
| "laravel-forge"
| "circleci"
| "travisci"
| "supabase"
| "aws-parameter-store"
| "aws-secret-manager"
| "checkly"
| "cloudflare-pages"
| "codefresh"
| "digital-ocean-app-platform"
| "bitbucket"
| "cloud-66"
| "terraform-cloud"
| "teamcity"
| "northflank"
| "windmill"
| "gcp-secret-manager";
teamId: string;
accountId: string;
url: string;
namespace: string;
refreshCiphertext?: string;
refreshIV?: string;
refreshTag?: string;
accessIdCiphertext?: string;
accessIdIV?: string;
accessIdTag?: string;
accessCiphertext?: string;
accessIV?: string;
accessTag?: string;
algorithm?: "aes-256-gcm";
keyEncoding?: "utf8" | "base64";
accessExpiresAt?: Date;
}
const integrationAuthSchema = new Schema<IIntegrationAuth>(
{
workspace: {
type: Schema.Types.ObjectId,
ref: "Workspace",
required: true,
},
integration: {
type: String,
enum: [
INTEGRATION_AZURE_KEY_VAULT,
INTEGRATION_AWS_PARAMETER_STORE,
INTEGRATION_AWS_SECRET_MANAGER,
INTEGRATION_HEROKU,
INTEGRATION_VERCEL,
INTEGRATION_NETLIFY,
INTEGRATION_GITHUB,
INTEGRATION_GITLAB,
INTEGRATION_RENDER,
INTEGRATION_RAILWAY,
INTEGRATION_FLYIO,
INTEGRATION_CIRCLECI,
INTEGRATION_LARAVELFORGE,
INTEGRATION_TRAVISCI,
INTEGRATION_TEAMCITY,
INTEGRATION_SUPABASE,
INTEGRATION_TERRAFORM_CLOUD,
INTEGRATION_HASHICORP_VAULT,
INTEGRATION_CLOUDFLARE_PAGES,
INTEGRATION_CODEFRESH,
INTEGRATION_WINDMILL,
INTEGRATION_BITBUCKET,
INTEGRATION_DIGITAL_OCEAN_APP_PLATFORM,
INTEGRATION_CLOUD_66,
INTEGRATION_NORTHFLANK,
INTEGRATION_GCP_SECRET_MANAGER
],
required: true,
},
teamId: {
// vercel-specific integration param
type: String,
},
url: {
// for any self-hosted integrations (e.g. self-hosted hashicorp-vault)
type: String,
},
namespace: {
// hashicorp-vault-specific integration param
type: String,
},
accountId: {
// netlify-specific integration param
type: String,
},
refreshCiphertext: {
type: String,
select: false,
},
refreshIV: {
type: String,
select: false,
},
refreshTag: {
type: String,
select: false,
},
accessIdCiphertext: {
type: String,
select: false,
},
accessIdIV: {
type: String,
select: false,
},
accessIdTag: {
type: String,
select: false,
},
accessCiphertext: {
type: String,
select: false,
},
accessIV: {
type: String,
select: false,
},
accessTag: {
type: String,
select: false,
},
accessExpiresAt: {
type: Date,
select: false,
},
algorithm: { // the encryption algorithm used
type: String,
enum: [ALGORITHM_AES_256_GCM],
required: true,
},
keyEncoding: {
type: String,
enum: [
ENCODING_SCHEME_UTF8,
ENCODING_SCHEME_BASE64,
],
required: true,
},
},
{
timestamps: true,
}
);
export const IntegrationAuth = model<IIntegrationAuth>(
"IntegrationAuth",
integrationAuthSchema
);
@@ -0,0 +1 @@
export * from "./integrationAuth";
@@ -0,0 +1,205 @@
import {
ALGORITHM_AES_256_GCM,
ENCODING_SCHEME_BASE64,
ENCODING_SCHEME_UTF8,
INTEGRATION_AWS_PARAMETER_STORE,
INTEGRATION_AWS_SECRET_MANAGER,
INTEGRATION_AZURE_KEY_VAULT,
INTEGRATION_BITBUCKET,
INTEGRATION_CIRCLECI,
INTEGRATION_CLOUDFLARE_PAGES,
INTEGRATION_CLOUD_66,
INTEGRATION_CODEFRESH,
INTEGRATION_DIGITAL_OCEAN_APP_PLATFORM,
INTEGRATION_FLYIO,
INTEGRATION_GCP_SECRET_MANAGER,
INTEGRATION_GITHUB,
INTEGRATION_GITLAB,
INTEGRATION_HASHICORP_VAULT,
INTEGRATION_HEROKU,
INTEGRATION_LARAVELFORGE,
INTEGRATION_NETLIFY,
INTEGRATION_NORTHFLANK,
INTEGRATION_RAILWAY,
INTEGRATION_RENDER,
INTEGRATION_SUPABASE,
INTEGRATION_TEAMCITY,
INTEGRATION_TERRAFORM_CLOUD,
INTEGRATION_TRAVISCI,
INTEGRATION_VERCEL,
INTEGRATION_WINDMILL
} from "../../variables";
import { Document, Schema, Types, model } from "mongoose";
import { IntegrationAuthMetadata } from "./types";
export interface IIntegrationAuth extends Document {
_id: Types.ObjectId;
workspace: Types.ObjectId;
integration:
| "heroku"
| "vercel"
| "netlify"
| "github"
| "gitlab"
| "render"
| "railway"
| "flyio"
| "azure-key-vault"
| "laravel-forge"
| "circleci"
| "travisci"
| "supabase"
| "aws-parameter-store"
| "aws-secret-manager"
| "checkly"
| "qovery"
| "cloudflare-pages"
| "codefresh"
| "digital-ocean-app-platform"
| "bitbucket"
| "cloud-66"
| "terraform-cloud"
| "teamcity"
| "northflank"
| "windmill"
| "gcp-secret-manager";
teamId: string;
accountId: string;
url: string;
namespace: string;
refreshCiphertext?: string;
refreshIV?: string;
refreshTag?: string;
accessIdCiphertext?: string;
accessIdIV?: string;
accessIdTag?: string;
accessCiphertext?: string;
accessIV?: string;
accessTag?: string;
algorithm?: "aes-256-gcm";
keyEncoding?: "utf8" | "base64";
accessExpiresAt?: Date;
metadata?: IntegrationAuthMetadata;
}
const integrationAuthSchema = new Schema<IIntegrationAuth>(
{
workspace: {
type: Schema.Types.ObjectId,
ref: "Workspace",
required: true,
},
integration: {
type: String,
enum: [
INTEGRATION_AZURE_KEY_VAULT,
INTEGRATION_AWS_PARAMETER_STORE,
INTEGRATION_AWS_SECRET_MANAGER,
INTEGRATION_HEROKU,
INTEGRATION_VERCEL,
INTEGRATION_NETLIFY,
INTEGRATION_GITHUB,
INTEGRATION_GITLAB,
INTEGRATION_RENDER,
INTEGRATION_RAILWAY,
INTEGRATION_FLYIO,
INTEGRATION_CIRCLECI,
INTEGRATION_LARAVELFORGE,
INTEGRATION_TRAVISCI,
INTEGRATION_TEAMCITY,
INTEGRATION_SUPABASE,
INTEGRATION_TERRAFORM_CLOUD,
INTEGRATION_HASHICORP_VAULT,
INTEGRATION_CLOUDFLARE_PAGES,
INTEGRATION_CODEFRESH,
INTEGRATION_WINDMILL,
INTEGRATION_BITBUCKET,
INTEGRATION_DIGITAL_OCEAN_APP_PLATFORM,
INTEGRATION_CLOUD_66,
INTEGRATION_NORTHFLANK,
INTEGRATION_GCP_SECRET_MANAGER
],
required: true,
},
teamId: {
// vercel-specific integration param
type: String,
},
url: {
// for any self-hosted integrations (e.g. self-hosted hashicorp-vault)
type: String,
},
namespace: {
// hashicorp-vault-specific integration param
type: String,
},
accountId: {
// netlify-specific integration param
type: String,
},
refreshCiphertext: {
type: String,
select: false,
},
refreshIV: {
type: String,
select: false,
},
refreshTag: {
type: String,
select: false,
},
accessIdCiphertext: {
type: String,
select: false,
},
accessIdIV: {
type: String,
select: false,
},
accessIdTag: {
type: String,
select: false,
},
accessCiphertext: {
type: String,
select: false,
},
accessIV: {
type: String,
select: false,
},
accessTag: {
type: String,
select: false,
},
accessExpiresAt: {
type: Date,
select: false,
},
algorithm: { // the encryption algorithm used
type: String,
enum: [ALGORITHM_AES_256_GCM],
required: true,
},
keyEncoding: {
type: String,
enum: [
ENCODING_SCHEME_UTF8,
ENCODING_SCHEME_BASE64,
],
required: true,
},
metadata: {
type: Schema.Types.Mixed
}
},
{
timestamps: true,
}
);
export const IntegrationAuth = model<IIntegrationAuth>(
"IntegrationAuth",
integrationAuthSchema
);
@@ -0,0 +1,5 @@
interface GCPIntegrationAuthMetadata {
authMethod: "oauth2" | "serviceAccount"
}
export type IntegrationAuthMetadata = GCPIntegrationAuthMetadata;
+48 -43
View File
@@ -1,55 +1,60 @@
import { Schema, Types, model } from "mongoose"; import { Schema, Types, model } from "mongoose";
import { ADMIN, MEMBER } from "../variables"; import { ADMIN, CUSTOM, MEMBER, VIEWER } from "../variables";
export interface IMembershipPermission { export interface IMembershipPermission {
environmentSlug: string, environmentSlug: string;
ability: string ability: string;
} }
export interface IMembership { export interface IMembership {
_id: Types.ObjectId; _id: Types.ObjectId;
user: Types.ObjectId; user: Types.ObjectId;
inviteEmail?: string; inviteEmail?: string;
workspace: Types.ObjectId; workspace: Types.ObjectId;
role: "admin" | "member"; role: "admin" | "member" | "viewer" | "custom";
deniedPermissions: IMembershipPermission[] customRole: Types.ObjectId;
deniedPermissions: IMembershipPermission[];
} }
const membershipSchema = new Schema<IMembership>( const membershipSchema = new Schema<IMembership>(
{ {
user: { user: {
type: Schema.Types.ObjectId, type: Schema.Types.ObjectId,
ref: "User", ref: "User"
}, },
inviteEmail: { inviteEmail: {
type: String, type: String
}, },
workspace: { workspace: {
type: Schema.Types.ObjectId, type: Schema.Types.ObjectId,
ref: "Workspace", ref: "Workspace",
required: true, required: true
}, },
deniedPermissions: { deniedPermissions: {
type: [ type: [
{ {
environmentSlug: String, environmentSlug: String,
ability: { ability: {
type: String, type: String,
enum: ["read", "write"], enum: ["read", "write"]
}, }
}, }
], ],
default: [], default: []
}, },
role: { role: {
type: String, type: String,
enum: [ADMIN, MEMBER], enum: [ADMIN, MEMBER, VIEWER, CUSTOM],
required: true, required: true
}, },
}, customRole: {
{ type: Schema.Types.ObjectId,
timestamps: true, ref: "Role"
} }
},
{
timestamps: true
}
); );
export const Membership = model<IMembership>("Membership", membershipSchema); export const Membership = model<IMembership>("Membership", membershipSchema);
+39 -37
View File
@@ -1,45 +1,47 @@
import { Document, Schema, Types, model } from "mongoose"; import { Document, Schema, Types, model } from "mongoose";
import { ACCEPTED, ADMIN, INVITED, MEMBER, OWNER } from "../variables"; import { ACCEPTED, ADMIN, CUSTOM, INVITED, MEMBER } from "../variables";
export interface IMembershipOrg extends Document { export interface IMembershipOrg extends Document {
_id: Types.ObjectId; _id: Types.ObjectId;
user: Types.ObjectId; user: Types.ObjectId;
inviteEmail: string; inviteEmail: string;
organization: Types.ObjectId; organization: Types.ObjectId;
role: "owner" | "admin" | "member"; role: "owner" | "admin" | "member" | "custom";
status: "invited" | "accepted"; customRole: Types.ObjectId;
status: "invited" | "accepted";
} }
const membershipOrgSchema = new Schema( const membershipOrgSchema = new Schema(
{ {
user: { user: {
type: Schema.Types.ObjectId, type: Schema.Types.ObjectId,
ref: "User", ref: "User"
}, },
inviteEmail: { inviteEmail: {
type: String, type: String
}, },
organization: { organization: {
type: Schema.Types.ObjectId, type: Schema.Types.ObjectId,
ref: "Organization", ref: "Organization"
}, },
role: { role: {
type: String, type: String,
enum: [OWNER, ADMIN, MEMBER], enum: [ADMIN, MEMBER, CUSTOM],
required: true, required: true
}, },
status: { status: {
type: String, type: String,
enum: [INVITED, ACCEPTED], enum: [INVITED, ACCEPTED],
required: true, required: true
}, },
}, customRole: {
{ type: Schema.Types.ObjectId,
timestamps: true, ref: "Role"
} }
},
{
timestamps: true
}
); );
export const MembershipOrg = model<IMembershipOrg>( export const MembershipOrg = model<IMembershipOrg>("MembershipOrg", membershipOrgSchema);
"MembershipOrg",
membershipOrgSchema
);
+3 -1
View File
@@ -1,5 +1,5 @@
import { Document, Schema, Types, model } from "mongoose"; import { Document, Schema, Types, model } from "mongoose";
import { import {
ALGORITHM_AES_256_GCM, ALGORITHM_AES_256_GCM,
ENCODING_SCHEME_BASE64, ENCODING_SCHEME_BASE64,
ENCODING_SCHEME_UTF8, ENCODING_SCHEME_UTF8,
@@ -53,4 +53,6 @@ const secretBlindIndexDataSchema = new Schema<ISecretBlindIndexData>(
} }
); );
secretBlindIndexDataSchema.index({ workspace: 1 });
export const SecretBlindIndexData = model<ISecretBlindIndexData>("SecretBlindIndexData", secretBlindIndexDataSchema); export const SecretBlindIndexData = model<ISecretBlindIndexData>("SecretBlindIndexData", secretBlindIndexDataSchema);
@@ -22,7 +22,6 @@ syncSecretsToThirdPartyServices.process(async (job: Job) => {
} }
: {}), : {}),
isActive: true, isActive: true,
app: { $ne: null }
}); });
// for each workspace integration, sync/push secrets // for each workspace integration, sync/push secrets
@@ -36,9 +35,12 @@ syncSecretsToThirdPartyServices.process(async (job: Job) => {
}); });
const suffixedSecrets: any = {}; const suffixedSecrets: any = {};
if (integration.metadata?.secretSuffix) { if (integration.metadata) {
for (const key in secrets) { for (const key in secrets) {
const newKey = key + integration.metadata?.secretSuffix; const prefix = (integration.metadata?.secretPrefix || "");
const suffix = (integration.metadata?.secretSuffix || "");
const newKey = prefix + key + suffix;
suffixedSecrets[newKey] = secrets[key]; suffixedSecrets[newKey] = secrets[key];
} }
} }
@@ -1,201 +1,100 @@
// import Queue, { Job } from "bull"; import Queue, { Job } from "bull";
// import { ProbotOctokit } from "probot" import { ProbotOctokit } from "probot"
// import { Commit, Committer, Repository } from "@octokit/webhooks-types"; import TelemetryService from "../../services/TelemetryService";
// import TelemetryService from "../../services/TelemetryService"; import { sendMail } from "../../helpers";
// import { sendMail } from "../../helpers"; import GitRisks from "../../ee/models/gitRisks";
// import GitRisks from "../../ee/models/gitRisks"; import { MembershipOrg, User } from "../../models";
// import { MembershipOrg, User } from "../../models"; import { ADMIN } from "../../variables";
// import { OWNER, ADMIN } from "../../variables"; import { convertKeysToLowercase, scanFullRepoContentAndGetFindings } from "../../ee/services/GithubSecretScanning/helper";
// import { convertKeysToLowercase, getFilesFromCommit, scanContentAndGetFindings } from "../../ee/services/GithubSecretScanning/helper"; import { getSecretScanningGitAppId, getSecretScanningPrivateKey } from "../../config";
// import { getSecretScanningGitAppId, getSecretScanningPrivateKey } from "../../config"; import { SecretMatch } from "../../ee/services/GithubSecretScanning/types";
// const githubFullRepositoryScan = new Queue('github-historical-secret-scanning', 'redis://redis:6379'); export const githubFullRepositorySecretScan = new Queue("github-full-repository-secret-scanning", "redis://redis:6379");
// type TScanFullRepositoryDetails = { type TScanPushEventQueueDetails = {
// organizationId: string, organizationId: string,
// repositories: { installationId: number,
// id: number; repository: {
// node_id: string; id: number,
// name: string; fullName: string,
// full_name: string; },
// private: boolean; }
// }[] | undefined
// installationId: number
// }
// type SecretMatch = { githubFullRepositorySecretScan.process(async (job: Job, done: Queue.DoneCallback) => {
// Description: string; const { organizationId, repository, installationId }: TScanPushEventQueueDetails = job.data
// StartLine: number; try {
// EndLine: number; const octokit = new ProbotOctokit({
// StartColumn: number; auth: {
// EndColumn: number; appId: await getSecretScanningGitAppId(),
// Match: string; privateKey: await getSecretScanningPrivateKey(),
// Secret: string; installationId: installationId
// File: string; },
// SymlinkFile: string; });
// Commit: string; const findings: SecretMatch[] = await scanFullRepoContentAndGetFindings(octokit, installationId, repository.fullName)
// Entropy: number; for (const finding of findings) {
// Author: string; await GitRisks.findOneAndUpdate({ fingerprint: finding.Fingerprint },
// Email: string; {
// Date: string; ...convertKeysToLowercase(finding),
// Message: string; installationId: installationId,
// Tags: string[]; organization: organizationId,
// RuleID: string; repositoryFullName: repository.fullName,
// Fingerprint: string; repositoryId: repository.id
// FingerPrintWithoutCommitId: string }, {
// }; upsert: true
}).lean()
}
// type Helllo = { // get emails of admins
// url: string; const adminsOfWork = await MembershipOrg.find({
// sha: string; organization: organizationId,
// node_id: string; role: ADMIN,
// html_url: string; }).lean()
// comments_url: string;
// commit: {
// url: string;
// author: {
// name?: string | undefined;
// email?: string | undefined;
// date?: string | undefined;
// } | null;
// verification?: {
// } | undefined;
// };
// files?: {}[] | undefined;
// }[]
const userEmails = await User.find({
_id: {
$in: [adminsOfWork.map(orgMembership => orgMembership.user)]
}
}).select("email").lean()
// githubFullRepositoryScan.process(async (job: Job, done: Queue.DoneCallback) => { const usersToNotify = userEmails.map(userObject => userObject.email)
// const { organizationId, repositories, installationId }: TScanFullRepositoryDetails = job.data
// const repositoryFullNamesList = repositories ? repositories.map(repoDetails => repoDetails.full_name) : []
// const octokit = new ProbotOctokit({
// auth: {
// appId: await getSecretScanningGitAppId(),
// privateKey: await getSecretScanningPrivateKey(),
// installationId: installationId
// },
// });
// for (const repositoryFullName of repositoryFullNamesList) { if (findings.length) {
// const [owner, repo] = repositoryFullName.split("/"); await sendMail({
template: "historicalSecretLeakIncident.handlebars",
subjectLine: `Incident alert: leaked secrets found in Github repository ${repository.fullName}`,
recipients: usersToNotify,
substitutions: {
numberOfSecrets: findings.length,
}
});
}
// let page = 1; const postHogClient = await TelemetryService.getPostHogClient();
// while (true) { if (postHogClient) {
// // octokit.repos.getco postHogClient.capture({
// const { data } = await octokit.repos.listCommits({ event: "historical cloud secret scan",
// owner, distinctId: repository.fullName,
// repo, properties: {
// per_page: 100, numberOfRisksFound: findings.length,
// page }
// }); });
}
done(null, findings)
// await getFilesFromCommit(octokit, owner, repo, "646b386605177ed0a2cc0a596eeee0cf57666342") } catch (error) {
done(new Error(`gitHubHistoricalScanning.process: an error occurred ${error}`), null)
}
// page++; })
// }
// }
// done()
// // const allFindingsByFingerprint: { [key: string]: SecretMatch; } = {}
// // for (const commit of commits) {
// // for (const filepath of [...commit.added, ...commit.modified]) {
// // try {
// // const fileContentsResponse = await octokit.repos.getContent({
// // owner,
// // repo,
// // path: filepath,
// // });
// // const data: any = fileContentsResponse.data;
// // const fileContent = Buffer.from(data.content, "base64").toString();
// // const findings = await scanContentAndGetFindings(`\n${fileContent}`) // extra line to count lines correctly
// // for (const finding of findings) {
// // const fingerPrintWithCommitId = `${commit.id}:${filepath}:${finding.RuleID}:${finding.StartLine}`
// // const fingerPrintWithoutCommitId = `${filepath}:${finding.RuleID}:${finding.StartLine}`
// // finding.Fingerprint = fingerPrintWithCommitId
// // finding.FingerPrintWithoutCommitId = fingerPrintWithoutCommitId
// // finding.Commit = commit.id
// // finding.File = filepath
// // finding.Author = commit.author.name
// // finding.Email = commit?.author?.email ? commit?.author?.email : ""
// // allFindingsByFingerprint[fingerPrintWithCommitId] = finding
// // }
// // } catch (error) {
// // done(new Error(`gitHubHistoricalScanning.process: unable to fetch content for [filepath=${filepath}] because [error=${error}]`), null)
// // }
// // }
// // }
// // // change to update
// // for (const key in allFindingsByFingerprint) {
// // await GitRisks.findOneAndUpdate({ fingerprint: allFindingsByFingerprint[key].Fingerprint },
// // {
// // ...convertKeysToLowercase(allFindingsByFingerprint[key]),
// // installationId: installationId,
// // organization: organizationId,
// // repositoryFullName: repository.fullName,
// // repositoryId: repository.id
// // }, {
// // upsert: true
// // }).lean()
// // }
// // // get emails of admins
// // const adminsOfWork = await MembershipOrg.find({
// // organization: organizationId,
// // $or: [
// // { role: OWNER },
// // { role: ADMIN }
// // ]
// // }).lean()
// // const userEmails = await User.find({
// // _id: {
// // $in: [adminsOfWork.map(orgMembership => orgMembership.user)]
// // }
// // }).select("email").lean()
// // const adminOrOwnerEmails = userEmails.map(userObject => userObject.email)
// // const usersToNotify = pusher?.email ? [pusher.email, ...adminOrOwnerEmails] : [...adminOrOwnerEmails]
// // if (Object.keys(allFindingsByFingerprint).length) {
// // await sendMail({
// // template: "secretLeakIncident.handlebars",
// // subjectLine: `Incident alert: leaked secrets found in Github repository ${repository.fullName}`,
// // recipients: usersToNotify,
// // substitutions: {
// // numberOfSecrets: Object.keys(allFindingsByFingerprint).length,
// // pusher_email: pusher.email,
// // pusher_name: pusher.name
// // }
// // });
// // }
// // const postHogClient = await TelemetryService.getPostHogClient();
// // if (postHogClient) {
// // postHogClient.capture({
// // event: "cloud secret scan",
// // distinctId: pusher.email,
// // properties: {
// // numberOfCommitsScanned: commits.length,
// // numberOfRisksFound: Object.keys(allFindingsByFingerprint).length,
// // }
// // });
// // }
// // done(null, allFindingsByFingerprint)
// })
// export const scanGithubFullRepositoryForSecretLeaks = (scanFullRepositoryDetails: TScanFullRepositoryDetails) => {
// console.log("full repo scan started")
// githubFullRepositoryScan.add(scanFullRepositoryDetails)
// }
export const scanGithubFullRepoForSecretLeaks = (pushEventPayload: TScanPushEventQueueDetails) => {
githubFullRepositorySecretScan.add(pushEventPayload, {
attempts: 3,
backoff: {
type: "exponential",
delay: 5000
},
removeOnComplete: true,
removeOnFail: {
count: 20 // keep the most recent 20 jobs
}
})
}
@@ -5,7 +5,7 @@ import TelemetryService from "../../services/TelemetryService";
import { sendMail } from "../../helpers"; import { sendMail } from "../../helpers";
import GitRisks from "../../ee/models/gitRisks"; import GitRisks from "../../ee/models/gitRisks";
import { MembershipOrg, User } from "../../models"; import { MembershipOrg, User } from "../../models";
import { ADMIN, OWNER } from "../../variables"; import { ADMIN } from "../../variables";
import { convertKeysToLowercase, scanContentAndGetFindings } from "../../ee/services/GithubSecretScanning/helper"; import { convertKeysToLowercase, scanContentAndGetFindings } from "../../ee/services/GithubSecretScanning/helper";
import { getSecretScanningGitAppId, getSecretScanningPrivateKey } from "../../config"; import { getSecretScanningGitAppId, getSecretScanningPrivateKey } from "../../config";
import { SecretMatch } from "../../ee/services/GithubSecretScanning/types"; import { SecretMatch } from "../../ee/services/GithubSecretScanning/types";
@@ -88,10 +88,7 @@ githubPushEventSecretScan.process(async (job: Job, done: Queue.DoneCallback) =>
// get emails of admins // get emails of admins
const adminsOfWork = await MembershipOrg.find({ const adminsOfWork = await MembershipOrg.find({
organization: organizationId, organization: organizationId,
$or: [ role: ADMIN
{ role: OWNER },
{ role: ADMIN }
]
}).lean() }).lean()
const userEmails = await User.find({ const userEmails = await User.find({
-7
View File
@@ -1,6 +1,5 @@
import express from "express"; import express from "express";
const router = express.Router(); const router = express.Router();
import { body } from "express-validator";
import { requireAuth, validateRequest } from "../../middleware"; import { requireAuth, validateRequest } from "../../middleware";
import { authController } from "../../controllers/v1"; import { authController } from "../../controllers/v1";
import { authLimiter } from "../../helpers/rateLimiter"; import { authLimiter } from "../../helpers/rateLimiter";
@@ -12,9 +11,6 @@ router.post(
// TODO endpoint: deprecate (moved to api/v3/auth/login1) // TODO endpoint: deprecate (moved to api/v3/auth/login1)
"/login1", "/login1",
authLimiter, authLimiter,
body("email").exists().trim().notEmpty().toLowerCase(),
body("clientPublicKey").exists().trim().notEmpty(),
validateRequest,
authController.login1 authController.login1
); );
@@ -22,9 +18,6 @@ router.post(
// TODO endpoint: deprecate (moved to api/v3/auth/login2) // TODO endpoint: deprecate (moved to api/v3/auth/login2)
"/login2", "/login2",
authLimiter, authLimiter,
body("email").exists().trim().notEmpty().toLowerCase(),
body("clientProof").exists().trim().notEmpty(),
validateRequest,
authController.login2 authController.login2
); );
+13 -29
View File
@@ -1,41 +1,25 @@
import express from "express"; import express from "express";
const router = express.Router(); const router = express.Router();
import { body, param } from "express-validator";
import { import {
requireAuth, requireAuth
requireBotAuth,
requireWorkspaceAuth,
validateRequest,
} from "../../middleware"; } from "../../middleware";
import { botController } from "../../controllers/v1"; import { botController } from "../../controllers/v1";
import { ADMIN, AuthMode, MEMBER } from "../../variables"; import { AuthMode } from "../../variables";
router.get( router.get(
"/:workspaceId", "/:workspaceId",
requireAuth({ requireAuth({
acceptedAuthModes: [AuthMode.JWT], acceptedAuthModes: [AuthMode.JWT]
}), }),
requireWorkspaceAuth({ botController.getBotByWorkspaceId
acceptedRoles: [ADMIN, MEMBER],
locationWorkspaceId: "params",
}),
param("workspaceId").exists().trim().notEmpty(),
validateRequest,
botController.getBotByWorkspaceId
); );
router.patch( router.patch(
"/:botId/active", "/:botId/active",
requireAuth({ requireAuth({
acceptedAuthModes: [AuthMode.JWT], acceptedAuthModes: [AuthMode.JWT]
}), }),
requireBotAuth({ botController.setBotActiveState
acceptedRoles: [ADMIN, MEMBER],
}),
body("isActive").exists().isBoolean(),
body("botKey"),
validateRequest,
botController.setBotActiveState
); );
export default router; export default router;
+2 -2
View File
@@ -16,7 +16,7 @@ import integration from "./integration";
import integrationAuth from "./integrationAuth"; import integrationAuth from "./integrationAuth";
import secretsFolder from "./secretsFolder"; import secretsFolder from "./secretsFolder";
import webhooks from "./webhook"; import webhooks from "./webhook";
import secretImport from "./secretImport"; import secretImps from "./secretImps";
export { export {
signup, signup,
@@ -37,5 +37,5 @@ export {
integrationAuth, integrationAuth,
secretsFolder, secretsFolder,
webhooks, webhooks,
secretImport secretImps
}; };
+3 -58
View File
@@ -1,45 +1,14 @@
import express from "express"; import express from "express";
const router = express.Router(); const router = express.Router();
import { import { requireAuth } from "../../middleware";
requireAuth, import { AuthMode } from "../../variables";
requireIntegrationAuth,
requireIntegrationAuthorizationAuth,
requireWorkspaceAuth,
validateRequest,
} from "../../middleware";
import {
ADMIN,
AuthMode,
MEMBER
} from "../../variables";
import { body, param } from "express-validator";
import { integrationController } from "../../controllers/v1"; import { integrationController } from "../../controllers/v1";
router.post( router.post(
"/", "/",
requireAuth({ 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("app").trim(),
body("isActive").exists().isBoolean(),
body("appId").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 integrationController.createIntegration
); );
@@ -48,18 +17,6 @@ router.patch(
requireAuth({ requireAuth({
acceptedAuthModes: [AuthMode.JWT] 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 integrationController.updateIntegration
); );
@@ -68,11 +25,6 @@ router.delete(
requireAuth({ requireAuth({
acceptedAuthModes: [AuthMode.JWT] acceptedAuthModes: [AuthMode.JWT]
}), }),
requireIntegrationAuth({
acceptedRoles: [ADMIN, MEMBER],
}),
param("integrationId").exists().trim(),
validateRequest,
integrationController.deleteIntegration integrationController.deleteIntegration
); );
@@ -81,13 +33,6 @@ router.post(
requireAuth({ requireAuth({
acceptedAuthModes: [AuthMode.JWT] acceptedAuthModes: [AuthMode.JWT]
}), }),
requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER],
locationWorkspaceId: "body",
}),
body("environment").isString().exists().trim(),
body("workspaceId").exists().trim(),
validateRequest,
integrationController.manualSync integrationController.manualSync
); );

Some files were not shown because too many files have changed in this diff Show More