1
0
mirror of https://github.com/Infisical/infisical.git synced 2025-03-27 09:40:45 +00:00

Compare commits

..

1 Commits

Author SHA1 Message Date
066cc94d3c fix: frontend/package.json & frontend/package-lock.json to reduce vulnerabilities
The following vulnerabilities are fixed with an upgrade:
- https://snyk.io/vuln/SNYK-JS-ZOD-5925617
2023-10-04 16:32:40 +00:00
1715 changed files with 68389 additions and 150913 deletions
.dockerignore.env.example
.github
.gitignore.goreleaser.yamlDockerfile.standalone-infisicalMakefileREADME.md
backend-mongo
.dockerignore.eslintignoreDockerfilenodemon.jsonpackage-lock.jsonpackage.json
src
bootstrap.ts
config
controllers
ee
helpers
interfaces/middleware
middleware
models
queues/reminders
routes
services
templates
utils
validation
variables
tsconfig.json
backend
.eslintignore.eslintrc.eslintrc.js.gitignore.prettierrc.prettierrc.jsonDockerfileDockerfile.dev
e2e-test
environment.d.tshealthcheck.js
img
jest.config.tsnodemon.jsonpackage-lock.jsonpackage.json
scripts
spec.json
src
@types
cache
config
controllers
data
db
index.tsinstance.tsknexfile.ts
migrations
schemas
seed-data.ts
seeds
utils.ts
ee
LICENSELICENSE.md
controllers/v1
helpers
middleware
models
routes/v1
services
validation
events
helpers
index.ts
integrations
interfaces
lib
main.ts
middleware
models
queue
queues
routes
server
services
BotOrgService.tsBotService.tsDatabaseService.tsEventService.tsFolderService.tsIntegrationService.tsRedisService.tsSecretImportService.tsSecretService.tsTelemetryService.tsTokenService.tsWebhookService.ts
api-key
auth-token
auth
health.ts
identity-access-token
identity-project
identity-ua
identity
index.ts
integration-auth
integration
org
project-bot
project-env
project-key
project-membership
project-role
project
secret-blind-index
secret-folder
secret-import
secret-tag
secret
service-token
smtp.ts
smtp
super-admin
telemetry
user
webhook
templates
types
utils
validation
variables
swagger
test-resources
tests
tsconfig.jsontsup.config.jsvitest.e2e.config.ts
cli
cypress.config.jsdocker-compose.pg.ymldocker-compose.yml
docs
CONTRIBUTING.MD
api-reference
changelog
cli
contributing
documentation
images
agent
getting-started/api
integrations
platform
project-token-add.pngproject-token-added.pngproject-token-name.pngproject-token-old-add.pngproject-token-old-permissions.pngproject-token-permissions.pngsdk-flow.png
self-hosting/deployment-options
service-token-permissions.png
sso
infisical-agent
integrations
internals
mint.json
sdks
self-hosting
spec.yaml
ecosystem.config.js
frontend
.eslintrc.jsDockerfilecypress.config.js
cypress
next.config.jspackage-lock.jsonpackage.json
public
scripts
src
components
const.ts
context
AuthContext
OrgPermissionContext
OrganizationContext
ProjectPermissionContext
ServerConfigContext
SubscriptionContext
UserContext
WorkspaceContext
index.tsx
ee
helpers
hoc
withPermission
withProjectPermission
hooks/api
layouts
lib/crypto
pages
views
IntegrationsPage
Login
Org
Project
SecretApprovalPage
SecretMainPage
SecretOverviewPage
SecretRotationPage
SecretScanning/components
Settings
BillingSettingsPage/components
OrgSettingsPage/components
PersonalSettingsPage
ProjectSettingsPage
Signup
SignupSSO.tsx
components/UserInfoSSOStep
admin
tsconfig.jsontsconfig.tsbuildinfo
helm-charts
img
k8-operator
nginx
package-lock.jsonpackage.json
pg-migrator
.gitignorepackage-lock.jsonpackage.json
src
@types
audit-log-migrator.tsfolder.tsindex.ts
migrations
models
rollback.ts
schemas
utils.ts
tsconfig.json
standalone-entrypoint.sh

@ -1,10 +1,2 @@
backend/node_modules
frontend/node_modules
backend/frontend-build
**/node_modules
**/.next
.dockerignore
.git
README.md
.dockerignore
**/Dockerfile
frontend/node_modules

@ -1,12 +1,23 @@
# Keys
# Required key for platform encryption/decryption ops
# THIS IS A SAMPLE ENCRYPTION KEY AND SHOULD NEVER BE USED FOR PRODUCTION
# THIS IS A SAMPLE ENCRYPTION KEY AND SHOULD NOT BE USED FOR PRODUCTION
ENCRYPTION_KEY=6c1fe4e407b8911c104518103505b218
# JWT
# Required secrets to sign JWT tokens
# THIS IS A SAMPLE AUTH_SECRET KEY AND SHOULD NEVER BE USED FOR PRODUCTION
AUTH_SECRET=5lrMXKKWCVocS/uerPsl7V+TX/aaUaI7iDkgl3tSmLE=
JWT_SIGNUP_SECRET=3679e04ca949f914c03332aaaeba805a
JWT_REFRESH_SECRET=5f2f3c8f0159068dc2bbb3a652a716ff
JWT_AUTH_SECRET=4be6ba5602e0fa0ac6ac05c3cd4d247f
JWT_SERVICE_SECRET=f32f716d70a42c5703f4656015e76200
JWT_PROVIDER_AUTH_SECRET=f32f716d70a42c5703f4656015e76201
# JWT lifetime
# Optional lifetimes for JWT tokens expressed in seconds or a string
# describing a time span (e.g. 60, "2 days", "10h", "7d")
JWT_AUTH_LIFETIME=
JWT_REFRESH_LIFETIME=
JWT_SIGNUP_LIFETIME=
JWT_PROVIDER_AUTH_LIFETIME=
# MongoDB
# Backend will connect to the MongoDB instance at connection string MONGO_URL which can either be a ref
@ -56,12 +67,5 @@ SENTRY_DSN=
POSTHOG_HOST=
POSTHOG_PROJECT_API_KEY=
# SSO-specific variables
CLIENT_ID_GOOGLE_LOGIN=
CLIENT_SECRET_GOOGLE_LOGIN=
CLIENT_ID_GITHUB_LOGIN=
CLIENT_SECRET_GITHUB_LOGIN=
CLIENT_ID_GITLAB_LOGIN=
CLIENT_SECRET_GITLAB_LOGIN=
CLIENT_ID_GOOGLE=
CLIENT_SECRET_GOOGLE=

@ -6,7 +6,7 @@ services:
restart: unless-stopped
depends_on:
- mongo
image: infisical/infisical:test
image: infisical/backend:test
command: npm run start
environment:
- NODE_ENV=production

41
.github/values.yaml vendored

@ -1,3 +1,29 @@
# secretScanningGitApp:
# enabled: false
# deploymentAnnotations:
# secrets.infisical.com/auto-reload: "true"
# image:
# repository: infisical/staging_deployment_secret-scanning-git-app
frontend:
enabled: true
name: frontend
podAnnotations: {}
deploymentAnnotations:
secrets.infisical.com/auto-reload: "true"
replicaCount: 2
image:
repository: infisical/staging_deployment_frontend
tag: "latest"
pullPolicy: Always
kubeSecretRef: managed-secret-frontend
service:
annotations: {}
type: ClusterIP
nodePort: ""
frontendEnvironmentVariables: null
backend:
enabled: true
name: backend
@ -6,7 +32,7 @@ backend:
secrets.infisical.com/auto-reload: "true"
replicaCount: 2
image:
repository: infisical/staging_infisical
repository: infisical/staging_deployment_backend
tag: "latest"
pullPolicy: Always
kubeSecretRef: managed-backend-secret
@ -14,15 +40,12 @@ backend:
annotations: {}
type: ClusterIP
nodePort: ""
resources:
limits:
memory: 300Mi
backendEnvironmentVariables: null
## Mongo DB persistence
mongodb:
enabled: false
enabled: true
persistence:
enabled: false
@ -37,8 +60,14 @@ ingress:
enabled: true
# annotations:
# kubernetes.io/ingress.class: "nginx"
# cert-manager.io/issuer: letsencrypt-nginx
# cert-manager.io/issuer: letsencrypt-nginx
hostName: gamma.infisical.com ## <- Replace with your own domain
frontend:
path: /
pathType: Prefix
backend:
path: /api
pathType: Prefix
tls:
[]
# - secretName: letsencrypt-nginx

@ -3,7 +3,6 @@ on:
push:
tags:
- "infisical/v*.*.*"
- "!infisical/v*.*.*-postgres"
jobs:
backend-image:
@ -40,7 +39,7 @@ jobs:
token: ${{ secrets.DEPOT_PROJECT_TOKEN }}
load: true
context: backend
tags: infisical/infisical:test
tags: infisical/backend:test
- name: ⏻ Spawn backend container and dependencies
run: |
docker compose -f .github/resources/docker-compose.be-test.yml up --wait --quiet-pull
@ -94,7 +93,6 @@ jobs:
tags: infisical/frontend:test
build-args: |
POSTHOG_API_KEY=${{ secrets.PUBLIC_POSTHOG_API_KEY }}
NEXT_INFISICAL_PLATFORM_VERSION=${{ steps.extract_version.outputs.version }}
- name: ⏻ Spawn frontend container
run: |
docker run -d --rm --name infisical-frontend-test infisical/frontend:test
@ -118,4 +116,3 @@ jobs:
platforms: linux/amd64,linux/arm64
build-args: |
POSTHOG_API_KEY=${{ secrets.PUBLIC_POSTHOG_API_KEY }}
NEXT_INFISICAL_PLATFORM_VERSION=${{ steps.extract_version.outputs.version }}

@ -2,7 +2,7 @@ name: Build, Publish and Deploy to Gamma
on: [workflow_dispatch]
jobs:
infisical-image:
backend-image:
name: Build backend image
runs-on: ubuntu-latest
steps:
@ -32,9 +32,8 @@ jobs:
project: 64mmf0n610
token: ${{ secrets.DEPOT_PROJECT_TOKEN }}
load: true
context: .
file: Dockerfile.standalone-infisical
tags: infisical/infisical:test
context: backend
tags: infisical/backend:test
- name: ⏻ Spawn backend container and dependencies
run: |
docker compose -f .github/resources/docker-compose.be-test.yml up --wait --quiet-pull
@ -50,20 +49,66 @@ jobs:
project: 64mmf0n610
token: ${{ secrets.DEPOT_PROJECT_TOKEN }}
push: true
context: .
file: Dockerfile.standalone-infisical
context: backend
tags: |
infisical/staging_infisical:${{ steps.commit.outputs.short }}
infisical/staging_infisical:latest
infisical/staging_deployment_backend:${{ steps.commit.outputs.short }}
infisical/staging_deployment_backend:latest
platforms: linux/amd64,linux/arm64
frontend-image:
name: Build frontend image
runs-on: ubuntu-latest
steps:
- name: ☁️ Checkout source
uses: actions/checkout@v3
- 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 frontend and export to Docker
uses: depot/build-push-action@v1
with:
load: true
token: ${{ secrets.DEPOT_PROJECT_TOKEN }}
project: 64mmf0n610
context: frontend
tags: infisical/staging_deployment_frontend:test
build-args: |
POSTHOG_API_KEY=${{ secrets.PUBLIC_POSTHOG_API_KEY }}
- name: ⏻ Spawn frontend container
run: |
docker run -d --rm --name infisical-frontend-test infisical/staging_deployment_frontend:test
- name: 🧪 Test frontend image
run: |
./.github/resources/healthcheck.sh infisical-frontend-test
- name: ⏻ Shut down frontend container
run: |
docker stop infisical-frontend-test
- name: 🏗️ Build frontend and push
uses: depot/build-push-action@v1
with:
project: 64mmf0n610
push: true
token: ${{ secrets.DEPOT_PROJECT_TOKEN }}
context: frontend
tags: |
infisical/staging_deployment_frontend:${{ steps.commit.outputs.short }}
infisical/staging_deployment_frontend:latest
platforms: linux/amd64,linux/arm64
build-args: |
POSTHOG_API_KEY=${{ secrets.PUBLIC_POSTHOG_API_KEY }}
INFISICAL_PLATFORM_VERSION=${{ steps.extract_version.outputs.version }}
gamma-deployment:
name: Deploy to gamma
runs-on: ubuntu-latest
needs: [infisical-image]
needs: [frontend-image, backend-image]
steps:
- name: ☁️ Checkout source
uses: actions/checkout@v3

@ -1,57 +0,0 @@
name: Release standalone docker image
on:
push:
tags:
- "infisical/v*.*.*-postgres"
jobs:
infisical-standalone:
name: Build infisical standalone image postgres
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
- 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-postgres
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 }}

@ -3,7 +3,6 @@ on:
push:
tags:
- "infisical/v*.*.*"
- "!infisical/v*.*.*-postgres"
jobs:
infisical-standalone:
@ -74,6 +73,3 @@ jobs:
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 }}

7
.gitignore vendored

