mirror of
https://github.com/Infisical/infisical.git
synced 2025-03-29 22:02:57 +00:00
Compare commits
38 Commits
daniel/eve
...
ecs-docs
Author | SHA1 | Date | |
---|---|---|---|
28a9d8e739 | |||
a1321e4749 | |||
50c0fae557 | |||
44f087991c | |||
59f662f8a8 | |||
b68b3840d4 | |||
06dd55888e | |||
7711994018 | |||
cb27cfdd84 | |||
4b3e4f6a1e | |||
560f8d4a9b | |||
7bf1f4708a | |||
bcbb49ed1b | |||
5855cc660a | |||
7c4dd5000f | |||
c9ca59dedf | |||
d12f775202 | |||
5a4ed1dbe6 | |||
cae0c9afdb | |||
102af389e3 | |||
a2ef7482ea | |||
e38dd9e275 | |||
b73987d2c2 | |||
f180b5ed6a | |||
2feca7ef2e | |||
40768631e0 | |||
3cbda8e785 | |||
aa1e17dae5 | |||
90a2ca7cc8 | |||
cd83141617 | |||
e231b74616 | |||
d18cf10fc4 | |||
6fe3a8bd67 | |||
6de3059724 | |||
6c6fae3793 | |||
e6a7c5cb6c | |||
af4ac90617 | |||
571e3c4961 |
85
.github/values.yaml
vendored
85
.github/values.yaml
vendored
@ -1,49 +1,52 @@
|
||||
backend:
|
||||
enabled: true
|
||||
name: backend
|
||||
podAnnotations: {}
|
||||
deploymentAnnotations:
|
||||
secrets.infisical.com/auto-reload: "true"
|
||||
## @section Common parameters
|
||||
##
|
||||
|
||||
## @param nameOverride Override release name
|
||||
##
|
||||
nameOverride: ""
|
||||
## @param fullnameOverride Override release fullname
|
||||
##
|
||||
fullnameOverride: ""
|
||||
|
||||
## @section Infisical backend parameters
|
||||
## Documentation : https://infisical.com/docs/self-hosting/deployments/kubernetes
|
||||
##
|
||||
|
||||
infisical:
|
||||
## @param backend.enabled Enable backend
|
||||
##
|
||||
enabled: false
|
||||
## @param backend.name Backend name
|
||||
##
|
||||
name: infisical
|
||||
replicaCount: 2
|
||||
image:
|
||||
repository: infisical/staging_infisical
|
||||
tag: "latest"
|
||||
pullPolicy: Always
|
||||
kubeSecretRef: managed-backend-secret
|
||||
service:
|
||||
annotations: {}
|
||||
type: ClusterIP
|
||||
nodePort: ""
|
||||
resources:
|
||||
limits:
|
||||
memory: 300Mi
|
||||
repository: infisical/infisical
|
||||
tag: "latest-postgres"
|
||||
pullPolicy: IfNotPresent
|
||||
|
||||
backendEnvironmentVariables: null
|
||||
deploymentAnnotations:
|
||||
secrets.infisical.com/auto-reload: "true"
|
||||
|
||||
## Mongo DB persistence
|
||||
mongodb:
|
||||
enabled: false
|
||||
persistence:
|
||||
enabled: false
|
||||
|
||||
## By default the backend will be connected to a Mongo instance within the cluster
|
||||
## However, it is recommended to add a managed document DB connection string for production-use (DBaaS)
|
||||
## Learn about connection string type here https://www.mongodb.com/docs/manual/reference/connection-string/
|
||||
## e.g. "mongodb://<user>:<pass>@<host>:<port>/<database-name>"
|
||||
mongodbConnection:
|
||||
externalMongoDBConnectionString: ""
|
||||
kubeSecretRef: "infisical-gamma-secrets"
|
||||
|
||||
ingress:
|
||||
## @param ingress.enabled Enable ingress
|
||||
##
|
||||
enabled: true
|
||||
# annotations:
|
||||
# kubernetes.io/ingress.class: "nginx"
|
||||
# cert-manager.io/issuer: letsencrypt-nginx
|
||||
hostName: gamma.infisical.com ## <- Replace with your own domain
|
||||
## @param ingress.ingressClassName Ingress class name
|
||||
##
|
||||
ingressClassName: nginx
|
||||
## @param ingress.nginx.enabled Ingress controller
|
||||
##
|
||||
# nginx:
|
||||
# enabled: true
|
||||
## @param ingress.annotations Ingress annotations
|
||||
##
|
||||
annotations:
|
||||
cert-manager.io/cluster-issuer: "letsencrypt-prod"
|
||||
hostName: "gamma.infisical.com"
|
||||
tls:
|
||||
[]
|
||||
# - secretName: letsencrypt-nginx
|
||||
# hosts:
|
||||
# - infisical.local
|
||||
|
||||
mailhog:
|
||||
enabled: false
|
||||
- secretName: letsencrypt-prod
|
||||
hosts:
|
||||
- gamma.infisical.com
|
||||
|
@ -35,15 +35,15 @@ jobs:
|
||||
context: .
|
||||
file: Dockerfile.standalone-infisical
|
||||
tags: infisical/infisical:test
|
||||
- name: ⏻ Spawn backend container and dependencies
|
||||
run: |
|
||||
docker compose -f .github/resources/docker-compose.be-test.yml up --wait --quiet-pull
|
||||
- name: 🧪 Test backend image
|
||||
run: |
|
||||
./.github/resources/healthcheck.sh infisical-backend-test
|
||||
- name: ⏻ Shut down backend container and dependencies
|
||||
run: |
|
||||
docker compose -f .github/resources/docker-compose.be-test.yml down
|
||||
# - name: ⏻ Spawn backend container and dependencies
|
||||
# run: |
|
||||
# docker compose -f .github/resources/docker-compose.be-test.yml up --wait --quiet-pull
|
||||
# - name: 🧪 Test backend image
|
||||
# run: |
|
||||
# ./.github/resources/healthcheck.sh infisical-backend-test
|
||||
# - name: ⏻ Shut down backend container and dependencies
|
||||
# run: |
|
||||
# docker compose -f .github/resources/docker-compose.be-test.yml down
|
||||
- name: 🏗️ Build backend and push
|
||||
uses: depot/build-push-action@v1
|
||||
with:
|
||||
@ -59,11 +59,32 @@ jobs:
|
||||
build-args: |
|
||||
POSTHOG_API_KEY=${{ secrets.PUBLIC_POSTHOG_API_KEY }}
|
||||
INFISICAL_PLATFORM_VERSION=${{ steps.extract_version.outputs.version }}
|
||||
|
||||
postgres-migration:
|
||||
name: Run latest migration files
|
||||
runs-on: ubuntu-latest
|
||||
needs: [infisical-image]
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v2
|
||||
- name: Setup Node.js environment
|
||||
uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: "20"
|
||||
- name: Change directory to backend and install dependencies
|
||||
env:
|
||||
DB_CONNECTION_URI: ${{ secrets.DB_CONNECTION_URI }}
|
||||
run: |
|
||||
cd backend
|
||||
npm install
|
||||
npm run migration:latest
|
||||
# - name: Run postgres DB migration files
|
||||
# env:
|
||||
# DB_CONNECTION_URI: ${{ secrets.DB_CONNECTION_URI }}
|
||||
# run: npm run migration:latest
|
||||
gamma-deployment:
|
||||
name: Deploy to gamma
|
||||
runs-on: ubuntu-latest
|
||||
needs: [infisical-image]
|
||||
needs: [postgres-migration]
|
||||
steps:
|
||||
- name: ☁️ Checkout source
|
||||
uses: actions/checkout@v3
|
||||
@ -82,7 +103,7 @@ jobs:
|
||||
with:
|
||||
token: ${{ secrets.DIGITALOCEAN_ACCESS_TOKEN }}
|
||||
- name: Save DigitalOcean kubeconfig with short-lived credentials
|
||||
run: doctl kubernetes cluster kubeconfig save --expiry-seconds 600 k8s-1-25-4-do-0-nyc1-1670645170179
|
||||
run: doctl kubernetes cluster kubeconfig save --expiry-seconds 600 infisical-gamma-postgres
|
||||
- name: switch to gamma namespace
|
||||
run: kubectl config set-context --current --namespace=gamma
|
||||
- name: test kubectl
|
||||
@ -90,7 +111,7 @@ jobs:
|
||||
- name: Download helm values to file and upgrade gamma deploy
|
||||
run: |
|
||||
wget https://raw.githubusercontent.com/Infisical/infisical/main/.github/values.yaml
|
||||
helm upgrade infisical infisical-helm-charts/infisical --values values.yaml --wait --install
|
||||
helm upgrade infisical infisical-helm-charts/infisical-standalone --values values.yaml --wait --install
|
||||
if [[ $(helm status infisical) == *"FAILED"* ]]; then
|
||||
echo "Helm upgrade failed"
|
||||
exit 1
|
@ -1,79 +0,0 @@
|
||||
name: Release standalone docker image
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- "infisical/v*.*.*"
|
||||
- "!infisical/v*.*.*-postgres"
|
||||
|
||||
jobs:
|
||||
infisical-standalone:
|
||||
name: Build infisical standalone image
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Extract version from tag
|
||||
id: extract_version
|
||||
run: echo "::set-output name=version::${GITHUB_REF_NAME#infisical/}"
|
||||
- name: ☁️ Checkout source
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: 📦 Install dependencies to test all dependencies
|
||||
run: npm ci --only-production
|
||||
working-directory: backend
|
||||
- uses: paulhatch/semantic-version@v5.0.2
|
||||
id: version
|
||||
with:
|
||||
# The prefix to use to identify tags
|
||||
tag_prefix: "infisical-standalone/v"
|
||||
# A string which, if present in a git commit, indicates that a change represents a
|
||||
# major (breaking) change, supports regular expressions wrapped with '/'
|
||||
major_pattern: "(MAJOR)"
|
||||
# Same as above except indicating a minor change, supports regular expressions wrapped with '/'
|
||||
minor_pattern: "(MINOR)"
|
||||
# A string to determine the format of the version output
|
||||
version_format: "${major}.${minor}.${patch}-prerelease${increment}"
|
||||
# Optional path to check for changes. If any changes are detected in the path the
|
||||
# 'changed' output will true. Enter multiple paths separated by spaces.
|
||||
change_path: "backend,frontend"
|
||||
# Prevents pre-v1.0.0 version from automatically incrementing the major version.
|
||||
# If enabled, when the major version is 0, major releases will be treated as minor and minor as patch. Note that the version_type output is unchanged.
|
||||
enable_prerelease_mode: true
|
||||
# - name: 🧪 Run tests
|
||||
# run: npm run test:ci
|
||||
# working-directory: backend
|
||||
- name: version output
|
||||
run: |
|
||||
echo "Output Value: ${{ steps.version.outputs.major }}"
|
||||
echo "Output Value: ${{ steps.version.outputs.minor }}"
|
||||
echo "Output Value: ${{ steps.version.outputs.patch }}"
|
||||
echo "Output Value: ${{ steps.version.outputs.version }}"
|
||||
echo "Output Value: ${{ steps.version.outputs.version_type }}"
|
||||
echo "Output Value: ${{ steps.version.outputs.increment }}"
|
||||
- name: Save commit hashes for tag
|
||||
id: commit
|
||||
uses: pr-mpt/actions-commit-hash@v2
|
||||
- name: 🔧 Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
- name: 🐋 Login to Docker Hub
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
- name: Set up Depot CLI
|
||||
uses: depot/setup-action@v1
|
||||
- name: 📦 Build backend and export to Docker
|
||||
uses: depot/build-push-action@v1
|
||||
with:
|
||||
project: 64mmf0n610
|
||||
token: ${{ secrets.DEPOT_PROJECT_TOKEN }}
|
||||
push: true
|
||||
context: .
|
||||
tags: |
|
||||
infisical/infisical:latest
|
||||
infisical/infisical:${{ steps.commit.outputs.short }}
|
||||
infisical/infisical:${{ steps.extract_version.outputs.version }}
|
||||
platforms: linux/amd64,linux/arm64
|
||||
file: Dockerfile.standalone-infisical
|
||||
build-args: |
|
||||
POSTHOG_API_KEY=${{ secrets.PUBLIC_POSTHOG_API_KEY }}
|
||||
INFISICAL_PLATFORM_VERSION=${{ steps.extract_version.outputs.version }}
|
@ -1,11 +0,0 @@
|
||||
node_modules
|
||||
.env
|
||||
.env.*
|
||||
.git
|
||||
.gitignore
|
||||
Dockerfile
|
||||
.dockerignore
|
||||
docker-compose.*
|
||||
.DS_Store
|
||||
*.swp
|
||||
*~
|
@ -1,2 +0,0 @@
|
||||
node_modules
|
||||
built
|
@ -1,41 +0,0 @@
|
||||
{
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"plugins": [
|
||||
"@typescript-eslint",
|
||||
"unused-imports"
|
||||
],
|
||||
"extends": [
|
||||
"eslint:recommended",
|
||||
"plugin:@typescript-eslint/eslint-recommended",
|
||||
"plugin:@typescript-eslint/recommended"
|
||||
],
|
||||
"rules": {
|
||||
"no-empty-function": "off",
|
||||
"@typescript-eslint/no-empty-function": "off",
|
||||
"no-console": 2,
|
||||
"quotes": [
|
||||
"error",
|
||||
"double",
|
||||
{
|
||||
"avoidEscape": true
|
||||
}
|
||||
],
|
||||
"comma-dangle": [
|
||||
"error",
|
||||
"only-multiline"
|
||||
],
|
||||
"@typescript-eslint/no-unused-vars": "off",
|
||||
"unused-imports/no-unused-imports": "error",
|
||||
"@typescript-eslint/no-extra-semi": "off", // added to be able to push
|
||||
"unused-imports/no-unused-vars": [
|
||||
"warn",
|
||||
{
|
||||
"vars": "all",
|
||||
"varsIgnorePattern": "^_",
|
||||
"args": "after-used",
|
||||
"argsIgnorePattern": "^_"
|
||||
}
|
||||
],
|
||||
"sort-imports": 1
|
||||
}
|
||||
}
|
@ -1,7 +0,0 @@
|
||||
{
|
||||
"singleQuote": false,
|
||||
"printWidth": 100,
|
||||
"trailingComma": "none",
|
||||
"tabWidth": 2,
|
||||
"semi": true
|
||||
}
|
@ -1,33 +0,0 @@
|
||||
# Build stage
|
||||
FROM node:16-alpine AS build
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY package*.json ./
|
||||
RUN npm ci --only-production
|
||||
|
||||
COPY . .
|
||||
RUN npm run build
|
||||
|
||||
# Production stage
|
||||
FROM node:16-alpine
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
ENV npm_config_cache /home/node/.npm
|
||||
|
||||
COPY package*.json ./
|
||||
RUN npm ci --only-production && npm cache clean --force
|
||||
|
||||
COPY --from=build /app .
|
||||
|
||||
RUN apk add --no-cache bash curl && curl -1sLf \
|
||||
'https://dl.cloudsmith.io/public/infisical/infisical-cli/setup.alpine.sh' | bash \
|
||||
&& apk add infisical=0.8.1 && apk add --no-cache git
|
||||
|
||||
HEALTHCHECK --interval=10s --timeout=3s --start-period=10s \
|
||||
CMD node healthcheck.js
|
||||
|
||||
EXPOSE 4000
|
||||
|
||||
CMD ["node", "build/index.js"]
|
46
backend-mongo/environment.d.ts
vendored
46
backend-mongo/environment.d.ts
vendored
@ -1,46 +0,0 @@
|
||||
export {};
|
||||
|
||||
declare global {
|
||||
namespace NodeJS {
|
||||
interface ProcessEnv {
|
||||
PORT: string;
|
||||
ENCRYPTION_KEY: string;
|
||||
SALT_ROUNDS: string;
|
||||
JWT_AUTH_LIFETIME: string;
|
||||
JWT_AUTH_SECRET: string;
|
||||
JWT_REFRESH_LIFETIME: string;
|
||||
JWT_REFRESH_SECRET: string;
|
||||
JWT_SERVICE_SECRET: string;
|
||||
JWT_SIGNUP_LIFETIME: string;
|
||||
JWT_SIGNUP_SECRET: string;
|
||||
MONGO_URL: string;
|
||||
NODE_ENV: "development" | "staging" | "testing" | "production";
|
||||
VERBOSE_ERROR_OUTPUT: string;
|
||||
LOKI_HOST: string;
|
||||
CLIENT_ID_HEROKU: string;
|
||||
CLIENT_ID_VERCEL: string;
|
||||
CLIENT_ID_NETLIFY: string;
|
||||
CLIENT_ID_GITHUB: string;
|
||||
CLIENT_ID_GITLAB: string;
|
||||
CLIENT_SECRET_HEROKU: string;
|
||||
CLIENT_SECRET_VERCEL: string;
|
||||
CLIENT_SECRET_NETLIFY: string;
|
||||
CLIENT_SECRET_GITHUB: string;
|
||||
CLIENT_SECRET_GITLAB: string;
|
||||
CLIENT_SLUG_VERCEL: string;
|
||||
POSTHOG_HOST: string;
|
||||
POSTHOG_PROJECT_API_KEY: string;
|
||||
SENTRY_DSN: string;
|
||||
SITE_URL: string;
|
||||
SMTP_HOST: string;
|
||||
SMTP_SECURE: string;
|
||||
SMTP_PORT: string;
|
||||
SMTP_USERNAME: string;
|
||||
SMTP_PASSWORD: string;
|
||||
SMTP_FROM_ADDRESS: string;
|
||||
SMTP_FROM_NAME: string;
|
||||
TELEMETRY_ENABLED: string;
|
||||
LICENSE_KEY: string;
|
||||
}
|
||||
}
|
||||
}
|
@ -1,24 +0,0 @@
|
||||
const http = require('http');
|
||||
const PORT = process.env.PORT || 4000;
|
||||
const options = {
|
||||
host: 'localhost',
|
||||
port: PORT,
|
||||
timeout: 2000,
|
||||
path: '/healthcheck'
|
||||
};
|
||||
|
||||
const healthCheck = http.request(options, (res) => {
|
||||
console.log(`HEALTHCHECK STATUS: ${res.statusCode}`);
|
||||
if (res.statusCode == 200) {
|
||||
process.exit(0);
|
||||
} else {
|
||||
process.exit(1);
|
||||
}
|
||||
});
|
||||
|
||||
healthCheck.on('error', function (err) {
|
||||
console.error(`HEALTH CHECK ERROR: ${err}`);
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
healthCheck.end();
|
Binary file not shown.
Before Width: | Height: | Size: 493 KiB |
@ -1,9 +0,0 @@
|
||||
export default {
|
||||
preset: "ts-jest",
|
||||
testEnvironment: "node",
|
||||
collectCoverageFrom: ["src/*.{js,ts}", "!**/node_modules/**"],
|
||||
modulePaths: ["<rootDir>/src"],
|
||||
testMatch: ["<rootDir>/tests/**/*.test.ts"],
|
||||
setupFiles: ["<rootDir>/test-resources/env-vars.js"],
|
||||
setupFilesAfterEnv: ["<rootDir>/tests/setupTests.ts"],
|
||||
};
|
@ -1,6 +0,0 @@
|
||||
{
|
||||
"watch": ["src"],
|
||||
"ext": ".ts,.js",
|
||||
"ignore": [],
|
||||
"exec": "ts-node ./src/index.ts"
|
||||
}
|
32861
backend-mongo/package-lock.json
generated
32861
backend-mongo/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -1,148 +0,0 @@
|
||||
{
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-secrets-manager": "^3.319.0",
|
||||
"@casl/ability": "^6.5.0",
|
||||
"@casl/mongoose": "^7.2.1",
|
||||
"@godaddy/terminus": "^4.12.0",
|
||||
"@node-saml/passport-saml": "^4.0.4",
|
||||
"@octokit/rest": "^19.0.5",
|
||||
"@sentry/node": "^7.77.0",
|
||||
"@sentry/tracing": "^7.48.0",
|
||||
"@serdnam/pino-cloudwatch-transport": "^1.0.4",
|
||||
"@types/crypto-js": "^4.1.1",
|
||||
"@types/libsodium-wrappers": "^0.7.10",
|
||||
"@ucast/mongo2js": "^1.3.4",
|
||||
"ajv": "^8.12.0",
|
||||
"argon2": "^0.30.3",
|
||||
"aws-sdk": "^2.1364.0",
|
||||
"axios": "^1.6.0",
|
||||
"axios-retry": "^3.4.0",
|
||||
"bcrypt": "^5.1.0",
|
||||
"bigint-conversion": "^2.4.0",
|
||||
"cookie-parser": "^1.4.6",
|
||||
"cors": "^2.8.5",
|
||||
"crypto-js": "^4.2.0",
|
||||
"dotenv": "^16.0.1",
|
||||
"express": "^4.18.1",
|
||||
"express-async-errors": "^3.1.1",
|
||||
"express-rate-limit": "^6.7.0",
|
||||
"express-validator": "^6.14.2",
|
||||
"handlebars": "^4.7.7",
|
||||
"helmet": "^5.1.1",
|
||||
"infisical-node": "^1.2.1",
|
||||
"ioredis": "^5.3.2",
|
||||
"jmespath": "^0.16.0",
|
||||
"js-yaml": "^4.1.0",
|
||||
"jsonwebtoken": "^9.0.0",
|
||||
"jsrp": "^0.2.4",
|
||||
"libsodium-wrappers": "^0.7.10",
|
||||
"lodash": "^4.17.21",
|
||||
"mongoose": "^7.4.1",
|
||||
"mysql2": "^3.6.2",
|
||||
"nanoid": "^3.3.6",
|
||||
"node-cache": "^5.1.2",
|
||||
"nodemailer": "^6.8.0",
|
||||
"ora": "^5.4.1",
|
||||
"passport": "^0.6.0",
|
||||
"passport-github": "^1.1.0",
|
||||
"passport-gitlab2": "^5.0.0",
|
||||
"passport-google-oauth20": "^2.0.0",
|
||||
"pg": "^8.11.3",
|
||||
"pino": "^8.16.1",
|
||||
"pino-http": "^8.5.1",
|
||||
"posthog-node": "^2.6.0",
|
||||
"probot": "^12.3.3",
|
||||
"query-string": "^7.1.3",
|
||||
"rate-limit-mongo": "^2.3.2",
|
||||
"rimraf": "^3.0.2",
|
||||
"swagger-ui-express": "^4.6.2",
|
||||
"tweetnacl": "^1.0.3",
|
||||
"tweetnacl-util": "^0.15.1",
|
||||
"typescript": "^4.9.3",
|
||||
"utility-types": "^3.10.0",
|
||||
"zod": "^3.22.3"
|
||||
},
|
||||
"overrides": {
|
||||
"rate-limit-mongo": {
|
||||
"mongodb": "5.8.0"
|
||||
}
|
||||
},
|
||||
"name": "infisical-api",
|
||||
"version": "1.0.0",
|
||||
"main": "src/index.js",
|
||||
"scripts": {
|
||||
"start": "node build/index.js",
|
||||
"dev": "nodemon index.js",
|
||||
"swagger-autogen": "node ./swagger/index.ts",
|
||||
"build": "rimraf ./build && tsc && cp -R ./src/templates ./build && cp -R ./src/data ./build",
|
||||
"lint": "eslint . --ext .ts",
|
||||
"lint-and-fix": "eslint . --ext .ts --fix",
|
||||
"lint-staged": "lint-staged",
|
||||
"pretest": "docker compose -f test-resources/docker-compose.test.yml up -d",
|
||||
"test": "cross-env NODE_ENV=test jest --verbose --testTimeout=10000 --detectOpenHandles; npm run posttest",
|
||||
"test:ci": "npm test -- --watchAll=false --ci --reporters=default --reporters=jest-junit --reporters=github-actions --coverage --testLocationInResults --json --outputFile=coverage/report.json",
|
||||
"posttest": "docker compose -f test-resources/docker-compose.test.yml down"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/Infisical/infisical-api.git"
|
||||
},
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"bugs": {
|
||||
"url": "https://github.com/Infisical/infisical-api/issues"
|
||||
},
|
||||
"homepage": "https://github.com/Infisical/infisical-api#readme",
|
||||
"description": "",
|
||||
"devDependencies": {
|
||||
"@jest/globals": "^29.3.1",
|
||||
"@posthog/plugin-scaffold": "^1.3.4",
|
||||
"@swc/core": "^1.3.99",
|
||||
"@swc/helpers": "^0.5.3",
|
||||
"@types/bcrypt": "^5.0.0",
|
||||
"@types/bcryptjs": "^2.4.2",
|
||||
"@types/bull": "^4.10.0",
|
||||
"@types/cookie-parser": "^1.4.3",
|
||||
"@types/cors": "^2.8.12",
|
||||
"@types/express": "^4.17.14",
|
||||
"@types/jest": "^29.5.0",
|
||||
"@types/jmespath": "^0.15.1",
|
||||
"@types/jsonwebtoken": "^8.5.9",
|
||||
"@types/lodash": "^4.14.191",
|
||||
"@types/node": "^18.11.3",
|
||||
"@types/nodemailer": "^6.4.6",
|
||||
"@types/passport": "^1.0.12",
|
||||
"@types/pg": "^8.10.7",
|
||||
"@types/picomatch": "^2.3.0",
|
||||
"@types/pino": "^7.0.5",
|
||||
"@types/supertest": "^2.0.12",
|
||||
"@types/swagger-jsdoc": "^6.0.1",
|
||||
"@types/swagger-ui-express": "^4.1.3",
|
||||
"@typescript-eslint/eslint-plugin": "^5.54.0",
|
||||
"@typescript-eslint/parser": "^5.40.1",
|
||||
"cross-env": "^7.0.3",
|
||||
"eslint": "^8.26.0",
|
||||
"eslint-plugin-unused-imports": "^2.0.0",
|
||||
"install": "^0.13.0",
|
||||
"jest": "^29.3.1",
|
||||
"jest-junit": "^15.0.0",
|
||||
"nodemon": "^2.0.19",
|
||||
"npm": "^8.19.3",
|
||||
"pino-pretty": "^10.2.3",
|
||||
"regenerator-runtime": "^0.14.0",
|
||||
"smee-client": "^1.2.3",
|
||||
"supertest": "^6.3.3",
|
||||
"swagger-autogen": "^2.23.5",
|
||||
"ts-jest": "^29.0.3",
|
||||
"ts-node": "^10.9.1"
|
||||
},
|
||||
"jest-junit": {
|
||||
"outputDirectory": "reports",
|
||||
"outputName": "jest-junit.xml",
|
||||
"ancestorSeparator": " › ",
|
||||
"uniqueOutputName": "false",
|
||||
"suiteNameTemplate": "{filepath}",
|
||||
"classNameTemplate": "{classname}",
|
||||
"titleTemplate": "{title}"
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
@ -1,43 +0,0 @@
|
||||
import ora from "ora";
|
||||
import nodemailer from "nodemailer";
|
||||
import { getSmtpHost, getSmtpPort } from "./config";
|
||||
import { logger } from "./utils/logging";
|
||||
import mongoose from "mongoose";
|
||||
import { redisClient } from "./services/RedisService";
|
||||
|
||||
type BootstrapOpt = {
|
||||
transporter: nodemailer.Transporter;
|
||||
};
|
||||
|
||||
export const bootstrap = async ({ transporter }: BootstrapOpt) => {
|
||||
const spinner = ora().start();
|
||||
spinner.info("Checking configurations...");
|
||||
spinner.info("Testing smtp connection");
|
||||
|
||||
await transporter
|
||||
.verify()
|
||||
.then(async () => {
|
||||
spinner.succeed("SMTP successfully connected");
|
||||
})
|
||||
.catch(async (err) => {
|
||||
spinner.fail(`SMTP - Failed to connect to ${await getSmtpHost()}:${await getSmtpPort()}`);
|
||||
logger.error(err);
|
||||
});
|
||||
|
||||
spinner.info("Testing mongodb connection");
|
||||
if (mongoose.connection.readyState !== mongoose.ConnectionStates.connected) {
|
||||
spinner.fail("Mongo DB - Failed to connect");
|
||||
} else {
|
||||
spinner.succeed("Mongodb successfully connected");
|
||||
}
|
||||
|
||||
spinner.info("Testing redis connection");
|
||||
const redisPing = await redisClient?.ping();
|
||||
if (!redisPing) {
|
||||
spinner.fail("Redis - Failed to connect");
|
||||
} else {
|
||||
spinner.succeed("Redis successfully connected");
|
||||
}
|
||||
|
||||
spinner.stop();
|
||||
};
|
@ -1,176 +0,0 @@
|
||||
import { GITLAB_URL } from "../variables";
|
||||
|
||||
import InfisicalClient from "infisical-node";
|
||||
|
||||
export const client = new InfisicalClient({
|
||||
token: process.env.INFISICAL_TOKEN!
|
||||
});
|
||||
|
||||
export const getIsMigrationMode = async () =>
|
||||
(await client.getSecret("MIGRATION_MODE")).secretValue === "true";
|
||||
|
||||
export const getPort = async () => (await client.getSecret("PORT")).secretValue || 4000;
|
||||
export const getEncryptionKey = async () => {
|
||||
const secretValue = (await client.getSecret("ENCRYPTION_KEY")).secretValue;
|
||||
return secretValue === "" ? undefined : secretValue;
|
||||
};
|
||||
export const getRootEncryptionKey = async () => {
|
||||
const secretValue = (await client.getSecret("ROOT_ENCRYPTION_KEY")).secretValue;
|
||||
return secretValue === "" ? undefined : secretValue;
|
||||
};
|
||||
export const getInviteOnlySignup = async () =>
|
||||
(await client.getSecret("INVITE_ONLY_SIGNUP")).secretValue === "true";
|
||||
export const getSaltRounds = async () =>
|
||||
parseInt((await client.getSecret("SALT_ROUNDS")).secretValue) || 10;
|
||||
export const getAuthSecret = async () =>
|
||||
(await client.getSecret("JWT_AUTH_SECRET")).secretValue ??
|
||||
(await client.getSecret("AUTH_SECRET")).secretValue;
|
||||
export const getJwtAuthLifetime = async () =>
|
||||
(await client.getSecret("JWT_AUTH_LIFETIME")).secretValue || "10d";
|
||||
export const getJwtMfaLifetime = async () =>
|
||||
(await client.getSecret("JWT_MFA_LIFETIME")).secretValue || "5m";
|
||||
export const getJwtRefreshLifetime = async () =>
|
||||
(await client.getSecret("JWT_REFRESH_LIFETIME")).secretValue || "90d";
|
||||
export const getJwtServiceSecret = async () =>
|
||||
(await client.getSecret("JWT_SERVICE_SECRET")).secretValue; // TODO: deprecate (related to ST V1)
|
||||
export const getJwtSignupLifetime = async () =>
|
||||
(await client.getSecret("JWT_SIGNUP_LIFETIME")).secretValue || "15m";
|
||||
export const getJwtProviderAuthLifetime = async () =>
|
||||
(await client.getSecret("JWT_PROVIDER_AUTH_LIFETIME")).secretValue || "15m";
|
||||
export const getMongoURL = async () => (await client.getSecret("MONGO_URL")).secretValue;
|
||||
export const getNodeEnv = async () =>
|
||||
(await client.getSecret("NODE_ENV")).secretValue || "production";
|
||||
export const getVerboseErrorOutput = async () =>
|
||||
(await client.getSecret("VERBOSE_ERROR_OUTPUT")).secretValue === "true" && true;
|
||||
export const getLokiHost = async () => (await client.getSecret("LOKI_HOST")).secretValue;
|
||||
export const getClientIdAzure = async () => (await client.getSecret("CLIENT_ID_AZURE")).secretValue;
|
||||
export const getClientIdHeroku = async () =>
|
||||
(await client.getSecret("CLIENT_ID_HEROKU")).secretValue;
|
||||
export const getClientIdVercel = async () =>
|
||||
(await client.getSecret("CLIENT_ID_VERCEL")).secretValue;
|
||||
export const getClientIdNetlify = async () =>
|
||||
(await client.getSecret("CLIENT_ID_NETLIFY")).secretValue;
|
||||
export const getClientIdGitHub = async () =>
|
||||
(await client.getSecret("CLIENT_ID_GITHUB")).secretValue;
|
||||
export const getClientIdGitLab = async () =>
|
||||
(await client.getSecret("CLIENT_ID_GITLAB")).secretValue;
|
||||
export const getClientIdBitBucket = async () =>
|
||||
(await client.getSecret("CLIENT_ID_BITBUCKET")).secretValue;
|
||||
export const getClientIdGCPSecretManager = async () =>
|
||||
(await client.getSecret("CLIENT_ID_GCP_SECRET_MANAGER")).secretValue;
|
||||
export const getClientSecretAzure = async () =>
|
||||
(await client.getSecret("CLIENT_SECRET_AZURE")).secretValue;
|
||||
export const getClientSecretHeroku = async () =>
|
||||
(await client.getSecret("CLIENT_SECRET_HEROKU")).secretValue;
|
||||
export const getClientSecretVercel = async () =>
|
||||
(await client.getSecret("CLIENT_SECRET_VERCEL")).secretValue;
|
||||
export const getClientSecretNetlify = async () =>
|
||||
(await client.getSecret("CLIENT_SECRET_NETLIFY")).secretValue;
|
||||
export const getClientSecretGitHub = async () =>
|
||||
(await client.getSecret("CLIENT_SECRET_GITHUB")).secretValue;
|
||||
export const getClientSecretGitLab = async () =>
|
||||
(await client.getSecret("CLIENT_SECRET_GITLAB")).secretValue;
|
||||
export const getClientSecretBitBucket = async () =>
|
||||
(await client.getSecret("CLIENT_SECRET_BITBUCKET")).secretValue;
|
||||
export const getClientSecretGCPSecretManager = async () =>
|
||||
(await client.getSecret("CLIENT_SECRET_GCP_SECRET_MANAGER")).secretValue;
|
||||
export const getClientSlugVercel = async () =>
|
||||
(await client.getSecret("CLIENT_SLUG_VERCEL")).secretValue;
|
||||
|
||||
export const getClientIdGoogleLogin = async () =>
|
||||
(await client.getSecret("CLIENT_ID_GOOGLE_LOGIN")).secretValue;
|
||||
export const getClientSecretGoogleLogin = async () =>
|
||||
(await client.getSecret("CLIENT_SECRET_GOOGLE_LOGIN")).secretValue;
|
||||
export const getClientIdGitHubLogin = async () =>
|
||||
(await client.getSecret("CLIENT_ID_GITHUB_LOGIN")).secretValue;
|
||||
export const getClientSecretGitHubLogin = async () =>
|
||||
(await client.getSecret("CLIENT_SECRET_GITHUB_LOGIN")).secretValue;
|
||||
export const getClientIdGitLabLogin = async () =>
|
||||
(await client.getSecret("CLIENT_ID_GITLAB_LOGIN")).secretValue;
|
||||
export const getClientSecretGitLabLogin = async () =>
|
||||
(await client.getSecret("CLIENT_SECRET_GITLAB_LOGIN")).secretValue;
|
||||
export const getUrlGitLabLogin = async () =>
|
||||
(await client.getSecret("URL_GITLAB_LOGIN")).secretValue || GITLAB_URL;
|
||||
|
||||
export const getAwsCloudWatchLog = async () => {
|
||||
const logGroupName =
|
||||
(await client.getSecret("AWS_CLOUDWATCH_LOG_GROUP_NAME")).secretValue || "infisical-log-stream";
|
||||
const region = (await client.getSecret("AWS_CLOUDWATCH_LOG_REGION")).secretValue;
|
||||
const accessKeyId = (await client.getSecret("AWS_CLOUDWATCH_LOG_ACCESS_KEY_ID")).secretValue;
|
||||
const accessKeySecret = (await client.getSecret("AWS_CLOUDWATCH_LOG_ACCESS_KEY_SECRET"))
|
||||
.secretValue;
|
||||
const interval = parseInt(
|
||||
(await client.getSecret("AWS_CLOUDWATCH_LOG_INTERVAL")).secretValue || 1000,
|
||||
10
|
||||
);
|
||||
if (!region || !accessKeyId || !accessKeySecret) return;
|
||||
return { logGroupName, region, accessKeySecret, accessKeyId, interval };
|
||||
};
|
||||
|
||||
export const getPostHogHost = async () =>
|
||||
(await client.getSecret("POSTHOG_HOST")).secretValue || "https://app.posthog.com";
|
||||
export const getPostHogProjectApiKey = async () =>
|
||||
(await client.getSecret("POSTHOG_PROJECT_API_KEY")).secretValue ||
|
||||
"phc_nSin8j5q2zdhpFDI1ETmFNUIuTG4DwKVyIigrY10XiE";
|
||||
export const getSentryDSN = async () => (await client.getSecret("SENTRY_DSN")).secretValue;
|
||||
export const getSiteURL = async () => (await client.getSecret("SITE_URL")).secretValue;
|
||||
export const getSmtpHost = async () => (await client.getSecret("SMTP_HOST")).secretValue;
|
||||
export const getSmtpSecure = async () =>
|
||||
(await client.getSecret("SMTP_SECURE")).secretValue === "true" || false;
|
||||
export const getSmtpPort = async () =>
|
||||
parseInt((await client.getSecret("SMTP_PORT")).secretValue) || 587;
|
||||
export const getSmtpUsername = async () => (await client.getSecret("SMTP_USERNAME")).secretValue;
|
||||
export const getSmtpPassword = async () => (await client.getSecret("SMTP_PASSWORD")).secretValue;
|
||||
export const getSmtpFromAddress = async () =>
|
||||
(await client.getSecret("SMTP_FROM_ADDRESS")).secretValue;
|
||||
export const getSmtpFromName = async () =>
|
||||
(await client.getSecret("SMTP_FROM_NAME")).secretValue || "Infisical";
|
||||
|
||||
export const getSecretScanningWebhookProxy = async () =>
|
||||
(await client.getSecret("SECRET_SCANNING_WEBHOOK_PROXY")).secretValue;
|
||||
export const getSecretScanningWebhookSecret = async () =>
|
||||
(await client.getSecret("SECRET_SCANNING_WEBHOOK_SECRET")).secretValue;
|
||||
export const getSecretScanningGitAppId = async () =>
|
||||
(await client.getSecret("SECRET_SCANNING_GIT_APP_ID")).secretValue;
|
||||
export const getSecretScanningPrivateKey = async () =>
|
||||
(await client.getSecret("SECRET_SCANNING_PRIVATE_KEY")).secretValue;
|
||||
|
||||
export const getRedisUrl = async () => (await client.getSecret("REDIS_URL")).secretValue;
|
||||
export const getIsInfisicalCloud = async () =>
|
||||
(await client.getSecret("INFISICAL_CLOUD")).secretValue === "true";
|
||||
|
||||
export const getLicenseKey = async () => {
|
||||
const secretValue = (await client.getSecret("LICENSE_KEY")).secretValue;
|
||||
return secretValue === "" ? undefined : secretValue;
|
||||
};
|
||||
export const getLicenseServerKey = async () => {
|
||||
const secretValue = (await client.getSecret("LICENSE_SERVER_KEY")).secretValue;
|
||||
return secretValue === "" ? undefined : secretValue;
|
||||
};
|
||||
export const getLicenseServerUrl = async () =>
|
||||
(await client.getSecret("LICENSE_SERVER_URL")).secretValue || "https://portal.infisical.com";
|
||||
|
||||
export const getTelemetryEnabled = async () =>
|
||||
(await client.getSecret("TELEMETRY_ENABLED")).secretValue !== "false" && true;
|
||||
export const getLoopsApiKey = async () => (await client.getSecret("LOOPS_API_KEY")).secretValue;
|
||||
export const getSmtpConfigured = async () =>
|
||||
(await client.getSecret("SMTP_HOST")).secretValue == "" ||
|
||||
(await client.getSecret("SMTP_HOST")).secretValue == undefined
|
||||
? false
|
||||
: true;
|
||||
export const getHttpsEnabled = async () => {
|
||||
if ((await getNodeEnv()) != "production") {
|
||||
// no https for anything other than prod
|
||||
return false;
|
||||
}
|
||||
|
||||
if (
|
||||
(await client.getSecret("HTTPS_ENABLED")).secretValue == undefined ||
|
||||
(await client.getSecret("HTTPS_ENABLED")).secretValue == ""
|
||||
) {
|
||||
// default when no value present
|
||||
return true;
|
||||
}
|
||||
|
||||
return (await client.getSecret("HTTPS_ENABLED")).secretValue === "true" && true;
|
||||
};
|
@ -1,124 +0,0 @@
|
||||
import axios from "axios";
|
||||
import axiosRetry from "axios-retry";
|
||||
import {
|
||||
getLicenseKeyAuthToken,
|
||||
getLicenseServerKeyAuthToken,
|
||||
setLicenseKeyAuthToken,
|
||||
setLicenseServerKeyAuthToken,
|
||||
} from "./storage";
|
||||
import {
|
||||
getLicenseKey,
|
||||
getLicenseServerKey,
|
||||
getLicenseServerUrl,
|
||||
} from "./index";
|
||||
|
||||
// should have JWT to interact with the license server
|
||||
export const licenseServerKeyRequest = axios.create();
|
||||
export const licenseKeyRequest = axios.create();
|
||||
export const standardRequest = axios.create();
|
||||
|
||||
// add retry functionality to the axios instance
|
||||
axiosRetry(standardRequest, {
|
||||
retries: 3,
|
||||
retryDelay: axiosRetry.exponentialDelay, // exponential back-off delay between retries
|
||||
retryCondition: (error) => {
|
||||
// only retry if the error is a network error or a 5xx server error
|
||||
return axiosRetry.isNetworkError(error) || axiosRetry.isRetryableError(error);
|
||||
},
|
||||
});
|
||||
|
||||
export const refreshLicenseServerKeyToken = async () => {
|
||||
const licenseServerKey = await getLicenseServerKey();
|
||||
const licenseServerUrl = await getLicenseServerUrl();
|
||||
|
||||
const { data: { token } } = await standardRequest.post(
|
||||
`${licenseServerUrl}/api/auth/v1/license-server-login`, {},
|
||||
{
|
||||
headers: {
|
||||
"X-API-KEY": licenseServerKey,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
setLicenseServerKeyAuthToken(token);
|
||||
|
||||
return token;
|
||||
}
|
||||
|
||||
export const refreshLicenseKeyToken = async () => {
|
||||
const licenseKey = await getLicenseKey();
|
||||
const licenseServerUrl = await getLicenseServerUrl();
|
||||
|
||||
const { data: { token } } = await standardRequest.post(
|
||||
`${licenseServerUrl}/api/auth/v1/license-login`, {},
|
||||
{
|
||||
headers: {
|
||||
"X-API-KEY": licenseKey,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
setLicenseKeyAuthToken(token);
|
||||
|
||||
return token;
|
||||
}
|
||||
|
||||
licenseServerKeyRequest.interceptors.request.use((config) => {
|
||||
const token = getLicenseServerKeyAuthToken();
|
||||
|
||||
if (token && config.headers) {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
config.headers.Authorization = `Bearer ${token}`;
|
||||
}
|
||||
return config;
|
||||
}, (err) => {
|
||||
return Promise.reject(err);
|
||||
});
|
||||
|
||||
licenseServerKeyRequest.interceptors.response.use((response) => {
|
||||
return response
|
||||
}, async function (err) {
|
||||
const originalRequest = err.config;
|
||||
|
||||
if (err.response.status === 401 && !originalRequest._retry) {
|
||||
originalRequest._retry = true;
|
||||
|
||||
// refresh
|
||||
const token = await refreshLicenseServerKeyToken();
|
||||
|
||||
axios.defaults.headers.common["Authorization"] = "Bearer " + token;
|
||||
return licenseServerKeyRequest(originalRequest);
|
||||
}
|
||||
|
||||
return Promise.reject(err);
|
||||
});
|
||||
|
||||
licenseKeyRequest.interceptors.request.use((config) => {
|
||||
const token = getLicenseKeyAuthToken();
|
||||
|
||||
if (token && config.headers) {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
config.headers.Authorization = `Bearer ${token}`;
|
||||
}
|
||||
return config;
|
||||
}, (err) => {
|
||||
return Promise.reject(err);
|
||||
});
|
||||
|
||||
licenseKeyRequest.interceptors.response.use((response) => {
|
||||
return response
|
||||
}, async function (err) {
|
||||
const originalRequest = err.config;
|
||||
|
||||
if (err.response.status === 401 && !originalRequest._retry) {
|
||||
originalRequest._retry = true;
|
||||
|
||||
// refresh
|
||||
const token = await refreshLicenseKeyToken();
|
||||
|
||||
axios.defaults.headers.common["Authorization"] = "Bearer " + token;
|
||||
return licenseKeyRequest(originalRequest);
|
||||
}
|
||||
|
||||
return Promise.reject(err);
|
||||
});
|
@ -1,24 +0,0 @@
|
||||
import { IServerConfig, ServerConfig } from "../models/serverConfig";
|
||||
|
||||
let serverConfig: IServerConfig;
|
||||
|
||||
export const serverConfigInit = async () => {
|
||||
const cfg = await ServerConfig.findOne({}).lean();
|
||||
if (!cfg) {
|
||||
const cfg = new ServerConfig();
|
||||
await cfg.save();
|
||||
serverConfig = cfg.toObject();
|
||||
} else {
|
||||
serverConfig = cfg;
|
||||
}
|
||||
return serverConfig;
|
||||
};
|
||||
|
||||
export const getServerConfig = () => serverConfig;
|
||||
|
||||
export const updateServerConfig = async (data: Partial<IServerConfig>) => {
|
||||
const cfg = await ServerConfig.findByIdAndUpdate(serverConfig._id, data, { new: true });
|
||||
if (!cfg) throw new Error("Failed to update server config");
|
||||
serverConfig = cfg.toObject();
|
||||
return serverConfig;
|
||||
};
|
@ -1,30 +0,0 @@
|
||||
const MemoryLicenseServerKeyTokenStorage = () => {
|
||||
let authToken: string;
|
||||
|
||||
return {
|
||||
setToken: (token: string) => {
|
||||
authToken = token;
|
||||
},
|
||||
getToken: () => authToken,
|
||||
};
|
||||
};
|
||||
|
||||
const MemoryLicenseKeyTokenStorage = () => {
|
||||
let authToken: string;
|
||||
|
||||
return {
|
||||
setToken: (token: string) => {
|
||||
authToken = token;
|
||||
},
|
||||
getToken: () => authToken,
|
||||
};
|
||||
};
|
||||
|
||||
const licenseServerTokenStorage = MemoryLicenseServerKeyTokenStorage();
|
||||
const licenseTokenStorage = MemoryLicenseKeyTokenStorage();
|
||||
|
||||
export const getLicenseServerKeyAuthToken = licenseServerTokenStorage.getToken;
|
||||
export const setLicenseServerKeyAuthToken = licenseServerTokenStorage.setToken;
|
||||
|
||||
export const getLicenseKeyAuthToken = licenseTokenStorage.getToken;
|
||||
export const setLicenseKeyAuthToken = licenseTokenStorage.setToken;
|
@ -1,101 +0,0 @@
|
||||
import { Request, Response } from "express";
|
||||
import { getHttpsEnabled, getIsMigrationMode } from "../../config";
|
||||
import { getServerConfig, updateServerConfig as setServerConfig } from "../../config/serverConfig";
|
||||
import { initializeDefaultOrg, issueAuthTokens } from "../../helpers";
|
||||
import { validateRequest } from "../../helpers/validation";
|
||||
import { User } from "../../models";
|
||||
import { TelemetryService } from "../../services";
|
||||
import { BadRequestError, UnauthorizedRequestError } from "../../utils/errors";
|
||||
import * as reqValidator from "../../validation/admin";
|
||||
|
||||
export const getServerConfigInfo = async (_req: Request, res: Response) => {
|
||||
const config = getServerConfig();
|
||||
const isMigrationModeOn = await getIsMigrationMode();
|
||||
return res.send({ config: { ...config, isMigrationModeOn } });
|
||||
};
|
||||
|
||||
export const updateServerConfig = async (req: Request, res: Response) => {
|
||||
const {
|
||||
body: { allowSignUp }
|
||||
} = await validateRequest(reqValidator.UpdateServerConfigV1, req);
|
||||
const config = await setServerConfig({ allowSignUp });
|
||||
return res.send({ config });
|
||||
};
|
||||
|
||||
export const adminSignUp = async (req: Request, res: Response) => {
|
||||
const cfg = getServerConfig();
|
||||
if (cfg.initialized) throw UnauthorizedRequestError({ message: "Admin has been created" });
|
||||
const {
|
||||
body: {
|
||||
email,
|
||||
publicKey,
|
||||
salt,
|
||||
lastName,
|
||||
verifier,
|
||||
firstName,
|
||||
protectedKey,
|
||||
protectedKeyIV,
|
||||
protectedKeyTag,
|
||||
encryptedPrivateKey,
|
||||
encryptedPrivateKeyIV,
|
||||
encryptedPrivateKeyTag
|
||||
}
|
||||
} = await validateRequest(reqValidator.SignupV1, req);
|
||||
let user = await User.findOne({ email });
|
||||
if (user) throw BadRequestError({ message: "User already exist" });
|
||||
user = new User({
|
||||
email,
|
||||
firstName,
|
||||
lastName,
|
||||
encryptionVersion: 2,
|
||||
protectedKey,
|
||||
protectedKeyIV,
|
||||
protectedKeyTag,
|
||||
publicKey,
|
||||
encryptedPrivateKey,
|
||||
iv: encryptedPrivateKeyIV,
|
||||
tag: encryptedPrivateKeyTag,
|
||||
salt,
|
||||
verifier,
|
||||
superAdmin: true
|
||||
});
|
||||
await user.save();
|
||||
await initializeDefaultOrg({ organizationName: "Admin Org", user });
|
||||
|
||||
await setServerConfig({ initialized: true });
|
||||
|
||||
// issue tokens
|
||||
const tokens = await issueAuthTokens({
|
||||
userId: user._id,
|
||||
ip: req.realIP,
|
||||
userAgent: req.headers["user-agent"] ?? ""
|
||||
});
|
||||
|
||||
const token = tokens.token;
|
||||
|
||||
const postHogClient = await TelemetryService.getPostHogClient();
|
||||
if (postHogClient) {
|
||||
postHogClient.capture({
|
||||
event: "admin initialization",
|
||||
properties: {
|
||||
email: user.email,
|
||||
lastName,
|
||||
firstName
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// store (refresh) token in httpOnly cookie
|
||||
res.cookie("jid", tokens.refreshToken, {
|
||||
httpOnly: true,
|
||||
path: "/",
|
||||
sameSite: "strict",
|
||||
secure: await getHttpsEnabled()
|
||||
});
|
||||
|
||||
return res.status(200).send({
|
||||
message: "Successfully set up admin account",
|
||||
user,
|
||||
token
|
||||
});
|
||||
};
|
@ -1,277 +0,0 @@
|
||||
import { Request, Response } from "express";
|
||||
import jwt from "jsonwebtoken";
|
||||
import * as bigintConversion from "bigint-conversion";
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const jsrp = require("jsrp");
|
||||
import {
|
||||
LoginSRPDetail,
|
||||
TokenVersion,
|
||||
User
|
||||
} from "../../models";
|
||||
import { clearTokens, createToken, issueAuthTokens } from "../../helpers/auth";
|
||||
import { checkUserDevice } from "../../helpers/user";
|
||||
import { AuthTokenType } from "../../variables";
|
||||
import {
|
||||
BadRequestError,
|
||||
UnauthorizedRequestError
|
||||
} from "../../utils/errors";
|
||||
import {
|
||||
getAuthSecret,
|
||||
getHttpsEnabled,
|
||||
getJwtAuthLifetime,
|
||||
} from "../../config";
|
||||
import { ActorType } from "../../ee/models";
|
||||
import { validateRequest } from "../../helpers/validation";
|
||||
import * as reqValidator from "../../validation/auth";
|
||||
|
||||
declare module "jsonwebtoken" {
|
||||
export interface AuthnJwtPayload extends jwt.JwtPayload {
|
||||
authTokenType: AuthTokenType;
|
||||
}
|
||||
export interface UserIDJwtPayload extends jwt.JwtPayload {
|
||||
userId: string;
|
||||
refreshVersion?: number;
|
||||
}
|
||||
export interface IdentityAccessTokenJwtPayload extends jwt.JwtPayload {
|
||||
_id: string;
|
||||
clientSecretId: string;
|
||||
identityAccessTokenId: string;
|
||||
authTokenType: string;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Log in user step 1: Return [salt] and [serverPublicKey] as part of step 1 of SRP protocol
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
export const login1 = async (req: Request, res: Response) => {
|
||||
const {
|
||||
body: { email, clientPublicKey }
|
||||
} = await validateRequest(reqValidator.Login1V1, req);
|
||||
|
||||
const user = await User.findOne({
|
||||
email
|
||||
}).select("+salt +verifier");
|
||||
|
||||
if (!user) throw new Error("Failed to find user");
|
||||
|
||||
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(
|
||||
{ email: email },
|
||||
{
|
||||
email: email,
|
||||
clientPublicKey: clientPublicKey,
|
||||
serverBInt: bigintConversion.bigintToBuf(server.bInt)
|
||||
},
|
||||
{ upsert: true, returnNewDocument: false }
|
||||
);
|
||||
|
||||
return res.status(200).send({
|
||||
serverPublicKey,
|
||||
salt: user.salt
|
||||
});
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Log in user step 2: complete step 2 of SRP protocol and return token and their (encrypted)
|
||||
* private key
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
export const login2 = async (req: Request, res: Response) => {
|
||||
const {
|
||||
body: { email, clientProof }
|
||||
} = await validateRequest(reqValidator.Login2V1, req);
|
||||
|
||||
const user = await User.findOne({
|
||||
email
|
||||
}).select("+salt +verifier +publicKey +encryptedPrivateKey +iv +tag");
|
||||
|
||||
if (!user) throw new Error("Failed to find user");
|
||||
|
||||
const loginSRPDetailFromDB = await LoginSRPDetail.findOneAndDelete({ email: email });
|
||||
|
||||
if (!loginSRPDetailFromDB) {
|
||||
return BadRequestError(
|
||||
Error(
|
||||
"It looks like some details from the first login are not found. Please try login one again"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const server = new jsrp.server();
|
||||
server.init(
|
||||
{
|
||||
salt: user.salt,
|
||||
verifier: user.verifier,
|
||||
b: loginSRPDetailFromDB.serverBInt
|
||||
},
|
||||
async () => {
|
||||
server.setClientPublicKey(loginSRPDetailFromDB.clientPublicKey);
|
||||
|
||||
// compare server and client shared keys
|
||||
if (server.checkClientProof(clientProof)) {
|
||||
// issue tokens
|
||||
|
||||
await checkUserDevice({
|
||||
user,
|
||||
ip: req.realIP,
|
||||
userAgent: req.headers["user-agent"] ?? ""
|
||||
});
|
||||
|
||||
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()
|
||||
});
|
||||
|
||||
// return (access) token in response
|
||||
return res.status(200).send({
|
||||
token: tokens.token,
|
||||
publicKey: user.publicKey,
|
||||
encryptedPrivateKey: user.encryptedPrivateKey,
|
||||
iv: user.iv,
|
||||
tag: user.tag
|
||||
});
|
||||
}
|
||||
|
||||
return res.status(400).send({
|
||||
message: "Failed to authenticate. Try again?"
|
||||
});
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Log out user
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
export const logout = async (req: Request, res: Response) => {
|
||||
if (req.authData.actor.type === ActorType.USER && req.authData.tokenVersionId) {
|
||||
await clearTokens(req.authData.tokenVersionId);
|
||||
}
|
||||
|
||||
// clear httpOnly cookie
|
||||
res.cookie("jid", "", {
|
||||
httpOnly: true,
|
||||
path: "/",
|
||||
sameSite: "strict",
|
||||
secure: (await getHttpsEnabled()) as boolean
|
||||
});
|
||||
|
||||
return res.status(200).send({
|
||||
message: "Successfully logged out."
|
||||
});
|
||||
};
|
||||
|
||||
export const revokeAllSessions = async (req: Request, res: Response) => {
|
||||
await TokenVersion.updateMany(
|
||||
{
|
||||
user: req.user._id
|
||||
},
|
||||
{
|
||||
$inc: {
|
||||
refreshVersion: 1,
|
||||
accessVersion: 1
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
return res.status(200).send({
|
||||
message: "Successfully revoked all sessions."
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Return user is authenticated
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
export const checkAuth = async (req: Request, res: Response) => {
|
||||
return res.status(200).send({
|
||||
message: "Authenticated"
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Return new JWT access token by first validating the refresh token
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
export const getNewToken = async (req: Request, res: Response) => {
|
||||
|
||||
const refreshToken = req.cookies.jid;
|
||||
|
||||
if (!refreshToken)
|
||||
throw BadRequestError({
|
||||
message: "Failed to find refresh token in request cookies"
|
||||
});
|
||||
|
||||
const decodedToken = <jwt.UserIDJwtPayload>jwt.verify(refreshToken, await getAuthSecret());
|
||||
|
||||
if (decodedToken.authTokenType !== AuthTokenType.REFRESH_TOKEN) throw UnauthorizedRequestError();
|
||||
|
||||
const user = await User.findOne({
|
||||
_id: decodedToken.userId
|
||||
}).select("+publicKey +refreshVersion +accessVersion");
|
||||
|
||||
if (!user) throw new Error("Failed to authenticate unfound user");
|
||||
if (!user?.publicKey) throw new Error("Failed to authenticate not fully set up account");
|
||||
|
||||
const tokenVersion = await TokenVersion.findById(decodedToken.tokenVersionId);
|
||||
|
||||
if (!tokenVersion)
|
||||
throw UnauthorizedRequestError({
|
||||
message: "Failed to validate refresh token"
|
||||
});
|
||||
|
||||
if (decodedToken.refreshVersion !== tokenVersion.refreshVersion)
|
||||
throw BadRequestError({
|
||||
message: "Failed to validate refresh token"
|
||||
});
|
||||
|
||||
const token = createToken({
|
||||
payload: {
|
||||
authTokenType: AuthTokenType.ACCESS_TOKEN,
|
||||
userId: decodedToken.userId,
|
||||
tokenVersionId: tokenVersion._id.toString(),
|
||||
accessVersion: tokenVersion.refreshVersion
|
||||
},
|
||||
expiresIn: await getJwtAuthLifetime(),
|
||||
secret: await getAuthSecret()
|
||||
});
|
||||
|
||||
return res.status(200).send({
|
||||
token
|
||||
});
|
||||
};
|
||||
|
||||
export const handleAuthProviderCallback = (req: Request, res: Response) => {
|
||||
res.redirect(`/login/provider/success?token=${encodeURIComponent(req.providerAuthToken)}`);
|
||||
};
|
@ -1,135 +0,0 @@
|
||||
import { Request, Response } from "express";
|
||||
import { Types } from "mongoose";
|
||||
import { Bot, BotKey } from "../../models";
|
||||
import { createBot } from "../../helpers/bot";
|
||||
import { validateRequest } from "../../helpers/validation";
|
||||
import * as reqValidator from "../../validation/bot";
|
||||
import {
|
||||
ProjectPermissionActions,
|
||||
ProjectPermissionSub,
|
||||
getAuthDataProjectPermissions
|
||||
} from "../../ee/services/ProjectRoleService";
|
||||
import { ForbiddenError } from "@casl/ability";
|
||||
import { BadRequestError } from "../../utils/errors";
|
||||
|
||||
interface BotKey {
|
||||
encryptedKey: string;
|
||||
nonce: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return bot for workspace with id [workspaceId]. If a workspace bot doesn't exist,
|
||||
* then create and return a new bot.
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
export const getBotByWorkspaceId = async (req: Request, res: Response) => {
|
||||
const {
|
||||
params: { workspaceId }
|
||||
} = await validateRequest(reqValidator.GetBotByWorkspaceIdV1, req);
|
||||
const { permission } = await getAuthDataProjectPermissions({
|
||||
authData: req.authData,
|
||||
workspaceId: new Types.ObjectId(workspaceId)
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Read,
|
||||
ProjectPermissionSub.Integrations
|
||||
);
|
||||
|
||||
let bot = await Bot.findOne({
|
||||
workspace: workspaceId
|
||||
});
|
||||
|
||||
if (!bot) {
|
||||
// case: bot doesn't exist for workspace with id [workspaceId]
|
||||
// -> create a new bot and return it
|
||||
bot = await createBot({
|
||||
name: "Infisical Bot",
|
||||
workspaceId: new Types.ObjectId(workspaceId)
|
||||
});
|
||||
}
|
||||
|
||||
return res.status(200).send({
|
||||
bot
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Return bot with id [req.bot._id] with active state set to [isActive].
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
export const setBotActiveState = async (req: Request, res: Response) => {
|
||||
const {
|
||||
body: { botKey, isActive },
|
||||
params: { botId }
|
||||
} = await validateRequest(reqValidator.SetBotActiveStateV1, req);
|
||||
|
||||
const bot = await Bot.findById(botId);
|
||||
if (!bot) {
|
||||
throw BadRequestError({ message: "Bot not found" });
|
||||
}
|
||||
const userId = req.user._id;
|
||||
|
||||
const { permission } = await getAuthDataProjectPermissions({
|
||||
authData: req.authData,
|
||||
workspaceId: bot.workspace
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Edit,
|
||||
ProjectPermissionSub.Integrations
|
||||
);
|
||||
|
||||
if (isActive) {
|
||||
// bot state set to active -> share workspace key with bot
|
||||
if (!botKey?.encryptedKey || !botKey?.nonce) {
|
||||
return res.status(400).send({
|
||||
message: "Failed to set bot state to active - missing bot key"
|
||||
});
|
||||
}
|
||||
|
||||
await BotKey.findOneAndUpdate(
|
||||
{
|
||||
workspace: bot.workspace
|
||||
},
|
||||
{
|
||||
encryptedKey: botKey.encryptedKey,
|
||||
nonce: botKey.nonce,
|
||||
sender: userId,
|
||||
bot: bot._id,
|
||||
workspace: bot.workspace
|
||||
},
|
||||
{
|
||||
upsert: true,
|
||||
new: true
|
||||
}
|
||||
);
|
||||
} else {
|
||||
// case: bot state set to inactive -> delete bot's workspace key
|
||||
await BotKey.deleteOne({
|
||||
bot: bot._id
|
||||
});
|
||||
}
|
||||
|
||||
const updatedBot = await Bot.findOneAndUpdate(
|
||||
{
|
||||
_id: bot._id
|
||||
},
|
||||
{
|
||||
isActive
|
||||
},
|
||||
{
|
||||
new: true
|
||||
}
|
||||
);
|
||||
|
||||
if (!updatedBot) throw new Error("Failed to update bot active state");
|
||||
|
||||
return res.status(200).send({
|
||||
bot
|
||||
});
|
||||
};
|
@ -1,43 +0,0 @@
|
||||
import * as authController from "./authController";
|
||||
import * as universalAuthController from "./universalAuthController";
|
||||
import * as botController from "./botController";
|
||||
import * as integrationAuthController from "./integrationAuthController";
|
||||
import * as integrationController from "./integrationController";
|
||||
import * as keyController from "./keyController";
|
||||
import * as membershipController from "./membershipController";
|
||||
import * as membershipOrgController from "./membershipOrgController";
|
||||
import * as organizationController from "./organizationController";
|
||||
import * as passwordController from "./passwordController";
|
||||
import * as secretController from "./secretController";
|
||||
import * as serviceTokenController from "./serviceTokenController";
|
||||
import * as signupController from "./signupController";
|
||||
import * as userActionController from "./userActionController";
|
||||
import * as userController from "./userController";
|
||||
import * as workspaceController from "./workspaceController";
|
||||
import * as secretScanningController from "./secretScanningController";
|
||||
import * as webhookController from "./webhookController";
|
||||
import * as secretImpsController from "./secretImpsController";
|
||||
import * as adminController from "./adminController";
|
||||
|
||||
export {
|
||||
authController,
|
||||
universalAuthController,
|
||||
botController,
|
||||
integrationAuthController,
|
||||
integrationController,
|
||||
keyController,
|
||||
membershipController,
|
||||
membershipOrgController,
|
||||
organizationController,
|
||||
passwordController,
|
||||
secretController,
|
||||
serviceTokenController,
|
||||
signupController,
|
||||
userActionController,
|
||||
userController,
|
||||
workspaceController,
|
||||
secretScanningController,
|
||||
webhookController,
|
||||
secretImpsController,
|
||||
adminController
|
||||
};
|
File diff suppressed because it is too large
Load Diff
@ -1,322 +0,0 @@
|
||||
import { Request, Response } from "express";
|
||||
import { Types } from "mongoose";
|
||||
import { Folder, IWorkspace, Integration, IntegrationAuth } from "../../models";
|
||||
import { EventService } from "../../services";
|
||||
import { eventStartIntegration } from "../../events";
|
||||
import { getFolderByPath } from "../../services/FolderService";
|
||||
import { BadRequestError } from "../../utils/errors";
|
||||
import { EEAuditLogService } from "../../ee/services";
|
||||
import { EventType } from "../../ee/models";
|
||||
import { syncSecretsToActiveIntegrationsQueue } from "../../queues/integrations/syncSecretsToThirdPartyServices";
|
||||
import { validateRequest } from "../../helpers/validation";
|
||||
import * as reqValidator from "../../validation/integration";
|
||||
import {
|
||||
ProjectPermissionActions,
|
||||
ProjectPermissionSub,
|
||||
getAuthDataProjectPermissions
|
||||
} from "../../ee/services/ProjectRoleService";
|
||||
import { ForbiddenError } from "@casl/ability";
|
||||
|
||||
/**
|
||||
* Create/initialize an (empty) integration for integration authorization
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
export const createIntegration = async (req: Request, res: Response) => {
|
||||
const {
|
||||
body: {
|
||||
isActive,
|
||||
sourceEnvironment,
|
||||
secretPath,
|
||||
app,
|
||||
path,
|
||||
appId,
|
||||
owner,
|
||||
region,
|
||||
scope,
|
||||
targetService,
|
||||
targetServiceId,
|
||||
integrationAuthId,
|
||||
targetEnvironment,
|
||||
targetEnvironmentId,
|
||||
metadata
|
||||
}
|
||||
} = await validateRequest(reqValidator.CreateIntegrationV1, req);
|
||||
|
||||
const integrationAuth = await IntegrationAuth.findById(integrationAuthId)
|
||||
.populate<{ workspace: IWorkspace }>("workspace")
|
||||
.select(
|
||||
"+refreshCiphertext +refreshIV +refreshTag +accessCiphertext +accessIV +accessTag +accessExpiresAt"
|
||||
);
|
||||
|
||||
if (!integrationAuth) throw BadRequestError({ message: "Integration auth not found" });
|
||||
|
||||
const { permission } = await getAuthDataProjectPermissions({
|
||||
authData: req.authData,
|
||||
workspaceId: integrationAuth.workspace._id
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Create,
|
||||
ProjectPermissionSub.Integrations
|
||||
);
|
||||
|
||||
const folders = await Folder.findOne({
|
||||
workspace: integrationAuth.workspace._id,
|
||||
environment: sourceEnvironment
|
||||
});
|
||||
|
||||
if (folders) {
|
||||
const folder = getFolderByPath(folders.nodes, secretPath);
|
||||
if (!folder) {
|
||||
throw BadRequestError({
|
||||
message: "Folder path doesn't exist"
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: validate [sourceEnvironment] and [targetEnvironment]
|
||||
|
||||
// initialize new integration after saving integration access token
|
||||
const integration = await new Integration({
|
||||
workspace: integrationAuth.workspace._id,
|
||||
environment: sourceEnvironment,
|
||||
isActive,
|
||||
app,
|
||||
appId,
|
||||
targetEnvironment,
|
||||
targetEnvironmentId,
|
||||
targetService,
|
||||
targetServiceId,
|
||||
owner,
|
||||
path,
|
||||
region,
|
||||
scope,
|
||||
secretPath,
|
||||
integration: integrationAuth.integration,
|
||||
integrationAuth: new Types.ObjectId(integrationAuthId),
|
||||
metadata
|
||||
}).save();
|
||||
|
||||
if (integration) {
|
||||
// trigger event - push secrets
|
||||
EventService.handleEvent({
|
||||
event: eventStartIntegration({
|
||||
workspaceId: integration.workspace,
|
||||
environment: sourceEnvironment
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
await EEAuditLogService.createAuditLog(
|
||||
req.authData,
|
||||
{
|
||||
type: EventType.CREATE_INTEGRATION,
|
||||
metadata: {
|
||||
integrationId: integration._id.toString(),
|
||||
integration: integration.integration,
|
||||
environment: integration.environment,
|
||||
secretPath,
|
||||
url: integration.url,
|
||||
app: integration.app,
|
||||
appId: integration.appId,
|
||||
targetEnvironment: integration.targetEnvironment,
|
||||
targetEnvironmentId: integration.targetEnvironmentId,
|
||||
targetService: integration.targetService,
|
||||
targetServiceId: integration.targetServiceId,
|
||||
path: integration.path,
|
||||
region: integration.region
|
||||
}
|
||||
},
|
||||
{
|
||||
workspaceId: integration.workspace
|
||||
}
|
||||
);
|
||||
|
||||
return res.status(200).send({
|
||||
integration
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Change environment or name of integration with id [integrationId]
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
export const updateIntegration = async (req: Request, res: Response) => {
|
||||
// TODO: add integration-specific validation to ensure that each
|
||||
// integration has the correct fields populated in [Integration]
|
||||
|
||||
const {
|
||||
body: {
|
||||
environment,
|
||||
isActive,
|
||||
app,
|
||||
appId,
|
||||
targetEnvironment,
|
||||
owner, // github-specific integration param
|
||||
secretPath
|
||||
},
|
||||
params: { integrationId }
|
||||
} = await validateRequest(reqValidator.UpdateIntegrationV1, req);
|
||||
|
||||
const integration = await Integration.findById(integrationId);
|
||||
if (!integration) throw BadRequestError({ message: "Integration not found" });
|
||||
|
||||
const { permission } = await getAuthDataProjectPermissions({
|
||||
authData: req.authData,
|
||||
workspaceId: integration.workspace
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Edit,
|
||||
ProjectPermissionSub.Integrations
|
||||
);
|
||||
|
||||
const folders = await Folder.findOne({
|
||||
workspace: integration.workspace,
|
||||
environment
|
||||
});
|
||||
|
||||
if (folders) {
|
||||
const folder = getFolderByPath(folders.nodes, secretPath);
|
||||
if (!folder) {
|
||||
throw BadRequestError({
|
||||
message: "Path for service token does not exist"
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const updatedIntegration = await Integration.findOneAndUpdate(
|
||||
{
|
||||
_id: integration._id
|
||||
},
|
||||
{
|
||||
environment,
|
||||
isActive,
|
||||
app,
|
||||
appId,
|
||||
targetEnvironment,
|
||||
owner,
|
||||
secretPath
|
||||
},
|
||||
{
|
||||
new: true
|
||||
}
|
||||
);
|
||||
|
||||
if (updatedIntegration) {
|
||||
// trigger event - push secrets
|
||||
EventService.handleEvent({
|
||||
event: eventStartIntegration({
|
||||
workspaceId: updatedIntegration.workspace,
|
||||
environment
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
return res.status(200).send({
|
||||
integration: updatedIntegration
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Delete integration with id [integrationId]
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
export const deleteIntegration = async (req: Request, res: Response) => {
|
||||
const {
|
||||
params: { integrationId }
|
||||
} = await validateRequest(reqValidator.DeleteIntegrationV1, req);
|
||||
|
||||
const integration = await Integration.findById(integrationId);
|
||||
if (!integration) throw BadRequestError({ message: "Integration not found" });
|
||||
|
||||
const { permission } = await getAuthDataProjectPermissions({
|
||||
authData: req.authData,
|
||||
workspaceId: integration.workspace
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Delete,
|
||||
ProjectPermissionSub.Integrations
|
||||
);
|
||||
|
||||
const deletedIntegration = await Integration.findOneAndDelete({
|
||||
_id: integrationId
|
||||
});
|
||||
|
||||
if (!deletedIntegration) throw new Error("Failed to find integration");
|
||||
|
||||
const numOtherIntegrationsUsingSameAuth = await Integration.countDocuments({
|
||||
integrationAuth: deletedIntegration.integrationAuth,
|
||||
_id: {
|
||||
$nin: [deletedIntegration._id]
|
||||
}
|
||||
});
|
||||
|
||||
if (numOtherIntegrationsUsingSameAuth === 0) {
|
||||
// no other integrations are using the same integration auth
|
||||
// -> delete integration auth associated with the integration being deleted
|
||||
await IntegrationAuth.deleteOne({
|
||||
_id: deletedIntegration.integrationAuth
|
||||
});
|
||||
}
|
||||
|
||||
await EEAuditLogService.createAuditLog(
|
||||
req.authData,
|
||||
{
|
||||
type: EventType.DELETE_INTEGRATION,
|
||||
metadata: {
|
||||
integrationId: integration._id.toString(),
|
||||
integration: integration.integration,
|
||||
environment: integration.environment,
|
||||
secretPath: integration.secretPath,
|
||||
url: integration.url,
|
||||
app: integration.app,
|
||||
appId: integration.appId,
|
||||
targetEnvironment: integration.targetEnvironment,
|
||||
targetEnvironmentId: integration.targetEnvironmentId,
|
||||
targetService: integration.targetService,
|
||||
targetServiceId: integration.targetServiceId,
|
||||
path: integration.path,
|
||||
region: integration.region
|
||||
}
|
||||
},
|
||||
{
|
||||
workspaceId: integration.workspace
|
||||
}
|
||||
);
|
||||
|
||||
return res.status(200).send({
|
||||
integration
|
||||
});
|
||||
};
|
||||
|
||||
// Will trigger sync for all integrations within the given env and workspace id
|
||||
export const manualSync = async (req: Request, res: Response) => {
|
||||
const {
|
||||
body: { workspaceId, environment }
|
||||
} = await validateRequest(reqValidator.ManualSyncV1, req);
|
||||
|
||||
const { permission } = await getAuthDataProjectPermissions({
|
||||
authData: req.authData,
|
||||
workspaceId: new Types.ObjectId(workspaceId)
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Edit,
|
||||
ProjectPermissionSub.Integrations
|
||||
);
|
||||
|
||||
syncSecretsToActiveIntegrationsQueue({
|
||||
workspaceId,
|
||||
environment
|
||||
});
|
||||
|
||||
res.status(200).send();
|
||||
};
|
@ -1,101 +0,0 @@
|
||||
import { Types } from "mongoose";
|
||||
import { Request, Response } from "express";
|
||||
import { Key } from "../../models";
|
||||
import { findMembership } from "../../helpers/membership";
|
||||
import { EventType } from "../../ee/models";
|
||||
import { EEAuditLogService } from "../../ee/services";
|
||||
import { validateRequest } from "../../helpers/validation";
|
||||
import * as reqValidator from "../../validation/key";
|
||||
import {
|
||||
ProjectPermissionActions,
|
||||
ProjectPermissionSub,
|
||||
getAuthDataProjectPermissions
|
||||
} from "../../ee/services/ProjectRoleService";
|
||||
import { ForbiddenError } from "@casl/ability";
|
||||
|
||||
/**
|
||||
* Add (encrypted) copy of workspace key for workspace with id [workspaceId] for user with
|
||||
* id [key.userId]
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
export const uploadKey = async (req: Request, res: Response) => {
|
||||
const {
|
||||
params: { workspaceId },
|
||||
body: { key }
|
||||
} = await validateRequest(reqValidator.UploadKeyV1, req);
|
||||
|
||||
const { permission } = await getAuthDataProjectPermissions({
|
||||
authData: req.authData,
|
||||
workspaceId: new Types.ObjectId(workspaceId)
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Edit,
|
||||
ProjectPermissionSub.Member
|
||||
);
|
||||
|
||||
// validate membership of receiver
|
||||
const receiverMembership = await findMembership({
|
||||
user: key.userId,
|
||||
workspace: workspaceId
|
||||
});
|
||||
|
||||
if (!receiverMembership) {
|
||||
throw new Error("Failed receiver membership validation for workspace");
|
||||
}
|
||||
|
||||
await new Key({
|
||||
encryptedKey: key.encryptedKey,
|
||||
nonce: key.nonce,
|
||||
sender: req.user._id,
|
||||
receiver: key.userId,
|
||||
workspace: workspaceId
|
||||
}).save();
|
||||
|
||||
return res.status(200).send({
|
||||
message: "Successfully uploaded key to workspace"
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Return latest (encrypted) copy of workspace key for user
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
export const getLatestKey = async (req: Request, res: Response) => {
|
||||
const {
|
||||
params: { workspaceId }
|
||||
} = await validateRequest(reqValidator.GetLatestKeyV1, req);
|
||||
|
||||
// get latest key
|
||||
const latestKey = await Key.find({
|
||||
workspace: workspaceId,
|
||||
receiver: req.user._id
|
||||
})
|
||||
.sort({ createdAt: -1 })
|
||||
.limit(1)
|
||||
.populate("sender", "+publicKey");
|
||||
|
||||
const resObj: any = {};
|
||||
|
||||
if (latestKey.length > 0) {
|
||||
resObj["latestKey"] = latestKey[0];
|
||||
await EEAuditLogService.createAuditLog(
|
||||
req.authData,
|
||||
{
|
||||
type: EventType.GET_WORKSPACE_KEY,
|
||||
metadata: {
|
||||
keyId: latestKey[0]._id.toString()
|
||||
}
|
||||
},
|
||||
{
|
||||
workspaceId: new Types.ObjectId(workspaceId)
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
return res.status(200).send(resObj);
|
||||
};
|
@ -1,286 +0,0 @@
|
||||
import { Request, Response } from "express";
|
||||
import { Types } from "mongoose";
|
||||
import { IUser, Key, Membership, MembershipOrg, User, Workspace } from "../../models";
|
||||
import { EventType, Role } from "../../ee/models";
|
||||
import { deleteMembership as deleteMember, findMembership } from "../../helpers/membership";
|
||||
import { sendMail } from "../../helpers/nodemailer";
|
||||
import { ACCEPTED, ADMIN, CUSTOM, MEMBER, NO_ACCESS, VIEWER } from "../../variables";
|
||||
import { getSiteURL } from "../../config";
|
||||
import { EEAuditLogService, EELicenseService } from "../../ee/services";
|
||||
import { validateRequest } from "../../helpers/validation";
|
||||
import * as reqValidator from "../../validation/membership";
|
||||
import {
|
||||
ProjectPermissionActions,
|
||||
ProjectPermissionSub,
|
||||
getAuthDataProjectPermissions
|
||||
} from "../../ee/services/ProjectRoleService";
|
||||
import { ForbiddenError } from "@casl/ability";
|
||||
import { BadRequestError } from "../../utils/errors";
|
||||
import { InviteUserToWorkspaceV1 } from "../../validation/workspace";
|
||||
|
||||
/**
|
||||
* Check that user is a member of workspace with id [workspaceId]
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
export const validateMembership = async (req: Request, res: Response) => {
|
||||
const {
|
||||
params: { workspaceId }
|
||||
} = await validateRequest(reqValidator.ValidateMembershipV1, req);
|
||||
|
||||
// validate membership
|
||||
const membership = await findMembership({
|
||||
user: req.user._id,
|
||||
workspace: workspaceId
|
||||
});
|
||||
|
||||
if (!membership) {
|
||||
throw new Error("Failed to validate membership");
|
||||
}
|
||||
|
||||
return res.status(200).send({
|
||||
message: "Workspace membership confirmed"
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Delete membership with id [membershipId]
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
export const deleteMembership = async (req: Request, res: Response) => {
|
||||
const {
|
||||
params: { membershipId }
|
||||
} = await validateRequest(reqValidator.DeleteMembershipV1, req);
|
||||
|
||||
// check if membership to delete exists
|
||||
const membershipToDelete = await Membership.findOne({
|
||||
_id: membershipId
|
||||
}).populate<{ user: IUser }>("user");
|
||||
|
||||
if (!membershipToDelete) {
|
||||
throw new Error("Failed to delete workspace membership that doesn't exist");
|
||||
}
|
||||
|
||||
const { permission } = await getAuthDataProjectPermissions({
|
||||
authData: req.authData,
|
||||
workspaceId: membershipToDelete.workspace
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Delete,
|
||||
ProjectPermissionSub.Member
|
||||
);
|
||||
|
||||
// delete workspace membership
|
||||
const deletedMembership = await deleteMember({
|
||||
membershipId: membershipToDelete._id.toString()
|
||||
});
|
||||
|
||||
await EEAuditLogService.createAuditLog(
|
||||
req.authData,
|
||||
{
|
||||
type: EventType.REMOVE_WORKSPACE_MEMBER,
|
||||
metadata: {
|
||||
userId: membershipToDelete.user._id.toString(),
|
||||
email: membershipToDelete.user.email
|
||||
}
|
||||
},
|
||||
{
|
||||
workspaceId: membershipToDelete.workspace
|
||||
}
|
||||
);
|
||||
|
||||
return res.status(200).send({
|
||||
deletedMembership
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Change and return workspace membership role
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
export const changeMembershipRole = async (req: Request, res: Response) => {
|
||||
const {
|
||||
body: { role },
|
||||
params: { membershipId }
|
||||
} = await validateRequest(reqValidator.ChangeMembershipRoleV1, req);
|
||||
|
||||
// validate target membership
|
||||
const membershipToChangeRole = await Membership.findById(membershipId).populate<{ user: IUser }>(
|
||||
"user"
|
||||
);
|
||||
|
||||
if (!membershipToChangeRole) {
|
||||
throw new Error("Failed to find membership to change role");
|
||||
}
|
||||
|
||||
const { permission } = await getAuthDataProjectPermissions({
|
||||
authData: req.authData,
|
||||
workspaceId: membershipToChangeRole.workspace
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Edit,
|
||||
ProjectPermissionSub.Member
|
||||
);
|
||||
|
||||
const isCustomRole = ![ADMIN, MEMBER, VIEWER, NO_ACCESS].includes(role);
|
||||
if (isCustomRole) {
|
||||
const wsRole = await Role.findOne({
|
||||
slug: role,
|
||||
isOrgRole: false,
|
||||
workspace: membershipToChangeRole.workspace
|
||||
});
|
||||
if (!wsRole) throw BadRequestError({ message: "Role not found" });
|
||||
|
||||
const plan = await EELicenseService.getPlan(wsRole.organization);
|
||||
|
||||
if (!plan.rbac) return res.status(400).send({
|
||||
message: "Failed to assign custom role due to RBAC restriction. Upgrade plan to assign custom role to member."
|
||||
});
|
||||
|
||||
const membership = await Membership.findByIdAndUpdate(membershipId, {
|
||||
role: CUSTOM,
|
||||
customRole: wsRole
|
||||
});
|
||||
return res.status(200).send({
|
||||
membership
|
||||
});
|
||||
}
|
||||
|
||||
const membership = await Membership.findByIdAndUpdate(
|
||||
membershipId,
|
||||
{
|
||||
$set: {
|
||||
role
|
||||
},
|
||||
$unset: {
|
||||
customRole: 1
|
||||
}
|
||||
},
|
||||
{
|
||||
new: true
|
||||
}
|
||||
);
|
||||
|
||||
await EEAuditLogService.createAuditLog(
|
||||
req.authData,
|
||||
{
|
||||
type: EventType.UPDATE_USER_WORKSPACE_ROLE,
|
||||
metadata: {
|
||||
userId: membershipToChangeRole.user._id.toString(),
|
||||
email: membershipToChangeRole.user.email,
|
||||
oldRole: membershipToChangeRole.role,
|
||||
newRole: role
|
||||
}
|
||||
},
|
||||
{
|
||||
workspaceId: membershipToChangeRole.workspace
|
||||
}
|
||||
);
|
||||
|
||||
return res.status(200).send({
|
||||
membership
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Add user with email [email] to workspace with id [workspaceId]
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
export const inviteUserToWorkspace = async (req: Request, res: Response) => {
|
||||
const {
|
||||
params: { workspaceId },
|
||||
body: { email }
|
||||
} = await validateRequest(InviteUserToWorkspaceV1, req);
|
||||
|
||||
const { permission } = await getAuthDataProjectPermissions({
|
||||
authData: req.authData,
|
||||
workspaceId: new Types.ObjectId(workspaceId)
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Create,
|
||||
ProjectPermissionSub.Member
|
||||
);
|
||||
|
||||
const invitee = await User.findOne({
|
||||
email
|
||||
}).select("+publicKey");
|
||||
|
||||
if (!invitee || !invitee?.publicKey) throw new Error("Failed to validate invitee");
|
||||
|
||||
// validate invitee's workspace membership - ensure member isn't
|
||||
// already a member of the workspace
|
||||
const inviteeMembership = await Membership.findOne({
|
||||
user: invitee._id,
|
||||
workspace: workspaceId
|
||||
}).populate<{ user: IUser }>("user");
|
||||
|
||||
if (inviteeMembership) throw new Error("Failed to add existing member of workspace");
|
||||
|
||||
const workspace = await Workspace.findById(workspaceId);
|
||||
if (!workspace) throw new Error("Failed to find workspace");
|
||||
// validate invitee's organization membership - ensure that only
|
||||
// (accepted) organization members can be added to the workspace
|
||||
const membershipOrg = await MembershipOrg.findOne({
|
||||
user: invitee._id,
|
||||
organization: workspace.organization,
|
||||
status: ACCEPTED
|
||||
});
|
||||
|
||||
if (!membershipOrg) throw new Error("Failed to validate invitee's organization membership");
|
||||
|
||||
// get latest key
|
||||
const latestKey = await Key.findOne({
|
||||
workspace: workspaceId,
|
||||
receiver: req.user._id
|
||||
})
|
||||
.sort({ createdAt: -1 })
|
||||
.populate("sender", "+publicKey");
|
||||
|
||||
// create new workspace membership
|
||||
await new Membership({
|
||||
user: invitee._id,
|
||||
workspace: workspaceId,
|
||||
role: MEMBER
|
||||
}).save();
|
||||
|
||||
await sendMail({
|
||||
template: "workspaceInvitation.handlebars",
|
||||
subjectLine: "Infisical workspace invitation",
|
||||
recipients: [invitee.email],
|
||||
substitutions: {
|
||||
inviterFirstName: req.user.firstName,
|
||||
inviterEmail: req.user.email,
|
||||
workspaceName: workspace.name,
|
||||
callback_url: (await getSiteURL()) + "/login"
|
||||
}
|
||||
});
|
||||
|
||||
await EEAuditLogService.createAuditLog(
|
||||
req.authData,
|
||||
{
|
||||
type: EventType.ADD_WORKSPACE_MEMBER,
|
||||
metadata: {
|
||||
userId: invitee._id.toString(),
|
||||
email: invitee.email
|
||||
}
|
||||
},
|
||||
{
|
||||
workspaceId: new Types.ObjectId(workspaceId)
|
||||
}
|
||||
);
|
||||
|
||||
return res.status(200).send({
|
||||
invitee,
|
||||
latestKey
|
||||
});
|
||||
};
|
@ -1,292 +0,0 @@
|
||||
import { Types } from "mongoose";
|
||||
import { Request, Response } from "express";
|
||||
import { MembershipOrg, Organization, User } from "../../models";
|
||||
import { SSOConfig } from "../../ee/models";
|
||||
import { deleteMembershipOrg as deleteMemberFromOrg } from "../../helpers/membershipOrg";
|
||||
import { createToken } from "../../helpers/auth";
|
||||
import { updateSubscriptionOrgQuantity } from "../../helpers/organization";
|
||||
import { sendMail } from "../../helpers/nodemailer";
|
||||
import { TokenService } from "../../services";
|
||||
import { EELicenseService } from "../../ee/services";
|
||||
import { ACCEPTED, AuthTokenType, INVITED, MEMBER, TOKEN_EMAIL_ORG_INVITATION } from "../../variables";
|
||||
import * as reqValidator from "../../validation/membershipOrg";
|
||||
import {
|
||||
getAuthSecret,
|
||||
getJwtSignupLifetime,
|
||||
getSiteURL,
|
||||
getSmtpConfigured
|
||||
} from "../../config";
|
||||
import { validateUserEmail } from "../../validation";
|
||||
import { validateRequest } from "../../helpers/validation";
|
||||
import {
|
||||
OrgPermissionActions,
|
||||
OrgPermissionSubjects,
|
||||
getAuthDataOrgPermissions
|
||||
} from "../../ee/services/RoleService";
|
||||
import { ForbiddenError } from "@casl/ability";
|
||||
|
||||
/**
|
||||
* Delete organization membership with id [membershipOrgId] from organization
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
export const deleteMembershipOrg = async (req: Request, _res: Response) => {
|
||||
const {
|
||||
params: { membershipOrgId }
|
||||
} = await validateRequest(reqValidator.DelOrgMembershipv1, req);
|
||||
|
||||
// check if organization membership to delete exists
|
||||
const membershipOrgToDelete = await MembershipOrg.findOne({
|
||||
_id: membershipOrgId
|
||||
}).populate("user");
|
||||
|
||||
if (!membershipOrgToDelete) {
|
||||
throw new Error("Failed to delete organization membership that doesn't exist");
|
||||
}
|
||||
|
||||
const { permission } = await getAuthDataOrgPermissions({
|
||||
authData: req.authData,
|
||||
organizationId: membershipOrgToDelete.organization
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
OrgPermissionActions.Delete,
|
||||
OrgPermissionSubjects.Member
|
||||
);
|
||||
|
||||
// delete organization membership
|
||||
await deleteMemberFromOrg({
|
||||
membershipOrgId: membershipOrgToDelete._id.toString()
|
||||
});
|
||||
|
||||
await updateSubscriptionOrgQuantity({
|
||||
organizationId: membershipOrgToDelete.organization.toString()
|
||||
});
|
||||
|
||||
return membershipOrgToDelete;
|
||||
};
|
||||
|
||||
/**
|
||||
* Change and return organization membership role
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
export const changeMembershipOrgRole = async (req: Request, res: Response) => {
|
||||
// change role for (target) organization membership with id
|
||||
// [membershipOrgId]
|
||||
|
||||
let membershipToChangeRole;
|
||||
|
||||
return res.status(200).send({
|
||||
membershipOrg: membershipToChangeRole
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Organization invitation step 1: Send email invitation to user with email [email]
|
||||
* for organization with id [organizationId] containing magic link
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
export const inviteUserToOrganization = async (req: Request, res: Response) => {
|
||||
let inviteeMembershipOrg, completeInviteLink;
|
||||
const {
|
||||
body: { inviteeEmail, organizationId }
|
||||
} = await validateRequest(reqValidator.InviteUserToOrgv1, req);
|
||||
|
||||
const { permission } = await getAuthDataOrgPermissions({
|
||||
authData: req.authData,
|
||||
organizationId: new Types.ObjectId(organizationId)
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
OrgPermissionActions.Create,
|
||||
OrgPermissionSubjects.Member
|
||||
);
|
||||
|
||||
const host = req.headers.host;
|
||||
const siteUrl = `${req.protocol}://${host}`;
|
||||
const plan = await EELicenseService.getPlan(new Types.ObjectId(organizationId));
|
||||
|
||||
const ssoConfig = await SSOConfig.findOne({
|
||||
organization: new Types.ObjectId(organizationId)
|
||||
});
|
||||
|
||||
if (ssoConfig && ssoConfig.isActive) {
|
||||
// case: SAML SSO is enabled for the organization
|
||||
return res.status(400).send({
|
||||
message: "Failed to invite member due to SAML SSO configured for organization"
|
||||
});
|
||||
}
|
||||
|
||||
if (plan.memberLimit !== null) {
|
||||
// case: limit imposed on number of members allowed
|
||||
|
||||
if (plan.membersUsed >= plan.memberLimit) {
|
||||
// case: number of members used exceeds the number of members allowed
|
||||
return res.status(400).send({
|
||||
message:
|
||||
"Failed to invite member due to member limit reached. Upgrade plan to invite more members."
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const invitee = await User.findOne({
|
||||
email: inviteeEmail
|
||||
}).select("+publicKey");
|
||||
|
||||
if (invitee) {
|
||||
// case: invitee is an existing user
|
||||
|
||||
inviteeMembershipOrg = await MembershipOrg.findOne({
|
||||
user: invitee._id,
|
||||
organization: organizationId
|
||||
});
|
||||
|
||||
if (inviteeMembershipOrg && inviteeMembershipOrg.status === ACCEPTED) {
|
||||
throw new Error("Failed to invite an existing member of the organization");
|
||||
}
|
||||
|
||||
if (!inviteeMembershipOrg) {
|
||||
await new MembershipOrg({
|
||||
user: invitee,
|
||||
inviteEmail: inviteeEmail,
|
||||
organization: organizationId,
|
||||
role: MEMBER,
|
||||
status: INVITED
|
||||
}).save();
|
||||
}
|
||||
} else {
|
||||
// check if invitee has been invited before
|
||||
inviteeMembershipOrg = await MembershipOrg.findOne({
|
||||
inviteEmail: inviteeEmail,
|
||||
organization: organizationId
|
||||
});
|
||||
|
||||
if (!inviteeMembershipOrg) {
|
||||
// case: invitee has never been invited before
|
||||
|
||||
// validate that email is not disposable
|
||||
validateUserEmail(inviteeEmail);
|
||||
|
||||
await new MembershipOrg({
|
||||
inviteEmail: inviteeEmail,
|
||||
organization: organizationId,
|
||||
role: MEMBER,
|
||||
status: INVITED
|
||||
}).save();
|
||||
}
|
||||
}
|
||||
|
||||
const organization = await Organization.findOne({ _id: organizationId });
|
||||
|
||||
if (organization) {
|
||||
const token = await TokenService.createToken({
|
||||
type: TOKEN_EMAIL_ORG_INVITATION,
|
||||
email: inviteeEmail,
|
||||
organizationId: organization._id
|
||||
});
|
||||
|
||||
await sendMail({
|
||||
template: "organizationInvitation.handlebars",
|
||||
subjectLine: "Infisical organization invitation",
|
||||
recipients: [inviteeEmail],
|
||||
substitutions: {
|
||||
inviterFirstName: req.user.firstName,
|
||||
inviterEmail: req.user.email,
|
||||
organizationName: organization.name,
|
||||
email: inviteeEmail,
|
||||
organizationId: organization._id.toString(),
|
||||
token,
|
||||
callback_url: (await getSiteURL()) + "/signupinvite"
|
||||
}
|
||||
});
|
||||
|
||||
if (!(await getSmtpConfigured())) {
|
||||
completeInviteLink = `${
|
||||
siteUrl + "/signupinvite"
|
||||
}?token=${token}&to=${inviteeEmail}&organization_id=${organization._id}`;
|
||||
}
|
||||
}
|
||||
|
||||
await updateSubscriptionOrgQuantity({ organizationId });
|
||||
|
||||
return res.status(200).send({
|
||||
message: `Sent an invite link to ${req.body.inviteeEmail}`,
|
||||
completeInviteLink
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Organization invitation step 2: Verify that code [code] was sent to email [email] as part of
|
||||
* magic link and issue a temporary signup token for user to complete setting up their account
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
export const verifyUserToOrganization = async (req: Request, res: Response) => {
|
||||
let user;
|
||||
|
||||
const {
|
||||
body: { organizationId, email, code }
|
||||
} = await validateRequest(reqValidator.VerifyUserToOrgv1, req);
|
||||
|
||||
user = await User.findOne({ email }).select("+publicKey");
|
||||
|
||||
const membershipOrg = await MembershipOrg.findOne({
|
||||
inviteEmail: email,
|
||||
status: INVITED,
|
||||
organization: new Types.ObjectId(organizationId)
|
||||
});
|
||||
|
||||
if (!membershipOrg) throw new Error("Failed to find any invitations for email");
|
||||
|
||||
await TokenService.validateToken({
|
||||
type: TOKEN_EMAIL_ORG_INVITATION,
|
||||
email,
|
||||
organizationId: membershipOrg.organization,
|
||||
token: code
|
||||
});
|
||||
|
||||
if (user && user?.publicKey) {
|
||||
// case: user has already completed account
|
||||
// membership can be approved and redirected to login/dashboard
|
||||
membershipOrg.status = ACCEPTED;
|
||||
await membershipOrg.save();
|
||||
|
||||
await updateSubscriptionOrgQuantity({
|
||||
organizationId
|
||||
});
|
||||
|
||||
return res.status(200).send({
|
||||
message: "Successfully verified email",
|
||||
user
|
||||
});
|
||||
}
|
||||
|
||||
if (!user) {
|
||||
// initialize user account
|
||||
user = await new User({
|
||||
email
|
||||
}).save();
|
||||
}
|
||||
|
||||
// generate temporary signup token
|
||||
const token = createToken({
|
||||
payload: {
|
||||
authTokenType: AuthTokenType.SIGNUP_TOKEN,
|
||||
userId: user._id.toString()
|
||||
},
|
||||
expiresIn: await getJwtSignupLifetime(),
|
||||
secret: await getAuthSecret()
|
||||
});
|
||||
|
||||
return res.status(200).send({
|
||||
message: "Successfully verified email",
|
||||
user,
|
||||
token
|
||||
});
|
||||
};
|
@ -1,387 +0,0 @@
|
||||
import { Request, Response } from "express";
|
||||
import { Types } from "mongoose";
|
||||
import {
|
||||
IncidentContactOrg,
|
||||
Membership,
|
||||
MembershipOrg,
|
||||
Organization,
|
||||
Workspace
|
||||
} from "../../models";
|
||||
import { getLicenseServerUrl, getSiteURL } from "../../config";
|
||||
import { licenseServerKeyRequest } from "../../config/request";
|
||||
import { validateRequest } from "../../helpers/validation";
|
||||
import * as reqValidator from "../../validation/organization";
|
||||
import { ACCEPTED } from "../../variables";
|
||||
import {
|
||||
OrgPermissionActions,
|
||||
OrgPermissionSubjects,
|
||||
getAuthDataOrgPermissions
|
||||
} from "../../ee/services/RoleService";
|
||||
import { OrganizationNotFoundError } from "../../utils/errors";
|
||||
import { ForbiddenError } from "@casl/ability";
|
||||
|
||||
export const getOrganizations = async (req: Request, res: Response) => {
|
||||
const organizations = (
|
||||
await MembershipOrg.find({
|
||||
user: req.user._id,
|
||||
status: ACCEPTED
|
||||
}).populate("organization")
|
||||
).map((m) => m.organization);
|
||||
|
||||
return res.status(200).send({
|
||||
organizations
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Return organization with id [organizationId]
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
export const getOrganization = async (req: Request, res: Response) => {
|
||||
const {
|
||||
params: { organizationId }
|
||||
} = await validateRequest(reqValidator.GetOrgv1, req);
|
||||
|
||||
// ensure user has membership
|
||||
await getAuthDataOrgPermissions({
|
||||
authData: req.authData,
|
||||
organizationId: new Types.ObjectId(organizationId)
|
||||
})
|
||||
|
||||
const organization = await Organization.findById(organizationId);
|
||||
if (!organization) {
|
||||
throw OrganizationNotFoundError({
|
||||
message: "Failed to find organization"
|
||||
});
|
||||
}
|
||||
|
||||
return res.status(200).send({
|
||||
organization
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Return organization memberships for organization with id [organizationId]
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
export const getOrganizationMembers = async (req: Request, res: Response) => {
|
||||
const {
|
||||
params: { organizationId }
|
||||
} = await validateRequest(reqValidator.GetOrgMembersv1, req);
|
||||
|
||||
const { permission } = await getAuthDataOrgPermissions({
|
||||
authData: req.authData,
|
||||
organizationId: new Types.ObjectId(organizationId)
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
OrgPermissionActions.Read,
|
||||
OrgPermissionSubjects.Member
|
||||
);
|
||||
|
||||
const users = await MembershipOrg.find({
|
||||
organization: organizationId
|
||||
}).populate("user", "+publicKey");
|
||||
|
||||
return res.status(200).send({
|
||||
users
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Return workspaces that user is part of in organization with id [organizationId]
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
export const getOrganizationWorkspaces = async (req: Request, res: Response) => {
|
||||
const {
|
||||
params: { organizationId }
|
||||
} = await validateRequest(reqValidator.GetOrgWorkspacesv1, req);
|
||||
|
||||
const { permission } = await getAuthDataOrgPermissions({
|
||||
authData: req.authData,
|
||||
organizationId: new Types.ObjectId(organizationId)
|
||||
})
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
OrgPermissionActions.Read,
|
||||
OrgPermissionSubjects.Workspace
|
||||
);
|
||||
|
||||
const workspacesSet = new Set(
|
||||
(
|
||||
await Workspace.find(
|
||||
{
|
||||
organization: organizationId
|
||||
},
|
||||
"_id"
|
||||
)
|
||||
).map((w) => w._id.toString())
|
||||
);
|
||||
|
||||
const workspaces = (
|
||||
await Membership.find({
|
||||
user: req.user._id
|
||||
}).populate("workspace")
|
||||
)
|
||||
.filter((m) => workspacesSet.has(m.workspace._id.toString()))
|
||||
.map((m) => m.workspace);
|
||||
|
||||
return res.status(200).send({
|
||||
workspaces
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Change name of organization with id [organizationId] to [name]
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
export const changeOrganizationName = async (req: Request, res: Response) => {
|
||||
const {
|
||||
params: { organizationId },
|
||||
body: { name }
|
||||
} = await validateRequest(reqValidator.ChangeOrgNamev1, req);
|
||||
|
||||
const { permission } = await getAuthDataOrgPermissions({
|
||||
authData: req.authData,
|
||||
organizationId: new Types.ObjectId(organizationId)
|
||||
});
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
OrgPermissionActions.Edit,
|
||||
OrgPermissionSubjects.Settings
|
||||
);
|
||||
|
||||
const organization = await Organization.findOneAndUpdate(
|
||||
{
|
||||
_id: organizationId
|
||||
},
|
||||
{
|
||||
name
|
||||
},
|
||||
{
|
||||
new: true
|
||||
}
|
||||
);
|
||||
|
||||
return res.status(200).send({
|
||||
message: "Successfully changed organization name",
|
||||
organization
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Return incident contacts of organization with id [organizationId]
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
export const getOrganizationIncidentContacts = async (req: Request, res: Response) => {
|
||||
const {
|
||||
params: { organizationId }
|
||||
} = await validateRequest(reqValidator.GetOrgIncidentContactv1, req);
|
||||
|
||||
const { permission } = await getAuthDataOrgPermissions({
|
||||
authData: req.authData,
|
||||
organizationId: new Types.ObjectId(organizationId)
|
||||
});
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
OrgPermissionActions.Read,
|
||||
OrgPermissionSubjects.IncidentAccount
|
||||
);
|
||||
|
||||
const incidentContactsOrg = await IncidentContactOrg.find({
|
||||
organization: organizationId
|
||||
});
|
||||
|
||||
return res.status(200).send({
|
||||
incidentContactsOrg
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Add and return new incident contact with email [email] for organization with id [organizationId]
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
export const addOrganizationIncidentContact = async (req: Request, res: Response) => {
|
||||
const {
|
||||
params: { organizationId },
|
||||
body: { email }
|
||||
} = await validateRequest(reqValidator.CreateOrgIncideContact, req);
|
||||
|
||||
const { permission } = await getAuthDataOrgPermissions({
|
||||
authData: req.authData,
|
||||
organizationId: new Types.ObjectId(organizationId)
|
||||
});
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
OrgPermissionActions.Create,
|
||||
OrgPermissionSubjects.IncidentAccount
|
||||
);
|
||||
|
||||
const incidentContactOrg = await IncidentContactOrg.findOneAndUpdate(
|
||||
{ email, organization: organizationId },
|
||||
{ email, organization: organizationId },
|
||||
{ upsert: true, new: true }
|
||||
);
|
||||
|
||||
return res.status(200).send({
|
||||
incidentContactOrg
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Delete incident contact with email [email] for organization with id [organizationId]
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
export const deleteOrganizationIncidentContact = async (req: Request, res: Response) => {
|
||||
const {
|
||||
params: { organizationId },
|
||||
body: { email }
|
||||
} = await validateRequest(reqValidator.DelOrgIncideContact, req);
|
||||
|
||||
const { permission } = await getAuthDataOrgPermissions({
|
||||
authData: req.authData,
|
||||
organizationId: new Types.ObjectId(organizationId)
|
||||
});
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
OrgPermissionActions.Delete,
|
||||
OrgPermissionSubjects.IncidentAccount
|
||||
);
|
||||
|
||||
const incidentContactOrg = await IncidentContactOrg.findOneAndDelete({
|
||||
email,
|
||||
organization: organizationId
|
||||
});
|
||||
|
||||
return res.status(200).send({
|
||||
message: "Successfully deleted organization incident contact",
|
||||
incidentContactOrg
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Redirect user to billing portal or add card page depending on
|
||||
* if there is a card on file
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
export const createOrganizationPortalSession = async (req: Request, res: Response) => {
|
||||
const {
|
||||
params: { organizationId }
|
||||
} = await validateRequest(reqValidator.GetOrgPlanBillingInfov1, req);
|
||||
|
||||
const { permission } = await getAuthDataOrgPermissions({
|
||||
authData: req.authData,
|
||||
organizationId: new Types.ObjectId(organizationId)
|
||||
});
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
OrgPermissionActions.Edit,
|
||||
OrgPermissionSubjects.Billing
|
||||
);
|
||||
|
||||
const organization = await Organization.findById(organizationId);
|
||||
if (!organization) {
|
||||
throw OrganizationNotFoundError({
|
||||
message: "Failed to find organization"
|
||||
});
|
||||
}
|
||||
|
||||
const {
|
||||
data: { pmtMethods }
|
||||
} = await licenseServerKeyRequest.get(
|
||||
`${await getLicenseServerUrl()}/api/license-server/v1/customers/${
|
||||
organization.customerId
|
||||
}/billing-details/payment-methods`
|
||||
);
|
||||
|
||||
if (pmtMethods.length < 1) {
|
||||
// case: organization has no payment method on file
|
||||
// -> redirect to add payment method portal
|
||||
const {
|
||||
data: { url }
|
||||
} = await licenseServerKeyRequest.post(
|
||||
`${await getLicenseServerUrl()}/api/license-server/v1/customers/${
|
||||
organization.customerId
|
||||
}/billing-details/payment-methods`,
|
||||
{
|
||||
success_url: (await getSiteURL()) + "/dashboard",
|
||||
cancel_url: (await getSiteURL()) + "/dashboard"
|
||||
}
|
||||
);
|
||||
return res.status(200).send({ url });
|
||||
} else {
|
||||
// case: organization has payment method on file
|
||||
// -> redirect to billing portal
|
||||
const {
|
||||
data: { url }
|
||||
} = await licenseServerKeyRequest.post(
|
||||
`${await getLicenseServerUrl()}/api/license-server/v1/customers/${
|
||||
organization.customerId
|
||||
}/billing-details/billing-portal`,
|
||||
{
|
||||
return_url: (await getSiteURL()) + "/dashboard"
|
||||
}
|
||||
);
|
||||
return res.status(200).send({ url });
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Given a org id, return the projects each member of the org belongs to
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
export const getOrganizationMembersAndTheirWorkspaces = async (req: Request, res: Response) => {
|
||||
const {
|
||||
params: { organizationId }
|
||||
} = await validateRequest(reqValidator.GetOrgMembersv1, req);
|
||||
|
||||
const { permission } = await getAuthDataOrgPermissions({
|
||||
authData: req.authData,
|
||||
organizationId: new Types.ObjectId(organizationId)
|
||||
});
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
OrgPermissionActions.Read,
|
||||
OrgPermissionSubjects.Member
|
||||
);
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
OrgPermissionActions.Read,
|
||||
OrgPermissionSubjects.Workspace
|
||||
);
|
||||
|
||||
const workspacesSet = (
|
||||
await Workspace.find(
|
||||
{
|
||||
organization: organizationId
|
||||
},
|
||||
"_id"
|
||||
)
|
||||
).map((w) => w._id.toString());
|
||||
|
||||
const memberships = await Membership.find({
|
||||
workspace: { $in: workspacesSet }
|
||||
}).populate("workspace");
|
||||
const userToWorkspaceIds: any = {};
|
||||
|
||||
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);
|
||||
};
|
@ -1,370 +0,0 @@
|
||||
import { Request, Response } from "express";
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const jsrp = require("jsrp");
|
||||
import * as bigintConversion from "bigint-conversion";
|
||||
import { BackupPrivateKey, LoginSRPDetail, User } from "../../models";
|
||||
import { clearTokens, createToken, sendMail } from "../../helpers";
|
||||
import { TokenService } from "../../services";
|
||||
import { AuthTokenType, TOKEN_EMAIL_PASSWORD_RESET } from "../../variables";
|
||||
import { BadRequestError } from "../../utils/errors";
|
||||
import {
|
||||
getAuthSecret,
|
||||
getHttpsEnabled,
|
||||
getJwtSignupLifetime,
|
||||
getSiteURL
|
||||
} from "../../config";
|
||||
import { ActorType } from "../../ee/models";
|
||||
import { validateRequest } from "../../helpers/validation";
|
||||
import * as reqValidator from "../../validation/auth";
|
||||
|
||||
/**
|
||||
* Password reset step 1: Send email verification link to email [email]
|
||||
* for account recovery.
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
export const emailPasswordReset = async (req: Request, res: Response) => {
|
||||
const {
|
||||
body: { email }
|
||||
} = await validateRequest(reqValidator.EmailPasswordResetV1, req);
|
||||
|
||||
const user = await User.findOne({ email }).select("+publicKey");
|
||||
if (!user || !user?.publicKey) {
|
||||
// case: user has already completed account
|
||||
|
||||
return res.status(200).send({
|
||||
message: "If an account exists with this email, a password reset link has been sent"
|
||||
});
|
||||
}
|
||||
|
||||
const token = await TokenService.createToken({
|
||||
type: TOKEN_EMAIL_PASSWORD_RESET,
|
||||
email
|
||||
});
|
||||
|
||||
await sendMail({
|
||||
template: "passwordReset.handlebars",
|
||||
subjectLine: "Infisical password reset",
|
||||
recipients: [email],
|
||||
substitutions: {
|
||||
email,
|
||||
token,
|
||||
callback_url: (await getSiteURL()) + "/password-reset"
|
||||
}
|
||||
});
|
||||
|
||||
return res.status(200).send({
|
||||
message: "If an account exists with this email, a password reset link has been sent"
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Password reset step 2: Verify email verification link sent to email [email]
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
export const emailPasswordResetVerify = async (req: Request, res: Response) => {
|
||||
const {
|
||||
body: { email, code }
|
||||
} = await validateRequest(reqValidator.EmailPasswordResetVerifyV1, req);
|
||||
|
||||
const user = await User.findOne({ email }).select("+publicKey");
|
||||
if (!user || !user?.publicKey) {
|
||||
// case: user doesn't exist with email [email] or
|
||||
// hasn't even completed their account
|
||||
return res.status(403).send({
|
||||
error: "Failed email verification for password reset"
|
||||
});
|
||||
}
|
||||
|
||||
await TokenService.validateToken({
|
||||
type: TOKEN_EMAIL_PASSWORD_RESET,
|
||||
email,
|
||||
token: code
|
||||
});
|
||||
|
||||
// generate temporary password-reset token
|
||||
const token = createToken({
|
||||
payload: {
|
||||
authTokenType: AuthTokenType.SIGNUP_TOKEN,
|
||||
userId: user._id.toString()
|
||||
},
|
||||
expiresIn: await getJwtSignupLifetime(),
|
||||
secret: await getAuthSecret()
|
||||
});
|
||||
|
||||
return res.status(200).send({
|
||||
message: "Successfully verified email",
|
||||
user,
|
||||
token
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Return [salt] and [serverPublicKey] as part of step 1 of SRP protocol
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
export const srp1 = async (req: Request, res: Response) => {
|
||||
// return salt, serverPublicKey as part of first step of SRP protocol
|
||||
const {
|
||||
body: { clientPublicKey }
|
||||
} = await validateRequest(reqValidator.Srp1V1, req);
|
||||
|
||||
const user = await User.findOne({
|
||||
email: req.user.email
|
||||
}).select("+salt +verifier");
|
||||
|
||||
if (!user) throw new Error("Failed to find user");
|
||||
|
||||
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(
|
||||
{ email: req.user.email },
|
||||
{
|
||||
email: req.user.email,
|
||||
clientPublicKey: clientPublicKey,
|
||||
serverBInt: bigintConversion.bigintToBuf(server.bInt)
|
||||
},
|
||||
{ upsert: true, returnNewDocument: false }
|
||||
);
|
||||
|
||||
return res.status(200).send({
|
||||
serverPublicKey,
|
||||
salt: user.salt
|
||||
});
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Change account SRP authentication information for user
|
||||
* Requires verifying [clientProof] as part of step 2 of SRP protocol
|
||||
* as initiated in POST /srp1
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
export const changePassword = async (req: Request, res: Response) => {
|
||||
const {
|
||||
body: {
|
||||
clientProof,
|
||||
protectedKey,
|
||||
protectedKeyIV,
|
||||
protectedKeyTag,
|
||||
encryptedPrivateKey,
|
||||
encryptedPrivateKeyIV,
|
||||
encryptedPrivateKeyTag,
|
||||
salt,
|
||||
verifier
|
||||
}
|
||||
} = await validateRequest(reqValidator.ChangePasswordV1, req);
|
||||
|
||||
const user = await User.findOne({
|
||||
email: req.user.email
|
||||
}).select("+salt +verifier");
|
||||
|
||||
if (!user) throw new Error("Failed to find user");
|
||||
|
||||
const loginSRPDetailFromDB = await LoginSRPDetail.findOneAndDelete({ email: req.user.email });
|
||||
|
||||
if (!loginSRPDetailFromDB) {
|
||||
return BadRequestError(
|
||||
Error(
|
||||
"It looks like some details from the first login are not found. Please try login one again"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const server = new jsrp.server();
|
||||
server.init(
|
||||
{
|
||||
salt: user.salt,
|
||||
verifier: user.verifier,
|
||||
b: loginSRPDetailFromDB.serverBInt
|
||||
},
|
||||
async () => {
|
||||
server.setClientPublicKey(loginSRPDetailFromDB.clientPublicKey);
|
||||
|
||||
// compare server and client shared keys
|
||||
if (server.checkClientProof(clientProof)) {
|
||||
// change password
|
||||
|
||||
await User.findByIdAndUpdate(
|
||||
req.user._id.toString(),
|
||||
{
|
||||
encryptionVersion: 2,
|
||||
protectedKey,
|
||||
protectedKeyIV,
|
||||
protectedKeyTag,
|
||||
encryptedPrivateKey,
|
||||
iv: encryptedPrivateKeyIV,
|
||||
tag: encryptedPrivateKeyTag,
|
||||
salt,
|
||||
verifier
|
||||
},
|
||||
{
|
||||
new: true
|
||||
}
|
||||
);
|
||||
|
||||
if (req.authData.actor.type === ActorType.USER && req.authData.tokenVersionId) {
|
||||
await clearTokens(req.authData.tokenVersionId);
|
||||
}
|
||||
|
||||
// clear httpOnly cookie
|
||||
|
||||
res.cookie("jid", "", {
|
||||
httpOnly: true,
|
||||
path: "/",
|
||||
sameSite: "strict",
|
||||
secure: (await getHttpsEnabled()) as boolean
|
||||
});
|
||||
|
||||
return res.status(200).send({
|
||||
message: "Successfully changed password"
|
||||
});
|
||||
}
|
||||
|
||||
return res.status(400).send({
|
||||
error: "Failed to change password. Try again?"
|
||||
});
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Create or change backup private key for user
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
export const createBackupPrivateKey = async (req: Request, res: Response) => {
|
||||
// create/change backup private key
|
||||
// requires verifying [clientProof] as part of second step of SRP protocol
|
||||
// as initiated in /srp1
|
||||
const {
|
||||
body: { clientProof, encryptedPrivateKey, salt, verifier, iv, tag }
|
||||
} = await validateRequest(reqValidator.CreateBackupPrivateKeyV1, req);
|
||||
const user = await User.findOne({
|
||||
email: req.user.email
|
||||
}).select("+salt +verifier");
|
||||
|
||||
if (!user) throw new Error("Failed to find user");
|
||||
|
||||
const loginSRPDetailFromDB = await LoginSRPDetail.findOneAndDelete({ email: req.user.email });
|
||||
|
||||
if (!loginSRPDetailFromDB) {
|
||||
return BadRequestError(
|
||||
Error(
|
||||
"It looks like some details from the first login are not found. Please try login one again"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const server = new jsrp.server();
|
||||
server.init(
|
||||
{
|
||||
salt: user.salt,
|
||||
verifier: user.verifier,
|
||||
b: loginSRPDetailFromDB.serverBInt
|
||||
},
|
||||
async () => {
|
||||
server.setClientPublicKey(loginSRPDetailFromDB.clientPublicKey);
|
||||
|
||||
// compare server and client shared keys
|
||||
if (server.checkClientProof(clientProof)) {
|
||||
// create new or replace backup private key
|
||||
|
||||
const backupPrivateKey = await BackupPrivateKey.findOneAndUpdate(
|
||||
{ user: req.user._id },
|
||||
{
|
||||
user: req.user._id,
|
||||
encryptedPrivateKey,
|
||||
iv,
|
||||
tag,
|
||||
salt,
|
||||
verifier
|
||||
},
|
||||
{ upsert: true, new: true }
|
||||
).select("+user, encryptedPrivateKey");
|
||||
|
||||
// issue tokens
|
||||
return res.status(200).send({
|
||||
message: "Successfully updated backup private key",
|
||||
backupPrivateKey
|
||||
});
|
||||
}
|
||||
|
||||
return res.status(400).send({
|
||||
message: "Failed to update backup private key"
|
||||
});
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Return backup private key for user
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
export const getBackupPrivateKey = async (req: Request, res: Response) => {
|
||||
const backupPrivateKey = await BackupPrivateKey.findOne({
|
||||
user: req.user._id
|
||||
}).select("+encryptedPrivateKey +iv +tag");
|
||||
|
||||
if (!backupPrivateKey) throw new Error("Failed to find backup private key");
|
||||
|
||||
return res.status(200).send({
|
||||
backupPrivateKey
|
||||
});
|
||||
};
|
||||
|
||||
export const resetPassword = async (req: Request, res: Response) => {
|
||||
const {
|
||||
body: {
|
||||
encryptedPrivateKey,
|
||||
protectedKeyTag,
|
||||
protectedKey,
|
||||
protectedKeyIV,
|
||||
salt,
|
||||
verifier,
|
||||
encryptedPrivateKeyIV,
|
||||
encryptedPrivateKeyTag
|
||||
}
|
||||
} = await validateRequest(reqValidator.ResetPasswordV1, req);
|
||||
|
||||
await User.findByIdAndUpdate(
|
||||
req.user._id.toString(),
|
||||
{
|
||||
encryptionVersion: 2,
|
||||
protectedKey,
|
||||
protectedKeyIV,
|
||||
protectedKeyTag,
|
||||
encryptedPrivateKey,
|
||||
iv: encryptedPrivateKeyIV,
|
||||
tag: encryptedPrivateKeyTag,
|
||||
salt,
|
||||
verifier
|
||||
},
|
||||
{
|
||||
new: true
|
||||
}
|
||||
);
|
||||
|
||||
return res.status(200).send({
|
||||
message: "Successfully reset password"
|
||||
});
|
||||
};
|
@ -1,209 +0,0 @@
|
||||
import { Request, Response } from "express";
|
||||
import { Types } from "mongoose";
|
||||
import { Key } from "../../models";
|
||||
import {
|
||||
pullSecrets as pull,
|
||||
v1PushSecrets as push,
|
||||
reformatPullSecrets
|
||||
} from "../../helpers/secret";
|
||||
import { pushKeys } from "../../helpers/key";
|
||||
import { eventPushSecrets } from "../../events";
|
||||
import { EventService } from "../../services";
|
||||
import { TelemetryService } from "../../services";
|
||||
|
||||
interface PushSecret {
|
||||
ciphertextKey: string;
|
||||
ivKey: string;
|
||||
tagKey: string;
|
||||
hashKey: string;
|
||||
ciphertextValue: string;
|
||||
ivValue: string;
|
||||
tagValue: string;
|
||||
hashValue: string;
|
||||
ciphertextComment: string;
|
||||
ivComment: string;
|
||||
tagComment: string;
|
||||
hashComment: string;
|
||||
type: "shared" | "personal";
|
||||
}
|
||||
|
||||
/**
|
||||
* Upload (encrypted) secrets to workspace with id [workspaceId]
|
||||
* for environment [environment]
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
export const pushSecrets = async (req: Request, res: Response) => {
|
||||
// upload (encrypted) secrets to workspace with id [workspaceId]
|
||||
const postHogClient = await TelemetryService.getPostHogClient();
|
||||
let { secrets }: { secrets: PushSecret[] } = req.body;
|
||||
const { keys, environment, channel } = req.body;
|
||||
const { workspaceId } = req.params;
|
||||
|
||||
// validate environment
|
||||
const workspaceEnvs = req.membership.workspace.environments;
|
||||
if (!workspaceEnvs.find(({ slug }: { slug: string }) => slug === environment)) {
|
||||
throw new Error("Failed to validate environment");
|
||||
}
|
||||
|
||||
// sanitize secrets
|
||||
secrets = secrets.filter((s: PushSecret) => s.ciphertextKey !== "" && s.ciphertextValue !== "");
|
||||
|
||||
await push({
|
||||
userId: req.user._id,
|
||||
workspaceId,
|
||||
environment,
|
||||
secrets
|
||||
});
|
||||
|
||||
await pushKeys({
|
||||
userId: req.user._id,
|
||||
workspaceId,
|
||||
keys
|
||||
});
|
||||
|
||||
if (postHogClient) {
|
||||
postHogClient.capture({
|
||||
event: "secrets pushed",
|
||||
distinctId: req.user.email,
|
||||
properties: {
|
||||
numberOfSecrets: secrets.length,
|
||||
environment,
|
||||
workspaceId,
|
||||
channel: channel ? channel : "cli"
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// trigger event - push secrets
|
||||
EventService.handleEvent({
|
||||
event: eventPushSecrets({
|
||||
workspaceId: new Types.ObjectId(workspaceId),
|
||||
environment,
|
||||
secretPath: "/"
|
||||
})
|
||||
});
|
||||
|
||||
return res.status(200).send({
|
||||
message: "Successfully uploaded workspace secrets"
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Return (encrypted) secrets for workspace with id [workspaceId]
|
||||
* for environment [environment] and (encrypted) workspace key
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
export const pullSecrets = async (req: Request, res: Response) => {
|
||||
let secrets;
|
||||
|
||||
const postHogClient = await TelemetryService.getPostHogClient();
|
||||
const environment: string = req.query.environment as string;
|
||||
const channel: string = req.query.channel as string;
|
||||
const { workspaceId } = req.params;
|
||||
|
||||
// validate environment
|
||||
const workspaceEnvs = req.membership.workspace.environments;
|
||||
if (!workspaceEnvs.find(({ slug }: { slug: string }) => slug === environment)) {
|
||||
throw new Error("Failed to validate environment");
|
||||
}
|
||||
|
||||
secrets = await pull({
|
||||
userId: req.user._id.toString(),
|
||||
workspaceId,
|
||||
environment,
|
||||
channel: channel ? channel : "cli",
|
||||
ipAddress: req.realIP
|
||||
});
|
||||
|
||||
const key = await Key.findOne({
|
||||
workspace: workspaceId,
|
||||
receiver: req.user._id
|
||||
})
|
||||
.sort({ createdAt: -1 })
|
||||
.populate("sender", "+publicKey");
|
||||
|
||||
if (channel !== "cli") {
|
||||
secrets = reformatPullSecrets({ secrets });
|
||||
}
|
||||
|
||||
if (postHogClient) {
|
||||
// capture secrets pushed event in production
|
||||
postHogClient.capture({
|
||||
distinctId: req.user.email,
|
||||
event: "secrets pulled",
|
||||
properties: {
|
||||
numberOfSecrets: secrets.length,
|
||||
environment,
|
||||
workspaceId,
|
||||
channel: channel ? channel : "cli"
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return res.status(200).send({
|
||||
secrets,
|
||||
key
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Return (encrypted) secrets for workspace with id [workspaceId]
|
||||
* for environment [environment] and (encrypted) workspace key
|
||||
* via service token
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
export const pullSecretsServiceToken = async (req: Request, res: Response) => {
|
||||
const postHogClient = await TelemetryService.getPostHogClient();
|
||||
const environment: string = req.query.environment as string;
|
||||
const channel: string = req.query.channel as string;
|
||||
const { workspaceId } = req.params;
|
||||
|
||||
// validate environment
|
||||
const workspaceEnvs = req.membership.workspace.environments;
|
||||
if (!workspaceEnvs.find(({ slug }: { slug: string }) => slug === environment)) {
|
||||
throw new Error("Failed to validate environment");
|
||||
}
|
||||
|
||||
const secrets = await pull({
|
||||
userId: req.serviceToken.user._id.toString(),
|
||||
workspaceId,
|
||||
environment,
|
||||
channel: "cli",
|
||||
ipAddress: req.realIP
|
||||
});
|
||||
|
||||
const key = {
|
||||
encryptedKey: req.serviceToken.encryptedKey,
|
||||
nonce: req.serviceToken.nonce,
|
||||
sender: {
|
||||
publicKey: req.serviceToken.publicKey
|
||||
},
|
||||
receiver: req.serviceToken.user,
|
||||
workspace: req.serviceToken.workspace
|
||||
};
|
||||
|
||||
if (postHogClient) {
|
||||
// capture secrets pulled event in production
|
||||
postHogClient.capture({
|
||||
distinctId: req.serviceToken.user.email,
|
||||
event: "secrets pulled",
|
||||
properties: {
|
||||
numberOfSecrets: secrets.length,
|
||||
environment,
|
||||
workspaceId,
|
||||
channel: channel ? channel : "cli"
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return res.status(200).send({
|
||||
secrets: reformatPullSecrets({ secrets }),
|
||||
key
|
||||
});
|
||||
};
|
@ -1,734 +0,0 @@
|
||||
|
||||
import { Request, Response } from "express";
|
||||
import { Types } from "mongoose";
|
||||
import { isValidScope } from "../../helpers";
|
||||
import { Folder, IServiceTokenData, SecretImport, ServiceTokenData } from "../../models";
|
||||
import { getAllImportedSecrets } from "../../services/SecretImportService";
|
||||
import { getFolderByPath, 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,
|
||||
getAuthDataProjectPermissions
|
||||
} 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 secret import'
|
||||
|
||||
#swagger.requestBody = {
|
||||
content: {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"workspaceId": {
|
||||
"type": "string",
|
||||
"description": "ID of workspace where to create secret import",
|
||||
"example": "someWorkspaceId"
|
||||
},
|
||||
"environment": {
|
||||
"type": "string",
|
||||
"description": "Slug of environment where to create secret import",
|
||||
"example": "dev"
|
||||
},
|
||||
"directory": {
|
||||
"type": "string",
|
||||
"description": "Path where to create secret import like / or /foo/bar. Default is /",
|
||||
"example": "/foo/bar"
|
||||
},
|
||||
"secretImport": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"environment": {
|
||||
"type": "string",
|
||||
"description": "Slug of environment to import from",
|
||||
"example": "development"
|
||||
},
|
||||
"secretPath": {
|
||||
"type": "string",
|
||||
"description": "Path where to import from like / or /foo/bar.",
|
||||
"example": "/user/oauth"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": ["workspaceId", "environment", "directory", "secretImport"]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#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, directory, secretImport }
|
||||
} = await validateRequest(reqValidator.CreateSecretImportV1, req);
|
||||
|
||||
if (req.authData.authPayload instanceof ServiceTokenData) {
|
||||
// root check
|
||||
const isValidScopeAccess = isValidScope(req.authData.authPayload, environment, directory);
|
||||
if (!isValidScopeAccess) {
|
||||
throw UnauthorizedRequestError({ message: "Folder Permission Denied" });
|
||||
}
|
||||
} else {
|
||||
const { permission } = await getAuthDataProjectPermissions({
|
||||
authData: req.authData,
|
||||
workspaceId: new Types.ObjectId(workspaceId)
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Create,
|
||||
subject(ProjectPermissionSub.Secrets, { environment, secretPath: directory })
|
||||
);
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Create,
|
||||
subject(ProjectPermissionSub.Secrets, { environment: secretImport.environment, secretPath: secretImport.secretPath })
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
const folders = await Folder.findOne({
|
||||
workspace: workspaceId,
|
||||
environment
|
||||
}).lean();
|
||||
|
||||
if (!folders && directory !== "/")
|
||||
throw ResourceNotFoundError({ message: "Failed to find folder" });
|
||||
|
||||
let folderId = "root";
|
||||
if (folders) {
|
||||
const folder = getFolderByPath(folders.nodes, directory);
|
||||
if (!folder) throw BadRequestError({ message: "Folder not found" });
|
||||
folderId = folder.id;
|
||||
}
|
||||
|
||||
const importSecDoc = await SecretImport.findOne({
|
||||
workspace: workspaceId,
|
||||
environment,
|
||||
folderId
|
||||
});
|
||||
|
||||
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: directory
|
||||
}
|
||||
},
|
||||
{
|
||||
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: directory
|
||||
}
|
||||
},
|
||||
{
|
||||
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 secret import'
|
||||
#swagger.description = 'Update secret import'
|
||||
|
||||
#swagger.parameters['id'] = {
|
||||
in: 'path',
|
||||
description: 'ID of secret import to update',
|
||||
required: true,
|
||||
type: 'string',
|
||||
example: 'import12345'
|
||||
}
|
||||
|
||||
#swagger.requestBody = {
|
||||
content: {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"secretImports": {
|
||||
"type": "array",
|
||||
"description": "List of secret imports to update to",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"environment": {
|
||||
"type": "string",
|
||||
"description": "Slug of environment to import from",
|
||||
"example": "dev"
|
||||
},
|
||||
"secretPath": {
|
||||
"type": "string",
|
||||
"description": "Path where to import secrets from like / or /foo/bar",
|
||||
"example": "/foo/bar"
|
||||
}
|
||||
},
|
||||
"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 getAuthDataProjectPermissions({
|
||||
authData: req.authData,
|
||||
workspaceId: importSecDoc.workspace
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Edit,
|
||||
subject(ProjectPermissionSub.Secrets, {
|
||||
environment: importSecDoc.environment,
|
||||
secretPath
|
||||
})
|
||||
);
|
||||
|
||||
secretImports.forEach(({ environment, secretPath }) => {
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Create,
|
||||
subject(ProjectPermissionSub.Secrets, { 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 parent secret import document from which to delete secret import',
|
||||
required: true,
|
||||
type: 'string',
|
||||
example: '12345abcde'
|
||||
}
|
||||
|
||||
#swagger.requestBody = {
|
||||
content: {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"secretImportEnv": {
|
||||
"type": "string",
|
||||
"description": "Slug of environment of import to delete",
|
||||
"example": "someWorkspaceId"
|
||||
},
|
||||
"secretImportPath": {
|
||||
"type": "string",
|
||||
"description": "Path like / or /foo/bar of import to delete",
|
||||
"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 getAuthDataProjectPermissions({
|
||||
authData: req.authData,
|
||||
workspaceId: importSecDoc.workspace
|
||||
});
|
||||
|
||||
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 = 'Get secret imports'
|
||||
#swagger.description = 'Get secret imports'
|
||||
|
||||
#swagger.parameters['workspaceId'] = {
|
||||
in: 'query',
|
||||
description: 'ID of workspace where to get secret imports from',
|
||||
required: true,
|
||||
type: 'string',
|
||||
example: 'workspace12345'
|
||||
}
|
||||
|
||||
#swagger.parameters['environment'] = {
|
||||
in: 'query',
|
||||
description: 'Slug of environment where to get secret imports from',
|
||||
required: true,
|
||||
type: 'string',
|
||||
example: 'production'
|
||||
}
|
||||
|
||||
#swagger.parameters['directory'] = {
|
||||
in: 'query',
|
||||
description: 'Path where to get secret imports from like / or /foo/bar. Default is /',
|
||||
required: false,
|
||||
type: 'string',
|
||||
example: 'folder12345'
|
||||
}
|
||||
|
||||
#swagger.responses[200] = {
|
||||
description: 'Successfully retrieved secret import',
|
||||
content: {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"secretImport": {
|
||||
$ref: '#/definitions/SecretImport'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#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, directory }
|
||||
} = await validateRequest(reqValidator.GetSecretImportsV1, req);
|
||||
|
||||
if (req.authData.authPayload instanceof ServiceTokenData) {
|
||||
const isValidScopeAccess = isValidScope(req.authData.authPayload, environment, directory);
|
||||
if (!isValidScopeAccess) {
|
||||
throw UnauthorizedRequestError({ message: "Folder Permission Denied" });
|
||||
}
|
||||
} else {
|
||||
const { permission } = await getAuthDataProjectPermissions({
|
||||
authData: req.authData,
|
||||
workspaceId: new Types.ObjectId(workspaceId)
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Read,
|
||||
subject(ProjectPermissionSub.Secrets, {
|
||||
environment,
|
||||
secretPath: directory
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
const folders = await Folder.findOne({
|
||||
workspace: workspaceId,
|
||||
environment
|
||||
}).lean();
|
||||
if (!folders && directory !== "/") throw BadRequestError({ message: "Folder not found" });
|
||||
|
||||
let folderId = "root";
|
||||
if (folders) {
|
||||
const folder = getFolderByPath(folders.nodes, directory);
|
||||
if (!folder) throw BadRequestError({ message: "Folder not found" });
|
||||
folderId = folder.id;
|
||||
}
|
||||
|
||||
const importSecDoc = await SecretImport.findOne({
|
||||
workspace: workspaceId,
|
||||
environment,
|
||||
folderId
|
||||
});
|
||||
|
||||
if (!importSecDoc) {
|
||||
return res.status(200).json({ secretImport: {} });
|
||||
}
|
||||
|
||||
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, directory }
|
||||
} = await validateRequest(reqValidator.GetAllSecretsFromImportV1, req);
|
||||
|
||||
if (req.authData.authPayload instanceof ServiceTokenData) {
|
||||
// check for service token validity
|
||||
const isValidScopeAccess = isValidScope(req.authData.authPayload, environment, directory);
|
||||
if (!isValidScopeAccess) {
|
||||
throw UnauthorizedRequestError({ message: "Folder Permission Denied" });
|
||||
}
|
||||
} else {
|
||||
const { permission } = await getAuthDataProjectPermissions({
|
||||
authData: req.authData,
|
||||
workspaceId: new Types.ObjectId(workspaceId)
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Read,
|
||||
subject(ProjectPermissionSub.Secrets, {
|
||||
environment,
|
||||
secretPath: directory
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
const folders = await Folder.findOne({
|
||||
workspace: workspaceId,
|
||||
environment
|
||||
}).lean();
|
||||
if (!folders && directory !== "/") throw BadRequestError({ message: "Folder not found" });
|
||||
|
||||
let folderId = "root";
|
||||
if (folders) {
|
||||
const folder = getFolderByPath(folders.nodes, directory);
|
||||
if (!folder) throw BadRequestError({ message: "Folder not found" });
|
||||
folderId = folder.id;
|
||||
}
|
||||
|
||||
const importSecDoc = await SecretImport.findOne({
|
||||
workspace: workspaceId,
|
||||
environment,
|
||||
folderId
|
||||
});
|
||||
|
||||
if (!importSecDoc) {
|
||||
return res.status(200).json({ secrets: [] });
|
||||
}
|
||||
|
||||
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 getAuthDataProjectPermissions({
|
||||
authData: req.authData,
|
||||
workspaceId: importSecDoc.workspace
|
||||
});
|
||||
|
||||
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 });
|
||||
};
|
@ -1,193 +0,0 @@
|
||||
import { Request, Response } from "express";
|
||||
import {
|
||||
GitAppInstallationSession,
|
||||
GitAppOrganizationInstallation,
|
||||
GitRisks
|
||||
} from "../../ee/models";
|
||||
import crypto from "crypto";
|
||||
import { Types } from "mongoose";
|
||||
import { OrganizationNotFoundError, UnauthorizedRequestError } from "../../utils/errors";
|
||||
import { scanGithubFullRepoForSecretLeaks } from "../../queues/secret-scanning/githubScanFullRepository";
|
||||
import { getSecretScanningGitAppId, getSecretScanningPrivateKey } from "../../config";
|
||||
import {
|
||||
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,
|
||||
getAuthDataOrgPermissions
|
||||
} from "../../ee/services/RoleService";
|
||||
import { ForbiddenError } from "@casl/ability";
|
||||
|
||||
export const createInstallationSession = async (req: Request, res: Response) => {
|
||||
const sessionId = crypto.randomBytes(16).toString("hex");
|
||||
const {
|
||||
params: { organizationId }
|
||||
} = await validateRequest(reqValidator.CreateInstalLSessionv1, req);
|
||||
|
||||
const organization = await Organization.findById(organizationId);
|
||||
if (!organization) {
|
||||
throw OrganizationNotFoundError({
|
||||
message: "Failed to find organization"
|
||||
});
|
||||
}
|
||||
|
||||
const { permission } = await getAuthDataOrgPermissions({
|
||||
authData: req.authData,
|
||||
organizationId: new Types.ObjectId(organizationId)
|
||||
});
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
OrgPermissionActions.Create,
|
||||
OrgPermissionSubjects.SecretScanning
|
||||
);
|
||||
|
||||
await GitAppInstallationSession.findByIdAndUpdate(
|
||||
organization,
|
||||
{
|
||||
organization: organization.id,
|
||||
sessionId: sessionId,
|
||||
user: new Types.ObjectId(req.user._id)
|
||||
},
|
||||
{ upsert: true }
|
||||
).lean();
|
||||
|
||||
res.send({
|
||||
sessionId: sessionId
|
||||
});
|
||||
};
|
||||
|
||||
export const linkInstallationToOrganization = async (req: Request, res: Response) => {
|
||||
const {
|
||||
body: { sessionId, installationId }
|
||||
} = await validateRequest(reqValidator.LinkInstallationToOrgv1, req);
|
||||
|
||||
const installationSession = await GitAppInstallationSession.findOneAndDelete({
|
||||
sessionId: sessionId
|
||||
});
|
||||
if (!installationSession) {
|
||||
throw UnauthorizedRequestError();
|
||||
}
|
||||
|
||||
const { permission } = await getAuthDataOrgPermissions({
|
||||
authData: req.authData,
|
||||
organizationId: installationSession.organization
|
||||
});
|
||||
|
||||
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);
|
||||
};
|
||||
|
||||
export const getCurrentOrganizationInstallationStatus = async (req: Request, res: Response) => {
|
||||
const { organizationId } = req.params;
|
||||
try {
|
||||
const appInstallation = await GitAppOrganizationInstallation.findOne({
|
||||
organizationId: organizationId
|
||||
}).lean();
|
||||
if (!appInstallation) {
|
||||
res.json({
|
||||
appInstallationComplete: false
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
appInstallationComplete: true
|
||||
});
|
||||
} catch {
|
||||
res.json({
|
||||
appInstallationComplete: false
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export const getRisksForOrganization = async (req: Request, res: Response) => {
|
||||
const {
|
||||
params: { organizationId }
|
||||
} = await validateRequest(reqValidator.GetOrgRisksv1, req);
|
||||
|
||||
const { permission } = await getAuthDataOrgPermissions({
|
||||
authData: req.authData,
|
||||
organizationId: new Types.ObjectId(organizationId)
|
||||
});
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
OrgPermissionActions.Read,
|
||||
OrgPermissionSubjects.SecretScanning
|
||||
);
|
||||
|
||||
const risks = await GitRisks.find({ organization: organizationId })
|
||||
.sort({ createdAt: -1 })
|
||||
.lean();
|
||||
res.json({
|
||||
risks: risks
|
||||
});
|
||||
};
|
||||
|
||||
export const updateRisksStatus = async (req: Request, res: Response) => {
|
||||
const {
|
||||
params: { organizationId, riskId },
|
||||
body: { status }
|
||||
} = await validateRequest(reqValidator.UpdateRiskStatusv1, req);
|
||||
|
||||
const { permission } = await getAuthDataOrgPermissions({
|
||||
authData: req.authData,
|
||||
organizationId: new Types.ObjectId(organizationId)
|
||||
});
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
OrgPermissionActions.Edit,
|
||||
OrgPermissionSubjects.SecretScanning
|
||||
);
|
||||
|
||||
const isRiskResolved =
|
||||
status == STATUS_RESOLVED_FALSE_POSITIVE ||
|
||||
status == STATUS_RESOLVED_REVOKED ||
|
||||
status == STATUS_RESOLVED_NOT_REVOKED
|
||||
? true
|
||||
: false;
|
||||
const risk = await GitRisks.findByIdAndUpdate(riskId, {
|
||||
status: status,
|
||||
isResolved: isRiskResolved
|
||||
}).lean();
|
||||
|
||||
res.json(risk);
|
||||
};
|
@ -1,680 +0,0 @@
|
||||
import { ForbiddenError, subject } from "@casl/ability";
|
||||
import { Request, Response } from "express";
|
||||
import { Types } from "mongoose";
|
||||
import { EventType, FolderVersion } from "../../ee/models";
|
||||
import { EEAuditLogService, EESecretService } from "../../ee/services";
|
||||
import { isValidScope } from "../../helpers/secrets";
|
||||
import { validateRequest } from "../../helpers/validation";
|
||||
import { Secret, ServiceTokenData } from "../../models";
|
||||
import { Folder } from "../../models/folder";
|
||||
import {
|
||||
appendFolder,
|
||||
getAllFolderIds,
|
||||
getFolderByPath,
|
||||
getFolderWithPathFromId,
|
||||
validateFolderName
|
||||
} from "../../services/FolderService";
|
||||
import {
|
||||
ProjectPermissionActions,
|
||||
ProjectPermissionSub,
|
||||
getAuthDataProjectPermissions
|
||||
} from "../../ee/services/ProjectRoleService";
|
||||
import { BadRequestError, UnauthorizedRequestError } from "../../utils/errors";
|
||||
import * as reqValidator from "../../validation/folders";
|
||||
|
||||
const ERR_FOLDER_NOT_FOUND = BadRequestError({ message: "The folder doesn't exist" });
|
||||
|
||||
// verify workspace id/environment
|
||||
export const createFolder = async (req: Request, res: Response) => {
|
||||
/*
|
||||
#swagger.summary = 'Create folder'
|
||||
#swagger.description = 'Create folder'
|
||||
|
||||
#swagger.security = [{
|
||||
"apiKeyAuth": [],
|
||||
"bearerAuth": []
|
||||
}]
|
||||
|
||||
#swagger.requestBody = {
|
||||
content: {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"workspaceId": {
|
||||
"type": "string",
|
||||
"description": "ID of the workspace where to create folder",
|
||||
"example": "someWorkspaceId"
|
||||
},
|
||||
"environment": {
|
||||
"type": "string",
|
||||
"description": "Slug of environment where to create folder",
|
||||
"example": "production"
|
||||
},
|
||||
"folderName": {
|
||||
"type": "string",
|
||||
"description": "Name of folder to create",
|
||||
"example": "my_folder"
|
||||
},
|
||||
"directory": {
|
||||
"type": "string",
|
||||
"description": "Path where to create folder like / or /foo/bar. Default is /",
|
||||
"example": "/foo/bar"
|
||||
}
|
||||
},
|
||||
"required": ["workspaceId", "environment", "folderName"]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#swagger.responses[200] = {
|
||||
content: {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"folder": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string",
|
||||
"description": "ID of folder",
|
||||
"example": "someFolderId"
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "Name of folder",
|
||||
"example": "my_folder"
|
||||
},
|
||||
"version": {
|
||||
"type": "number",
|
||||
"description": "Version of folder",
|
||||
"example": 1
|
||||
}
|
||||
},
|
||||
"description": "Details of 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, directory }
|
||||
} = await validateRequest(reqValidator.CreateFolderV1, req);
|
||||
|
||||
if (!validateFolderName(folderName)) {
|
||||
throw BadRequestError({
|
||||
message: "Folder name cannot contain spaces. Only underscore and dashes"
|
||||
});
|
||||
}
|
||||
|
||||
if (req.authData.authPayload instanceof ServiceTokenData) {
|
||||
// token check
|
||||
const isValidScopeAccess = isValidScope(req.authData.authPayload, environment, directory);
|
||||
if (!isValidScopeAccess) {
|
||||
throw UnauthorizedRequestError({ message: "Folder Permission Denied" });
|
||||
}
|
||||
} else {
|
||||
// user check
|
||||
const { permission } = await getAuthDataProjectPermissions({
|
||||
authData: req.authData,
|
||||
workspaceId: new Types.ObjectId(workspaceId)
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Create,
|
||||
subject(ProjectPermissionSub.Secrets, { environment, secretPath: directory })
|
||||
);
|
||||
}
|
||||
|
||||
const folders = await Folder.findOne({
|
||||
workspace: workspaceId,
|
||||
environment
|
||||
}).lean();
|
||||
|
||||
// space has no folders initialized
|
||||
if (!folders) {
|
||||
const folder = new Folder({
|
||||
workspace: workspaceId,
|
||||
environment,
|
||||
nodes: {
|
||||
id: "root",
|
||||
name: "root",
|
||||
version: 1,
|
||||
children: []
|
||||
}
|
||||
});
|
||||
const { parent, child } = appendFolder(folder.nodes, { folderName, directory });
|
||||
await folder.save();
|
||||
const folderVersion = new FolderVersion({
|
||||
workspace: workspaceId,
|
||||
environment,
|
||||
nodes: parent
|
||||
});
|
||||
await folderVersion.save();
|
||||
await EESecretService.takeSecretSnapshot({
|
||||
workspaceId: new Types.ObjectId(workspaceId),
|
||||
environment
|
||||
});
|
||||
|
||||
await EEAuditLogService.createAuditLog(
|
||||
req.authData,
|
||||
{
|
||||
type: EventType.CREATE_FOLDER,
|
||||
metadata: {
|
||||
environment,
|
||||
folderId: child.id,
|
||||
folderName,
|
||||
folderPath: directory
|
||||
}
|
||||
},
|
||||
{
|
||||
workspaceId: new Types.ObjectId(workspaceId)
|
||||
}
|
||||
);
|
||||
|
||||
return res.json({ folder: { id: child.id, name: folderName } });
|
||||
}
|
||||
|
||||
const { parent, child, hasCreated } = appendFolder(folders.nodes, { folderName, directory });
|
||||
|
||||
if (!hasCreated) return res.json({ folder: child });
|
||||
|
||||
await Folder.findByIdAndUpdate(folders._id, folders);
|
||||
|
||||
const folderVersion = new FolderVersion({
|
||||
workspace: workspaceId,
|
||||
environment,
|
||||
nodes: parent
|
||||
});
|
||||
await folderVersion.save();
|
||||
|
||||
await EESecretService.takeSecretSnapshot({
|
||||
workspaceId: new Types.ObjectId(workspaceId),
|
||||
environment,
|
||||
folderId: child.id
|
||||
});
|
||||
|
||||
await EEAuditLogService.createAuditLog(
|
||||
req.authData,
|
||||
{
|
||||
type: EventType.CREATE_FOLDER,
|
||||
metadata: {
|
||||
environment,
|
||||
folderId: child.id,
|
||||
folderName,
|
||||
folderPath: directory
|
||||
}
|
||||
},
|
||||
{
|
||||
workspaceId: new Types.ObjectId(workspaceId)
|
||||
}
|
||||
);
|
||||
|
||||
return res.json({ folder: child });
|
||||
};
|
||||
|
||||
/**
|
||||
* Update folder with id [folderId]
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
export const updateFolderById = async (req: Request, res: Response) => {
|
||||
/*
|
||||
#swagger.summary = 'Update folder'
|
||||
#swagger.description = 'Update folder'
|
||||
|
||||
#swagger.security = [{
|
||||
"apiKeyAuth": [],
|
||||
"bearerAuth": []
|
||||
}]
|
||||
|
||||
#swagger.parameters['folderName'] = {
|
||||
"description": "Name of folder to update",
|
||||
"required": true,
|
||||
"type": "string"
|
||||
}
|
||||
|
||||
#swagger.requestBody = {
|
||||
content: {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"workspaceId": {
|
||||
"type": "string",
|
||||
"description": "ID of workspace where to update folder",
|
||||
"example": "someWorkspaceId"
|
||||
},
|
||||
"environment": {
|
||||
"type": "string",
|
||||
"description": "Slug of environment where to update folder",
|
||||
"example": "production"
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "Name of folder to update to",
|
||||
"example": "updated_folder_name"
|
||||
},
|
||||
"directory": {
|
||||
"type": "string",
|
||||
"description": "Path where to update folder like / or /foo/bar. Default is /",
|
||||
"example": "/foo/bar"
|
||||
}
|
||||
},
|
||||
"required": ["workspaceId", "environment", "name"]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#swagger.responses[200] = {
|
||||
content: {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"message": {
|
||||
"type": "string",
|
||||
"description": "Success message",
|
||||
"example": "Successfully updated folder"
|
||||
},
|
||||
"folder": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "Name of updated folder",
|
||||
"example": "updated_folder_name"
|
||||
},
|
||||
"id": {
|
||||
"type": "string",
|
||||
"description": "ID of created folder",
|
||||
"example": "abc123"
|
||||
}
|
||||
},
|
||||
"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, directory },
|
||||
params: { folderName }
|
||||
} = await validateRequest(reqValidator.UpdateFolderV1, req);
|
||||
|
||||
if (!validateFolderName(name)) {
|
||||
throw BadRequestError({
|
||||
message: "Folder name cannot contain spaces. Only underscore and dashes"
|
||||
});
|
||||
}
|
||||
|
||||
if (req.authData.authPayload instanceof ServiceTokenData) {
|
||||
const isValidScopeAccess = isValidScope(req.authData.authPayload, environment, directory);
|
||||
if (!isValidScopeAccess) {
|
||||
throw UnauthorizedRequestError({ message: "Folder Permission Denied" });
|
||||
}
|
||||
} else {
|
||||
const { permission } = await getAuthDataProjectPermissions({
|
||||
authData: req.authData,
|
||||
workspaceId: new Types.ObjectId(workspaceId)
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Edit,
|
||||
subject(ProjectPermissionSub.Secrets, { environment, secretPath: directory })
|
||||
);
|
||||
}
|
||||
|
||||
const folders = await Folder.findOne({ workspace: workspaceId, environment });
|
||||
if (!folders) {
|
||||
throw BadRequestError({ message: "The folder doesn't exist" });
|
||||
}
|
||||
|
||||
const parentFolder = getFolderByPath(folders.nodes, directory);
|
||||
if (!parentFolder) {
|
||||
throw BadRequestError({ message: "The folder doesn't exist" });
|
||||
}
|
||||
|
||||
const folder = parentFolder.children.find(({ name }) => name === folderName);
|
||||
if (!folder) throw ERR_FOLDER_NOT_FOUND;
|
||||
|
||||
const oldFolderName = folder.name;
|
||||
parentFolder.version += 1;
|
||||
folder.name = name;
|
||||
|
||||
await Folder.findByIdAndUpdate(folders._id, folders);
|
||||
const folderVersion = new FolderVersion({
|
||||
workspace: workspaceId,
|
||||
environment,
|
||||
nodes: parentFolder
|
||||
});
|
||||
await folderVersion.save();
|
||||
|
||||
await EESecretService.takeSecretSnapshot({
|
||||
workspaceId: new Types.ObjectId(workspaceId),
|
||||
environment,
|
||||
folderId: parentFolder.id
|
||||
});
|
||||
|
||||
const { folderPath } = getFolderWithPathFromId(folders.nodes, folder.id);
|
||||
|
||||
await EEAuditLogService.createAuditLog(
|
||||
req.authData,
|
||||
{
|
||||
type: EventType.UPDATE_FOLDER,
|
||||
metadata: {
|
||||
environment,
|
||||
folderId: folder.id,
|
||||
oldFolderName,
|
||||
newFolderName: name,
|
||||
folderPath
|
||||
}
|
||||
},
|
||||
{
|
||||
workspaceId: new Types.ObjectId(workspaceId)
|
||||
}
|
||||
);
|
||||
|
||||
return res.json({
|
||||
message: "Successfully updated folder",
|
||||
folder: { name: folder.name, id: folder.id }
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Delete folder with id [folderId]
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
export const deleteFolder = async (req: Request, res: Response) => {
|
||||
/*
|
||||
#swagger.summary = 'Delete folder'
|
||||
#swagger.description = 'Delete folder'
|
||||
|
||||
#swagger.security = [{
|
||||
"apiKeyAuth": [],
|
||||
"bearerAuth": []
|
||||
}]
|
||||
|
||||
#swagger.parameters['folderName'] = {
|
||||
"description": "Name of folder to delete",
|
||||
"required": true,
|
||||
"type": "string",
|
||||
"in": "path"
|
||||
}
|
||||
|
||||
#swagger.requestBody = {
|
||||
content: {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"workspaceId": {
|
||||
"type": "string",
|
||||
"description": "ID of the workspace where to delete folder",
|
||||
"example": "someWorkspaceId"
|
||||
},
|
||||
"environment": {
|
||||
"type": "string",
|
||||
"description": "Slug of environment where to delete folder",
|
||||
"example": "production"
|
||||
},
|
||||
"directory": {
|
||||
"type": "string",
|
||||
"description": "Path where to delete folder like / or /foo/bar. Default is /",
|
||||
"example": "/foo/bar"
|
||||
}
|
||||
},
|
||||
"required": ["workspaceId", "environment"]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#swagger.responses[200] = {
|
||||
content: {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"message": {
|
||||
"type": "string",
|
||||
"description": "Success message",
|
||||
"example": "successfully deleted folders"
|
||||
},
|
||||
"folders": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string",
|
||||
"description": "ID of deleted folder",
|
||||
"example": "abc123"
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "Name of deleted folder",
|
||||
"example": "someFolderName"
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": "List of IDs and names of 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: { folderName },
|
||||
body: { environment, workspaceId, directory }
|
||||
} = await validateRequest(reqValidator.DeleteFolderV1, req);
|
||||
|
||||
if (req.authData.authPayload instanceof ServiceTokenData) {
|
||||
const isValidScopeAccess = isValidScope(req.authData.authPayload, environment, directory);
|
||||
if (!isValidScopeAccess) {
|
||||
throw UnauthorizedRequestError({ message: "Folder Permission Denied" });
|
||||
}
|
||||
} else {
|
||||
// check that user is a member of the workspace
|
||||
const { permission } = await getAuthDataProjectPermissions({
|
||||
authData: req.authData,
|
||||
workspaceId: new Types.ObjectId(workspaceId)
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Delete,
|
||||
subject(ProjectPermissionSub.Secrets, { environment, secretPath: directory })
|
||||
);
|
||||
}
|
||||
|
||||
const folders = await Folder.findOne({ workspace: workspaceId, environment });
|
||||
if (!folders) throw ERR_FOLDER_NOT_FOUND;
|
||||
|
||||
const parentFolder = getFolderByPath(folders.nodes, directory);
|
||||
if (!parentFolder) throw ERR_FOLDER_NOT_FOUND;
|
||||
|
||||
const index = parentFolder.children.findIndex(({ name }) => name === folderName);
|
||||
if (index === -1) throw ERR_FOLDER_NOT_FOUND;
|
||||
|
||||
const deletedFolder = parentFolder.children.splice(index, 1)[0];
|
||||
|
||||
parentFolder.version += 1;
|
||||
const delFolderIds = getAllFolderIds(deletedFolder);
|
||||
|
||||
await Folder.findByIdAndUpdate(folders._id, folders);
|
||||
const folderVersion = new FolderVersion({
|
||||
workspace: workspaceId,
|
||||
environment,
|
||||
nodes: parentFolder
|
||||
});
|
||||
await folderVersion.save();
|
||||
if (delFolderIds.length) {
|
||||
await Secret.deleteMany({
|
||||
folder: { $in: delFolderIds.map(({ id }) => id) },
|
||||
workspace: workspaceId,
|
||||
environment
|
||||
});
|
||||
}
|
||||
|
||||
await EESecretService.takeSecretSnapshot({
|
||||
workspaceId: new Types.ObjectId(workspaceId),
|
||||
environment,
|
||||
folderId: parentFolder.id
|
||||
});
|
||||
|
||||
await EEAuditLogService.createAuditLog(
|
||||
req.authData,
|
||||
{
|
||||
type: EventType.DELETE_FOLDER,
|
||||
metadata: {
|
||||
environment,
|
||||
folderId: deletedFolder.id,
|
||||
folderName: deletedFolder.name,
|
||||
folderPath: directory
|
||||
}
|
||||
},
|
||||
{
|
||||
workspaceId: new Types.ObjectId(workspaceId)
|
||||
}
|
||||
);
|
||||
|
||||
return res.send({ message: "successfully deleted folders", folders: delFolderIds });
|
||||
};
|
||||
|
||||
/**
|
||||
* Get folders for workspace with id [workspaceId] and environment [environment]
|
||||
* considering directory/path [directory]
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
export const getFolders = async (req: Request, res: Response) => {
|
||||
/*
|
||||
#swagger.summary = 'Get folders'
|
||||
#swagger.description = 'Get folders'
|
||||
|
||||
#swagger.security = [{
|
||||
"apiKeyAuth": [],
|
||||
"bearerAuth": []
|
||||
}]
|
||||
|
||||
#swagger.parameters['workspaceId'] = {
|
||||
"description": "ID of the workspace where to get folders from",
|
||||
"required": true,
|
||||
"type": "string",
|
||||
"in": "query"
|
||||
}
|
||||
|
||||
#swagger.parameters['environment'] = {
|
||||
"description": "Slug of environment where to get folders from",
|
||||
"required": true,
|
||||
"type": "string",
|
||||
"in": "query"
|
||||
}
|
||||
|
||||
#swagger.parameters['directory'] = {
|
||||
"description": "Path where to get fodlers from like / or /foo/bar. Default is /",
|
||||
"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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#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, directory }
|
||||
} = await validateRequest(reqValidator.GetFoldersV1, req);
|
||||
|
||||
if (req.authData.authPayload instanceof ServiceTokenData) {
|
||||
const isValidScopeAccess = isValidScope(req.authData.authPayload, environment, directory);
|
||||
if (!isValidScopeAccess) {
|
||||
throw UnauthorizedRequestError({ message: "Folder Permission Denied" });
|
||||
}
|
||||
} else {
|
||||
// check that user is a member of the workspace
|
||||
await getAuthDataProjectPermissions({
|
||||
authData: req.authData,
|
||||
workspaceId: new Types.ObjectId(workspaceId)
|
||||
});
|
||||
}
|
||||
|
||||
const folders = await Folder.findOne({ workspace: workspaceId, environment });
|
||||
if (!folders) {
|
||||
return res.send({ folders: [], dir: [] });
|
||||
}
|
||||
|
||||
const folder = getFolderByPath(folders.nodes, directory);
|
||||
|
||||
return res.send({
|
||||
folders: folder?.children?.map(({ id, name }) => ({ id, name })) || []
|
||||
});
|
||||
};
|
@ -1,75 +0,0 @@
|
||||
import { Request, Response } from "express";
|
||||
import { ServiceToken } from "../../models";
|
||||
import { createToken } from "../../helpers/auth";
|
||||
import { getJwtServiceSecret } from "../../config";
|
||||
|
||||
/**
|
||||
* Return service token on request
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
export const getServiceToken = async (req: Request, res: Response) => {
|
||||
return res.status(200).send({
|
||||
serviceToken: req.serviceToken,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Create and return a new service token
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
export const createServiceToken = async (req: Request, res: Response) => {
|
||||
let token;
|
||||
try {
|
||||
const {
|
||||
name,
|
||||
workspaceId,
|
||||
environment,
|
||||
expiresIn,
|
||||
publicKey,
|
||||
encryptedKey,
|
||||
nonce,
|
||||
} = req.body;
|
||||
|
||||
// validate environment
|
||||
const workspaceEnvs = req.membership.workspace.environments;
|
||||
if (!workspaceEnvs.find(({ slug }: { slug: string }) => slug === environment)) {
|
||||
throw new Error("Failed to validate environment");
|
||||
}
|
||||
|
||||
// compute access token expiration date
|
||||
const expiresAt = new Date();
|
||||
expiresAt.setSeconds(expiresAt.getSeconds() + expiresIn);
|
||||
|
||||
const serviceToken = await new ServiceToken({
|
||||
name,
|
||||
user: req.user._id,
|
||||
workspace: workspaceId,
|
||||
environment,
|
||||
expiresAt,
|
||||
publicKey,
|
||||
encryptedKey,
|
||||
nonce,
|
||||
}).save();
|
||||
|
||||
token = createToken({
|
||||
payload: {
|
||||
serviceTokenId: serviceToken._id.toString(),
|
||||
workspaceId,
|
||||
},
|
||||
expiresIn: expiresIn,
|
||||
secret: await getJwtServiceSecret(),
|
||||
});
|
||||
} catch (err) {
|
||||
return res.status(400).send({
|
||||
message: "Failed to create service token",
|
||||
});
|
||||
}
|
||||
|
||||
return res.status(200).send({
|
||||
token,
|
||||
});
|
||||
};
|
@ -1,99 +0,0 @@
|
||||
import { Request, Response } from "express";
|
||||
import { AuthMethod, User } from "../../models";
|
||||
import { checkEmailVerification, sendEmailVerification } from "../../helpers/signup";
|
||||
import { createToken } from "../../helpers/auth";
|
||||
import {
|
||||
getAuthSecret,
|
||||
getJwtSignupLifetime,
|
||||
getSmtpConfigured
|
||||
} from "../../config";
|
||||
import { validateUserEmail } from "../../validation";
|
||||
import { validateRequest } from "../../helpers/validation";
|
||||
import * as reqValidator from "../../validation/auth";
|
||||
import { AuthTokenType } from "../../variables";
|
||||
|
||||
/**
|
||||
* Signup step 1: Initialize account for user under email [email] and send a verification code
|
||||
* to that email
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
export const beginEmailSignup = async (req: Request, res: Response) => {
|
||||
const {
|
||||
body: { email }
|
||||
} = await validateRequest(reqValidator.BeginEmailSignUpV1, req);
|
||||
|
||||
// validate that email is not disposable
|
||||
validateUserEmail(email);
|
||||
|
||||
const user = await User.findOne({ email }).select("+publicKey");
|
||||
if (user && user?.publicKey) {
|
||||
// case: user has already completed account
|
||||
|
||||
return res.status(403).send({
|
||||
error: "Failed to send email verification code for complete account"
|
||||
});
|
||||
}
|
||||
|
||||
// send send verification email
|
||||
await sendEmailVerification({ email });
|
||||
|
||||
return res.status(200).send({
|
||||
message: `Sent an email verification code to ${email}`
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Signup step 2: Verify that code [code] was sent to email [email] and issue
|
||||
* a temporary signup token for user to complete setting up their account
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
export const verifyEmailSignup = async (req: Request, res: Response) => {
|
||||
let user;
|
||||
const {
|
||||
body: { email, code }
|
||||
} = await validateRequest(reqValidator.VerifyEmailSignUpV1, req);
|
||||
|
||||
// initialize user account
|
||||
user = await User.findOne({ email }).select("+publicKey");
|
||||
if (user && user?.publicKey) {
|
||||
// case: user has already completed account
|
||||
return res.status(403).send({
|
||||
error: "Failed email verification for complete user"
|
||||
});
|
||||
}
|
||||
|
||||
// verify email
|
||||
if (await getSmtpConfigured()) {
|
||||
await checkEmailVerification({
|
||||
email,
|
||||
code
|
||||
});
|
||||
}
|
||||
|
||||
if (!user) {
|
||||
user = await new User({
|
||||
email,
|
||||
authMethods: [AuthMethod.EMAIL]
|
||||
}).save();
|
||||
}
|
||||
|
||||
// generate temporary signup token
|
||||
const token = createToken({
|
||||
payload: {
|
||||
authTokenType: AuthTokenType.SIGNUP_TOKEN,
|
||||
userId: user._id.toString()
|
||||
},
|
||||
expiresIn: await getJwtSignupLifetime(),
|
||||
secret: await getAuthSecret()
|
||||
});
|
||||
|
||||
return res.status(200).send({
|
||||
message: "Successfuly verified email",
|
||||
user,
|
||||
token
|
||||
});
|
||||
};
|
File diff suppressed because it is too large
Load Diff
@ -1,56 +0,0 @@
|
||||
import { Request, Response } from "express";
|
||||
import { validateRequest } from "../../helpers/validation";
|
||||
import { UserAction } from "../../models";
|
||||
import * as reqValidator from "../../validation/action";
|
||||
|
||||
/**
|
||||
* Add user action [action]
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
export const addUserAction = async (req: Request, res: Response) => {
|
||||
// add/record new action [action] for user with id [req.user._id]
|
||||
const {
|
||||
body: { action }
|
||||
} = await validateRequest(reqValidator.AddUserActionV1, req);
|
||||
|
||||
const userAction = await UserAction.findOneAndUpdate(
|
||||
{
|
||||
user: req.user._id,
|
||||
action
|
||||
},
|
||||
{ user: req.user._id, action },
|
||||
{
|
||||
new: true,
|
||||
upsert: true
|
||||
}
|
||||
);
|
||||
|
||||
return res.status(200).send({
|
||||
message: "Successfully recorded user action",
|
||||
userAction
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Return user action [action] for user
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
export const getUserAction = async (req: Request, res: Response) => {
|
||||
// get user action [action] for user with id [req.user._id]
|
||||
const {
|
||||
query: { action }
|
||||
} = await validateRequest(reqValidator.GetUserActionV1, req);
|
||||
|
||||
const userAction = await UserAction.findOne({
|
||||
user: req.user._id,
|
||||
action
|
||||
});
|
||||
|
||||
return res.status(200).send({
|
||||
userAction
|
||||
});
|
||||
};
|
@ -1,13 +0,0 @@
|
||||
import { Request, Response } from "express";
|
||||
|
||||
/**
|
||||
* Return user on request
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
export const getUser = async (req: Request, res: Response) => {
|
||||
return res.status(200).send({
|
||||
user: req.user,
|
||||
});
|
||||
};
|
@ -1,268 +0,0 @@
|
||||
import { Request, Response } from "express";
|
||||
import { Types } from "mongoose";
|
||||
import { client, getEncryptionKey, getRootEncryptionKey } from "../../config";
|
||||
import { Webhook } from "../../models";
|
||||
import { getWebhookPayload, triggerWebhookRequest } from "../../services/WebhookService";
|
||||
import { BadRequestError, ResourceNotFoundError } from "../../utils/errors";
|
||||
import { EEAuditLogService } from "../../ee/services";
|
||||
import { EventType } from "../../ee/models";
|
||||
import {
|
||||
ALGORITHM_AES_256_GCM,
|
||||
ENCODING_SCHEME_BASE64,
|
||||
ENCODING_SCHEME_UTF8
|
||||
} from "../../variables";
|
||||
import { validateRequest } from "../../helpers/validation";
|
||||
import * as reqValidator from "../../validation/webhooks";
|
||||
import {
|
||||
ProjectPermissionActions,
|
||||
ProjectPermissionSub,
|
||||
getAuthDataProjectPermissions
|
||||
} from "../../ee/services/ProjectRoleService";
|
||||
import { ForbiddenError } from "@casl/ability";
|
||||
import { encryptSymmetric128BitHexKeyUTF8 } from "../../utils/crypto";
|
||||
|
||||
export const createWebhook = async (req: Request, res: Response) => {
|
||||
const {
|
||||
body: { webhookUrl, webhookSecretKey, environment, workspaceId, secretPath }
|
||||
} = await validateRequest(reqValidator.CreateWebhookV1, req);
|
||||
|
||||
const { permission } = await getAuthDataProjectPermissions({
|
||||
authData: req.authData,
|
||||
workspaceId: new Types.ObjectId(workspaceId)
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Create,
|
||||
ProjectPermissionSub.Webhooks
|
||||
);
|
||||
|
||||
const webhook = new Webhook({
|
||||
workspace: workspaceId,
|
||||
environment,
|
||||
secretPath,
|
||||
url: webhookUrl
|
||||
});
|
||||
|
||||
if (webhookSecretKey) {
|
||||
const encryptionKey = await getEncryptionKey();
|
||||
const rootEncryptionKey = await getRootEncryptionKey();
|
||||
|
||||
if (rootEncryptionKey) {
|
||||
const { ciphertext, iv, tag } = client.encryptSymmetric(webhookSecretKey, rootEncryptionKey);
|
||||
webhook.iv = iv;
|
||||
webhook.tag = tag;
|
||||
webhook.encryptedSecretKey = ciphertext;
|
||||
webhook.algorithm = ALGORITHM_AES_256_GCM;
|
||||
webhook.keyEncoding = ENCODING_SCHEME_BASE64;
|
||||
} else if (encryptionKey) {
|
||||
const { ciphertext, iv, tag } = encryptSymmetric128BitHexKeyUTF8({
|
||||
plaintext: webhookSecretKey,
|
||||
key: encryptionKey
|
||||
});
|
||||
webhook.iv = iv;
|
||||
webhook.tag = tag;
|
||||
webhook.encryptedSecretKey = ciphertext;
|
||||
webhook.algorithm = ALGORITHM_AES_256_GCM;
|
||||
webhook.keyEncoding = ENCODING_SCHEME_UTF8;
|
||||
}
|
||||
}
|
||||
|
||||
await webhook.save();
|
||||
|
||||
await EEAuditLogService.createAuditLog(
|
||||
req.authData,
|
||||
{
|
||||
type: EventType.CREATE_WEBHOOK,
|
||||
metadata: {
|
||||
webhookId: webhook._id.toString(),
|
||||
environment,
|
||||
secretPath,
|
||||
webhookUrl,
|
||||
isDisabled: false
|
||||
}
|
||||
},
|
||||
{
|
||||
workspaceId: new Types.ObjectId(workspaceId)
|
||||
}
|
||||
);
|
||||
|
||||
return res.status(200).send({
|
||||
webhook,
|
||||
message: "successfully created webhook"
|
||||
});
|
||||
};
|
||||
|
||||
export const updateWebhook = async (req: Request, res: Response) => {
|
||||
const {
|
||||
body: { isDisabled },
|
||||
params: { webhookId }
|
||||
} = await validateRequest(reqValidator.UpdateWebhookV1, req);
|
||||
|
||||
const webhook = await Webhook.findById(webhookId);
|
||||
if (!webhook) {
|
||||
throw BadRequestError({ message: "Webhook not found!!" });
|
||||
}
|
||||
|
||||
const { permission } = await getAuthDataProjectPermissions({
|
||||
authData: req.authData,
|
||||
workspaceId: webhook.workspace
|
||||
});
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Edit,
|
||||
ProjectPermissionSub.Webhooks
|
||||
);
|
||||
|
||||
if (typeof isDisabled !== undefined) {
|
||||
webhook.isDisabled = isDisabled;
|
||||
}
|
||||
await webhook.save();
|
||||
|
||||
await EEAuditLogService.createAuditLog(
|
||||
req.authData,
|
||||
{
|
||||
type: EventType.UPDATE_WEBHOOK_STATUS,
|
||||
metadata: {
|
||||
webhookId: webhook._id.toString(),
|
||||
environment: webhook.environment,
|
||||
secretPath: webhook.secretPath,
|
||||
webhookUrl: webhook.url,
|
||||
isDisabled
|
||||
}
|
||||
},
|
||||
{
|
||||
workspaceId: webhook.workspace
|
||||
}
|
||||
);
|
||||
|
||||
return res.status(200).send({
|
||||
webhook,
|
||||
message: "successfully updated webhook"
|
||||
});
|
||||
};
|
||||
|
||||
export const deleteWebhook = async (req: Request, res: Response) => {
|
||||
const {
|
||||
params: { webhookId }
|
||||
} = await validateRequest(reqValidator.DeleteWebhookV1, req);
|
||||
let webhook = await Webhook.findById(webhookId);
|
||||
|
||||
if (!webhook) {
|
||||
throw ResourceNotFoundError({ message: "Webhook not found!!" });
|
||||
}
|
||||
|
||||
const { permission } = await getAuthDataProjectPermissions({
|
||||
authData: req.authData,
|
||||
workspaceId: webhook.workspace
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Delete,
|
||||
ProjectPermissionSub.Webhooks
|
||||
);
|
||||
|
||||
webhook = await Webhook.findByIdAndDelete(webhookId);
|
||||
|
||||
if (!webhook) {
|
||||
throw ResourceNotFoundError({ message: "Webhook not found!!" });
|
||||
}
|
||||
|
||||
await EEAuditLogService.createAuditLog(
|
||||
req.authData,
|
||||
{
|
||||
type: EventType.DELETE_WEBHOOK,
|
||||
metadata: {
|
||||
webhookId: webhook._id.toString(),
|
||||
environment: webhook.environment,
|
||||
secretPath: webhook.secretPath,
|
||||
webhookUrl: webhook.url,
|
||||
isDisabled: webhook.isDisabled
|
||||
}
|
||||
},
|
||||
{
|
||||
workspaceId: webhook.workspace
|
||||
}
|
||||
);
|
||||
|
||||
return res.status(200).send({
|
||||
message: "successfully removed webhook"
|
||||
});
|
||||
};
|
||||
|
||||
export const testWebhook = async (req: Request, res: Response) => {
|
||||
const {
|
||||
params: { webhookId }
|
||||
} = await validateRequest(reqValidator.TestWebhookV1, req);
|
||||
|
||||
const webhook = await Webhook.findById(webhookId);
|
||||
if (!webhook) {
|
||||
throw BadRequestError({ message: "Webhook not found!!" });
|
||||
}
|
||||
|
||||
const { permission } = await getAuthDataProjectPermissions({
|
||||
authData: req.authData,
|
||||
workspaceId: webhook.workspace
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Read,
|
||||
ProjectPermissionSub.Webhooks
|
||||
);
|
||||
|
||||
try {
|
||||
await triggerWebhookRequest(
|
||||
webhook,
|
||||
getWebhookPayload(
|
||||
"test",
|
||||
webhook.workspace.toString(),
|
||||
webhook.environment,
|
||||
webhook.secretPath
|
||||
)
|
||||
);
|
||||
await Webhook.findByIdAndUpdate(webhookId, {
|
||||
lastStatus: "success",
|
||||
lastRunErrorMessage: null
|
||||
});
|
||||
} catch (err) {
|
||||
await Webhook.findByIdAndUpdate(webhookId, {
|
||||
lastStatus: "failed",
|
||||
lastRunErrorMessage: (err as Error).message
|
||||
});
|
||||
return res.status(400).send({
|
||||
message: "Failed to receive response",
|
||||
error: (err as Error).message
|
||||
});
|
||||
}
|
||||
|
||||
return res.status(200).send({
|
||||
message: "Successfully received response"
|
||||
});
|
||||
};
|
||||
|
||||
export const listWebhooks = async (req: Request, res: Response) => {
|
||||
const {
|
||||
query: { environment, workspaceId, secretPath }
|
||||
} = await validateRequest(reqValidator.ListWebhooksV1, req);
|
||||
|
||||
const { permission } = await getAuthDataProjectPermissions({
|
||||
authData: req.authData,
|
||||
workspaceId: new Types.ObjectId(workspaceId)
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Read,
|
||||
ProjectPermissionSub.Webhooks
|
||||
);
|
||||
|
||||
const optionalFilters: Record<string, string> = {};
|
||||
if (environment) optionalFilters.environment = environment as string;
|
||||
if (secretPath) optionalFilters.secretPath = secretPath as string;
|
||||
|
||||
const webhooks = await Webhook.find({
|
||||
workspace: new Types.ObjectId(workspaceId as string),
|
||||
...optionalFilters
|
||||
});
|
||||
|
||||
return res.status(200).send({
|
||||
webhooks
|
||||
});
|
||||
};
|
@ -1,359 +0,0 @@
|
||||
import { Types } from "mongoose";
|
||||
import { Request, Response } from "express";
|
||||
import {
|
||||
IUser,
|
||||
Integration,
|
||||
IntegrationAuth,
|
||||
Membership,
|
||||
Organization,
|
||||
ServiceToken,
|
||||
Workspace
|
||||
} from "../../models";
|
||||
import { createWorkspace as create, deleteWorkspace as deleteWork } from "../../helpers/workspace";
|
||||
import { EELicenseService } from "../../ee/services";
|
||||
import { addMemberships } from "../../helpers/membership";
|
||||
import { ADMIN } from "../../variables";
|
||||
import { OrganizationNotFoundError } from "../../utils/errors";
|
||||
import {
|
||||
OrgPermissionActions,
|
||||
OrgPermissionSubjects,
|
||||
getAuthDataOrgPermissions
|
||||
} from "../../ee/services/RoleService";
|
||||
import { ForbiddenError } from "@casl/ability";
|
||||
import { validateRequest } from "../../helpers/validation";
|
||||
import * as reqValidator from "../../validation";
|
||||
import {
|
||||
ProjectPermissionActions,
|
||||
ProjectPermissionSub,
|
||||
getAuthDataProjectPermissions
|
||||
} from "../../ee/services/ProjectRoleService";
|
||||
|
||||
/**
|
||||
* Return public keys of members of workspace with id [workspaceId]
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
export const getWorkspacePublicKeys = async (req: Request, res: Response) => {
|
||||
const {
|
||||
params: { workspaceId }
|
||||
} = await validateRequest(reqValidator.GetWorkspacePublicKeysV1, req);
|
||||
|
||||
const { permission } = await getAuthDataProjectPermissions({
|
||||
authData: req.authData,
|
||||
workspaceId: new Types.ObjectId(workspaceId)
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Read,
|
||||
ProjectPermissionSub.Member
|
||||
);
|
||||
|
||||
const publicKeys = (
|
||||
await Membership.find({
|
||||
workspace: workspaceId
|
||||
}).populate<{ user: IUser }>("user", "publicKey")
|
||||
).map((member) => {
|
||||
return {
|
||||
publicKey: member.user.publicKey,
|
||||
userId: member.user._id
|
||||
};
|
||||
});
|
||||
|
||||
return res.status(200).send({
|
||||
publicKeys
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Return memberships for workspace with id [workspaceId]
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
export const getWorkspaceMemberships = async (req: Request, res: Response) => {
|
||||
const {
|
||||
params: { workspaceId }
|
||||
} = await validateRequest(reqValidator.GetWorkspaceMembershipsV1, req);
|
||||
|
||||
const { permission } = await getAuthDataProjectPermissions({
|
||||
authData: req.authData,
|
||||
workspaceId: new Types.ObjectId(workspaceId)
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Read,
|
||||
ProjectPermissionSub.Member
|
||||
);
|
||||
|
||||
const users = await Membership.find({
|
||||
workspace: workspaceId
|
||||
}).populate("user", "+publicKey");
|
||||
|
||||
return res.status(200).send({
|
||||
users
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Return workspaces that user is part of
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
export const getWorkspaces = async (req: Request, res: Response) => {
|
||||
const workspaces = (
|
||||
await Membership.find({
|
||||
user: req.user._id
|
||||
}).populate("workspace")
|
||||
).map((m) => m.workspace);
|
||||
|
||||
return res.status(200).send({
|
||||
workspaces
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Return workspace with id [workspaceId]
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
export const getWorkspace = async (req: Request, res: Response) => {
|
||||
const {
|
||||
params: { workspaceId }
|
||||
} = await validateRequest(reqValidator.GetWorkspaceV1, req);
|
||||
|
||||
const workspace = await Workspace.findOne({
|
||||
_id: workspaceId
|
||||
});
|
||||
|
||||
return res.status(200).send({
|
||||
workspace
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Create new workspace named [workspaceName] under organization with id
|
||||
* [organizationId] and add user as admin
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
export const createWorkspace = async (req: Request, res: Response) => {
|
||||
const {
|
||||
body: { organizationId, workspaceName }
|
||||
} = await validateRequest(reqValidator.CreateWorkspaceV1, req);
|
||||
|
||||
const organization = await Organization.findById(organizationId);
|
||||
if (!organization) {
|
||||
throw OrganizationNotFoundError({
|
||||
message: "Failed to find organization"
|
||||
});
|
||||
}
|
||||
|
||||
const { permission } = await getAuthDataOrgPermissions({
|
||||
authData: req.authData,
|
||||
organizationId: new Types.ObjectId(organizationId)
|
||||
});
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
OrgPermissionActions.Create,
|
||||
OrgPermissionSubjects.Workspace
|
||||
);
|
||||
|
||||
const plan = await EELicenseService.getPlan(new Types.ObjectId(organizationId));
|
||||
|
||||
if (plan.workspaceLimit !== null) {
|
||||
// case: limit imposed on number of workspaces allowed
|
||||
if (plan.workspacesUsed >= plan.workspaceLimit) {
|
||||
// case: number of workspaces used exceeds the number of workspaces allowed
|
||||
return res.status(400).send({
|
||||
message:
|
||||
"Failed to create workspace due to plan limit reached. Upgrade plan to add more workspaces."
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (workspaceName.length < 1) {
|
||||
throw new Error("Workspace names must be at least 1-character long");
|
||||
}
|
||||
|
||||
// create workspace and add user as member
|
||||
const workspace = await create({
|
||||
name: workspaceName,
|
||||
organizationId: new Types.ObjectId(organizationId)
|
||||
});
|
||||
|
||||
await addMemberships({
|
||||
userIds: [req.user._id],
|
||||
workspaceId: workspace._id.toString(),
|
||||
roles: [ADMIN]
|
||||
});
|
||||
|
||||
return res.status(200).send({
|
||||
workspace
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Delete workspace with id [workspaceId]
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
export const deleteWorkspace = async (req: Request, res: Response) => {
|
||||
const {
|
||||
params: { workspaceId }
|
||||
} = await validateRequest(reqValidator.DeleteWorkspaceV1, req);
|
||||
|
||||
const { permission } = await getAuthDataProjectPermissions({
|
||||
authData: req.authData,
|
||||
workspaceId: new Types.ObjectId(workspaceId)
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Delete,
|
||||
ProjectPermissionSub.Workspace
|
||||
);
|
||||
|
||||
// delete workspace
|
||||
const workspace = await deleteWork({
|
||||
workspaceId: new Types.ObjectId(workspaceId)
|
||||
});
|
||||
|
||||
return res.status(200).send({
|
||||
workspace
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Change name of workspace with id [workspaceId] to [name]
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
export const changeWorkspaceName = async (req: Request, res: Response) => {
|
||||
const {
|
||||
params: { workspaceId },
|
||||
body: { name }
|
||||
} = await validateRequest(reqValidator.ChangeWorkspaceNameV1, req);
|
||||
|
||||
const { permission } = await getAuthDataProjectPermissions({
|
||||
authData: req.authData,
|
||||
workspaceId: new Types.ObjectId(workspaceId)
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Edit,
|
||||
ProjectPermissionSub.Workspace
|
||||
);
|
||||
|
||||
const workspace = await Workspace.findOneAndUpdate(
|
||||
{
|
||||
_id: workspaceId
|
||||
},
|
||||
{
|
||||
name
|
||||
},
|
||||
{
|
||||
new: true
|
||||
}
|
||||
);
|
||||
|
||||
return res.status(200).send({
|
||||
message: "Successfully changed workspace name",
|
||||
workspace
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Return integrations for workspace with id [workspaceId]
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
export const getWorkspaceIntegrations = async (req: Request, res: Response) => {
|
||||
const {
|
||||
params: { workspaceId }
|
||||
} = await validateRequest(reqValidator.GetWorkspaceIntegrationsV1, req);
|
||||
|
||||
const { permission } = await getAuthDataProjectPermissions({
|
||||
authData: req.authData,
|
||||
workspaceId: new Types.ObjectId(workspaceId)
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Read,
|
||||
ProjectPermissionSub.Integrations
|
||||
);
|
||||
|
||||
const integrations = await Integration.find({
|
||||
workspace: workspaceId
|
||||
});
|
||||
|
||||
return res.status(200).send({
|
||||
integrations
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Return (integration) authorizations for workspace with id [workspaceId]
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
export const getWorkspaceIntegrationAuthorizations = async (req: Request, res: Response) => {
|
||||
const {
|
||||
params: { workspaceId }
|
||||
} = await validateRequest(reqValidator.GetWorkspaceIntegrationAuthorizationsV1, req);
|
||||
|
||||
const { permission } = await getAuthDataProjectPermissions({
|
||||
authData: req.authData,
|
||||
workspaceId: new Types.ObjectId(workspaceId)
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Read,
|
||||
ProjectPermissionSub.Integrations
|
||||
);
|
||||
|
||||
const authorizations = await IntegrationAuth.find({
|
||||
workspace: workspaceId
|
||||
});
|
||||
|
||||
return res.status(200).send({
|
||||
authorizations
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Return service service tokens for workspace [workspaceId] belonging to user
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
export const getWorkspaceServiceTokens = async (req: Request, res: Response) => {
|
||||
const {
|
||||
params: { workspaceId }
|
||||
} = await validateRequest(reqValidator.GetWorkspaceServiceTokensV1, req);
|
||||
|
||||
const { permission } = await getAuthDataProjectPermissions({
|
||||
authData: req.authData,
|
||||
workspaceId: new Types.ObjectId(workspaceId)
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Read,
|
||||
ProjectPermissionSub.ServiceTokens
|
||||
);
|
||||
|
||||
// ?? FIX.
|
||||
const serviceTokens = await ServiceToken.find({
|
||||
user: req.user._id,
|
||||
workspace: workspaceId
|
||||
});
|
||||
|
||||
return res.status(200).send({
|
||||
serviceTokens
|
||||
});
|
||||
};
|
@ -1,315 +0,0 @@
|
||||
/* eslint-disable @typescript-eslint/no-var-requires */
|
||||
import { Request, Response } from "express";
|
||||
import jwt from "jsonwebtoken";
|
||||
import * as bigintConversion from "bigint-conversion";
|
||||
const jsrp = require("jsrp");
|
||||
import { LoginSRPDetail, User } from "../../models";
|
||||
import { createToken, issueAuthTokens } from "../../helpers/auth";
|
||||
import { checkUserDevice } from "../../helpers/user";
|
||||
import { sendMail } from "../../helpers/nodemailer";
|
||||
import { TokenService } from "../../services";
|
||||
import { BadRequestError, InternalServerError } from "../../utils/errors";
|
||||
import { AuthTokenType, TOKEN_EMAIL_MFA } from "../../variables";
|
||||
import { getAuthSecret, getHttpsEnabled, getJwtMfaLifetime } from "../../config";
|
||||
import { validateRequest } from "../../helpers/validation";
|
||||
import * as reqValidator from "../../validation/auth";
|
||||
|
||||
declare module "jsonwebtoken" {
|
||||
export interface UserIDJwtPayload extends jwt.JwtPayload {
|
||||
userId: string;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Log in user step 1: Return [salt] and [serverPublicKey] as part of step 1 of SRP protocol
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
export const login1 = async (req: Request, res: Response) => {
|
||||
const { email, clientPublicKey }: { email: string; clientPublicKey: string } = req.body;
|
||||
|
||||
const user = await User.findOne({
|
||||
email
|
||||
}).select("+salt +verifier");
|
||||
|
||||
if (!user) throw new Error("Failed to find user");
|
||||
|
||||
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(
|
||||
{ email: email },
|
||||
{
|
||||
email: email,
|
||||
clientPublicKey: clientPublicKey,
|
||||
serverBInt: bigintConversion.bigintToBuf(server.bInt)
|
||||
},
|
||||
{ upsert: true, returnNewDocument: false }
|
||||
);
|
||||
|
||||
return res.status(200).send({
|
||||
serverPublicKey,
|
||||
salt: user.salt
|
||||
});
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Log in user step 2: complete step 2 of SRP protocol and return token and their (encrypted)
|
||||
* private key
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
export const login2 = async (req: Request, res: Response) => {
|
||||
if (!req.headers["user-agent"])
|
||||
throw InternalServerError({ message: "User-Agent header is required" });
|
||||
|
||||
const { email, clientProof } = req.body;
|
||||
const user = await User.findOne({
|
||||
email
|
||||
}).select(
|
||||
"+salt +verifier +encryptionVersion +protectedKey +protectedKeyIV +protectedKeyTag +publicKey +encryptedPrivateKey +iv +tag +devices"
|
||||
);
|
||||
|
||||
if (!user) throw new Error("Failed to find user");
|
||||
|
||||
const loginSRPDetail = await LoginSRPDetail.findOneAndDelete({ email: email });
|
||||
|
||||
if (!loginSRPDetail) {
|
||||
return BadRequestError(Error("Failed to find login details for SRP"));
|
||||
}
|
||||
|
||||
const server = new jsrp.server();
|
||||
server.init(
|
||||
{
|
||||
salt: user.salt,
|
||||
verifier: user.verifier,
|
||||
b: loginSRPDetail.serverBInt
|
||||
},
|
||||
async () => {
|
||||
server.setClientPublicKey(loginSRPDetail.clientPublicKey);
|
||||
|
||||
// compare server and client shared keys
|
||||
if (server.checkClientProof(clientProof)) {
|
||||
if (user.isMfaEnabled) {
|
||||
// case: user has MFA enabled
|
||||
|
||||
// generate temporary MFA token
|
||||
const token = createToken({
|
||||
payload: {
|
||||
authTokenType: AuthTokenType.MFA_TOKEN,
|
||||
userId: user._id.toString()
|
||||
},
|
||||
expiresIn: await getJwtMfaLifetime(),
|
||||
secret: await getAuthSecret()
|
||||
});
|
||||
|
||||
const code = await TokenService.createToken({
|
||||
type: TOKEN_EMAIL_MFA,
|
||||
email
|
||||
});
|
||||
|
||||
// send MFA code [code] to [email]
|
||||
await sendMail({
|
||||
template: "emailMfa.handlebars",
|
||||
subjectLine: "Infisical MFA code",
|
||||
recipients: [email],
|
||||
substitutions: {
|
||||
code
|
||||
}
|
||||
});
|
||||
|
||||
return res.status(200).send({
|
||||
mfaEnabled: true,
|
||||
token
|
||||
});
|
||||
}
|
||||
|
||||
await checkUserDevice({
|
||||
user,
|
||||
ip: req.realIP,
|
||||
userAgent: req.headers["user-agent"] ?? ""
|
||||
});
|
||||
|
||||
// issue tokens
|
||||
const tokens = await issueAuthTokens({
|
||||
userId: user._id,
|
||||
ip: req.realIP,
|
||||
userAgent: req.headers["user-agent"] ?? ""
|
||||
});
|
||||
|
||||
// store (refresh) token in httpOnly cookie
|
||||
res.cookie("jid", tokens.refreshToken, {
|
||||
httpOnly: true,
|
||||
path: "/",
|
||||
sameSite: "strict",
|
||||
secure: await getHttpsEnabled()
|
||||
});
|
||||
|
||||
// case: user does not have MFA enabled
|
||||
// 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;
|
||||
}
|
||||
|
||||
return res.status(200).send(response);
|
||||
}
|
||||
|
||||
return res.status(400).send({
|
||||
message: "Failed to authenticate. Try again?"
|
||||
});
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Send MFA token to email [email]
|
||||
* @param req
|
||||
* @param res
|
||||
*/
|
||||
export const sendMfaToken = async (req: Request, res: Response) => {
|
||||
const code = await TokenService.createToken({
|
||||
type: TOKEN_EMAIL_MFA,
|
||||
email: req.user.email
|
||||
});
|
||||
|
||||
// send MFA code [code] to [email]
|
||||
await sendMail({
|
||||
template: "emailMfa.handlebars",
|
||||
subjectLine: "Infisical MFA code",
|
||||
recipients: [req.user.email],
|
||||
substitutions: {
|
||||
code
|
||||
}
|
||||
});
|
||||
|
||||
return res.status(200).send({
|
||||
message: "Successfully sent new MFA code"
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Verify MFA token [mfaToken] and issue JWT and refresh tokens if the
|
||||
* MFA token [mfaToken] is valid
|
||||
* @param req
|
||||
* @param res
|
||||
*/
|
||||
export const verifyMfaToken = async (req: Request, res: Response) => {
|
||||
const {
|
||||
body: { mfaToken }
|
||||
} = await validateRequest(reqValidator.VerifyMfaTokenV2, req);
|
||||
|
||||
await TokenService.validateToken({
|
||||
type: TOKEN_EMAIL_MFA,
|
||||
email: req.user.email,
|
||||
token: mfaToken
|
||||
});
|
||||
|
||||
const user = await User.findOne({
|
||||
email: req.user.email
|
||||
}).select(
|
||||
"+salt +verifier +encryptionVersion +protectedKey +protectedKeyIV +protectedKeyTag +publicKey +encryptedPrivateKey +iv +tag +devices"
|
||||
);
|
||||
|
||||
if (!user) throw new Error("Failed to find user");
|
||||
|
||||
await LoginSRPDetail.deleteOne({ userId: user.id });
|
||||
|
||||
await 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()
|
||||
});
|
||||
|
||||
interface VerifyMfaTokenRes {
|
||||
encryptionVersion: number;
|
||||
protectedKey?: string;
|
||||
protectedKeyIV?: string;
|
||||
protectedKeyTag?: string;
|
||||
token: string;
|
||||
publicKey: string;
|
||||
encryptedPrivateKey: string;
|
||||
iv: string;
|
||||
tag: string;
|
||||
}
|
||||
|
||||
interface VerifyMfaTokenRes {
|
||||
encryptionVersion: number;
|
||||
protectedKey?: string;
|
||||
protectedKeyIV?: string;
|
||||
protectedKeyTag?: string;
|
||||
token: string;
|
||||
publicKey: string;
|
||||
encryptedPrivateKey: string;
|
||||
iv: string;
|
||||
tag: string;
|
||||
}
|
||||
|
||||
const resObj: VerifyMfaTokenRes = {
|
||||
encryptionVersion: user.encryptionVersion,
|
||||
token: tokens.token,
|
||||
publicKey: user.publicKey as string,
|
||||
encryptedPrivateKey: user.encryptedPrivateKey as string,
|
||||
iv: user.iv as string,
|
||||
tag: user.tag as string
|
||||
};
|
||||
|
||||
if (user?.protectedKey && user?.protectedKeyIV && user?.protectedKeyTag) {
|
||||
resObj.protectedKey = user.protectedKey;
|
||||
resObj.protectedKeyIV = user.protectedKeyIV;
|
||||
resObj.protectedKeyTag = user.protectedKeyTag;
|
||||
}
|
||||
|
||||
return res.status(200).send(resObj);
|
||||
};
|
@ -1,604 +0,0 @@
|
||||
import { Request, Response } from "express";
|
||||
import { Types } from "mongoose";
|
||||
import {
|
||||
Folder,
|
||||
Integration,
|
||||
Membership,
|
||||
Secret,
|
||||
ServiceToken,
|
||||
ServiceTokenData,
|
||||
Workspace
|
||||
} from "../../models";
|
||||
import { EventType, SecretVersion } from "../../ee/models";
|
||||
import { EEAuditLogService, EELicenseService } from "../../ee/services";
|
||||
import { BadRequestError, WorkspaceNotFoundError } from "../../utils/errors";
|
||||
import { validateRequest } from "../../helpers/validation";
|
||||
import * as reqValidator from "../../validation/environments";
|
||||
import {
|
||||
ProjectPermissionActions,
|
||||
ProjectPermissionSub,
|
||||
getAuthDataProjectPermissions
|
||||
} from "../../ee/services/ProjectRoleService";
|
||||
import { ForbiddenError } from "@casl/ability";
|
||||
import { SecretImport } from "../../models";
|
||||
import { Webhook } from "../../models";
|
||||
|
||||
/**
|
||||
* Create new workspace environment named [environmentName]
|
||||
* with slug [environmentSlug] under workspace with id
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
export const createWorkspaceEnvironment = async (req: Request, res: Response) => {
|
||||
/*
|
||||
#swagger.summary = 'Create environment'
|
||||
#swagger.description = 'Create environment'
|
||||
|
||||
#swagger.security = [{
|
||||
"apiKeyAuth": [],
|
||||
}]
|
||||
|
||||
#swagger.parameters['workspaceId'] = {
|
||||
"description": "ID of workspace where to create environment",
|
||||
"required": true,
|
||||
"type": "string",
|
||||
"in": "path"
|
||||
}
|
||||
|
||||
#swagger.requestBody = {
|
||||
content: {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"environmentName": {
|
||||
"type": "string",
|
||||
"description": "Name of the environment to create",
|
||||
"example": "development"
|
||||
},
|
||||
"environmentSlug": {
|
||||
"type": "string",
|
||||
"description": "Slug of environment to create",
|
||||
"example": "dev-environment"
|
||||
}
|
||||
},
|
||||
"required": ["environmentName", "environmentSlug"]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#swagger.responses[200] = {
|
||||
content: {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"message": {
|
||||
"type": "string",
|
||||
"description": "Sucess message",
|
||||
"example": "Successfully created environment"
|
||||
},
|
||||
"workspace": {
|
||||
"type": "string",
|
||||
"description": "ID of workspace where environment was created",
|
||||
"example": "abc123"
|
||||
},
|
||||
"environment": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "Name of created environment",
|
||||
"example": "Staging"
|
||||
},
|
||||
"slug": {
|
||||
"type": "string",
|
||||
"description": "Slug of created environment",
|
||||
"example": "staging"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": "Details of the created environment"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
*/
|
||||
const {
|
||||
params: { workspaceId },
|
||||
body: { environmentName, environmentSlug }
|
||||
} = await validateRequest(reqValidator.CreateWorkspaceEnvironmentV2, req);
|
||||
|
||||
const { permission } = await getAuthDataProjectPermissions({
|
||||
authData: req.authData,
|
||||
workspaceId: new Types.ObjectId(workspaceId)
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Create,
|
||||
ProjectPermissionSub.Environments
|
||||
);
|
||||
|
||||
const workspace = await Workspace.findById(workspaceId).exec();
|
||||
|
||||
if (!workspace) throw WorkspaceNotFoundError();
|
||||
|
||||
const plan = await EELicenseService.getPlan(workspace.organization);
|
||||
|
||||
if (plan.environmentLimit !== null) {
|
||||
// case: limit imposed on number of environments allowed
|
||||
if (workspace.environments.length >= plan.environmentLimit) {
|
||||
// case: number of environments used exceeds the number of environments allowed
|
||||
|
||||
return res.status(400).send({
|
||||
message:
|
||||
"Failed to create environment due to environment limit reached. Upgrade plan to create more environments."
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
!workspace ||
|
||||
workspace?.environments.find(
|
||||
({ name, slug }) => slug === environmentSlug || environmentName === name
|
||||
)
|
||||
) {
|
||||
throw new Error("Failed to create workspace environment");
|
||||
}
|
||||
|
||||
workspace?.environments.push({
|
||||
name: environmentName,
|
||||
slug: environmentSlug.toLowerCase()
|
||||
});
|
||||
await workspace.save();
|
||||
|
||||
await EELicenseService.refreshPlan(workspace.organization, new Types.ObjectId(workspaceId));
|
||||
|
||||
await EEAuditLogService.createAuditLog(
|
||||
req.authData,
|
||||
{
|
||||
type: EventType.CREATE_ENVIRONMENT,
|
||||
metadata: {
|
||||
name: environmentName,
|
||||
slug: environmentSlug
|
||||
}
|
||||
},
|
||||
{
|
||||
workspaceId: workspace._id
|
||||
}
|
||||
);
|
||||
|
||||
return res.status(200).send({
|
||||
message: "Successfully created new environment",
|
||||
workspace: workspaceId,
|
||||
environment: {
|
||||
name: environmentName,
|
||||
slug: environmentSlug
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Swaps the ordering of two environments in the database. This is purely for aesthetic purposes.
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
export const reorderWorkspaceEnvironments = async (req: Request, res: Response) => {
|
||||
const {
|
||||
params: { workspaceId },
|
||||
body: { environmentName, environmentSlug, otherEnvironmentSlug, otherEnvironmentName }
|
||||
} = await validateRequest(reqValidator.ReorderWorkspaceEnvironmentsV2, req);
|
||||
|
||||
const { permission } = await getAuthDataProjectPermissions({
|
||||
authData: req.authData,
|
||||
workspaceId: new Types.ObjectId(workspaceId)
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Edit,
|
||||
ProjectPermissionSub.Environments
|
||||
);
|
||||
|
||||
// atomic update the env to avoid conflict
|
||||
const workspace = await Workspace.findById(workspaceId).exec();
|
||||
if (!workspace) {
|
||||
throw BadRequestError({ message: "Couldn't load workspace" });
|
||||
}
|
||||
|
||||
const environmentIndex = workspace.environments.findIndex(
|
||||
(env) => env.name === environmentName && env.slug === environmentSlug
|
||||
);
|
||||
const otherEnvironmentIndex = workspace.environments.findIndex(
|
||||
(env) => env.name === otherEnvironmentName && env.slug === otherEnvironmentSlug
|
||||
);
|
||||
|
||||
if (environmentIndex === -1 || otherEnvironmentIndex === -1) {
|
||||
throw BadRequestError({ message: "environment or otherEnvironment couldn't be found" });
|
||||
}
|
||||
|
||||
// swap the order of the environments
|
||||
[workspace.environments[environmentIndex], workspace.environments[otherEnvironmentIndex]] = [
|
||||
workspace.environments[otherEnvironmentIndex],
|
||||
workspace.environments[environmentIndex]
|
||||
];
|
||||
|
||||
await workspace.save();
|
||||
|
||||
return res.status(200).send({
|
||||
message: "Successfully reordered environments",
|
||||
workspace: workspaceId
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Rename workspace environment with new name and slug of a workspace with [workspaceId]
|
||||
* Old slug [oldEnvironmentSlug] must be provided
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
export const renameWorkspaceEnvironment = async (req: Request, res: Response) => {
|
||||
/*
|
||||
#swagger.summary = 'Update environment'
|
||||
#swagger.description = 'Update environment'
|
||||
|
||||
#swagger.security = [{
|
||||
"apiKeyAuth": [],
|
||||
}]
|
||||
|
||||
#swagger.parameters['workspaceId'] = {
|
||||
"description": "ID of workspace where to update environment",
|
||||
"required": true,
|
||||
"type": "string",
|
||||
"in": "path"
|
||||
}
|
||||
|
||||
#swagger.requestBody = {
|
||||
content: {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"environmentName": {
|
||||
"type": "string",
|
||||
"description": "Name of environment to update to",
|
||||
"example": "Staging-Renamed"
|
||||
},
|
||||
"environmentSlug": {
|
||||
"type": "string",
|
||||
"description": "Slug of environment to update to",
|
||||
"example": "staging-renamed"
|
||||
},
|
||||
"oldEnvironmentSlug": {
|
||||
"type": "string",
|
||||
"description": "Current slug of environment",
|
||||
"example": "staging-old"
|
||||
}
|
||||
},
|
||||
"required": ["environmentName", "environmentSlug", "oldEnvironmentSlug"]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#swagger.responses[200] = {
|
||||
content: {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"message": {
|
||||
"type": "string",
|
||||
"description": "Success message",
|
||||
"example": "Successfully update environment"
|
||||
},
|
||||
"workspace": {
|
||||
"type": "string",
|
||||
"description": "ID of workspace where environment was updated",
|
||||
"example": "abc123"
|
||||
},
|
||||
"environment": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "Name of updated environment",
|
||||
"example": "Staging-Renamed"
|
||||
},
|
||||
"slug": {
|
||||
"type": "string",
|
||||
"description": "Slug of updated environment",
|
||||
"example": "staging-renamed"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": "Details of the renamed environment"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
*/
|
||||
const {
|
||||
params: { workspaceId },
|
||||
body: { environmentName, environmentSlug, oldEnvironmentSlug }
|
||||
} = await validateRequest(reqValidator.UpdateWorkspaceEnvironmentV2, req);
|
||||
|
||||
const { permission } = await getAuthDataProjectPermissions({
|
||||
authData: req.authData,
|
||||
workspaceId: new Types.ObjectId(workspaceId)
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Edit,
|
||||
ProjectPermissionSub.Environments
|
||||
);
|
||||
|
||||
// user should pass both new slug and env name
|
||||
if (!environmentSlug || !environmentName) {
|
||||
throw new Error("Invalid environment given.");
|
||||
}
|
||||
|
||||
// atomic update the env to avoid conflict
|
||||
const workspace = await Workspace.findById(workspaceId).exec();
|
||||
if (!workspace) {
|
||||
throw new Error("Failed to create workspace environment");
|
||||
}
|
||||
|
||||
const isEnvExist = workspace.environments.some(
|
||||
({ name, slug }) =>
|
||||
slug !== oldEnvironmentSlug && (name === environmentName || slug === environmentSlug)
|
||||
);
|
||||
if (isEnvExist) {
|
||||
throw new Error("Invalid environment given");
|
||||
}
|
||||
|
||||
const envIndex = workspace?.environments.findIndex(({ slug }) => slug === oldEnvironmentSlug);
|
||||
if (envIndex === -1) {
|
||||
throw new Error("Invalid environment given");
|
||||
}
|
||||
|
||||
const oldEnvironment = workspace.environments[envIndex];
|
||||
|
||||
workspace.environments[envIndex].name = environmentName;
|
||||
workspace.environments[envIndex].slug = environmentSlug.toLowerCase();
|
||||
|
||||
await workspace.save();
|
||||
await Secret.updateMany(
|
||||
{ workspace: workspaceId, environment: oldEnvironmentSlug },
|
||||
{ environment: environmentSlug }
|
||||
);
|
||||
await SecretVersion.updateMany(
|
||||
{ workspace: workspaceId, environment: oldEnvironmentSlug },
|
||||
{ environment: environmentSlug }
|
||||
);
|
||||
await ServiceToken.updateMany(
|
||||
{ workspace: workspaceId, environment: oldEnvironmentSlug },
|
||||
{ environment: environmentSlug }
|
||||
);
|
||||
await ServiceTokenData.updateMany(
|
||||
{
|
||||
workspace: workspaceId,
|
||||
"scopes.environment": oldEnvironmentSlug
|
||||
},
|
||||
{ $set: { "scopes.$[element].environment": environmentSlug } },
|
||||
{ arrayFilters: [{ "element.environment": oldEnvironmentSlug }] }
|
||||
);
|
||||
await Integration.updateMany(
|
||||
{ workspace: workspaceId, environment: oldEnvironmentSlug },
|
||||
{ environment: environmentSlug }
|
||||
);
|
||||
|
||||
await Folder.updateMany(
|
||||
{ workspace: workspaceId, environment: oldEnvironmentSlug },
|
||||
{ environment: environmentSlug }
|
||||
);
|
||||
|
||||
await SecretImport.updateMany(
|
||||
{ workspace: workspaceId, environment: oldEnvironmentSlug },
|
||||
{ environment: environmentSlug }
|
||||
);
|
||||
await SecretImport.updateMany(
|
||||
{ workspace: workspaceId, "imports.environment": oldEnvironmentSlug },
|
||||
{ $set: { "imports.$[element].environment": environmentSlug } },
|
||||
{ arrayFilters: [{ "element.environment": oldEnvironmentSlug }] },
|
||||
);
|
||||
|
||||
await Webhook.updateMany(
|
||||
{ workspace: workspaceId, environment: oldEnvironmentSlug },
|
||||
{ environment: environmentSlug }
|
||||
);
|
||||
|
||||
await Membership.updateMany(
|
||||
{
|
||||
workspace: workspaceId,
|
||||
"deniedPermissions.environmentSlug": oldEnvironmentSlug
|
||||
},
|
||||
{ $set: { "deniedPermissions.$[element].environmentSlug": environmentSlug } },
|
||||
{ arrayFilters: [{ "element.environmentSlug": oldEnvironmentSlug }] }
|
||||
);
|
||||
|
||||
await EEAuditLogService.createAuditLog(
|
||||
req.authData,
|
||||
{
|
||||
type: EventType.UPDATE_ENVIRONMENT,
|
||||
metadata: {
|
||||
oldName: oldEnvironment.name,
|
||||
newName: environmentName,
|
||||
oldSlug: oldEnvironment.slug,
|
||||
newSlug: environmentSlug.toLowerCase()
|
||||
}
|
||||
},
|
||||
{
|
||||
workspaceId: workspace._id
|
||||
}
|
||||
);
|
||||
|
||||
return res.status(200).send({
|
||||
message: "Successfully update environment",
|
||||
workspace: workspaceId,
|
||||
environment: {
|
||||
name: environmentName,
|
||||
slug: environmentSlug
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Delete workspace environment by [environmentSlug] of workspace [workspaceId] and do the clean up
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
export const deleteWorkspaceEnvironment = async (req: Request, res: Response) => {
|
||||
/*
|
||||
#swagger.summary = 'Delete environment'
|
||||
#swagger.description = 'Delete environment'
|
||||
|
||||
#swagger.security = [{
|
||||
"apiKeyAuth": []
|
||||
}]
|
||||
|
||||
#swagger.parameters['workspaceId'] = {
|
||||
"description": "ID of workspace where to delete environment",
|
||||
"required": true,
|
||||
"type": "string",
|
||||
"in": "path"
|
||||
}
|
||||
|
||||
#swagger.requestBody = {
|
||||
content: {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"environmentSlug": {
|
||||
"type": "string",
|
||||
"description": "Slug of environment to delete",
|
||||
"example": "dev"
|
||||
}
|
||||
},
|
||||
"required": ["environmentSlug"]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#swagger.responses[200] = {
|
||||
content: {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"message": {
|
||||
"type": "string",
|
||||
"description": "Success message",
|
||||
"example": "Successfully deleted environment"
|
||||
},
|
||||
"workspace": {
|
||||
"type": "string",
|
||||
"description": "ID of workspace where environment was deleted",
|
||||
"example": "abc123"
|
||||
},
|
||||
"environment": {
|
||||
"type": "string",
|
||||
"description": "Slug of deleted environment",
|
||||
"example": "dev"
|
||||
}
|
||||
},
|
||||
"description": "Response after deleting an environment from a workspace"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
*/
|
||||
const {
|
||||
params: { workspaceId },
|
||||
body: { environmentSlug }
|
||||
} = await validateRequest(reqValidator.DeleteWorkspaceEnvironmentV2, req);
|
||||
|
||||
const { permission } = await getAuthDataProjectPermissions({
|
||||
authData: req.authData,
|
||||
workspaceId: new Types.ObjectId(workspaceId)
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Delete,
|
||||
ProjectPermissionSub.Environments
|
||||
);
|
||||
|
||||
// atomic update the env to avoid conflict
|
||||
const workspace = await Workspace.findById(workspaceId).exec();
|
||||
if (!workspace) {
|
||||
throw new Error("Failed to create workspace environment");
|
||||
}
|
||||
|
||||
const envIndex = workspace?.environments.findIndex(({ slug }) => slug === environmentSlug);
|
||||
if (envIndex === -1) {
|
||||
throw new Error("Invalid environment given");
|
||||
}
|
||||
|
||||
const oldEnvironment = workspace.environments[envIndex];
|
||||
|
||||
workspace.environments.splice(envIndex, 1);
|
||||
await workspace.save();
|
||||
|
||||
// clean up
|
||||
await Secret.deleteMany({
|
||||
workspace: workspaceId,
|
||||
environment: environmentSlug
|
||||
});
|
||||
await SecretVersion.deleteMany({
|
||||
workspace: workspaceId,
|
||||
environment: environmentSlug
|
||||
});
|
||||
|
||||
// await ServiceToken.deleteMany({
|
||||
// workspace: workspaceId,
|
||||
// environment: environmentSlug,
|
||||
// });
|
||||
|
||||
const result = await ServiceTokenData.updateMany(
|
||||
{ workspace: workspaceId },
|
||||
{ $pull: { scopes: { environment: environmentSlug } } }
|
||||
);
|
||||
|
||||
if (result.modifiedCount > 0) {
|
||||
await ServiceTokenData.deleteMany({ workspace: workspaceId, scopes: { $size: 0 } });
|
||||
}
|
||||
|
||||
await Integration.deleteMany({
|
||||
workspace: workspaceId,
|
||||
environment: environmentSlug
|
||||
});
|
||||
await Membership.updateMany(
|
||||
{ workspace: workspaceId },
|
||||
{ $pull: { deniedPermissions: { environmentSlug: environmentSlug } } }
|
||||
);
|
||||
|
||||
await EELicenseService.refreshPlan(workspace.organization, new Types.ObjectId(workspaceId));
|
||||
|
||||
await EEAuditLogService.createAuditLog(
|
||||
req.authData,
|
||||
{
|
||||
type: EventType.DELETE_ENVIRONMENT,
|
||||
metadata: {
|
||||
name: oldEnvironment.name,
|
||||
slug: oldEnvironment.slug
|
||||
}
|
||||
},
|
||||
{
|
||||
workspaceId: workspace._id
|
||||
}
|
||||
);
|
||||
|
||||
return res.status(200).send({
|
||||
message: "Successfully deleted environment",
|
||||
workspace: workspaceId,
|
||||
environment: environmentSlug
|
||||
});
|
||||
};
|
@ -1,25 +0,0 @@
|
||||
import * as authController from "./authController";
|
||||
import * as signupController from "./signupController";
|
||||
import * as usersController from "./usersController";
|
||||
import * as organizationsController from "./organizationsController";
|
||||
import * as workspaceController from "./workspaceController";
|
||||
import * as serviceTokenDataController from "./serviceTokenDataController";
|
||||
import * as secretController from "./secretController";
|
||||
import * as secretsController from "./secretsController";
|
||||
import * as environmentController from "./environmentController";
|
||||
import * as tagController from "./tagController";
|
||||
import * as membershipController from "./membershipController";
|
||||
|
||||
export {
|
||||
authController,
|
||||
signupController,
|
||||
usersController,
|
||||
organizationsController,
|
||||
workspaceController,
|
||||
serviceTokenDataController,
|
||||
secretController,
|
||||
secretsController,
|
||||
environmentController,
|
||||
tagController,
|
||||
membershipController
|
||||
};
|
@ -1,107 +0,0 @@
|
||||
import { ForbiddenError } from "@casl/ability";
|
||||
import { Request, Response } from "express";
|
||||
import { Types } from "mongoose";
|
||||
|
||||
import { getSiteURL } from "../../config";
|
||||
import { EventType } from "../../ee/models";
|
||||
import { EEAuditLogService } from "../../ee/services";
|
||||
import {
|
||||
ProjectPermissionActions,
|
||||
ProjectPermissionSub,
|
||||
getAuthDataProjectPermissions
|
||||
} from "../../ee/services/ProjectRoleService";
|
||||
import { sendMail } from "../../helpers";
|
||||
import { validateRequest } from "../../helpers/validation";
|
||||
import { IUser, Key, Membership, MembershipOrg, Workspace } from "../../models";
|
||||
import { BadRequestError } from "../../utils/errors";
|
||||
import * as reqValidator from "../../validation/membership";
|
||||
import { ACCEPTED, MEMBER } from "../../variables";
|
||||
|
||||
export const addUserToWorkspace = async (req: Request, res: Response) => {
|
||||
const {
|
||||
params: { workspaceId },
|
||||
body: { members }
|
||||
} = await validateRequest(reqValidator.AddUserToWorkspaceV2, req);
|
||||
// check workspace
|
||||
const workspace = await Workspace.findById(workspaceId);
|
||||
if (!workspace) throw new Error("Failed to find workspace");
|
||||
|
||||
// check permission
|
||||
const { permission } = await getAuthDataProjectPermissions({
|
||||
authData: req.authData,
|
||||
workspaceId: new Types.ObjectId(workspaceId)
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Create,
|
||||
ProjectPermissionSub.Member
|
||||
);
|
||||
|
||||
// validate members are part of the organization
|
||||
const orgMembers = await MembershipOrg.find({
|
||||
status: ACCEPTED,
|
||||
_id: { $in: members.map(({ orgMembershipId }) => orgMembershipId) },
|
||||
organization: workspace.organization
|
||||
})
|
||||
.populate<{ user: IUser }>("user")
|
||||
.select({ _id: 1, user: 1 })
|
||||
.lean();
|
||||
if (orgMembers.length !== members.length)
|
||||
throw BadRequestError({ message: "Org member not found" });
|
||||
|
||||
const existingMember = await Membership.find({
|
||||
workspace: workspaceId,
|
||||
user: { $in: orgMembers.map(({ user }) => user) }
|
||||
});
|
||||
if (existingMember?.length)
|
||||
throw BadRequestError({ message: "Some users are already part of workspace" });
|
||||
|
||||
await Membership.insertMany(
|
||||
orgMembers.map(({ user }) => ({ user: user._id, workspace: workspaceId, role: MEMBER }))
|
||||
);
|
||||
|
||||
const encKeyGroupedByOrgMemberId = members.reduce<Record<string, (typeof members)[number]>>(
|
||||
(prev, curr) => ({ ...prev, [curr.orgMembershipId]: curr }),
|
||||
{}
|
||||
);
|
||||
await Key.insertMany(
|
||||
orgMembers.map(({ user, _id: id }) => ({
|
||||
encryptedKey: encKeyGroupedByOrgMemberId[id.toString()].workspaceEncryptedKey,
|
||||
nonce: encKeyGroupedByOrgMemberId[id.toString()].workspaceEncryptedNonce,
|
||||
sender: req.user._id,
|
||||
receiver: user._id,
|
||||
workspace: workspaceId
|
||||
}))
|
||||
);
|
||||
|
||||
await sendMail({
|
||||
template: "workspaceInvitation.handlebars",
|
||||
subjectLine: "Infisical workspace invitation",
|
||||
recipients: orgMembers.map(({ user }) => user.email),
|
||||
substitutions: {
|
||||
inviterFirstName: req.user.firstName,
|
||||
inviterEmail: req.user.email,
|
||||
workspaceName: workspace.name,
|
||||
callback_url: (await getSiteURL()) + "/login"
|
||||
}
|
||||
});
|
||||
|
||||
await EEAuditLogService.createAuditLog(
|
||||
req.authData,
|
||||
{
|
||||
type: EventType.ADD_BATCH_WORKSPACE_MEMBER,
|
||||
metadata: orgMembers.map(({ user }) => ({
|
||||
userId: user._id.toString(),
|
||||
email: user.email
|
||||
}))
|
||||
},
|
||||
{
|
||||
workspaceId: new Types.ObjectId(workspaceId)
|
||||
}
|
||||
);
|
||||
|
||||
return res.status(200).send({
|
||||
success: true,
|
||||
data: orgMembers
|
||||
});
|
||||
};
|
@ -1,505 +0,0 @@
|
||||
import { Request, Response } from "express";
|
||||
import { Types } from "mongoose";
|
||||
import {
|
||||
IWorkspace,
|
||||
Identity,
|
||||
IdentityMembership,
|
||||
IdentityMembershipOrg,
|
||||
Membership,
|
||||
MembershipOrg,
|
||||
User,
|
||||
Workspace
|
||||
} from "../../models";
|
||||
import { Role } from "../../ee/models";
|
||||
import { deleteMembershipOrg } from "../../helpers/membershipOrg";
|
||||
import {
|
||||
createOrganization as create,
|
||||
deleteOrganization,
|
||||
updateSubscriptionOrgQuantity
|
||||
} from "../../helpers/organization";
|
||||
import { addMembershipsOrg } from "../../helpers/membershipOrg";
|
||||
import { BadRequestError, ResourceNotFoundError, UnauthorizedRequestError } from "../../utils/errors";
|
||||
import { ACCEPTED, ADMIN, CUSTOM, MEMBER, NO_ACCESS } from "../../variables";
|
||||
import * as reqValidator from "../../validation/organization";
|
||||
import { validateRequest } from "../../helpers/validation";
|
||||
import {
|
||||
OrgPermissionActions,
|
||||
OrgPermissionSubjects,
|
||||
getAuthDataOrgPermissions
|
||||
} from "../../ee/services/RoleService";
|
||||
import { EELicenseService } from "../../ee/services";
|
||||
import { ForbiddenError } from "@casl/ability";
|
||||
|
||||
/**
|
||||
* Return memberships for organization with id [organizationId]
|
||||
* @param req
|
||||
* @param res
|
||||
*/
|
||||
export const getOrganizationMemberships = async (req: Request, res: Response) => {
|
||||
/*
|
||||
#swagger.summary = 'Return organization user memberships'
|
||||
#swagger.description = 'Return organization user memberships'
|
||||
|
||||
#swagger.security = [{
|
||||
"apiKeyAuth": [],
|
||||
"bearerAuth": []
|
||||
}]
|
||||
|
||||
#swagger.parameters['organizationId'] = {
|
||||
"description": "ID of organization",
|
||||
"required": true,
|
||||
"type": "string"
|
||||
}
|
||||
|
||||
#swagger.responses[200] = {
|
||||
content: {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"memberships": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
$ref: "#/components/schemas/MembershipOrg"
|
||||
},
|
||||
"description": "Memberships of organization"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
*/
|
||||
const {
|
||||
params: { organizationId }
|
||||
} = await validateRequest(reqValidator.GetOrgMembersv2, req);
|
||||
|
||||
const { permission } = await getAuthDataOrgPermissions({
|
||||
authData: req.authData,
|
||||
organizationId: new Types.ObjectId(organizationId)
|
||||
});
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
OrgPermissionActions.Read,
|
||||
OrgPermissionSubjects.Member
|
||||
);
|
||||
|
||||
const memberships = await MembershipOrg.find({
|
||||
organization: organizationId
|
||||
}).populate("user", "+publicKey");
|
||||
|
||||
return res.status(200).send({
|
||||
memberships
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Update role of membership with id [membershipId] to role [role]
|
||||
* @param req
|
||||
* @param res
|
||||
*/
|
||||
export const updateOrganizationMembership = async (req: Request, res: Response) => {
|
||||
/*
|
||||
#swagger.summary = 'Update organization user membership'
|
||||
#swagger.description = 'Update organization user membership'
|
||||
|
||||
#swagger.security = [{
|
||||
"apiKeyAuth": [],
|
||||
"bearerAuth": []
|
||||
}]
|
||||
|
||||
#swagger.parameters['organizationId'] = {
|
||||
"description": "ID of organization",
|
||||
"required": true,
|
||||
"type": "string"
|
||||
}
|
||||
|
||||
#swagger.parameters['membershipId'] = {
|
||||
"description": "ID of organization membership to update",
|
||||
"required": true,
|
||||
"type": "string"
|
||||
}
|
||||
|
||||
#swagger.requestBody = {
|
||||
"required": true,
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"role": {
|
||||
"type": "string",
|
||||
"description": "Role of organization membership - either owner, admin, or member",
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#swagger.responses[200] = {
|
||||
content: {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"membership": {
|
||||
$ref: "#/components/schemas/MembershipOrg",
|
||||
"description": "Updated organization membership"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
*/
|
||||
const {
|
||||
params: { organizationId, membershipId },
|
||||
body: { role }
|
||||
} = await validateRequest(reqValidator.UpdateOrgMemberv2, req);
|
||||
|
||||
const { permission } = await getAuthDataOrgPermissions({
|
||||
authData: req.authData,
|
||||
organizationId: new Types.ObjectId(organizationId)
|
||||
});
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
OrgPermissionActions.Edit,
|
||||
OrgPermissionSubjects.Member
|
||||
);
|
||||
|
||||
const isCustomRole = ![ADMIN, MEMBER, NO_ACCESS].includes(role);
|
||||
if (isCustomRole) {
|
||||
const orgRole = await Role.findOne({
|
||||
slug: role,
|
||||
isOrgRole: true,
|
||||
organization: new Types.ObjectId(organizationId)
|
||||
});
|
||||
|
||||
if (!orgRole) throw BadRequestError({ message: "Role not found" });
|
||||
|
||||
const plan = await EELicenseService.getPlan(new Types.ObjectId(organizationId));
|
||||
|
||||
if (!plan.rbac) return res.status(400).send({
|
||||
message:
|
||||
"Failed to assign custom role due to RBAC restriction. Upgrade plan to assign custom role to member."
|
||||
});
|
||||
|
||||
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]
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
export const deleteOrganizationMembership = async (req: Request, res: Response) => {
|
||||
/*
|
||||
#swagger.summary = 'Delete organization user membership'
|
||||
#swagger.description = 'Delete organization user membership'
|
||||
|
||||
#swagger.security = [{
|
||||
"apiKeyAuth": [],
|
||||
"bearerAuth": []
|
||||
}]
|
||||
|
||||
#swagger.parameters['organizationId'] = {
|
||||
"description": "ID of organization",
|
||||
"required": true,
|
||||
"type": "string"
|
||||
}
|
||||
|
||||
#swagger.parameters['membershipId'] = {
|
||||
"description": "ID of organization membership to delete",
|
||||
"required": true,
|
||||
"type": "string"
|
||||
}
|
||||
|
||||
#swagger.responses[200] = {
|
||||
content: {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"membership": {
|
||||
$ref: "#/components/schemas/MembershipOrg",
|
||||
"description": "Deleted organization membership"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
*/
|
||||
const {
|
||||
params: { organizationId, membershipId }
|
||||
} = await validateRequest(reqValidator.DeleteOrgMemberv2, req);
|
||||
|
||||
const membershipOrg = await MembershipOrg.findOne({
|
||||
_id: new Types.ObjectId(membershipId),
|
||||
organization: new Types.ObjectId(organizationId)
|
||||
});
|
||||
|
||||
if (!membershipOrg) throw ResourceNotFoundError();
|
||||
|
||||
const { permission } = await getAuthDataOrgPermissions({
|
||||
authData: req.authData,
|
||||
organizationId: membershipOrg.organization
|
||||
});
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
OrgPermissionActions.Delete,
|
||||
OrgPermissionSubjects.Member
|
||||
);
|
||||
|
||||
// delete organization membership
|
||||
const membership = await deleteMembershipOrg({
|
||||
membershipOrgId: membershipId
|
||||
});
|
||||
|
||||
await updateSubscriptionOrgQuantity({
|
||||
organizationId: membership.organization.toString()
|
||||
});
|
||||
|
||||
return res.status(200).send({
|
||||
membership
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Return workspaces for organization with id [organizationId] that user has
|
||||
* access to
|
||||
* @param req
|
||||
* @param res
|
||||
*/
|
||||
export const getOrganizationWorkspaces = async (req: Request, res: Response) => {
|
||||
/*
|
||||
#swagger.summary = 'Return projects in organization that user is part of'
|
||||
#swagger.description = 'Return projects in organization that user is part of'
|
||||
|
||||
#swagger.security = [{
|
||||
"apiKeyAuth": [],
|
||||
"bearerAuth": []
|
||||
}]
|
||||
|
||||
#swagger.parameters['organizationId'] = {
|
||||
"description": "ID of organization",
|
||||
"required": true,
|
||||
"type": "string"
|
||||
}
|
||||
|
||||
#swagger.responses[200] = {
|
||||
content: {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"workspaces": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
$ref: "#/components/schemas/Project"
|
||||
},
|
||||
"description": "Projects of organization"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
const {
|
||||
params: { organizationId }
|
||||
} = await validateRequest(reqValidator.GetOrgWorkspacesv2, req);
|
||||
|
||||
const { permission } = await getAuthDataOrgPermissions({
|
||||
authData: req.authData,
|
||||
organizationId: new Types.ObjectId(organizationId)
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
OrgPermissionActions.Read,
|
||||
OrgPermissionSubjects.Workspace
|
||||
);
|
||||
|
||||
const workspacesSet = new Set(
|
||||
(
|
||||
await Workspace.find(
|
||||
{
|
||||
organization: organizationId
|
||||
},
|
||||
"_id"
|
||||
)
|
||||
).map((w) => w._id.toString())
|
||||
);
|
||||
|
||||
let workspaces: IWorkspace[] = [];
|
||||
|
||||
if (req.authData.authPayload instanceof Identity) {
|
||||
workspaces = (
|
||||
await IdentityMembership.find({
|
||||
identity: req.authData.authPayload._id
|
||||
}).populate<{ workspace: IWorkspace }>("workspace")
|
||||
)
|
||||
.filter((m) => workspacesSet.has(m.workspace._id.toString()))
|
||||
.map((m) => m.workspace);
|
||||
}
|
||||
|
||||
if (req.authData.authPayload instanceof User) {
|
||||
workspaces = (
|
||||
await Membership.find({
|
||||
user: req.authData.authPayload._id
|
||||
}).populate<{ workspace: IWorkspace }>("workspace")
|
||||
)
|
||||
.filter((m) => workspacesSet.has(m.workspace._id.toString()))
|
||||
.map((m) => m.workspace);
|
||||
}
|
||||
|
||||
return res.status(200).send({
|
||||
workspaces
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Create new organization named [organizationName]
|
||||
* and add user as owner
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
export const createOrganization = async (req: Request, res: Response) => {
|
||||
const {
|
||||
body: { name }
|
||||
} = await validateRequest(reqValidator.CreateOrgv2, req);
|
||||
|
||||
// create organization and add user as member
|
||||
const organization = await create({
|
||||
email: req.user.email,
|
||||
name
|
||||
});
|
||||
|
||||
await addMembershipsOrg({
|
||||
userIds: [req.user._id.toString()],
|
||||
organizationId: organization._id.toString(),
|
||||
roles: [ADMIN],
|
||||
statuses: [ACCEPTED]
|
||||
});
|
||||
|
||||
return res.status(200).send({
|
||||
organization
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Delete organization with id [organizationId]
|
||||
* @param req
|
||||
* @param res
|
||||
*/
|
||||
export const deleteOrganizationById = async (req: Request, res: Response) => {
|
||||
const {
|
||||
params: { organizationId }
|
||||
} = await validateRequest(reqValidator.DeleteOrgv2, req);
|
||||
|
||||
const membershipOrg = await MembershipOrg.findOne({
|
||||
user: req.user._id,
|
||||
organization: new Types.ObjectId(organizationId),
|
||||
role: ADMIN
|
||||
});
|
||||
|
||||
if (!membershipOrg) throw UnauthorizedRequestError();
|
||||
|
||||
const organization = await deleteOrganization({
|
||||
organizationId: new Types.ObjectId(organizationId)
|
||||
});
|
||||
|
||||
return res.status(200).send({
|
||||
organization
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Return list of identity memberships for organization with id [organizationId]
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
export const getOrganizationIdentityMemberships = async (req: Request, res: Response) => {
|
||||
/*
|
||||
#swagger.summary = 'Return organization identity memberships'
|
||||
#swagger.description = 'Return organization identity memberships'
|
||||
|
||||
#swagger.security = [{
|
||||
"bearerAuth": []
|
||||
}]
|
||||
|
||||
#swagger.parameters['organizationId'] = {
|
||||
"description": "ID of organization",
|
||||
"required": true,
|
||||
"type": "string",
|
||||
"in": "path"
|
||||
}
|
||||
|
||||
#swagger.responses[200] = {
|
||||
content: {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"identityMemberships": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
$ref: "#/components/schemas/IdentityMembershipOrg"
|
||||
},
|
||||
"description": "Identity memberships of organization"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
*/
|
||||
const {
|
||||
params: { organizationId }
|
||||
} = await validateRequest(reqValidator.GetOrgIdentityMembershipsV2, req);
|
||||
|
||||
const { permission } = await getAuthDataOrgPermissions({
|
||||
authData: req.authData,
|
||||
organizationId: new Types.ObjectId(organizationId)
|
||||
});
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
OrgPermissionActions.Read,
|
||||
OrgPermissionSubjects.Identity
|
||||
);
|
||||
|
||||
const identityMemberships = await IdentityMembershipOrg.find({
|
||||
organization: new Types.ObjectId(organizationId)
|
||||
}).populate("identity customRole");
|
||||
|
||||
return res.status(200).send({
|
||||
identityMemberships
|
||||
});
|
||||
}
|
@ -1,419 +0,0 @@
|
||||
import { Request, Response } from "express";
|
||||
import mongoose, { Types } from "mongoose";
|
||||
import {
|
||||
CreateSecretRequestBody,
|
||||
ModifySecretRequestBody,
|
||||
SanitizedSecretForCreate,
|
||||
SanitizedSecretModify
|
||||
} from "../../types/secret";
|
||||
const { ValidationError } = mongoose.Error;
|
||||
import {
|
||||
ValidationError as RouteValidationError,
|
||||
UnauthorizedRequestError
|
||||
} from "../../utils/errors";
|
||||
import {
|
||||
ALGORITHM_AES_256_GCM,
|
||||
ENCODING_SCHEME_UTF8,
|
||||
SECRET_PERSONAL,
|
||||
SECRET_SHARED
|
||||
} from "../../variables";
|
||||
import { TelemetryService } from "../../services";
|
||||
import { Secret, User } from "../../models";
|
||||
import { AccountNotFoundError } from "../../utils/errors";
|
||||
|
||||
/**
|
||||
* Create secret for workspace with id [workspaceId] and environment [environment]
|
||||
* @param req
|
||||
* @param res
|
||||
*/
|
||||
export const createSecret = async (req: Request, res: Response) => {
|
||||
const postHogClient = await TelemetryService.getPostHogClient();
|
||||
const secretToCreate: CreateSecretRequestBody = req.body.secret;
|
||||
const { workspaceId, environment } = req.params;
|
||||
const sanitizedSecret: SanitizedSecretForCreate = {
|
||||
secretKeyCiphertext: secretToCreate.secretKeyCiphertext,
|
||||
secretKeyIV: secretToCreate.secretKeyIV,
|
||||
secretKeyTag: secretToCreate.secretKeyTag,
|
||||
secretKeyHash: secretToCreate.secretKeyHash,
|
||||
secretValueCiphertext: secretToCreate.secretValueCiphertext,
|
||||
secretValueIV: secretToCreate.secretValueIV,
|
||||
secretValueTag: secretToCreate.secretValueTag,
|
||||
secretValueHash: secretToCreate.secretValueHash,
|
||||
secretCommentCiphertext: secretToCreate.secretCommentCiphertext,
|
||||
secretCommentIV: secretToCreate.secretCommentIV,
|
||||
secretCommentTag: secretToCreate.secretCommentTag,
|
||||
secretCommentHash: secretToCreate.secretCommentHash,
|
||||
workspace: new Types.ObjectId(workspaceId),
|
||||
environment,
|
||||
type: secretToCreate.type,
|
||||
user: new Types.ObjectId(req.user._id),
|
||||
algorithm: ALGORITHM_AES_256_GCM,
|
||||
keyEncoding: ENCODING_SCHEME_UTF8
|
||||
};
|
||||
|
||||
const secret = await new Secret(sanitizedSecret).save();
|
||||
|
||||
if (postHogClient) {
|
||||
postHogClient.capture({
|
||||
event: "secrets added",
|
||||
distinctId: req.user.email,
|
||||
properties: {
|
||||
numberOfSecrets: 1,
|
||||
workspaceId,
|
||||
environment,
|
||||
channel: req.headers?.["user-agent"]?.toLowerCase().includes("mozilla") ? "web" : "cli",
|
||||
userAgent: req.headers?.["user-agent"]
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
res.status(200).send({
|
||||
secret
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Create many secrets for workspace with id [workspaceId] and environment [environment]
|
||||
* @param req
|
||||
* @param res
|
||||
*/
|
||||
export const createSecrets = async (req: Request, res: Response) => {
|
||||
const postHogClient = await TelemetryService.getPostHogClient();
|
||||
const secretsToCreate: CreateSecretRequestBody[] = req.body.secrets;
|
||||
const { workspaceId, environment } = req.params;
|
||||
const sanitizedSecretesToCreate: SanitizedSecretForCreate[] = [];
|
||||
|
||||
secretsToCreate.forEach((rawSecret) => {
|
||||
const safeUpdateFields: SanitizedSecretForCreate = {
|
||||
secretKeyCiphertext: rawSecret.secretKeyCiphertext,
|
||||
secretKeyIV: rawSecret.secretKeyIV,
|
||||
secretKeyTag: rawSecret.secretKeyTag,
|
||||
secretKeyHash: rawSecret.secretKeyHash,
|
||||
secretValueCiphertext: rawSecret.secretValueCiphertext,
|
||||
secretValueIV: rawSecret.secretValueIV,
|
||||
secretValueTag: rawSecret.secretValueTag,
|
||||
secretValueHash: rawSecret.secretValueHash,
|
||||
secretCommentCiphertext: rawSecret.secretCommentCiphertext,
|
||||
secretCommentIV: rawSecret.secretCommentIV,
|
||||
secretCommentTag: rawSecret.secretCommentTag,
|
||||
secretCommentHash: rawSecret.secretCommentHash,
|
||||
workspace: new Types.ObjectId(workspaceId),
|
||||
environment,
|
||||
type: rawSecret.type,
|
||||
user: new Types.ObjectId(req.user._id),
|
||||
algorithm: ALGORITHM_AES_256_GCM,
|
||||
keyEncoding: ENCODING_SCHEME_UTF8
|
||||
};
|
||||
|
||||
sanitizedSecretesToCreate.push(safeUpdateFields);
|
||||
});
|
||||
|
||||
const secrets = await Secret.insertMany(sanitizedSecretesToCreate);
|
||||
|
||||
if (postHogClient) {
|
||||
postHogClient.capture({
|
||||
event: "secrets added",
|
||||
distinctId: req.user.email,
|
||||
properties: {
|
||||
numberOfSecrets: (secretsToCreate ?? []).length,
|
||||
workspaceId,
|
||||
environment,
|
||||
channel: req.headers?.["user-agent"]?.toLowerCase().includes("mozilla") ? "web" : "cli",
|
||||
userAgent: req.headers?.["user-agent"]
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
res.status(200).send({
|
||||
secrets
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Delete secrets in workspace with id [workspaceId] and environment [environment]
|
||||
* @param req
|
||||
* @param res
|
||||
*/
|
||||
export const deleteSecrets = async (req: Request, res: Response) => {
|
||||
const postHogClient = await TelemetryService.getPostHogClient();
|
||||
const { workspaceId, environmentName } = req.params;
|
||||
const secretIdsToDelete: string[] = req.body.secretIds;
|
||||
|
||||
const secretIdsUserCanDelete = await Secret.find({ workspace: workspaceId, environment: environmentName }, { _id: 1 });
|
||||
|
||||
const secretsUserCanDeleteSet: Set<string> = new Set(
|
||||
secretIdsUserCanDelete.map((objectId) => objectId._id.toString())
|
||||
);
|
||||
|
||||
// Filter out IDs that user can delete and then map them to delete operations
|
||||
const deleteOperationsToPerform = secretIdsToDelete
|
||||
.filter(secretIdToDelete => {
|
||||
if (!secretsUserCanDeleteSet.has(secretIdToDelete)) {
|
||||
throw RouteValidationError({
|
||||
message: "You cannot delete secrets that you do not have access to"
|
||||
});
|
||||
}
|
||||
return true;
|
||||
})
|
||||
.map(secretIdToDelete => ({
|
||||
deleteOne: { filter: { _id: new Types.ObjectId(secretIdToDelete) } }
|
||||
}));
|
||||
|
||||
const numSecretsDeleted = deleteOperationsToPerform.length;
|
||||
|
||||
await Secret.bulkWrite(deleteOperationsToPerform);
|
||||
|
||||
if (postHogClient) {
|
||||
postHogClient.capture({
|
||||
event: "secrets deleted",
|
||||
distinctId: req.user.email,
|
||||
properties: {
|
||||
numberOfSecrets: numSecretsDeleted,
|
||||
environment: environmentName,
|
||||
workspaceId,
|
||||
channel: req.headers?.["user-agent"]?.toLowerCase().includes("mozilla") ? "web" : "cli",
|
||||
userAgent: req.headers?.["user-agent"]
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
res.status(200).send();
|
||||
};
|
||||
|
||||
/**
|
||||
* Delete secret with id [secretId]
|
||||
* @param req
|
||||
* @param res
|
||||
*/
|
||||
export const deleteSecret = async (req: Request, res: Response) => {
|
||||
const postHogClient = await TelemetryService.getPostHogClient();
|
||||
await Secret.findByIdAndDelete(req._secret._id);
|
||||
|
||||
if (postHogClient) {
|
||||
postHogClient.capture({
|
||||
event: "secrets deleted",
|
||||
distinctId: req.user.email,
|
||||
properties: {
|
||||
numberOfSecrets: 1,
|
||||
workspaceId: req._secret.workspace.toString(),
|
||||
environment: req._secret.environment,
|
||||
channel: req.headers?.["user-agent"]?.toLowerCase().includes("mozilla") ? "web" : "cli",
|
||||
userAgent: req.headers?.["user-agent"]
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
res.status(200).send({
|
||||
secret: req._secret
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Update secrets for workspace with id [workspaceId] and environment [environment]
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
export const updateSecrets = async (req: Request, res: Response) => {
|
||||
const postHogClient = await TelemetryService.getPostHogClient();
|
||||
const { workspaceId, environmentName } = req.params;
|
||||
const secretsModificationsRequested: ModifySecretRequestBody[] = req.body.secrets;
|
||||
const secretIdsUserCanModify = await Secret.find({ workspace: workspaceId, environment: environmentName }, { _id: 1 });
|
||||
|
||||
const secretsUserCanModifySet: Set<string> = new Set(
|
||||
secretIdsUserCanModify.map((objectId) => objectId._id.toString())
|
||||
);
|
||||
const updateOperationsToPerform: any = [];
|
||||
|
||||
secretsModificationsRequested.forEach((userModifiedSecret) => {
|
||||
if (secretsUserCanModifySet.has(userModifiedSecret._id.toString())) {
|
||||
const sanitizedSecret: SanitizedSecretModify = {
|
||||
secretKeyCiphertext: userModifiedSecret.secretKeyCiphertext,
|
||||
secretKeyIV: userModifiedSecret.secretKeyIV,
|
||||
secretKeyTag: userModifiedSecret.secretKeyTag,
|
||||
secretKeyHash: userModifiedSecret.secretKeyHash,
|
||||
secretValueCiphertext: userModifiedSecret.secretValueCiphertext,
|
||||
secretValueIV: userModifiedSecret.secretValueIV,
|
||||
secretValueTag: userModifiedSecret.secretValueTag,
|
||||
secretValueHash: userModifiedSecret.secretValueHash,
|
||||
secretCommentCiphertext: userModifiedSecret.secretCommentCiphertext,
|
||||
secretCommentIV: userModifiedSecret.secretCommentIV,
|
||||
secretCommentTag: userModifiedSecret.secretCommentTag,
|
||||
secretCommentHash: userModifiedSecret.secretCommentHash
|
||||
};
|
||||
|
||||
const updateOperation = {
|
||||
updateOne: {
|
||||
filter: { _id: userModifiedSecret._id, workspace: workspaceId },
|
||||
update: { $inc: { version: 1 }, $set: sanitizedSecret }
|
||||
}
|
||||
};
|
||||
updateOperationsToPerform.push(updateOperation);
|
||||
} else {
|
||||
throw UnauthorizedRequestError({
|
||||
message: "You do not have permission to modify one or more of the requested secrets"
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
await Secret.bulkWrite(updateOperationsToPerform);
|
||||
|
||||
if (postHogClient) {
|
||||
postHogClient.capture({
|
||||
event: "secrets modified",
|
||||
distinctId: req.user.email,
|
||||
properties: {
|
||||
numberOfSecrets: (secretsModificationsRequested ?? []).length,
|
||||
environment: environmentName,
|
||||
workspaceId,
|
||||
channel: req.headers?.["user-agent"]?.toLowerCase().includes("mozilla") ? "web" : "cli",
|
||||
userAgent: req.headers?.["user-agent"]
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return res.status(200).send();
|
||||
};
|
||||
|
||||
/**
|
||||
* Update a secret within workspace with id [workspaceId] and environment [environment]
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
export const updateSecret = async (req: Request, res: Response) => {
|
||||
const postHogClient = await TelemetryService.getPostHogClient();
|
||||
const { workspaceId, environmentName } = req.params;
|
||||
const secretModificationsRequested: ModifySecretRequestBody = req.body.secret;
|
||||
|
||||
await Secret.findOne({ workspace: workspaceId, environment: environmentName }, { _id: 1 });
|
||||
|
||||
const sanitizedSecret: SanitizedSecretModify = {
|
||||
secretKeyCiphertext: secretModificationsRequested.secretKeyCiphertext,
|
||||
secretKeyIV: secretModificationsRequested.secretKeyIV,
|
||||
secretKeyTag: secretModificationsRequested.secretKeyTag,
|
||||
secretKeyHash: secretModificationsRequested.secretKeyHash,
|
||||
secretValueCiphertext: secretModificationsRequested.secretValueCiphertext,
|
||||
secretValueIV: secretModificationsRequested.secretValueIV,
|
||||
secretValueTag: secretModificationsRequested.secretValueTag,
|
||||
secretValueHash: secretModificationsRequested.secretValueHash,
|
||||
secretCommentCiphertext: secretModificationsRequested.secretCommentCiphertext,
|
||||
secretCommentIV: secretModificationsRequested.secretCommentIV,
|
||||
secretCommentTag: secretModificationsRequested.secretCommentTag,
|
||||
secretCommentHash: secretModificationsRequested.secretCommentHash
|
||||
};
|
||||
|
||||
const singleModificationUpdate = await Secret.updateOne(
|
||||
{ _id: secretModificationsRequested._id, workspace: workspaceId },
|
||||
{ $inc: { version: 1 }, $set: sanitizedSecret }
|
||||
)
|
||||
.catch((error) => {
|
||||
if (error instanceof ValidationError) {
|
||||
throw RouteValidationError({
|
||||
message: "Unable to apply modifications, please try again",
|
||||
stack: error.stack
|
||||
});
|
||||
}
|
||||
|
||||
throw error;
|
||||
});
|
||||
|
||||
if (postHogClient) {
|
||||
postHogClient.capture({
|
||||
event: "secrets modified",
|
||||
distinctId: req.user.email,
|
||||
properties: {
|
||||
numberOfSecrets: 1,
|
||||
environment: environmentName,
|
||||
workspaceId,
|
||||
channel: req.headers?.["user-agent"]?.toLowerCase().includes("mozilla") ? "web" : "cli",
|
||||
userAgent: req.headers?.["user-agent"]
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return res.status(200).send(singleModificationUpdate);
|
||||
};
|
||||
|
||||
/**
|
||||
* Return secrets for workspace with id [workspaceId], environment [environment] and user
|
||||
* with id [req.user._id]
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
export const getSecrets = async (req: Request, res: Response) => {
|
||||
const postHogClient = await TelemetryService.getPostHogClient();
|
||||
const { environment } = req.query;
|
||||
const { workspaceId } = req.params;
|
||||
|
||||
let userId: Types.ObjectId | undefined = undefined; // used for getting personal secrets for user
|
||||
let userEmail: string | undefined = undefined; // used for posthog
|
||||
if (req.user) {
|
||||
userId = req.user._id;
|
||||
userEmail = req.user.email;
|
||||
}
|
||||
|
||||
if (req.serviceTokenData) {
|
||||
userId = req.serviceTokenData.user;
|
||||
|
||||
const user = await User.findById(req.serviceTokenData.user, "email");
|
||||
if (!user) throw AccountNotFoundError();
|
||||
userEmail = user.email;
|
||||
}
|
||||
|
||||
const secrets = await Secret.find({
|
||||
workspace: workspaceId,
|
||||
environment,
|
||||
$or: [{ user: userId }, { user: { $exists: false } }],
|
||||
type: { $in: [SECRET_SHARED, SECRET_PERSONAL] }
|
||||
})
|
||||
.catch((err) => {
|
||||
throw RouteValidationError({
|
||||
message: "Failed to get secrets, please try again",
|
||||
stack: err.stack
|
||||
});
|
||||
})
|
||||
|
||||
if (postHogClient) {
|
||||
postHogClient.capture({
|
||||
event: "secrets pulled",
|
||||
distinctId: userEmail,
|
||||
properties: {
|
||||
numberOfSecrets: (secrets ?? []).length,
|
||||
environment,
|
||||
workspaceId,
|
||||
channel: req.headers?.["user-agent"]?.toLowerCase().includes("mozilla") ? "web" : "cli",
|
||||
userAgent: req.headers?.["user-agent"]
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return res.json(secrets);
|
||||
};
|
||||
|
||||
/**
|
||||
* Return secret with id [secretId]
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
export const getSecret = async (req: Request, res: Response) => {
|
||||
// if (postHogClient) {
|
||||
// postHogClient.capture({
|
||||
// event: 'secrets pulled',
|
||||
// distinctId: req.user.email,
|
||||
// properties: {
|
||||
// numberOfSecrets: 1,
|
||||
// workspaceId: req._secret.workspace.toString(),
|
||||
// environment: req._secret.environment,
|
||||
// channel: req.headers?.['user-agent']?.toLowerCase().includes('mozilla') ? 'web' : 'cli',
|
||||
// userAgent: req.headers?.['user-agent']
|
||||
// }
|
||||
// });
|
||||
// }
|
||||
|
||||
return res.status(200).send({
|
||||
secret: req._secret
|
||||
});
|
||||
};
|
File diff suppressed because it is too large
Load Diff
@ -1,201 +0,0 @@
|
||||
import { Request, Response } from "express";
|
||||
import crypto from "crypto";
|
||||
import bcrypt from "bcrypt";
|
||||
import { ServiceTokenData } from "../../models";
|
||||
import { getSaltRounds } from "../../config";
|
||||
import { BadRequestError } from "../../utils/errors";
|
||||
import { ActorType, EventType } from "../../ee/models";
|
||||
import { EEAuditLogService } from "../../ee/services";
|
||||
import { validateRequest } from "../../helpers/validation";
|
||||
import * as reqValidator from "../../validation/serviceTokenData";
|
||||
import {
|
||||
ProjectPermissionActions,
|
||||
ProjectPermissionSub,
|
||||
getAuthDataProjectPermissions
|
||||
} from "../../ee/services/ProjectRoleService";
|
||||
import { ForbiddenError, subject } from "@casl/ability";
|
||||
import { Types } from "mongoose";
|
||||
|
||||
/**
|
||||
* Return service token data associated with service token on request
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
export const getServiceTokenData = async (req: Request, res: Response) => {
|
||||
/*
|
||||
#swagger.summary = 'Return Infisical Token data'
|
||||
#swagger.description = 'Return Infisical Token data'
|
||||
|
||||
#swagger.security = [{
|
||||
"bearerAuth": []
|
||||
}]
|
||||
|
||||
#swagger.responses[200] = {
|
||||
content: {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"serviceTokenData": {
|
||||
"type": "object",
|
||||
$ref: "#/components/schemas/ServiceTokenData",
|
||||
"description": "Details of service token"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
if (!(req.authData.authPayload instanceof ServiceTokenData))
|
||||
throw BadRequestError({
|
||||
message: "Failed accepted client validation for service token data"
|
||||
});
|
||||
|
||||
const serviceTokenData = await ServiceTokenData.findById(req.authData.authPayload._id)
|
||||
.select("+encryptedKey +iv +tag")
|
||||
.populate("user")
|
||||
.lean();
|
||||
|
||||
return res.status(200).json(serviceTokenData);
|
||||
};
|
||||
|
||||
/**
|
||||
* Create new service token data for workspace with id [workspaceId] and
|
||||
* environment [environment].
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
export const createServiceTokenData = async (req: Request, res: Response) => {
|
||||
let serviceTokenData;
|
||||
|
||||
const {
|
||||
body: { workspaceId, permissions, tag, encryptedKey, scopes, name, expiresIn, iv }
|
||||
} = await validateRequest(reqValidator.CreateServiceTokenV2, req);
|
||||
|
||||
const { permission } = await getAuthDataProjectPermissions({
|
||||
authData: req.authData,
|
||||
workspaceId: new Types.ObjectId(workspaceId)
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Create,
|
||||
ProjectPermissionSub.ServiceTokens
|
||||
);
|
||||
|
||||
scopes.forEach(({ environment, secretPath }) => {
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Create,
|
||||
subject(ProjectPermissionSub.Secrets, { environment, secretPath: secretPath })
|
||||
);
|
||||
})
|
||||
|
||||
|
||||
const secret = crypto.randomBytes(16).toString("hex");
|
||||
const secretHash = await bcrypt.hash(secret, await getSaltRounds());
|
||||
|
||||
let expiresAt;
|
||||
if (expiresIn) {
|
||||
expiresAt = new Date();
|
||||
expiresAt.setSeconds(expiresAt.getSeconds() + expiresIn);
|
||||
}
|
||||
|
||||
let user;
|
||||
|
||||
if (req.authData.actor.type === ActorType.USER) {
|
||||
user = req.authData.authPayload._id;
|
||||
}
|
||||
|
||||
serviceTokenData = await new ServiceTokenData({
|
||||
name,
|
||||
workspace: workspaceId,
|
||||
user,
|
||||
scopes,
|
||||
lastUsed: new Date(),
|
||||
expiresAt,
|
||||
secretHash,
|
||||
encryptedKey,
|
||||
iv,
|
||||
tag,
|
||||
permissions
|
||||
}).save();
|
||||
|
||||
// return service token data without sensitive data
|
||||
serviceTokenData = await ServiceTokenData.findById(serviceTokenData._id);
|
||||
|
||||
if (!serviceTokenData) throw new Error("Failed to find service token data");
|
||||
|
||||
const serviceToken = `st.${serviceTokenData._id.toString()}.${secret}`;
|
||||
|
||||
await EEAuditLogService.createAuditLog(
|
||||
req.authData,
|
||||
{
|
||||
type: EventType.CREATE_SERVICE_TOKEN,
|
||||
metadata: {
|
||||
name,
|
||||
scopes
|
||||
}
|
||||
},
|
||||
{
|
||||
workspaceId: new Types.ObjectId(workspaceId)
|
||||
}
|
||||
);
|
||||
|
||||
return res.status(200).send({
|
||||
serviceToken,
|
||||
serviceTokenData
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Delete service token data with id [serviceTokenDataId].
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
export const deleteServiceTokenData = async (req: Request, res: Response) => {
|
||||
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 getAuthDataProjectPermissions({
|
||||
authData: req.authData,
|
||||
workspaceId: serviceTokenData.workspace
|
||||
});
|
||||
|
||||
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"
|
||||
});
|
||||
|
||||
await EEAuditLogService.createAuditLog(
|
||||
req.authData,
|
||||
{
|
||||
type: EventType.DELETE_SERVICE_TOKEN,
|
||||
metadata: {
|
||||
name: serviceTokenData.name,
|
||||
scopes: serviceTokenData?.scopes
|
||||
}
|
||||
},
|
||||
{
|
||||
workspaceId: serviceTokenData.workspace
|
||||
}
|
||||
);
|
||||
|
||||
return res.status(200).send({
|
||||
serviceTokenData
|
||||
});
|
||||
};
|
@ -1,262 +0,0 @@
|
||||
import { Request, Response } from "express";
|
||||
import { MembershipOrg, User } from "../../models";
|
||||
import { completeAccount } from "../../helpers/user";
|
||||
import {
|
||||
initializeDefaultOrg,
|
||||
} from "../../helpers/signup";
|
||||
import { issueAuthTokens } from "../../helpers/auth";
|
||||
import { ACCEPTED, INVITED } from "../../variables";
|
||||
import { standardRequest } from "../../config/request";
|
||||
import { getHttpsEnabled, getLoopsApiKey } from "../../config";
|
||||
import { updateSubscriptionOrgQuantity } from "../../helpers/organization";
|
||||
|
||||
/**
|
||||
* Complete setting up user by adding their personal and auth information as part of the
|
||||
* signup flow
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
export const completeAccountSignup = async (req: Request, res: Response) => {
|
||||
let user;
|
||||
const {
|
||||
email,
|
||||
firstName,
|
||||
lastName,
|
||||
protectedKey,
|
||||
protectedKeyIV,
|
||||
protectedKeyTag,
|
||||
publicKey,
|
||||
encryptedPrivateKey,
|
||||
encryptedPrivateKeyIV,
|
||||
encryptedPrivateKeyTag,
|
||||
salt,
|
||||
verifier,
|
||||
organizationName,
|
||||
}: {
|
||||
email: string;
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
protectedKey: string;
|
||||
protectedKeyIV: string;
|
||||
protectedKeyTag: string;
|
||||
publicKey: string;
|
||||
encryptedPrivateKey: string;
|
||||
encryptedPrivateKeyIV: string;
|
||||
encryptedPrivateKeyTag: string;
|
||||
salt: string;
|
||||
verifier: string;
|
||||
organizationName: string;
|
||||
} = req.body;
|
||||
|
||||
// get user
|
||||
user = await User.findOne({ email });
|
||||
|
||||
if (!user || (user && user?.publicKey)) {
|
||||
// case 1: user doesn't exist.
|
||||
// case 2: user has already completed account
|
||||
return res.status(403).send({
|
||||
error: "Failed to complete account for complete user",
|
||||
});
|
||||
}
|
||||
|
||||
// complete setting up user's account
|
||||
user = await completeAccount({
|
||||
userId: user._id.toString(),
|
||||
firstName,
|
||||
lastName,
|
||||
encryptionVersion: 2,
|
||||
protectedKey,
|
||||
protectedKeyIV,
|
||||
protectedKeyTag,
|
||||
publicKey,
|
||||
encryptedPrivateKey,
|
||||
encryptedPrivateKeyIV,
|
||||
encryptedPrivateKeyTag,
|
||||
salt,
|
||||
verifier,
|
||||
});
|
||||
|
||||
if (!user)
|
||||
throw new Error("Failed to complete account for non-existent user"); // ensure user is non-null
|
||||
|
||||
// initialize default organization and workspace
|
||||
await initializeDefaultOrg({
|
||||
organizationName,
|
||||
user,
|
||||
});
|
||||
|
||||
// update organization membership statuses that are
|
||||
// invited to completed with user attached
|
||||
const membershipsToUpdate = await MembershipOrg.find({
|
||||
inviteEmail: email,
|
||||
status: INVITED,
|
||||
});
|
||||
|
||||
membershipsToUpdate.forEach(async (membership) => {
|
||||
await updateSubscriptionOrgQuantity({
|
||||
organizationId: membership.organization.toString(),
|
||||
});
|
||||
});
|
||||
|
||||
// update organization membership statuses that are
|
||||
// invited to completed with user attached
|
||||
await MembershipOrg.updateMany(
|
||||
{
|
||||
inviteEmail: email,
|
||||
status: INVITED,
|
||||
},
|
||||
{
|
||||
user,
|
||||
status: ACCEPTED,
|
||||
}
|
||||
);
|
||||
|
||||
// issue tokens
|
||||
const tokens = await issueAuthTokens({
|
||||
userId: user._id,
|
||||
ip: req.realIP,
|
||||
userAgent: req.headers["user-agent"] ?? "",
|
||||
});
|
||||
|
||||
const token = tokens.token;
|
||||
|
||||
// sending a welcome email to new users
|
||||
if (await getLoopsApiKey()) {
|
||||
await standardRequest.post("https://app.loops.so/api/v1/events/send", {
|
||||
"email": email,
|
||||
"eventName": "Sign Up",
|
||||
"firstName": firstName,
|
||||
"lastName": lastName,
|
||||
}, {
|
||||
headers: {
|
||||
"Accept": "application/json",
|
||||
"Authorization": "Bearer " + (await getLoopsApiKey()),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// store (refresh) token in httpOnly cookie
|
||||
res.cookie("jid", tokens.refreshToken, {
|
||||
httpOnly: true,
|
||||
path: "/",
|
||||
sameSite: "strict",
|
||||
secure: await getHttpsEnabled(),
|
||||
});
|
||||
|
||||
return res.status(200).send({
|
||||
message: "Successfully set up account",
|
||||
user,
|
||||
token,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Complete setting up user by adding their personal and auth information as part of the
|
||||
* invite flow
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
export const completeAccountInvite = async (req: Request, res: Response) => {
|
||||
let user;
|
||||
const {
|
||||
email,
|
||||
firstName,
|
||||
lastName,
|
||||
protectedKey,
|
||||
protectedKeyIV,
|
||||
protectedKeyTag,
|
||||
publicKey,
|
||||
encryptedPrivateKey,
|
||||
encryptedPrivateKeyIV,
|
||||
encryptedPrivateKeyTag,
|
||||
salt,
|
||||
verifier,
|
||||
} = req.body;
|
||||
|
||||
// get user
|
||||
user = await User.findOne({ email });
|
||||
|
||||
if (!user || (user && user?.publicKey)) {
|
||||
// case 1: user doesn't exist.
|
||||
// case 2: user has already completed account
|
||||
return res.status(403).send({
|
||||
error: "Failed to complete account for complete user",
|
||||
});
|
||||
}
|
||||
|
||||
const membershipOrg = await MembershipOrg.findOne({
|
||||
inviteEmail: email,
|
||||
status: INVITED,
|
||||
});
|
||||
|
||||
if (!membershipOrg) throw new Error("Failed to find invitations for email");
|
||||
|
||||
// complete setting up user's account
|
||||
user = await completeAccount({
|
||||
userId: user._id.toString(),
|
||||
firstName,
|
||||
lastName,
|
||||
encryptionVersion: 2,
|
||||
protectedKey,
|
||||
protectedKeyIV,
|
||||
protectedKeyTag,
|
||||
publicKey,
|
||||
encryptedPrivateKey,
|
||||
encryptedPrivateKeyIV,
|
||||
encryptedPrivateKeyTag,
|
||||
salt,
|
||||
verifier,
|
||||
});
|
||||
|
||||
if (!user)
|
||||
throw new Error("Failed to complete account for non-existent user");
|
||||
|
||||
// update organization membership statuses that are
|
||||
// invited to completed with user attached
|
||||
const membershipsToUpdate = await MembershipOrg.find({
|
||||
inviteEmail: email,
|
||||
status: INVITED,
|
||||
});
|
||||
|
||||
membershipsToUpdate.forEach(async (membership) => {
|
||||
await updateSubscriptionOrgQuantity({
|
||||
organizationId: membership.organization.toString(),
|
||||
});
|
||||
});
|
||||
|
||||
await MembershipOrg.updateMany(
|
||||
{
|
||||
inviteEmail: email,
|
||||
status: INVITED,
|
||||
},
|
||||
{
|
||||
user,
|
||||
status: ACCEPTED,
|
||||
}
|
||||
);
|
||||
|
||||
// issue tokens
|
||||
const tokens = await issueAuthTokens({
|
||||
userId: user._id,
|
||||
ip: req.realIP,
|
||||
userAgent: req.headers["user-agent"] ?? "",
|
||||
});
|
||||
|
||||
const token = tokens.token;
|
||||
|
||||
// store (refresh) token in httpOnly cookie
|
||||
res.cookie("jid", tokens.refreshToken, {
|
||||
httpOnly: true,
|
||||
path: "/",
|
||||
sameSite: "strict",
|
||||
secure: await getHttpsEnabled(),
|
||||
});
|
||||
|
||||
return res.status(200).send({
|
||||
message: "Successfully set up account",
|
||||
user,
|
||||
token,
|
||||
});
|
||||
};
|
@ -1,92 +0,0 @@
|
||||
import { ForbiddenError } from "@casl/ability";
|
||||
import { Request, Response } from "express";
|
||||
import { Types } from "mongoose";
|
||||
import { Secret, Tag } from "../../models";
|
||||
import { BadRequestError } from "../../utils/errors";
|
||||
import { validateRequest } from "../../helpers/validation";
|
||||
import {
|
||||
ProjectPermissionActions,
|
||||
ProjectPermissionSub,
|
||||
getAuthDataProjectPermissions
|
||||
} from "../../ee/services/ProjectRoleService";
|
||||
import * as reqValidator from "../../validation/tags";
|
||||
|
||||
export const createWorkspaceTag = async (req: Request, res: Response) => {
|
||||
const {
|
||||
body: { name, slug },
|
||||
params: { workspaceId }
|
||||
} = await validateRequest(reqValidator.CreateWorkspaceTagsV2, req);
|
||||
|
||||
const { permission } = await getAuthDataProjectPermissions({
|
||||
authData: req.authData,
|
||||
workspaceId: new Types.ObjectId(workspaceId)
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Create,
|
||||
ProjectPermissionSub.Tags
|
||||
);
|
||||
|
||||
const tagToCreate = {
|
||||
name,
|
||||
workspace: new Types.ObjectId(workspaceId),
|
||||
slug,
|
||||
user: new Types.ObjectId(req.user._id)
|
||||
};
|
||||
|
||||
const createdTag = await new Tag(tagToCreate).save();
|
||||
|
||||
res.json(createdTag);
|
||||
};
|
||||
|
||||
export const deleteWorkspaceTag = async (req: Request, res: Response) => {
|
||||
const {
|
||||
params: { tagId }
|
||||
} = await validateRequest(reqValidator.DeleteWorkspaceTagsV2, req);
|
||||
|
||||
const tagFromDB = await Tag.findById(tagId);
|
||||
if (!tagFromDB) {
|
||||
throw BadRequestError();
|
||||
}
|
||||
|
||||
const { permission } = await getAuthDataProjectPermissions({
|
||||
authData: req.authData,
|
||||
workspaceId: tagFromDB.workspace
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Delete,
|
||||
ProjectPermissionSub.Tags
|
||||
);
|
||||
|
||||
const result = await Tag.findByIdAndDelete(tagId);
|
||||
|
||||
// remove the tag from secrets
|
||||
await Secret.updateMany({ tags: { $in: [tagId] } }, { $pull: { tags: tagId } });
|
||||
|
||||
res.json(result);
|
||||
};
|
||||
|
||||
export const getWorkspaceTags = async (req: Request, res: Response) => {
|
||||
const {
|
||||
params: { workspaceId }
|
||||
} = await validateRequest(reqValidator.GetWorkspaceTagsV2, req);
|
||||
|
||||
const { permission } = await getAuthDataProjectPermissions({
|
||||
authData: req.authData,
|
||||
workspaceId: new Types.ObjectId(workspaceId)
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Read,
|
||||
ProjectPermissionSub.Tags
|
||||
);
|
||||
|
||||
const workspaceTags = await Tag.find({
|
||||
workspace: new Types.ObjectId(workspaceId)
|
||||
});
|
||||
|
||||
return res.json({
|
||||
workspaceTags
|
||||
});
|
||||
};
|
@ -1,314 +0,0 @@
|
||||
import { Request, Response } from "express";
|
||||
import { Types } from "mongoose";
|
||||
import crypto from "crypto";
|
||||
import bcrypt from "bcrypt";
|
||||
import { APIKeyData, AuthMethod, MembershipOrg, TokenVersion, User } from "../../models";
|
||||
import { getSaltRounds } from "../../config";
|
||||
import { validateRequest } from "../../helpers/validation";
|
||||
import { deleteUser } from "../../helpers/user";
|
||||
import * as reqValidator from "../../validation";
|
||||
|
||||
/**
|
||||
* Update the current user's MFA-enabled status [isMfaEnabled].
|
||||
* Note: Infisical currently only supports email-based 2FA only; this will expand to
|
||||
* include SMS and authenticator app modes of authentication in the future.
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
export const updateMyMfaEnabled = async (req: Request, res: Response) => {
|
||||
const {
|
||||
body: { isMfaEnabled }
|
||||
} = await validateRequest(reqValidator.UpdateMyMfaEnabledV2, req);
|
||||
|
||||
req.user.isMfaEnabled = isMfaEnabled;
|
||||
|
||||
if (isMfaEnabled) {
|
||||
// TODO: adapt this route/controller
|
||||
// to work for different forms of MFA
|
||||
req.user.mfaMethods = ["email"];
|
||||
} else {
|
||||
req.user.mfaMethods = [];
|
||||
}
|
||||
|
||||
await req.user.save();
|
||||
|
||||
const user = req.user;
|
||||
|
||||
return res.status(200).send({
|
||||
user
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Update name of the current user to [firstName, lastName].
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
export const updateName = async (req: Request, res: Response) => {
|
||||
const {
|
||||
body: { lastName, firstName }
|
||||
} = await validateRequest(reqValidator.UpdateNameV2, req);
|
||||
|
||||
const user = await User.findByIdAndUpdate(
|
||||
req.user._id.toString(),
|
||||
{
|
||||
firstName,
|
||||
lastName: lastName ?? ""
|
||||
},
|
||||
{
|
||||
new: true
|
||||
}
|
||||
);
|
||||
|
||||
return res.status(200).send({
|
||||
user
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Update auth method of the current user to [authMethods]
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
export const updateAuthMethods = async (req: Request, res: Response) => {
|
||||
const {
|
||||
body: { authMethods }
|
||||
} = 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) {
|
||||
return res.status(400).send({
|
||||
message: "Failed to update user authentication method because SAML SSO is enforced"
|
||||
});
|
||||
}
|
||||
|
||||
const user = await User.findByIdAndUpdate(
|
||||
req.user._id.toString(),
|
||||
{
|
||||
authMethods
|
||||
},
|
||||
{
|
||||
new: true
|
||||
}
|
||||
);
|
||||
|
||||
return res.status(200).send({
|
||||
user
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Return organizations that the current user is part of.
|
||||
* @param req
|
||||
* @param res
|
||||
*/
|
||||
export const getMyOrganizations = async (req: Request, res: Response) => {
|
||||
/*
|
||||
#swagger.summary = 'Return organizations that current user is part of'
|
||||
#swagger.description = 'Return organizations that current user is part of'
|
||||
|
||||
#swagger.security = [{
|
||||
"apiKeyAuth": []
|
||||
}]
|
||||
|
||||
#swagger.responses[200] = {
|
||||
content: {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"organizations": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
$ref: "#/components/schemas/Organization"
|
||||
},
|
||||
"description": "Organizations that user is part of"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
*/
|
||||
const organizations = (
|
||||
await MembershipOrg.find({
|
||||
user: req.user._id
|
||||
}).populate("organization")
|
||||
).map((m) => m.organization);
|
||||
|
||||
return res.status(200).send({
|
||||
organizations
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Return API keys belonging to current user.
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
export const getMyAPIKeys = async (req: Request, res: Response) => {
|
||||
const apiKeyData = await APIKeyData.find({
|
||||
user: req.user._id
|
||||
});
|
||||
|
||||
return res.status(200).send(apiKeyData);
|
||||
};
|
||||
|
||||
/**
|
||||
* Create new API key for current user.
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
export const createAPIKey = async (req: Request, res: Response) => {
|
||||
const {
|
||||
body: { name, expiresIn }
|
||||
} = await validateRequest(reqValidator.CreateApiKeyV2, req);
|
||||
|
||||
const secret = crypto.randomBytes(16).toString("hex");
|
||||
const secretHash = await bcrypt.hash(secret, await getSaltRounds());
|
||||
|
||||
const expiresAt = new Date();
|
||||
expiresAt.setSeconds(expiresAt.getSeconds() + expiresIn);
|
||||
|
||||
let apiKeyData = await new APIKeyData({
|
||||
name,
|
||||
lastUsed: new Date(),
|
||||
expiresAt,
|
||||
user: req.user._id,
|
||||
secretHash
|
||||
}).save();
|
||||
|
||||
// return api key data without sensitive data
|
||||
apiKeyData = (await APIKeyData.findById(apiKeyData._id)) as any;
|
||||
|
||||
if (!apiKeyData) throw new Error("Failed to find API key data");
|
||||
|
||||
const apiKey = `ak.${apiKeyData._id.toString()}.${secret}`;
|
||||
|
||||
return res.status(200).send({
|
||||
apiKey,
|
||||
apiKeyData
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Delete API key with id [apiKeyDataId] belonging to current user
|
||||
* @param req
|
||||
* @param res
|
||||
*/
|
||||
export const deleteAPIKey = async (req: Request, res: Response) => {
|
||||
const {
|
||||
params: { apiKeyDataId }
|
||||
} = await validateRequest(reqValidator.DeleteApiKeyV2, req);
|
||||
|
||||
const apiKeyData = await APIKeyData.findOneAndDelete({
|
||||
_id: new Types.ObjectId(apiKeyDataId),
|
||||
user: req.user._id
|
||||
});
|
||||
|
||||
return res.status(200).send({
|
||||
apiKeyData
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Return active sessions (TokenVersion) belonging to user
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
export const getMySessions = async (req: Request, res: Response) => {
|
||||
const tokenVersions = await TokenVersion.find({
|
||||
user: req.user._id
|
||||
});
|
||||
|
||||
return res.status(200).send(tokenVersions);
|
||||
};
|
||||
|
||||
/**
|
||||
* Revoke all active sessions belong to user
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
export const deleteMySessions = async (req: Request, res: Response) => {
|
||||
await TokenVersion.updateMany(
|
||||
{
|
||||
user: req.user._id
|
||||
},
|
||||
{
|
||||
$inc: {
|
||||
refreshVersion: 1,
|
||||
accessVersion: 1
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
return res.status(200).send({
|
||||
message: "Successfully revoked all sessions"
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Return the current user.
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
export const getMe = async (req: Request, res: Response) => {
|
||||
/*
|
||||
#swagger.summary = "Retrieve the current user on the request"
|
||||
#swagger.description = "Retrieve the current user on the request"
|
||||
|
||||
#swagger.security = [{
|
||||
"apiKeyAuth": []
|
||||
}]
|
||||
|
||||
#swagger.responses[200] = {
|
||||
content: {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"user": {
|
||||
"type": "object",
|
||||
$ref: "#/components/schemas/CurrentUser",
|
||||
"description": "Current user on request"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
*/
|
||||
const user = await User.findById(req.user._id).select(
|
||||
"+salt +publicKey +encryptedPrivateKey +iv +tag +encryptionVersion +protectedKey +protectedKeyIV +protectedKeyTag"
|
||||
);
|
||||
|
||||
return res.status(200).send({
|
||||
user
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Delete the current user.
|
||||
* @param req
|
||||
* @param res
|
||||
*/
|
||||
export const deleteMe = async (req: Request, res: Response) => {
|
||||
const user = await deleteUser({
|
||||
userId: req.user._id
|
||||
});
|
||||
|
||||
return res.status(200).send({
|
||||
user
|
||||
});
|
||||
}
|
@ -1,883 +0,0 @@
|
||||
import { Request, Response } from "express";
|
||||
import { Types } from "mongoose";
|
||||
import {
|
||||
IIdentity,
|
||||
IdentityMembership,
|
||||
IdentityMembershipOrg,
|
||||
Key,
|
||||
Membership,
|
||||
ServiceTokenData,
|
||||
Workspace
|
||||
} from "../../models";
|
||||
import { IRole, Role } from "../../ee/models";
|
||||
import {
|
||||
pullSecrets as pull,
|
||||
v2PushSecrets as push,
|
||||
reformatPullSecrets
|
||||
} from "../../helpers/secret";
|
||||
import { pushKeys } from "../../helpers/key";
|
||||
import { EventService, TelemetryService } from "../../services";
|
||||
import { eventPushSecrets } from "../../events";
|
||||
import { EEAuditLogService } from "../../ee/services";
|
||||
import { EventType } from "../../ee/models";
|
||||
import { validateRequest } from "../../helpers/validation";
|
||||
import * as reqValidator from "../../validation";
|
||||
import {
|
||||
ProjectPermissionActions,
|
||||
ProjectPermissionSub,
|
||||
getAuthDataProjectPermissions,
|
||||
getWorkspaceRolePermissions,
|
||||
isAtLeastAsPrivilegedWorkspace
|
||||
} from "../../ee/services/ProjectRoleService";
|
||||
import { ForbiddenError } from "@casl/ability";
|
||||
import { BadRequestError, ForbiddenRequestError, ResourceNotFoundError } from "../../utils/errors";
|
||||
import { ADMIN, CUSTOM, MEMBER, NO_ACCESS, VIEWER } from "../../variables";
|
||||
|
||||
interface V2PushSecret {
|
||||
type: string; // personal or shared
|
||||
secretKeyCiphertext: string;
|
||||
secretKeyIV: string;
|
||||
secretKeyTag: string;
|
||||
secretKeyHash: string;
|
||||
secretValueCiphertext: string;
|
||||
secretValueIV: string;
|
||||
secretValueTag: string;
|
||||
secretValueHash: string;
|
||||
secretCommentCiphertext?: string;
|
||||
secretCommentIV?: string;
|
||||
secretCommentTag?: string;
|
||||
secretCommentHash?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Upload (encrypted) secrets to workspace with id [workspaceId]
|
||||
* for environment [environment]
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
export const pushWorkspaceSecrets = async (req: Request, res: Response) => {
|
||||
// upload (encrypted) secrets to workspace with id [workspaceId]
|
||||
const postHogClient = await TelemetryService.getPostHogClient();
|
||||
let { secrets }: { secrets: V2PushSecret[] } = req.body;
|
||||
const { keys, environment, channel } = req.body;
|
||||
const { workspaceId } = req.params;
|
||||
|
||||
// validate environment
|
||||
const workspaceEnvs = req.membership.workspace.environments;
|
||||
if (!workspaceEnvs.find(({ slug }: { slug: string }) => slug === environment)) {
|
||||
throw new Error("Failed to validate environment");
|
||||
}
|
||||
|
||||
// sanitize secrets
|
||||
secrets = secrets.filter(
|
||||
(s: V2PushSecret) => s.secretKeyCiphertext !== "" && s.secretValueCiphertext !== ""
|
||||
);
|
||||
|
||||
await push({
|
||||
userId: req.user._id,
|
||||
workspaceId,
|
||||
environment,
|
||||
secrets,
|
||||
channel: channel ? channel : "cli",
|
||||
ipAddress: req.realIP
|
||||
});
|
||||
|
||||
await pushKeys({
|
||||
userId: req.user._id,
|
||||
workspaceId,
|
||||
keys
|
||||
});
|
||||
|
||||
if (postHogClient) {
|
||||
postHogClient.capture({
|
||||
event: "secrets pushed",
|
||||
distinctId: req.user.email,
|
||||
properties: {
|
||||
numberOfSecrets: secrets.length,
|
||||
environment,
|
||||
workspaceId,
|
||||
channel: channel ? channel : "cli"
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// trigger event - push secrets
|
||||
EventService.handleEvent({
|
||||
event: eventPushSecrets({
|
||||
workspaceId: new Types.ObjectId(workspaceId),
|
||||
environment,
|
||||
secretPath: "/"
|
||||
})
|
||||
});
|
||||
|
||||
return res.status(200).send({
|
||||
message: "Successfully uploaded workspace secrets"
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Return (encrypted) secrets for workspace with id [workspaceId]
|
||||
* for environment [environment]
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
export const pullSecrets = async (req: Request, res: Response) => {
|
||||
let secrets;
|
||||
const postHogClient = await TelemetryService.getPostHogClient();
|
||||
const environment: string = req.query.environment as string;
|
||||
const channel: string = req.query.channel as string;
|
||||
const { workspaceId } = req.params;
|
||||
|
||||
let userId;
|
||||
if (req.user) {
|
||||
userId = req.user._id.toString();
|
||||
} else if (req.serviceTokenData) {
|
||||
userId = req.serviceTokenData.user.toString();
|
||||
}
|
||||
// validate environment
|
||||
const workspaceEnvs = req.membership.workspace.environments;
|
||||
if (!workspaceEnvs.find(({ slug }: { slug: string }) => slug === environment)) {
|
||||
throw new Error("Failed to validate environment");
|
||||
}
|
||||
|
||||
secrets = await pull({
|
||||
userId,
|
||||
workspaceId,
|
||||
environment,
|
||||
channel: channel ? channel : "cli",
|
||||
ipAddress: req.realIP
|
||||
});
|
||||
|
||||
if (channel !== "cli") {
|
||||
secrets = reformatPullSecrets({ secrets });
|
||||
}
|
||||
|
||||
if (postHogClient) {
|
||||
// capture secrets pushed event in production
|
||||
postHogClient.capture({
|
||||
distinctId: req.user.email,
|
||||
event: "secrets pulled",
|
||||
properties: {
|
||||
numberOfSecrets: secrets.length,
|
||||
environment,
|
||||
workspaceId,
|
||||
channel: channel ? channel : "cli"
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return res.status(200).send({
|
||||
secrets
|
||||
});
|
||||
};
|
||||
|
||||
export const getWorkspaceKey = async (req: Request, res: Response) => {
|
||||
/*
|
||||
#swagger.summary = 'Return encrypted project key'
|
||||
#swagger.description = 'Return encrypted project key'
|
||||
|
||||
#swagger.security = [{
|
||||
"apiKeyAuth": []
|
||||
}]
|
||||
|
||||
#swagger.parameters['workspaceId'] = {
|
||||
"description": "ID of project",
|
||||
"required": true,
|
||||
"type": "string"
|
||||
}
|
||||
|
||||
#swagger.responses[200] = {
|
||||
content: {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
$ref: "#/components/schemas/ProjectKey"
|
||||
},
|
||||
"description": "Encrypted project key for the given project"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
*/
|
||||
const {
|
||||
params: { workspaceId }
|
||||
} = await validateRequest(reqValidator.GetWorkspaceKeyV2, req);
|
||||
|
||||
const key = await Key.findOne({
|
||||
workspace: workspaceId,
|
||||
receiver: req.user._id
|
||||
}).populate("sender", "+publicKey");
|
||||
|
||||
if (!key) throw new Error(`getWorkspaceKey: Failed to find workspace key [workspaceId=${workspaceId}] [receiver=${req.user._id}]`);
|
||||
|
||||
await EEAuditLogService.createAuditLog(
|
||||
req.authData,
|
||||
{
|
||||
type: EventType.GET_WORKSPACE_KEY,
|
||||
metadata: {
|
||||
keyId: key._id.toString()
|
||||
}
|
||||
},
|
||||
{
|
||||
workspaceId: new Types.ObjectId(workspaceId)
|
||||
}
|
||||
);
|
||||
|
||||
return res.status(200).json(key);
|
||||
};
|
||||
|
||||
export const getWorkspaceServiceTokenData = async (req: Request, res: Response) => {
|
||||
const { workspaceId } = req.params;
|
||||
|
||||
const serviceTokenData = await ServiceTokenData.find({
|
||||
workspace: workspaceId
|
||||
}).select("+encryptedKey +iv +tag");
|
||||
|
||||
return res.status(200).send({
|
||||
serviceTokenData
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Return memberships for workspace with id [workspaceId]
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
export const getWorkspaceMemberships = async (req: Request, res: Response) => {
|
||||
/*
|
||||
#swagger.summary = 'Return project user memberships'
|
||||
#swagger.description = 'Return project user memberships'
|
||||
|
||||
#swagger.security = [{
|
||||
"apiKeyAuth": [],
|
||||
"bearerAuth": []
|
||||
}]
|
||||
|
||||
#swagger.parameters['workspaceId'] = {
|
||||
"description": "ID of project",
|
||||
"required": true,
|
||||
"type": "string"
|
||||
}
|
||||
|
||||
#swagger.responses[200] = {
|
||||
content: {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"memberships": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
$ref: "#/components/schemas/Membership"
|
||||
},
|
||||
"description": "Memberships of project"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
*/
|
||||
const {
|
||||
params: { workspaceId }
|
||||
} = await validateRequest(reqValidator.GetWorkspaceMembershipsV2, req);
|
||||
|
||||
const { permission } = await getAuthDataProjectPermissions({
|
||||
authData: req.authData,
|
||||
workspaceId: new Types.ObjectId(workspaceId)
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Read,
|
||||
ProjectPermissionSub.Member
|
||||
);
|
||||
|
||||
const memberships = await Membership.find({
|
||||
workspace: workspaceId
|
||||
}).populate("user", "+publicKey");
|
||||
|
||||
return res.status(200).send({
|
||||
memberships
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Update role of membership with id [membershipId] to role [role]
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
export const updateWorkspaceMembership = async (req: Request, res: Response) => {
|
||||
/*
|
||||
#swagger.summary = 'Update project user membership'
|
||||
#swagger.description = 'Update project user membership'
|
||||
|
||||
#swagger.security = [{
|
||||
"apiKeyAuth": [],
|
||||
"bearerAuth": []
|
||||
}]
|
||||
|
||||
#swagger.parameters['workspaceId'] = {
|
||||
"description": "ID of project",
|
||||
"required": true,
|
||||
"type": "string"
|
||||
}
|
||||
|
||||
#swagger.parameters['membershipId'] = {
|
||||
"description": "ID of project membership to update",
|
||||
"required": true,
|
||||
"type": "string"
|
||||
}
|
||||
|
||||
#swagger.requestBody = {
|
||||
"required": true,
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"role": {
|
||||
"type": "string",
|
||||
"description": "Role to update to for project membership",
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#swagger.responses[200] = {
|
||||
content: {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"membership": {
|
||||
$ref: "#/components/schemas/Membership",
|
||||
"description": "Updated membership"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
*/
|
||||
const {
|
||||
params: { workspaceId, membershipId },
|
||||
body: { role }
|
||||
} = await validateRequest(reqValidator.UpdateWorkspaceMembershipsV2, req);
|
||||
|
||||
const { permission } = await getAuthDataProjectPermissions({
|
||||
authData: req.authData,
|
||||
workspaceId: new Types.ObjectId(workspaceId)
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Edit,
|
||||
ProjectPermissionSub.Member
|
||||
);
|
||||
|
||||
const membership = await Membership.findByIdAndUpdate(
|
||||
membershipId,
|
||||
{
|
||||
role
|
||||
},
|
||||
{
|
||||
new: true
|
||||
}
|
||||
);
|
||||
|
||||
return res.status(200).send({
|
||||
membership
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Delete workspace membership with id [membershipId]
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
export const deleteWorkspaceMembership = async (req: Request, res: Response) => {
|
||||
/*
|
||||
#swagger.summary = 'Delete project user membership'
|
||||
#swagger.description = 'Delete project user membership'
|
||||
|
||||
#swagger.security = [{
|
||||
"apiKeyAuth": [],
|
||||
"bearerAuth": []
|
||||
}]
|
||||
|
||||
#swagger.parameters['workspaceId'] = {
|
||||
"description": "ID of project",
|
||||
"required": true,
|
||||
"type": "string"
|
||||
}
|
||||
|
||||
#swagger.parameters['membershipId'] = {
|
||||
"description": "ID of project membership to delete",
|
||||
"required": true,
|
||||
"type": "string"
|
||||
}
|
||||
|
||||
#swagger.responses[200] = {
|
||||
content: {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"membership": {
|
||||
$ref: "#/components/schemas/Membership",
|
||||
"description": "Deleted membership"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
*/
|
||||
const {
|
||||
params: { workspaceId, membershipId }
|
||||
} = await validateRequest(reqValidator.DeleteWorkspaceMembershipsV2, req);
|
||||
|
||||
const { permission } = await getAuthDataProjectPermissions({
|
||||
authData: req.authData,
|
||||
workspaceId: new Types.ObjectId(workspaceId)
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Delete,
|
||||
ProjectPermissionSub.Member
|
||||
);
|
||||
|
||||
const membership = await Membership.findByIdAndDelete(membershipId);
|
||||
|
||||
if (!membership) throw new Error("Failed to delete workspace membership");
|
||||
|
||||
await Key.deleteMany({
|
||||
receiver: membership.user,
|
||||
workspace: membership.workspace
|
||||
});
|
||||
|
||||
return res.status(200).send({
|
||||
membership
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Change autoCapitilzation Rule of workspace
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
export const toggleAutoCapitalization = async (req: Request, res: Response) => {
|
||||
const {
|
||||
params: { workspaceId },
|
||||
body: { autoCapitalization }
|
||||
} = await validateRequest(reqValidator.ToggleAutoCapitalizationV2, req);
|
||||
|
||||
const { permission } = await getAuthDataProjectPermissions({
|
||||
authData: req.authData,
|
||||
workspaceId: new Types.ObjectId(workspaceId)
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Edit,
|
||||
ProjectPermissionSub.Settings
|
||||
);
|
||||
|
||||
const workspace = await Workspace.findOneAndUpdate(
|
||||
{
|
||||
_id: workspaceId
|
||||
},
|
||||
{
|
||||
autoCapitalization
|
||||
},
|
||||
{
|
||||
new: true
|
||||
}
|
||||
);
|
||||
|
||||
return res.status(200).send({
|
||||
message: "Successfully changed autoCapitalization setting",
|
||||
workspace
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Add identity with id [identityId] to workspace
|
||||
* with id [workspaceId]
|
||||
* @param req
|
||||
* @param res
|
||||
*/
|
||||
export const addIdentityToWorkspace = async (req: Request, res: Response) => {
|
||||
const {
|
||||
params: { workspaceId, identityId },
|
||||
body: {
|
||||
role
|
||||
}
|
||||
} = await validateRequest(reqValidator.AddIdentityToWorkspaceV2, req);
|
||||
|
||||
const { permission } = await getAuthDataProjectPermissions({
|
||||
authData: req.authData,
|
||||
workspaceId: new Types.ObjectId(workspaceId)
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Create,
|
||||
ProjectPermissionSub.Identity
|
||||
);
|
||||
|
||||
let identityMembership = await IdentityMembership.findOne({
|
||||
identity: new Types.ObjectId(identityId),
|
||||
workspace: new Types.ObjectId(workspaceId)
|
||||
});
|
||||
|
||||
if (identityMembership) throw BadRequestError({
|
||||
message: `Identity with id ${identityId} already exists in project with id ${workspaceId}`
|
||||
});
|
||||
|
||||
|
||||
const workspace = await Workspace.findById(workspaceId);
|
||||
if (!workspace) throw ResourceNotFoundError();
|
||||
|
||||
const identityMembershipOrg = await IdentityMembershipOrg.findOne({
|
||||
identity: new Types.ObjectId(identityId),
|
||||
organization: workspace.organization
|
||||
});
|
||||
|
||||
if (!identityMembershipOrg) throw ResourceNotFoundError({
|
||||
message: `Failed to find identity with id ${identityId}`
|
||||
});
|
||||
|
||||
if (!identityMembershipOrg.organization.equals(workspace.organization)) throw BadRequestError({
|
||||
message: "Failed to add identity to project in another organization"
|
||||
});
|
||||
|
||||
const rolePermission = await getWorkspaceRolePermissions(role, workspaceId);
|
||||
const isAsPrivilegedAsIntendedRole = isAtLeastAsPrivilegedWorkspace(permission, rolePermission);
|
||||
|
||||
if (!isAsPrivilegedAsIntendedRole) throw ForbiddenRequestError({
|
||||
message: "Failed to add identity to project with more privileged role"
|
||||
});
|
||||
|
||||
let customRole;
|
||||
if (role) {
|
||||
const isCustomRole = ![ADMIN, MEMBER, VIEWER, NO_ACCESS].includes(role);
|
||||
if (isCustomRole) {
|
||||
customRole = await Role.findOne({
|
||||
slug: role,
|
||||
isOrgRole: false,
|
||||
workspace: new Types.ObjectId(workspaceId)
|
||||
});
|
||||
|
||||
if (!customRole) throw BadRequestError({ message: "Role not found" });
|
||||
}
|
||||
}
|
||||
|
||||
identityMembership = await new IdentityMembership({
|
||||
identity: identityMembershipOrg.identity,
|
||||
workspace: new Types.ObjectId(workspaceId),
|
||||
role: customRole ? CUSTOM : role,
|
||||
customRole
|
||||
}).save();
|
||||
|
||||
return res.status(200).send({
|
||||
identityMembership
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Update role of identity with id [identityId] in workspace
|
||||
* with id [workspaceId] to [role]
|
||||
* @param req
|
||||
* @param res
|
||||
*/
|
||||
export const updateIdentityWorkspaceRole = async (req: Request, res: Response) => {
|
||||
/*
|
||||
#swagger.summary = 'Update project identity membership'
|
||||
#swagger.description = 'Update project identity membership'
|
||||
|
||||
#swagger.security = [{
|
||||
"bearerAuth": []
|
||||
}]
|
||||
|
||||
#swagger.parameters['workspaceId'] = {
|
||||
"description": "ID of project",
|
||||
"required": true,
|
||||
"type": "string"
|
||||
}
|
||||
|
||||
#swagger.parameters['identityId'] = {
|
||||
"description": "ID of identity whose membership to update in project",
|
||||
"required": true,
|
||||
"type": "string"
|
||||
}
|
||||
|
||||
#swagger.requestBody = {
|
||||
"required": true,
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"role": {
|
||||
"type": "string",
|
||||
"description": "Role to update to for identity project membership",
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#swagger.responses[200] = {
|
||||
content: {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"identityMembership": {
|
||||
$ref: "#/components/schemas/IdentityMembership",
|
||||
"description": "Updated identity membership"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
*/
|
||||
const {
|
||||
params: { workspaceId, identityId },
|
||||
body: {
|
||||
role
|
||||
}
|
||||
} = await validateRequest(reqValidator.UpdateIdentityWorkspaceRoleV2, req);
|
||||
|
||||
const { permission } = await getAuthDataProjectPermissions({
|
||||
authData: req.authData,
|
||||
workspaceId: new Types.ObjectId(workspaceId)
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Edit,
|
||||
ProjectPermissionSub.Identity
|
||||
);
|
||||
|
||||
let identityMembership = await IdentityMembership
|
||||
.findOne({
|
||||
identity: new Types.ObjectId(identityId),
|
||||
workspace: new Types.ObjectId(workspaceId)
|
||||
})
|
||||
.populate<{
|
||||
identity: IIdentity,
|
||||
customRole: IRole
|
||||
}>("identity customRole");
|
||||
|
||||
if (!identityMembership) throw BadRequestError({
|
||||
message: `Identity with id ${identityId} does not exist in project with id ${workspaceId}`
|
||||
});
|
||||
|
||||
const identityRolePermission = await getWorkspaceRolePermissions(
|
||||
identityMembership?.customRole?.slug ?? identityMembership.role,
|
||||
identityMembership.workspace.toString()
|
||||
);
|
||||
const isAsPrivilegedAsIdentity = isAtLeastAsPrivilegedWorkspace(permission, identityRolePermission);
|
||||
if (!isAsPrivilegedAsIdentity) throw ForbiddenRequestError({
|
||||
message: "Failed to update role of more privileged identity"
|
||||
});
|
||||
|
||||
const rolePermission = await getWorkspaceRolePermissions(role, workspaceId);
|
||||
const isAsPrivilegedAsIntendedRole = isAtLeastAsPrivilegedWorkspace(permission, rolePermission);
|
||||
|
||||
if (!isAsPrivilegedAsIntendedRole) throw ForbiddenRequestError({
|
||||
message: "Failed to update identity to a more privileged role"
|
||||
});
|
||||
|
||||
let customRole;
|
||||
if (role) {
|
||||
const isCustomRole = ![ADMIN, MEMBER, VIEWER, NO_ACCESS].includes(role);
|
||||
if (isCustomRole) {
|
||||
customRole = await Role.findOne({
|
||||
slug: role,
|
||||
isOrgRole: false,
|
||||
workspace: new Types.ObjectId(workspaceId)
|
||||
});
|
||||
|
||||
if (!customRole) throw BadRequestError({ message: "Role not found" });
|
||||
}
|
||||
}
|
||||
|
||||
identityMembership = await IdentityMembership.findOneAndUpdate(
|
||||
{
|
||||
identity: identityMembership.identity._id,
|
||||
workspace: new Types.ObjectId(workspaceId),
|
||||
},
|
||||
{
|
||||
role: customRole ? CUSTOM : role,
|
||||
customRole
|
||||
},
|
||||
{
|
||||
new: true
|
||||
}
|
||||
);
|
||||
|
||||
return res.status(200).send({
|
||||
identityMembership
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete identity with id [identityId] from workspace
|
||||
* with id [workspaceId]
|
||||
* @param req
|
||||
* @param res
|
||||
*/
|
||||
export const deleteIdentityFromWorkspace = async (req: Request, res: Response) => {
|
||||
/*
|
||||
#swagger.summary = 'Delete project identity membership'
|
||||
#swagger.description = 'Delete project identity membership'
|
||||
|
||||
#swagger.security = [{
|
||||
"bearerAuth": []
|
||||
}]
|
||||
|
||||
#swagger.parameters['workspaceId'] = {
|
||||
"description": "ID of project",
|
||||
"required": true,
|
||||
"type": "string"
|
||||
}
|
||||
|
||||
#swagger.parameters['identityId'] = {
|
||||
"description": "ID of identity whose membership to delete in project",
|
||||
"required": true,
|
||||
"type": "string"
|
||||
}
|
||||
|
||||
#swagger.responses[200] = {
|
||||
content: {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"identityMembership": {
|
||||
$ref: "#/components/schemas/IdentityMembership",
|
||||
"description": "Deleted identity membership"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
*/
|
||||
const {
|
||||
params: { workspaceId, identityId }
|
||||
} = await validateRequest(reqValidator.DeleteIdentityFromWorkspaceV2, req);
|
||||
|
||||
const { permission } = await getAuthDataProjectPermissions({
|
||||
authData: req.authData,
|
||||
workspaceId: new Types.ObjectId(workspaceId)
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Delete,
|
||||
ProjectPermissionSub.Identity
|
||||
);
|
||||
|
||||
const identityMembership = await IdentityMembership
|
||||
.findOne({
|
||||
identity: new Types.ObjectId(identityId),
|
||||
workspace: new Types.ObjectId(workspaceId)
|
||||
})
|
||||
.populate<{
|
||||
identity: IIdentity,
|
||||
customRole: IRole
|
||||
}>("identity customRole");
|
||||
|
||||
if (!identityMembership) throw ResourceNotFoundError({
|
||||
message: `Identity with id ${identityId} does not exist in project with id ${workspaceId}`
|
||||
});
|
||||
|
||||
const identityRolePermission = await getWorkspaceRolePermissions(
|
||||
identityMembership?.customRole?.slug ?? identityMembership.role,
|
||||
identityMembership.workspace.toString()
|
||||
);
|
||||
const isAsPrivilegedAsIdentity = isAtLeastAsPrivilegedWorkspace(permission, identityRolePermission);
|
||||
if (!isAsPrivilegedAsIdentity) throw ForbiddenRequestError({
|
||||
message: "Failed to remove more privileged identity from project"
|
||||
});
|
||||
|
||||
await IdentityMembership.findByIdAndDelete(identityMembership._id);
|
||||
|
||||
return res.status(200).send({
|
||||
identityMembership
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Return list of identity memberships for workspace with id [workspaceId]
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
export const getWorkspaceIdentityMemberships = async (req: Request, res: Response) => {
|
||||
/*
|
||||
#swagger.summary = 'Return project identity memberships'
|
||||
#swagger.description = 'Return project identity memberships'
|
||||
|
||||
#swagger.security = [{
|
||||
"bearerAuth": []
|
||||
}]
|
||||
|
||||
#swagger.parameters['workspaceId'] = {
|
||||
"description": "ID of project",
|
||||
"required": true,
|
||||
"type": "string",
|
||||
"in": "path"
|
||||
}
|
||||
|
||||
#swagger.responses[200] = {
|
||||
content: {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"identityMemberships": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
$ref: "#/components/schemas/IdentityMembership"
|
||||
},
|
||||
"description": "Identity memberships of project"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
*/
|
||||
const {
|
||||
params: { workspaceId }
|
||||
} = await validateRequest(reqValidator.GetWorkspaceIdentityMembersV2, req);
|
||||
|
||||
const { permission } = await getAuthDataProjectPermissions({
|
||||
authData: req.authData,
|
||||
workspaceId: new Types.ObjectId(workspaceId)
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Read,
|
||||
ProjectPermissionSub.Identity
|
||||
);
|
||||
|
||||
const identityMemberships = await IdentityMembership.find({
|
||||
workspace: new Types.ObjectId(workspaceId)
|
||||
}).populate("identity customRole");
|
||||
|
||||
return res.status(200).send({
|
||||
identityMemberships
|
||||
});
|
||||
}
|
@ -1,224 +0,0 @@
|
||||
/* eslint-disable @typescript-eslint/no-var-requires */
|
||||
import { Request, Response } from "express";
|
||||
import jwt from "jsonwebtoken";
|
||||
import * as bigintConversion from "bigint-conversion";
|
||||
const jsrp = require("jsrp");
|
||||
import { LoginSRPDetail, User } from "../../models";
|
||||
import { createToken, issueAuthTokens, validateProviderAuthToken } from "../../helpers/auth";
|
||||
import { checkUserDevice } from "../../helpers/user";
|
||||
import { sendMail } from "../../helpers/nodemailer";
|
||||
import { TokenService } from "../../services";
|
||||
import { BadRequestError, InternalServerError } from "../../utils/errors";
|
||||
import { AuthTokenType, TOKEN_EMAIL_MFA } from "../../variables";
|
||||
import { getAuthSecret, getHttpsEnabled, getJwtMfaLifetime } from "../../config";
|
||||
import { AuthMethod } from "../../models/user";
|
||||
import { validateRequest } from "../../helpers/validation";
|
||||
import * as reqValidator from "../../validation/auth";
|
||||
|
||||
declare module "jsonwebtoken" {
|
||||
export interface ProviderAuthJwtPayload extends jwt.JwtPayload {
|
||||
userId: string;
|
||||
email: string;
|
||||
authProvider: AuthMethod;
|
||||
isUserCompleted: boolean;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Log in user step 1: Return [salt] and [serverPublicKey] as part of step 1 of SRP protocol
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
export const login1 = async (req: Request, res: Response) => {
|
||||
const {
|
||||
body: { email, clientPublicKey, providerAuthToken }
|
||||
} = await validateRequest(reqValidator.Login1V3, req);
|
||||
|
||||
const user = await User.findOne({
|
||||
email
|
||||
}).select("+salt +verifier");
|
||||
|
||||
if (!user) throw new Error("Failed to find user");
|
||||
|
||||
if (!user.authMethods.includes(AuthMethod.EMAIL)) {
|
||||
await validateProviderAuthToken({
|
||||
email,
|
||||
providerAuthToken
|
||||
});
|
||||
}
|
||||
|
||||
const server = new jsrp.server();
|
||||
server.init(
|
||||
{
|
||||
salt: user.salt,
|
||||
verifier: user.verifier
|
||||
},
|
||||
async () => {
|
||||
// generate server-side public key
|
||||
const serverPublicKey = server.getPublicKey();
|
||||
await LoginSRPDetail.findOneAndReplace(
|
||||
{
|
||||
email: email
|
||||
},
|
||||
{
|
||||
email,
|
||||
userId: user.id,
|
||||
clientPublicKey: clientPublicKey,
|
||||
serverBInt: bigintConversion.bigintToBuf(server.bInt)
|
||||
},
|
||||
{ upsert: true, returnNewDocument: false }
|
||||
);
|
||||
|
||||
return res.status(200).send({
|
||||
serverPublicKey,
|
||||
salt: user.salt
|
||||
});
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Log in user step 2: complete step 2 of SRP protocol and return token and their (encrypted)
|
||||
* private key
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
export const login2 = async (req: Request, res: Response) => {
|
||||
if (!req.headers["user-agent"])
|
||||
throw InternalServerError({ message: "User-Agent header is required" });
|
||||
|
||||
const {
|
||||
body: { email, providerAuthToken, clientProof }
|
||||
} = await validateRequest(reqValidator.Login2V3, req);
|
||||
|
||||
const user = await User.findOne({
|
||||
email
|
||||
}).select(
|
||||
"+salt +verifier +encryptionVersion +protectedKey +protectedKeyIV +protectedKeyTag +publicKey +encryptedPrivateKey +iv +tag +devices"
|
||||
);
|
||||
|
||||
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 (!loginSRPDetail) {
|
||||
return BadRequestError(Error("Failed to find login details for SRP"));
|
||||
}
|
||||
|
||||
const server = new jsrp.server();
|
||||
server.init(
|
||||
{
|
||||
salt: user.salt,
|
||||
verifier: user.verifier,
|
||||
b: loginSRPDetail.serverBInt
|
||||
},
|
||||
async () => {
|
||||
server.setClientPublicKey(loginSRPDetail.clientPublicKey);
|
||||
|
||||
// compare server and client shared keys
|
||||
if (server.checkClientProof(clientProof)) {
|
||||
if (user.isMfaEnabled) {
|
||||
// case: user has MFA enabled
|
||||
|
||||
// generate temporary MFA token
|
||||
const token = createToken({
|
||||
payload: {
|
||||
authTokenType: AuthTokenType.MFA_TOKEN,
|
||||
userId: user._id.toString()
|
||||
},
|
||||
expiresIn: await getJwtMfaLifetime(),
|
||||
secret: await getAuthSecret()
|
||||
});
|
||||
|
||||
const code = await TokenService.createToken({
|
||||
type: TOKEN_EMAIL_MFA,
|
||||
email
|
||||
});
|
||||
|
||||
// send MFA code [code] to [email]
|
||||
await sendMail({
|
||||
template: "emailMfa.handlebars",
|
||||
subjectLine: "Infisical MFA code",
|
||||
recipients: [user.email],
|
||||
substitutions: {
|
||||
code
|
||||
}
|
||||
});
|
||||
|
||||
return res.status(200).send({
|
||||
mfaEnabled: true,
|
||||
token
|
||||
});
|
||||
}
|
||||
|
||||
await checkUserDevice({
|
||||
user,
|
||||
ip: req.realIP,
|
||||
userAgent: req.headers["user-agent"] ?? ""
|
||||
});
|
||||
|
||||
// issue tokens
|
||||
const tokens = await issueAuthTokens({
|
||||
userId: user._id,
|
||||
ip: req.realIP,
|
||||
userAgent: req.headers["user-agent"] ?? ""
|
||||
});
|
||||
|
||||
// store (refresh) token in httpOnly cookie
|
||||
res.cookie("jid", tokens.refreshToken, {
|
||||
httpOnly: true,
|
||||
path: "/",
|
||||
sameSite: "strict",
|
||||
secure: await getHttpsEnabled()
|
||||
});
|
||||
|
||||
// case: user does not have MFA enablgged
|
||||
// return (access) token in response
|
||||
|
||||
interface ResponseData {
|
||||
mfaEnabled: boolean;
|
||||
encryptionVersion: any;
|
||||
protectedKey?: string;
|
||||
protectedKeyIV?: string;
|
||||
protectedKeyTag?: string;
|
||||
token: string;
|
||||
publicKey?: string;
|
||||
encryptedPrivateKey?: string;
|
||||
iv?: string;
|
||||
tag?: string;
|
||||
}
|
||||
|
||||
const response: ResponseData = {
|
||||
mfaEnabled: false,
|
||||
encryptionVersion: user.encryptionVersion,
|
||||
token: tokens.token,
|
||||
publicKey: user.publicKey,
|
||||
encryptedPrivateKey: user.encryptedPrivateKey,
|
||||
iv: user.iv,
|
||||
tag: user.tag
|
||||
};
|
||||
|
||||
if (user?.protectedKey && user?.protectedKeyIV && user?.protectedKeyTag) {
|
||||
response.protectedKey = user.protectedKey;
|
||||
response.protectedKeyIV = user.protectedKeyIV;
|
||||
response.protectedKeyTag = user.protectedKeyTag;
|
||||
}
|
||||
|
||||
return res.status(200).send(response);
|
||||
}
|
||||
|
||||
return res.status(400).send({
|
||||
message: "Failed to authenticate. Try again?"
|
||||
});
|
||||
}
|
||||
);
|
||||
};
|
@ -1,13 +0,0 @@
|
||||
import * as usersController from "./usersController";
|
||||
import * as secretsController from "./secretsController";
|
||||
import * as workspacesController from "./workspacesController";
|
||||
import * as authController from "./authController";
|
||||
import * as signupController from "./signupController";
|
||||
|
||||
export {
|
||||
usersController,
|
||||
authController,
|
||||
secretsController,
|
||||
signupController,
|
||||
workspacesController
|
||||
}
|
File diff suppressed because it is too large
Load Diff
@ -1,193 +0,0 @@
|
||||
import jwt from "jsonwebtoken";
|
||||
import { Request, Response } from "express";
|
||||
import * as Sentry from "@sentry/node";
|
||||
import { MembershipOrg, User } from "../../models";
|
||||
import { completeAccount } from "../../helpers/user";
|
||||
import { initializeDefaultOrg } from "../../helpers/signup";
|
||||
import { issueAuthTokens, validateProviderAuthToken } from "../../helpers/auth";
|
||||
import { ACCEPTED, AuthTokenType, INVITED } from "../../variables";
|
||||
import { standardRequest } from "../../config/request";
|
||||
import { getAuthSecret, getHttpsEnabled, getLoopsApiKey } from "../../config";
|
||||
import { BadRequestError, UnauthorizedRequestError } from "../../utils/errors";
|
||||
import { TelemetryService } from "../../services";
|
||||
import { AuthMethod } from "../../models";
|
||||
import { validateRequest } from "../../helpers/validation";
|
||||
import * as reqValidator from "../../validation/auth";
|
||||
|
||||
/**
|
||||
* Complete setting up user by adding their personal and auth information as part of the
|
||||
* signup flow
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
export const completeAccountSignup = async (req: Request, res: Response) => {
|
||||
let user, token;
|
||||
try {
|
||||
const {
|
||||
body: {
|
||||
email,
|
||||
publicKey,
|
||||
salt,
|
||||
lastName,
|
||||
verifier,
|
||||
firstName,
|
||||
protectedKey,
|
||||
protectedKeyIV,
|
||||
protectedKeyTag,
|
||||
organizationName,
|
||||
providerAuthToken,
|
||||
attributionSource,
|
||||
encryptedPrivateKey,
|
||||
encryptedPrivateKeyIV,
|
||||
encryptedPrivateKeyTag
|
||||
}
|
||||
} = await validateRequest(reqValidator.CompletedAccountSignupV3, req);
|
||||
|
||||
user = await User.findOne({ email });
|
||||
|
||||
if (!user || (user && user?.publicKey)) {
|
||||
// case 1: user doesn't exist.
|
||||
// case 2: user has already completed account
|
||||
return res.status(403).send({
|
||||
error: "Failed to complete account for complete user"
|
||||
});
|
||||
}
|
||||
|
||||
if (providerAuthToken) {
|
||||
await validateProviderAuthToken({
|
||||
email,
|
||||
providerAuthToken
|
||||
});
|
||||
} else {
|
||||
const [AUTH_TOKEN_TYPE, AUTH_TOKEN_VALUE] = <[string, string]>(
|
||||
req.headers["authorization"]?.split(" ", 2)
|
||||
) ?? [null, null];
|
||||
if (AUTH_TOKEN_TYPE === null) {
|
||||
throw BadRequestError({ message: "Missing Authorization Header in the request header." });
|
||||
}
|
||||
if (AUTH_TOKEN_TYPE.toLowerCase() !== "bearer") {
|
||||
throw BadRequestError({
|
||||
message: `The provided authentication type '${AUTH_TOKEN_TYPE}' is not supported.`
|
||||
});
|
||||
}
|
||||
if (AUTH_TOKEN_VALUE === null) {
|
||||
throw BadRequestError({
|
||||
message: "Missing Authorization Body in the request header"
|
||||
});
|
||||
}
|
||||
|
||||
const decodedToken = <jwt.UserIDJwtPayload>(
|
||||
jwt.verify(AUTH_TOKEN_VALUE, await getAuthSecret())
|
||||
);
|
||||
|
||||
if (decodedToken.authTokenType !== AuthTokenType.SIGNUP_TOKEN) throw UnauthorizedRequestError();
|
||||
if (decodedToken.userId !== user.id) throw UnauthorizedRequestError();
|
||||
}
|
||||
|
||||
// complete setting up user's account
|
||||
user = await completeAccount({
|
||||
userId: user._id.toString(),
|
||||
firstName,
|
||||
lastName,
|
||||
encryptionVersion: 2,
|
||||
protectedKey,
|
||||
protectedKeyIV,
|
||||
protectedKeyTag,
|
||||
publicKey,
|
||||
encryptedPrivateKey,
|
||||
encryptedPrivateKeyIV,
|
||||
encryptedPrivateKeyTag,
|
||||
salt,
|
||||
verifier
|
||||
});
|
||||
|
||||
if (!user) throw new Error("Failed to complete account for non-existent user"); // ensure user is non-null
|
||||
|
||||
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
|
||||
// invited to completed with user attached
|
||||
await MembershipOrg.updateMany(
|
||||
{
|
||||
inviteEmail: email,
|
||||
status: INVITED
|
||||
},
|
||||
{
|
||||
user,
|
||||
status: ACCEPTED
|
||||
}
|
||||
);
|
||||
|
||||
// issue tokens
|
||||
const tokens = await issueAuthTokens({
|
||||
userId: user._id,
|
||||
ip: req.realIP,
|
||||
userAgent: req.headers["user-agent"] ?? ""
|
||||
});
|
||||
|
||||
token = tokens.token;
|
||||
|
||||
// sending a welcome email to new users
|
||||
if (await getLoopsApiKey()) {
|
||||
await standardRequest.post(
|
||||
"https://app.loops.so/api/v1/events/send",
|
||||
{
|
||||
email: email,
|
||||
eventName: "Sign Up",
|
||||
firstName: firstName,
|
||||
lastName: lastName
|
||||
},
|
||||
{
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
Authorization: "Bearer " + (await getLoopsApiKey())
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// store (refresh) token in httpOnly cookie
|
||||
res.cookie("jid", tokens.refreshToken, {
|
||||
httpOnly: true,
|
||||
path: "/",
|
||||
sameSite: "strict",
|
||||
secure: await getHttpsEnabled()
|
||||
});
|
||||
|
||||
const postHogClient = await TelemetryService.getPostHogClient();
|
||||
if (postHogClient) {
|
||||
postHogClient.capture({
|
||||
event: "User Signed Up",
|
||||
distinctId: email,
|
||||
properties: {
|
||||
email,
|
||||
...(attributionSource ? { attributionSource } : {})
|
||||
}
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
Sentry.setUser(null);
|
||||
Sentry.captureException(err);
|
||||
return res.status(400).send({
|
||||
message: "Failed to complete account setup"
|
||||
});
|
||||
}
|
||||
|
||||
return res.status(200).send({
|
||||
message: "Successfully set up account",
|
||||
user,
|
||||
token
|
||||
});
|
||||
};
|
@ -1,18 +0,0 @@
|
||||
import { Request, Response } from "express";
|
||||
import { APIKeyDataV2 } from "../../models";
|
||||
|
||||
/**
|
||||
* Return API keys belonging to current user.
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
export const getMyAPIKeys = async (req: Request, res: Response) => {
|
||||
const apiKeyData = await APIKeyDataV2.find({
|
||||
user: req.user._id
|
||||
});
|
||||
|
||||
return res.status(200).send({
|
||||
apiKeyData
|
||||
});
|
||||
}
|
@ -1,142 +0,0 @@
|
||||
import { Request, Response } from "express";
|
||||
import { Types } from "mongoose";
|
||||
import { validateRequest } from "../../helpers/validation";
|
||||
import { Membership, Secret, User } from "../../models";
|
||||
import { SecretService } from "../../services";
|
||||
import { getAuthDataProjectPermissions } from "../../ee/services/ProjectRoleService";
|
||||
import { UnauthorizedRequestError } from "../../utils/errors";
|
||||
import * as reqValidator from "../../validation/workspace";
|
||||
|
||||
/**
|
||||
* Return whether or not all secrets in workspace with id [workspaceId]
|
||||
* are blind-indexed
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
export const getWorkspaceBlindIndexStatus = async (req: Request, res: Response) => {
|
||||
const {
|
||||
params: { workspaceId }
|
||||
} = await validateRequest(reqValidator.GetWorkspaceBlinkIndexStatusV3, req);
|
||||
|
||||
await getAuthDataProjectPermissions({
|
||||
authData: req.authData,
|
||||
workspaceId: new Types.ObjectId(workspaceId)
|
||||
});
|
||||
|
||||
if (req.authData.authPayload instanceof User) {
|
||||
const membership = await Membership.findOne({
|
||||
user: req.authData.authPayload._id,
|
||||
workspace: new Types.ObjectId(workspaceId)
|
||||
});
|
||||
|
||||
if (!membership) throw UnauthorizedRequestError();
|
||||
|
||||
if (membership.role !== "admin")
|
||||
throw UnauthorizedRequestError({ message: "User must be an admin" });
|
||||
}
|
||||
|
||||
const secretsWithoutBlindIndex = await Secret.countDocuments({
|
||||
workspace: new Types.ObjectId(workspaceId),
|
||||
secretBlindIndex: {
|
||||
$exists: false
|
||||
}
|
||||
});
|
||||
|
||||
return res.status(200).send(secretsWithoutBlindIndex === 0);
|
||||
};
|
||||
|
||||
/**
|
||||
* Get all secrets for workspace with id [workspaceId]
|
||||
*/
|
||||
export const getWorkspaceSecrets = async (req: Request, res: Response) => {
|
||||
const {
|
||||
params: { workspaceId }
|
||||
} = await validateRequest(reqValidator.GetWorkspaceSecretsV3, req);
|
||||
|
||||
await getAuthDataProjectPermissions({
|
||||
authData: req.authData,
|
||||
workspaceId: new Types.ObjectId(workspaceId)
|
||||
});
|
||||
|
||||
if (req.authData.authPayload instanceof User) {
|
||||
const membership = await Membership.findOne({
|
||||
user: req.authData.authPayload._id,
|
||||
workspace: new Types.ObjectId(workspaceId)
|
||||
});
|
||||
|
||||
if (!membership) throw UnauthorizedRequestError();
|
||||
|
||||
if (membership.role !== "admin")
|
||||
throw UnauthorizedRequestError({ message: "User must be an admin" });
|
||||
}
|
||||
|
||||
const secrets = await Secret.find({
|
||||
workspace: new Types.ObjectId(workspaceId)
|
||||
});
|
||||
|
||||
return res.status(200).send({
|
||||
secrets
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Update blind indices for secrets in workspace with id [workspaceId]
|
||||
* @param req
|
||||
* @param res
|
||||
*/
|
||||
export const nameWorkspaceSecrets = async (req: Request, res: Response) => {
|
||||
const {
|
||||
params: { workspaceId },
|
||||
body: { secretsToUpdate }
|
||||
} = await validateRequest(reqValidator.NameWorkspaceSecretsV3, req);
|
||||
|
||||
await getAuthDataProjectPermissions({
|
||||
authData: req.authData,
|
||||
workspaceId: new Types.ObjectId(workspaceId)
|
||||
});
|
||||
|
||||
if (req.authData.authPayload instanceof User) {
|
||||
const membership = await Membership.findOne({
|
||||
user: req.authData.authPayload._id,
|
||||
workspace: new Types.ObjectId(workspaceId)
|
||||
});
|
||||
|
||||
if (!membership) throw UnauthorizedRequestError();
|
||||
|
||||
if (membership.role !== "admin")
|
||||
throw UnauthorizedRequestError({ message: "User must be an admin" });
|
||||
}
|
||||
|
||||
// get secret blind index salt
|
||||
const salt = await SecretService.getSecretBlindIndexSalt({
|
||||
workspaceId: new Types.ObjectId(workspaceId)
|
||||
});
|
||||
|
||||
// update secret blind indices
|
||||
const operations = await Promise.all(
|
||||
secretsToUpdate.map(async (secretToUpdate) => {
|
||||
const secretBlindIndex = await SecretService.generateSecretBlindIndexWithSalt({
|
||||
secretName: secretToUpdate.secretName,
|
||||
salt
|
||||
});
|
||||
|
||||
return {
|
||||
updateOne: {
|
||||
filter: {
|
||||
_id: new Types.ObjectId(secretToUpdate._id)
|
||||
},
|
||||
update: {
|
||||
secretBlindIndex
|
||||
}
|
||||
}
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
await Secret.bulkWrite(operations);
|
||||
|
||||
return res.status(200).send({
|
||||
message: "Successfully named workspace secrets"
|
||||
});
|
||||
};
|
File diff suppressed because it is too large
Load Diff
@ -1,36 +0,0 @@
|
||||
The Infisical Enterprise license (the “Enterprise License”)
|
||||
Copyright (c) 2022 Infisical Inc
|
||||
|
||||
With regard to the Infisical Software:
|
||||
|
||||
This software and associated documentation files (the "Software") may only be
|
||||
used in production, if you (and any entity that you represent) have agreed to,
|
||||
and are in compliance with, the Infisical Subscription Terms of Service, available
|
||||
at https://infisical.com/terms (the “Enterprise Terms”), or other
|
||||
agreement governing the use of the Software, as agreed by you and Infisical,
|
||||
and otherwise have a valid Infisical Enterprise License for the
|
||||
correct number of user seats. Subject to the foregoing sentence, you are free to
|
||||
modify this Software and publish patches to the Software. You agree that Infisical
|
||||
and/or its licensors (as applicable) retain all right, title and interest in and
|
||||
to all such modifications and/or patches, and all such modifications and/or
|
||||
patches may only be used, copied, modified, displayed, distributed, or otherwise
|
||||
exploited with a valid Infiscial Enterprise subscription for the correct
|
||||
number of user seats. Notwithstanding the foregoing, you may copy and modify
|
||||
the Software for development and testing purposes, without requiring a
|
||||
subscription. You agree that Infisical and/or its licensors (as applicable) retain
|
||||
all right, title and interest in and to all such modifications. You are not
|
||||
granted any other rights beyond what is expressly stated herein. Subject to the
|
||||
foregoing, it is forbidden to copy, merge, publish, distribute, sublicense,
|
||||
and/or sell the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
For all third party components incorporated into the Infisical Software, those
|
||||
components are licensed under the original license provided by the owner of the
|
||||
applicable component.
|
@ -1,32 +0,0 @@
|
||||
import { Request, Response } from "express";
|
||||
import { EELicenseService } from "../../services";
|
||||
import { getLicenseServerUrl } from "../../../config";
|
||||
import { licenseServerKeyRequest } from "../../../config/request";
|
||||
import { validateRequest } from "../../../helpers/validation";
|
||||
import * as reqValidator from "../../../validation/cloudProducts";
|
||||
|
||||
/**
|
||||
* Return available cloud product information.
|
||||
* Note: Nicely formatted to easily construct a table from
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
export const getCloudProducts = async (req: Request, res: Response) => {
|
||||
const {
|
||||
query: { "billing-cycle": billingCycle }
|
||||
} = await validateRequest(reqValidator.GetCloudProductsV1, req);
|
||||
|
||||
if (EELicenseService.instanceType === "cloud") {
|
||||
const { data } = await licenseServerKeyRequest.get(
|
||||
`${await getLicenseServerUrl()}/api/license-server/v1/cloud-products?billing-cycle=${billingCycle}`
|
||||
);
|
||||
|
||||
return res.status(200).send(data);
|
||||
}
|
||||
|
||||
return res.status(200).send({
|
||||
head: [],
|
||||
rows: []
|
||||
});
|
||||
};
|
@ -1,460 +0,0 @@
|
||||
import { Request, Response } from "express";
|
||||
import { Types } from "mongoose";
|
||||
import {
|
||||
IIdentity,
|
||||
Identity,
|
||||
IdentityAccessToken,
|
||||
IdentityMembership,
|
||||
IdentityMembershipOrg,
|
||||
IdentityUniversalAuth,
|
||||
IdentityUniversalAuthClientSecret,
|
||||
Organization
|
||||
} from "../../../models";
|
||||
import {
|
||||
EventType,
|
||||
IRole,
|
||||
Role
|
||||
} from "../../models";
|
||||
import { validateRequest } from "../../../helpers/validation";
|
||||
import * as reqValidator from "../../../validation/identities";
|
||||
import {
|
||||
getAuthDataOrgPermissions,
|
||||
getOrgRolePermissions,
|
||||
isAtLeastAsPrivilegedOrg
|
||||
} from "../../services/RoleService";
|
||||
import {
|
||||
BadRequestError,
|
||||
ForbiddenRequestError,
|
||||
ResourceNotFoundError,
|
||||
} from "../../../utils/errors";
|
||||
import { ADMIN, CUSTOM, MEMBER, NO_ACCESS } from "../../../variables";
|
||||
import {
|
||||
OrgPermissionActions,
|
||||
OrgPermissionSubjects
|
||||
} from "../../services/RoleService";
|
||||
import { EEAuditLogService } from "../../services";
|
||||
import { ForbiddenError } from "@casl/ability";
|
||||
|
||||
/**
|
||||
* Create identity
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
export const createIdentity = async (req: Request, res: Response) => {
|
||||
/*
|
||||
#swagger.summary = 'Create identity'
|
||||
#swagger.description = 'Create identity'
|
||||
|
||||
#swagger.security = [{
|
||||
"bearerAuth": []
|
||||
}]
|
||||
|
||||
#swagger.requestBody = {
|
||||
content: {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "Name of entity to create",
|
||||
"example": "development"
|
||||
},
|
||||
"organizationId": {
|
||||
"type": "string",
|
||||
"description": "ID of organization where to create identity",
|
||||
"example": "dev-environment"
|
||||
},
|
||||
"role": {
|
||||
"type": "string",
|
||||
"description": "Role to assume for organization membership",
|
||||
"example": "no-access"
|
||||
}
|
||||
},
|
||||
"required": ["name", "organizationId", "role"]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#swagger.responses[200] = {
|
||||
content: {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"identity": {
|
||||
$ref: '#/definitions/Identity'
|
||||
}
|
||||
},
|
||||
"description": "Details of the created identity"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
*/
|
||||
const {
|
||||
body: {
|
||||
name,
|
||||
organizationId,
|
||||
role
|
||||
}
|
||||
} = await validateRequest(reqValidator.CreateIdentityV1, req);
|
||||
|
||||
const { permission } = await getAuthDataOrgPermissions({
|
||||
authData: req.authData,
|
||||
organizationId: new Types.ObjectId(organizationId)
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
OrgPermissionActions.Create,
|
||||
OrgPermissionSubjects.Identity
|
||||
);
|
||||
|
||||
const rolePermission = await getOrgRolePermissions(role, organizationId);
|
||||
const hasRequiredPrivileges = isAtLeastAsPrivilegedOrg(permission, rolePermission);
|
||||
|
||||
if (!hasRequiredPrivileges) throw ForbiddenRequestError({
|
||||
message: "Failed to create a more privileged identity"
|
||||
});
|
||||
|
||||
const organization = await Organization.findById(organizationId);
|
||||
if (!organization) throw BadRequestError({ message: `Organization with id ${organizationId} not found` });
|
||||
|
||||
const isCustomRole = ![ADMIN, MEMBER, NO_ACCESS].includes(role);
|
||||
|
||||
let customRole;
|
||||
if (isCustomRole) {
|
||||
customRole = await Role.findOne({
|
||||
slug: role,
|
||||
isOrgRole: true,
|
||||
organization: new Types.ObjectId(organizationId)
|
||||
});
|
||||
|
||||
if (!customRole) throw BadRequestError({ message: "Role not found" });
|
||||
}
|
||||
|
||||
const identity = await new Identity({
|
||||
name
|
||||
}).save();
|
||||
|
||||
await new IdentityMembershipOrg({
|
||||
identity: identity._id,
|
||||
organization: new Types.ObjectId(organizationId),
|
||||
role: isCustomRole ? CUSTOM : role,
|
||||
customRole
|
||||
}).save();
|
||||
|
||||
await EEAuditLogService.createAuditLog(
|
||||
req.authData,
|
||||
{
|
||||
type: EventType.CREATE_IDENTITY,
|
||||
metadata: {
|
||||
identityId: identity._id.toString(),
|
||||
name
|
||||
}
|
||||
},
|
||||
{
|
||||
organizationId: new Types.ObjectId(organizationId)
|
||||
}
|
||||
);
|
||||
|
||||
return res.status(200).send({
|
||||
identity
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Update identity with id [identityId]
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
export const updateIdentity = async (req: Request, res: Response) => {
|
||||
/*
|
||||
#swagger.summary = 'Update identity'
|
||||
#swagger.description = 'Update identity'
|
||||
|
||||
#swagger.security = [{
|
||||
"bearerAuth": []
|
||||
}]
|
||||
|
||||
#swagger.parameters['identityId'] = {
|
||||
"description": "ID of identity to update",
|
||||
"required": true,
|
||||
"type": "string",
|
||||
"in": "path"
|
||||
}
|
||||
|
||||
#swagger.requestBody = {
|
||||
content: {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "Name of entity to update to",
|
||||
"example": "development"
|
||||
},
|
||||
"role": {
|
||||
"type": "string",
|
||||
"description": "Role to update to for organization membership",
|
||||
"example": "no-access"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#swagger.responses[200] = {
|
||||
content: {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"identity": {
|
||||
$ref: '#/definitions/Identity'
|
||||
}
|
||||
},
|
||||
"description": "Details of the updated identity"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
*/
|
||||
const {
|
||||
params: { identityId },
|
||||
body: {
|
||||
name,
|
||||
role
|
||||
}
|
||||
} = await validateRequest(reqValidator.UpdateIdentityV1, req);
|
||||
|
||||
const identityMembershipOrg = await IdentityMembershipOrg
|
||||
.findOne({
|
||||
identity: new Types.ObjectId(identityId)
|
||||
})
|
||||
.populate<{
|
||||
identity: IIdentity,
|
||||
customRole: IRole
|
||||
}>("identity customRole");
|
||||
|
||||
if (!identityMembershipOrg) throw ResourceNotFoundError({
|
||||
message: `Failed to find identity with id ${identityId}`
|
||||
});
|
||||
|
||||
const { permission } = await getAuthDataOrgPermissions({
|
||||
authData: req.authData,
|
||||
organizationId: identityMembershipOrg.organization
|
||||
});
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
OrgPermissionActions.Edit,
|
||||
OrgPermissionSubjects.Identity
|
||||
);
|
||||
|
||||
const identityRolePermission = await getOrgRolePermissions(
|
||||
identityMembershipOrg?.customRole?.slug ?? identityMembershipOrg.role,
|
||||
identityMembershipOrg.organization.toString()
|
||||
);
|
||||
const hasRequiredPrivileges = isAtLeastAsPrivilegedOrg(permission, identityRolePermission);
|
||||
if (!hasRequiredPrivileges) throw ForbiddenRequestError({
|
||||
message: "Failed to update more privileged identity"
|
||||
});
|
||||
|
||||
if (role) {
|
||||
const rolePermission = await getOrgRolePermissions(role, identityMembershipOrg.organization.toString());
|
||||
const hasRequiredPrivileges = isAtLeastAsPrivilegedOrg(permission, rolePermission);
|
||||
|
||||
if (!hasRequiredPrivileges) throw ForbiddenRequestError({
|
||||
message: "Failed to update identity to a more privileged role"
|
||||
});
|
||||
}
|
||||
|
||||
let customRole;
|
||||
if (role) {
|
||||
const isCustomRole = ![ADMIN, MEMBER, NO_ACCESS].includes(role);
|
||||
if (isCustomRole) {
|
||||
customRole = await Role.findOne({
|
||||
slug: role,
|
||||
isOrgRole: true,
|
||||
organization: identityMembershipOrg.organization
|
||||
});
|
||||
|
||||
if (!customRole) throw BadRequestError({ message: "Role not found" });
|
||||
}
|
||||
}
|
||||
|
||||
const identity = await Identity.findByIdAndUpdate(
|
||||
identityId,
|
||||
{
|
||||
name,
|
||||
},
|
||||
{
|
||||
new: true
|
||||
}
|
||||
);
|
||||
|
||||
if (!identity) throw BadRequestError({
|
||||
message: `Failed to update identity with id ${identityId}`
|
||||
});
|
||||
|
||||
await IdentityMembershipOrg.findOneAndUpdate(
|
||||
{
|
||||
identity: identity._id
|
||||
},
|
||||
{
|
||||
role: customRole ? CUSTOM : role,
|
||||
...(customRole ? {
|
||||
customRole
|
||||
} : {}),
|
||||
...(role && !customRole ? { // non-custom role
|
||||
$unset: {
|
||||
customRole: 1
|
||||
}
|
||||
} : {})
|
||||
},
|
||||
{
|
||||
new: true
|
||||
}
|
||||
);
|
||||
|
||||
await EEAuditLogService.createAuditLog(
|
||||
req.authData,
|
||||
{
|
||||
type: EventType.UPDATE_IDENTITY,
|
||||
metadata: {
|
||||
identityId: identity._id.toString(),
|
||||
name: identity.name,
|
||||
}
|
||||
},
|
||||
{
|
||||
organizationId: identityMembershipOrg.organization
|
||||
}
|
||||
);
|
||||
|
||||
return res.status(200).send({
|
||||
identity
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete identity with id [identityId]
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
export const deleteIdentity = async (req: Request, res: Response) => {
|
||||
/*
|
||||
#swagger.summary = 'Delete identity'
|
||||
#swagger.description = 'Delete identity'
|
||||
|
||||
#swagger.security = [{
|
||||
"bearerAuth": []
|
||||
}]
|
||||
|
||||
#swagger.parameters['identityId'] = {
|
||||
"description": "ID of identity",
|
||||
"required": true,
|
||||
"type": "string",
|
||||
"in": "path"
|
||||
}
|
||||
|
||||
#swagger.responses[200] = {
|
||||
content: {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"identity": {
|
||||
$ref: '#/definitions/Identity'
|
||||
}
|
||||
},
|
||||
"description": "Details of the deleted identity"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
*/
|
||||
const {
|
||||
params: { identityId }
|
||||
} = await validateRequest(reqValidator.DeleteIdentityV1, req);
|
||||
|
||||
const identityMembershipOrg = await IdentityMembershipOrg
|
||||
.findOne({
|
||||
identity: new Types.ObjectId(identityId)
|
||||
})
|
||||
.populate<{
|
||||
identity: IIdentity,
|
||||
customRole: IRole
|
||||
}>("identity customRole");
|
||||
|
||||
if (!identityMembershipOrg) throw ResourceNotFoundError({
|
||||
message: `Failed to find identity with id ${identityId}`
|
||||
});
|
||||
|
||||
const { permission } = await getAuthDataOrgPermissions({
|
||||
authData: req.authData,
|
||||
organizationId: identityMembershipOrg.organization
|
||||
});
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
OrgPermissionActions.Delete,
|
||||
OrgPermissionSubjects.Identity
|
||||
);
|
||||
|
||||
const identityRolePermission = await getOrgRolePermissions(
|
||||
identityMembershipOrg?.customRole?.slug ?? identityMembershipOrg.role,
|
||||
identityMembershipOrg.organization.toString()
|
||||
);
|
||||
const hasRequiredPrivileges = isAtLeastAsPrivilegedOrg(permission, identityRolePermission);
|
||||
if (!hasRequiredPrivileges) throw ForbiddenRequestError({
|
||||
message: "Failed to delete more privileged identity"
|
||||
});
|
||||
|
||||
const identity = await Identity.findByIdAndDelete(identityMembershipOrg.identity);
|
||||
if (!identity) throw ResourceNotFoundError({
|
||||
message: `Identity with id ${identityId} not found`
|
||||
});
|
||||
|
||||
await IdentityMembershipOrg.findByIdAndDelete(identityMembershipOrg._id);
|
||||
|
||||
await IdentityMembership.deleteMany({
|
||||
identity: identityMembershipOrg.identity
|
||||
});
|
||||
|
||||
await IdentityUniversalAuth.deleteMany({
|
||||
identity: identityMembershipOrg.identity
|
||||
});
|
||||
|
||||
await IdentityUniversalAuthClientSecret.deleteMany({
|
||||
identity: identityMembershipOrg.identity
|
||||
});
|
||||
|
||||
await IdentityAccessToken.deleteMany({
|
||||
identity: identityMembershipOrg.identity
|
||||
});
|
||||
|
||||
await EEAuditLogService.createAuditLog(
|
||||
req.authData,
|
||||
{
|
||||
type: EventType.DELETE_IDENTITY,
|
||||
metadata: {
|
||||
identityId: identity._id.toString()
|
||||
}
|
||||
},
|
||||
{
|
||||
organizationId: identityMembershipOrg.organization
|
||||
}
|
||||
);
|
||||
|
||||
return res.status(200).send({
|
||||
identity
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
@ -1,31 +0,0 @@
|
||||
import * as identitiesController from "./identitiesController";
|
||||
import * as secretController from "./secretController";
|
||||
import * as secretSnapshotController from "./secretSnapshotController";
|
||||
import * as organizationsController from "./organizationsController";
|
||||
import * as ssoController from "./ssoController";
|
||||
import * as usersController from "./usersController";
|
||||
import * as workspaceController from "./workspaceController";
|
||||
import * as membershipController from "./membershipController";
|
||||
import * as cloudProductsController from "./cloudProductsController";
|
||||
import * as roleController from "./roleController";
|
||||
import * as secretApprovalPolicyController from "./secretApprovalPolicyController";
|
||||
import * as secretApprovalRequestController from "./secretApprovalRequestsController";
|
||||
import * as secretRotationProviderController from "./secretRotationProviderController";
|
||||
import * as secretRotationController from "./secretRotationController";
|
||||
|
||||
export {
|
||||
identitiesController,
|
||||
secretController,
|
||||
secretSnapshotController,
|
||||
organizationsController,
|
||||
ssoController,
|
||||
usersController,
|
||||
workspaceController,
|
||||
membershipController,
|
||||
cloudProductsController,
|
||||
roleController,
|
||||
secretApprovalPolicyController,
|
||||
secretApprovalRequestController,
|
||||
secretRotationProviderController,
|
||||
secretRotationController
|
||||
};
|
@ -1,86 +0,0 @@
|
||||
import { Request, Response } from "express";
|
||||
import { IUser, Membership, Workspace } from "../../../models";
|
||||
import { EventType } from "../../../ee/models";
|
||||
import { IMembershipPermission } from "../../../models/membership";
|
||||
import { BadRequestError, UnauthorizedRequestError } from "../../../utils/errors";
|
||||
import { ADMIN, MEMBER } from "../../../variables/organization";
|
||||
import { PERMISSION_READ_SECRETS, PERMISSION_WRITE_SECRETS } from "../../../variables";
|
||||
import _ from "lodash";
|
||||
import { EEAuditLogService } from "../../services";
|
||||
|
||||
export const denyMembershipPermissions = async (req: Request, res: Response) => {
|
||||
const { membershipId } = req.params;
|
||||
const { permissions } = req.body;
|
||||
const sanitizedMembershipPermissions: IMembershipPermission[] = permissions.map((permission: IMembershipPermission) => {
|
||||
if (!permission.ability || !permission.environmentSlug || ![PERMISSION_READ_SECRETS, PERMISSION_WRITE_SECRETS].includes(permission.ability)) {
|
||||
throw BadRequestError({ message: "One or more required fields are missing from the request or have incorrect type" })
|
||||
}
|
||||
|
||||
return {
|
||||
environmentSlug: permission.environmentSlug,
|
||||
ability: permission.ability
|
||||
}
|
||||
})
|
||||
|
||||
const sanitizedMembershipPermissionsUnique = _.uniqWith(sanitizedMembershipPermissions, _.isEqual)
|
||||
|
||||
const membershipToModify = await Membership.findById(membershipId)
|
||||
if (!membershipToModify) {
|
||||
throw BadRequestError({ message: "Unable to locate resource" })
|
||||
}
|
||||
|
||||
// check if the user making the request is a admin of this project
|
||||
if (![ADMIN, MEMBER].includes(membershipToModify.role)) {
|
||||
throw UnauthorizedRequestError()
|
||||
}
|
||||
|
||||
// check if the requested slugs are indeed a part of this related workspace
|
||||
const relatedWorkspace = await Workspace.findById(membershipToModify.workspace)
|
||||
if (!relatedWorkspace) {
|
||||
throw BadRequestError({ message: "Something went wrong when locating the related workspace" })
|
||||
}
|
||||
|
||||
const uniqueEnvironmentSlugs = new Set(_.uniq(_.map(relatedWorkspace.environments, "slug")));
|
||||
|
||||
sanitizedMembershipPermissionsUnique.forEach(permission => {
|
||||
if (!uniqueEnvironmentSlugs.has(permission.environmentSlug)) {
|
||||
throw BadRequestError({ message: "Unknown environment slug reference" })
|
||||
}
|
||||
})
|
||||
|
||||
// update the permissions
|
||||
const updatedMembershipWithPermissions = await Membership.findByIdAndUpdate(
|
||||
{ _id: membershipToModify._id },
|
||||
{ $set: { deniedPermissions: sanitizedMembershipPermissionsUnique } },
|
||||
{ new: true }
|
||||
).populate<{ user: IUser }>("user");
|
||||
|
||||
if (!updatedMembershipWithPermissions) {
|
||||
throw BadRequestError({ message: "The resource has been removed before it can be modified" })
|
||||
}
|
||||
|
||||
await EEAuditLogService.createAuditLog(
|
||||
req.authData,
|
||||
{
|
||||
type: EventType.UPDATE_USER_WORKSPACE_DENIED_PERMISSIONS,
|
||||
metadata: {
|
||||
userId: updatedMembershipWithPermissions.user._id.toString(),
|
||||
email: updatedMembershipWithPermissions.user.email,
|
||||
deniedPermissions: updatedMembershipWithPermissions.deniedPermissions.map(({
|
||||
environmentSlug,
|
||||
ability
|
||||
}) => ({
|
||||
environmentSlug,
|
||||
ability
|
||||
}))
|
||||
}
|
||||
},
|
||||
{
|
||||
workspaceId: updatedMembershipWithPermissions.workspace
|
||||
}
|
||||
);
|
||||
|
||||
res.send({
|
||||
permissionsDenied: updatedMembershipWithPermissions.deniedPermissions,
|
||||
})
|
||||
}
|
@ -1,550 +0,0 @@
|
||||
import { Types } from "mongoose";
|
||||
import { Request, Response } from "express";
|
||||
import { getLicenseServerUrl } from "../../../config";
|
||||
import { licenseServerKeyRequest } from "../../../config/request";
|
||||
import { EELicenseService } from "../../services";
|
||||
import { validateRequest } from "../../../helpers/validation";
|
||||
import * as reqValidator from "../../../validation/organization";
|
||||
import {
|
||||
OrgPermissionActions,
|
||||
OrgPermissionSubjects,
|
||||
getAuthDataOrgPermissions,
|
||||
} from "../../services/RoleService";
|
||||
import { ForbiddenError } from "@casl/ability";
|
||||
import { Organization } from "../../../models";
|
||||
import { OrganizationNotFoundError } from "../../../utils/errors";
|
||||
|
||||
export const getOrganizationPlansTable = async (req: Request, res: Response) => {
|
||||
const {
|
||||
query: { billingCycle },
|
||||
params: { organizationId }
|
||||
} = await validateRequest(reqValidator.GetOrgPlansTablev1, req);
|
||||
|
||||
const { permission } = await getAuthDataOrgPermissions({
|
||||
authData: req.authData,
|
||||
organizationId: new Types.ObjectId(organizationId)
|
||||
});
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
OrgPermissionActions.Read,
|
||||
OrgPermissionSubjects.Billing
|
||||
);
|
||||
|
||||
const { data } = await licenseServerKeyRequest.get(
|
||||
`${await getLicenseServerUrl()}/api/license-server/v1/cloud-products?billing-cycle=${billingCycle}`
|
||||
);
|
||||
|
||||
return res.status(200).send(data);
|
||||
};
|
||||
|
||||
/**
|
||||
* Return the organization current plan's feature set
|
||||
*/
|
||||
export const getOrganizationPlan = async (req: Request, res: Response) => {
|
||||
const {
|
||||
query: { workspaceId },
|
||||
params: { organizationId }
|
||||
} = await validateRequest(reqValidator.GetOrgPlanv1, req);
|
||||
|
||||
const { permission } = await getAuthDataOrgPermissions({
|
||||
authData: req.authData,
|
||||
organizationId: new Types.ObjectId(organizationId)
|
||||
});
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
OrgPermissionActions.Read,
|
||||
OrgPermissionSubjects.Billing
|
||||
);
|
||||
|
||||
const plan = await EELicenseService.getPlan(
|
||||
new Types.ObjectId(organizationId),
|
||||
new Types.ObjectId(workspaceId)
|
||||
);
|
||||
|
||||
return res.status(200).send({
|
||||
plan
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Return checkout url for pro trial
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
export const startOrganizationTrial = async (req: Request, res: Response) => {
|
||||
const {
|
||||
params: { organizationId },
|
||||
body: { success_url }
|
||||
} = await validateRequest(reqValidator.StartOrgTrailv1, req);
|
||||
|
||||
const { permission } = await getAuthDataOrgPermissions({
|
||||
authData: req.authData,
|
||||
organizationId: new Types.ObjectId(organizationId)
|
||||
});
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
OrgPermissionActions.Create,
|
||||
OrgPermissionSubjects.Billing
|
||||
);
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
OrgPermissionActions.Edit,
|
||||
OrgPermissionSubjects.Billing
|
||||
);
|
||||
|
||||
const organization = await Organization.findById(organizationId);
|
||||
if (!organization) {
|
||||
throw OrganizationNotFoundError({
|
||||
message: "Failed to find organization"
|
||||
});
|
||||
}
|
||||
|
||||
const {
|
||||
data: { url }
|
||||
} = await licenseServerKeyRequest.post(
|
||||
`${await getLicenseServerUrl()}/api/license-server/v1/customers/${
|
||||
organization.customerId
|
||||
}/session/trial`,
|
||||
{
|
||||
success_url
|
||||
}
|
||||
);
|
||||
|
||||
EELicenseService.delPlan(new Types.ObjectId(organizationId));
|
||||
|
||||
return res.status(200).send({
|
||||
url
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Return the organization's current plan's billing info
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
export const getOrganizationPlanBillingInfo = async (req: Request, res: Response) => {
|
||||
const {
|
||||
params: { organizationId }
|
||||
} = await validateRequest(reqValidator.GetOrgPlanBillingInfov1, req);
|
||||
|
||||
const { permission } = await getAuthDataOrgPermissions({
|
||||
authData: req.authData,
|
||||
organizationId: new Types.ObjectId(organizationId)
|
||||
});
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
OrgPermissionActions.Read,
|
||||
OrgPermissionSubjects.Billing
|
||||
);
|
||||
|
||||
const organization = await Organization.findById(organizationId);
|
||||
if (!organization) {
|
||||
throw OrganizationNotFoundError({
|
||||
message: "Failed to find organization"
|
||||
});
|
||||
}
|
||||
|
||||
const { data } = await licenseServerKeyRequest.get(
|
||||
`${await getLicenseServerUrl()}/api/license-server/v1/customers/${
|
||||
organization.customerId
|
||||
}/cloud-plan/billing`
|
||||
);
|
||||
|
||||
return res.status(200).send(data);
|
||||
};
|
||||
|
||||
/**
|
||||
* Return the organization's current plan's feature table
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
export const getOrganizationPlanTable = async (req: Request, res: Response) => {
|
||||
const {
|
||||
params: { organizationId }
|
||||
} = await validateRequest(reqValidator.GetOrgPlanTablev1, req);
|
||||
|
||||
const { permission } = await getAuthDataOrgPermissions({
|
||||
authData: req.authData,
|
||||
organizationId: new Types.ObjectId(organizationId)
|
||||
});
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
OrgPermissionActions.Read,
|
||||
OrgPermissionSubjects.Billing
|
||||
);
|
||||
|
||||
const organization = await Organization.findById(organizationId);
|
||||
if (!organization) {
|
||||
throw OrganizationNotFoundError({
|
||||
message: "Failed to find organization"
|
||||
});
|
||||
}
|
||||
|
||||
const { data } = await licenseServerKeyRequest.get(
|
||||
`${await getLicenseServerUrl()}/api/license-server/v1/customers/${
|
||||
organization.customerId
|
||||
}/cloud-plan/table`
|
||||
);
|
||||
|
||||
return res.status(200).send(data);
|
||||
};
|
||||
|
||||
export const getOrganizationBillingDetails = async (req: Request, res: Response) => {
|
||||
const {
|
||||
params: { organizationId }
|
||||
} = await validateRequest(reqValidator.GetOrgBillingDetailsv1, req);
|
||||
|
||||
const { permission } = await getAuthDataOrgPermissions({
|
||||
authData: req.authData,
|
||||
organizationId: new Types.ObjectId(organizationId)
|
||||
});
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
OrgPermissionActions.Read,
|
||||
OrgPermissionSubjects.Billing
|
||||
);
|
||||
|
||||
const organization = await Organization.findById(organizationId);
|
||||
if (!organization) {
|
||||
throw OrganizationNotFoundError({
|
||||
message: "Failed to find organization"
|
||||
});
|
||||
}
|
||||
|
||||
const { data } = await licenseServerKeyRequest.get(
|
||||
`${await getLicenseServerUrl()}/api/license-server/v1/customers/${
|
||||
organization.customerId
|
||||
}/billing-details`
|
||||
);
|
||||
|
||||
return res.status(200).send(data);
|
||||
};
|
||||
|
||||
export const updateOrganizationBillingDetails = async (req: Request, res: Response) => {
|
||||
const {
|
||||
params: { organizationId },
|
||||
body: { name, email }
|
||||
} = await validateRequest(reqValidator.UpdateOrgBillingDetailsv1, req);
|
||||
|
||||
const { permission } = await getAuthDataOrgPermissions({
|
||||
authData: req.authData,
|
||||
organizationId: new Types.ObjectId(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 } = await licenseServerKeyRequest.patch(
|
||||
`${await getLicenseServerUrl()}/api/license-server/v1/customers/${
|
||||
organization.customerId
|
||||
}/billing-details`,
|
||||
{
|
||||
...(name ? { name } : {}),
|
||||
...(email ? { email } : {})
|
||||
}
|
||||
);
|
||||
|
||||
return res.status(200).send(data);
|
||||
};
|
||||
|
||||
/**
|
||||
* Return the organization's payment methods on file
|
||||
*/
|
||||
export const getOrganizationPmtMethods = async (req: Request, res: Response) => {
|
||||
const {
|
||||
params: { organizationId }
|
||||
} = await validateRequest(reqValidator.GetOrgPmtMethodsv1, req);
|
||||
|
||||
const { permission } = await getAuthDataOrgPermissions({
|
||||
authData: req.authData,
|
||||
organizationId: new Types.ObjectId(organizationId)
|
||||
});
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
OrgPermissionActions.Read,
|
||||
OrgPermissionSubjects.Billing
|
||||
);
|
||||
|
||||
const organization = await Organization.findById(organizationId);
|
||||
if (!organization) {
|
||||
throw OrganizationNotFoundError({
|
||||
message: "Failed to find organization"
|
||||
});
|
||||
}
|
||||
|
||||
const {
|
||||
data: { pmtMethods }
|
||||
} = await licenseServerKeyRequest.get(
|
||||
`${await getLicenseServerUrl()}/api/license-server/v1/customers/${
|
||||
organization.customerId
|
||||
}/billing-details/payment-methods`
|
||||
);
|
||||
|
||||
return res.status(200).send(pmtMethods);
|
||||
};
|
||||
|
||||
/**
|
||||
* Return URL to add payment method for organization
|
||||
*/
|
||||
export const addOrganizationPmtMethod = async (req: Request, res: Response) => {
|
||||
const {
|
||||
params: { organizationId },
|
||||
body: { success_url, cancel_url }
|
||||
} = await validateRequest(reqValidator.CreateOrgPmtMethodv1, req);
|
||||
|
||||
const { permission } = await getAuthDataOrgPermissions({
|
||||
authData: req.authData,
|
||||
organizationId: new Types.ObjectId(organizationId)
|
||||
});
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
OrgPermissionActions.Create,
|
||||
OrgPermissionSubjects.Billing
|
||||
);
|
||||
|
||||
const organization = await Organization.findById(organizationId);
|
||||
if (!organization) {
|
||||
throw OrganizationNotFoundError({
|
||||
message: "Failed to find organization"
|
||||
});
|
||||
}
|
||||
|
||||
const {
|
||||
data: { url }
|
||||
} = await licenseServerKeyRequest.post(
|
||||
`${await getLicenseServerUrl()}/api/license-server/v1/customers/${
|
||||
organization.customerId
|
||||
}/billing-details/payment-methods`,
|
||||
{
|
||||
success_url,
|
||||
cancel_url
|
||||
}
|
||||
);
|
||||
|
||||
return res.status(200).send({
|
||||
url
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Delete payment method with id [pmtMethodId] for organization
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
export const deleteOrganizationPmtMethod = async (req: Request, res: Response) => {
|
||||
const {
|
||||
params: { organizationId, pmtMethodId }
|
||||
} = await validateRequest(reqValidator.DelOrgPmtMethodv1, req);
|
||||
|
||||
const { permission } = await getAuthDataOrgPermissions({
|
||||
authData: req.authData,
|
||||
organizationId: new Types.ObjectId(organizationId)
|
||||
});
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
OrgPermissionActions.Delete,
|
||||
OrgPermissionSubjects.Billing
|
||||
);
|
||||
|
||||
const organization = await Organization.findById(organizationId);
|
||||
if (!organization) {
|
||||
throw OrganizationNotFoundError({
|
||||
message: "Failed to find organization"
|
||||
});
|
||||
}
|
||||
|
||||
const { data } = await licenseServerKeyRequest.delete(
|
||||
`${await getLicenseServerUrl()}/api/license-server/v1/customers/${
|
||||
organization.customerId
|
||||
}/billing-details/payment-methods/${pmtMethodId}`
|
||||
);
|
||||
|
||||
return res.status(200).send(data);
|
||||
};
|
||||
|
||||
/**
|
||||
* Return the organization's tax ids on file
|
||||
*/
|
||||
export const getOrganizationTaxIds = async (req: Request, res: Response) => {
|
||||
const {
|
||||
params: { organizationId }
|
||||
} = await validateRequest(reqValidator.GetOrgTaxIdsv1, req);
|
||||
|
||||
const { permission } = await getAuthDataOrgPermissions({
|
||||
authData: req.authData,
|
||||
organizationId: new Types.ObjectId(organizationId)
|
||||
});
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
OrgPermissionActions.Read,
|
||||
OrgPermissionSubjects.Billing
|
||||
);
|
||||
|
||||
const organization = await Organization.findById(organizationId);
|
||||
if (!organization) {
|
||||
throw OrganizationNotFoundError({
|
||||
message: "Failed to find organization"
|
||||
});
|
||||
}
|
||||
|
||||
const {
|
||||
data: { tax_ids }
|
||||
} = await licenseServerKeyRequest.get(
|
||||
`${await getLicenseServerUrl()}/api/license-server/v1/customers/${
|
||||
organization.customerId
|
||||
}/billing-details/tax-ids`
|
||||
);
|
||||
|
||||
return res.status(200).send(tax_ids);
|
||||
};
|
||||
|
||||
/**
|
||||
* Add tax id to organization
|
||||
*/
|
||||
export const addOrganizationTaxId = async (req: Request, res: Response) => {
|
||||
const {
|
||||
params: { organizationId },
|
||||
body: { type, value }
|
||||
} = await validateRequest(reqValidator.CreateOrgTaxId, req);
|
||||
|
||||
const { permission } = await getAuthDataOrgPermissions({
|
||||
authData: req.authData,
|
||||
organizationId: new Types.ObjectId(organizationId)
|
||||
});
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
OrgPermissionActions.Create,
|
||||
OrgPermissionSubjects.Billing
|
||||
);
|
||||
|
||||
const organization = await Organization.findById(organizationId);
|
||||
if (!organization) {
|
||||
throw OrganizationNotFoundError({
|
||||
message: "Failed to find organization"
|
||||
});
|
||||
}
|
||||
|
||||
const { data } = await licenseServerKeyRequest.post(
|
||||
`${await getLicenseServerUrl()}/api/license-server/v1/customers/${
|
||||
organization.customerId
|
||||
}/billing-details/tax-ids`,
|
||||
{
|
||||
type,
|
||||
value
|
||||
}
|
||||
);
|
||||
|
||||
return res.status(200).send(data);
|
||||
};
|
||||
|
||||
/**
|
||||
* Delete tax id with id [taxId] from organization tax ids on file
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
export const deleteOrganizationTaxId = async (req: Request, res: Response) => {
|
||||
const {
|
||||
params: { organizationId, taxId }
|
||||
} = await validateRequest(reqValidator.DelOrgTaxIdv1, req);
|
||||
|
||||
const { permission } = await getAuthDataOrgPermissions({
|
||||
authData: req.authData,
|
||||
organizationId: new Types.ObjectId(organizationId)
|
||||
});
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
OrgPermissionActions.Delete,
|
||||
OrgPermissionSubjects.Billing
|
||||
);
|
||||
|
||||
const organization = await Organization.findById(organizationId);
|
||||
if (!organization) {
|
||||
throw OrganizationNotFoundError({
|
||||
message: "Failed to find organization"
|
||||
});
|
||||
}
|
||||
|
||||
const { data } = await licenseServerKeyRequest.delete(
|
||||
`${await getLicenseServerUrl()}/api/license-server/v1/customers/${
|
||||
organization.customerId
|
||||
}/billing-details/tax-ids/${taxId}`
|
||||
);
|
||||
|
||||
return res.status(200).send(data);
|
||||
};
|
||||
|
||||
/**
|
||||
* Return organization's invoices on file
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
export const getOrganizationInvoices = async (req: Request, res: Response) => {
|
||||
const {
|
||||
params: { organizationId }
|
||||
} = await validateRequest(reqValidator.GetOrgInvoicesv1, req);
|
||||
|
||||
const { permission } = await getAuthDataOrgPermissions({
|
||||
authData: req.authData,
|
||||
organizationId: new Types.ObjectId(organizationId)
|
||||
});
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
OrgPermissionActions.Read,
|
||||
OrgPermissionSubjects.Billing
|
||||
);
|
||||
|
||||
const organization = await Organization.findById(organizationId);
|
||||
if (!organization) {
|
||||
throw OrganizationNotFoundError({
|
||||
message: "Failed to find organization"
|
||||
});
|
||||
}
|
||||
|
||||
const {
|
||||
data: { invoices }
|
||||
} = await licenseServerKeyRequest.get(
|
||||
`${await getLicenseServerUrl()}/api/license-server/v1/customers/${
|
||||
organization.customerId
|
||||
}/invoices`
|
||||
);
|
||||
|
||||
return res.status(200).send(invoices);
|
||||
};
|
||||
|
||||
/**
|
||||
* Return organization's licenses on file
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
export const getOrganizationLicenses = async (req: Request, res: Response) => {
|
||||
const {
|
||||
params: { organizationId }
|
||||
} = await validateRequest(reqValidator.GetOrgLicencesv1, req);
|
||||
|
||||
const { permission } = await getAuthDataOrgPermissions({
|
||||
authData: req.authData,
|
||||
organizationId: new Types.ObjectId(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);
|
||||
};
|
@ -1,290 +0,0 @@
|
||||
import { Request, Response } from "express";
|
||||
import { Types } from "mongoose";
|
||||
import { Membership, User } from "../../../models";
|
||||
import {
|
||||
CreateRoleSchema,
|
||||
DeleteRoleSchema,
|
||||
GetRoleSchema,
|
||||
GetUserPermission,
|
||||
GetUserProjectPermission,
|
||||
UpdateRoleSchema
|
||||
} from "../../validation/role";
|
||||
import {
|
||||
ProjectPermissionActions,
|
||||
ProjectPermissionSub,
|
||||
adminProjectPermissions,
|
||||
getAuthDataProjectPermissions,
|
||||
memberProjectPermissions,
|
||||
noAccessProjectPermissions,
|
||||
viewerProjectPermission
|
||||
} from "../../services/ProjectRoleService";
|
||||
import {
|
||||
OrgPermissionActions,
|
||||
OrgPermissionSubjects,
|
||||
adminPermissions,
|
||||
getAuthDataOrgPermissions,
|
||||
getUserOrgPermissions,
|
||||
memberPermissions,
|
||||
noAccessPermissions
|
||||
} from "../../services/RoleService";
|
||||
import { BadRequestError } from "../../../utils/errors";
|
||||
import { Role } from "../../models";
|
||||
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 getAuthDataOrgPermissions({
|
||||
authData: req.authData,
|
||||
organizationId: new Types.ObjectId(orgId)
|
||||
});
|
||||
|
||||
if (permission.cannot(OrgPermissionActions.Create, OrgPermissionSubjects.Role)) {
|
||||
throw BadRequestError({ message: "user doesn't have the permission." });
|
||||
}
|
||||
} else {
|
||||
const { permission } = await getAuthDataProjectPermissions({
|
||||
authData: req.authData,
|
||||
workspaceId: new Types.ObjectId(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 getAuthDataOrgPermissions({
|
||||
authData: req.authData,
|
||||
organizationId: new Types.ObjectId(orgId)
|
||||
});
|
||||
if (permission.cannot(OrgPermissionActions.Edit, OrgPermissionSubjects.Role)) {
|
||||
throw BadRequestError({ message: "User doesn't have the org permission." });
|
||||
}
|
||||
} else {
|
||||
const { permission } = await getAuthDataProjectPermissions({
|
||||
authData: req.authData,
|
||||
workspaceId: new Types.ObjectId(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 getAuthDataOrgPermissions({
|
||||
authData: req.authData,
|
||||
organizationId: role.organization
|
||||
});
|
||||
if (permission.cannot(OrgPermissionActions.Delete, OrgPermissionSubjects.Role)) {
|
||||
throw BadRequestError({ message: "User doesn't have the org permission." });
|
||||
}
|
||||
} else {
|
||||
const { permission } = await getAuthDataProjectPermissions({
|
||||
authData: req.authData,
|
||||
workspaceId: role.workspace
|
||||
});
|
||||
|
||||
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 getAuthDataOrgPermissions({
|
||||
authData: req.authData,
|
||||
organizationId: new Types.ObjectId(orgId)
|
||||
});
|
||||
if (permission.cannot(OrgPermissionActions.Read, OrgPermissionSubjects.Role)) {
|
||||
throw BadRequestError({ message: "User doesn't have the org permission." });
|
||||
}
|
||||
} else {
|
||||
const { permission } = await getAuthDataProjectPermissions({
|
||||
authData: req.authData,
|
||||
workspaceId: new Types.ObjectId(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: "no-access",
|
||||
name: "No Access",
|
||||
slug: "no-access",
|
||||
description: "No access to any resources in the organization",
|
||||
permissions: isOrgRole ? noAccessPermissions.rules : noAccessProjectPermissions.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, membership } = await getUserOrgPermissions(req.user._id, orgId);
|
||||
|
||||
res.status(200).json({
|
||||
data: {
|
||||
permissions: packRules(permission.rules),
|
||||
membership
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const getUserWorkspacePermissions = async (req: Request, res: Response) => {
|
||||
const {
|
||||
params: { workspaceId }
|
||||
} = await validateRequest(GetUserProjectPermission, req);
|
||||
|
||||
const { permission } = await getAuthDataProjectPermissions({
|
||||
authData: req.authData,
|
||||
workspaceId: new Types.ObjectId(workspaceId)
|
||||
});
|
||||
|
||||
let membership;
|
||||
if (req.authData.authPayload instanceof User) {
|
||||
membership = await Membership.findOne({
|
||||
user: req.authData.authPayload._id,
|
||||
workspace: new Types.ObjectId(workspaceId)
|
||||
})
|
||||
}
|
||||
|
||||
res.status(200).json({
|
||||
data: {
|
||||
permissions: packRules(permission.rules),
|
||||
membership
|
||||
}
|
||||
});
|
||||
};
|
@ -1,143 +0,0 @@
|
||||
import { Types } from "mongoose";
|
||||
import { ForbiddenError, subject } from "@casl/ability";
|
||||
import { Request, Response } from "express";
|
||||
import { nanoid } from "nanoid";
|
||||
import {
|
||||
ProjectPermissionActions,
|
||||
ProjectPermissionSub,
|
||||
getAuthDataProjectPermissions
|
||||
} from "../../services/ProjectRoleService";
|
||||
import { validateRequest } from "../../../helpers/validation";
|
||||
import { SecretApprovalPolicy } from "../../models/secretApprovalPolicy";
|
||||
import { getSecretPolicyOfBoard } from "../../services/SecretApprovalService";
|
||||
import { BadRequestError } from "../../../utils/errors";
|
||||
import * as reqValidator from "../../validation/secretApproval";
|
||||
|
||||
const ERR_SECRET_APPROVAL_NOT_FOUND = BadRequestError({ message: "secret approval not found" });
|
||||
|
||||
export const createSecretApprovalPolicy = async (req: Request, res: Response) => {
|
||||
const {
|
||||
body: { approvals, secretPath, approvers, environment, workspaceId, name }
|
||||
} = await validateRequest(reqValidator.CreateSecretApprovalRule, req);
|
||||
|
||||
const { permission } = await getAuthDataProjectPermissions({
|
||||
authData: req.authData,
|
||||
workspaceId: new Types.ObjectId(workspaceId)
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Create,
|
||||
ProjectPermissionSub.SecretApproval
|
||||
);
|
||||
|
||||
const secretApproval = new SecretApprovalPolicy({
|
||||
workspace: workspaceId,
|
||||
name: name ?? `${environment}-${nanoid(3)}`,
|
||||
secretPath,
|
||||
environment,
|
||||
approvals,
|
||||
approvers
|
||||
});
|
||||
await secretApproval.save();
|
||||
|
||||
return res.send({
|
||||
approval: secretApproval
|
||||
});
|
||||
};
|
||||
|
||||
export const updateSecretApprovalPolicy = async (req: Request, res: Response) => {
|
||||
const {
|
||||
body: { approvals, approvers, secretPath, name },
|
||||
params: { id }
|
||||
} = await validateRequest(reqValidator.UpdateSecretApprovalRule, req);
|
||||
|
||||
const secretApproval = await SecretApprovalPolicy.findById(id);
|
||||
if (!secretApproval) throw ERR_SECRET_APPROVAL_NOT_FOUND;
|
||||
|
||||
const { permission } = await getAuthDataProjectPermissions({
|
||||
authData: req.authData,
|
||||
workspaceId: secretApproval.workspace
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Edit,
|
||||
ProjectPermissionSub.SecretApproval
|
||||
);
|
||||
|
||||
const updatedDoc = await SecretApprovalPolicy.findByIdAndUpdate(id, {
|
||||
approvals,
|
||||
approvers,
|
||||
name: (name || secretApproval?.name) ?? `${secretApproval.environment}-${nanoid(3)}`,
|
||||
...(secretPath === null ? { $unset: { secretPath: 1 } } : { secretPath })
|
||||
});
|
||||
|
||||
return res.send({
|
||||
approval: updatedDoc
|
||||
});
|
||||
};
|
||||
|
||||
export const deleteSecretApprovalPolicy = async (req: Request, res: Response) => {
|
||||
const {
|
||||
params: { id }
|
||||
} = await validateRequest(reqValidator.DeleteSecretApprovalRule, req);
|
||||
|
||||
const secretApproval = await SecretApprovalPolicy.findById(id);
|
||||
if (!secretApproval) throw ERR_SECRET_APPROVAL_NOT_FOUND;
|
||||
|
||||
const { permission } = await getAuthDataProjectPermissions({
|
||||
authData: req.authData,
|
||||
workspaceId: secretApproval.workspace
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Delete,
|
||||
ProjectPermissionSub.SecretApproval
|
||||
);
|
||||
|
||||
const deletedDoc = await SecretApprovalPolicy.findByIdAndDelete(id);
|
||||
|
||||
return res.send({
|
||||
approval: deletedDoc
|
||||
});
|
||||
};
|
||||
|
||||
export const getSecretApprovalPolicy = async (req: Request, res: Response) => {
|
||||
const {
|
||||
query: { workspaceId }
|
||||
} = await validateRequest(reqValidator.GetSecretApprovalRuleList, req);
|
||||
|
||||
const { permission } = await getAuthDataProjectPermissions({
|
||||
authData: req.authData,
|
||||
workspaceId: new Types.ObjectId(workspaceId)
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Read,
|
||||
ProjectPermissionSub.SecretApproval
|
||||
);
|
||||
|
||||
const doc = await SecretApprovalPolicy.find({ workspace: workspaceId });
|
||||
|
||||
return res.send({
|
||||
approvals: doc
|
||||
});
|
||||
};
|
||||
|
||||
export const getSecretApprovalPolicyOfBoard = async (req: Request, res: Response) => {
|
||||
const {
|
||||
query: { workspaceId, environment, secretPath }
|
||||
} = await validateRequest(reqValidator.GetSecretApprovalPolicyOfABoard, req);
|
||||
|
||||
const { permission } = await getAuthDataProjectPermissions({
|
||||
authData: req.authData,
|
||||
workspaceId: new Types.ObjectId(workspaceId)
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Read,
|
||||
subject(ProjectPermissionSub.Secrets, { secretPath, environment })
|
||||
);
|
||||
|
||||
const secretApprovalPolicy = await getSecretPolicyOfBoard(workspaceId, environment, secretPath);
|
||||
return res.send({ policy: secretApprovalPolicy });
|
||||
};
|
@ -1,366 +0,0 @@
|
||||
import { Request, Response } from "express";
|
||||
import { validateRequest } from "../../../helpers/validation";
|
||||
import { Folder, Membership, User } from "../../../models";
|
||||
import { ApprovalStatus, SecretApprovalRequest } from "../../models/secretApprovalRequest";
|
||||
import * as reqValidator from "../../validation/secretApprovalRequest";
|
||||
import { getFolderWithPathFromId } from "../../../services/FolderService";
|
||||
import { BadRequestError, UnauthorizedRequestError } from "../../../utils/errors";
|
||||
import { ISecretApprovalPolicy, SecretApprovalPolicy } from "../../models/secretApprovalPolicy";
|
||||
import { performSecretApprovalRequestMerge } from "../../services/SecretApprovalService";
|
||||
import { Types } from "mongoose";
|
||||
import { EEAuditLogService } from "../../services";
|
||||
import { EventType } from "../../models";
|
||||
|
||||
export const getSecretApprovalRequestCount = async (req: Request, res: Response) => {
|
||||
const {
|
||||
query: { workspaceId }
|
||||
} = await validateRequest(reqValidator.getSecretApprovalRequestCount, req);
|
||||
|
||||
if (!(req.authData.authPayload instanceof User)) return;
|
||||
|
||||
const membership = await Membership.findOne({
|
||||
user: req.authData.authPayload._id,
|
||||
workspace: new Types.ObjectId(workspaceId)
|
||||
});
|
||||
|
||||
if (!membership) throw UnauthorizedRequestError();
|
||||
|
||||
const approvalRequestCount = await SecretApprovalRequest.aggregate([
|
||||
{
|
||||
$match: {
|
||||
workspace: new Types.ObjectId(workspaceId)
|
||||
}
|
||||
},
|
||||
{
|
||||
$lookup: {
|
||||
from: SecretApprovalPolicy.collection.name,
|
||||
localField: "policy",
|
||||
foreignField: "_id",
|
||||
as: "policy"
|
||||
}
|
||||
},
|
||||
{ $unwind: "$policy" },
|
||||
...(membership.role !== "admin"
|
||||
? [
|
||||
{
|
||||
$match: {
|
||||
$or: [
|
||||
{ committer: new Types.ObjectId(membership.id) },
|
||||
{ "policy.approvers": new Types.ObjectId(membership.id) }
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
: []),
|
||||
{
|
||||
$group: {
|
||||
_id: "$status",
|
||||
count: { $sum: 1 }
|
||||
}
|
||||
}
|
||||
]);
|
||||
const openRequests = approvalRequestCount.find(({ _id }) => _id === "open");
|
||||
const closedRequests = approvalRequestCount.find(({ _id }) => _id === "close");
|
||||
|
||||
return res.send({
|
||||
approvals: { open: openRequests?.count || 0, closed: closedRequests?.count || 0 }
|
||||
});
|
||||
};
|
||||
|
||||
export const getSecretApprovalRequests = async (req: Request, res: Response) => {
|
||||
const {
|
||||
query: { status, committer, workspaceId, environment, limit, offset }
|
||||
} = await validateRequest(reqValidator.getSecretApprovalRequests, req);
|
||||
|
||||
if (!(req.authData.authPayload instanceof User)) return;
|
||||
|
||||
const membership = await Membership.findOne({
|
||||
user: req.authData.authPayload._id,
|
||||
workspace: new Types.ObjectId(workspaceId)
|
||||
});
|
||||
|
||||
if (!membership) throw UnauthorizedRequestError();
|
||||
|
||||
const query = {
|
||||
workspace: new Types.ObjectId(workspaceId),
|
||||
environment,
|
||||
committer: committer ? new Types.ObjectId(committer) : undefined,
|
||||
status
|
||||
};
|
||||
// to strip of undefined in query we use es6 spread to ignore those fields
|
||||
Object.entries(query).forEach(
|
||||
([key, value]) => value === undefined && delete query[key as keyof typeof query]
|
||||
);
|
||||
const approvalRequests = await SecretApprovalRequest.aggregate([
|
||||
{
|
||||
$match: query
|
||||
},
|
||||
{ $sort: { createdAt: -1 } },
|
||||
{
|
||||
$lookup: {
|
||||
from: SecretApprovalPolicy.collection.name,
|
||||
localField: "policy",
|
||||
foreignField: "_id",
|
||||
as: "policy"
|
||||
}
|
||||
},
|
||||
{ $unwind: "$policy" },
|
||||
...(membership.role !== "admin"
|
||||
? [
|
||||
{
|
||||
$match: {
|
||||
$or: [
|
||||
{ committer: new Types.ObjectId(membership.id) },
|
||||
{ "policy.approvers": new Types.ObjectId(membership.id) }
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
: []),
|
||||
{ $skip: offset },
|
||||
{ $limit: limit }
|
||||
]);
|
||||
if (!approvalRequests.length) return res.send({ approvals: [] });
|
||||
|
||||
const unqiueEnvs = environment ?? {
|
||||
$in: [...new Set(approvalRequests.map(({ environment }) => environment))]
|
||||
};
|
||||
const approvalRootFolders = await Folder.find({
|
||||
workspace: workspaceId,
|
||||
environment: unqiueEnvs
|
||||
}).lean();
|
||||
|
||||
const formatedApprovals = approvalRequests.map((el) => {
|
||||
let secretPath = "/";
|
||||
const folders = approvalRootFolders.find(({ environment }) => environment === el.environment);
|
||||
if (folders) {
|
||||
secretPath = getFolderWithPathFromId(folders?.nodes, el.folderId)?.folderPath || "/";
|
||||
}
|
||||
return { ...el, secretPath };
|
||||
});
|
||||
|
||||
return res.send({
|
||||
approvals: formatedApprovals
|
||||
});
|
||||
};
|
||||
|
||||
export const getSecretApprovalRequestDetails = async (req: Request, res: Response) => {
|
||||
const {
|
||||
params: { id }
|
||||
} = await validateRequest(reqValidator.getSecretApprovalRequestDetails, req);
|
||||
const secretApprovalRequest = await SecretApprovalRequest.findById(id)
|
||||
.populate<{ policy: ISecretApprovalPolicy }>("policy")
|
||||
.populate({
|
||||
path: "commits.secretVersion",
|
||||
populate: {
|
||||
path: "tags"
|
||||
}
|
||||
})
|
||||
.populate("commits.secret", "version")
|
||||
.populate("commits.newVersion.tags")
|
||||
.lean();
|
||||
if (!secretApprovalRequest)
|
||||
throw BadRequestError({ message: "Secret approval request not found" });
|
||||
|
||||
if (!(req.authData.authPayload instanceof User)) return;
|
||||
|
||||
const membership = await Membership.findOne({
|
||||
user: req.authData.authPayload._id,
|
||||
workspace: secretApprovalRequest.workspace
|
||||
});
|
||||
|
||||
if (!membership) throw UnauthorizedRequestError();
|
||||
|
||||
// allow to fetch only if its admin or is the committer or approver
|
||||
if (
|
||||
membership.role !== "admin" &&
|
||||
!secretApprovalRequest.committer.equals(membership.id) &&
|
||||
!secretApprovalRequest.policy.approvers.find(
|
||||
(approverId) => approverId.toString() === membership._id.toString()
|
||||
)
|
||||
) {
|
||||
throw UnauthorizedRequestError({ message: "User has no access" });
|
||||
}
|
||||
|
||||
let secretPath = "/";
|
||||
const approvalRootFolders = await Folder.findOne({
|
||||
workspace: secretApprovalRequest.workspace,
|
||||
environment: secretApprovalRequest.environment
|
||||
}).lean();
|
||||
if (approvalRootFolders) {
|
||||
secretPath =
|
||||
getFolderWithPathFromId(approvalRootFolders?.nodes, secretApprovalRequest.folderId)
|
||||
?.folderPath || "/";
|
||||
}
|
||||
|
||||
return res.send({
|
||||
approval: { ...secretApprovalRequest, secretPath }
|
||||
});
|
||||
};
|
||||
|
||||
export const updateSecretApprovalReviewStatus = async (req: Request, res: Response) => {
|
||||
const {
|
||||
body: { status },
|
||||
params: { id }
|
||||
} = await validateRequest(reqValidator.updateSecretApprovalReviewStatus, req);
|
||||
const secretApprovalRequest = await SecretApprovalRequest.findById(id).populate<{
|
||||
policy: ISecretApprovalPolicy;
|
||||
}>("policy");
|
||||
if (!secretApprovalRequest)
|
||||
throw BadRequestError({ message: "Secret approval request not found" });
|
||||
|
||||
if (!(req.authData.authPayload instanceof User)) return;
|
||||
|
||||
const membership = await Membership.findOne({
|
||||
user: req.authData.authPayload._id,
|
||||
workspace: secretApprovalRequest.workspace
|
||||
});
|
||||
|
||||
if (!membership) throw UnauthorizedRequestError();
|
||||
|
||||
if (
|
||||
membership.role !== "admin" &&
|
||||
secretApprovalRequest.committer !== membership.id &&
|
||||
!secretApprovalRequest.policy.approvers.find((approverId) => approverId.equals(membership.id))
|
||||
) {
|
||||
throw UnauthorizedRequestError({ message: "User has no access" });
|
||||
}
|
||||
|
||||
const reviewerPos = secretApprovalRequest.reviewers.findIndex(
|
||||
({ member }) => member.toString() === membership._id.toString()
|
||||
);
|
||||
if (reviewerPos !== -1) {
|
||||
secretApprovalRequest.reviewers[reviewerPos].status = status;
|
||||
} else {
|
||||
secretApprovalRequest.reviewers.push({ member: membership._id, status });
|
||||
}
|
||||
await secretApprovalRequest.save();
|
||||
|
||||
return res.send({ status });
|
||||
};
|
||||
|
||||
export const mergeSecretApprovalRequest = async (req: Request, res: Response) => {
|
||||
const {
|
||||
params: { id }
|
||||
} = await validateRequest(reqValidator.mergeSecretApprovalRequest, req);
|
||||
|
||||
const secretApprovalRequest = await SecretApprovalRequest.findById(id).populate<{
|
||||
policy: ISecretApprovalPolicy;
|
||||
}>("policy");
|
||||
|
||||
if (!secretApprovalRequest)
|
||||
throw BadRequestError({ message: "Secret approval request not found" });
|
||||
|
||||
if (!(req.authData.authPayload instanceof User)) return;
|
||||
|
||||
const membership = await Membership.findOne({
|
||||
user: req.authData.authPayload._id,
|
||||
workspace: secretApprovalRequest.workspace
|
||||
});
|
||||
|
||||
if (!membership) throw UnauthorizedRequestError();
|
||||
|
||||
if (
|
||||
membership.role !== "admin" &&
|
||||
secretApprovalRequest.committer !== membership.id &&
|
||||
!secretApprovalRequest.policy.approvers.find((approverId) => approverId.equals(membership.id))
|
||||
) {
|
||||
throw UnauthorizedRequestError({ message: "User has no access" });
|
||||
}
|
||||
|
||||
const reviewers = secretApprovalRequest.reviewers.reduce<Record<string, ApprovalStatus>>(
|
||||
(prev, curr) => ({ ...prev, [curr.member.toString()]: curr.status }),
|
||||
{}
|
||||
);
|
||||
const hasMinApproval =
|
||||
secretApprovalRequest.policy.approvals <=
|
||||
secretApprovalRequest.policy.approvers.filter(
|
||||
(approverId) => reviewers[approverId.toString()] === ApprovalStatus.APPROVED
|
||||
).length;
|
||||
|
||||
if (!hasMinApproval) throw BadRequestError({ message: "Doesn't have minimum approvals needed" });
|
||||
|
||||
const approval = await performSecretApprovalRequestMerge(
|
||||
id,
|
||||
req.authData,
|
||||
membership._id.toString()
|
||||
);
|
||||
return res.send({ approval });
|
||||
};
|
||||
|
||||
export const updateSecretApprovalRequestStatus = async (req: Request, res: Response) => {
|
||||
const {
|
||||
body: { status },
|
||||
params: { id }
|
||||
} = await validateRequest(reqValidator.updateSecretApprovalRequestStatus, req);
|
||||
|
||||
const secretApprovalRequest = await SecretApprovalRequest.findById(id).populate<{
|
||||
policy: ISecretApprovalPolicy;
|
||||
}>("policy");
|
||||
|
||||
if (!secretApprovalRequest)
|
||||
throw BadRequestError({ message: "Secret approval request not found" });
|
||||
|
||||
if (!(req.authData.authPayload instanceof User)) return;
|
||||
|
||||
const membership = await Membership.findOne({
|
||||
user: req.authData.authPayload._id,
|
||||
workspace: secretApprovalRequest.workspace
|
||||
});
|
||||
|
||||
if (!membership) throw UnauthorizedRequestError();
|
||||
|
||||
if (
|
||||
membership.role !== "admin" &&
|
||||
secretApprovalRequest.committer !== membership.id &&
|
||||
!secretApprovalRequest.policy.approvers.find((approverId) => approverId.equals(membership._id))
|
||||
) {
|
||||
throw UnauthorizedRequestError({ message: "User has no access" });
|
||||
}
|
||||
|
||||
if (secretApprovalRequest.hasMerged)
|
||||
throw BadRequestError({ message: "Approval request has been merged" });
|
||||
if (secretApprovalRequest.status === "close" && status === "close")
|
||||
throw BadRequestError({ message: "Approval request is already closed" });
|
||||
if (secretApprovalRequest.status === "open" && status === "open")
|
||||
throw BadRequestError({ message: "Approval request is already open" });
|
||||
|
||||
const updatedRequest = await SecretApprovalRequest.findByIdAndUpdate(
|
||||
id,
|
||||
{ status, statusChangeBy: membership._id },
|
||||
{ new: true }
|
||||
);
|
||||
|
||||
if (status === "close") {
|
||||
await EEAuditLogService.createAuditLog(
|
||||
req.authData,
|
||||
{
|
||||
type: EventType.SECRET_APPROVAL_CLOSED,
|
||||
metadata: {
|
||||
closedBy: membership._id.toString(),
|
||||
secretApprovalRequestId: id,
|
||||
secretApprovalRequestSlug: secretApprovalRequest.slug
|
||||
}
|
||||
},
|
||||
{
|
||||
workspaceId: secretApprovalRequest.workspace
|
||||
}
|
||||
);
|
||||
} else {
|
||||
await EEAuditLogService.createAuditLog(
|
||||
req.authData,
|
||||
{
|
||||
type: EventType.SECRET_APPROVAL_REOPENED,
|
||||
metadata: {
|
||||
reopenedBy: membership._id.toString(),
|
||||
secretApprovalRequestId: id,
|
||||
secretApprovalRequestSlug: secretApprovalRequest.slug
|
||||
}
|
||||
},
|
||||
{
|
||||
workspaceId: secretApprovalRequest.workspace
|
||||
}
|
||||
);
|
||||
}
|
||||
return res.send({ approval: updatedRequest });
|
||||
};
|
@ -1,269 +0,0 @@
|
||||
import { ForbiddenError, subject } from "@casl/ability";
|
||||
import { Request, Response } from "express";
|
||||
import { validateRequest } from "../../../helpers/validation";
|
||||
import { Folder, Secret } from "../../../models";
|
||||
import {
|
||||
ProjectPermissionActions,
|
||||
ProjectPermissionSub,
|
||||
getAuthDataProjectPermissions
|
||||
} from "../../services/ProjectRoleService";
|
||||
import { BadRequestError } from "../../../utils/errors";
|
||||
import * as reqValidator from "../../../validation";
|
||||
import { SecretVersion } from "../../models";
|
||||
import { EESecretService } from "../../services";
|
||||
import { getFolderWithPathFromId } from "../../../services/FolderService";
|
||||
|
||||
/**
|
||||
* Return secret versions for secret with id [secretId]
|
||||
* @param req
|
||||
* @param res
|
||||
*/
|
||||
export const getSecretVersions = async (req: Request, res: Response) => {
|
||||
/*
|
||||
#swagger.summary = 'Return secret versions'
|
||||
#swagger.description = 'Return secret versions'
|
||||
|
||||
#swagger.security = [{
|
||||
"apiKeyAuth": []
|
||||
}]
|
||||
|
||||
#swagger.parameters['secretId'] = {
|
||||
"description": "ID of secret",
|
||||
"required": true,
|
||||
"type": "string"
|
||||
}
|
||||
|
||||
#swagger.parameters['offset'] = {
|
||||
"description": "Number of versions to skip",
|
||||
"required": false,
|
||||
"type": "string"
|
||||
}
|
||||
|
||||
#swagger.parameters['limit'] = {
|
||||
"description": "Maximum number of versions to return",
|
||||
"required": false,
|
||||
"type": "string"
|
||||
}
|
||||
|
||||
#swagger.responses[200] = {
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"secretVersions": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
$ref: "#/components/schemas/SecretVersion"
|
||||
},
|
||||
"description": "Secret versions"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
*/
|
||||
const {
|
||||
params: { secretId },
|
||||
query: { offset, limit }
|
||||
} = await validateRequest(reqValidator.GetSecretVersionsV1, req);
|
||||
|
||||
const secret = await Secret.findById(secretId);
|
||||
if (!secret) {
|
||||
throw BadRequestError({ message: "Failed to find secret" });
|
||||
}
|
||||
|
||||
const { permission } = await getAuthDataProjectPermissions({
|
||||
authData: req.authData,
|
||||
workspaceId: secret.workspace
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Read,
|
||||
ProjectPermissionSub.SecretRollback
|
||||
);
|
||||
|
||||
const secretVersions = await SecretVersion.find({
|
||||
secret: secretId
|
||||
})
|
||||
.sort({ createdAt: -1 })
|
||||
.skip(offset)
|
||||
.limit(limit);
|
||||
|
||||
return res.status(200).send({
|
||||
secretVersions
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Roll back secret with id [secretId] to version [version]
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
export const rollbackSecretVersion = async (req: Request, res: Response) => {
|
||||
/*
|
||||
#swagger.summary = 'Roll back secret to a version.'
|
||||
#swagger.description = 'Roll back secret to a version.'
|
||||
|
||||
#swagger.security = [{
|
||||
"apiKeyAuth": []
|
||||
}]
|
||||
|
||||
#swagger.parameters['secretId'] = {
|
||||
"description": "ID of secret",
|
||||
"required": true,
|
||||
"type": "string"
|
||||
}
|
||||
|
||||
#swagger.requestBody = {
|
||||
"required": true,
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"version": {
|
||||
"type": "integer",
|
||||
"description": "Version of secret to roll back to"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#swagger.responses[200] = {
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"secret": {
|
||||
"type": "object",
|
||||
$ref: "#/components/schemas/Secret",
|
||||
"description": "Secret rolled back to"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
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 getAuthDataProjectPermissions({
|
||||
authData: req.authData,
|
||||
workspaceId: toBeUpdatedSec.workspace
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Create,
|
||||
ProjectPermissionSub.SecretRollback
|
||||
);
|
||||
|
||||
// validate secret version
|
||||
const oldSecretVersion = await SecretVersion.findOne({
|
||||
secret: secretId,
|
||||
version
|
||||
}).select("+secretBlindIndex");
|
||||
|
||||
if (!oldSecretVersion) throw new Error("Failed to find secret version");
|
||||
|
||||
const {
|
||||
workspace,
|
||||
type,
|
||||
user,
|
||||
environment,
|
||||
secretBlindIndex,
|
||||
secretKeyCiphertext,
|
||||
secretKeyIV,
|
||||
secretKeyTag,
|
||||
secretValueCiphertext,
|
||||
secretValueIV,
|
||||
secretValueTag,
|
||||
algorithm,
|
||||
folder,
|
||||
keyEncoding
|
||||
} = oldSecretVersion;
|
||||
|
||||
let secretPath = "/";
|
||||
const folders = await Folder.findOne({ workspace, environment });
|
||||
if (folders)
|
||||
secretPath = getFolderWithPathFromId(folders.nodes, folder || "root")?.folderPath || "/";
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Edit,
|
||||
subject(ProjectPermissionSub.Secrets, { environment: toBeUpdatedSec.environment, secretPath })
|
||||
);
|
||||
|
||||
// update secret
|
||||
const secret = await Secret.findByIdAndUpdate(
|
||||
secretId,
|
||||
{
|
||||
$inc: {
|
||||
version: 1
|
||||
},
|
||||
workspace,
|
||||
type,
|
||||
user,
|
||||
environment,
|
||||
...(secretBlindIndex ? { secretBlindIndex } : {}),
|
||||
secretKeyCiphertext,
|
||||
secretKeyIV,
|
||||
secretKeyTag,
|
||||
secretValueCiphertext,
|
||||
secretValueIV,
|
||||
secretValueTag,
|
||||
folderId: folder,
|
||||
algorithm,
|
||||
keyEncoding
|
||||
},
|
||||
{
|
||||
new: true
|
||||
}
|
||||
);
|
||||
|
||||
if (!secret) throw new Error("Failed to find and update secret");
|
||||
|
||||
// add new secret version
|
||||
await new SecretVersion({
|
||||
secret: secretId,
|
||||
version: secret.version,
|
||||
workspace,
|
||||
type,
|
||||
user,
|
||||
environment,
|
||||
isDeleted: false,
|
||||
...(secretBlindIndex ? { secretBlindIndex } : {}),
|
||||
secretKeyCiphertext,
|
||||
secretKeyIV,
|
||||
secretKeyTag,
|
||||
secretValueCiphertext,
|
||||
secretValueIV,
|
||||
secretValueTag,
|
||||
folder,
|
||||
algorithm,
|
||||
keyEncoding
|
||||
}).save();
|
||||
|
||||
// take secret snapshot
|
||||
await EESecretService.takeSecretSnapshot({
|
||||
workspaceId: secret.workspace,
|
||||
environment,
|
||||
folderId: folder
|
||||
});
|
||||
|
||||
return res.status(200).send({
|
||||
secret
|
||||
});
|
||||
};
|
@ -1,110 +0,0 @@
|
||||
import { Request, Response } from "express";
|
||||
import { Types } from "mongoose";
|
||||
import { validateRequest } from "../../../helpers/validation";
|
||||
import * as reqValidator from "../../validation/secretRotation";
|
||||
import * as secretRotationService from "../../secretRotation/service";
|
||||
import {
|
||||
ProjectPermissionActions,
|
||||
ProjectPermissionSub,
|
||||
getAuthDataProjectPermissions
|
||||
} from "../../services/ProjectRoleService";
|
||||
import { ForbiddenError } from "@casl/ability";
|
||||
|
||||
export const createSecretRotation = async (req: Request, res: Response) => {
|
||||
const {
|
||||
body: {
|
||||
provider,
|
||||
customProvider,
|
||||
interval,
|
||||
outputs,
|
||||
secretPath,
|
||||
environment,
|
||||
workspaceId,
|
||||
inputs
|
||||
}
|
||||
} = await validateRequest(reqValidator.createSecretRotationV1, req);
|
||||
|
||||
const { permission } = await getAuthDataProjectPermissions({
|
||||
authData: req.authData,
|
||||
workspaceId: new Types.ObjectId(workspaceId)
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Create,
|
||||
ProjectPermissionSub.SecretRotation
|
||||
);
|
||||
|
||||
const secretRotation = await secretRotationService.createSecretRotation({
|
||||
workspaceId,
|
||||
inputs,
|
||||
environment,
|
||||
secretPath,
|
||||
outputs,
|
||||
interval,
|
||||
customProvider,
|
||||
provider
|
||||
});
|
||||
|
||||
return res.send({ secretRotation });
|
||||
};
|
||||
|
||||
export const restartSecretRotations = async (req: Request, res: Response) => {
|
||||
const {
|
||||
body: { id }
|
||||
} = await validateRequest(reqValidator.restartSecretRotationV1, req);
|
||||
|
||||
const doc = await secretRotationService.getSecretRotationById({ id });
|
||||
|
||||
const { permission } = await getAuthDataProjectPermissions({
|
||||
authData: req.authData,
|
||||
workspaceId: doc.workspace
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Edit,
|
||||
ProjectPermissionSub.SecretRotation
|
||||
);
|
||||
|
||||
const secretRotation = await secretRotationService.restartSecretRotation({ id });
|
||||
return res.send({ secretRotation });
|
||||
};
|
||||
|
||||
export const deleteSecretRotations = async (req: Request, res: Response) => {
|
||||
const {
|
||||
params: { id }
|
||||
} = await validateRequest(reqValidator.removeSecretRotationV1, req);
|
||||
|
||||
const doc = await secretRotationService.getSecretRotationById({ id });
|
||||
|
||||
const { permission } = await getAuthDataProjectPermissions({
|
||||
authData: req.authData,
|
||||
workspaceId: doc.workspace
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Delete,
|
||||
ProjectPermissionSub.SecretRotation
|
||||
);
|
||||
|
||||
const secretRotations = await secretRotationService.deleteSecretRotation({ id });
|
||||
return res.send({ secretRotations });
|
||||
};
|
||||
|
||||
export const getSecretRotations = async (req: Request, res: Response) => {
|
||||
const {
|
||||
query: { workspaceId }
|
||||
} = await validateRequest(reqValidator.getSecretRotationV1, req);
|
||||
|
||||
const { permission } = await getAuthDataProjectPermissions({
|
||||
authData: req.authData,
|
||||
workspaceId: new Types.ObjectId(workspaceId)
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Read,
|
||||
ProjectPermissionSub.SecretRotation
|
||||
);
|
||||
|
||||
const secretRotations = await secretRotationService.getSecretRotationOfWorkspace(workspaceId);
|
||||
return res.send({ secretRotations });
|
||||
};
|
@ -1,33 +0,0 @@
|
||||
import { Request, Response } from "express";
|
||||
import { Types } from "mongoose";
|
||||
import { validateRequest } from "../../../helpers/validation";
|
||||
import * as reqValidator from "../../validation/secretRotationProvider";
|
||||
import * as secretRotationProviderService from "../../secretRotation/service";
|
||||
import {
|
||||
ProjectPermissionActions,
|
||||
ProjectPermissionSub,
|
||||
getAuthDataProjectPermissions
|
||||
} from "../../services/ProjectRoleService";
|
||||
import { ForbiddenError } from "@casl/ability";
|
||||
|
||||
export const getProviderTemplates = async (req: Request, res: Response) => {
|
||||
const {
|
||||
params: { workspaceId }
|
||||
} = await validateRequest(reqValidator.getSecretRotationProvidersV1, req);
|
||||
|
||||
const { permission } = await getAuthDataProjectPermissions({
|
||||
authData: req.authData,
|
||||
workspaceId: new Types.ObjectId(workspaceId)
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Read,
|
||||
ProjectPermissionSub.SecretRotation
|
||||
);
|
||||
|
||||
const rotationProviderList = await secretRotationProviderService.getProviderTemplate({
|
||||
workspaceId
|
||||
});
|
||||
|
||||
return res.send(rotationProviderList);
|
||||
};
|
@ -1,62 +0,0 @@
|
||||
import { ForbiddenError } from "@casl/ability";
|
||||
import { Request, Response } from "express";
|
||||
import { validateRequest } from "../../../helpers/validation";
|
||||
import {
|
||||
ProjectPermissionActions,
|
||||
ProjectPermissionSub,
|
||||
getAuthDataProjectPermissions
|
||||
} from "../../services/ProjectRoleService";
|
||||
import * as reqValidator from "../../../validation/secretSnapshot";
|
||||
import { ISecretVersion, SecretSnapshot, TFolderRootVersionSchema } from "../../models";
|
||||
|
||||
/**
|
||||
* Return secret snapshot with id [secretSnapshotId]
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
export const getSecretSnapshot = async (req: Request, res: Response) => {
|
||||
const {
|
||||
params: { secretSnapshotId }
|
||||
} = await validateRequest(reqValidator.GetSecretSnapshotV1, req);
|
||||
|
||||
const secretSnapshot = await SecretSnapshot.findById(secretSnapshotId)
|
||||
.lean()
|
||||
.populate<{ secretVersions: ISecretVersion[] }>({
|
||||
path: "secretVersions",
|
||||
populate: {
|
||||
path: "tags",
|
||||
model: "Tag"
|
||||
}
|
||||
})
|
||||
.populate<{ folderVersion: TFolderRootVersionSchema }>("folderVersion");
|
||||
|
||||
if (!secretSnapshot) throw new Error("Failed to find secret snapshot");
|
||||
|
||||
const { permission } = await getAuthDataProjectPermissions({
|
||||
authData: req.authData,
|
||||
workspaceId: secretSnapshot.workspace
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Read,
|
||||
ProjectPermissionSub.SecretRollback
|
||||
);
|
||||
|
||||
const folderId = secretSnapshot.folderId;
|
||||
// to show only the folder required secrets
|
||||
secretSnapshot.secretVersions = secretSnapshot.secretVersions.filter(
|
||||
({ folder }) => folder === folderId
|
||||
);
|
||||
|
||||
secretSnapshot.folderVersion = secretSnapshot?.folderVersion?.nodes?.children?.map(
|
||||
({ id, name }) => ({
|
||||
id,
|
||||
name
|
||||
})
|
||||
) as any;
|
||||
|
||||
return res.status(200).send({
|
||||
secretSnapshot
|
||||
});
|
||||
};
|
@ -1,268 +0,0 @@
|
||||
import { Request, Response } from "express";
|
||||
import { Types } from "mongoose";
|
||||
import { BotOrgService } from "../../../services";
|
||||
import { SSOConfig } from "../../models";
|
||||
import { AuthMethod, MembershipOrg, User } from "../../../models";
|
||||
import { getSSOConfigHelper } from "../../helpers/organizations";
|
||||
import { client } from "../../../config";
|
||||
import { ResourceNotFoundError } from "../../../utils/errors";
|
||||
import { getSiteURL } from "../../../config";
|
||||
import { EELicenseService } from "../../services";
|
||||
import * as reqValidator from "../../../validation/sso";
|
||||
import { validateRequest } from "../../../helpers/validation";
|
||||
import {
|
||||
OrgPermissionActions,
|
||||
OrgPermissionSubjects,
|
||||
getAuthDataOrgPermissions
|
||||
} from "../../services/RoleService";
|
||||
import { ForbiddenError } from "@casl/ability";
|
||||
|
||||
/**
|
||||
* Redirect user to appropriate SSO endpoint after successful authentication
|
||||
* to finish inputting their master key for logging in or signing up
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
export const redirectSSO = async (req: Request, res: Response) => {
|
||||
if (req.isUserCompleted) {
|
||||
return res.redirect(
|
||||
`${await getSiteURL()}/login/sso?token=${encodeURIComponent(req.providerAuthToken)}`
|
||||
);
|
||||
}
|
||||
|
||||
return res.redirect(
|
||||
`${await getSiteURL()}/signup/sso?token=${encodeURIComponent(req.providerAuthToken)}`
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Return organization SAML SSO configuration
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
export const getSSOConfig = async (req: Request, res: Response) => {
|
||||
const {
|
||||
query: { organizationId }
|
||||
} = await validateRequest(reqValidator.GetSsoConfigv1, req);
|
||||
|
||||
const { permission } = await getAuthDataOrgPermissions({
|
||||
authData: req.authData,
|
||||
organizationId: new Types.ObjectId(organizationId)
|
||||
});
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
OrgPermissionActions.Read,
|
||||
OrgPermissionSubjects.Sso
|
||||
);
|
||||
|
||||
const data = await getSSOConfigHelper({
|
||||
organizationId: new Types.ObjectId(organizationId)
|
||||
});
|
||||
|
||||
return res.status(200).send(data);
|
||||
};
|
||||
|
||||
/**
|
||||
* Update organization SAML SSO configuration
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
export const updateSSOConfig = async (req: Request, res: Response) => {
|
||||
const {
|
||||
body: { organizationId, authProvider, isActive, entryPoint, issuer, cert }
|
||||
} = await validateRequest(reqValidator.UpdateSsoConfigv1, req);
|
||||
|
||||
const { permission } = await getAuthDataOrgPermissions({
|
||||
authData: req.authData,
|
||||
organizationId: new Types.ObjectId(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 {
|
||||
ciphertext: encryptedEntryPoint,
|
||||
iv: entryPointIV,
|
||||
tag: entryPointTag
|
||||
} = client.encryptSymmetric(entryPoint, key);
|
||||
|
||||
update.encryptedEntryPoint = encryptedEntryPoint;
|
||||
update.entryPointIV = entryPointIV;
|
||||
update.entryPointTag = entryPointTag;
|
||||
}
|
||||
|
||||
if (issuer) {
|
||||
const {
|
||||
ciphertext: encryptedIssuer,
|
||||
iv: issuerIV,
|
||||
tag: issuerTag
|
||||
} = client.encryptSymmetric(issuer, key);
|
||||
|
||||
update.encryptedIssuer = encryptedIssuer;
|
||||
update.issuerIV = issuerIV;
|
||||
update.issuerTag = issuerTag;
|
||||
}
|
||||
|
||||
if (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"
|
||||
});
|
||||
|
||||
if (update.isActive !== undefined) {
|
||||
const membershipOrgs = await MembershipOrg.find({
|
||||
organization: new Types.ObjectId(organizationId)
|
||||
}).select("user");
|
||||
|
||||
if (update.isActive) {
|
||||
await User.updateMany(
|
||||
{
|
||||
_id: {
|
||||
$in: membershipOrgs.map((membershipOrg) => membershipOrg.user)
|
||||
}
|
||||
},
|
||||
{
|
||||
authMethods: [ssoConfig.authProvider]
|
||||
}
|
||||
);
|
||||
} else {
|
||||
await User.updateMany(
|
||||
{
|
||||
_id: {
|
||||
$in: membershipOrgs.map((membershipOrg) => membershipOrg.user)
|
||||
}
|
||||
},
|
||||
{
|
||||
authMethods: [AuthMethod.EMAIL]
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return res.status(200).send(ssoConfig);
|
||||
};
|
||||
|
||||
/**
|
||||
* Create organization SAML SSO configuration
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
export const createSSOConfig = async (req: Request, res: Response) => {
|
||||
const {
|
||||
body: { organizationId, authProvider, isActive, entryPoint, issuer, cert }
|
||||
} = await validateRequest(reqValidator.CreateSsoConfigv1, req);
|
||||
|
||||
const { permission } = await getAuthDataOrgPermissions({
|
||||
authData: req.authData,
|
||||
organizationId: new Types.ObjectId(organizationId)
|
||||
});
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
OrgPermissionActions.Create,
|
||||
OrgPermissionSubjects.Sso
|
||||
);
|
||||
|
||||
const plan = await EELicenseService.getPlan(new Types.ObjectId(organizationId));
|
||||
|
||||
if (!plan.samlSSO)
|
||||
return res.status(400).send({
|
||||
message:
|
||||
"Failed to create SAML SSO configuration due to plan restriction. Upgrade plan to add SSO configuration."
|
||||
});
|
||||
|
||||
const key = await BotOrgService.getSymmetricKey(new Types.ObjectId(organizationId));
|
||||
|
||||
const {
|
||||
ciphertext: encryptedEntryPoint,
|
||||
iv: entryPointIV,
|
||||
tag: entryPointTag
|
||||
} = client.encryptSymmetric(entryPoint, key);
|
||||
|
||||
const {
|
||||
ciphertext: encryptedIssuer,
|
||||
iv: issuerIV,
|
||||
tag: issuerTag
|
||||
} = client.encryptSymmetric(issuer, key);
|
||||
|
||||
const {
|
||||
ciphertext: encryptedCert,
|
||||
iv: certIV,
|
||||
tag: certTag
|
||||
} = client.encryptSymmetric(cert, key);
|
||||
|
||||
const ssoConfig = await new SSOConfig({
|
||||
organization: new Types.ObjectId(organizationId),
|
||||
authProvider,
|
||||
isActive,
|
||||
encryptedEntryPoint,
|
||||
entryPointIV,
|
||||
entryPointTag,
|
||||
encryptedIssuer,
|
||||
issuerIV,
|
||||
issuerTag,
|
||||
encryptedCert,
|
||||
certIV,
|
||||
certTag
|
||||
}).save();
|
||||
|
||||
return res.status(200).send(ssoConfig);
|
||||
};
|
@ -1,13 +0,0 @@
|
||||
import { Request, Response } from "express";
|
||||
|
||||
/**
|
||||
* Return the ip address of the current user
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
export const getMyIp = (req: Request, res: Response) => {
|
||||
return res.status(200).send({
|
||||
ip: req.authData.ipAddress
|
||||
});
|
||||
}
|
File diff suppressed because it is too large
Load Diff
@ -1,101 +0,0 @@
|
||||
import { Request, Response } from "express";
|
||||
import { Types } from "mongoose";
|
||||
import { APIKeyDataV2 } from "../../../models/apiKeyDataV2";
|
||||
import { validateRequest } from "../../../helpers/validation";
|
||||
import { BadRequestError } from "../../../utils/errors";
|
||||
import * as reqValidator from "../../../validation";
|
||||
import { createToken } from "../../../helpers";
|
||||
import { AuthTokenType } from "../../../variables";
|
||||
import { getAuthSecret } from "../../../config";
|
||||
|
||||
/**
|
||||
* Create API key data v2
|
||||
* @param req
|
||||
* @param res
|
||||
*/
|
||||
export const createAPIKeyData = async (req: Request, res: Response) => {
|
||||
const {
|
||||
body: {
|
||||
name
|
||||
}
|
||||
} = await validateRequest(reqValidator.CreateAPIKeyV3, req);
|
||||
|
||||
const apiKeyData = await new APIKeyDataV2({
|
||||
name,
|
||||
user: req.user._id,
|
||||
usageCount: 0,
|
||||
}).save();
|
||||
|
||||
const apiKey = createToken({
|
||||
payload: {
|
||||
authTokenType: AuthTokenType.API_KEY,
|
||||
apiKeyDataId: apiKeyData._id.toString(),
|
||||
userId: req.user._id.toString()
|
||||
},
|
||||
secret: await getAuthSecret()
|
||||
});
|
||||
|
||||
return res.status(200).send({
|
||||
apiKeyData,
|
||||
apiKey
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Update API key data v2 with id [apiKeyDataId]
|
||||
* @param req
|
||||
* @param res
|
||||
*/
|
||||
export const updateAPIKeyData = async (req: Request, res: Response) => {
|
||||
const {
|
||||
params: { apiKeyDataId },
|
||||
body: {
|
||||
name,
|
||||
}
|
||||
} = await validateRequest(reqValidator.UpdateAPIKeyV3, req);
|
||||
|
||||
const apiKeyData = await APIKeyDataV2.findOneAndUpdate(
|
||||
{
|
||||
_id: new Types.ObjectId(apiKeyDataId),
|
||||
user: req.user._id
|
||||
},
|
||||
{
|
||||
name
|
||||
},
|
||||
{
|
||||
new: true
|
||||
}
|
||||
);
|
||||
|
||||
if (!apiKeyData) throw BadRequestError({
|
||||
message: "Failed to update API key"
|
||||
});
|
||||
|
||||
return res.status(200).send({
|
||||
apiKeyData
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete API key data v2 with id [apiKeyDataId]
|
||||
* @param req
|
||||
* @param res
|
||||
*/
|
||||
export const deleteAPIKeyData = async (req: Request, res: Response) => {
|
||||
const {
|
||||
params: { apiKeyDataId }
|
||||
} = await validateRequest(reqValidator.DeleteAPIKeyV3, req);
|
||||
|
||||
const apiKeyData = await APIKeyDataV2.findOneAndDelete({
|
||||
_id: new Types.ObjectId(apiKeyDataId),
|
||||
user: req.user._id
|
||||
});
|
||||
|
||||
if (!apiKeyData) throw BadRequestError({
|
||||
message: "Failed to delete API key"
|
||||
});
|
||||
|
||||
return res.status(200).send({
|
||||
apiKeyData
|
||||
});
|
||||
}
|
@ -1,5 +0,0 @@
|
||||
import * as apiKeyDataController from "./apiKeyDataController";
|
||||
|
||||
export {
|
||||
apiKeyDataController
|
||||
}
|
@ -1,55 +0,0 @@
|
||||
import { Types } from "mongoose";
|
||||
import _ from "lodash";
|
||||
import { Membership } from "../../models";
|
||||
import { PERMISSION_READ_SECRETS, PERMISSION_WRITE_SECRETS } from "../../variables";
|
||||
|
||||
export const userHasWorkspaceAccess = async (userId: Types.ObjectId, workspaceId: Types.ObjectId, environment: string, action: any) => {
|
||||
const membershipForWorkspace = await Membership.findOne({ workspace: workspaceId, user: userId })
|
||||
if (!membershipForWorkspace) {
|
||||
return false
|
||||
}
|
||||
|
||||
const deniedMembershipPermissions = membershipForWorkspace.deniedPermissions;
|
||||
const isDisallowed = _.some(deniedMembershipPermissions, { environmentSlug: environment, ability: action });
|
||||
|
||||
if (isDisallowed) {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
export const userHasWriteOnlyAbility = async (userId: Types.ObjectId, workspaceId: Types.ObjectId, environment: string) => {
|
||||
const membershipForWorkspace = await Membership.findOne({ workspace: workspaceId, user: userId })
|
||||
if (!membershipForWorkspace) {
|
||||
return false
|
||||
}
|
||||
|
||||
const deniedMembershipPermissions = membershipForWorkspace.deniedPermissions;
|
||||
const isWriteDisallowed = _.some(deniedMembershipPermissions, { environmentSlug: environment, ability: PERMISSION_WRITE_SECRETS });
|
||||
const isReadDisallowed = _.some(deniedMembershipPermissions, { environmentSlug: environment, ability: PERMISSION_READ_SECRETS });
|
||||
|
||||
// case: you have write only if read is blocked and write is not
|
||||
if (isReadDisallowed && !isWriteDisallowed) {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
export const userHasNoAbility = async (userId: Types.ObjectId, workspaceId: Types.ObjectId, environment: string) => {
|
||||
const membershipForWorkspace = await Membership.findOne({ workspace: workspaceId, user: userId })
|
||||
if (!membershipForWorkspace) {
|
||||
return true
|
||||
}
|
||||
|
||||
const deniedMembershipPermissions = membershipForWorkspace.deniedPermissions;
|
||||
const isWriteDisallowed = _.some(deniedMembershipPermissions, { environmentSlug: environment, ability: PERMISSION_WRITE_SECRETS });
|
||||
const isReadBlocked = _.some(deniedMembershipPermissions, { environmentSlug: environment, ability: PERMISSION_READ_SECRETS });
|
||||
|
||||
if (isReadBlocked && isWriteDisallowed) {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
@ -1,64 +0,0 @@
|
||||
import { Types } from "mongoose";
|
||||
import {
|
||||
SSOConfig
|
||||
} from "../models";
|
||||
import {
|
||||
BotOrgService
|
||||
} from "../../services";
|
||||
import { client } from "../../config";
|
||||
import { ValidationError } from "../../utils/errors";
|
||||
|
||||
export const getSSOConfigHelper = async ({
|
||||
organizationId,
|
||||
ssoConfigId
|
||||
}: {
|
||||
organizationId?: Types.ObjectId;
|
||||
ssoConfigId?: Types.ObjectId;
|
||||
}) => {
|
||||
|
||||
if (!organizationId && !ssoConfigId) throw ValidationError({
|
||||
message: "Getting SSO data requires either id of organization or SSO data"
|
||||
});
|
||||
|
||||
const ssoConfig = await SSOConfig.findOne({
|
||||
...(organizationId ? { organization: organizationId } : {}),
|
||||
...(ssoConfigId ? { _id: ssoConfigId } : {})
|
||||
});
|
||||
|
||||
if (!ssoConfig) throw new Error("Failed to find organization SSO data");
|
||||
|
||||
const key = await BotOrgService.getSymmetricKey(
|
||||
ssoConfig.organization
|
||||
);
|
||||
|
||||
const entryPoint = client.decryptSymmetric(
|
||||
ssoConfig.encryptedEntryPoint,
|
||||
key,
|
||||
ssoConfig.entryPointIV,
|
||||
ssoConfig.entryPointTag
|
||||
);
|
||||
|
||||
const issuer = client.decryptSymmetric(
|
||||
ssoConfig.encryptedIssuer,
|
||||
key,
|
||||
ssoConfig.issuerIV,
|
||||
ssoConfig.issuerTag
|
||||
);
|
||||
|
||||
const cert = client.decryptSymmetric(
|
||||
ssoConfig.encryptedCert,
|
||||
key,
|
||||
ssoConfig.certIV,
|
||||
ssoConfig.certTag
|
||||
);
|
||||
|
||||
return ({
|
||||
_id: ssoConfig._id,
|
||||
organization: ssoConfig.organization,
|
||||
authProvider: ssoConfig.authProvider,
|
||||
isActive: ssoConfig.isActive,
|
||||
entryPoint,
|
||||
issuer,
|
||||
cert
|
||||
});
|
||||
}
|
@ -1,122 +0,0 @@
|
||||
import { Types } from "mongoose";
|
||||
import { Secret } from "../../models";
|
||||
import {
|
||||
FolderVersion,
|
||||
ISecretVersion,
|
||||
SecretSnapshot,
|
||||
SecretVersion,
|
||||
} from "../models";
|
||||
|
||||
/**
|
||||
* Save a secret snapshot that is a copy of the current state of secrets in workspace with id
|
||||
* [workspaceId] under a new snapshot with incremented version under the
|
||||
* secretsnapshots collection.
|
||||
* @param {Object} obj
|
||||
* @param {String} obj.workspaceId
|
||||
* @returns {SecretSnapshot} secretSnapshot - new secret snapshot
|
||||
*/
|
||||
const takeSecretSnapshotHelper = async ({
|
||||
workspaceId,
|
||||
environment,
|
||||
folderId = "root",
|
||||
}: {
|
||||
workspaceId: Types.ObjectId;
|
||||
environment: string;
|
||||
folderId?: string;
|
||||
}) => {
|
||||
// get all folder ids
|
||||
const secretIds = (
|
||||
await Secret.find(
|
||||
{
|
||||
workspace: workspaceId,
|
||||
environment,
|
||||
folder: folderId,
|
||||
},
|
||||
"_id"
|
||||
).lean()
|
||||
).map((s) => s._id);
|
||||
|
||||
const latestSecretVersions = (
|
||||
await SecretVersion.aggregate([
|
||||
{
|
||||
$match: {
|
||||
environment,
|
||||
workspace: new Types.ObjectId(workspaceId),
|
||||
secret: {
|
||||
$in: secretIds,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
$group: {
|
||||
_id: "$secret",
|
||||
version: { $max: "$version" },
|
||||
versionId: { $max: "$_id" }, // secret version id
|
||||
},
|
||||
},
|
||||
{
|
||||
$sort: { version: -1 },
|
||||
},
|
||||
]).exec()
|
||||
).map((s) => s.versionId);
|
||||
const latestFolderVersion = await FolderVersion.findOne({
|
||||
environment,
|
||||
workspace: workspaceId,
|
||||
"nodes.id": folderId,
|
||||
}).sort({ "nodes.version": -1 });
|
||||
|
||||
const latestSecretSnapshot = await SecretSnapshot.findOne({
|
||||
workspace: workspaceId,
|
||||
}).sort({ version: -1 });
|
||||
|
||||
const secretSnapshot = await new SecretSnapshot({
|
||||
workspace: workspaceId,
|
||||
environment,
|
||||
version: latestSecretSnapshot ? latestSecretSnapshot.version + 1 : 1,
|
||||
secretVersions: latestSecretVersions,
|
||||
folderId,
|
||||
folderVersion: latestFolderVersion,
|
||||
}).save();
|
||||
|
||||
return secretSnapshot;
|
||||
};
|
||||
|
||||
/**
|
||||
* Add secret versions [secretVersions] to the SecretVersion collection.
|
||||
* @param {Object} obj
|
||||
* @param {Object[]} obj.secretVersions
|
||||
* @returns {SecretVersion[]} newSecretVersions - new secret versions
|
||||
*/
|
||||
const addSecretVersionsHelper = async ({
|
||||
secretVersions,
|
||||
}: {
|
||||
secretVersions: ISecretVersion[];
|
||||
}) => {
|
||||
const newSecretVersions = await SecretVersion.insertMany(secretVersions);
|
||||
|
||||
return newSecretVersions;
|
||||
};
|
||||
|
||||
const markDeletedSecretVersionsHelper = async ({
|
||||
secretIds,
|
||||
}: {
|
||||
secretIds: Types.ObjectId[];
|
||||
}) => {
|
||||
await SecretVersion.updateMany(
|
||||
{
|
||||
secret: { $in: secretIds },
|
||||
},
|
||||
{
|
||||
isDeleted: true,
|
||||
},
|
||||
{
|
||||
new: true,
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
export {
|
||||
takeSecretSnapshotHelper,
|
||||
addSecretVersionsHelper,
|
||||
markDeletedSecretVersionsHelper,
|
||||
};
|
@ -1,82 +0,0 @@
|
||||
import { Types } from "mongoose";
|
||||
import { SecretVersion } from "../models";
|
||||
|
||||
/**
|
||||
* Return latest secret versions for secrets with ids [secretIds]
|
||||
* @param {Object} obj
|
||||
* @param {Object} obj.secretIds = ids of secrets to get latest versions for
|
||||
* @returns
|
||||
*/
|
||||
const getLatestSecretVersionIds = async ({
|
||||
secretIds,
|
||||
}: {
|
||||
secretIds: Types.ObjectId[];
|
||||
}) => {
|
||||
const latestSecretVersionIds = await SecretVersion.aggregate([
|
||||
{
|
||||
$match: {
|
||||
secret: {
|
||||
$in: secretIds,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
$group: {
|
||||
_id: "$secret",
|
||||
version: { $max: "$version" },
|
||||
versionId: { $max: "$_id" }, // id of latest secret version
|
||||
},
|
||||
},
|
||||
{
|
||||
$sort: { version: -1 },
|
||||
},
|
||||
]).exec();
|
||||
|
||||
return latestSecretVersionIds;
|
||||
};
|
||||
|
||||
/**
|
||||
* Return latest [n] secret versions for secrets with ids [secretIds]
|
||||
* @param {Object} obj
|
||||
* @param {Object} obj.secretIds = ids of secrets to get latest versions for
|
||||
* @param {Number} obj.n - number of latest secret versions to return for each secret
|
||||
* @returns
|
||||
*/
|
||||
const getLatestNSecretSecretVersionIds = async ({
|
||||
secretIds,
|
||||
n,
|
||||
}: {
|
||||
secretIds: Types.ObjectId[];
|
||||
n: number;
|
||||
}) => {
|
||||
// TODO: optimize query
|
||||
const latestNSecretVersions = await SecretVersion.aggregate([
|
||||
{
|
||||
$match: {
|
||||
secret: {
|
||||
$in: secretIds,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
$sort: { version: -1 },
|
||||
},
|
||||
{
|
||||
$group: {
|
||||
_id: "$secret",
|
||||
versions: { $push: "$$ROOT" },
|
||||
},
|
||||
},
|
||||
{
|
||||
$project: {
|
||||
_id: 0,
|
||||
secret: "$_id",
|
||||
versions: { $slice: ["$versions", n] },
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
return latestNSecretVersions;
|
||||
};
|
||||
|
||||
export { getLatestSecretVersionIds, getLatestNSecretSecretVersionIds };
|
@ -1,70 +0,0 @@
|
||||
import { Schema, Types, model } from "mongoose";
|
||||
import { ActorType, EventType, UserAgentType } from "./enums";
|
||||
import { Actor, Event } from "./types";
|
||||
|
||||
export interface IAuditLog {
|
||||
actor: Actor;
|
||||
organization: Types.ObjectId;
|
||||
workspace: Types.ObjectId;
|
||||
ipAddress: string;
|
||||
event: Event;
|
||||
userAgent: string;
|
||||
userAgentType: UserAgentType;
|
||||
expiresAt?: Date;
|
||||
}
|
||||
|
||||
const auditLogSchema = new Schema<IAuditLog>(
|
||||
{
|
||||
actor: {
|
||||
type: {
|
||||
type: String,
|
||||
enum: ActorType,
|
||||
required: true
|
||||
},
|
||||
metadata: {
|
||||
type: Schema.Types.Mixed
|
||||
}
|
||||
},
|
||||
organization: {
|
||||
type: Schema.Types.ObjectId,
|
||||
required: false
|
||||
},
|
||||
workspace: {
|
||||
type: Schema.Types.ObjectId,
|
||||
required: false,
|
||||
index: true
|
||||
},
|
||||
ipAddress: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
event: {
|
||||
type: {
|
||||
type: String,
|
||||
enum: EventType,
|
||||
required: true
|
||||
},
|
||||
metadata: {
|
||||
type: Schema.Types.Mixed
|
||||
}
|
||||
},
|
||||
userAgent: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
userAgentType: {
|
||||
type: String,
|
||||
enum: UserAgentType,
|
||||
required: true
|
||||
},
|
||||
expiresAt: {
|
||||
type: Date,
|
||||
expires: 0
|
||||
}
|
||||
},
|
||||
{
|
||||
timestamps: true
|
||||
}
|
||||
);
|
||||
|
||||
export const AuditLog = model<IAuditLog>("AuditLog", auditLogSchema);
|
@ -1,69 +0,0 @@
|
||||
export enum ActorType { // would extend to AWS, Azure, ...
|
||||
USER = "user", // userIdentity
|
||||
SERVICE = "service",
|
||||
IDENTITY = "identity"
|
||||
}
|
||||
|
||||
export enum UserAgentType {
|
||||
WEB = "web",
|
||||
CLI = "cli",
|
||||
K8_OPERATOR = "k8-operator",
|
||||
TERRAFORM = "terraform",
|
||||
OTHER = "other",
|
||||
PYTHON_SDK = "InfisicalPythonSDK",
|
||||
NODE_SDK = "InfisicalNodeSDK"
|
||||
}
|
||||
|
||||
export enum EventType {
|
||||
GET_SECRETS = "get-secrets",
|
||||
GET_SECRET = "get-secret",
|
||||
REVEAL_SECRET = "reveal-secret",
|
||||
CREATE_SECRET = "create-secret",
|
||||
CREATE_SECRETS = "create-secrets",
|
||||
UPDATE_SECRET = "update-secret",
|
||||
UPDATE_SECRETS = "update-secrets",
|
||||
DELETE_SECRET = "delete-secret",
|
||||
DELETE_SECRETS = "delete-secrets",
|
||||
GET_WORKSPACE_KEY = "get-workspace-key",
|
||||
AUTHORIZE_INTEGRATION = "authorize-integration",
|
||||
UNAUTHORIZE_INTEGRATION = "unauthorize-integration",
|
||||
CREATE_INTEGRATION = "create-integration",
|
||||
DELETE_INTEGRATION = "delete-integration",
|
||||
ADD_TRUSTED_IP = "add-trusted-ip",
|
||||
UPDATE_TRUSTED_IP = "update-trusted-ip",
|
||||
DELETE_TRUSTED_IP = "delete-trusted-ip",
|
||||
CREATE_SERVICE_TOKEN = "create-service-token", // v2
|
||||
DELETE_SERVICE_TOKEN = "delete-service-token", // v2
|
||||
CREATE_IDENTITY = "create-identity",
|
||||
UPDATE_IDENTITY = "update-identity",
|
||||
DELETE_IDENTITY = "delete-identity",
|
||||
LOGIN_IDENTITY_UNIVERSAL_AUTH = "login-identity-universal-auth",
|
||||
ADD_IDENTITY_UNIVERSAL_AUTH = "add-identity-universal-auth",
|
||||
UPDATE_IDENTITY_UNIVERSAL_AUTH = "update-identity-universal-auth",
|
||||
GET_IDENTITY_UNIVERSAL_AUTH = "get-identity-universal-auth",
|
||||
CREATE_IDENTITY_UNIVERSAL_AUTH_CLIENT_SECRET = "create-identity-universal-auth-client-secret",
|
||||
REVOKE_IDENTITY_UNIVERSAL_AUTH_CLIENT_SECRET = "revoke-identity-universal-auth-client-secret",
|
||||
GET_IDENTITY_UNIVERSAL_AUTH_CLIENT_SECRETS = "get-identity-universal-auth-client-secret",
|
||||
CREATE_ENVIRONMENT = "create-environment",
|
||||
UPDATE_ENVIRONMENT = "update-environment",
|
||||
DELETE_ENVIRONMENT = "delete-environment",
|
||||
ADD_WORKSPACE_MEMBER = "add-workspace-member",
|
||||
ADD_BATCH_WORKSPACE_MEMBER = "add-workspace-members",
|
||||
REMOVE_WORKSPACE_MEMBER = "remove-workspace-member",
|
||||
CREATE_FOLDER = "create-folder",
|
||||
UPDATE_FOLDER = "update-folder",
|
||||
DELETE_FOLDER = "delete-folder",
|
||||
CREATE_WEBHOOK = "create-webhook",
|
||||
UPDATE_WEBHOOK_STATUS = "update-webhook-status",
|
||||
DELETE_WEBHOOK = "delete-webhook",
|
||||
GET_SECRET_IMPORTS = "get-secret-imports",
|
||||
CREATE_SECRET_IMPORT = "create-secret-import",
|
||||
UPDATE_SECRET_IMPORT = "update-secret-import",
|
||||
DELETE_SECRET_IMPORT = "delete-secret-import",
|
||||
UPDATE_USER_WORKSPACE_ROLE = "update-user-workspace-role",
|
||||
UPDATE_USER_WORKSPACE_DENIED_PERMISSIONS = "update-user-workspace-denied-permissions",
|
||||
SECRET_APPROVAL_MERGED = "secret-approval-merged",
|
||||
SECRET_APPROVAL_REQUEST = "secret-approval-request",
|
||||
SECRET_APPROVAL_CLOSED = "secret-approval-closed",
|
||||
SECRET_APPROVAL_REOPENED = "secret-approval-reopened"
|
||||
}
|
@ -1,3 +0,0 @@
|
||||
export * from "./auditLog";
|
||||
export * from "./enums";
|
||||
export * from "./types";
|
@ -1,585 +0,0 @@
|
||||
import { ActorType, EventType } from "./enums";
|
||||
import { IIdentityTrustedIp } from "../../../models";
|
||||
|
||||
interface UserActorMetadata {
|
||||
userId: string;
|
||||
email: string;
|
||||
}
|
||||
|
||||
interface ServiceActorMetadata {
|
||||
serviceId: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
interface IdentityActorMetadata {
|
||||
identityId: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface UserActor {
|
||||
type: ActorType.USER;
|
||||
metadata: UserActorMetadata;
|
||||
}
|
||||
|
||||
export interface ServiceActor {
|
||||
type: ActorType.SERVICE;
|
||||
metadata: ServiceActorMetadata;
|
||||
}
|
||||
|
||||
export interface IdentityActor {
|
||||
type: ActorType.IDENTITY;
|
||||
metadata: IdentityActorMetadata;
|
||||
}
|
||||
|
||||
export type Actor = UserActor | ServiceActor | IdentityActor;
|
||||
|
||||
interface GetSecretsEvent {
|
||||
type: EventType.GET_SECRETS;
|
||||
metadata: {
|
||||
environment: string;
|
||||
secretPath: string;
|
||||
numberOfSecrets: number;
|
||||
};
|
||||
}
|
||||
|
||||
interface GetSecretEvent {
|
||||
type: EventType.GET_SECRET;
|
||||
metadata: {
|
||||
environment: string;
|
||||
secretPath: string;
|
||||
secretId: string;
|
||||
secretKey: string;
|
||||
secretVersion: number;
|
||||
};
|
||||
}
|
||||
|
||||
interface CreateSecretEvent {
|
||||
type: EventType.CREATE_SECRET;
|
||||
metadata: {
|
||||
environment: string;
|
||||
secretPath: string;
|
||||
secretId: string;
|
||||
secretKey: string;
|
||||
secretVersion: number;
|
||||
};
|
||||
}
|
||||
|
||||
interface CreateSecretBatchEvent {
|
||||
type: EventType.CREATE_SECRETS;
|
||||
metadata: {
|
||||
environment: string;
|
||||
secretPath: string;
|
||||
secrets: Array<{ secretId: string; secretKey: string; secretVersion: number }>;
|
||||
};
|
||||
}
|
||||
|
||||
interface UpdateSecretEvent {
|
||||
type: EventType.UPDATE_SECRET;
|
||||
metadata: {
|
||||
environment: string;
|
||||
secretPath: string;
|
||||
secretId: string;
|
||||
secretKey: string;
|
||||
secretVersion: number;
|
||||
};
|
||||
}
|
||||
|
||||
interface UpdateSecretBatchEvent {
|
||||
type: EventType.UPDATE_SECRETS;
|
||||
metadata: {
|
||||
environment: string;
|
||||
secretPath: string;
|
||||
secrets: Array<{ secretId: string; secretKey: string; secretVersion: number }>;
|
||||
};
|
||||
}
|
||||
|
||||
interface DeleteSecretEvent {
|
||||
type: EventType.DELETE_SECRET;
|
||||
metadata: {
|
||||
environment: string;
|
||||
secretPath: string;
|
||||
secretId: string;
|
||||
secretKey: string;
|
||||
secretVersion: number;
|
||||
};
|
||||
}
|
||||
|
||||
interface DeleteSecretBatchEvent {
|
||||
type: EventType.DELETE_SECRETS;
|
||||
metadata: {
|
||||
environment: string;
|
||||
secretPath: string;
|
||||
secrets: Array<{ secretId: string; secretKey: string; secretVersion: number }>;
|
||||
};
|
||||
}
|
||||
|
||||
interface GetWorkspaceKeyEvent {
|
||||
type: EventType.GET_WORKSPACE_KEY;
|
||||
metadata: {
|
||||
keyId: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface AuthorizeIntegrationEvent {
|
||||
type: EventType.AUTHORIZE_INTEGRATION;
|
||||
metadata: {
|
||||
integration: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface UnauthorizeIntegrationEvent {
|
||||
type: EventType.UNAUTHORIZE_INTEGRATION;
|
||||
metadata: {
|
||||
integration: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface CreateIntegrationEvent {
|
||||
type: EventType.CREATE_INTEGRATION;
|
||||
metadata: {
|
||||
integrationId: string;
|
||||
integration: string; // TODO: fix type
|
||||
environment: string;
|
||||
secretPath: string;
|
||||
url?: string;
|
||||
app?: string;
|
||||
appId?: string;
|
||||
targetEnvironment?: string;
|
||||
targetEnvironmentId?: string;
|
||||
targetService?: string;
|
||||
targetServiceId?: string;
|
||||
path?: string;
|
||||
region?: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface DeleteIntegrationEvent {
|
||||
type: EventType.DELETE_INTEGRATION;
|
||||
metadata: {
|
||||
integrationId: string;
|
||||
integration: string; // TODO: fix type
|
||||
environment: string;
|
||||
secretPath: string;
|
||||
url?: string;
|
||||
app?: string;
|
||||
appId?: string;
|
||||
targetEnvironment?: string;
|
||||
targetEnvironmentId?: string;
|
||||
targetService?: string;
|
||||
targetServiceId?: string;
|
||||
path?: string;
|
||||
region?: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface AddTrustedIPEvent {
|
||||
type: EventType.ADD_TRUSTED_IP;
|
||||
metadata: {
|
||||
trustedIpId: string;
|
||||
ipAddress: string;
|
||||
prefix?: number;
|
||||
};
|
||||
}
|
||||
|
||||
interface UpdateTrustedIPEvent {
|
||||
type: EventType.UPDATE_TRUSTED_IP;
|
||||
metadata: {
|
||||
trustedIpId: string;
|
||||
ipAddress: string;
|
||||
prefix?: number;
|
||||
};
|
||||
}
|
||||
|
||||
interface DeleteTrustedIPEvent {
|
||||
type: EventType.DELETE_TRUSTED_IP;
|
||||
metadata: {
|
||||
trustedIpId: string;
|
||||
ipAddress: string;
|
||||
prefix?: number;
|
||||
};
|
||||
}
|
||||
|
||||
interface CreateServiceTokenEvent {
|
||||
type: EventType.CREATE_SERVICE_TOKEN;
|
||||
metadata: {
|
||||
name: string;
|
||||
scopes: Array<{
|
||||
environment: string;
|
||||
secretPath: string;
|
||||
}>;
|
||||
};
|
||||
}
|
||||
|
||||
interface DeleteServiceTokenEvent {
|
||||
type: EventType.DELETE_SERVICE_TOKEN;
|
||||
metadata: {
|
||||
name: string;
|
||||
scopes: Array<{
|
||||
environment: string;
|
||||
secretPath: string;
|
||||
}>;
|
||||
};
|
||||
}
|
||||
|
||||
interface CreateIdentityEvent { // note: currently not logging org-role
|
||||
type: EventType.CREATE_IDENTITY;
|
||||
metadata: {
|
||||
identityId: string;
|
||||
name: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface UpdateIdentityEvent {
|
||||
type: EventType.UPDATE_IDENTITY;
|
||||
metadata: {
|
||||
identityId: string;
|
||||
name?: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface DeleteIdentityEvent {
|
||||
type: EventType.DELETE_IDENTITY;
|
||||
metadata: {
|
||||
identityId: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface LoginIdentityUniversalAuthEvent {
|
||||
type: EventType.LOGIN_IDENTITY_UNIVERSAL_AUTH ;
|
||||
metadata: {
|
||||
identityId: string;
|
||||
identityUniversalAuthId: string;
|
||||
clientSecretId: string;
|
||||
identityAccessTokenId: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface AddIdentityUniversalAuthEvent {
|
||||
type: EventType.ADD_IDENTITY_UNIVERSAL_AUTH;
|
||||
metadata: {
|
||||
identityId: string;
|
||||
clientSecretTrustedIps: Array<IIdentityTrustedIp>;
|
||||
accessTokenTTL: number;
|
||||
accessTokenMaxTTL: number;
|
||||
accessTokenNumUsesLimit: number;
|
||||
accessTokenTrustedIps: Array<IIdentityTrustedIp>;
|
||||
};
|
||||
}
|
||||
|
||||
interface UpdateIdentityUniversalAuthEvent {
|
||||
type: EventType.UPDATE_IDENTITY_UNIVERSAL_AUTH;
|
||||
metadata: {
|
||||
identityId: string;
|
||||
clientSecretTrustedIps?: Array<IIdentityTrustedIp>;
|
||||
accessTokenTTL?: number;
|
||||
accessTokenMaxTTL?: number;
|
||||
accessTokenNumUsesLimit?: number;
|
||||
accessTokenTrustedIps?: Array<IIdentityTrustedIp>;
|
||||
};
|
||||
}
|
||||
|
||||
interface GetIdentityUniversalAuthEvent {
|
||||
type: EventType.GET_IDENTITY_UNIVERSAL_AUTH;
|
||||
metadata: {
|
||||
identityId: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface CreateIdentityUniversalAuthClientSecretEvent {
|
||||
type: EventType.CREATE_IDENTITY_UNIVERSAL_AUTH_CLIENT_SECRET ;
|
||||
metadata: {
|
||||
identityId: string;
|
||||
clientSecretId: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface GetIdentityUniversalAuthClientSecretsEvent {
|
||||
type: EventType.GET_IDENTITY_UNIVERSAL_AUTH_CLIENT_SECRETS;
|
||||
metadata: {
|
||||
identityId: string;
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
interface RevokeIdentityUniversalAuthClientSecretEvent {
|
||||
type: EventType.REVOKE_IDENTITY_UNIVERSAL_AUTH_CLIENT_SECRET ;
|
||||
metadata: {
|
||||
identityId: string;
|
||||
clientSecretId: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface CreateEnvironmentEvent {
|
||||
type: EventType.CREATE_ENVIRONMENT;
|
||||
metadata: {
|
||||
name: string;
|
||||
slug: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface UpdateEnvironmentEvent {
|
||||
type: EventType.UPDATE_ENVIRONMENT;
|
||||
metadata: {
|
||||
oldName: string;
|
||||
newName: string;
|
||||
oldSlug: string;
|
||||
newSlug: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface DeleteEnvironmentEvent {
|
||||
type: EventType.DELETE_ENVIRONMENT;
|
||||
metadata: {
|
||||
name: string;
|
||||
slug: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface AddWorkspaceMemberEvent {
|
||||
type: EventType.ADD_WORKSPACE_MEMBER;
|
||||
metadata: {
|
||||
userId: string;
|
||||
email: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface AddBatchWorkspaceMemberEvent {
|
||||
type: EventType.ADD_BATCH_WORKSPACE_MEMBER;
|
||||
metadata: Array<{
|
||||
userId: string;
|
||||
email: string;
|
||||
}>;
|
||||
}
|
||||
|
||||
interface RemoveWorkspaceMemberEvent {
|
||||
type: EventType.REMOVE_WORKSPACE_MEMBER;
|
||||
metadata: {
|
||||
userId: string;
|
||||
email: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface CreateFolderEvent {
|
||||
type: EventType.CREATE_FOLDER;
|
||||
metadata: {
|
||||
environment: string;
|
||||
folderId: string;
|
||||
folderName: string;
|
||||
folderPath: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface UpdateFolderEvent {
|
||||
type: EventType.UPDATE_FOLDER;
|
||||
metadata: {
|
||||
environment: string;
|
||||
folderId: string;
|
||||
oldFolderName: string;
|
||||
newFolderName: string;
|
||||
folderPath: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface DeleteFolderEvent {
|
||||
type: EventType.DELETE_FOLDER;
|
||||
metadata: {
|
||||
environment: string;
|
||||
folderId: string;
|
||||
folderName: string;
|
||||
folderPath: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface CreateWebhookEvent {
|
||||
type: EventType.CREATE_WEBHOOK;
|
||||
metadata: {
|
||||
webhookId: string;
|
||||
environment: string;
|
||||
secretPath: string;
|
||||
webhookUrl: string;
|
||||
isDisabled: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
interface UpdateWebhookStatusEvent {
|
||||
type: EventType.UPDATE_WEBHOOK_STATUS;
|
||||
metadata: {
|
||||
webhookId: string;
|
||||
environment: string;
|
||||
secretPath: string;
|
||||
webhookUrl: string;
|
||||
isDisabled: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
interface DeleteWebhookEvent {
|
||||
type: EventType.DELETE_WEBHOOK;
|
||||
metadata: {
|
||||
webhookId: string;
|
||||
environment: string;
|
||||
secretPath: string;
|
||||
webhookUrl: string;
|
||||
isDisabled: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
interface GetSecretImportsEvent {
|
||||
type: EventType.GET_SECRET_IMPORTS;
|
||||
metadata: {
|
||||
environment: string;
|
||||
secretImportId: string;
|
||||
folderId: string;
|
||||
numberOfImports: number;
|
||||
};
|
||||
}
|
||||
|
||||
interface CreateSecretImportEvent {
|
||||
type: EventType.CREATE_SECRET_IMPORT;
|
||||
metadata: {
|
||||
secretImportId: string;
|
||||
folderId: string;
|
||||
importFromEnvironment: string;
|
||||
importFromSecretPath: string;
|
||||
importToEnvironment: string;
|
||||
importToSecretPath: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface UpdateSecretImportEvent {
|
||||
type: EventType.UPDATE_SECRET_IMPORT;
|
||||
metadata: {
|
||||
secretImportId: string;
|
||||
folderId: string;
|
||||
importToEnvironment: string;
|
||||
importToSecretPath: string;
|
||||
orderBefore: {
|
||||
environment: string;
|
||||
secretPath: string;
|
||||
}[];
|
||||
orderAfter: {
|
||||
environment: string;
|
||||
secretPath: string;
|
||||
}[];
|
||||
};
|
||||
}
|
||||
|
||||
interface DeleteSecretImportEvent {
|
||||
type: EventType.DELETE_SECRET_IMPORT;
|
||||
metadata: {
|
||||
secretImportId: string;
|
||||
folderId: string;
|
||||
importFromEnvironment: string;
|
||||
importFromSecretPath: string;
|
||||
importToEnvironment: string;
|
||||
importToSecretPath: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface UpdateUserRole {
|
||||
type: EventType.UPDATE_USER_WORKSPACE_ROLE;
|
||||
metadata: {
|
||||
userId: string;
|
||||
email: string;
|
||||
oldRole: string;
|
||||
newRole: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface UpdateUserDeniedPermissions {
|
||||
type: EventType.UPDATE_USER_WORKSPACE_DENIED_PERMISSIONS;
|
||||
metadata: {
|
||||
userId: string;
|
||||
email: string;
|
||||
deniedPermissions: {
|
||||
environmentSlug: string;
|
||||
ability: string;
|
||||
}[];
|
||||
};
|
||||
}
|
||||
interface SecretApprovalMerge {
|
||||
type: EventType.SECRET_APPROVAL_MERGED;
|
||||
metadata: {
|
||||
mergedBy: string;
|
||||
secretApprovalRequestSlug: string;
|
||||
secretApprovalRequestId: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface SecretApprovalClosed {
|
||||
type: EventType.SECRET_APPROVAL_CLOSED;
|
||||
metadata: {
|
||||
closedBy: string;
|
||||
secretApprovalRequestSlug: string;
|
||||
secretApprovalRequestId: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface SecretApprovalReopened {
|
||||
type: EventType.SECRET_APPROVAL_REOPENED;
|
||||
metadata: {
|
||||
reopenedBy: string;
|
||||
secretApprovalRequestSlug: string;
|
||||
secretApprovalRequestId: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface SecretApprovalRequest {
|
||||
type: EventType.SECRET_APPROVAL_REQUEST;
|
||||
metadata: {
|
||||
committedBy: string;
|
||||
secretApprovalRequestSlug: string;
|
||||
secretApprovalRequestId: string;
|
||||
};
|
||||
}
|
||||
|
||||
export type Event =
|
||||
| GetSecretsEvent
|
||||
| GetSecretEvent
|
||||
| CreateSecretEvent
|
||||
| CreateSecretBatchEvent
|
||||
| UpdateSecretEvent
|
||||
| UpdateSecretBatchEvent
|
||||
| DeleteSecretEvent
|
||||
| DeleteSecretBatchEvent
|
||||
| GetWorkspaceKeyEvent
|
||||
| AuthorizeIntegrationEvent
|
||||
| UnauthorizeIntegrationEvent
|
||||
| CreateIntegrationEvent
|
||||
| DeleteIntegrationEvent
|
||||
| AddTrustedIPEvent
|
||||
| UpdateTrustedIPEvent
|
||||
| DeleteTrustedIPEvent
|
||||
| CreateServiceTokenEvent
|
||||
| DeleteServiceTokenEvent
|
||||
| CreateIdentityEvent
|
||||
| UpdateIdentityEvent
|
||||
| DeleteIdentityEvent
|
||||
| LoginIdentityUniversalAuthEvent
|
||||
| AddIdentityUniversalAuthEvent
|
||||
| UpdateIdentityUniversalAuthEvent
|
||||
| GetIdentityUniversalAuthEvent
|
||||
| CreateIdentityUniversalAuthClientSecretEvent
|
||||
| GetIdentityUniversalAuthClientSecretsEvent
|
||||
| RevokeIdentityUniversalAuthClientSecretEvent
|
||||
| CreateEnvironmentEvent
|
||||
| UpdateEnvironmentEvent
|
||||
| DeleteEnvironmentEvent
|
||||
| AddWorkspaceMemberEvent
|
||||
| AddBatchWorkspaceMemberEvent
|
||||
| RemoveWorkspaceMemberEvent
|
||||
| CreateFolderEvent
|
||||
| UpdateFolderEvent
|
||||
| DeleteFolderEvent
|
||||
| CreateWebhookEvent
|
||||
| UpdateWebhookStatusEvent
|
||||
| DeleteWebhookEvent
|
||||
| GetSecretImportsEvent
|
||||
| CreateSecretImportEvent
|
||||
| UpdateSecretImportEvent
|
||||
| DeleteSecretImportEvent
|
||||
| UpdateUserRole
|
||||
| UpdateUserDeniedPermissions
|
||||
| SecretApprovalMerge
|
||||
| SecretApprovalClosed
|
||||
| SecretApprovalRequest
|
||||
| SecretApprovalReopened;
|
@ -1,58 +0,0 @@
|
||||
import { Schema, Types, model } from "mongoose";
|
||||
|
||||
export type TFolderRootVersionSchema = {
|
||||
_id: Types.ObjectId;
|
||||
workspace: Types.ObjectId;
|
||||
environment: string;
|
||||
nodes: TFolderVersionSchema;
|
||||
};
|
||||
|
||||
export type TFolderVersionSchema = {
|
||||
id: string;
|
||||
name: string;
|
||||
version: number;
|
||||
children: TFolderVersionSchema[];
|
||||
};
|
||||
|
||||
const folderVersionSchema = new Schema<TFolderVersionSchema>({
|
||||
id: {
|
||||
required: true,
|
||||
type: String,
|
||||
default: "root",
|
||||
},
|
||||
name: {
|
||||
required: true,
|
||||
type: String,
|
||||
default: "root",
|
||||
},
|
||||
version: {
|
||||
required: true,
|
||||
type: Number,
|
||||
default: 1,
|
||||
},
|
||||
});
|
||||
|
||||
folderVersionSchema.add({ children: [folderVersionSchema] });
|
||||
|
||||
const folderRootVersionSchema = new Schema<TFolderRootVersionSchema>(
|
||||
{
|
||||
workspace: {
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: "Workspace",
|
||||
required: true,
|
||||
},
|
||||
environment: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
nodes: folderVersionSchema,
|
||||
},
|
||||
{
|
||||
timestamps: true,
|
||||
}
|
||||
);
|
||||
|
||||
export const FolderVersion = model<TFolderRootVersionSchema>(
|
||||
"FolderVersion",
|
||||
folderRootVersionSchema
|
||||
);
|
@ -1,32 +0,0 @@
|
||||
import { Schema, Types, model } from "mongoose";
|
||||
|
||||
type GitAppInstallationSession = {
|
||||
id: string;
|
||||
sessionId: string;
|
||||
organization: Types.ObjectId;
|
||||
user: Types.ObjectId;
|
||||
}
|
||||
|
||||
const gitAppInstallationSession = new Schema<GitAppInstallationSession>({
|
||||
id: {
|
||||
required: true,
|
||||
type: String,
|
||||
},
|
||||
sessionId: {
|
||||
type: String,
|
||||
required: true,
|
||||
unique: true
|
||||
},
|
||||
organization: {
|
||||
type: Schema.Types.ObjectId,
|
||||
required: true,
|
||||
unique: true
|
||||
},
|
||||
user: {
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: "User"
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
export const GitAppInstallationSession = model<GitAppInstallationSession>("git_app_installation_session", gitAppInstallationSession);
|
@ -1,29 +0,0 @@
|
||||
import { Schema, model } from "mongoose";
|
||||
|
||||
type Installation = {
|
||||
installationId: string
|
||||
organizationId: string
|
||||
user: Schema.Types.ObjectId
|
||||
};
|
||||
|
||||
|
||||
const gitAppOrganizationInstallation = new Schema<Installation>({
|
||||
installationId: {
|
||||
type: String,
|
||||
required: true,
|
||||
unique: true
|
||||
},
|
||||
organizationId: {
|
||||
type: String,
|
||||
required: true,
|
||||
unique: true
|
||||
},
|
||||
user: {
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: "User",
|
||||
required: true,
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
export const GitAppOrganizationInstallation = model<Installation>("git_app_organization_installation", gitAppOrganizationInstallation);
|
@ -1,150 +0,0 @@
|
||||
import { Schema, model } from "mongoose";
|
||||
|
||||
export const STATUS_RESOLVED_FALSE_POSITIVE = "RESOLVED_FALSE_POSITIVE";
|
||||
export const STATUS_RESOLVED_REVOKED = "RESOLVED_REVOKED";
|
||||
export const STATUS_RESOLVED_NOT_REVOKED = "RESOLVED_NOT_REVOKED";
|
||||
export const STATUS_UNRESOLVED = "UNRESOLVED";
|
||||
|
||||
export type IGitRisks = {
|
||||
id: string;
|
||||
description: string;
|
||||
startLine: string;
|
||||
endLine: string;
|
||||
startColumn: string;
|
||||
endColumn: string;
|
||||
match: string;
|
||||
secret: string;
|
||||
file: string;
|
||||
symlinkFile: string;
|
||||
commit: string;
|
||||
entropy: string;
|
||||
author: string;
|
||||
email: string;
|
||||
date: string;
|
||||
message: string;
|
||||
tags: string[];
|
||||
ruleID: string;
|
||||
fingerprint: string;
|
||||
fingerPrintWithoutCommitId: string
|
||||
|
||||
isFalsePositive: boolean; // New field for marking risks as false positives
|
||||
isResolved: boolean; // New field for marking risks as resolved
|
||||
riskOwner: string | null; // New field for setting a risk owner (nullable string)
|
||||
installationId: string,
|
||||
repositoryId: string,
|
||||
repositoryLink: string
|
||||
repositoryFullName: string
|
||||
status: string
|
||||
pusher: {
|
||||
name: string,
|
||||
email: string
|
||||
},
|
||||
organization: Schema.Types.ObjectId,
|
||||
}
|
||||
|
||||
const gitRisks = new Schema<IGitRisks>({
|
||||
id: {
|
||||
type: String,
|
||||
},
|
||||
description: {
|
||||
type: String,
|
||||
},
|
||||
startLine: {
|
||||
type: String,
|
||||
},
|
||||
endLine: {
|
||||
type: String,
|
||||
},
|
||||
startColumn: {
|
||||
type: String,
|
||||
},
|
||||
endColumn: {
|
||||
type: String,
|
||||
},
|
||||
file: {
|
||||
type: String,
|
||||
},
|
||||
symlinkFile: {
|
||||
type: String,
|
||||
},
|
||||
commit: {
|
||||
type: String,
|
||||
},
|
||||
entropy: {
|
||||
type: String,
|
||||
},
|
||||
author: {
|
||||
type: String,
|
||||
},
|
||||
email: {
|
||||
type: String,
|
||||
},
|
||||
date: {
|
||||
type: String,
|
||||
},
|
||||
message: {
|
||||
type: String,
|
||||
},
|
||||
tags: {
|
||||
type: [String],
|
||||
},
|
||||
ruleID: {
|
||||
type: String,
|
||||
},
|
||||
fingerprint: {
|
||||
type: String,
|
||||
unique: true
|
||||
},
|
||||
fingerPrintWithoutCommitId: {
|
||||
type: String,
|
||||
},
|
||||
isFalsePositive: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
isResolved: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
riskOwner: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
installationId: {
|
||||
type: String,
|
||||
require: true
|
||||
},
|
||||
repositoryId: {
|
||||
type: String
|
||||
},
|
||||
repositoryLink: {
|
||||
type: String
|
||||
},
|
||||
repositoryFullName: {
|
||||
type: String
|
||||
},
|
||||
pusher: {
|
||||
name: {
|
||||
type: String
|
||||
},
|
||||
email: {
|
||||
type: String
|
||||
},
|
||||
},
|
||||
organization: {
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: "Organization",
|
||||
},
|
||||
status: {
|
||||
type: String,
|
||||
enum: [
|
||||
STATUS_RESOLVED_FALSE_POSITIVE,
|
||||
STATUS_RESOLVED_REVOKED,
|
||||
STATUS_RESOLVED_NOT_REVOKED,
|
||||
STATUS_UNRESOLVED
|
||||
],
|
||||
default: STATUS_UNRESOLVED
|
||||
}
|
||||
}, { timestamps: true });
|
||||
|
||||
export const GitRisks = model<IGitRisks>("GitRisks", gitRisks);
|
@ -1,12 +0,0 @@
|
||||
export * from "./secretSnapshot";
|
||||
export * from "./secretVersion";
|
||||
export * from "./folderVersion";
|
||||
export * from "./role";
|
||||
export * from "./ssoConfig";
|
||||
export * from "./trustedIp";
|
||||
export * from "./auditLog";
|
||||
export * from "./gitRisks";
|
||||
export * from "./gitAppOrganizationInstallation";
|
||||
export * from "./gitAppInstallationSession";
|
||||
export * from "./secretApprovalPolicy";
|
||||
export * from "./secretApprovalRequest";
|
@ -1,53 +0,0 @@
|
||||
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 });
|
||||
|
||||
export const Role = model<IRole>("Role", roleSchema);
|
@ -1,51 +0,0 @@
|
||||
import { Schema, Types, model } from "mongoose";
|
||||
|
||||
export interface ISecretApprovalPolicy {
|
||||
_id: Types.ObjectId;
|
||||
workspace: Types.ObjectId;
|
||||
name: string;
|
||||
environment: string;
|
||||
secretPath?: string;
|
||||
approvers: Types.ObjectId[];
|
||||
approvals: number;
|
||||
}
|
||||
|
||||
const secretApprovalPolicySchema = new Schema<ISecretApprovalPolicy>(
|
||||
{
|
||||
workspace: {
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: "Workspace",
|
||||
required: true
|
||||
},
|
||||
approvers: [
|
||||
{
|
||||
// user associated with the personal secret
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: "Membership"
|
||||
}
|
||||
],
|
||||
name: {
|
||||
type: String
|
||||
},
|
||||
environment: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
secretPath: {
|
||||
type: String,
|
||||
required: false
|
||||
},
|
||||
approvals: {
|
||||
type: Number,
|
||||
default: 1
|
||||
}
|
||||
},
|
||||
{
|
||||
timestamps: true
|
||||
}
|
||||
);
|
||||
|
||||
export const SecretApprovalPolicy = model<ISecretApprovalPolicy>(
|
||||
"SecretApprovalPolicy",
|
||||
secretApprovalPolicySchema
|
||||
);
|
@ -1,203 +0,0 @@
|
||||
import { Schema, Types, model } from "mongoose";
|
||||
import { customAlphabet } from "nanoid";
|
||||
import {
|
||||
ALGORITHM_AES_256_GCM,
|
||||
ENCODING_SCHEME_BASE64,
|
||||
ENCODING_SCHEME_UTF8
|
||||
} from "../../variables";
|
||||
|
||||
export enum ApprovalStatus {
|
||||
PENDING = "pending",
|
||||
APPROVED = "approved",
|
||||
REJECTED = "rejected"
|
||||
}
|
||||
|
||||
export enum CommitType {
|
||||
DELETE = "delete",
|
||||
UPDATE = "update",
|
||||
CREATE = "create"
|
||||
}
|
||||
|
||||
const SLUG_ALPHABETS = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
|
||||
const nanoId = customAlphabet(SLUG_ALPHABETS, 10);
|
||||
|
||||
export interface ISecretApprovalSecChange {
|
||||
_id: Types.ObjectId;
|
||||
version: number;
|
||||
secretBlindIndex?: string;
|
||||
secretKeyCiphertext: string;
|
||||
secretKeyIV: string;
|
||||
secretKeyTag: string;
|
||||
secretValueCiphertext: string;
|
||||
secretValueIV: string;
|
||||
secretValueTag: string;
|
||||
secretCommentIV?: string;
|
||||
secretCommentTag?: string;
|
||||
secretCommentCiphertext?: string;
|
||||
skipMultilineEncoding?: boolean;
|
||||
algorithm?: "aes-256-gcm";
|
||||
keyEncoding?: "utf8" | "base64";
|
||||
tags?: string[];
|
||||
}
|
||||
|
||||
export type ISecretCommits<T = Types.ObjectId, J = Types.ObjectId> = Array<
|
||||
| {
|
||||
newVersion: ISecretApprovalSecChange;
|
||||
op: CommitType.CREATE;
|
||||
}
|
||||
| {
|
||||
// secret is recorded to get the latest version, we can keep ref to secret for pulling change as it will also get changed
|
||||
// on merge
|
||||
secretVersion: J;
|
||||
secret: T;
|
||||
newVersion: Partial<Omit<ISecretApprovalSecChange, "_id">> & { _id: Types.ObjectId };
|
||||
op: CommitType.UPDATE;
|
||||
}
|
||||
| {
|
||||
secret: T;
|
||||
secretVersion: J;
|
||||
op: CommitType.DELETE;
|
||||
}
|
||||
>;
|
||||
export interface ISecretApprovalRequest {
|
||||
_id: Types.ObjectId;
|
||||
committer: Types.ObjectId;
|
||||
slug: string;
|
||||
statusChangeBy: Types.ObjectId;
|
||||
reviewers: {
|
||||
member: Types.ObjectId;
|
||||
status: ApprovalStatus;
|
||||
}[];
|
||||
workspace: Types.ObjectId;
|
||||
environment: string;
|
||||
folderId: string;
|
||||
hasMerged: boolean;
|
||||
status: "open" | "close";
|
||||
policy: Types.ObjectId;
|
||||
commits: ISecretCommits;
|
||||
conflicts: Array<{ secretId: string; op: CommitType }>;
|
||||
}
|
||||
|
||||
const secretApprovalSecretChangeSchema = new Schema<ISecretApprovalSecChange>({
|
||||
version: {
|
||||
type: Number,
|
||||
default: 1,
|
||||
required: true
|
||||
},
|
||||
secretBlindIndex: {
|
||||
type: String,
|
||||
select: false
|
||||
},
|
||||
secretKeyCiphertext: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
secretKeyIV: {
|
||||
type: String, // symmetric
|
||||
required: true
|
||||
},
|
||||
secretKeyTag: {
|
||||
type: String, // symmetric
|
||||
required: true
|
||||
},
|
||||
secretValueCiphertext: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
secretValueIV: {
|
||||
type: String, // symmetric
|
||||
required: true
|
||||
},
|
||||
secretValueTag: {
|
||||
type: String, // symmetric
|
||||
required: true
|
||||
},
|
||||
skipMultilineEncoding: {
|
||||
type: Boolean,
|
||||
required: false
|
||||
},
|
||||
algorithm: {
|
||||
// the encryption algorithm used
|
||||
type: String,
|
||||
enum: [ALGORITHM_AES_256_GCM],
|
||||
required: true,
|
||||
default: ALGORITHM_AES_256_GCM
|
||||
},
|
||||
keyEncoding: {
|
||||
type: String,
|
||||
enum: [ENCODING_SCHEME_UTF8, ENCODING_SCHEME_BASE64],
|
||||
required: true,
|
||||
default: ENCODING_SCHEME_UTF8
|
||||
},
|
||||
tags: {
|
||||
ref: "Tag",
|
||||
type: [Schema.Types.ObjectId],
|
||||
default: []
|
||||
}
|
||||
});
|
||||
|
||||
const secretApprovalRequestSchema = new Schema<ISecretApprovalRequest>(
|
||||
{
|
||||
workspace: {
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: "Workspace",
|
||||
required: true
|
||||
},
|
||||
environment: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
folderId: {
|
||||
type: String,
|
||||
required: true,
|
||||
default: "root"
|
||||
},
|
||||
slug: {
|
||||
type: String,
|
||||
default: () => nanoId()
|
||||
},
|
||||
reviewers: {
|
||||
type: [
|
||||
{
|
||||
member: {
|
||||
// user associated with the personal secret
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: "Membership"
|
||||
},
|
||||
status: { type: String, enum: ApprovalStatus, default: ApprovalStatus.PENDING }
|
||||
}
|
||||
],
|
||||
default: []
|
||||
},
|
||||
policy: { type: Schema.Types.ObjectId, ref: "SecretApprovalPolicy" },
|
||||
hasMerged: { type: Boolean, default: false },
|
||||
status: { type: String, enum: ["close", "open"], default: "open" },
|
||||
committer: { type: Schema.Types.ObjectId, ref: "Membership" },
|
||||
statusChangeBy: { type: Schema.Types.ObjectId, ref: "Membership" },
|
||||
commits: [
|
||||
{
|
||||
secret: { type: Types.ObjectId, ref: "Secret" },
|
||||
newVersion: secretApprovalSecretChangeSchema,
|
||||
secretVersion: { type: Types.ObjectId, ref: "SecretVersion" },
|
||||
op: { type: String, enum: [CommitType], required: true }
|
||||
}
|
||||
],
|
||||
conflicts: {
|
||||
type: [
|
||||
{
|
||||
secretId: { type: String, required: true },
|
||||
op: { type: String, enum: [CommitType], required: true }
|
||||
}
|
||||
],
|
||||
default: []
|
||||
}
|
||||
},
|
||||
{
|
||||
timestamps: true
|
||||
}
|
||||
);
|
||||
|
||||
export const SecretApprovalRequest = model<ISecretApprovalRequest>(
|
||||
"SecretApprovalRequest",
|
||||
secretApprovalRequestSchema
|
||||
);
|
@ -1,52 +0,0 @@
|
||||
import { Schema, Types, model } from "mongoose";
|
||||
|
||||
export interface ISecretSnapshot {
|
||||
workspace: Types.ObjectId;
|
||||
environment: string;
|
||||
folderId: string | "root";
|
||||
version: number;
|
||||
secretVersions: Types.ObjectId[];
|
||||
folderVersion: Types.ObjectId;
|
||||
}
|
||||
|
||||
const secretSnapshotSchema = new Schema<ISecretSnapshot>(
|
||||
{
|
||||
workspace: {
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: "Workspace",
|
||||
required: true,
|
||||
},
|
||||
environment: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
folderId: {
|
||||
type: String,
|
||||
default: "root",
|
||||
},
|
||||
version: {
|
||||
type: Number,
|
||||
default: 1,
|
||||
required: true,
|
||||
},
|
||||
secretVersions: [
|
||||
{
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: "SecretVersion",
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
folderVersion: {
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: "FolderVersion",
|
||||
},
|
||||
},
|
||||
{
|
||||
timestamps: true,
|
||||
}
|
||||
);
|
||||
|
||||
export const SecretSnapshot = model<ISecretSnapshot>(
|
||||
"SecretSnapshot",
|
||||
secretSnapshotSchema
|
||||
);
|
@ -1,132 +0,0 @@
|
||||
import { Schema, Types, model } from "mongoose";
|
||||
import {
|
||||
ALGORITHM_AES_256_GCM,
|
||||
ENCODING_SCHEME_BASE64,
|
||||
ENCODING_SCHEME_UTF8,
|
||||
SECRET_PERSONAL,
|
||||
SECRET_SHARED
|
||||
} from "../../variables";
|
||||
|
||||
export interface ISecretVersion {
|
||||
_id: Types.ObjectId;
|
||||
secret: Types.ObjectId;
|
||||
version: number;
|
||||
workspace: Types.ObjectId; // new
|
||||
type: string; // new
|
||||
user?: Types.ObjectId; // new
|
||||
environment: string; // new
|
||||
isDeleted: boolean;
|
||||
secretBlindIndex?: string;
|
||||
secretKeyCiphertext: string;
|
||||
secretKeyIV: string;
|
||||
secretKeyTag: string;
|
||||
secretValueCiphertext: string;
|
||||
secretValueIV: string;
|
||||
secretValueTag: string;
|
||||
skipMultilineEncoding?: boolean;
|
||||
algorithm: "aes-256-gcm";
|
||||
keyEncoding: "utf8" | "base64";
|
||||
createdAt: string;
|
||||
folder?: string;
|
||||
tags?: string[];
|
||||
}
|
||||
|
||||
const secretVersionSchema = new Schema<ISecretVersion>(
|
||||
{
|
||||
secret: {
|
||||
// could be deleted
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: "Secret",
|
||||
required: true
|
||||
},
|
||||
version: {
|
||||
type: Number,
|
||||
default: 1,
|
||||
required: true
|
||||
},
|
||||
workspace: {
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: "Workspace",
|
||||
required: true
|
||||
},
|
||||
type: {
|
||||
type: String,
|
||||
enum: [SECRET_SHARED, SECRET_PERSONAL],
|
||||
required: true
|
||||
},
|
||||
user: {
|
||||
// user associated with the personal secret
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: "User"
|
||||
},
|
||||
environment: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
isDeleted: {
|
||||
// consider removing field
|
||||
type: Boolean,
|
||||
default: false,
|
||||
required: true
|
||||
},
|
||||
secretBlindIndex: {
|
||||
type: String,
|
||||
select: false
|
||||
},
|
||||
secretKeyCiphertext: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
secretKeyIV: {
|
||||
type: String, // symmetric
|
||||
required: true
|
||||
},
|
||||
secretKeyTag: {
|
||||
type: String, // symmetric
|
||||
required: true
|
||||
},
|
||||
secretValueCiphertext: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
secretValueIV: {
|
||||
type: String, // symmetric
|
||||
required: true
|
||||
},
|
||||
secretValueTag: {
|
||||
type: String, // symmetric
|
||||
required: true
|
||||
},
|
||||
skipMultilineEncoding: {
|
||||
type: Boolean,
|
||||
required: false
|
||||
},
|
||||
algorithm: {
|
||||
// the encryption algorithm used
|
||||
type: String,
|
||||
enum: [ALGORITHM_AES_256_GCM],
|
||||
required: true,
|
||||
default: ALGORITHM_AES_256_GCM
|
||||
},
|
||||
keyEncoding: {
|
||||
type: String,
|
||||
enum: [ENCODING_SCHEME_UTF8, ENCODING_SCHEME_BASE64],
|
||||
required: true,
|
||||
default: ENCODING_SCHEME_UTF8
|
||||
},
|
||||
folder: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
tags: {
|
||||
ref: "Tag",
|
||||
type: [Schema.Types.ObjectId],
|
||||
default: []
|
||||
}
|
||||
},
|
||||
{
|
||||
timestamps: true
|
||||
}
|
||||
);
|
||||
|
||||
export const SecretVersion = model<ISecretVersion>("SecretVersion", secretVersionSchema);
|
@ -1,72 +0,0 @@
|
||||
import { Schema, Types, model } from "mongoose";
|
||||
|
||||
export enum AuthProvider {
|
||||
OKTA_SAML = "okta-saml",
|
||||
AZURE_SAML = "azure-saml",
|
||||
JUMPCLOUD_SAML = "jumpcloud-saml"
|
||||
}
|
||||
|
||||
export interface ISSOConfig {
|
||||
organization: Types.ObjectId;
|
||||
authProvider: AuthProvider;
|
||||
isActive: boolean;
|
||||
encryptedEntryPoint: string;
|
||||
entryPointIV: string;
|
||||
entryPointTag: string;
|
||||
encryptedIssuer: string;
|
||||
issuerIV: string;
|
||||
issuerTag: string;
|
||||
encryptedCert: string;
|
||||
certIV: string;
|
||||
certTag: string;
|
||||
}
|
||||
|
||||
const ssoConfigSchema = new Schema<ISSOConfig>(
|
||||
{
|
||||
organization: {
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: "Organization"
|
||||
},
|
||||
authProvider: {
|
||||
type: String,
|
||||
enum: AuthProvider,
|
||||
required: true
|
||||
},
|
||||
isActive: {
|
||||
type: Boolean,
|
||||
required: true
|
||||
},
|
||||
encryptedEntryPoint: {
|
||||
type: String
|
||||
},
|
||||
entryPointIV: {
|
||||
type: String
|
||||
},
|
||||
entryPointTag: {
|
||||
type: String
|
||||
},
|
||||
encryptedIssuer: {
|
||||
type: String
|
||||
},
|
||||
issuerIV: {
|
||||
type: String
|
||||
},
|
||||
issuerTag: {
|
||||
type: String
|
||||
},
|
||||
encryptedCert: {
|
||||
type: String
|
||||
},
|
||||
certIV: {
|
||||
type: String
|
||||
},
|
||||
certTag: {
|
||||
type: String
|
||||
}
|
||||
},
|
||||
{
|
||||
timestamps: true
|
||||
}
|
||||
);
|
||||
|
||||
export const SSOConfig = model<ISSOConfig>("SSOConfig", ssoConfigSchema);
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user