mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-04 23:26:20 +00:00
Merge branch 'main' into gitlab-sso
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -57,3 +57,6 @@ yarn-error.log*
|
|||||||
|
|
||||||
# Infisical init
|
# Infisical init
|
||||||
.infisical.json
|
.infisical.json
|
||||||
|
|
||||||
|
# Editor specific
|
||||||
|
.vscode/*
|
||||||
+1
-1
@@ -1 +1 @@
|
|||||||
.github/resources/docker-compose.be-test.yml:generic-api-key:16
|
.github/resources/docker-compose.be-test.yml:generic-api-key:16
|
||||||
|
|||||||
@@ -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
@@ -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"]
|
||||||
|
|||||||
Generated
+197
-83
@@ -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=="
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
File diff suppressed because it is too large
Load Diff
@@ -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");
|
||||||
|
|||||||
@@ -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
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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
|
||||||
});
|
});
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -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,
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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(
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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?"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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: []
|
||||||
});
|
});
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -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
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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
|
||||||
});
|
});
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -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;
|
||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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
|
||||||
|
};
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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" });
|
||||||
|
};
|
||||||
@@ -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" });
|
||||||
|
};
|
||||||
@@ -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()
|
||||||
|
})
|
||||||
|
});
|
||||||
@@ -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 }) => {
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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[] = [];
|
||||||
|
|||||||
@@ -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
@@ -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
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|||||||
@@ -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) });
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
|||||||
@@ -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: {
|
||||||
|
|||||||
@@ -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
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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 };
|
||||||
@@ -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 };
|
||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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;
|
||||||
@@ -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;
|
||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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: {}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,3 +1,8 @@
|
|||||||
export type Metadata = {
|
export type Metadata = {
|
||||||
|
secretPrefix?: string;
|
||||||
secretSuffix?: string;
|
secretSuffix?: string;
|
||||||
|
secretGCPLabel?: {
|
||||||
|
labelName: string;
|
||||||
|
labelValue: string;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -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;
|
||||||
@@ -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);
|
||||||
@@ -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
|
|
||||||
);
|
|
||||||
|
|||||||
@@ -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({
|
||||||
|
|||||||
@@ -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
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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
Reference in New Issue
Block a user