@ -1,7 +1,6 @@
# backend
node_modules
.env
.env.test
.env.dev
.env.gamma
.env.prod
@ -34,7 +33,7 @@ reports
junit.xml
# next.js
.next/
/.next/
/out/
# production
@ -60,6 +59,4 @@ yarn-error.log*
.infisical.json
# Editor specific
.vscode/*
frontend-build
.vscode/*

@ -108,7 +108,7 @@ brews:
zsh_completion.install "completions/infisical.zsh" => "_infisical"
fish_completion.install "completions/infisical.fish"
man1.install "manpages/infisical.1.gz"
- name: "infisical@{{.Version}}"
- name: 'infisical@{{.Version}}'
tap:
owner: Infisical
name: homebrew-get-cli
@ -186,14 +186,12 @@ aurs:
# man pages
install -Dm644 "./manpages/infisical.1.gz" "${pkgdir}/usr/share/man/man1/infisical.1.gz"
dockers:
- dockerfile: docker/alpine
goos: linux
goarch: amd64
ids:
- all-other-builds
image_templates:
- "infisical/cli:{{ .Version }}"
- "infisical/cli:{{ .Major }}.{{ .Minor }}"
- "infisical/cli:{{ .Major }}"
- "infisical/cli:latest"
# dockers:
# - dockerfile: cli/docker/Dockerfile
# goos: linux
# goarch: amd64
# ids:
# - infisical
# image_templates:
# - "infisical/cli:{{ .Version }}"
# - "infisical/cli:latest"

@ -1,13 +1,7 @@
ARG POSTHOG_HOST=https://app.posthog.com
ARG POSTHOG_API_KEY=posthog-api-key
ARG INTERCOM_ID=intercom-id
FROM node:20-alpine AS base
FROM base AS frontend-dependencies
# Check https://github.com/nodejs/docker-node/tree/b4117f9333da4138b03a546ec926ef50a31506c3#nodealpine to understand why libc6-compat might be needed.
RUN apk add --no-cache libc6-compat
FROM node:16-alpine AS frontend-dependencies
WORKDIR /app
@ -17,7 +11,7 @@ COPY frontend/package.json frontend/package-lock.json frontend/next.config.js ./
RUN npm ci --only-production --ignore-scripts
# Rebuild the source code only when needed
FROM base AS frontend-builder
FROM node:16-alpine AS frontend-builder
WORKDIR /app
# Copy dependencies
@ -33,38 +27,41 @@ ARG POSTHOG_API_KEY
ENV NEXT_PUBLIC_POSTHOG_API_KEY $POSTHOG_API_KEY
ARG INTERCOM_ID
ENV NEXT_PUBLIC_INTERCOM_ID $INTERCOM_ID
ARG INFISICAL_PLATFORM_VERSION
ENV NEXT_PUBLIC_INFISICAL_PLATFORM_VERSION $INFISICAL_PLATFORM_VERSION
# Build
RUN npm run build
# Production image
FROM base AS frontend-runner
FROM node:16-alpine AS frontend-runner
WORKDIR /app
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 non-root-user
RUN adduser --system --uid 1001 nextjs
RUN mkdir -p /app/.next/cache/images && chown non-root-user:nodejs /app/.next/cache/images
RUN mkdir -p /app/.next/cache/images && chown nextjs:nodejs /app/.next/cache/images
VOLUME /app/.next/cache/images
COPY --chown=non-root-user:nodejs --chmod=555 frontend/scripts ./scripts
COPY --from=frontend-builder /app/public ./public
RUN chown non-root-user:nodejs ./public/data
COPY --from=frontend-builder --chown=non-root-user:nodejs /app/.next/standalone ./
COPY --from=frontend-builder --chown=non-root-user:nodejs /app/.next/static ./.next/static
ARG POSTHOG_API_KEY
ENV NEXT_PUBLIC_POSTHOG_API_KEY=$POSTHOG_API_KEY \
BAKED_NEXT_PUBLIC_POSTHOG_API_KEY=$POSTHOG_API_KEY
ARG INTERCOM_ID
ENV NEXT_PUBLIC_INTERCOM_ID=$INTERCOM_ID \
BAKED_NEXT_PUBLIC_INTERCOM_ID=$INTERCOM_ID
USER non-root-user
COPY --chown=nextjs:nodejs --chmod=555 frontend/scripts ./scripts
COPY --from=frontend-builder /app/public ./public
RUN chown nextjs:nodejs ./public/data
COPY --from=frontend-builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=frontend-builder --chown=nextjs:nodejs /app/.next/static ./.next/static
USER nextjs
ENV NEXT_TELEMETRY_DISABLED 1
##
## BACKEND
##
FROM base AS backend-build
RUN addgroup --system --gid 1001 nodejs \
&& adduser --system --uid 1001 non-root-user
FROM node:16-alpine AS backend-build
WORKDIR /app
@ -72,12 +69,10 @@ COPY backend/package*.json ./
RUN npm ci --only-production
COPY /backend .
COPY --chown=non-root-user:nodejs standalone-entrypoint.sh standalone-entrypoint.sh
RUN npm i -D tsconfig-paths
RUN npm run build
# Production stage
FROM base AS backend-runner
FROM node:16-alpine AS backend-runner
WORKDIR /app
@ -86,45 +81,27 @@ RUN npm ci --only-production
COPY --from=backend-build /app .
RUN mkdir frontend-build
# Production stage
FROM base AS production
RUN addgroup --system --gid 1001 nodejs \
&& adduser --system --uid 1001 non-root-user
## set pre baked keys
ARG POSTHOG_API_KEY
ENV NEXT_PUBLIC_POSTHOG_API_KEY=$POSTHOG_API_KEY \
BAKED_NEXT_PUBLIC_POSTHOG_API_KEY=$POSTHOG_API_KEY
ARG INTERCOM_ID=intercom-id
ENV NEXT_PUBLIC_INTERCOM_ID=$INTERCOM_ID \
BAKED_NEXT_PUBLIC_INTERCOM_ID=$INTERCOM_ID
FROM node:16-alpine AS production
WORKDIR /
# Install PM2
RUN npm install -g pm2
# Copy ecosystem.config.js
COPY ecosystem.config.js .
RUN apk add --no-cache nginx
COPY nginx/default-stand-alone-docker.conf /etc/nginx/nginx.conf
COPY --from=backend-runner /app /backend
COPY --from=backend-runner /app/dist/services/smtp/templates /backend/dist/templates
COPY --from=frontend-runner /app ./backend/frontend-build
COPY --from=frontend-runner /app/ /app/
ENV PORT 8080
ENV HOST=0.0.0.0
EXPOSE 80
ENV HTTPS_ENABLED false
ENV NODE_ENV production
ENV STANDALONE_BUILD true
ENV STANDALONE_MODE true
WORKDIR /backend
ENV TELEMETRY_ENABLED true
CMD ["pm2-runtime", "start", "ecosystem.config.js"]
HEALTHCHECK --interval=10s --timeout=3s --start-period=10s \
CMD node healthcheck.js
EXPOSE 8080
EXPOSE 443
USER non-root-user
CMD ["./standalone-entrypoint.sh"]

@ -7,9 +7,6 @@ push:
up-dev:
docker-compose -f docker-compose.dev.yml up --build
up-pg-dev:
docker compose -f docker-compose.pg.yml up --build
i-dev:
infisical run -- docker-compose -f docker-compose.dev.yml up --build

@ -1,8 +1,9 @@
<h1 align="center">
<img width="300" src="/img/logoname-black.svg#gh-light-mode-only" alt="infisical">
<img width="300" src="/img/logoname-white.svg#gh-dark-mode-only" alt="infisical">
</h1>
<p align="center">
<p align="center"><b>The open-source secret management platform</b>: Sync secrets/configs across your team/infrastructure and prevent secret leaks.</p>
<p align="center"><b>Open-source, end-to-end encrypted secret management platform</b>: distribute secrets/configs across your team/infrastructure and prevent secret leaks.</p>
</p>
<h4 align="center">
@ -33,7 +34,7 @@
<img src="https://img.shields.io/github/commit-activity/m/infisical/infisical" alt="git commit activity" />
</a>
<a href="https://cloudsmith.io/~infisical/repos/">
<img src="https://img.shields.io/badge/Downloads-2.58M-orange" alt="Cloudsmith downloads" />
<img src="https://img.shields.io/badge/Downloads-1.38M-orange" alt="Cloudsmith downloads" />
</a>
<a href="https://infisical.com/slack">
<img src="https://img.shields.io/badge/chat-on%20Slack-blueviolet" alt="Slack community channel" />
@ -43,11 +44,11 @@
</a>
</h4>
<img src="/img/infisical_github_repo2.png" width="100%" alt="Dashboard" />
<img src="/img/infisical_github_repo.png" width="100%" alt="Dashboard" />
## Introduction
**[Infisical](https://infisical.com)** is the open source secret management platform that teams use to centralize their secrets like API keys, database credentials, and configurations.
**[Infisical](https://infisical.com)** is an open source, end-to-end encrypted secret management platform that teams use to centralize their secrets like API keys, database credentials, and configurations.
We're on a mission to make secret management more accessible to everyone, not just security teams, and that means redesigning the entire developer experience from ground up.
@ -129,10 +130,11 @@ Note that this security address should be used only for undisclosed vulnerabilit
## Contributing
Whether it's big or small, we love contributions. Check out our guide to see how to [get started](https://infisical.com/docs/contributing/getting-started).
Whether it's big or small, we love contributions. Check out our guide to see how to [get started](https://infisical.com/docs/contributing/overview).
Not sure where to get started? You can:
- [Book a free, non-pressure pairing session / code walkthrough with one of our teammates](https://cal.com/tony-infisical/30-min-meeting-contributing)!
- Join our <a href="https://infisical.com/slack">Slack</a>, and ask us any questions there.
- Join our [community calls](https://us06web.zoom.us/j/82623506356) every Wednesday at 11am EST to ask any questions, provide feedback, hangout and more.

@ -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,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"]

@ -1,6 +0,0 @@
{
"watch": ["src"],
"ext": ".ts,.js",
"ignore": [],
"exec": "ts-node ./src/index.ts"
}

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}"
}
}

@ -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,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,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
});
};

File diff suppressed because it is too large Load Diff

@ -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,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
});
}

File diff suppressed because it is too large Load Diff

@ -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,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,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,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,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,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,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,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,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,31 +0,0 @@
import express from "express";
const router = express.Router();
import { requireAuth } from "../../../middleware";
import { AuthMode } from "../../../variables";
import { identitiesController } from "../../controllers/v1";
router.post(
"/",
requireAuth({
acceptedAuthModes: [AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]
}),
identitiesController.createIdentity
);
router.patch(
"/:identityId",
requireAuth({
acceptedAuthModes: [AuthMode.JWT]
}),
identitiesController.updateIdentity
);
router.delete(
"/:identityId",
requireAuth({
acceptedAuthModes: [AuthMode.JWT]
}),
identitiesController.deleteIdentity
);
export default router;

@ -1,31 +0,0 @@
import identities from "./identities";
import secret from "./secret";
import secretSnapshot from "./secretSnapshot";
import organizations from "./organizations";
import sso from "./sso";
import users from "./users";
import workspace from "./workspace";
import cloudProducts from "./cloudProducts";
import secretScanning from "./secretScanning";
import roles from "./role";
import secretApprovalPolicy from "./secretApprovalPolicy";
import secretApprovalRequest from "./secretApprovalRequest";
import secretRotationProvider from "./secretRotationProvider";
import secretRotation from "./secretRotation";
export {
identities,
secret,
secretSnapshot,
organizations,
sso,
users,
workspace,
cloudProducts,
secretScanning,
roles,
secretApprovalPolicy,
secretApprovalRequest,
secretRotationProvider,
secretRotation
};

@ -1,55 +0,0 @@
import express from "express";
const router = express.Router();
import { requireAuth } from "../../../middleware";
import { secretApprovalRequestController } from "../../controllers/v1";
import { AuthMode } from "../../../variables";
router.get(
"/",
requireAuth({
acceptedAuthModes: [AuthMode.JWT]
}),
secretApprovalRequestController.getSecretApprovalRequests
);
router.get(
"/count",
requireAuth({
acceptedAuthModes: [AuthMode.JWT]
}),
secretApprovalRequestController.getSecretApprovalRequestCount
);
router.get(
"/:id",
requireAuth({
acceptedAuthModes: [AuthMode.JWT]
}),
secretApprovalRequestController.getSecretApprovalRequestDetails
);
router.post(
"/:id/merge",
requireAuth({
acceptedAuthModes: [AuthMode.JWT]
}),
secretApprovalRequestController.mergeSecretApprovalRequest
);
router.post(
"/:id/review",
requireAuth({
acceptedAuthModes: [AuthMode.JWT]
}),
secretApprovalRequestController.updateSecretApprovalReviewStatus
);
router.post(
"/:id/status",
requireAuth({
acceptedAuthModes: [AuthMode.JWT]
}),
secretApprovalRequestController.updateSecretApprovalRequestStatus
);
export default router;

@ -1,41 +0,0 @@
import express from "express";
import { AuthMode } from "../../../variables";
import { requireAuth } from "../../../middleware";
import { secretRotationController } from "../../controllers/v1";
const router = express.Router();
router.post(
"/",
requireAuth({
acceptedAuthModes: [AuthMode.JWT]
}),
secretRotationController.createSecretRotation
);
router.post(
"/restart",
requireAuth({
acceptedAuthModes: [AuthMode.JWT]
}),
secretRotationController.restartSecretRotations
);
router.get(
"/",
requireAuth({
acceptedAuthModes: [AuthMode.JWT]
}),
secretRotationController.getSecretRotations
);
router.delete(
"/:id",
requireAuth({
acceptedAuthModes: [AuthMode.JWT]
}),
secretRotationController.deleteSecretRotations
);
export default router;

@ -1,17 +0,0 @@
import express from "express";
import { AuthMode } from "../../../variables";
import { requireAuth } from "../../../middleware";
import { secretRotationProviderController } from "../../controllers/v1";
const router = express.Router();
router.get(
"/:workspaceId",
requireAuth({
acceptedAuthModes: [AuthMode.JWT]
}),
secretRotationProviderController.getProviderTemplates
);
export default router;

@ -1,60 +0,0 @@
import express from "express";
const router = express.Router();
import passport from "passport";
import { requireAuth } from "../../../middleware";
import { ssoController } from "../../controllers/v1";
import { authLimiter } from "../../../helpers/rateLimiter";
import { AuthMode } from "../../../variables";
router.get(
"/redirect/saml2/:ssoIdentifier",
authLimiter,
(req, res, next) => {
const options = {
failureRedirect: "/",
additionalParams: {
RelayState: JSON.stringify({
spInitiated: true,
callbackPort: req.query.callback_port ?? ""
})
},
};
passport.authenticate("saml", options)(req, res, next);
}
);
router.post(
"/saml2/:ssoIdentifier",
passport.authenticate("saml", {
failureRedirect: "/login/provider/error",
failureFlash: true,
session: false
}),
ssoController.redirectSSO
);
router.get(
"/config",
requireAuth({
acceptedAuthModes: [AuthMode.JWT]
}),
ssoController.getSSOConfig
);
router.post(
"/config",
requireAuth({
acceptedAuthModes: [AuthMode.JWT]
}),
ssoController.createSSOConfig
);
router.patch(
"/config",
requireAuth({
acceptedAuthModes: [AuthMode.JWT]
}),
ssoController.updateSSOConfig
);
export default router;

@ -1,31 +0,0 @@
import express from "express";
const router = express.Router();
import { requireAuth } from "../../../middleware";
import { AuthMode } from "../../../variables";
import { apiKeyDataController } from "../../controllers/v3";
router.post(
"/",
requireAuth({
acceptedAuthModes: [AuthMode.JWT]
}),
apiKeyDataController.createAPIKeyData
);
router.patch(
"/:apiKeyDataId",
requireAuth({
acceptedAuthModes: [AuthMode.JWT]
}),
apiKeyDataController.updateAPIKeyData
);
router.delete(
"/:apiKeyDataId",
requireAuth({
acceptedAuthModes: [AuthMode.JWT]
}),
apiKeyDataController.deleteAPIKeyData
);
export default router;

@ -1,5 +0,0 @@
import apiKeyData from "./apiKeyData";
export {
apiKeyData
}

@ -1,91 +0,0 @@
import { Schema, model } from "mongoose";
import {
ALGORITHM_AES_256_GCM,
ENCODING_SCHEME_BASE64,
ENCODING_SCHEME_UTF8
} from "../../variables";
import { ISecretRotation } from "./types";
const secretRotationSchema = new Schema(
{
workspace: {
type: Schema.Types.ObjectId,
ref: "Workspace"
},
provider: {
type: String,
required: true
},
customProvider: {
type: Schema.Types.ObjectId,
ref: "SecretRotationProvider"
},
environment: {
type: String,
required: true
},
secretPath: {
type: String,
required: true
},
interval: {
type: Number,
required: true
},
lastRotatedAt: {
type: String
},
status: {
type: String,
enum: ["success", "failed"]
},
statusMessage: {
type: String
},
// encrypted data on input keys and secrets got
encryptedData: {
type: String,
select: false
},
encryptedDataIV: {
type: String,
select: false
},
encryptedDataTag: {
type: String,
select: false
},
algorithm: {
// the encryption algorithm used
type: String,
enum: [ALGORITHM_AES_256_GCM],
required: true,
select: false,
default: ALGORITHM_AES_256_GCM
},
keyEncoding: {
type: String,
enum: [ENCODING_SCHEME_UTF8, ENCODING_SCHEME_BASE64],
required: true,
select: false,
default: ENCODING_SCHEME_UTF8
},
outputs: [
{
key: {
type: String,
required: true
},
secret: {
type: Schema.Types.ObjectId,
ref: "Secret"
}
}
]
},
{
timestamps: true
}
);
export const SecretRotation = model<ISecretRotation>("SecretRotation", secretRotationSchema);

@ -1,288 +0,0 @@
import Queue, { Job } from "bull";
import { client, getEncryptionKey, getRootEncryptionKey } from "../../../config";
import { BotService, EventService, TelemetryService } from "../../../services";
import { SecretRotation } from "../models";
import { rotationTemplates } from "../templates";
import {
ISecretRotationData,
ISecretRotationEncData,
ISecretRotationProviderTemplate,
TProviderFunctionTypes
} from "../types";
import {
decryptSymmetric128BitHexKeyUTF8,
encryptSymmetric128BitHexKeyUTF8
} from "../../../utils/crypto";
import { ISecret, Secret } from "../../../models";
import { ENCODING_SCHEME_BASE64, ENCODING_SCHEME_UTF8, SECRET_SHARED } from "../../../variables";
import { EESecretService } from "../../services";
import { SecretVersion } from "../../models";
import { eventPushSecrets } from "../../../events";
import { logger } from "../../../utils/logging";
import {
secretRotationPreSetFn,
secretRotationRemoveFn,
secretRotationSetFn,
secretRotationTestFn
} from "./queue.utils";
const secretRotationQueue = new Queue("secret-rotation-service", process.env.REDIS_URL as string);
secretRotationQueue.process(async (job: Job) => {
logger.info(`secretRotationQueue.process: [rotationDocument=${job.data.rotationDocId}]`);
const rotationStratDocId = job.data.rotationDocId;
const secretRotation = await SecretRotation.findById(rotationStratDocId)
.select("+encryptedData +encryptedDataTag +encryptedDataIV +keyEncoding")
.populate<{
outputs: [
{
key: string;
secret: ISecret;
}
];
}>("outputs.secret");
const infisicalRotationProvider = rotationTemplates.find(
({ name }) => name === secretRotation?.provider
);
try {
if (!infisicalRotationProvider || !secretRotation)
throw new Error("Failed to find rotation strategy");
if (secretRotation.outputs.some(({ secret }) => !secret))
throw new Error("Secrets not found in dashboard");
const workspaceId = secretRotation.workspace;
// deep copy
const provider = JSON.parse(
JSON.stringify(infisicalRotationProvider)
) as ISecretRotationProviderTemplate;
// decrypt user provided inputs for secret rotation
const encryptionKey = await getEncryptionKey();
const rootEncryptionKey = await getRootEncryptionKey();
let decryptedData = "";
if (rootEncryptionKey && secretRotation.keyEncoding === ENCODING_SCHEME_BASE64) {
// case: encoding scheme is base64
decryptedData = client.decryptSymmetric(
secretRotation.encryptedData,
rootEncryptionKey,
secretRotation.encryptedDataIV,
secretRotation.encryptedDataTag
);
} else if (encryptionKey && secretRotation.keyEncoding === ENCODING_SCHEME_UTF8) {
// case: encoding scheme is utf8
decryptedData = decryptSymmetric128BitHexKeyUTF8({
ciphertext: secretRotation.encryptedData,
iv: secretRotation.encryptedDataIV,
tag: secretRotation.encryptedDataTag,
key: encryptionKey
});
}
const variables = JSON.parse(decryptedData) as ISecretRotationEncData;
// rotation set cycle
const newCredential: ISecretRotationData = {
inputs: variables.inputs,
outputs: {},
internal: {}
};
// special glue code for database
if (provider.template.functions.set.type === TProviderFunctionTypes.DB) {
const lastCred = variables.creds.at(-1);
if (lastCred && variables.creds.length === 1) {
newCredential.internal.username =
lastCred.internal.username === variables.inputs.username1
? variables.inputs.username2
: variables.inputs.username1;
} else {
newCredential.internal.username = lastCred
? lastCred.internal.username
: variables.inputs.username1;
}
}
if (provider.template.functions.set?.pre) {
secretRotationPreSetFn(provider.template.functions.set.pre, newCredential);
}
await secretRotationSetFn(provider.template.functions.set, newCredential);
await secretRotationTestFn(provider.template.functions.test, newCredential);
if (variables.creds.length === 2) {
const deleteCycleCred = variables.creds.pop();
if (deleteCycleCred && provider.template.functions.remove) {
const deleteCycleVar = { inputs: variables.inputs, ...deleteCycleCred };
await secretRotationRemoveFn(provider.template.functions.remove, deleteCycleVar);
}
}
variables.creds.unshift({ outputs: newCredential.outputs, internal: newCredential.internal });
const { ciphertext, iv, tag } = client.encryptSymmetric(
JSON.stringify(variables),
rootEncryptionKey
);
// save the rotation state
await SecretRotation.findByIdAndUpdate(rotationStratDocId, {
encryptedData: ciphertext,
encryptedDataIV: iv,
encryptedDataTag: tag,
status: "success",
statusMessage: "Rotated successfully",
lastRotatedAt: new Date().toUTCString()
});
const key = await BotService.getWorkspaceKeyWithBot({
workspaceId: secretRotation.workspace
});
const encryptedSecrets = secretRotation.outputs.map(({ key: outputKey, secret }) => ({
secret,
value: encryptSymmetric128BitHexKeyUTF8({
plaintext:
typeof newCredential.outputs[outputKey] === "object"
? JSON.stringify(newCredential.outputs[outputKey])
: String(newCredential.outputs[outputKey]),
key
})
}));
// now save the secret do a bulk update
// can't use the updateSecret function due to various parameter required issue
// REFACTOR(akhilmhdh): secret module should be lot more flexible. Ability to update bulk or individually by blindIndex, by id etc
await Secret.bulkWrite(
encryptedSecrets.map(({ secret, value }) => ({
updateOne: {
filter: {
workspace: workspaceId,
environment: secretRotation.environment,
_id: secret._id,
type: SECRET_SHARED
},
update: {
$inc: {
version: 1
},
secretValueCiphertext: value.ciphertext,
secretValueIV: value.iv,
secretValueTag: value.tag
}
}
}))
);
await EESecretService.addSecretVersions({
secretVersions: encryptedSecrets.map(({ secret, value }) => {
const {
_id,
version,
workspace,
type,
folder,
secretBlindIndex,
secretKeyIV,
secretKeyTag,
secretKeyCiphertext,
skipMultilineEncoding,
environment,
algorithm,
keyEncoding
} = secret;
return new SecretVersion({
secret: _id,
version: version + 1,
workspace: workspace,
type,
folder,
environment,
isDeleted: false,
secretBlindIndex: secretBlindIndex,
secretKeyCiphertext: secretKeyCiphertext,
secretKeyIV: secretKeyIV,
secretKeyTag: secretKeyTag,
secretValueCiphertext: value.ciphertext,
secretValueIV: value.iv,
secretValueTag: value.tag,
algorithm,
keyEncoding,
skipMultilineEncoding
});
})
});
// akhilmhdh: @tony need to do something about this as its depend on authData which is not possibile in here
// await EEAuditLogService.createAuditLog(
// {actor:ActorType.Machine},
// {
// type: EventType.UPDATE_SECRETS,
// metadata: {
// environment,
// secretPath,
// secrets: secretsToBeUpdated.map(({ _id, version, secretBlindIndex }) => ({
// secretId: _id.toString(),
// secretKey: secretBlindIndexToKey[secretBlindIndex || ""],
// secretVersion: version + 1
// }))
// }
// },
// {
// workspaceId
// }
// );
const folderId = encryptedSecrets?.[0]?.secret?.folder;
// (EE) take a secret snapshot
await EESecretService.takeSecretSnapshot({
workspaceId,
environment: secretRotation.environment,
folderId
});
await EventService.handleEvent({
event: eventPushSecrets({
workspaceId: secretRotation.workspace,
environment: secretRotation.environment,
secretPath: secretRotation.secretPath
})
});
const postHogClient = await TelemetryService.getPostHogClient();
if (postHogClient) {
postHogClient.capture({
event: "secrets rotated",
properties: {
numberOfSecrets: encryptedSecrets.length,
environment: secretRotation.environment,
workspaceId,
folderId
}
});
}
} catch (err) {
logger.error(err);
await SecretRotation.findByIdAndUpdate(rotationStratDocId, {
status: "failed",
statusMessage: (err as Error).message,
lastRotatedAt: new Date().toUTCString()
});
}
return Promise.resolve();
});
const daysToMillisecond = (days: number) => days * 24 * 60 * 60 * 1000;
export const startSecretRotationQueue = async (rotationDocId: string, interval: number) => {
// when migration to bull mq just use the option immedite to trigger repeatable immediately
secretRotationQueue.add({ rotationDocId }, { jobId: rotationDocId, removeOnComplete: true });
return secretRotationQueue.add(
{ rotationDocId },
{ repeat: { every: daysToMillisecond(interval) }, jobId: rotationDocId }
);
};
export const removeSecretRotationQueue = async (rotationDocId: string, interval: number) => {
return secretRotationQueue.removeRepeatable({ every: interval * 1000, jobId: rotationDocId });
};

@ -1,179 +0,0 @@
import axios from "axios";
import jmespath from "jmespath";
import { customAlphabet } from "nanoid";
import { Client as PgClient } from "pg";
import mysql from "mysql2";
import {
ISecretRotationData,
TAssignOp,
TDbProviderClients,
TDbProviderFunction,
TDirectAssignOp,
THttpProviderFunction,
TProviderFunction,
TProviderFunctionTypes
} from "../types";
const REGEX = /\${([^}]+)}/g;
const SLUG_ALPHABETS = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
const nanoId = customAlphabet(SLUG_ALPHABETS, 10);
export const interpolate = (data: any, getValue: (key: string) => unknown) => {
if (!data) return;
if (typeof data === "number") return data;
if (typeof data === "string") {
return data.replace(REGEX, (_a, b) => getValue(b) as string);
}
if (typeof data === "object" && Array.isArray(data)) {
data.forEach((el, index) => {
data[index] = interpolate(el, getValue);
});
}
if (typeof data === "object") {
if ((data as { ref: string })?.ref) return getValue((data as { ref: string }).ref);
const temp = data as Record<string, unknown>; // for converting ts object to record type
Object.keys(temp).forEach((key) => {
temp[key as keyof typeof temp] = interpolate(data[key as keyof typeof temp], getValue);
});
}
return data;
};
const getInterpolationValue = (variables: ISecretRotationData) => (key: string) => {
if (key.includes("|")) {
const [keyword, ...arg] = key.split("|").map((el) => el.trim());
switch (keyword) {
case "random": {
return nanoId(parseInt(arg[0], 10));
}
default: {
throw Error(`Interpolation key not found - ${key}`);
}
}
}
const [type, keyName] = key.split(".").map((el) => el.trim());
return variables[type as keyof ISecretRotationData][keyName];
};
export const secretRotationHttpFn = async (
func: THttpProviderFunction,
variables: ISecretRotationData
) => {
// string interpolation
const headers = interpolate(func.header, getInterpolationValue(variables));
const url = interpolate(func.url, getInterpolationValue(variables));
const body = interpolate(func.body, getInterpolationValue(variables));
// axios will automatically throw error if req status is not between 2xx range
return axios({ method: func.method, url, headers, data: body });
};
export const secretRotationDbFn = async (
func: TDbProviderFunction,
variables: ISecretRotationData
) => {
const { type, client, pre, ...dbConnection } = func;
const { username, password, host, database, port, query, ca } = interpolate(
dbConnection,
getInterpolationValue(variables)
);
const ssl = ca ? { rejectUnauthorized: false, ca } : undefined;
if (host === "localhost" || host === "127.0.0.1") throw new Error("Invalid db host");
if (client === TDbProviderClients.Pg) {
const pgClient = new PgClient({ user: username, password, host, database, port, ssl });
await pgClient.connect();
const res = await pgClient.query(query);
await pgClient.end();
return res.rows[0];
} else if (client === TDbProviderClients.Sql) {
const sqlClient = mysql.createPool({
user: username,
password,
host,
database,
port,
connectionLimit: 1,
ssl
});
const res = await new Promise((resolve, reject) => {
sqlClient.query(query, (err, data) => {
if (err) return reject(err);
resolve(data);
});
});
await new Promise((resolve, reject) => {
sqlClient.end(function (err) {
if (err) return reject(err);
return resolve({});
});
});
return (res as any)?.[0];
}
};
export const secretRotationPreSetFn = (
op: Record<string, TDirectAssignOp>,
variables: ISecretRotationData
) => {
const getValFn = getInterpolationValue(variables);
Object.entries(op || {}).forEach(([key, assignFn]) => {
const [type, keyName] = key.split(".") as [keyof ISecretRotationData, string];
variables[type][keyName] = interpolate(assignFn.value, getValFn);
});
};
export const secretRotationSetFn = async (
func: TProviderFunction,
variables: ISecretRotationData
) => {
const getValFn = getInterpolationValue(variables);
// http setter
if (func.type === TProviderFunctionTypes.HTTP) {
const res = await secretRotationHttpFn(func, variables);
Object.entries(func.setter || {}).forEach(([key, assignFn]) => {
const [type, keyName] = key.split(".") as [keyof ISecretRotationData, string];
if (assignFn.assign === TAssignOp.JmesPath) {
variables[type][keyName] = jmespath.search(res.data, assignFn.path);
} else if (assignFn.value) {
variables[type][keyName] = interpolate(assignFn.value, getValFn);
}
});
// db setter
} else if (func.type === TProviderFunctionTypes.DB) {
const data = await secretRotationDbFn(func, variables);
Object.entries(func.setter || {}).forEach(([key, assignFn]) => {
const [type, keyName] = key.split(".") as [keyof ISecretRotationData, string];
if (assignFn.assign === TAssignOp.JmesPath) {
if (typeof data === "object") {
variables[type][keyName] = jmespath.search(data, assignFn.path);
}
} else if (assignFn.value) {
variables[type][keyName] = interpolate(assignFn.value, getValFn);
}
});
}
};
export const secretRotationTestFn = async (
func: TProviderFunction,
variables: ISecretRotationData
) => {
if (func.type === TProviderFunctionTypes.HTTP) {
await secretRotationHttpFn(func, variables);
} else if (func.type === TProviderFunctionTypes.DB) {
await secretRotationDbFn(func, variables);
}
};
export const secretRotationRemoveFn = async (
func: TProviderFunction,
variables: ISecretRotationData
) => {
if (!func) return;
if (func.type === TProviderFunctionTypes.HTTP) {
// string interpolation
return await secretRotationHttpFn(func, variables);
}
};

@ -1,130 +0,0 @@
import { ISecretRotationEncData, TCreateSecretRotation, TGetProviderTemplates } from "./types";
import { rotationTemplates } from "./templates";
import { SecretRotation } from "./models";
import { client, getEncryptionKey, getRootEncryptionKey } from "../../config";
import { BadRequestError } from "../../utils/errors";
import Ajv from "ajv";
import { removeSecretRotationQueue, startSecretRotationQueue } from "./queue/queue";
import {
ALGORITHM_AES_256_GCM,
ENCODING_SCHEME_BASE64,
ENCODING_SCHEME_UTF8
} from "../../variables";
import { encryptSymmetric128BitHexKeyUTF8 } from "../../utils/crypto";
const ajv = new Ajv({ strict: false });
export const getProviderTemplate = async ({ workspaceId }: TGetProviderTemplates) => {
return {
custom: [],
providers: rotationTemplates
};
};
export const createSecretRotation = async ({
workspaceId,
secretPath,
environment,
provider,
interval,
inputs,
outputs
}: TCreateSecretRotation) => {
const rotationTemplate = rotationTemplates.find(({ name }) => name === provider);
if (!rotationTemplate) throw BadRequestError({ message: "Provider not found" });
const formattedInputs: Record<string, unknown> = {};
Object.entries(inputs).forEach(([key, value]) => {
const type = rotationTemplate.template.inputs.properties[key].type;
if (type === "string") {
formattedInputs[key] = value;
return;
}
if (type === "integer") {
formattedInputs[key] = parseInt(value as string, 10);
return;
}
formattedInputs[key] = JSON.parse(value as string);
});
// ensure input one follows the correct schema
const valid = ajv.validate(rotationTemplate.template.inputs, formattedInputs);
if (!valid) {
throw BadRequestError({ message: ajv.errors?.[0].message });
}
const encData: Partial<ISecretRotationEncData> = {
inputs: formattedInputs,
creds: []
};
const secretRotation = new SecretRotation({
workspace: workspaceId,
provider,
environment,
secretPath,
interval,
outputs: Object.entries(outputs).map(([key, secret]) => ({ key, secret }))
});
const encryptionKey = await getEncryptionKey();
const rootEncryptionKey = await getRootEncryptionKey();
if (rootEncryptionKey) {
const { ciphertext, iv, tag } = client.encryptSymmetric(
JSON.stringify(encData),
rootEncryptionKey
);
secretRotation.encryptedDataIV = iv;
secretRotation.encryptedDataTag = tag;
secretRotation.encryptedData = ciphertext;
secretRotation.algorithm = ALGORITHM_AES_256_GCM;
secretRotation.keyEncoding = ENCODING_SCHEME_BASE64;
} else if (encryptionKey) {
const { ciphertext, iv, tag } = encryptSymmetric128BitHexKeyUTF8({
plaintext: JSON.stringify(encData),
key: encryptionKey
});
secretRotation.encryptedDataIV = iv;
secretRotation.encryptedDataTag = tag;
secretRotation.encryptedData = ciphertext;
secretRotation.algorithm = ALGORITHM_AES_256_GCM;
secretRotation.keyEncoding = ENCODING_SCHEME_UTF8;
}
await secretRotation.save();
await startSecretRotationQueue(secretRotation._id.toString(), interval);
return secretRotation;
};
export const deleteSecretRotation = async ({ id }: { id: string }) => {
const doc = await SecretRotation.findByIdAndRemove(id);
if (!doc) throw BadRequestError({ message: "Rotation not found" });
await removeSecretRotationQueue(doc._id.toString(), doc.interval);
return doc;
};
export const restartSecretRotation = async ({ id }: { id: string }) => {
const secretRotation = await SecretRotation.findById(id);
if (!secretRotation) throw BadRequestError({ message: "Rotation not found" });
await removeSecretRotationQueue(secretRotation._id.toString(), secretRotation.interval);
await startSecretRotationQueue(secretRotation._id.toString(), secretRotation.interval);
return secretRotation;
};
export const getSecretRotationById = async ({ id }: { id: string }) => {
const doc = await SecretRotation.findById(id);
if (!doc) throw BadRequestError({ message: "Rotation not found" });
return doc;
};
export const getSecretRotationOfWorkspace = async (workspaceId: string) => {
const secretRotations = await SecretRotation.find({
workspace: workspaceId
}).populate("outputs.secret");
return secretRotations;
};

@ -1,28 +0,0 @@
import { ISecretRotationProviderTemplate } from "../types";
import { MYSQL_TEMPLATE } from "./mysql";
import { POSTGRES_TEMPLATE } from "./postgres";
import { SENDGRID_TEMPLATE } from "./sendgrid";
export const rotationTemplates: ISecretRotationProviderTemplate[] = [
{
name: "sendgrid",
title: "Twilio Sendgrid",
image: "sendgrid.png",
description: "Rotate Twilio Sendgrid API keys",
template: SENDGRID_TEMPLATE
},
{
name: "postgres",
title: "PostgreSQL",
image: "postgres.png",
description: "Rotate PostgreSQL/CockroachDB user credentials",
template: POSTGRES_TEMPLATE
},
{
name: "mysql",
title: "MySQL",
image: "mysql.png",
description: "Rotate MySQL@7/MariaDB user credentials",
template: MYSQL_TEMPLATE
}
];

@ -1,83 +0,0 @@
import { TAssignOp, TDbProviderClients, TProviderFunctionTypes } from "../types";
export const MYSQL_TEMPLATE = {
inputs: {
type: "object" as const,
properties: {
admin_username: { type: "string" as const },
admin_password: { type: "string" as const },
host: { type: "string" as const },
database: { type: "string" as const },
port: { type: "integer" as const, default: "3306" },
username1: {
type: "string",
default: "infisical-sql-user1",
desc: "This user must be created in your database"
},
username2: {
type: "string",
default: "infisical-sql-user2",
desc: "This user must be created in your database"
},
ca: { type: "string", desc: "SSL certificate for db auth(string)" }
},
required: [
"admin_username",
"admin_password",
"host",
"database",
"username1",
"username2",
"port"
],
additionalProperties: false
},
outputs: {
db_username: { type: "string" },
db_password: { type: "string" }
},
internal: {
rotated_password: { type: "string" },
username: { type: "string" }
},
functions: {
set: {
type: TProviderFunctionTypes.DB as const,
client: TDbProviderClients.Sql,
username: "${inputs.admin_username}",
password: "${inputs.admin_password}",
host: "${inputs.host}",
database: "${inputs.database}",
port: "${inputs.port}",
ca: "${inputs.ca}",
query: "ALTER USER ${internal.username} IDENTIFIED BY '${internal.rotated_password}'",
setter: {
"outputs.db_username": {
assign: TAssignOp.Direct as const,
value: "${internal.username}"
},
"outputs.db_password": {
assign: TAssignOp.Direct as const,
value: "${internal.rotated_password}"
}
},
pre: {
"internal.rotated_password": {
assign: TAssignOp.Direct as const,
value: "${random | 32}"
}
}
},
test: {
type: TProviderFunctionTypes.DB as const,
client: TDbProviderClients.Sql,
username: "${internal.username}",
password: "${internal.rotated_password}",
host: "${inputs.host}",
database: "${inputs.database}",
port: "${inputs.port}",
ca: "${inputs.ca}",
query: "SELECT NOW()"
}
}
};

@ -1,83 +0,0 @@
import { TAssignOp, TDbProviderClients, TProviderFunctionTypes } from "../types";
export const POSTGRES_TEMPLATE = {
inputs: {
type: "object" as const,
properties: {
admin_username: { type: "string" as const },
admin_password: { type: "string" as const },
host: { type: "string" as const },
database: { type: "string" as const },
port: { type: "integer" as const, default: "5432" },
username1: {
type: "string",
default: "infisical-pg-user1",
desc: "This user must be created in your database"
},
username2: {
type: "string",
default: "infisical-pg-user2",
desc: "This user must be created in your database"
},
ca: { type: "string", desc: "SSL certificate for db auth(string)" }
},
required: [
"admin_username",
"admin_password",
"host",
"database",
"username1",
"username2",
"port"
],
additionalProperties: false
},
outputs: {
db_username: { type: "string" },
db_password: { type: "string" }
},
internal: {
rotated_password: { type: "string" },
username: { type: "string" }
},
functions: {
set: {
type: TProviderFunctionTypes.DB as const,
client: TDbProviderClients.Pg,
username: "${inputs.admin_username}",
password: "${inputs.admin_password}",
host: "${inputs.host}",
database: "${inputs.database}",
port: "${inputs.port}",
ca: "${inputs.ca}",
query: "ALTER USER ${internal.username} WITH PASSWORD '${internal.rotated_password}'",
setter: {
"outputs.db_username": {
assign: TAssignOp.Direct as const,
value: "${internal.username}"
},
"outputs.db_password": {
assign: TAssignOp.Direct as const,
value: "${internal.rotated_password}"
}
},
pre: {
"internal.rotated_password": {
assign: TAssignOp.Direct as const,
value: "${random | 32}"
}
}
},
test: {
type: TProviderFunctionTypes.DB as const,
client: TDbProviderClients.Pg,
username: "${internal.username}",
password: "${internal.rotated_password}",
host: "${inputs.host}",
database: "${inputs.database}",
port: "${inputs.port}",
ca: "${inputs.ca}",
query: "SELECT NOW()"
}
}
};

@ -1,63 +0,0 @@
import { TAssignOp, TProviderFunctionTypes } from "../types";
export const SENDGRID_TEMPLATE = {
inputs: {
type: "object" as const,
properties: {
admin_api_key: { type: "string" as const, desc: "Sendgrid admin api key to create new keys" },
api_key_scopes: {
type: "array",
items: { type: "string" as const },
desc: "Scopes for created tokens by rotation(Array)"
}
},
required: ["admin_api_key", "api_key_scopes"],
additionalProperties: false
},
outputs: {
api_key: { type: "string" }
},
internal: {
api_key_id: { type: "string" }
},
functions: {
set: {
type: TProviderFunctionTypes.HTTP as const,
url: "https://api.sendgrid.com/v3/api_keys",
method: "POST",
header: {
Authorization: "Bearer ${inputs.admin_api_key}"
},
body: {
name: "infisical-${random | 16}",
scopes: { ref: "inputs.api_key_scopes" }
},
setter: {
"outputs.api_key": {
assign: TAssignOp.JmesPath as const,
path: "api_key"
},
"internal.api_key_id": {
assign: TAssignOp.JmesPath as const,
path: "api_key_id"
}
}
},
remove: {
type: TProviderFunctionTypes.HTTP as const,
url: "https://api.sendgrid.com/v3/api_keys/${internal.api_key_id}",
header: {
Authorization: "Bearer ${inputs.admin_api_key}"
},
method: "DELETE"
},
test: {
type: TProviderFunctionTypes.HTTP as const,
url: "https://api.sendgrid.com/v3/api_keys/${internal.api_key_id}",
header: {
Authorization: "Bearer ${inputs.admin_api_key}"
},
method: "GET"
}
}
};

@ -1,131 +0,0 @@
import { Document, Types } from "mongoose";
export interface ISecretRotation extends Document {
_id: Types.ObjectId;
name: string;
interval: number;
provider: string;
customProvider: Types.ObjectId;
workspace: Types.ObjectId;
environment: string;
secretPath: string;
outputs: Array<{
key: string;
secret: Types.ObjectId;
}>;
status?: "success" | "failed";
lastRotatedAt?: string;
statusMessage?: string;
encryptedData: string;
encryptedDataIV: string;
encryptedDataTag: string;
algorithm: string;
keyEncoding: string;
}
export type ISecretRotationEncData = {
inputs: Record<string, unknown>;
creds: Array<{
outputs: Record<string, unknown>;
internal: Record<string, unknown>;
}>;
};
export type ISecretRotationData = {
inputs: Record<string, unknown>;
outputs: Record<string, unknown>;
internal: Record<string, unknown>;
};
export type ISecretRotationProviderTemplate = {
name: string;
title: string;
image?: string;
description?: string;
template: TProviderTemplate;
};
export enum TProviderFunctionTypes {
HTTP = "http",
DB = "database"
}
export enum TDbProviderClients {
// postgres, cockroack db, amazon red shift
Pg = "pg",
// mysql and maria db
Sql = "sql"
}
export enum TAssignOp {
Direct = "direct",
JmesPath = "jmesopath"
}
export type TJmesPathAssignOp = {
assign: TAssignOp.JmesPath;
path: string;
};
export type TDirectAssignOp = {
assign: TAssignOp.Direct;
value: string;
};
export type TAssignFunction = TJmesPathAssignOp | TDirectAssignOp;
export type THttpProviderFunction = {
type: TProviderFunctionTypes.HTTP;
url: string;
method: string;
header?: Record<string, string>;
query?: Record<string, string>;
body?: Record<string, unknown>;
setter?: Record<string, TAssignFunction>;
pre?: Record<string, TDirectAssignOp>;
};
export type TDbProviderFunction = {
type: TProviderFunctionTypes.DB;
client: TDbProviderClients;
username: string;
password: string;
host: string;
database: string;
port: string;
query: string;
setter?: Record<string, TAssignFunction>;
pre?: Record<string, TDirectAssignOp>;
};
export type TProviderFunction = THttpProviderFunction | TDbProviderFunction;
export type TProviderTemplate = {
inputs: {
type: "object";
properties: Record<string, { type: string; [x: string]: unknown; desc?: string }>;
required?: string[];
};
outputs: Record<string, unknown>;
functions: {
set: TProviderFunction;
remove?: TProviderFunction;
test: TProviderFunction;
};
};
// function type args
export type TGetProviderTemplates = {
workspaceId: string;
};
export type TCreateSecretRotation = {
provider: string;
customProvider?: string;
workspaceId: string;
secretPath: string;
environment: string;
interval: number;
inputs: Record<string, unknown>;
outputs: Record<string, string>;
};

@ -1,415 +0,0 @@
import { Types } from "mongoose";
import {
AbilityBuilder,
ForcedSubject,
MongoAbility,
RawRuleOf,
buildMongoQueryMatcher,
createMongoAbility
} from "@casl/ability";
import { UnauthorizedRequestError } from "../../utils/errors";
import { FieldCondition, FieldInstruction, JsInterpreter } from "@ucast/mongo2js";
import picomatch from "picomatch";
import { AuthData } from "../../interfaces/middleware";
import { ActorType, IRole, Role } from "../models";
import {
IIdentity,
IdentityMembership,
Membership,
ServiceTokenData
} from "../../models";
import { ADMIN, CUSTOM, MEMBER, NO_ACCESS, VIEWER } from "../../variables";
import { BadRequestError } from "../../utils/errors";
const $glob: FieldInstruction<string> = {
type: "field",
validate(instruction, value) {
if (typeof value !== "string") {
throw new Error(`"${instruction.name}" expects value to be a string`);
}
}
};
const glob: JsInterpreter<FieldCondition<string>> = (node, object, context) => {
const secretPath = context.get(object, node.field);
const permissionSecretGlobPath = node.value;
return picomatch.isMatch(secretPath, permissionSecretGlobPath, { strictSlashes: false });
};
export const conditionsMatcher = buildMongoQueryMatcher({ $glob }, { glob });
export enum ProjectPermissionActions {
Read = "read",
Create = "create",
Edit = "edit",
Delete = "delete"
}
export enum ProjectPermissionSub {
Role = "role",
Member = "member",
Settings = "settings",
Integrations = "integrations",
Webhooks = "webhooks",
ServiceTokens = "service-tokens",
Environments = "environments",
Tags = "tags",
AuditLogs = "audit-logs",
IpAllowList = "ip-allowlist",
Workspace = "workspace",
Secrets = "secrets",
SecretRollback = "secret-rollback",
SecretApproval = "secret-approval",
SecretRotation = "secret-rotation",
Identity = "identity"
}
type SubjectFields = {
environment: string;
secretPath: string;
};
export type ProjectPermissionSet =
| [
ProjectPermissionActions,
ProjectPermissionSub.Secrets | (ForcedSubject<ProjectPermissionSub.Secrets> & SubjectFields)
]
| [ProjectPermissionActions, ProjectPermissionSub.Role]
| [ProjectPermissionActions, ProjectPermissionSub.Tags]
| [ProjectPermissionActions, ProjectPermissionSub.Member]
| [ProjectPermissionActions, ProjectPermissionSub.Integrations]
| [ProjectPermissionActions, ProjectPermissionSub.Webhooks]
| [ProjectPermissionActions, ProjectPermissionSub.AuditLogs]
| [ProjectPermissionActions, ProjectPermissionSub.Environments]
| [ProjectPermissionActions, ProjectPermissionSub.IpAllowList]
| [ProjectPermissionActions, ProjectPermissionSub.Settings]
| [ProjectPermissionActions, ProjectPermissionSub.ServiceTokens]
| [ProjectPermissionActions, ProjectPermissionSub.SecretApproval]
| [ProjectPermissionActions, ProjectPermissionSub.SecretRotation]
| [ProjectPermissionActions, ProjectPermissionSub.Identity]
| [ProjectPermissionActions.Delete, ProjectPermissionSub.Workspace]
| [ProjectPermissionActions.Edit, ProjectPermissionSub.Workspace]
| [ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback]
| [ProjectPermissionActions.Create, ProjectPermissionSub.SecretRollback];
const buildAdminPermission = () => {
const { can, build } = new AbilityBuilder<MongoAbility<ProjectPermissionSet>>(createMongoAbility);
can(ProjectPermissionActions.Read, ProjectPermissionSub.Secrets);
can(ProjectPermissionActions.Create, ProjectPermissionSub.Secrets);
can(ProjectPermissionActions.Edit, ProjectPermissionSub.Secrets);
can(ProjectPermissionActions.Delete, ProjectPermissionSub.Secrets);
can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretApproval);
can(ProjectPermissionActions.Create, ProjectPermissionSub.SecretApproval);
can(ProjectPermissionActions.Edit, ProjectPermissionSub.SecretApproval);
can(ProjectPermissionActions.Delete, ProjectPermissionSub.SecretApproval);
can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretRotation);
can(ProjectPermissionActions.Create, ProjectPermissionSub.SecretRotation);
can(ProjectPermissionActions.Edit, ProjectPermissionSub.SecretRotation);
can(ProjectPermissionActions.Delete, ProjectPermissionSub.SecretRotation);
can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback);
can(ProjectPermissionActions.Create, ProjectPermissionSub.SecretRollback);
can(ProjectPermissionActions.Read, ProjectPermissionSub.Member);
can(ProjectPermissionActions.Create, ProjectPermissionSub.Member);
can(ProjectPermissionActions.Edit, ProjectPermissionSub.Member);
can(ProjectPermissionActions.Delete, ProjectPermissionSub.Member);
can(ProjectPermissionActions.Read, ProjectPermissionSub.Role);
can(ProjectPermissionActions.Create, ProjectPermissionSub.Role);
can(ProjectPermissionActions.Edit, ProjectPermissionSub.Role);
can(ProjectPermissionActions.Delete, ProjectPermissionSub.Role);
can(ProjectPermissionActions.Read, ProjectPermissionSub.Integrations);
can(ProjectPermissionActions.Create, ProjectPermissionSub.Integrations);
can(ProjectPermissionActions.Edit, ProjectPermissionSub.Integrations);
can(ProjectPermissionActions.Delete, ProjectPermissionSub.Integrations);
can(ProjectPermissionActions.Read, ProjectPermissionSub.Webhooks);
can(ProjectPermissionActions.Create, ProjectPermissionSub.Webhooks);
can(ProjectPermissionActions.Edit, ProjectPermissionSub.Webhooks);
can(ProjectPermissionActions.Delete, ProjectPermissionSub.Webhooks);
can(ProjectPermissionActions.Read, ProjectPermissionSub.Identity);
can(ProjectPermissionActions.Create, ProjectPermissionSub.Identity);
can(ProjectPermissionActions.Edit, ProjectPermissionSub.Identity);
can(ProjectPermissionActions.Delete, ProjectPermissionSub.Identity);
can(ProjectPermissionActions.Read, ProjectPermissionSub.ServiceTokens);
can(ProjectPermissionActions.Create, ProjectPermissionSub.ServiceTokens);
can(ProjectPermissionActions.Edit, ProjectPermissionSub.ServiceTokens);
can(ProjectPermissionActions.Delete, ProjectPermissionSub.ServiceTokens);
can(ProjectPermissionActions.Read, ProjectPermissionSub.Settings);
can(ProjectPermissionActions.Create, ProjectPermissionSub.Settings);
can(ProjectPermissionActions.Edit, ProjectPermissionSub.Settings);
can(ProjectPermissionActions.Delete, ProjectPermissionSub.Settings);
can(ProjectPermissionActions.Read, ProjectPermissionSub.Environments);
can(ProjectPermissionActions.Create, ProjectPermissionSub.Environments);
can(ProjectPermissionActions.Edit, ProjectPermissionSub.Environments);
can(ProjectPermissionActions.Delete, ProjectPermissionSub.Environments);
can(ProjectPermissionActions.Read, ProjectPermissionSub.Tags);
can(ProjectPermissionActions.Create, ProjectPermissionSub.Tags);
can(ProjectPermissionActions.Edit, ProjectPermissionSub.Tags);
can(ProjectPermissionActions.Delete, ProjectPermissionSub.Tags);
can(ProjectPermissionActions.Read, ProjectPermissionSub.AuditLogs);
can(ProjectPermissionActions.Create, ProjectPermissionSub.AuditLogs);
can(ProjectPermissionActions.Edit, ProjectPermissionSub.AuditLogs);
can(ProjectPermissionActions.Delete, ProjectPermissionSub.AuditLogs);
can(ProjectPermissionActions.Read, ProjectPermissionSub.IpAllowList);
can(ProjectPermissionActions.Create, ProjectPermissionSub.IpAllowList);
can(ProjectPermissionActions.Edit, ProjectPermissionSub.IpAllowList);
can(ProjectPermissionActions.Delete, ProjectPermissionSub.IpAllowList);
can(ProjectPermissionActions.Edit, ProjectPermissionSub.Workspace);
can(ProjectPermissionActions.Delete, ProjectPermissionSub.Workspace);
return build({ conditionsMatcher });
};
export const adminProjectPermissions = buildAdminPermission();
const buildMemberPermission = () => {
const { can, build } = new AbilityBuilder<MongoAbility<ProjectPermissionSet>>(createMongoAbility);
can(ProjectPermissionActions.Read, ProjectPermissionSub.Secrets);
can(ProjectPermissionActions.Create, ProjectPermissionSub.Secrets);
can(ProjectPermissionActions.Edit, ProjectPermissionSub.Secrets);
can(ProjectPermissionActions.Delete, ProjectPermissionSub.Secrets);
can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretApproval);
can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretRotation);
can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback);
can(ProjectPermissionActions.Create, ProjectPermissionSub.SecretRollback);
can(ProjectPermissionActions.Read, ProjectPermissionSub.Member);
can(ProjectPermissionActions.Create, ProjectPermissionSub.Member);
can(ProjectPermissionActions.Read, ProjectPermissionSub.Integrations);
can(ProjectPermissionActions.Create, ProjectPermissionSub.Integrations);
can(ProjectPermissionActions.Edit, ProjectPermissionSub.Integrations);
can(ProjectPermissionActions.Delete, ProjectPermissionSub.Integrations);
can(ProjectPermissionActions.Read, ProjectPermissionSub.Webhooks);
can(ProjectPermissionActions.Create, ProjectPermissionSub.Webhooks);
can(ProjectPermissionActions.Edit, ProjectPermissionSub.Webhooks);
can(ProjectPermissionActions.Delete, ProjectPermissionSub.Webhooks);
can(ProjectPermissionActions.Read, ProjectPermissionSub.Identity);
can(ProjectPermissionActions.Create, ProjectPermissionSub.Identity);
can(ProjectPermissionActions.Edit, ProjectPermissionSub.Identity);
can(ProjectPermissionActions.Delete, ProjectPermissionSub.Identity);
can(ProjectPermissionActions.Read, ProjectPermissionSub.ServiceTokens);
can(ProjectPermissionActions.Create, ProjectPermissionSub.ServiceTokens);
can(ProjectPermissionActions.Edit, ProjectPermissionSub.ServiceTokens);
can(ProjectPermissionActions.Delete, ProjectPermissionSub.ServiceTokens);
can(ProjectPermissionActions.Read, ProjectPermissionSub.Settings);
can(ProjectPermissionActions.Create, ProjectPermissionSub.Settings);
can(ProjectPermissionActions.Edit, ProjectPermissionSub.Settings);
can(ProjectPermissionActions.Delete, ProjectPermissionSub.Settings);
can(ProjectPermissionActions.Read, ProjectPermissionSub.Environments);
can(ProjectPermissionActions.Create, ProjectPermissionSub.Environments);
can(ProjectPermissionActions.Edit, ProjectPermissionSub.Environments);
can(ProjectPermissionActions.Delete, ProjectPermissionSub.Environments);
can(ProjectPermissionActions.Read, ProjectPermissionSub.Tags);
can(ProjectPermissionActions.Create, ProjectPermissionSub.Tags);
can(ProjectPermissionActions.Edit, ProjectPermissionSub.Tags);
can(ProjectPermissionActions.Delete, ProjectPermissionSub.Tags);
can(ProjectPermissionActions.Read, ProjectPermissionSub.Role);
can(ProjectPermissionActions.Read, ProjectPermissionSub.AuditLogs);
can(ProjectPermissionActions.Read, ProjectPermissionSub.IpAllowList);
return build({ conditionsMatcher });
};
export const memberProjectPermissions = buildMemberPermission();
const buildViewerPermission = () => {
const { can, build } = new AbilityBuilder<MongoAbility<ProjectPermissionSet>>(createMongoAbility);
can(ProjectPermissionActions.Read, ProjectPermissionSub.Secrets);
can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretApproval);
can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback);
can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretRotation);
can(ProjectPermissionActions.Read, ProjectPermissionSub.Member);
can(ProjectPermissionActions.Read, ProjectPermissionSub.Role);
can(ProjectPermissionActions.Read, ProjectPermissionSub.Integrations);
can(ProjectPermissionActions.Read, ProjectPermissionSub.Webhooks);
can(ProjectPermissionActions.Read, ProjectPermissionSub.Identity);
can(ProjectPermissionActions.Read, ProjectPermissionSub.ServiceTokens);
can(ProjectPermissionActions.Read, ProjectPermissionSub.Settings);
can(ProjectPermissionActions.Read, ProjectPermissionSub.Environments);
can(ProjectPermissionActions.Read, ProjectPermissionSub.Tags);
can(ProjectPermissionActions.Read, ProjectPermissionSub.AuditLogs);
can(ProjectPermissionActions.Read, ProjectPermissionSub.IpAllowList);
return build({ conditionsMatcher });
};
export const viewerProjectPermission = buildViewerPermission();
const buildNoAccessProjectPermission = () => {
const { build } = new AbilityBuilder<MongoAbility<ProjectPermissionSet>>(createMongoAbility);
return build({ conditionsMatcher });
}
export const noAccessProjectPermissions = buildNoAccessProjectPermission();
/**
* Return permissions for user/service pertaining to workspace with id [workspaceId]
*
* Note: should not rely on this function for ST V2 authorization logic
* b/c ST V2 does not support role-based access control
*/
export const getAuthDataProjectPermissions = async ({
authData,
workspaceId
}: {
authData: AuthData;
workspaceId: Types.ObjectId;
}) => {
let role: "admin" | "member" | "viewer" | "no-access" | "custom";
let customRole;
switch (authData.actor.type) {
case ActorType.USER: {
const membership = await Membership.findOne({
user: authData.authPayload._id,
workspace: workspaceId
})
.populate<{
customRole: IRole & { permissions: RawRuleOf<MongoAbility<ProjectPermissionSet>>[] };
}>("customRole")
.exec();
if (!membership || (membership.role === "custom" && !membership.customRole)) {
throw UnauthorizedRequestError();
}
role = membership.role;
customRole = membership.customRole;
break;
}
case ActorType.SERVICE: {
const serviceTokenData = await ServiceTokenData.findById(authData.authPayload._id);
if (!serviceTokenData || !serviceTokenData.workspace.equals(workspaceId)) throw UnauthorizedRequestError();
role = "viewer";
break;
}
case ActorType.IDENTITY: {
const identityMembership = await IdentityMembership.findOne({
identity: authData.authPayload._id,
workspace: workspaceId
})
.populate<{
customRole: IRole & { permissions: RawRuleOf<MongoAbility<ProjectPermissionSet>>[] };
identity: IIdentity
}>("customRole identity")
.exec();
if (!identityMembership || (identityMembership.role === "custom" && !identityMembership.customRole)) {
throw UnauthorizedRequestError();
}
role = identityMembership.role;
customRole = identityMembership.customRole;
break;
}
default:
throw UnauthorizedRequestError();
}
switch (role) {
case ADMIN:
return { permission: adminProjectPermissions };
case MEMBER:
return { permission: memberProjectPermissions };
case VIEWER:
return { permission: viewerProjectPermission };
case NO_ACCESS:
return { permission: noAccessProjectPermissions };
case CUSTOM: {
if (!customRole) throw UnauthorizedRequestError();
return {
permission: createMongoAbility<ProjectPermissionSet>(
customRole.permissions,
{ conditionsMatcher }
)
};
}
default:
throw UnauthorizedRequestError();
}
}
export const getWorkspaceRolePermissions = async (role: string, workspaceId: string) => {
const isCustomRole = ![ADMIN, MEMBER, VIEWER, NO_ACCESS].includes(role);
if (isCustomRole) {
const workspaceRole = await Role.findOne({
slug: role,
isOrgRole: false,
workspace: new Types.ObjectId(workspaceId)
});
if (!workspaceRole) throw BadRequestError({ message: "Role not found" });
return createMongoAbility<ProjectPermissionSet>(workspaceRole.permissions as RawRuleOf<MongoAbility<ProjectPermissionSet>>[], {
conditionsMatcher
});
}
switch (role) {
case ADMIN:
return adminProjectPermissions;
case MEMBER:
return memberProjectPermissions;
case VIEWER:
return viewerProjectPermission;
case NO_ACCESS:
return noAccessProjectPermissions;
default:
throw BadRequestError({ message: "Role not found" });
}
}
/**
* Extracts and formats permissions from a CASL Ability object or a raw permission set.
* @param ability
* @returns
*/
const extractPermissions = (ability: any) => {
return ability.A.map((permission: any) => `${permission.action}_${permission.subject}`);
}
/**
* Compares two sets of permissions to determine if the first set is at least as privileged as the second set.
* The function checks if all permissions in the second set are contained within the first set and if the first set has equal or more permissions.
*
*/
export const isAtLeastAsPrivilegedWorkspace = (permissions1: MongoAbility<ProjectPermissionSet> | ProjectPermissionSet, permissions2: MongoAbility<ProjectPermissionSet> | ProjectPermissionSet) => {
const set1 = new Set(extractPermissions(permissions1));
const set2 = new Set(extractPermissions(permissions2));
for (const perm of set2) {
if (!set1.has(perm)) {
return false;
}
}
return set1.size >= set2.size;
}

@ -1,301 +0,0 @@
import { Types } from "mongoose";
import { AbilityBuilder, MongoAbility, RawRuleOf, createMongoAbility } from "@casl/ability";
import {
IIdentity,
IdentityMembershipOrg,
MembershipOrg
} from "../../models";
import { ActorType, IRole, Role } from "../models";
import { BadRequestError, UnauthorizedRequestError } from "../../utils/errors";
import { ACCEPTED, ADMIN, CUSTOM, MEMBER, NO_ACCESS} from "../../variables";
import { conditionsMatcher } from "./ProjectRoleService";
import { AuthData } from "../../interfaces/middleware";
export enum OrgPermissionActions {
Read = "read",
Create = "create",
Edit = "edit",
Delete = "delete"
}
export enum OrgPermissionSubjects {
Workspace = "workspace",
Role = "role",
Member = "member",
Settings = "settings",
IncidentAccount = "incident-contact",
Sso = "sso",
Billing = "billing",
SecretScanning = "secret-scanning",
Identity = "identity"
}
export type OrgPermissionSet =
| [OrgPermissionActions.Read, OrgPermissionSubjects.Workspace]
| [OrgPermissionActions.Create, OrgPermissionSubjects.Workspace]
| [OrgPermissionActions, OrgPermissionSubjects.Role]
| [OrgPermissionActions, OrgPermissionSubjects.Member]
| [OrgPermissionActions, OrgPermissionSubjects.Settings]
| [OrgPermissionActions, OrgPermissionSubjects.IncidentAccount]
| [OrgPermissionActions, OrgPermissionSubjects.Sso]
| [OrgPermissionActions, OrgPermissionSubjects.SecretScanning]
| [OrgPermissionActions, OrgPermissionSubjects.Billing]
| [OrgPermissionActions, OrgPermissionSubjects.Identity];
const buildAdminPermission = () => {
const { can, build } = new AbilityBuilder<MongoAbility<OrgPermissionSet>>(createMongoAbility);
// ws permissions
can(OrgPermissionActions.Read, OrgPermissionSubjects.Workspace);
can(OrgPermissionActions.Create, OrgPermissionSubjects.Workspace);
// role permission
can(OrgPermissionActions.Read, OrgPermissionSubjects.Role);
can(OrgPermissionActions.Create, OrgPermissionSubjects.Role);
can(OrgPermissionActions.Edit, OrgPermissionSubjects.Role);
can(OrgPermissionActions.Delete, OrgPermissionSubjects.Role);
can(OrgPermissionActions.Read, OrgPermissionSubjects.Member);
can(OrgPermissionActions.Create, OrgPermissionSubjects.Member);
can(OrgPermissionActions.Edit, OrgPermissionSubjects.Member);
can(OrgPermissionActions.Delete, OrgPermissionSubjects.Member);
can(OrgPermissionActions.Read, OrgPermissionSubjects.SecretScanning);
can(OrgPermissionActions.Create, OrgPermissionSubjects.SecretScanning);
can(OrgPermissionActions.Edit, OrgPermissionSubjects.SecretScanning);
can(OrgPermissionActions.Delete, OrgPermissionSubjects.SecretScanning);
can(OrgPermissionActions.Read, OrgPermissionSubjects.Settings);
can(OrgPermissionActions.Create, OrgPermissionSubjects.Settings);
can(OrgPermissionActions.Edit, OrgPermissionSubjects.Settings);
can(OrgPermissionActions.Delete, OrgPermissionSubjects.Settings);
can(OrgPermissionActions.Read, OrgPermissionSubjects.IncidentAccount);
can(OrgPermissionActions.Create, OrgPermissionSubjects.IncidentAccount);
can(OrgPermissionActions.Edit, OrgPermissionSubjects.IncidentAccount);
can(OrgPermissionActions.Delete, OrgPermissionSubjects.IncidentAccount);
can(OrgPermissionActions.Read, OrgPermissionSubjects.Sso);
can(OrgPermissionActions.Create, OrgPermissionSubjects.Sso);
can(OrgPermissionActions.Edit, OrgPermissionSubjects.Sso);
can(OrgPermissionActions.Delete, OrgPermissionSubjects.Sso);
can(OrgPermissionActions.Read, OrgPermissionSubjects.Billing);
can(OrgPermissionActions.Create, OrgPermissionSubjects.Billing);
can(OrgPermissionActions.Edit, OrgPermissionSubjects.Billing);
can(OrgPermissionActions.Delete, OrgPermissionSubjects.Billing);
can(OrgPermissionActions.Read, OrgPermissionSubjects.Identity);
can(OrgPermissionActions.Create, OrgPermissionSubjects.Identity);
can(OrgPermissionActions.Edit, OrgPermissionSubjects.Identity);
can(OrgPermissionActions.Delete, OrgPermissionSubjects.Identity);
return build({ conditionsMatcher });
};
export const adminPermissions = buildAdminPermission();
const buildMemberPermission = () => {
const { can, build } = new AbilityBuilder<MongoAbility<OrgPermissionSet>>(createMongoAbility);
can(OrgPermissionActions.Read, OrgPermissionSubjects.Workspace);
can(OrgPermissionActions.Create, OrgPermissionSubjects.Workspace);
can(OrgPermissionActions.Read, OrgPermissionSubjects.Member);
can(OrgPermissionActions.Create, OrgPermissionSubjects.Member);
can(OrgPermissionActions.Read, OrgPermissionSubjects.Role);
can(OrgPermissionActions.Read, OrgPermissionSubjects.Settings);
can(OrgPermissionActions.Read, OrgPermissionSubjects.Billing);
can(OrgPermissionActions.Read, OrgPermissionSubjects.Sso);
can(OrgPermissionActions.Read, OrgPermissionSubjects.IncidentAccount);
can(OrgPermissionActions.Read, OrgPermissionSubjects.SecretScanning);
can(OrgPermissionActions.Create, OrgPermissionSubjects.SecretScanning);
can(OrgPermissionActions.Edit, OrgPermissionSubjects.SecretScanning);
can(OrgPermissionActions.Delete, OrgPermissionSubjects.SecretScanning);
can(OrgPermissionActions.Read, OrgPermissionSubjects.Identity);
can(OrgPermissionActions.Create, OrgPermissionSubjects.Identity);
can(OrgPermissionActions.Edit, OrgPermissionSubjects.Identity);
can(OrgPermissionActions.Delete, OrgPermissionSubjects.Identity);
return build({ conditionsMatcher });
};
export const memberPermissions = buildMemberPermission();
const buildNoAccessPermission = () => {
const { build } = new AbilityBuilder<MongoAbility<OrgPermissionSet>>(createMongoAbility);
return build({ conditionsMatcher });
}
export const noAccessPermissions = buildNoAccessPermission();
export const getUserOrgPermissions = async (userId: string, orgId: string) => {
// TODO(akhilmhdh): speed this up by pulling from cache later
const membership = await MembershipOrg.findOne({
user: userId,
organization: orgId,
status: ACCEPTED
})
.populate<{ customRole: IRole & { permissions: RawRuleOf<MongoAbility<OrgPermissionSet>>[] } }>(
"customRole"
)
.exec();
if (!membership || (membership.role === "custom" && !membership.customRole)) {
throw UnauthorizedRequestError({ message: "User doesn't belong to organization" });
}
if (membership.role === ADMIN) return { permission: adminPermissions, membership };
if (membership.role === MEMBER) return { permission: memberPermissions, membership };
if (membership.role === NO_ACCESS) return { permission: noAccessPermissions, membership }
if (membership.role === CUSTOM) {
const permission = createMongoAbility<OrgPermissionSet>(membership.customRole.permissions, {
conditionsMatcher
});
return { permission, membership };
}
throw BadRequestError({ message: "User role not found" });
};
/**
* Return permissions for user/service pertaining to organization with id [organizationId]
*
* Note: should not rely on this function for ST V2 authorization logic
* b/c ST V2 does not support role-based access control but also not organization-level resources
*/
export const getAuthDataOrgPermissions = async ({
authData,
organizationId
}: {
authData: AuthData;
organizationId: Types.ObjectId;
}) => {
let role: "admin" | "member" | "no-access" | "custom";
let customRole;
switch (authData.actor.type) {
case ActorType.USER: {
const membershipOrg = await MembershipOrg.findOne({
user: authData.authPayload._id,
organization: organizationId,
status: ACCEPTED
})
.populate<{ customRole: IRole & { permissions: RawRuleOf<MongoAbility<OrgPermissionSet>>[] } }>(
"customRole"
)
.exec();
if (!membershipOrg || (membershipOrg.role === "custom" && !membershipOrg.customRole)) {
throw UnauthorizedRequestError({ message: "User doesn't belong to organization" });
}
role = membershipOrg.role;
customRole = membershipOrg.customRole;
break;
}
case ActorType.SERVICE: {
throw UnauthorizedRequestError({
message: "Failed to access organization-level resources with service token"
});
}
case ActorType.IDENTITY: {
const identityMembershipOrg = await IdentityMembershipOrg.findOne({
identity: authData.authPayload._id,
organization: organizationId
})
.populate<{
customRole: IRole & { permissions: RawRuleOf<MongoAbility<OrgPermissionSet>>[] };
identity: IIdentity
}>("customRole identity")
.exec();
if (!identityMembershipOrg || (identityMembershipOrg.role === "custom" && !identityMembershipOrg.customRole)) {
throw UnauthorizedRequestError();
}
role = identityMembershipOrg.role;
customRole = identityMembershipOrg.customRole;
break;
}
default:
throw UnauthorizedRequestError();
}
switch (role) {
case ADMIN:
return { permission: adminPermissions };
case MEMBER:
return { permission: memberPermissions };
case NO_ACCESS:
return { permission: noAccessPermissions };
case CUSTOM: {
if (!customRole) throw UnauthorizedRequestError();
return {
permission: createMongoAbility<OrgPermissionSet>(
customRole.permissions,
{ conditionsMatcher }
)
};
}
}
}
export const getOrgRolePermissions = async (role: string, orgId: string) => {
const isCustomRole = ![ADMIN, MEMBER, NO_ACCESS].includes(role);
if (isCustomRole) {
const orgRole = await Role.findOne({
slug: role,
isOrgRole: true,
organization: new Types.ObjectId(orgId)
});
if (!orgRole) throw BadRequestError({ message: "Org Role not found" });
return createMongoAbility<OrgPermissionSet>(orgRole.permissions as RawRuleOf<MongoAbility<OrgPermissionSet>>[], {
conditionsMatcher
});
}
switch (role) {
case ADMIN:
return adminPermissions;
case MEMBER:
return memberPermissions;
case NO_ACCESS:
return noAccessPermissions;
default:
throw BadRequestError({ message: "User org role not found" });
}
}
/**
* Extracts and formats permissions from a CASL Ability object or a raw permission set.
* @param ability
* @returns
*/
const extractPermissions = (ability: any) => {
return ability.A.map((permission: any) => `${permission.action}_${permission.subject}`);
}
/**
* Compares two sets of permissions to determine if the first set is at least as privileged as the second set.
* The function checks if all permissions in the second set are contained within the first set and if the first set has equal or more permissions.
*
*/
export const isAtLeastAsPrivilegedOrg = (permissions1: MongoAbility<OrgPermissionSet> | OrgPermissionSet, permissions2: MongoAbility<OrgPermissionSet> | OrgPermissionSet) => {
const set1 = new Set(extractPermissions(permissions1));
const set2 = new Set(extractPermissions(permissions2));
for (const perm of set2) {
if (!set1.has(perm)) {
return false;
}
}
return set1.size >= set2.size;
}

@ -1,656 +0,0 @@
import picomatch from "picomatch";
import { Types } from "mongoose";
import {
containsGlobPatterns,
generateSecretBlindIndexWithSaltHelper,
getSecretBlindIndexSaltHelper
} from "../../helpers/secrets";
import { Folder, ISecret, Secret } from "../../models";
import { ISecretApprovalPolicy, SecretApprovalPolicy } from "../models/secretApprovalPolicy";
import {
CommitType,
ISecretApprovalRequest,
ISecretApprovalSecChange,
ISecretCommits,
SecretApprovalRequest
} from "../models/secretApprovalRequest";
import { BadRequestError } from "../../utils/errors";
import { getFolderByPath } from "../../services/FolderService";
import { ALGORITHM_AES_256_GCM, ENCODING_SCHEME_UTF8, SECRET_SHARED } from "../../variables";
import TelemetryService from "../../services/TelemetryService";
import { EEAuditLogService, EESecretService } from "../services";
import { EventType, SecretVersion } from "../models";
import { AuthData } from "../../interfaces/middleware";
// if glob pattern score is 1, if not exist score is 0 and if its not both then its exact path meaning score 2
const getPolicyScore = (policy: ISecretApprovalPolicy) =>
policy.secretPath ? (containsGlobPatterns(policy.secretPath) ? 1 : 2) : 0;
// this will fetch the policy that gets priority for an environment and secret path
export const getSecretPolicyOfBoard = async (
workspaceId: string,
environment: string,
secretPath: string
) => {
const policies = await SecretApprovalPolicy.find({ workspace: workspaceId, environment });
if (!policies) return;
// this will filter policies either without scoped to secret path or the one that matches with secret path
const policiesFilteredByPath = policies.filter(
({ secretPath: policyPath }) =>
!policyPath || picomatch.isMatch(secretPath, policyPath, { strictSlashes: false })
);
// now sort by priority. exact secret path gets first match followed by glob followed by just env scoped
// if that is tie get by first createdAt
const policiesByPriority = policiesFilteredByPath.sort(
(a, b) => getPolicyScore(b) - getPolicyScore(a)
);
const finalPolicy = policiesByPriority.shift();
return finalPolicy;
};
const getLatestSecretVersion = async (secretIds: Types.ObjectId[]) => {
const latestSecretVersions = await SecretVersion.aggregate([
{
$match: {
secret: {
$in: secretIds
},
type: SECRET_SHARED
}
},
{
$sort: { version: -1 }
},
{
$group: {
_id: "$secret",
version: { $max: "$version" },
versionId: { $max: "$_id" }, // id of latest secret versionId
secret: { $first: "$$ROOT" }
}
}
]).exec();
// reduced with secret id and latest version as document
return latestSecretVersions.reduce(
(prev, curr) => ({ ...prev, [curr._id.toString()]: curr.secret }),
{}
);
};
type TApprovalCreateSecret = Omit<ISecretApprovalSecChange, "_id" | "version"> & {
secretName: string;
};
type TApprovalUpdateSecret = Partial<Omit<ISecretApprovalSecChange, "_id" | "version">> & {
secretName: string;
newSecretName?: string;
};
type TGenerateSecretApprovalRequestArg = {
workspaceId: string;
environment: string;
secretPath: string;
policy: ISecretApprovalPolicy;
data: {
[CommitType.CREATE]?: TApprovalCreateSecret[];
[CommitType.UPDATE]?: TApprovalUpdateSecret[];
[CommitType.DELETE]?: { secretName: string }[];
};
commiterMembershipId: string;
authData: AuthData;
};
export const generateSecretApprovalRequest = async ({
workspaceId,
environment,
secretPath,
policy,
data,
commiterMembershipId,
authData
}: TGenerateSecretApprovalRequestArg) => {
// calculate folder id from secret path
let folderId = "root";
const rootFolder = await Folder.findOne({ workspace: workspaceId, environment });
if (!rootFolder && secretPath !== "/") throw BadRequestError({ message: "Folder not found" });
if (rootFolder) {
const folder = getFolderByPath(rootFolder.nodes, secretPath);
if (!folder) throw BadRequestError({ message: "Folder not found" });
folderId = folder.id;
}
// generate secret blindIndexes
const salt = await getSecretBlindIndexSaltHelper({
workspaceId: new Types.ObjectId(workspaceId)
});
const commits: ISecretApprovalRequest["commits"] = [];
// -----
// for created secret approval change
const createdSecret = data[CommitType.CREATE];
if (createdSecret && createdSecret?.length) {
// validation checks whether secret exists for creation
const secretBlindIndexes = await Promise.all(
createdSecret.map(({ secretName }) =>
generateSecretBlindIndexWithSaltHelper({
secretName,
salt
})
)
).then((blindIndexes) =>
blindIndexes.reduce<Record<string, string>>((prev, curr, i) => {
prev[createdSecret[i].secretName] = curr;
return prev;
}, {})
);
// check created secret exists
const exists = await Secret.exists({
workspace: new Types.ObjectId(workspaceId),
folder: folderId,
environment
})
.or(
createdSecret.map(({ secretName }) => ({
secretBlindIndex: secretBlindIndexes[secretName],
type: SECRET_SHARED
}))
)
.exec();
if (exists) throw BadRequestError({ message: "Secrets already exist" });
commits.push(
...createdSecret.map((el) => ({
op: CommitType.CREATE as const,
newVersion: {
...el,
version: 0,
_id: new Types.ObjectId(),
secretBlindIndex: secretBlindIndexes[el.secretName]
}
}))
);
}
// ----
// updated secrets approval change
const updatedSecret = data[CommitType.UPDATE];
if (updatedSecret && updatedSecret?.length) {
// validation checks whether secret doesn't exists for update
const secretBlindIndexes = await Promise.all(
updatedSecret.map(({ secretName }) =>
generateSecretBlindIndexWithSaltHelper({
secretName,
salt
})
)
).then((blindIndexes) =>
blindIndexes.reduce<Record<string, string>>((prev, curr, i) => {
prev[updatedSecret[i].secretName] = curr;
return prev;
}, {})
);
// check update secret exists
const oldSecrets = await Secret.find({
workspace: new Types.ObjectId(workspaceId),
folder: folderId,
environment,
type: SECRET_SHARED,
secretBlindIndex: {
$in: updatedSecret.map(({ secretName }) => secretBlindIndexes[secretName])
}
})
.select("+secretBlindIndex")
.lean()
.exec();
if (oldSecrets.length !== updatedSecret.length)
throw BadRequestError({ message: "Secrets already exist" });
// finally check updating blindindex exist
const nameUpdatedSecrets = updatedSecret.filter(({ newSecretName }) => Boolean(newSecretName));
const newSecretBlindIndexes = await Promise.all(
nameUpdatedSecrets.map(({ newSecretName }) =>
generateSecretBlindIndexWithSaltHelper({
secretName: newSecretName as string,
salt
})
)
).then((blindIndexes) =>
blindIndexes.reduce<Record<string, string>>((prev, curr, i) => {
prev[nameUpdatedSecrets[i].secretName] = curr;
return prev;
}, {})
);
const doesAnySecretExistWithNewIndex = await Secret.find({
workspace: new Types.ObjectId(workspaceId),
folder: folderId,
environment,
secretBlindIndex: { $in: Object.values(newSecretBlindIndexes) }
});
if (doesAnySecretExistWithNewIndex.length)
throw BadRequestError({ message: "Secret with new name already exist" });
const oldSecretsGroupById = oldSecrets.reduce<Record<string, ISecret>>(
(prev, curr) => ({ ...prev, [curr?.secretBlindIndex || ""]: curr }),
{}
);
const latestSecretVersions = await getLatestSecretVersion(
updatedSecret.map((el) => oldSecretsGroupById[secretBlindIndexes[el.secretName]]._id)
);
commits.push(
...updatedSecret.map((el) => {
const secretId = oldSecretsGroupById[secretBlindIndexes[el.secretName]]._id;
return {
op: CommitType.UPDATE as const,
secret: secretId,
secretVersion: latestSecretVersions[secretId.toString()]._id,
newVersion: {
...el,
secretBlindIndex: newSecretBlindIndexes?.[el.secretName],
_id: new Types.ObjectId(),
version: oldSecretsGroupById[secretBlindIndexes[el.secretName]].version || 1
}
};
})
);
}
// -----
// deleted secrets
const deletedSecrets = data[CommitType.DELETE];
if (deletedSecrets && deletedSecrets.length) {
const secretBlindIndexes = await Promise.all(
deletedSecrets.map(({ secretName }) =>
generateSecretBlindIndexWithSaltHelper({
secretName,
salt
})
)
).then((blindIndexes) =>
blindIndexes.reduce<Record<string, string>>((prev, curr, i) => {
prev[deletedSecrets[i].secretName] = curr;
return prev;
}, {})
);
const secretsToDelete = await Secret.find({
workspace: new Types.ObjectId(workspaceId),
folder: folderId,
environment,
type: SECRET_SHARED,
secretBlindIndex: {
$in: deletedSecrets.map(({ secretName }) => secretBlindIndexes[secretName])
}
})
.select({ secretBlindIndex: 1, _id: 1 })
.lean()
.exec();
if (secretsToDelete.length !== deletedSecrets.length)
throw BadRequestError({ message: "Deleted secrets not found" });
const oldSecretsGroupById = secretsToDelete.reduce<Record<string, ISecret>>(
(prev, curr) => ({ ...prev, [curr?.secretBlindIndex || ""]: curr }),
{}
);
const latestSecretVersions = await getLatestSecretVersion(
deletedSecrets.map((el) => oldSecretsGroupById[secretBlindIndexes[el.secretName]]._id)
);
commits.push(
...deletedSecrets.map((el) => {
const secretId = oldSecretsGroupById[secretBlindIndexes[el.secretName]]._id;
return {
op: CommitType.DELETE as const,
secret: secretId,
secretVersion: latestSecretVersions[secretId.toString()]
};
})
);
}
const secretApprovalRequest = new SecretApprovalRequest({
workspace: workspaceId,
environment,
folderId,
policy,
commits,
committer: commiterMembershipId
});
await secretApprovalRequest.save();
await EEAuditLogService.createAuditLog(
authData,
{
type: EventType.SECRET_APPROVAL_REQUEST,
metadata: {
committedBy: commiterMembershipId,
secretApprovalRequestId: secretApprovalRequest._id.toString(),
secretApprovalRequestSlug: secretApprovalRequest.slug
}
},
{
workspaceId: secretApprovalRequest.workspace
}
);
return secretApprovalRequest;
};
// validation for a merge conditions happen in another function in controller
export const performSecretApprovalRequestMerge = async (
id: string,
authData: AuthData,
userMembershipId: string
) => {
const secretApprovalRequest = await SecretApprovalRequest.findById(id)
.populate<{ commits: ISecretCommits<ISecret> }>({
path: "commits.secret",
select: "+secretBlindIndex",
populate: {
path: "tags"
}
})
.select("+commits.newVersion.secretBlindIndex");
if (!secretApprovalRequest) throw BadRequestError({ message: "Approval request not found" });
const workspaceId = secretApprovalRequest.workspace;
const environment = secretApprovalRequest.environment;
const folderId = secretApprovalRequest.folderId;
const postHogClient = await TelemetryService.getPostHogClient();
const conflicts: Array<{ secretId: string; op: CommitType }> = [];
const secretCreationCommits = secretApprovalRequest.commits.filter(
({ op }) => op === CommitType.CREATE
) as Array<{ op: CommitType.CREATE; newVersion: ISecretApprovalSecChange }>;
if (secretCreationCommits.length) {
// the created secrets already exist thus creation conflict ones
const conflictedSecrets = await Secret.find({
workspace: workspaceId,
environment,
folder: folderId,
secretBlindIndex: {
$in: secretCreationCommits.map(({ newVersion }) => newVersion.secretBlindIndex)
}
})
.select("+secretBlindIndex")
.lean();
const conflictGroupByBlindIndex = conflictedSecrets.reduce<Record<string, boolean>>(
(prev, curr) => ({ ...prev, [curr.secretBlindIndex || ""]: true }),
{}
);
const nonConflictSecrets = secretCreationCommits.filter(
({ newVersion }) => !conflictGroupByBlindIndex[newVersion.secretBlindIndex || ""]
);
secretCreationCommits
.filter(({ newVersion }) => conflictGroupByBlindIndex[newVersion.secretBlindIndex || ""])
.forEach((el) => {
conflicts.push({ op: CommitType.CREATE, secretId: el.newVersion._id.toString() });
});
// create secret
const newlyCreatedSecrets: ISecret[] = await Secret.insertMany(
nonConflictSecrets.map(
({
newVersion: {
secretKeyIV,
secretKeyTag,
secretValueIV,
secretValueTag,
secretCommentIV,
secretCommentTag,
secretKeyCiphertext,
secretValueCiphertext,
secretCommentCiphertext,
skipMultilineEncoding,
secretBlindIndex,
algorithm,
keyEncoding,
tags
}
}) => ({
version: 1,
workspace: new Types.ObjectId(workspaceId),
environment,
type: SECRET_SHARED,
secretKeyCiphertext,
secretKeyIV,
secretKeyTag,
secretValueCiphertext,
secretValueIV,
secretValueTag,
secretCommentCiphertext,
secretCommentIV,
secretCommentTag,
folder: folderId,
algorithm: algorithm || ALGORITHM_AES_256_GCM,
keyEncoding: keyEncoding || ENCODING_SCHEME_UTF8,
tags,
skipMultilineEncoding,
secretBlindIndex
})
)
);
await EESecretService.addSecretVersions({
secretVersions: newlyCreatedSecrets.map(
(secret) =>
new SecretVersion({
secret: secret._id,
version: secret.version,
workspace: secret.workspace,
type: secret.type,
folder: folderId,
tags: secret.tags,
skipMultilineEncoding: secret?.skipMultilineEncoding,
environment: secret.environment,
isDeleted: false,
secretBlindIndex: secret.secretBlindIndex,
secretKeyCiphertext: secret.secretKeyCiphertext,
secretKeyIV: secret.secretKeyIV,
secretKeyTag: secret.secretKeyTag,
secretValueCiphertext: secret.secretValueCiphertext,
secretValueIV: secret.secretValueIV,
secretValueTag: secret.secretValueTag,
algorithm: ALGORITHM_AES_256_GCM,
keyEncoding: ENCODING_SCHEME_UTF8
})
)
});
}
const secretUpdationCommits = secretApprovalRequest.commits.filter(
({ op }) => op === CommitType.UPDATE
) as Array<{
op: CommitType.UPDATE;
newVersion: Partial<Omit<ISecretApprovalSecChange, "_id">> & { _id: Types.ObjectId };
secret: ISecret;
}>;
if (secretUpdationCommits.length) {
const conflictedByNewBlindIndex = await Secret.find({
workspace: workspaceId,
environment,
folder: folderId,
secretBlindIndex: {
$in: secretUpdationCommits
.map(({ newVersion }) => newVersion?.secretBlindIndex)
.filter(Boolean)
}
})
.select("+secretBlindIndex")
.lean();
const conflictGroupByBlindIndex = conflictedByNewBlindIndex.reduce<Record<string, boolean>>(
(prev, curr) => (curr?.secretBlindIndex ? { ...prev, [curr.secretBlindIndex]: true } : prev),
{}
);
secretUpdationCommits
.filter(
({ newVersion, secret }) =>
(newVersion.secretBlindIndex && conflictGroupByBlindIndex[newVersion.secretBlindIndex]) ||
!secret
)
.forEach((el) => {
conflicts.push({ op: CommitType.UPDATE, secretId: el.newVersion._id.toString() });
});
const nonConflictSecrets = secretUpdationCommits.filter(
({ newVersion, secret }) =>
Boolean(secret) &&
(newVersion?.secretBlindIndex
? !conflictGroupByBlindIndex[newVersion.secretBlindIndex]
: true)
);
await Secret.bulkWrite(
// id and version are stripped off
nonConflictSecrets.map(
({
newVersion: {
secretKeyIV,
secretKeyTag,
secretValueIV,
secretValueTag,
secretCommentIV,
secretCommentTag,
secretKeyCiphertext,
secretValueCiphertext,
secretCommentCiphertext,
skipMultilineEncoding,
secretBlindIndex,
tags
},
secret
}) => ({
updateOne: {
filter: {
workspace: new Types.ObjectId(workspaceId),
environment,
folder: folderId,
secretBlindIndex: secret.secretBlindIndex,
type: SECRET_SHARED
},
update: {
$inc: {
version: 1
},
secretKeyIV,
secretKeyTag,
secretValueIV,
secretValueTag,
secretCommentIV,
secretCommentTag,
secretKeyCiphertext,
secretValueCiphertext,
secretCommentCiphertext,
skipMultilineEncoding,
secretBlindIndex,
tags,
algorithm: ALGORITHM_AES_256_GCM,
keyEncoding: ENCODING_SCHEME_UTF8
}
}
})
)
);
await EESecretService.addSecretVersions({
secretVersions: nonConflictSecrets.map(({ newVersion, secret }) => {
return new SecretVersion({
secret: secret._id,
version: secret.version + 1,
workspace: workspaceId,
type: SECRET_SHARED,
folder: folderId,
environment,
isDeleted: false,
secretBlindIndex: newVersion?.secretBlindIndex ?? secret.secretBlindIndex,
secretKeyCiphertext: newVersion?.secretKeyCiphertext ?? secret.secretKeyCiphertext,
secretKeyIV: newVersion?.secretKeyIV ?? secret.secretKeyCiphertext,
secretKeyTag: newVersion?.secretKeyTag ?? secret.secretKeyTag,
secretValueCiphertext: newVersion?.secretValueCiphertext ?? secret.secretValueCiphertext,
secretValueIV: newVersion?.secretValueIV ?? secret.secretValueIV,
secretValueTag: newVersion?.secretValueTag ?? secret.secretValueTag,
tags: newVersion?.tags ?? secret.tags,
algorithm: ALGORITHM_AES_256_GCM,
keyEncoding: ENCODING_SCHEME_UTF8,
skipMultilineEncoding: newVersion?.skipMultilineEncoding ?? secret.skipMultilineEncoding
});
})
});
}
const secretDeletionCommits = secretApprovalRequest.commits.filter(
({ op }) => op === CommitType.DELETE
) as Array<{
op: CommitType.DELETE;
secret: ISecret;
}>;
if (secretDeletionCommits.length) {
await Secret.deleteMany({
workspace: new Types.ObjectId(workspaceId),
folder: folderId,
environment
})
.or(
secretDeletionCommits.map(({ secret: { secretBlindIndex } }) => ({
secretBlindIndex,
type: { $in: ["shared", "personal"] }
}))
)
.exec();
await EESecretService.markDeletedSecretVersions({
secretIds: secretDeletionCommits.map(({ secret }) => secret._id)
});
}
const updatedSecretApproval = await SecretApprovalRequest.findByIdAndUpdate(
id,
{
conflicts,
hasMerged: true,
status: "close",
statusChangeBy: userMembershipId
},
{ new: true }
);
if (postHogClient) {
if (postHogClient) {
postHogClient.capture({
event: "secrets merged",
distinctId: await TelemetryService.getDistinctId({
authData
}),
properties: {
numberOfSecrets: secretApprovalRequest.commits.length,
environment,
workspaceId,
folderId,
channel: authData.userAgentType,
userAgent: authData.userAgent
}
});
}
}
await EESecretService.takeSecretSnapshot({
workspaceId,
environment,
folderId
});
// question to team where to keep secretKey
await EEAuditLogService.createAuditLog(
authData,
{
type: EventType.SECRET_APPROVAL_MERGED,
metadata: {
mergedBy: userMembershipId,
secretApprovalRequestId: id,
secretApprovalRequestSlug: secretApprovalRequest.slug
}
},
{
workspaceId
}
);
return updatedSecretApproval;
};

@ -1,54 +0,0 @@
import { z } from "zod";
export const GetSecretApprovalRuleList = z.object({
query: z.object({
workspaceId: z.string().trim()
})
});
export const GetSecretApprovalPolicyOfABoard = z.object({
query: z.object({
workspaceId: z.string().trim(),
environment: z.string().trim(),
secretPath: z.string().trim()
})
});
export const CreateSecretApprovalRule = z.object({
body: z
.object({
workspaceId: z.string(),
name: z.string().optional(),
environment: z.string(),
secretPath: z.string().optional().nullable(),
approvers: z.string().array().min(1),
approvals: z.number().min(1).default(1)
})
.refine((data) => data.approvals <= data.approvers.length, {
path: ["approvals"],
message: "The number of approvals should be lower than the number of approvers."
})
});
export const UpdateSecretApprovalRule = z.object({
params: z.object({
id: z.string()
}),
body: z
.object({
name: z.string().optional(),
approvers: z.string().array().min(1),
approvals: z.number().min(1).default(1),
secretPath: z.string().optional().nullable()
})
.refine((data) => data.approvals <= data.approvers.length, {
path: ["approvals"],
message: "The number of approvals should be lower than the number of approvers."
})
});
export const DeleteSecretApprovalRule = z.object({
params: z.object({
id: z.string()
})
});

@ -1,49 +0,0 @@
import { z } from "zod";
import { ApprovalStatus } from "../models/secretApprovalRequest";
export const getSecretApprovalRequests = z.object({
query: z.object({
workspaceId: z.string().trim(),
environment: z.string().trim().optional(),
committer: z.string().trim().optional(),
status: z.enum(["open", "close"]).optional(),
limit: z.coerce.number().default(20),
offset: z.coerce.number().default(0)
})
});
export const getSecretApprovalRequestCount = z.object({
query: z.object({
workspaceId: z.string().trim()
})
});
export const getSecretApprovalRequestDetails = z.object({
params: z.object({
id: z.string().trim()
})
});
export const updateSecretApprovalReviewStatus = z.object({
body: z.object({
status: z.enum([ApprovalStatus.APPROVED, ApprovalStatus.REJECTED])
}),
params: z.object({
id: z.string().trim()
})
});
export const mergeSecretApprovalRequest = z.object({
params: z.object({
id: z.string().trim()
})
});
export const updateSecretApprovalRequestStatus = z.object({
params: z.object({
id: z.string().trim()
}),
body: z.object({
status: z.enum(["open", "close"])
})
});

@ -1,32 +0,0 @@
import { z } from "zod";
export const createSecretRotationV1 = z.object({
body: z.object({
workspaceId: z.string().trim(),
secretPath: z.string().trim(),
environment: z.string().trim(),
interval: z.number().min(1),
provider: z.string().trim(),
customProvider: z.string().trim().optional(),
inputs: z.record(z.unknown()),
outputs: z.record(z.string())
})
});
export const restartSecretRotationV1 = z.object({
body: z.object({
id: z.string().trim()
})
});
export const getSecretRotationV1 = z.object({
query: z.object({
workspaceId: z.string().trim()
})
});
export const removeSecretRotationV1 = z.object({
params: z.object({
id: z.string().trim()
})
});

@ -1,7 +0,0 @@
import { z } from "zod";
export const getSecretRotationProvidersV1 = z.object({
params: z.object({
workspaceId: z.string()
})
});

@ -1,145 +0,0 @@
import { Types } from "mongoose";
import jwt from "jsonwebtoken";
import { ITokenVersion, TokenVersion } from "../models";
import { UnauthorizedRequestError } from "../utils/errors";
import {
getAuthSecret,
getJwtAuthLifetime,
getJwtRefreshLifetime
} from "../config";
import { AuthTokenType } from "../variables";
/**
* Return newly issued (JWT) auth and refresh tokens to user with id [userId]
* @param {Object} obj
* @param {String} obj.userId - id of user who we are issuing tokens for
* @return {Object} obj
* @return {String} obj.token - issued JWT token
* @return {String} obj.refreshToken - issued refresh token
*/
export const issueAuthTokens = async ({
userId,
ip,
userAgent,
}: {
userId: Types.ObjectId;
ip: string;
userAgent: string;
}) => {
let tokenVersion: ITokenVersion | null;
// continue with (session) token version matching existing ip and user agent
tokenVersion = await TokenVersion.findOne({
user: userId,
ip,
userAgent,
});
if (!tokenVersion) {
// case: no existing ip and user agent exists
// -> create new (session) token version for ip and user agent
tokenVersion = await new TokenVersion({
user: userId,
refreshVersion: 0,
accessVersion: 0,
ip,
userAgent,
lastUsed: new Date(),
}).save();
}
// issue tokens
const token = createToken({
payload: {
authTokenType: AuthTokenType.ACCESS_TOKEN,
userId,
tokenVersionId: tokenVersion._id.toString(),
accessVersion: tokenVersion.accessVersion,
},
expiresIn: await getJwtAuthLifetime(),
secret: await getAuthSecret(),
});
const refreshToken = createToken({
payload: {
authTokenType: AuthTokenType.REFRESH_TOKEN,
userId,
tokenVersionId: tokenVersion._id.toString(),
refreshVersion: tokenVersion.refreshVersion,
},
expiresIn: await getJwtRefreshLifetime(),
secret: await getAuthSecret(),
});
return {
token,
refreshToken,
};
};
/**
* Remove JWT and refresh tokens for user with id [userId]
* @param {Object} obj
* @param {String} obj.userId - id of user whose tokens are cleared.
*/
export const clearTokens = async (tokenVersionId: Types.ObjectId): Promise<void> => {
// increment refreshVersion on user by 1
await TokenVersion.findOneAndUpdate({
_id: tokenVersionId,
}, {
$inc: {
refreshVersion: 1,
accessVersion: 1,
},
});
};
/**
* Return a new (JWT) token for user with id [userId] that expires in [expiresIn]; can be used to, for instance, generate
* bearer/auth, refresh, and temporary signup tokens
* @param {Object} obj
* @param {Object} obj.payload - payload of (JWT) token
* @param {String} obj.secret - (JWT) secret such as [AUTH_SECRET]
* @param {String} obj.expiresIn - string describing time span such as '10h' or '7d'
*/
export const createToken = ({
payload,
expiresIn,
secret,
}: {
payload: any;
expiresIn?: string | number;
secret: string;
}) => {
return jwt.sign(payload, secret, {
...(
(expiresIn !== undefined && expiresIn !== null)
? { expiresIn }
: {}
)
});
};
export const validateProviderAuthToken = async ({
email,
providerAuthToken,
}: {
email: string;
providerAuthToken?: string;
}) => {
if (!providerAuthToken) {
throw new Error("Invalid authentication request.");
}
const decodedToken = <jwt.ProviderAuthJwtPayload>(
jwt.verify(providerAuthToken, await getAuthSecret())
);
if (decodedToken.authTokenType !== AuthTokenType.PROVIDER_TOKEN) throw UnauthorizedRequestError();
if (decodedToken.email !== email) {
throw new Error("Invalid authentication credentials.")
}
}

@ -1,385 +0,0 @@
import { Types } from "mongoose";
import {
Bot,
BotKey,
BotOrg,
Folder,
Identity,
IdentityMembership,
IdentityMembershipOrg,
IdentityUniversalAuth,
IdentityUniversalAuthClientSecret,
IncidentContactOrg,
Integration,
IntegrationAuth,
Key,
Membership,
MembershipOrg,
Organization,
Secret,
SecretBlindIndexData,
SecretImport,
ServiceToken,
ServiceTokenData,
Tag,
Webhook,
Workspace
} from "../models";
import {
AuditLog,
FolderVersion,
GitAppInstallationSession,
GitAppOrganizationInstallation,
GitRisks,
Role,
SSOConfig,
SecretApprovalPolicy,
SecretApprovalRequest,
SecretSnapshot,
SecretVersion,
TrustedIP
} from "../ee/models";
import {
ACCEPTED,
} from "../variables";
import {
EELicenseService,
} from "../ee/services";
import {
getLicenseServerKey,
getLicenseServerUrl,
} from "../config";
import {
licenseKeyRequest,
licenseServerKeyRequest,
} from "../config/request";
import {
createBotOrg
} from "./botOrg";
import { ResourceNotFoundError } from "../utils/errors";
/**
* Create an organization with name [name]
* @param {Object} obj
* @param {String} obj.name - name of organization to create.
* @param {String} obj.email - POC email that will receive invoice info
* @param {Object} organization - new organization
*/
export const createOrganization = async ({
name,
email,
}: {
name: string;
email: string;
}) => {
const licenseServerKey = await getLicenseServerKey();
let organization;
if (licenseServerKey) {
const { data: { customerId } } = await licenseServerKeyRequest.post(
`${await getLicenseServerUrl()}/api/license-server/v1/customers`,
{
email,
name
}
);
organization = await new Organization({
name,
customerId
}).save();
} else {
organization = await new Organization({
name,
}).save();
}
// initialize bot for organization
await createBotOrg({
name,
organizationId: organization._id
});
return organization;
};
/**
* Delete organization with id [organizationId]
* @param {Object} obj
* @param {Types.ObjectId} obj.organizationId - id of organization to delete
* @returns
*/
export const deleteOrganization = async ({
organizationId
}: {
organizationId: Types.ObjectId;
}) => {
const organization = await Organization.findByIdAndDelete(
organizationId
);
if (!organization) throw ResourceNotFoundError();
await MembershipOrg.deleteMany({
organization: organization._id
});
const identityIds = await IdentityMembershipOrg.distinct("identity", {
organization: organization._id
});
await IdentityMembershipOrg.deleteMany({
organization: organization._id
});
await Identity.deleteMany({
_id: {
$in: identityIds
}
});
await IdentityUniversalAuth.deleteMany({
identity: {
$in: identityIds
}
});
await IdentityUniversalAuthClientSecret.deleteMany({
identity: {
$in: identityIds
}
});
await BotOrg.deleteMany({
organization: organization._id
});
await SSOConfig.deleteMany({
organization: organization._id
});
await Role.deleteMany({
organization: organization._id
});
await IncidentContactOrg.deleteMany({
organization: organization._id
});
await GitRisks.deleteMany({
organization: organization._id
});
await GitAppInstallationSession.deleteMany({
organization: organization._id
});
await GitAppOrganizationInstallation.deleteMany({
organization: organization._id
});
const workspaceIds = await Workspace.distinct("_id", {
organization: organization._id
});
await Workspace.deleteMany({
organization: organization._id
});
await Membership.deleteMany({
workspace: {
$in: workspaceIds
}
});
await Key.deleteMany({
workspace: {
$in: workspaceIds
}
});
await Bot.deleteMany({
workspace: {
$in: workspaceIds
}
});
await BotKey.deleteMany({
workspace: {
$in: workspaceIds
}
});
await SecretBlindIndexData.deleteMany({
workspace: {
$in: workspaceIds
}
});
await Secret.deleteMany({
workspace: {
$in: workspaceIds
}
});
await SecretVersion.deleteMany({
workspace: {
$in: workspaceIds
}
});
await SecretSnapshot.deleteMany({
workspace: {
$in: workspaceIds
}
});
await SecretImport.deleteMany({
workspace: {
$in: workspaceIds
}
});
await Folder.deleteMany({
workspace: {
$in: workspaceIds
}
});
await FolderVersion.deleteMany({
workspace: {
$in: workspaceIds
}
});
await Webhook.deleteMany({
workspace: {
$in: workspaceIds
}
});
await TrustedIP.deleteMany({
workspace: {
$in: workspaceIds
}
});
await Tag.deleteMany({
workspace: {
$in: workspaceIds
}
});
await IntegrationAuth.deleteMany({
workspace: {
$in: workspaceIds
}
});
await Integration.deleteMany({
workspace: {
$in: workspaceIds
}
});
await ServiceToken.deleteMany({
workspace: {
$in: workspaceIds
}
});
await ServiceTokenData.deleteMany({
workspace: {
$in: workspaceIds
}
});
await IdentityMembership.deleteMany({
workspace: {
$in: workspaceIds
}
});
await AuditLog.deleteMany({
workspace: {
$in: workspaceIds
}
});
await SecretApprovalPolicy.deleteMany({
workspace: {
$in: workspaceIds
}
});
await SecretApprovalRequest.deleteMany({
workspace: {
$in: workspaceIds
}
});
if (organization.customerId) {
// delete from stripe here
await licenseServerKeyRequest.delete(
`${await getLicenseServerUrl()}/api/license-server/v1/customers/${organization.customerId}`
);
}
return organization;
}
/**
* Update organization subscription quantity to reflect number of members in
* the organization.
* @param {Object} obj
* @param {Number} obj.organizationId - id of subscription's organization
*/
export const updateSubscriptionOrgQuantity = async ({
organizationId,
}: {
organizationId: string;
}) => {
// find organization
const organization = await Organization.findOne({
_id: organizationId,
});
if (organization && organization.customerId) {
if (EELicenseService.instanceType === "cloud") {
// instance of Infisical is a cloud instance
const quantity = await MembershipOrg.countDocuments({
organization: new Types.ObjectId(organizationId),
status: ACCEPTED,
});
await licenseServerKeyRequest.patch(
`${await getLicenseServerUrl()}/api/license-server/v1/customers/${organization.customerId}/cloud-plan`,
{
quantity,
}
);
EELicenseService.localFeatureSet.del(organizationId);
}
}
if (EELicenseService.instanceType === "enterprise-self-hosted") {
// instance of Infisical is an enterprise self-hosted instance
const usedSeats = await MembershipOrg.countDocuments({
status: ACCEPTED,
});
await licenseKeyRequest.patch(
`${await getLicenseServerUrl()}/api/license/v1/license`,
{
usedSeats,
}
);
}
await EELicenseService.refreshPlan(new Types.ObjectId(organizationId));
};

@ -1,58 +0,0 @@
import { ISecret } from "../models";
import {
createRecurringSecretReminder,
deleteRecurringSecretReminder,
updateRecurringSecretReminder
} from "../queues/reminders/sendSecretReminders";
type TPartialSecret = Pick<
ISecret,
"_id" | "secretReminderRepeatDays" | "secretReminderNote" | "workspace"
>;
type TPartialSecretDeleteReminder = Pick<ISecret, "_id" | "secretReminderRepeatDays">;
export const createReminder = async (oldSecret: TPartialSecret, newSecret: TPartialSecret) => {
if (oldSecret._id !== newSecret._id) {
throw new Error("Secret id's don't match");
}
if (!newSecret.secretReminderRepeatDays) {
throw new Error("No repeat days provided");
}
const secretId = oldSecret._id.toString();
const workspaceId = oldSecret.workspace.toString();
if (oldSecret.secretReminderRepeatDays) {
// This will first delete the existing recurring job, and then create a new one.
await updateRecurringSecretReminder({
workspaceId,
secretId,
repeatDays: newSecret.secretReminderRepeatDays,
note: newSecret.secretReminderNote
});
} else {
// This will create a new recurring job.
await createRecurringSecretReminder({
workspaceId,
secretId,
repeatDays: newSecret.secretReminderRepeatDays,
note: newSecret.secretReminderNote
});
}
};
export const deleteReminder = async (secret: TPartialSecretDeleteReminder) => {
if (!secret._id) {
throw new Error("No secret id provided");
}
if (!secret.secretReminderRepeatDays) {
throw new Error("No repeat days provided");
}
await deleteRecurringSecretReminder({
secretId: secret._id.toString(),
repeatDays: secret.secretReminderRepeatDays
});
};

@ -1,301 +0,0 @@
import { Types } from "mongoose";
import {
APIKeyData,
BackupPrivateKey,
IUser,
Key,
Membership,
MembershipOrg,
TokenVersion,
User,
UserAction
} from "../models";
import { sendMail } from "./nodemailer";
import {
InternalServerError,
ResourceNotFoundError
} from "../utils/errors";
import { ADMIN } from "../variables";
import { deleteOrganization } from "../helpers/organization";
import { deleteWorkspace } from "../helpers/workspace";
/**
* Initialize a user under email [email]
* @param {Object} obj
* @param {String} obj.email - email of user to initialize
* @returns {Object} user - the initialized user
*/
export const setupAccount = async ({ email }: { email: string }) => {
const user = await new User({
email
}).save();
return user;
};
/**
* Finish setting up user
* @param {Object} obj
* @param {String} obj.userId - id of user to finish setting up
* @param {String} obj.firstName - first name of user
* @param {String} obj.lastName - last name of user
* @param {Number} obj.encryptionVersion - version of auth encryption scheme used
* @param {String} obj.protectedKey - protected key in encryption version 2
* @param {String} obj.protectedKeyIV - IV of protected key in encryption version 2
* @param {String} obj.protectedKeyTag - tag of protected key in encryption version 2
* @param {String} obj.publicKey - publickey of user
* @param {String} obj.encryptedPrivateKey - (encrypted) private key of user
* @param {String} obj.encryptedPrivateKeyIV - iv for (encrypted) private key of user
* @param {String} obj.encryptedPrivateKeyTag - tag for (encrypted) private key of user
* @param {String} obj.salt - salt for auth SRP
* @param {String} obj.verifier - verifier for auth SRP
* @returns {Object} user - the completed user
*/
export const completeAccount = async ({
userId,
firstName,
lastName,
encryptionVersion,
protectedKey,
protectedKeyIV,
protectedKeyTag,
publicKey,
encryptedPrivateKey,
encryptedPrivateKeyIV,
encryptedPrivateKeyTag,
salt,
verifier
}: {
userId: string;
firstName: string;
lastName?: string;
encryptionVersion: number;
protectedKey: string;
protectedKeyIV: string;
protectedKeyTag: string;
publicKey: string;
encryptedPrivateKey: string;
encryptedPrivateKeyIV: string;
encryptedPrivateKeyTag: string;
salt: string;
verifier: string;
}) => {
const options = {
new: true
};
const user = await User.findByIdAndUpdate(
userId,
{
firstName,
lastName,
encryptionVersion,
protectedKey,
protectedKeyIV,
protectedKeyTag,
publicKey,
encryptedPrivateKey,
iv: encryptedPrivateKeyIV,
tag: encryptedPrivateKeyTag,
salt,
verifier
},
options
);
return user;
};
/**
* Check if device with ip [ip] and user-agent [userAgent] has been seen for user [user].
* If the device is unseen, then notify the user of the new device
* @param {Object} obj
* @param {String} obj.ip - login ip address
* @param {String} obj.userAgent - login user-agent
*/
export const checkUserDevice = async ({
user,
ip,
userAgent
}: {
user: IUser;
ip: string;
userAgent: string;
}) => {
const isDeviceSeen = user.devices.some(
(device) => device.ip === ip && device.userAgent === userAgent
);
if (!isDeviceSeen) {
// case: unseen login ip detected for user
// -> notify user about the sign-in from new ip
user.devices = user.devices.concat([
{
ip: String(ip),
userAgent
}
]);
await user.save();
// send MFA code [code] to [email]
await sendMail({
template: "newDevice.handlebars",
subjectLine: "Successful login from new device",
recipients: [user.email],
substitutions: {
email: user.email,
timestamp: new Date().toString(),
ip,
userAgent
}
});
}
};
/**
* Check that if we delete user with id [userId] then
* there won't be any admin-less organizations or projects
* @param {Object} obj
* @param {String} obj.userId - id of user to check deletion conditions for
*/
const checkDeleteUserConditions = async ({
userId
}: {
userId: Types.ObjectId;
}) => {
const memberships = await Membership.find({
user: userId
});
const membershipOrgs = await MembershipOrg.find({
user: userId
});
// delete organizations where user is only member
for await (const membershipOrg of membershipOrgs) {
const orgMemberCount = await MembershipOrg.countDocuments({
organization: membershipOrg.organization,
});
const otherOrgAdminCount = await MembershipOrg.countDocuments({
organization: membershipOrg.organization,
user: { $ne: userId },
role: ADMIN
});
if (orgMemberCount > 1 && otherOrgAdminCount === 0) {
throw InternalServerError({
message: "Failed to delete account because an org would be admin-less"
});
}
}
// delete workspaces where user is only member
for await (const membership of memberships) {
const workspaceMemberCount = await Membership.countDocuments({
workspace: membership.workspace
});
const otherWorkspaceAdminCount = await Membership.countDocuments({
workspace: membership.workspace,
user: { $ne: userId },
role: ADMIN
});
if (workspaceMemberCount > 1 && otherWorkspaceAdminCount === 0) {
throw InternalServerError({
message: "Failed to delete account because a workspace would be admin-less"
});
}
}
}
/**
* Delete account with id [userId]
* @param {Object} obj
* @param {Types.ObjectId} obj.userId - id of user to delete
* @returns {User} user - deleted user
*/
export const deleteUser = async ({
userId
}: {
userId: Types.ObjectId;
}) => {
const user = await User.findByIdAndDelete(userId);
if (!user) throw ResourceNotFoundError();
await checkDeleteUserConditions({
userId: user._id
});
await UserAction.deleteMany({
user: user._id
});
await BackupPrivateKey.deleteMany({
user: user._id
});
await APIKeyData.deleteMany({
user: user._id
});
await TokenVersion.deleteMany({
user: user._id
});
await Key.deleteMany({
receiver: user._id
});
const membershipOrgs = await MembershipOrg.find({
user: userId
});
// delete organizations where user is only member
for await (const membershipOrg of membershipOrgs) {
const memberCount = await MembershipOrg.countDocuments({
organization: membershipOrg.organization
});
if (memberCount === 1) {
// organization only has 1 member (the current user)
await deleteOrganization({
organizationId: membershipOrg.organization
});
}
}
const memberships = await Membership.find({
user: userId
});
// delete workspaces where user is only member
for await (const membership of memberships) {
const memberCount = await Membership.countDocuments({
workspace: membership.workspace
});
if (memberCount === 1) {
// workspace only has 1 member (the current user) -> delete workspace
await deleteWorkspace({
workspaceId: membership.workspace
});
}
}
await MembershipOrg.deleteMany({
user: userId
});
await Membership.deleteMany({
user: userId
});
return user;
}

@ -1,198 +0,0 @@
import { Types } from "mongoose";
import {
Bot,
BotKey,
Folder,
IdentityMembership,
Integration,
IntegrationAuth,
Key,
Membership,
Secret,
SecretBlindIndexData,
SecretImport,
ServiceToken,
ServiceTokenData,
Tag,
Webhook,
Workspace
} from "../models";
import {
AuditLog,
FolderVersion,
IPType,
SecretApprovalPolicy,
SecretApprovalRequest,
SecretSnapshot,
SecretVersion,
TrustedIP
} from "../ee/models";
import { createBot } from "../helpers/bot";
import { EELicenseService } from "../ee/services";
import { SecretService } from "../services";
import {
ResourceNotFoundError
} from "../utils/errors";
/**
* Create a workspace with name [name] in organization with id [organizationId]
* and a bot for it.
* @param {String} name - name of workspace to create.
* @param {String} organizationId - id of organization to create workspace in
* @param {Object} workspace - new workspace
*/
export const createWorkspace = async ({
name,
organizationId,
}: {
name: string;
organizationId: Types.ObjectId;
}) => {
// create workspace
const workspace = await new Workspace({
name,
organization: organizationId,
autoCapitalization: true,
}).save();
// initialize bot for workspace
await createBot({
name: "Infisical Bot",
workspaceId: workspace._id,
});
// initialize blind index salt for workspace
await SecretService.createSecretBlindIndexData({
workspaceId: workspace._id,
});
// initialize default trusted IPv4 CIDR - 0.0.0.0/0
await new TrustedIP({
workspace: workspace._id,
ipAddress: "0.0.0.0",
type: IPType.IPV4,
prefix: 0,
isActive: true,
comment: ""
}).save()
// initialize default trusted IPv6 CIDR - ::/0
await new TrustedIP({
workspace: workspace._id,
ipAddress: "::",
type: IPType.IPV6,
prefix: 0,
isActive: true,
comment: ""
});
await EELicenseService.refreshPlan(organizationId);
return workspace;
};
/**
* Delete workspace and all associated materials including memberships,
* secrets, keys, etc.
* @param {Object} obj
* @param {String} obj.id - id of workspace to delete
*/
export const deleteWorkspace = async ({
workspaceId
}: {
workspaceId: Types.ObjectId;
}) => {
const workspace = await Workspace.findByIdAndDelete(workspaceId);
if (!workspace) throw ResourceNotFoundError();
await Membership.deleteMany({
workspace: workspace._id
});
await Key.deleteMany({
workspace: workspace._id
});
await Bot.deleteMany({
workspace: workspace._id
});
await BotKey.deleteMany({
workspace: workspace._id
});
await SecretBlindIndexData.deleteMany({
workspace: workspace._id
});
await Secret.deleteMany({
workspace: workspace._id
});
await SecretVersion.deleteMany({
workspace: workspace._id
});
await SecretSnapshot.deleteMany({
workspace: workspace._id
});
await SecretImport.deleteMany({
workspace: workspace._id
});
await Folder.deleteMany({
workspace: workspace._id
});
await FolderVersion.deleteMany({
workspace: workspace._id
});
await Webhook.deleteMany({
workspace: workspace._id
});
await TrustedIP.deleteMany({
workspace: workspace._id
});
await Tag.deleteMany({
workspace: workspace._id
});
await IntegrationAuth.deleteMany({
workspace: workspace._id
});
await Integration.deleteMany({
workspace: workspace._id
});
await ServiceToken.deleteMany({
workspace: workspace._id
});
await ServiceTokenData.deleteMany({
workspace: workspace._id
});
await IdentityMembership.deleteMany({
workspace: workspace._id
});
await AuditLog.deleteMany({
workspace: workspace._id
});
await SecretApprovalPolicy.deleteMany({
workspace: workspace._id
});
await SecretApprovalRequest.deleteMany({
workspace: workspace._id
});
return workspace;
};

@ -1,27 +0,0 @@
import { Types } from "mongoose";
import { IIdentity, IServiceTokenData, IUser } from "../../models";
import { IdentityActor, ServiceActor, UserActor, UserAgentType } from "../../ee/models";
interface BaseAuthData {
ipAddress: string;
userAgent: string;
userAgentType: UserAgentType;
tokenVersionId?: Types.ObjectId;
}
export interface UserAuthData extends BaseAuthData {
actor: UserActor;
authPayload: IUser;
}
export interface IdentityAuthData extends BaseAuthData {
actor: IdentityActor;
authPayload: IIdentity;
}
export interface ServiceTokenAuthData extends BaseAuthData {
actor: ServiceActor;
authPayload: IServiceTokenData;
}
export type AuthData = UserAuthData | IdentityAuthData | ServiceTokenAuthData;

@ -1,27 +0,0 @@
import requireAuth from "./requireAuth";
import requireMfaAuth from "./requireMfaAuth";
import requireSignupAuth from "./requireSignupAuth";
import requireWorkspaceAuth from "./requireWorkspaceAuth";
import requireServiceTokenAuth from "./requireServiceTokenAuth";
import requireSecretAuth from "./requireSecretAuth";
import requireSecretsAuth from "./requireSecretsAuth";
import requireBlindIndicesEnabled from "./requireBlindIndicesEnabled";
import requireE2EEOff from "./requireE2EEOff";
import { requireSuperAdminAccess } from "./requireSuperAdminAccess";
import validateRequest from "./validateRequest";
import { disableSignUpByServerCfg } from "./serverAdmin";
export {
requireAuth,
requireMfaAuth,
requireSignupAuth,
requireWorkspaceAuth,
requireServiceTokenAuth,
requireSecretAuth,
requireSecretsAuth,
requireBlindIndicesEnabled,
requireE2EEOff,
validateRequest,
requireSuperAdminAccess,
disableSignUpByServerCfg
};

@ -1,47 +0,0 @@
import * as Sentry from "@sentry/node";
import { ErrorRequestHandler } from "express";
import { TokenExpiredError } from "jsonwebtoken";
import { InternalServerError, UnauthorizedRequestError } from "../utils/errors";
import { logger } from "../utils/logging";
import RequestError, { mapToPinoLogLevel } from "../utils/requestError";
import { ForbiddenError } from "@casl/ability";
export const requestErrorHandler: ErrorRequestHandler = async (
err: RequestError | Error,
req,
res,
next
) => {
if (res.headersSent) return next();
let error: RequestError;
switch (true) {
case err instanceof TokenExpiredError:
error = UnauthorizedRequestError({ stack: err.stack, message: "Token expired" });
break;
case err instanceof ForbiddenError:
error = UnauthorizedRequestError({ context: { exception: err.message }, stack: err.stack })
break;
case err instanceof RequestError:
error = err as RequestError;
break;
default:
error = InternalServerError({ context: { exception: err.message }, stack: err.stack });
break;
}
logger[mapToPinoLogLevel(error.level)]({ msg: error });
if (req.user) {
Sentry.setUser({ email: (req.user as any).email });
}
Sentry.captureException(error);
res.status((<RequestError>error).statusCode).send(
await error.format(req)
);
next();
};

@ -1,8 +0,0 @@
import { NextFunction, Request, Response } from "express";
import { UnauthorizedRequestError } from "../utils/errors";
export const requireSuperAdminAccess = (req: Request, _res: Response, next: NextFunction) => {
const isSuperAdmin = req.user.superAdmin;
if (!isSuperAdmin) throw UnauthorizedRequestError({ message: "Requires superadmin access" });
return next();
};

@ -1,9 +0,0 @@
import { NextFunction, Request, Response } from "express";
import { getServerConfig } from "../config/serverConfig";
import { BadRequestError } from "../utils/errors";
export const disableSignUpByServerCfg = (_req: Request, _res: Response, next: NextFunction) => {
const cfg = getServerConfig();
if (!cfg.allowSignUp) throw BadRequestError({ message: "Signup are disabled" });
return next();
};

@ -1,38 +0,0 @@
import { Document, Schema, Types, model } from "mongoose";
export interface IAPIKeyDataV2 extends Document {
_id: Types.ObjectId;
name: string;
user: Types.ObjectId;
lastUsed?: Date
usageCount: number;
expiresAt?: Date;
}
const apiKeyDataV2Schema = new Schema(
{
name: {
type: String,
required: true
},
user: {
type: Schema.Types.ObjectId,
ref: "User",
required: true
},
lastUsed: {
type: Date,
required: false
},
usageCount: {
type: Number,
default: 0,
required: true
}
},
{
timestamps: true
}
);
export const APIKeyDataV2 = model<IAPIKeyDataV2>("APIKeyDataV2", apiKeyDataV2Schema);

@ -1,38 +0,0 @@
import { Document, Schema, Types, model } from "mongoose";
import { IPType } from "../ee/models";
export interface IIdentityTrustedIp {
ipAddress: string;
type: IPType;
prefix: number;
}
export enum IdentityAuthMethod {
UNIVERSAL_AUTH = "universal-auth"
}
export interface IIdentity extends Document {
_id: Types.ObjectId;
name: string;
authMethod?: IdentityAuthMethod;
}
const identitySchema = new Schema(
{
name: {
type: String,
required: true
},
authMethod: {
type: String,
enum: IdentityAuthMethod,
required: false,
},
},
{
timestamps: true
}
);
export const Identity = model<IIdentity>("Identity", identitySchema);

@ -1,104 +0,0 @@
import { Document, Schema, Types, model } from "mongoose";
import { IIdentityTrustedIp } from "./identity";
import { IPType } from "../ee/models/trustedIp";
export interface IIdentityAccessToken extends Document {
_id: Types.ObjectId;
identity: Types.ObjectId;
identityUniversalAuthClientSecret?: Types.ObjectId;
accessTokenLastUsedAt?: Date;
accessTokenLastRenewedAt?: Date;
accessTokenNumUses: number;
accessTokenNumUsesLimit: number;
accessTokenTTL: number;
accessTokenMaxTTL: number;
accessTokenTrustedIps: Array<IIdentityTrustedIp>;
isAccessTokenRevoked: boolean;
updatedAt: Date;
createdAt: Date;
}
const identityAccessTokenSchema = new Schema(
{
identity: {
type: Schema.Types.ObjectId,
ref: "Identity",
required: false
},
identityUniversalAuthClientSecret: {
type: Schema.Types.ObjectId,
ref: "IdentityUniversalAuthClientSecret",
required: false
},
accessTokenLastUsedAt: {
type: Date,
required: false
},
accessTokenLastRenewedAt: {
type: Date,
required: false
},
accessTokenNumUses: {
// number of times access token has been used
type: Number,
default: 0,
required: true
},
accessTokenNumUsesLimit: {
// number of times access token can be used for
type: Number,
default: 0, // default: used as many times as needed
required: true
},
accessTokenTTL: { // seconds
// incremental lifetime
type: Number,
default: 2592000, // 30 days
required: true
},
accessTokenMaxTTL: { // seconds
// max lifetime
type: Number,
default: 2592000, // 30 days
required: true
},
accessTokenTrustedIps: {
type: [
{
ipAddress: {
type: String,
required: true
},
type: {
type: String,
enum: [
IPType.IPV4,
IPType.IPV6
],
required: true
},
prefix: {
type: Number,
required: false
}
}
],
default: [{
ipAddress: "0.0.0.0",
type: IPType.IPV4.toString(),
prefix: 0
}],
required: true
},
isAccessTokenRevoked: {
type: Boolean,
default: false,
required: true
},
},
{
timestamps: true
}
);
export const IdentityAccessToken = model<IIdentityAccessToken>("IdentityAccessToken", identityAccessTokenSchema);

@ -1,39 +0,0 @@
import { Schema, Types, model } from "mongoose";
import { ADMIN, CUSTOM, MEMBER, NO_ACCESS, VIEWER } from "../variables";
export interface IIdentityMembership {
_id: Types.ObjectId;
identity: Types.ObjectId;
workspace: Types.ObjectId;
role: "admin" | "member" | "viewer" | "no-access" | "custom";
customRole: Types.ObjectId;
}
const identityMembershipSchema = new Schema<IIdentityMembership>(
{
identity: {
type: Schema.Types.ObjectId,
ref: "Identity"
},
workspace: {
type: Schema.Types.ObjectId,
ref: "Workspace",
required: true,
index: true,
},
role: {
type: String,
enum: [ADMIN, MEMBER, VIEWER, CUSTOM, NO_ACCESS],
required: true
},
customRole: {
type: Schema.Types.ObjectId,
ref: "Role"
}
},
{
timestamps: true
}
);
export const IdentityMembership = model<IIdentityMembership>("IdentityMembership", identityMembershipSchema);

@ -1,37 +0,0 @@
import { Schema, Types, model } from "mongoose";
import { ADMIN, CUSTOM, MEMBER, NO_ACCESS} from "../variables";
export interface IIdentityMembershipOrg {
_id: Types.ObjectId;
identity: Types.ObjectId;
organization: Types.ObjectId;
role: "admin" | "member" | "no-access" | "custom";
customRole: Types.ObjectId;
}
const identityMembershipOrgSchema = new Schema<IIdentityMembershipOrg>(
{
identity: {
type: Schema.Types.ObjectId,
ref: "Identity"
},
organization: {
type: Schema.Types.ObjectId,
ref: "Organization"
},
role: {
type: String,
enum: [ADMIN, MEMBER, NO_ACCESS, CUSTOM],
required: true
},
customRole: {
type: Schema.Types.ObjectId,
ref: "Role"
}
},
{
timestamps: true
}
);
export const IdentityMembershipOrg = model<IIdentityMembershipOrg>("IdentityMembershipOrg", identityMembershipOrgSchema);

@ -1,107 +0,0 @@
import { Document, Schema, Types, model } from "mongoose";
import { IPType } from "../ee/models";
import { IIdentityTrustedIp } from "./identity";
export interface IIdentityUniversalAuth extends Document {
_id: Types.ObjectId;
identity: Types.ObjectId;
clientId: string;
clientSecretTrustedIps: Array<IIdentityTrustedIp>;
accessTokenTTL: number;
accessTokenMaxTTL: number;
accessTokenNumUsesLimit: number;
accessTokenTrustedIps: Array<IIdentityTrustedIp>;
}
const identityUniversalAuthSchema = new Schema(
{
identity: {
type: Schema.Types.ObjectId,
ref: "Identity",
required: true
},
clientId: {
type: String,
required: true
},
clientSecretTrustedIps: {
type: [
{
ipAddress: {
type: String,
required: true
},
type: {
type: String,
enum: [
IPType.IPV4,
IPType.IPV6
],
required: true
},
prefix: {
type: Number,
required: false
}
}
],
default: [{
ipAddress: "0.0.0.0",
type: IPType.IPV4.toString(),
prefix: 0
}],
required: true
},
accessTokenTTL: { // seconds
// incremental lifetime
type: Number,
default: 7200,
required: true
},
accessTokenMaxTTL: { // seconds
// max lifetime
type: Number,
default: 7200,
required: true
},
accessTokenNumUsesLimit: {
// number of times access token can be used for
type: Number,
default: 0, // default: used as many times as needed
required: true
},
accessTokenTrustedIps: {
type: [
{
ipAddress: {
type: String,
required: true
},
type: {
type: String,
enum: [
IPType.IPV4,
IPType.IPV6
],
required: true
},
prefix: {
type: Number,
required: false
}
}
],
default: [{
ipAddress: "0.0.0.0",
type: IPType.IPV4.toString(),
prefix: 0
}],
required: true
}
},
{
timestamps: true
}
);
export const IdentityUniversalAuth = model<IIdentityUniversalAuth>("IdentityUniversalAuth", identityUniversalAuthSchema);

@ -1,81 +0,0 @@
import { Document, Schema, Types, model } from "mongoose";
export interface IIdentityUniversalAuthClientSecret extends Document {
_id: Types.ObjectId;
identity: Types.ObjectId;
identityUniversalAuth : Types.ObjectId;
description: string;
clientSecretPrefix: string;
clientSecretHash: string;
clientSecretLastUsedAt?: Date;
clientSecretNumUses: number;
clientSecretNumUsesLimit: number;
clientSecretTTL: number;
updatedAt: Date;
createdAt: Date;
isClientSecretRevoked: boolean;
}
const identityUniversalAuthClientSecretSchema = new Schema(
{
identity: {
type: Schema.Types.ObjectId,
ref: "Identity",
required: true
},
identityUniversalAuth: {
type: Schema.Types.ObjectId,
ref: "IdentityUniversalAuth",
required: true
},
description: {
type: String,
required: true
},
clientSecretPrefix: {
type: String,
required: true
},
clientSecretHash: {
type: String,
required: true
},
clientSecretLastUsedAt: {
type: Date,
required: false
},
clientSecretNumUses: {
// number of times client secret has been used
// in login operation
type: Number,
default: 0,
required: true
},
clientSecretNumUsesLimit: {
// number of times client secret can be used for
// a login operation
type: Number,
default: 0, // default: used as many times as needed
required: true
},
clientSecretTTL: {
type: Number,
default: 0, // default: does not expire
required: true
},
isClientSecretRevoked: {
type: Boolean,
default: false,
required: true
}
},
{
timestamps: true
}
);
identityUniversalAuthClientSecretSchema.index(
{ identityUniversalAuth: 1, isClientSecretRevoked: 1 }
);
export const IdentityUniversalAuthClientSecret = model<IIdentityUniversalAuthClientSecret>("IdentityUniversalAuthClientSecret", identityUniversalAuthClientSecretSchema);

@ -1,206 +0,0 @@
import {
ALGORITHM_AES_256_GCM,
ENCODING_SCHEME_BASE64,
ENCODING_SCHEME_UTF8,
INTEGRATION_AWS_PARAMETER_STORE,
INTEGRATION_AWS_SECRET_MANAGER,
INTEGRATION_AZURE_KEY_VAULT,
INTEGRATION_BITBUCKET,
INTEGRATION_CIRCLECI,
INTEGRATION_CLOUDFLARE_PAGES,
INTEGRATION_CLOUDFLARE_WORKERS,
INTEGRATION_CLOUD_66,
INTEGRATION_CODEFRESH,
INTEGRATION_DIGITAL_OCEAN_APP_PLATFORM,
INTEGRATION_FLYIO,
INTEGRATION_GCP_SECRET_MANAGER,
INTEGRATION_GITHUB,
INTEGRATION_GITLAB,
INTEGRATION_HASHICORP_VAULT,
INTEGRATION_HASURA_CLOUD,
INTEGRATION_HEROKU,
INTEGRATION_LARAVELFORGE,
INTEGRATION_NETLIFY,
INTEGRATION_NORTHFLANK,
INTEGRATION_RAILWAY,
INTEGRATION_RENDER,
INTEGRATION_SUPABASE,
INTEGRATION_TEAMCITY,
INTEGRATION_TERRAFORM_CLOUD,
INTEGRATION_TRAVISCI,
INTEGRATION_VERCEL,
INTEGRATION_WINDMILL
} from "../../variables";
import { Document, Schema, Types, model } from "mongoose";
import { IntegrationAuthMetadata } from "./types";
export interface IIntegrationAuth extends Document {
_id: Types.ObjectId;
workspace: Types.ObjectId;
integration:
| "heroku"
| "vercel"
| "netlify"
| "github"
| "gitlab"
| "render"
| "railway"
| "flyio"
| "azure-key-vault"
| "laravel-forge"
| "circleci"
| "travisci"
| "supabase"
| "aws-parameter-store"
| "aws-secret-manager"
| "checkly"
| "qovery"
| "cloudflare-pages"
| "cloudflare-workers"
| "codefresh"
| "digital-ocean-app-platform"
| "bitbucket"
| "cloud-66"
| "terraform-cloud"
| "teamcity"
| "northflank"
| "windmill"
| "gcp-secret-manager"
| "hasura-cloud";
teamId: string;
accountId: string;
url: string;
namespace: string;
refreshCiphertext?: string;
refreshIV?: string;
refreshTag?: string;
accessIdCiphertext?: string;
accessIdIV?: string;
accessIdTag?: string;
accessCiphertext?: string;
accessIV?: string;
accessTag?: string;
algorithm?: "aes-256-gcm";
keyEncoding?: "utf8" | "base64";
accessExpiresAt?: Date;
metadata?: IntegrationAuthMetadata;
}
const integrationAuthSchema = new Schema<IIntegrationAuth>(
{
workspace: {
type: Schema.Types.ObjectId,
ref: "Workspace",
required: true
},
integration: {
type: String,
enum: [
INTEGRATION_AZURE_KEY_VAULT,
INTEGRATION_AWS_PARAMETER_STORE,
INTEGRATION_AWS_SECRET_MANAGER,
INTEGRATION_HEROKU,
INTEGRATION_VERCEL,
INTEGRATION_NETLIFY,
INTEGRATION_GITHUB,
INTEGRATION_GITLAB,
INTEGRATION_RENDER,
INTEGRATION_RAILWAY,
INTEGRATION_FLYIO,
INTEGRATION_CIRCLECI,
INTEGRATION_LARAVELFORGE,
INTEGRATION_TRAVISCI,
INTEGRATION_TEAMCITY,
INTEGRATION_SUPABASE,
INTEGRATION_TERRAFORM_CLOUD,
INTEGRATION_HASHICORP_VAULT,
INTEGRATION_CLOUDFLARE_PAGES,
INTEGRATION_CLOUDFLARE_WORKERS,
INTEGRATION_CODEFRESH,
INTEGRATION_WINDMILL,
INTEGRATION_BITBUCKET,
INTEGRATION_DIGITAL_OCEAN_APP_PLATFORM,
INTEGRATION_CLOUD_66,
INTEGRATION_NORTHFLANK,
INTEGRATION_GCP_SECRET_MANAGER,
INTEGRATION_HASURA_CLOUD
],
required: true
},
teamId: {
// vercel-specific integration param
type: String
},
url: {
// for any self-hosted integrations (e.g. self-hosted hashicorp-vault)
type: String
},
namespace: {
// hashicorp-vault-specific integration param
type: String
},
accountId: {
// netlify-specific integration param
type: String
},
refreshCiphertext: {
type: String,
select: false
},
refreshIV: {
type: String,
select: false
},
refreshTag: {
type: String,
select: false
},
accessIdCiphertext: {
type: String,
select: false
},
accessIdIV: {
type: String,
select: false
},
accessIdTag: {
type: String,
select: false
},
accessCiphertext: {
type: String,
select: false
},
accessIV: {
type: String,
select: false
},
accessTag: {
type: String,
select: false
},
accessExpiresAt: {
type: Date,
select: false
},
algorithm: {
// the encryption algorithm used
type: String,
enum: [ALGORITHM_AES_256_GCM],
required: true
},
keyEncoding: {
type: String,
enum: [ENCODING_SCHEME_UTF8, ENCODING_SCHEME_BASE64],
required: true
},
metadata: {
type: Schema.Types.Mixed
}
},
{
timestamps: true
}
);
export const IntegrationAuth = model<IIntegrationAuth>("IntegrationAuth", integrationAuthSchema);

@ -1,25 +0,0 @@
import { Schema, Types, model } from "mongoose";
export interface IServerConfig {
_id: Types.ObjectId;
initialized: boolean;
allowSignUp: boolean;
}
const serverConfigSchema = new Schema<IServerConfig>(
{
initialized: {
type: Boolean,
default: false
},
allowSignUp: {
type: Boolean,
default: true
}
},
{
timestamps: true
}
);
export const ServerConfig = model<IServerConfig>("ServerConfig", serverConfigSchema);

@ -1,141 +0,0 @@
import { Document, Schema, Types, model } from "mongoose";
export enum AuthMethod {
EMAIL = "email",
GOOGLE = "google",
GITHUB = "github",
GITLAB = "gitlab",
OKTA_SAML = "okta-saml",
AZURE_SAML = "azure-saml",
JUMPCLOUD_SAML = "jumpcloud-saml"
}
export interface IUser extends Document {
_id: Types.ObjectId;
authProvider?: AuthMethod;
authMethods: AuthMethod[];
email: string;
superAdmin?: boolean;
firstName?: string;
lastName?: string;
encryptionVersion: number;
protectedKey: string;
protectedKeyIV: string;
protectedKeyTag: string;
publicKey?: string;
encryptedPrivateKey?: string;
iv?: string;
tag?: string;
salt?: string;
verifier?: string;
isMfaEnabled: boolean;
mfaMethods: boolean;
devices: {
ip: string;
userAgent: string;
}[];
}
const userSchema = new Schema<IUser>(
{
authProvider: {
// TODO field: deprecate
type: String,
enum: AuthMethod
},
authMethods: {
type: [
{
type: String,
enum: AuthMethod
}
],
default: [AuthMethod.EMAIL],
required: true
},
email: {
type: String,
required: true,
unique: true
},
firstName: {
type: String
},
lastName: {
type: String
},
encryptionVersion: {
type: Number,
select: false,
default: 1 // to resolve backward-compatibility issues
},
protectedKey: {
// introduced as part of encryption version 2
type: String,
select: false
},
protectedKeyIV: {
// introduced as part of encryption version 2
type: String,
select: false
},
protectedKeyTag: {
// introduced as part of encryption version 2
type: String,
select: false
},
publicKey: {
type: String,
select: false
},
encryptedPrivateKey: {
type: String,
select: false
},
superAdmin: {
type: Boolean
},
iv: {
// iv of [encryptedPrivateKey]
type: String,
select: false
},
tag: {
// tag of [encryptedPrivateKey]
type: String,
select: false
},
salt: {
type: String,
select: false
},
verifier: {
type: String,
select: false
},
isMfaEnabled: {
type: Boolean,
default: false
},
mfaMethods: [
{
type: String
}
],
devices: {
type: [
{
ip: String,
userAgent: String
}
],
default: [],
select: false
}
},
{
timestamps: true
}
);
export const User = model<IUser>("User", userSchema);

@ -1,83 +0,0 @@
import Queue, { Job } from "bull";
import { IUser, Membership, Organization, Workspace } from "../../models";
import { Types } from "mongoose";
import { sendMail } from "../../helpers";
type TSendSecretReminders = {
workspaceId: string;
secretId: string;
repeatDays: number;
note: string | undefined | null;
};
type TDeleteSecretReminder = {
secretId: string;
repeatDays: number;
};
const DAY_IN_MS = 86400000;
export const sendSecretReminders = new Queue(
"send-secret-reminders",
process.env.REDIS_URL as string
);
sendSecretReminders.process(async (job: Job<TSendSecretReminders>) => {
const { workspaceId }: TSendSecretReminders = job.data;
const workspace = await Workspace.findById(new Types.ObjectId(workspaceId));
const organization = await Organization.findById(new Types.ObjectId(workspace?.organization));
if (!workspace) {
throw new Error("Workspace for reminder not found");
}
if (!organization) {
throw new Error("Organization for reminder not found");
}
const memberships = await Membership.find({
workspace: workspaceId
}).populate<{ user: IUser }>("user");
await sendMail({
template: "secretReminder.handlebars",
subjectLine: "Infisical secret reminder",
recipients: [...memberships.map((membership) => membership.user.email)],
substitutions: {
reminderNote: job.data.note, // May not be present.
workspaceName: workspace.name,
organizationName: organization.name
}
});
});
export const createRecurringSecretReminder = (jobDetails: TSendSecretReminders) => {
const repeat = jobDetails.repeatDays * DAY_IN_MS;
return sendSecretReminders.add(jobDetails, {
delay: repeat,
repeat: {
every: repeat
},
jobId: `reminder-${jobDetails.secretId}`,
removeOnComplete: true,
removeOnFail: {
count: 20
}
});
};
export const deleteRecurringSecretReminder = (jobDetails: TDeleteSecretReminder) => {
const repeat = jobDetails.repeatDays * DAY_IN_MS;
return sendSecretReminders.removeRepeatable({
every: repeat,
jobId: `reminder-${jobDetails.secretId}`
});
};
export const updateRecurringSecretReminder = async (jobDetails: TSendSecretReminders) => {
// We need to delete the potentially existing reminder job first, or the new one won't be created.
await deleteRecurringSecretReminder(jobDetails);
await createRecurringSecretReminder(jobDetails);
};

@ -1,20 +0,0 @@
import express from "express";
import { adminController } from "../../controllers/v1";
const router = express.Router();
import { requireAuth, requireSuperAdminAccess } from "../../middleware";
import { AuthMode } from "../../variables";
router.get("/config", adminController.getServerConfigInfo);
router.post("/signup", adminController.adminSignUp);
router.patch(
"/config",
requireAuth({
acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY]
}),
requireSuperAdminAccess,
adminController.updateServerConfig
);
export default router;

@ -1,72 +0,0 @@
import express from "express";
const router = express.Router();
import passport from "passport";
import { authLimiter } from "../../helpers/rateLimiter";
import { ssoController } from "../../ee/controllers/v1";
router.get("/redirect/google", authLimiter, (req, res, next) => {
passport.authenticate("google", {
scope: ["profile", "email"],
session: false,
...(req.query.callback_port
? {
state: req.query.callback_port as string
}
: {})
})(req, res, next);
});
router.get(
"/google",
passport.authenticate("google", {
failureRedirect: "/login/provider/error",
session: false
}),
ssoController.redirectSSO
);
router.get("/redirect/github", authLimiter, (req, res, next) => {
passport.authenticate("github", {
session: false,
...(req.query.callback_port
? {
state: req.query.callback_port as string
}
: {})
})(req, res, next);
});
router.get(
"/github",
authLimiter,
passport.authenticate("github", {
failureRedirect: "/login/provider/error",
session: false
}),
ssoController.redirectSSO
);
router.get(
"/redirect/gitlab",
authLimiter,
(req, res, next) => {
passport.authenticate("gitlab", {
session: false,
...(req.query.callback_port ? {
state: req.query.callback_port as string
} : {})
})(req, res, next);
}
);
router.get(
"/gitlab",
authLimiter,
passport.authenticate("gitlab", {
failureRedirect: "/login/provider/error",
session: false
}),
ssoController.redirectSSO
);
export default router;

@ -1,66 +0,0 @@
import express from "express";
const router = express.Router();
import { requireAuth } from "../../middleware";
import { universalAuthController } from "../../controllers/v1";
import { AuthMode } from "../../variables";
router.post(
"/token/renew",
universalAuthController.renewAccessToken
);
router.post(
"/universal-auth/login",
universalAuthController.loginIdentityUniversalAuth
);
router.post(
"/universal-auth/identities/:identityId",
requireAuth({
acceptedAuthModes: [AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]
}),
universalAuthController.attachIdentityUniversalAuth
);
router.patch(
"/universal-auth/identities/:identityId",
requireAuth({
acceptedAuthModes: [AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]
}),
universalAuthController.updateIdentityUniversalAuth
);
router.get(
"/universal-auth/identities/:identityId",
requireAuth({
acceptedAuthModes: [AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]
}),
universalAuthController.getIdentityUniversalAuth
);
router.post(
"/universal-auth/identities/:identityId/client-secrets",
requireAuth({
acceptedAuthModes: [AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]
}),
universalAuthController.createUniversalAuthClientSecret
);
router.get(
"/universal-auth/identities/:identityId/client-secrets",
requireAuth({
acceptedAuthModes: [AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]
}),
universalAuthController.getUniversalAuthClientSecretsDetails
);
router.post(
"/universal-auth/identities/:identityId/client-secrets/:clientSecretId/revoke",
requireAuth({
acceptedAuthModes: [AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]
}),
universalAuthController.revokeUniversalAuthClientSecret
);
export default router;

@ -1,15 +0,0 @@
import express from "express";
const router = express.Router();
import { membershipController } from "../../controllers/v2";
import { requireAuth } from "../../middleware";
import { AuthMode } from "../../variables";
router.post(
"/:workspaceId/memberships",
requireAuth({
acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY]
}),
membershipController.addUserToWorkspace
);
export default router;

@ -1,51 +0,0 @@
import express from "express";
const router = express.Router();
import { body } from "express-validator";
import { disableSignUpByServerCfg, requireSignupAuth, validateRequest } from "../../middleware";
import { signupController } from "../../controllers/v2";
import { authLimiter } from "../../helpers/rateLimiter";
router.post(
"/complete-account/signup", // TODO endpoint: deprecate (moved to v3/signup/complete/account-signup),
disableSignUpByServerCfg,
authLimiter,
requireSignupAuth,
body("email").exists().isString().trim().notEmpty().isEmail(),
body("firstName").exists().isString().trim().notEmpty(),
body("lastName").exists().isString().trim().notEmpty(),
body("protectedKey").exists().isString().trim().notEmpty(),
body("protectedKeyIV").exists().isString().trim().notEmpty(),
body("protectedKeyTag").exists().isString().trim().notEmpty(),
body("publicKey").exists().isString().trim().notEmpty(),
body("encryptedPrivateKey").exists().isString().trim().notEmpty(),
body("encryptedPrivateKeyIV").exists().isString().trim().notEmpty(),
body("encryptedPrivateKeyTag").exists().isString().trim().notEmpty(),
body("salt").exists().isString().trim().notEmpty(),
body("verifier").exists().isString().trim().notEmpty(),
body("organizationName").exists().isString().trim().notEmpty(),
validateRequest,
signupController.completeAccountSignup
);
router.post(
"/complete-account/invite", // TODO: consider moving to v3/users/new/complete-account/invite
disableSignUpByServerCfg,
authLimiter,
requireSignupAuth,
body("email").exists().isString().trim().notEmpty().isEmail(),
body("firstName").exists().isString().trim().notEmpty(),
body("lastName").exists().isString().trim().notEmpty(),
body("protectedKey").exists().isString().trim().notEmpty(),
body("protectedKeyIV").exists().isString().trim().notEmpty(),
body("protectedKeyTag").exists().isString().trim().notEmpty(),
body("publicKey").exists().trim().notEmpty(),
body("encryptedPrivateKey").exists().isString().trim().notEmpty(),
body("encryptedPrivateKeyIV").exists().isString().trim().notEmpty(),
body("encryptedPrivateKeyTag").exists().isString().trim().notEmpty(),
body("salt").exists().isString().trim().notEmpty(),
body("verifier").exists().isString().trim().notEmpty(),
validateRequest,
signupController.completeAccountInvite
);
export default router;

@ -1,15 +0,0 @@
import express from "express";
const router = express.Router();
import { requireAuth } from "../../middleware";
import { AuthMode } from "../../variables";
import { usersController } from "../../controllers/v3";
router.get(
"/me/api-keys",
requireAuth({
acceptedAuthModes: [AuthMode.JWT]
}),
usersController.getMyAPIKeys
);
export default router;

@ -1,16 +0,0 @@
import Redis, { Redis as TRedis } from "ioredis";
import { logger } from "../utils/logging";
let redisClient: TRedis | null;
export const initRedis = async () => {
if (process.env.REDIS_URL) {
redisClient = new Redis(process.env.REDIS_URL as string);
} else {
logger.warn("Redis URL not set, skipping Redis initialization.");
redisClient = null;
}
}
export { redisClient };

@ -1,19 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="x-ua-compatible" content="ie=edge">
<title>Secret Reminder</title>
</head>
<body>
<h2>Infisical</h2>
<h2>You have a new secret reminder!</h2>
<p>You have a new secret reminder from workspace "{{workspaceName}}", in {{organizationName}}</p>
{{#if reminderNote}}
<p>Here's the note included with the reminder: {{reminderNote}}</p>
{{/if}}
</body>
</html>

@ -1,50 +0,0 @@
import { Types } from "mongoose";
import {
APIKeyData,
IUser,
User
} from "../../../models";
import { AccountNotFoundError, UnauthorizedRequestError } from "../../errors";
import bcrypt from "bcrypt";
interface ValidateAPIKeyParams {
authTokenValue: string;
}
export const validateAPIKey = async ({
authTokenValue
}: ValidateAPIKeyParams) => {
const [_, TOKEN_IDENTIFIER, TOKEN_SECRET] = <[string, string, string]>authTokenValue.split(".", 3);
let apiKeyData = await APIKeyData
.findById(TOKEN_IDENTIFIER, "+secretHash +expiresAt")
.populate<{ user: IUser }>("user", "+publicKey");
if (!apiKeyData) {
throw UnauthorizedRequestError();
} else if (apiKeyData?.expiresAt && new Date(apiKeyData.expiresAt) < new Date()) {
// case: API key expired
await APIKeyData.findByIdAndDelete(apiKeyData._id);
throw UnauthorizedRequestError();
}
const isMatch = await bcrypt.compare(TOKEN_SECRET, apiKeyData.secretHash);
if (!isMatch) throw UnauthorizedRequestError();
apiKeyData = await APIKeyData.findOneAndUpdate({
_id: new Types.ObjectId(TOKEN_IDENTIFIER),
}, {
lastUsed: new Date(),
}, {
new: true,
});
if (!apiKeyData) throw UnauthorizedRequestError();
const user = await User.findById(apiKeyData.user).select("+publicKey");
if (!user) throw AccountNotFoundError();
return user;
}

@ -1,39 +0,0 @@
import jwt from "jsonwebtoken";
import { APIKeyDataV2, User } from "../../../models";
import { getAuthSecret } from "../../../config";
import { AuthTokenType } from "../../../variables";
import { AccountNotFoundError, UnauthorizedRequestError } from "../../errors";
interface ValidateAPIKeyV2Params {
authTokenValue: string;
}
export const validateAPIKeyV2 = async ({
authTokenValue
}: ValidateAPIKeyV2Params) => {
const decodedToken = <jwt.UserIDJwtPayload>(
jwt.verify(authTokenValue, await getAuthSecret())
);
if (decodedToken.authTokenType !== AuthTokenType.API_KEY) throw UnauthorizedRequestError();
const apiKeyData = await APIKeyDataV2.findByIdAndUpdate(
decodedToken.apiKeyDataId,
{
lastUsed: new Date(),
$inc: { usageCount: 1 }
},
{
new: true
}
);
if (!apiKeyData) throw UnauthorizedRequestError();
const user = await User.findById(apiKeyData.user).select("+publicKey");
if (!user) throw AccountNotFoundError();
return user;
}

@ -1,104 +0,0 @@
import jwt from "jsonwebtoken";
import { IIdentity, IdentityAccessToken } from "../../../models";
import { getAuthSecret } from "../../../config";
import { AuthTokenType } from "../../../variables";
import { UnauthorizedRequestError } from "../../errors";
import { checkIPAgainstBlocklist } from "../../../utils/ip";
interface ValidateIdentityParams {
authTokenValue: string;
ipAddress: string;
}
export const validateIdentity = async ({
authTokenValue,
ipAddress
}: ValidateIdentityParams) => {
const decodedToken = <jwt.IdentityAccessTokenJwtPayload>(
jwt.verify(authTokenValue, await getAuthSecret())
);
if (decodedToken.authTokenType !== AuthTokenType.IDENTITY_ACCESS_TOKEN) throw UnauthorizedRequestError();
const identityAccessToken = await IdentityAccessToken
.findOne({
_id: decodedToken.identityAccessTokenId,
isAccessTokenRevoked: false
})
.populate<{ identity: IIdentity }>("identity");
if (!identityAccessToken || !identityAccessToken?.identity) throw UnauthorizedRequestError();
const {
accessTokenNumUsesLimit,
accessTokenNumUses,
accessTokenTTL,
accessTokenLastRenewedAt,
accessTokenMaxTTL,
createdAt: accessTokenCreatedAt
} = identityAccessToken;
checkIPAgainstBlocklist({
ipAddress,
trustedIps: identityAccessToken.accessTokenTrustedIps
});
// ttl check
if (accessTokenTTL > 0) {
const currentDate = new Date();
if (accessTokenLastRenewedAt) {
// access token has been renewed
const accessTokenRenewed = new Date(accessTokenLastRenewedAt);
const ttlInMilliseconds = accessTokenTTL * 1000;
const expirationDate = new Date(accessTokenRenewed.getTime() + ttlInMilliseconds);
if (currentDate > expirationDate) throw UnauthorizedRequestError({
message: "Failed to authenticate identity access token due to TTL expiration"
});
} else {
// access token has never been renewed
const accessTokenCreated = new Date(accessTokenCreatedAt);
const ttlInMilliseconds = accessTokenTTL * 1000;
const expirationDate = new Date(accessTokenCreated.getTime() + ttlInMilliseconds);
if (currentDate > expirationDate) throw UnauthorizedRequestError({
message: "Failed to authenticate identity access token due to TTL expiration"
});
}
}
// max ttl check
if (accessTokenMaxTTL > 0) {
const accessTokenCreated = new Date(accessTokenCreatedAt);
const ttlInMilliseconds = accessTokenMaxTTL * 1000;
const currentDate = new Date();
const expirationDate = new Date(accessTokenCreated.getTime() + ttlInMilliseconds);
if (currentDate > expirationDate) throw UnauthorizedRequestError({
message: "Failed to authenticate identity access token due to Max TTL expiration"
});
}
// num uses check
if (
accessTokenNumUsesLimit > 0
&& accessTokenNumUses === accessTokenNumUsesLimit
) {
throw UnauthorizedRequestError({
message: "Failed to authenticate MI access token due to access token number of uses limit reached"
});
}
await IdentityAccessToken.findByIdAndUpdate(
identityAccessToken._id,
{
accessTokenLastUsedAt: new Date(),
$inc: { accessTokenNumUses: 1 }
},
{
new: true
}
);
return identityAccessToken.identity;
}

@ -1,5 +0,0 @@
export * from "./apiKey";
export * from "./apiKeyV2";
export * from "./jwt";
export * from "./serviceTokenV2";
export * from "./identity";

@ -1,41 +0,0 @@
import jwt from "jsonwebtoken";
import { Types } from "mongoose";
import { TokenVersion, User } from "../../../models";
import { getAuthSecret } from "../../../config";
import { AuthTokenType } from "../../../variables";
import { AccountNotFoundError, UnauthorizedRequestError } from "../../errors";
interface ValidateJWTParams {
authTokenValue: string;
}
export const validateJWT = async ({
authTokenValue
}: ValidateJWTParams) => {
const decodedToken = <jwt.UserIDJwtPayload>(
jwt.verify(authTokenValue, await getAuthSecret())
);
if (decodedToken.authTokenType !== AuthTokenType.ACCESS_TOKEN) throw UnauthorizedRequestError();
const tokenVersion = await TokenVersion.findOneAndUpdate({
_id: new Types.ObjectId(decodedToken.tokenVersionId),
user: decodedToken.userId
}, {
lastUsed: new Date(),
});
if (!tokenVersion) throw UnauthorizedRequestError();
if (decodedToken.accessVersion !== tokenVersion.accessVersion) throw UnauthorizedRequestError();
const user = await User.findOne({
_id: new Types.ObjectId(decodedToken.userId),
}).select("+publicKey");
if (!user) throw AccountNotFoundError({ message: "Failed to find user" });
if (!user?.publicKey) throw UnauthorizedRequestError({ message: "Failed to authenticate user with partially set up account" });
return user;
}

@ -1,44 +0,0 @@
import { Types } from "mongoose";
import { ServiceTokenData } from "../../../models";
import { ResourceNotFoundError, UnauthorizedRequestError } from "../../errors";
import bcrypt from "bcrypt";
interface ValidateServiceTokenV2Params {
authTokenValue: string;
}
export const validateServiceTokenV2 = async ({
authTokenValue
}: ValidateServiceTokenV2Params) => {
const [_, TOKEN_IDENTIFIER, TOKEN_SECRET] = <[string, string, string]>authTokenValue.split(".", 3);
const serviceTokenData = await ServiceTokenData
.findById(TOKEN_IDENTIFIER, "+secretHash +expiresAt")
if (!serviceTokenData) {
throw UnauthorizedRequestError();
} else if (serviceTokenData?.expiresAt && new Date(serviceTokenData.expiresAt) < new Date()) {
// case: service token expired
await ServiceTokenData.findByIdAndDelete(serviceTokenData._id);
throw UnauthorizedRequestError({
message: "Failed to authenticate expired service token",
});
}
const isMatch = await bcrypt.compare(TOKEN_SECRET, serviceTokenData.secretHash);
if (!isMatch) throw UnauthorizedRequestError();
const serviceTokenDataToReturn = await ServiceTokenData
.findOneAndUpdate({
_id: new Types.ObjectId(TOKEN_IDENTIFIER),
}, {
lastUsed: new Date(),
}, {
new: true,
})
.select("+encryptedKey +iv +tag")
if (!serviceTokenDataToReturn) throw ResourceNotFoundError();
return serviceTokenDataToReturn;
}

@ -1,44 +0,0 @@
import { AuthData } from "../../../interfaces/middleware";
import {
Identity,
ServiceTokenData,
User
} from "../../../models";
/**
* Returns an object containing the id of the authentication data payload
* @param {AuthData} authData - authentication data object
* @returns
*/
export const getAuthDataPayloadIdObj = (authData: AuthData) => {
if (authData.authPayload instanceof User) {
return { userId: authData.authPayload._id };
}
if (authData.authPayload instanceof ServiceTokenData) {
return { serviceTokenDataId: authData.authPayload._id };
}
if (authData.authPayload instanceof Identity) {
return { serviceTokenDataId: authData.authPayload._id };
}
};
/**
* Returns an object containing the user associated with the authentication data payload
* @param {AuthData} authData - authentication data object
* @returns
*/
export const getAuthDataPayloadUserObj = (authData: AuthData) => {
if (authData.authPayload instanceof User) {
return { user: authData.authPayload._id };
}
if (authData.authPayload instanceof ServiceTokenData) {
return { user: authData.authPayload.user };
}
if (authData.authPayload instanceof Identity) {
return {};
}
}

@ -1,194 +0,0 @@
import { AuthData } from "../../../interfaces/middleware";
import jwt from "jsonwebtoken";
import { getAuthSecret } from "../../../config";
import { ActorType } from "../../../ee/models";
import { AuthMode, AuthTokenType } from "../../../variables";
import { UnauthorizedRequestError } from "../../errors";
import {
validateAPIKey,
validateAPIKeyV2,
validateIdentity,
validateJWT,
validateServiceTokenV2
} from "../authModeValidators";
import { getUserAgentType } from "../../posthog";
export * from "./authDataExtractors";
interface ExtractAuthModeParams {
headers: { [key: string]: string | string[] | undefined };
}
interface ExtractAuthModeReturn {
authMode: AuthMode;
authTokenValue: string;
}
interface GetAuthDataParams {
authMode: AuthMode;
authTokenValue: string;
ipAddress: string;
userAgent: string;
}
/**
* Returns the recognized authentication mode based on token in [headers]; accepted token types include:
* - SERVICE_TOKEN
* - API_KEY
* - JWT
* - IDENTITY_ACCESS_TOKEN (from identity)
* - API_KEY_V2
* @param {Object} params
* @param {Object.<string, (string|string[]|undefined)>} params.headers - The HTTP request headers, usually from Express's `req.headers`.
* @returns {Promise<AuthMode>} The derived authentication mode based on the headers.
* @throws {UnauthorizedError} Throws an error if no applicable authMode is found.
*/
export const extractAuthMode = async ({
headers
}: ExtractAuthModeParams): Promise<ExtractAuthModeReturn> => {
const apiKey = headers["x-api-key"] as string;
const authHeader = headers["authorization"] as string;
if (apiKey) {
return { authMode: AuthMode.API_KEY, authTokenValue: apiKey };
}
if (!authHeader)
throw UnauthorizedRequestError({
message: "Failed to authenticate unknown authentication method"
});
if (!authHeader.startsWith("Bearer "))
throw UnauthorizedRequestError({
message: "Failed to authenticate unknown authentication method"
});
const authTokenValue = authHeader.slice(7);
if (authTokenValue.startsWith("st.")) {
return { authMode: AuthMode.SERVICE_TOKEN, authTokenValue };
}
const decodedToken = <jwt.AuthnJwtPayload>jwt.verify(authTokenValue, await getAuthSecret());
switch (decodedToken.authTokenType) {
case AuthTokenType.ACCESS_TOKEN:
return { authMode: AuthMode.JWT, authTokenValue };
case AuthTokenType.API_KEY:
return { authMode: AuthMode.API_KEY_V2, authTokenValue };
case AuthTokenType.IDENTITY_ACCESS_TOKEN:
return { authMode: AuthMode.IDENTITY_ACCESS_TOKEN, authTokenValue };
default:
throw UnauthorizedRequestError({
message: "Failed to authenticate unknown authentication method"
});
}
};
export const getAuthData = async ({
authMode,
authTokenValue,
ipAddress,
userAgent
}: GetAuthDataParams): Promise<AuthData> => {
const userAgentType = getUserAgentType(userAgent);
switch (authMode) {
case AuthMode.SERVICE_TOKEN: {
const serviceTokenData = await validateServiceTokenV2({
authTokenValue
});
return {
actor: {
type: ActorType.SERVICE,
metadata: {
serviceId: serviceTokenData._id.toString(),
name: serviceTokenData.name
}
},
authPayload: serviceTokenData,
ipAddress,
userAgent,
userAgentType
};
}
case AuthMode.IDENTITY_ACCESS_TOKEN: {
const identity = await validateIdentity({
authTokenValue,
ipAddress
});
return {
actor: {
type: ActorType.IDENTITY,
metadata: {
identityId: identity._id.toString(),
name: identity.name
}
},
authPayload: identity,
ipAddress,
userAgent,
userAgentType
};
}
case AuthMode.API_KEY: {
const user = await validateAPIKey({
authTokenValue
});
return {
actor: {
type: ActorType.USER,
metadata: {
userId: user._id.toString(),
email: user.email
}
},
authPayload: user,
ipAddress,
userAgent,
userAgentType
};
}
case AuthMode.API_KEY_V2: {
const user = await validateAPIKeyV2({
authTokenValue
});
return {
actor: {
type: ActorType.USER,
metadata: {
userId: user._id.toString(),
email: user.email
}
},
authPayload: user,
ipAddress,
userAgent,
userAgentType
};
}
case AuthMode.JWT: {
const user = await validateJWT({
authTokenValue
});
return {
actor: {
type: ActorType.USER,
metadata: {
userId: user._id.toString(),
email: user.email
}
},
authPayload: user,
ipAddress,
userAgent,
userAgentType
};
}
}
};

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