Compare commits

..

1 Commits

Author SHA1 Message Date
066bcdcf98 Scaffolding LDAP MVP 2024-01-13 21:21:40 +07:00
1171 changed files with 133337 additions and 68346 deletions

87
.github/values.yaml vendored
View File

@ -1,52 +1,49 @@
## @section Common parameters
##
## @param nameOverride Override release name
##
nameOverride: ""
## @param fullnameOverride Override release fullname
##
fullnameOverride: ""
## @section Infisical backend parameters
## Documentation : https://infisical.com/docs/self-hosting/deployments/kubernetes
##
infisical:
## @param backend.enabled Enable backend
##
enabled: false
## @param backend.name Backend name
##
name: infisical
replicaCount: 2
image:
repository: infisical/infisical
tag: "latest-postgres"
pullPolicy: IfNotPresent
backend:
enabled: true
name: backend
podAnnotations: {}
deploymentAnnotations:
secrets.infisical.com/auto-reload: "true"
replicaCount: 2
image:
repository: infisical/staging_infisical
tag: "latest"
pullPolicy: Always
kubeSecretRef: managed-backend-secret
service:
annotations: {}
type: ClusterIP
nodePort: ""
resources:
limits:
memory: 300Mi
kubeSecretRef: "infisical-gamma-secrets"
backendEnvironmentVariables: null
## Mongo DB persistence
mongodb:
enabled: false
persistence:
enabled: false
## By default the backend will be connected to a Mongo instance within the cluster
## However, it is recommended to add a managed document DB connection string for production-use (DBaaS)
## Learn about connection string type here https://www.mongodb.com/docs/manual/reference/connection-string/
## e.g. "mongodb://<user>:<pass>@<host>:<port>/<database-name>"
mongodbConnection:
externalMongoDBConnectionString: ""
ingress:
## @param ingress.enabled Enable ingress
##
enabled: true
## @param ingress.ingressClassName Ingress class name
##
ingressClassName: nginx
## @param ingress.nginx.enabled Ingress controller
##
# nginx:
# enabled: true
## @param ingress.annotations Ingress annotations
##
annotations:
cert-manager.io/cluster-issuer: "letsencrypt-prod"
hostName: "gamma.infisical.com"
# annotations:
# kubernetes.io/ingress.class: "nginx"
# cert-manager.io/issuer: letsencrypt-nginx
hostName: gamma.infisical.com ## <- Replace with your own domain
tls:
- secretName: letsencrypt-prod
hosts:
- gamma.infisical.com
[]
# - secretName: letsencrypt-nginx
# hosts:
# - infisical.local
mailhog:
enabled: false

View File

@ -3,7 +3,6 @@ on:
push:
tags:
- "infisical/v*.*.*"
- "!infisical/v*.*.*-postgres"
jobs:
backend-image:

View File

@ -35,15 +35,15 @@ jobs:
context: .
file: Dockerfile.standalone-infisical
tags: infisical/infisical:test
# - name: ⏻ Spawn backend container and dependencies
# run: |
# docker compose -f .github/resources/docker-compose.be-test.yml up --wait --quiet-pull
# - name: 🧪 Test backend image
# run: |
# ./.github/resources/healthcheck.sh infisical-backend-test
# - name: ⏻ Shut down backend container and dependencies
# run: |
# docker compose -f .github/resources/docker-compose.be-test.yml down
- name: ⏻ Spawn backend container and dependencies
run: |
docker compose -f .github/resources/docker-compose.be-test.yml up --wait --quiet-pull
- name: 🧪 Test backend image
run: |
./.github/resources/healthcheck.sh infisical-backend-test
- name: ⏻ Shut down backend container and dependencies
run: |
docker compose -f .github/resources/docker-compose.be-test.yml down
- name: 🏗️ Build backend and push
uses: depot/build-push-action@v1
with:
@ -59,32 +59,11 @@ jobs:
build-args: |
POSTHOG_API_KEY=${{ secrets.PUBLIC_POSTHOG_API_KEY }}
INFISICAL_PLATFORM_VERSION=${{ steps.extract_version.outputs.version }}
postgres-migration:
name: Run latest migration files
runs-on: ubuntu-latest
needs: [infisical-image]
steps:
- name: Checkout code
uses: actions/checkout@v2
- name: Setup Node.js environment
uses: actions/setup-node@v2
with:
node-version: "20"
- name: Change directory to backend and install dependencies
env:
DB_CONNECTION_URI: ${{ secrets.DB_CONNECTION_URI }}
run: |
cd backend
npm install
npm run migration:latest
# - name: Run postgres DB migration files
# env:
# DB_CONNECTION_URI: ${{ secrets.DB_CONNECTION_URI }}
# run: npm run migration:latest
gamma-deployment:
name: Deploy to gamma
runs-on: ubuntu-latest
needs: [postgres-migration]
needs: [infisical-image]
steps:
- name: ☁️ Checkout source
uses: actions/checkout@v3
@ -103,7 +82,7 @@ jobs:
with:
token: ${{ secrets.DIGITALOCEAN_ACCESS_TOKEN }}
- name: Save DigitalOcean kubeconfig with short-lived credentials
run: doctl kubernetes cluster kubeconfig save --expiry-seconds 600 infisical-gamma-postgres
run: doctl kubernetes cluster kubeconfig save --expiry-seconds 600 k8s-1-25-4-do-0-nyc1-1670645170179
- name: switch to gamma namespace
run: kubectl config set-context --current --namespace=gamma
- name: test kubectl
@ -111,7 +90,7 @@ jobs:
- name: Download helm values to file and upgrade gamma deploy
run: |
wget https://raw.githubusercontent.com/Infisical/infisical/main/.github/values.yaml
helm upgrade infisical infisical-helm-charts/infisical-standalone --values values.yaml --wait --install
helm upgrade infisical infisical-helm-charts/infisical --values values.yaml --wait --install
if [[ $(helm status infisical) == *"FAILED"* ]]; then
echo "Helm upgrade failed"
exit 1

View File

@ -2,11 +2,11 @@ name: Release standalone docker image
on:
push:
tags:
- "infisical/v*.*.*-postgres"
- "infisical/v*.*.*"
jobs:
infisical-standalone:
name: Build infisical standalone image postgres
name: Build infisical standalone image
runs-on: ubuntu-latest
steps:
- name: Extract version from tag
@ -19,6 +19,27 @@ jobs:
- name: 📦 Install dependencies to test all dependencies
run: npm ci --only-production
working-directory: backend
- uses: paulhatch/semantic-version@v5.0.2
id: version
with:
# The prefix to use to identify tags
tag_prefix: "infisical-standalone/v"
# A string which, if present in a git commit, indicates that a change represents a
# major (breaking) change, supports regular expressions wrapped with '/'
major_pattern: "(MAJOR)"
# Same as above except indicating a minor change, supports regular expressions wrapped with '/'
minor_pattern: "(MINOR)"
# A string to determine the format of the version output
version_format: "${major}.${minor}.${patch}-prerelease${increment}"
# Optional path to check for changes. If any changes are detected in the path the
# 'changed' output will true. Enter multiple paths separated by spaces.
change_path: "backend,frontend"
# Prevents pre-v1.0.0 version from automatically incrementing the major version.
# If enabled, when the major version is 0, major releases will be treated as minor and minor as patch. Note that the version_type output is unchanged.
enable_prerelease_mode: true
# - name: 🧪 Run tests
# run: npm run test:ci
# working-directory: backend
- name: version output
run: |
echo "Output Value: ${{ steps.version.outputs.major }}"
@ -47,7 +68,7 @@ jobs:
push: true
context: .
tags: |
infisical/infisical:latest-postgres
infisical/infisical:latest
infisical/infisical:${{ steps.commit.outputs.short }}
infisical/infisical:${{ steps.extract_version.outputs.version }}
platforms: linux/amd64,linux/arm64

3
.gitignore vendored
View File

@ -1,7 +1,6 @@
# backend
node_modules
.env
.env.test
.env.dev
.env.gamma
.env.prod
@ -62,4 +61,4 @@ yarn-error.log*
# Editor specific
.vscode/*
frontend-build
frontend-build

View File

@ -2,7 +2,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 node:16-alpine AS base
FROM base AS frontend-dependencies
@ -73,7 +73,6 @@ 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
@ -104,17 +103,14 @@ ENV NEXT_PUBLIC_INTERCOM_ID=$INTERCOM_ID \
WORKDIR /
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
ENV PORT 8080
ENV HOST=0.0.0.0
ENV HTTPS_ENABLED false
ENV NODE_ENV production
ENV STANDALONE_BUILD true
ENV STANDALONE_MODE true
WORKDIR /backend
ENV TELEMETRY_ENABLED true
@ -123,8 +119,10 @@ HEALTHCHECK --interval=10s --timeout=3s --start-period=10s \
CMD node healthcheck.js
EXPOSE 8080
EXPOSE 443
USER non-root-user
CMD ["./standalone-entrypoint.sh"]

View File

@ -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

View File

@ -1,2 +1,2 @@
vitest-environment-infisical.ts
vitest.config.ts
node_modules
built

41
backend/.eslintrc Normal file
View File

@ -0,0 +1,41 @@
{
"parser": "@typescript-eslint/parser",
"plugins": [
"@typescript-eslint",
"unused-imports"
],
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/eslint-recommended",
"plugin:@typescript-eslint/recommended"
],
"rules": {
"no-empty-function": "off",
"@typescript-eslint/no-empty-function": "off",
"no-console": 2,
"quotes": [
"error",
"double",
{
"avoidEscape": true
}
],
"comma-dangle": [
"error",
"only-multiline"
],
"@typescript-eslint/no-unused-vars": "off",
"unused-imports/no-unused-imports": "error",
"@typescript-eslint/no-extra-semi": "off", // added to be able to push
"unused-imports/no-unused-vars": [
"warn",
{
"vars": "all",
"varsIgnorePattern": "^_",
"args": "after-used",
"argsIgnorePattern": "^_"
}
],
"sort-imports": 1
}
}

View File

@ -1,61 +0,0 @@
/* eslint-env node */
module.exports = {
env: {
es6: true,
node: true
},
extends: [
"eslint:recommended",
"plugin:@typescript-eslint/recommended",
"plugin:@typescript-eslint/recommended-type-checked",
"airbnb-base",
"airbnb-typescript/base",
"plugin:prettier/recommended",
"prettier"
],
plugins: ["@typescript-eslint", "simple-import-sort", "import"],
parser: "@typescript-eslint/parser",
parserOptions: {
project: true,
sourceType: "module",
tsconfigRootDir: __dirname
},
root: true,
rules: {
"@typescript-eslint/no-empty-function": "off",
"@typescript-eslint/no-unsafe-enum-comparison": "off",
"no-void": "off",
"consistent-return": "off", // my style
"import/order": "off", // for simple-import-order
"import/prefer-default-export": "off", // why
"no-restricted-syntax": "off",
// importing rules
"simple-import-sort/exports": "error",
"import/first": "error",
"import/newline-after-import": "error",
"import/no-duplicates": "error",
"simple-import-sort/imports": [
"warn",
{
groups: [
// Side effect imports.
["^\\u0000"],
// Node.js builtins prefixed with `node:`.
["^node:"],
// Packages.
// Things that start with a letter (or digit or underscore), or `@` followed by a letter.
["^@?\\w"],
["^@app"],
["@lib"],
["@server"],
// Absolute imports and other imports such as Vue-style `@/foo`.
// Anything not matched in another group.
["^"],
// Relative imports.
// Anything that starts with a dot.
["^\\."]
]
}
]
}
};

1
backend/.gitignore vendored
View File

@ -1 +0,0 @@
dist

View File

@ -1,6 +1,6 @@
{
"singleQuote": false,
"printWidth": 120,
"printWidth": 100,
"trailingComma": "none",
"tabWidth": 2,
"semi": true

View File

@ -1,5 +1,5 @@
# Build stage
FROM node:20-alpine AS build
FROM node:16-alpine AS build
WORKDIR /app
@ -10,7 +10,7 @@ COPY . .
RUN npm run build
# Production stage
FROM node:20-alpine
FROM node:16-alpine
WORKDIR /app
@ -28,8 +28,6 @@ RUN apk add --no-cache bash curl && curl -1sLf \
HEALTHCHECK --interval=10s --timeout=3s --start-period=10s \
CMD node healthcheck.js
ENV HOST=0.0.0.0
EXPOSE 4000
CMD ["npm", "start"]
CMD ["node", "build/index.js"]

View File

@ -1,18 +0,0 @@
FROM node:20-alpine
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
WORKDIR /app
COPY package.json package.json
COPY package-lock.json package-lock.json
RUN npm install
COPY . .
ENV HOST=0.0.0.0
CMD ["npm", "run", "dev:docker"]

View File

@ -1,26 +0,0 @@
import { TQueueServiceFactory } from "@app/queue";
export const mockQueue = (): TQueueServiceFactory => {
const queues: Record<string, unknown> = {};
const workers: Record<string, unknown> = {};
const job: Record<string, unknown> = {};
const events: Record<string, unknown> = {};
return {
queue: async (name, jobData) => {
job[name] = jobData;
},
shutdown: async () => undefined,
stopRepeatableJob: async () => true,
start: (name, jobFn) => {
queues[name] = jobFn;
workers[name] = jobFn;
},
listen: (name, event) => {
events[name] = event;
},
clearQueue: async () => {},
stopJobById: async () => {},
stopRepeatableJobByJobId: async () => true
};
};

View File

@ -1,10 +0,0 @@
import { TSmtpSendMail, TSmtpService } from "@app/services/smtp/smtp-service";
export const mockSmtpServer = (): TSmtpService => {
const storage: TSmtpSendMail[] = [];
return {
sendMail: async (data) => {
storage.push(data);
}
};
};

View File

@ -1,45 +0,0 @@
import { seedData1 } from "@app/db/seed-data";
import jsrp from "jsrp";
describe("Login V1 Router", async () => {
// eslint-disable-next-line
const client = new jsrp.client();
await new Promise((resolve) => {
client.init({ username: seedData1.email, password: seedData1.password }, () => resolve(null));
});
let clientProof: string;
test("Login first phase", async () => {
const res = await testServer.inject({
method: "POST",
url: "/api/v3/auth/login1",
body: {
email: "test@localhost.local",
clientPublicKey: client.getPublicKey()
}
});
expect(res.statusCode).toBe(200);
const payload = JSON.parse(res.payload);
expect(payload).toHaveProperty("serverPublicKey");
expect(payload).toHaveProperty("salt");
client.setSalt(payload.salt);
client.setServerPublicKey(payload.serverPublicKey);
clientProof = client.getProof(); // called M1
});
test("Login second phase", async () => {
const res = await testServer.inject({
method: "POST",
url: "/api/v3/auth/login2",
body: {
email: seedData1.email,
clientProof
}
});
expect(res.statusCode).toBe(200);
const payload = JSON.parse(res.payload);
expect(payload).toHaveProperty("mfaEnabled");
expect(payload).toHaveProperty("token");
expect(payload.mfaEnabled).toBeFalsy();
});
});

View File

@ -1,19 +0,0 @@
import { seedData1 } from "@app/db/seed-data";
describe("Org V1 Router", async () => {
test("GET Org list", async () => {
const res = await testServer.inject({
method: "GET",
url: "/api/v1/organization",
headers: {
authorization: `Bearer ${jwtAuthToken}`
}
});
expect(res.statusCode).toBe(200);
const payload = JSON.parse(res.payload);
expect(payload).toHaveProperty("organizations");
expect(payload).toEqual({
organizations: [expect.objectContaining({ name: seedData1.organization.name })]
});
});
});

View File

@ -1,151 +0,0 @@
import { seedData1 } from "@app/db/seed-data";
import { DEFAULT_PROJECT_ENVS } from "@app/db/seeds/3-project";
describe("Project Environment Router", async () => {
test("Get default environments", async () => {
const res = await testServer.inject({
method: "GET",
url: `/api/v1/workspace/${seedData1.project.id}`,
headers: {
authorization: `Bearer ${jwtAuthToken}`
}
});
expect(res.statusCode).toBe(200);
const payload = JSON.parse(res.payload);
expect(payload).toHaveProperty("workspace");
// check for default environments
expect(payload).toEqual({
workspace: expect.objectContaining({
name: seedData1.project.name,
id: seedData1.project.id,
slug: seedData1.project.slug,
environments: expect.arrayContaining([
expect.objectContaining(DEFAULT_PROJECT_ENVS[0]),
expect.objectContaining(DEFAULT_PROJECT_ENVS[1]),
expect.objectContaining(DEFAULT_PROJECT_ENVS[2])
])
})
});
// ensure only two default environments exist
expect(payload.workspace.environments.length).toBe(3);
});
const mockProjectEnv = { name: "temp", slug: "temp", id: "" }; // id will be filled in create op
test("Create environment", async () => {
const res = await testServer.inject({
method: "POST",
url: `/api/v1/workspace/${seedData1.project.id}/environments`,
headers: {
authorization: `Bearer ${jwtAuthToken}`
},
body: {
name: mockProjectEnv.name,
slug: mockProjectEnv.slug
}
});
expect(res.statusCode).toBe(200);
const payload = JSON.parse(res.payload);
expect(payload).toHaveProperty("environment");
expect(payload.environment).toEqual(
expect.objectContaining({
id: expect.any(String),
name: mockProjectEnv.name,
slug: mockProjectEnv.slug,
projectId: seedData1.project.id,
position: DEFAULT_PROJECT_ENVS.length + 1,
createdAt: expect.any(String),
updatedAt: expect.any(String)
})
);
mockProjectEnv.id = payload.environment.id;
});
test("Update environment", async () => {
const updatedName = { name: "temp#2", slug: "temp2" };
const res = await testServer.inject({
method: "PATCH",
url: `/api/v1/workspace/${seedData1.project.id}/environments/${mockProjectEnv.id}`,
headers: {
authorization: `Bearer ${jwtAuthToken}`
},
body: {
name: updatedName.name,
slug: updatedName.slug,
position: 1
}
});
expect(res.statusCode).toBe(200);
const payload = JSON.parse(res.payload);
expect(payload).toHaveProperty("environment");
expect(payload.environment).toEqual(
expect.objectContaining({
id: expect.any(String),
name: updatedName.name,
slug: updatedName.slug,
projectId: seedData1.project.id,
position: 1,
createdAt: expect.any(String),
updatedAt: expect.any(String)
})
);
mockProjectEnv.name = updatedName.name;
mockProjectEnv.slug = updatedName.slug;
});
test("Delete environment", async () => {
const res = await testServer.inject({
method: "DELETE",
url: `/api/v1/workspace/${seedData1.project.id}/environments/${mockProjectEnv.id}`,
headers: {
authorization: `Bearer ${jwtAuthToken}`
}
});
expect(res.statusCode).toBe(200);
const payload = JSON.parse(res.payload);
expect(payload).toHaveProperty("environment");
expect(payload.environment).toEqual(
expect.objectContaining({
id: expect.any(String),
name: mockProjectEnv.name,
slug: mockProjectEnv.slug,
position: 1,
createdAt: expect.any(String),
updatedAt: expect.any(String)
})
);
});
// after all these opreations the list of environment should be still same
test("Default list of environment", async () => {
const res = await testServer.inject({
method: "GET",
url: `/api/v1/workspace/${seedData1.project.id}`,
headers: {
authorization: `Bearer ${jwtAuthToken}`
}
});
expect(res.statusCode).toBe(200);
const payload = JSON.parse(res.payload);
expect(payload).toHaveProperty("workspace");
// check for default environments
expect(payload).toEqual({
workspace: expect.objectContaining({
name: seedData1.project.name,
id: seedData1.project.id,
slug: seedData1.project.slug,
environments: expect.arrayContaining([
expect.objectContaining(DEFAULT_PROJECT_ENVS[0]),
expect.objectContaining(DEFAULT_PROJECT_ENVS[1]),
expect.objectContaining(DEFAULT_PROJECT_ENVS[2])
])
})
});
// ensure only two default environments exist
expect(payload.workspace.environments.length).toBe(3);
});
});

View File

@ -1,155 +0,0 @@
import { seedData1 } from "@app/db/seed-data";
describe("Secret Folder Router", async () => {
test.each([
{ name: "folder1", path: "/" }, // one in root
{ name: "folder1", path: "/level1/level2" }, // then create a deep one creating intermediate ones
{ name: "folder2", path: "/" },
{ name: "folder1", path: "/level1/level2" } // this should not create folder return same thing
])("Create folder $name in $path", async ({ name, path }) => {
const res = await testServer.inject({
method: "POST",
url: `/api/v1/folders`,
headers: {
authorization: `Bearer ${jwtAuthToken}`
},
body: {
workspaceId: seedData1.project.id,
environment: seedData1.environment.slug,
name,
path
}
});
expect(res.statusCode).toBe(200);
const payload = JSON.parse(res.payload);
expect(payload).toHaveProperty("folder");
// check for default environments
expect(payload).toEqual({
folder: expect.objectContaining({
name,
id: expect.any(String)
})
});
});
test.each([
{
path: "/",
expected: {
folders: [{ name: "folder1" }, { name: "level1" }, { name: "folder2" }],
length: 3
}
},
{ path: "/level1/level2", expected: { folders: [{ name: "folder1" }], length: 1 } }
])("Get folders $path", async ({ path, expected }) => {
const res = await testServer.inject({
method: "GET",
url: `/api/v1/folders`,
headers: {
authorization: `Bearer ${jwtAuthToken}`
},
query: {
workspaceId: seedData1.project.id,
environment: seedData1.environment.slug,
path
}
});
expect(res.statusCode).toBe(200);
const payload = JSON.parse(res.payload);
expect(payload).toHaveProperty("folders");
expect(payload.folders.length).toBe(expected.length);
expect(payload).toEqual({ folders: expected.folders.map((el) => expect.objectContaining(el)) });
});
let toBeDeleteFolderId = "";
test("Update a deep folder", async () => {
const res = await testServer.inject({
method: "PATCH",
url: `/api/v1/folders/folder1`,
headers: {
authorization: `Bearer ${jwtAuthToken}`
},
body: {
workspaceId: seedData1.project.id,
environment: seedData1.environment.slug,
name: "folder-updated",
path: "/level1/level2"
}
});
expect(res.statusCode).toBe(200);
const payload = JSON.parse(res.payload);
expect(payload).toHaveProperty("folder");
expect(payload.folder).toEqual(
expect.objectContaining({
id: expect.any(String),
name: "folder-updated"
})
);
toBeDeleteFolderId = payload.folder.id;
const resUpdatedFolders = await testServer.inject({
method: "GET",
url: `/api/v1/folders`,
headers: {
authorization: `Bearer ${jwtAuthToken}`
},
query: {
workspaceId: seedData1.project.id,
environment: seedData1.environment.slug,
path: "/level1/level2"
}
});
expect(resUpdatedFolders.statusCode).toBe(200);
const updatedFolderList = JSON.parse(resUpdatedFolders.payload);
expect(updatedFolderList).toHaveProperty("folders");
expect(updatedFolderList.folders.length).toEqual(1);
expect(updatedFolderList.folders[0].name).toEqual("folder-updated");
});
test("Delete a deep folder", async () => {
const res = await testServer.inject({
method: "DELETE",
url: `/api/v1/folders/${toBeDeleteFolderId}`,
headers: {
authorization: `Bearer ${jwtAuthToken}`
},
body: {
workspaceId: seedData1.project.id,
environment: seedData1.environment.slug,
path: "/level1/level2"
}
});
expect(res.statusCode).toBe(200);
const payload = JSON.parse(res.payload);
expect(payload).toHaveProperty("folder");
expect(payload.folder).toEqual(
expect.objectContaining({
id: expect.any(String),
name: "folder-updated"
})
);
const resUpdatedFolders = await testServer.inject({
method: "GET",
url: `/api/v1/folders`,
headers: {
authorization: `Bearer ${jwtAuthToken}`
},
query: {
workspaceId: seedData1.project.id,
environment: seedData1.environment.slug,
path: "/level1/level2"
}
});
expect(resUpdatedFolders.statusCode).toBe(200);
const updatedFolderList = JSON.parse(resUpdatedFolders.payload);
expect(updatedFolderList).toHaveProperty("folders");
expect(updatedFolderList.folders.length).toEqual(0);
});
});

View File

@ -1,179 +0,0 @@
import { seedData1 } from "@app/db/seed-data";
describe("Secret Folder Router", async () => {
test.each([
{ importEnv: "dev", importPath: "/" }, // one in root
{ importEnv: "staging", importPath: "/" } // then create a deep one creating intermediate ones
])("Create secret import $importEnv with path $importPath", async ({ importPath, importEnv }) => {
const res = await testServer.inject({
method: "POST",
url: `/api/v1/secret-imports`,
headers: {
authorization: `Bearer ${jwtAuthToken}`
},
body: {
workspaceId: seedData1.project.id,
environment: seedData1.environment.slug,
path: "/",
import: {
environment: importEnv,
path: importPath
}
}
});
expect(res.statusCode).toBe(200);
const payload = JSON.parse(res.payload);
expect(payload).toHaveProperty("secretImport");
// check for default environments
expect(payload.secretImport).toEqual(
expect.objectContaining({
id: expect.any(String),
importPath: expect.any(String),
importEnv: expect.objectContaining({
name: expect.any(String),
slug: expect.any(String),
id: expect.any(String)
})
})
);
});
let testSecretImportId = "";
test("Get secret imports", async () => {
const res = await testServer.inject({
method: "GET",
url: `/api/v1/secret-imports`,
headers: {
authorization: `Bearer ${jwtAuthToken}`
},
query: {
workspaceId: seedData1.project.id,
environment: seedData1.environment.slug,
path: "/"
}
});
expect(res.statusCode).toBe(200);
const payload = JSON.parse(res.payload);
expect(payload).toHaveProperty("secretImports");
expect(payload.secretImports.length).toBe(2);
testSecretImportId = payload.secretImports[0].id;
expect(payload.secretImports).toEqual(
expect.arrayContaining([
expect.objectContaining({
id: expect.any(String),
importPath: expect.any(String),
importEnv: expect.objectContaining({
name: expect.any(String),
slug: expect.any(String),
id: expect.any(String)
})
})
])
);
});
test("Update secret import position", async () => {
const res = await testServer.inject({
method: "PATCH",
url: `/api/v1/secret-imports/${testSecretImportId}`,
headers: {
authorization: `Bearer ${jwtAuthToken}`
},
body: {
workspaceId: seedData1.project.id,
environment: seedData1.environment.slug,
path: "/",
import: {
position: 2
}
}
});
expect(res.statusCode).toBe(200);
const payload = JSON.parse(res.payload);
expect(payload).toHaveProperty("secretImport");
// check for default environments
expect(payload.secretImport).toEqual(
expect.objectContaining({
id: expect.any(String),
importPath: expect.any(String),
position: 2,
importEnv: expect.objectContaining({
name: expect.any(String),
slug: expect.any(String),
id: expect.any(String)
})
})
);
const secretImportsListRes = await testServer.inject({
method: "GET",
url: `/api/v1/secret-imports`,
headers: {
authorization: `Bearer ${jwtAuthToken}`
},
query: {
workspaceId: seedData1.project.id,
environment: seedData1.environment.slug,
path: "/"
}
});
expect(secretImportsListRes.statusCode).toBe(200);
const secretImportList = JSON.parse(secretImportsListRes.payload);
expect(secretImportList).toHaveProperty("secretImports");
expect(secretImportList.secretImports[1].id).toEqual(testSecretImportId);
});
test("Delete secret import position", async () => {
const res = await testServer.inject({
method: "DELETE",
url: `/api/v1/secret-imports/${testSecretImportId}`,
headers: {
authorization: `Bearer ${jwtAuthToken}`
},
body: {
workspaceId: seedData1.project.id,
environment: seedData1.environment.slug,
path: "/"
}
});
expect(res.statusCode).toBe(200);
const payload = JSON.parse(res.payload);
expect(payload).toHaveProperty("secretImport");
// check for default environments
expect(payload.secretImport).toEqual(
expect.objectContaining({
id: expect.any(String),
importPath: expect.any(String),
importEnv: expect.objectContaining({
name: expect.any(String),
slug: expect.any(String),
id: expect.any(String)
})
})
);
const secretImportsListRes = await testServer.inject({
method: "GET",
url: `/api/v1/secret-imports`,
headers: {
authorization: `Bearer ${jwtAuthToken}`
},
query: {
workspaceId: seedData1.project.id,
environment: seedData1.environment.slug,
path: "/"
}
});
expect(secretImportsListRes.statusCode).toBe(200);
const secretImportList = JSON.parse(secretImportsListRes.payload);
expect(secretImportList).toHaveProperty("secretImports");
expect(secretImportList.secretImports.length).toEqual(1);
expect(secretImportList.secretImports[0].position).toEqual(1);
});
});

View File

@ -1,9 +0,0 @@
describe("Status V1 Router", async () => {
test("Simple check", async () => {
const res = await testServer.inject({
method: "GET",
url: "/api/status"
});
expect(res.statusCode).toBe(200);
});
});

View File

@ -1,75 +0,0 @@
// import { main } from "@app/server/app";
import { initEnvConfig } from "@app/lib/config/env";
import dotenv from "dotenv";
import knex from "knex";
import path from "path";
import { mockSmtpServer } from "./mocks/smtp";
import { initLogger } from "@app/lib/logger";
import jwt from "jsonwebtoken";
import "ts-node/register";
import { main } from "@app/server/app";
import { mockQueue } from "./mocks/queue";
import { AuthTokenType } from "@app/services/auth/auth-type";
import { seedData1 } from "@app/db/seed-data";
dotenv.config({ path: path.join(__dirname, "../.env.test") });
export default {
name: "knex-env",
transformMode: "ssr",
async setup() {
const db = knex({
client: "pg",
connection: process.env.DB_CONNECTION_URI,
migrations: {
directory: path.join(__dirname, "../src/db/migrations"),
extension: "ts",
tableName: "infisical_migrations"
},
seeds: {
directory: path.join(__dirname, "../src/db/seeds"),
extension: "ts"
}
});
try {
await db.migrate.latest();
await db.seed.run();
const smtp = mockSmtpServer();
const queue = mockQueue();
const logger = await initLogger();
const cfg = initEnvConfig(logger);
const server = await main({ db, smtp, logger, queue });
// @ts-expect-error type
globalThis.testServer = server;
// @ts-expect-error type
globalThis.jwtAuthToken = jwt.sign(
{
authTokenType: AuthTokenType.ACCESS_TOKEN,
userId: seedData1.id,
tokenVersionId: seedData1.token.id,
accessVersion: 1
},
cfg.AUTH_SECRET,
{ expiresIn: cfg.JWT_AUTH_LIFETIME }
);
} catch (error) {
await db.destroy();
throw error;
}
// custom setup
return {
async teardown() {
// @ts-expect-error type
await globalThis.testServer.close();
// @ts-expect-error type
delete globalThis.testServer;
// @ts-expect-error type
delete globalThis.jwtToken;
// called after all tests with this env have been run
await db.migrate.rollback({}, true);
await db.destroy();
}
};
}
};

46
backend/environment.d.ts vendored Normal file
View File

@ -0,0 +1,46 @@
export {};
declare global {
namespace NodeJS {
interface ProcessEnv {
PORT: string;
ENCRYPTION_KEY: string;
SALT_ROUNDS: string;
JWT_AUTH_LIFETIME: string;
JWT_AUTH_SECRET: string;
JWT_REFRESH_LIFETIME: string;
JWT_REFRESH_SECRET: string;
JWT_SERVICE_SECRET: string;
JWT_SIGNUP_LIFETIME: string;
JWT_SIGNUP_SECRET: string;
MONGO_URL: string;
NODE_ENV: "development" | "staging" | "testing" | "production";
VERBOSE_ERROR_OUTPUT: string;
LOKI_HOST: string;
CLIENT_ID_HEROKU: string;
CLIENT_ID_VERCEL: string;
CLIENT_ID_NETLIFY: string;
CLIENT_ID_GITHUB: string;
CLIENT_ID_GITLAB: string;
CLIENT_SECRET_HEROKU: string;
CLIENT_SECRET_VERCEL: string;
CLIENT_SECRET_NETLIFY: string;
CLIENT_SECRET_GITHUB: string;
CLIENT_SECRET_GITLAB: string;
CLIENT_SLUG_VERCEL: string;
POSTHOG_HOST: string;
POSTHOG_PROJECT_API_KEY: string;
SENTRY_DSN: string;
SITE_URL: string;
SMTP_HOST: string;
SMTP_SECURE: string;
SMTP_PORT: string;
SMTP_USERNAME: string;
SMTP_PASSWORD: string;
SMTP_FROM_ADDRESS: string;
SMTP_FROM_NAME: string;
TELEMETRY_ENABLED: string;
LICENSE_KEY: string;
}
}
}

24
backend/healthcheck.js Normal file
View File

@ -0,0 +1,24 @@
const http = require('http');
const PORT = process.env.PORT || 4000;
const options = {
host: 'localhost',
port: PORT,
timeout: 2000,
path: '/healthcheck'
};
const healthCheck = http.request(options, (res) => {
console.log(`HEALTHCHECK STATUS: ${res.statusCode}`);
if (res.statusCode == 200) {
process.exit(0);
} else {
process.exit(1);
}
});
healthCheck.on('error', function (err) {
console.error(`HEALTH CHECK ERROR: ${err}`);
process.exit(1);
});
healthCheck.end();

BIN
backend/img/dashboard.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 493 KiB

9
backend/jest.config.ts Normal file
View File

@ -0,0 +1,9 @@
export default {
preset: "ts-jest",
testEnvironment: "node",
collectCoverageFrom: ["src/*.{js,ts}", "!**/node_modules/**"],
modulePaths: ["<rootDir>/src"],
testMatch: ["<rootDir>/tests/**/*.test.ts"],
setupFiles: ["<rootDir>/test-resources/env-vars.js"],
setupFilesAfterEnv: ["<rootDir>/tests/setupTests.ts"],
};

View File

@ -1,6 +1,6 @@
{
"watch": ["src"],
"ext": ".ts,.js",
"ignore": [],
"exec": "tsx ./src/main.ts | pino-pretty --colorize --colorizeObjects --singleLine"
}
"watch": ["src"],
"ext": ".ts,.js",
"ignore": [],
"exec": "ts-node ./src/index.ts"
}

33621
backend/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,126 +1,149 @@
{
"name": "backend",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"dev": "tsx watch --clear-screen=false ./src/main.ts | pino-pretty --colorize --colorizeObjects --singleLine",
"dev:docker": "nodemon",
"build": "rimraf dist && tsup && cp -R ./src/lib/validator/disposable_emails.txt ./dist && cp -R ./src/services/smtp/templates ./dist",
"start": "node dist/main.mjs",
"type:check": "tsc --noEmit",
"lint:fix": "eslint --fix --ext js,ts ./src",
"lint": "eslint 'src/**/*.ts'",
"test:e2e": "vitest run -c vitest.e2e.config.ts",
"test:e2e-watch": "vitest -c vitest.e2e.config.ts",
"test:e2e-coverage": "vitest run --coverage -c vitest.e2e.config.ts",
"generate:component": "tsx ./scripts/create-backend-file.ts",
"generate:schema": "tsx ./scripts/generate-schema-types.ts",
"migration:new": "tsx ./scripts/create-migration.ts",
"migration:up": "knex --knexfile ./src/db/knexfile.ts --client pg migrate:up",
"migration:down": "knex --knexfile ./src/db/knexfile.ts --client pg migrate:down",
"migration:list": "knex --knexfile ./src/db/knexfile.ts --client pg migrate:list",
"migration:latest": "knex --knexfile ./src/db/knexfile.ts --client pg migrate:latest",
"migration:rollback": "knex --knexfile ./src/db/knexfile.ts migrate:rollback",
"seed:new": "tsx ./scripts/create-seed-file.ts",
"seed:run": "knex --knexfile ./src/db/knexfile.ts --client pg seed:run",
"db:reset": "npm run migration:rollback -- --all && npm run migration:latest && npm run seed:run"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"@types/bcrypt": "^5.0.2",
"@types/jmespath": "^0.15.2",
"@types/jsonwebtoken": "^9.0.5",
"@types/jsrp": "^0.2.6",
"@types/libsodium-wrappers": "^0.7.13",
"@types/lodash.isequal": "^4.5.8",
"@types/node": "^20.9.5",
"@types/nodemailer": "^6.4.14",
"@types/passport-github": "^1.1.12",
"@types/passport-google-oauth20": "^2.0.14",
"@types/pg": "^8.10.9",
"@types/picomatch": "^2.3.3",
"@types/prompt-sync": "^4.2.3",
"@types/uuid": "^9.0.7",
"eslint-config-prettier": "^9.1.0",
"eslint-import-resolver-typescript": "^3.6.1",
"eslint-plugin-import": "^2.29.1",
"eslint-plugin-prettier": "^5.1.3",
"eslint-plugin-simple-import-sort": "^10.0.0",
"nodemon": "^3.0.2",
"pino-pretty": "^10.2.3",
"prompt-sync": "^4.2.0",
"rimraf": "^5.0.5",
"ts-node": "^10.9.1",
"tsconfig-paths": "^4.2.0",
"tsup": "^8.0.1",
"tsx": "^4.4.0",
"typescript": "^5.3.2",
"vite-tsconfig-paths": "^4.2.2",
"vitest": "^1.0.4"
},
"dependencies": {
"@aws-sdk/client-secrets-manager": "^3.485.0",
"@aws-sdk/client-secrets-manager": "^3.319.0",
"@casl/ability": "^6.5.0",
"@fastify/cookie": "^9.2.0",
"@fastify/cors": "^8.4.1",
"@fastify/formbody": "^7.4.0",
"@fastify/helmet": "^11.1.1",
"@fastify/passport": "^2.4.0",
"@fastify/rate-limit": "^9.0.0",
"@fastify/session": "^10.7.0",
"@fastify/swagger": "^8.12.0",
"@fastify/swagger-ui": "^1.10.1",
"@casl/mongoose": "^7.2.1",
"@godaddy/terminus": "^4.12.0",
"@node-saml/passport-saml": "^4.0.4",
"@octokit/rest": "^20.0.2",
"@octokit/webhooks-types": "^7.3.1",
"@octokit/rest": "^19.0.5",
"@sentry/node": "^7.77.0",
"@sentry/tracing": "^7.48.0",
"@serdnam/pino-cloudwatch-transport": "^1.0.4",
"@sindresorhus/slugify": "^2.2.1",
"@typescript-eslint/eslint-plugin": "^6.20.0",
"@typescript-eslint/parser": "^6.20.0",
"@types/crypto-js": "^4.1.1",
"@types/libsodium-wrappers": "^0.7.10",
"@ucast/mongo2js": "^1.3.4",
"ajv": "^8.12.0",
"argon2": "^0.31.2",
"aws-sdk": "^2.1532.0",
"axios": "^1.6.2",
"axios-retry": "^4.0.0",
"bcrypt": "^5.1.1",
"bullmq": "^5.1.1",
"dotenv": "^16.3.1",
"eslint": "^8.56.0",
"eslint-config-airbnb-base": "^15.0.0",
"eslint-config-airbnb-typescript": "^17.1.0",
"fastify": "^4.24.3",
"fastify-plugin": "^4.5.1",
"handlebars": "^4.7.8",
"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",
"jsonwebtoken": "^9.0.2",
"js-yaml": "^4.1.0",
"jsonwebtoken": "^9.0.0",
"jsrp": "^0.2.4",
"knex": "^3.0.1",
"libsodium-wrappers": "^0.7.13",
"lodash.isequal": "^4.5.0",
"mysql2": "^3.6.5",
"nanoid": "^5.0.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.9.7",
"ora": "^7.0.1",
"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",
"passport-ldapauth": "^3.0.1",
"pg": "^8.11.3",
"picomatch": "^3.0.1",
"pino": "^8.16.2",
"posthog-node": "^3.6.0",
"pino": "^8.16.1",
"pino-http": "^8.5.1",
"posthog-node": "^2.6.0",
"probot": "^12.3.3",
"smee-client": "^2.0.0",
"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",
"uuid": "^9.0.1",
"zod": "^3.22.4",
"zod-to-json-schema": "^3.22.0"
"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}"
}
}

View File

@ -1,123 +0,0 @@
/* eslint-disable */
import { mkdirSync, writeFileSync } from "fs";
import path from "path";
import promptSync from "prompt-sync";
const prompt = promptSync({
sigint: true
});
console.log(`
Component List
--------------
1. Service component
2. DAL component
3. Router component
`);
const componentType = parseInt(prompt("Select a component: "), 10);
if (componentType === 1) {
const componentName = prompt("Enter service name: ");
const dir = path.join(__dirname, `../src/services/${componentName}`);
const pascalCase = componentName
.split("-")
.map((el) => `${el[0].toUpperCase()}${el.slice(1)}`)
.join("");
const camelCase = componentName
.split("-")
.map((el, index) => (index === 0 ? el : `${el[0].toUpperCase()}${el.slice(1)}`))
.join("");
const dalTypeName = `T${pascalCase}DALFactory`;
const dalName = `${camelCase}DALFactory`;
const serviceTypeName = `T${pascalCase}ServiceFactory`;
const serviceName = `${camelCase}ServiceFactory`;
mkdirSync(dir);
writeFileSync(
path.join(dir, `${componentName}-dal.ts`),
`import { TDbClient } from "@app/db";
import { TableName } from "@app/db/schemas";
export type ${dalTypeName} = ReturnType<typeof ${dalName}>;
export const ${dalName} = (db: TDbClient) => {
return { };
};
`
);
writeFileSync(
path.join(dir, `${componentName}-service.ts`),
`import { ${dalTypeName} } from "./${componentName}-dal";
type ${serviceTypeName}Dep = {
${camelCase}DAL: ${dalTypeName};
};
export type ${serviceTypeName} = ReturnType<typeof ${serviceName}>;
export const ${serviceName} = ({ ${camelCase}DAL }: ${serviceTypeName}Dep) => {
return {};
};
`
);
writeFileSync(path.join(dir, `${componentName}-types.ts`), "");
} else if (componentType === 2) {
const componentName = prompt("Enter service name: ");
const componentPath = prompt("Path wrt service folder: ");
const pascalCase = componentName
.split("-")
.map((el) => `${el[0].toUpperCase()}${el.slice(1)}`)
.join("");
const camelCase = componentName
.split("-")
.map((el, index) => (index === 0 ? el : `${el[0].toUpperCase()}${el.slice(1)}`))
.join("");
const dalTypeName = `T${pascalCase}DALFactory`;
const dalName = `${camelCase}DALFactory`;
writeFileSync(
path.join(__dirname, "../src/services", componentPath, `${componentName}-dal.ts`),
`import { TDbClient } from "@app/db";
import { TableName } from "@app/db/schemas";
export type ${dalTypeName} = ReturnType<typeof ${dalName}>;
export const ${dalName} = (db: TDbClient) => {
return { };
};
`
);
} else if (componentType === 3) {
const name = prompt("Enter router name: ");
const version = prompt("Version number: ");
const pascalCase = name
.split("-")
.map((el) => `${el[0].toUpperCase()}${el.slice(1)}`)
.join("");
writeFileSync(
path.join(__dirname, `../src/server/routes/v${Number(version)}/${name}-router.ts`),
`import { z } from "zod";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type";
export const register${pascalCase}Router = async (server: FastifyZodProvider) => {
server.route({
url: "/",
method: "GET",
schema: {
params: z.object({}),
response: {
200: z.object({})
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {}
});
};
`
);
}

View File

@ -1,16 +0,0 @@
/* eslint-disable */
import { execSync } from "child_process";
import path from "path";
import promptSync from "prompt-sync";
const prompt = promptSync({ sigint: true });
const migrationName = prompt("Enter name for migration: ");
execSync(
`npx knex migrate:make --knexfile ${path.join(
__dirname,
"../src/db/knexfile.ts"
)} -x ts ${migrationName}`,
{ stdio: "inherit" }
);

View File

@ -1,17 +0,0 @@
/* eslint-disable */
import { execSync } from "child_process";
import { readdirSync } from "fs";
import path from "path";
import promptSync from "prompt-sync";
const prompt = promptSync({ sigint: true });
const migrationName = prompt("Enter name for seedfile: ");
const fileCounter = readdirSync(path.join(__dirname, "../src/db/seed")).length || 1;
execSync(
`npx knex seed:make --knexfile ${path.join(
__dirname,
"../src/db/knexfile.ts"
)} -x ts ${fileCounter}-${migrationName}`,
{ stdio: "inherit" }
);

View File

@ -1,169 +0,0 @@
/* eslint-disable */
import dotenv from "dotenv";
import path from "path";
import knex from "knex";
import { writeFileSync } from "fs";
import promptSync from "prompt-sync";
const prompt = promptSync({ sigint: true });
dotenv.config({
path: path.join(__dirname, "../.env"),
debug: true
});
const db = knex({
client: "pg",
connection: process.env.DB_CONNECTION_URI
});
const getZodPrimitiveType = (type: string) => {
switch (type) {
case "uuid":
return "z.string().uuid()";
case "character varying":
return "z.string()";
case "ARRAY":
return "z.string().array()";
case "boolean":
return "z.boolean()";
case "jsonb":
return "z.unknown()";
case "json":
return "z.unknown()";
case "timestamp with time zone":
return "z.date()";
case "integer":
return "z.number()";
case "bigint":
return "z.coerce.number()";
case "text":
return "z.string()";
default:
throw new Error(`Invalid type: ${type}`);
}
};
const getZodDefaultValue = (type: unknown, value: string | number | boolean | Object) => {
if (!value || value === "null") return;
switch (type) {
case "uuid":
return;
case "character varying": {
if (value === "gen_random_uuid()") return;
if (typeof value === "string" && value.includes("::")) {
return `.default(${value.split("::")[0]})`;
}
return `.default(${value})`;
}
case "ARRAY":
return `.default(${value})`;
case "boolean":
return `.default(${value})`;
case "jsonb":
return "z.string()";
case "json":
return "z.string()";
case "timestamp with time zone": {
if (value === "CURRENT_TIMESTAMP") return;
return "z.string().datetime()";
}
case "integer": {
if ((value as string).includes("nextval")) return;
return `.default(${value})`;
}
case "bigint": {
if ((value as string).includes("nextval")) return;
return `.default(${parseInt((value as string).split("::")[0].slice(1, -1), 10)})`;
}
case "text":
if (typeof value === "string" && value.includes("::")) {
return `.default(${value.split("::")[0]})`;
}
return `.default(${value})`;
default:
throw new Error(`Invalid type: ${type}`);
}
};
const main = async () => {
const tables = (
await db("information_schema.tables")
.whereRaw("table_schema = current_schema()")
.select<{ tableName: string }[]>("table_name as tableName")
.orderBy("table_name")
).filter((el) => !el.tableName.includes("_migrations"));
console.log("Select a table to generate schema");
console.table(tables);
console.log("all: all tables");
const selectedTables = prompt("Type table numbers comma seperated: ");
const tableNumbers =
selectedTables !== "all" ? selectedTables.split(",").map((el) => Number(el)) : [];
for (let i = 0; i < tables.length; i += 1) {
// skip if not desired table
if (selectedTables !== "all" && !tableNumbers.includes(i)) continue;
const { tableName } = tables[i];
const columns = await db(tableName).columnInfo();
const columnNames = Object.keys(columns);
let schema = "";
for (let colNum = 0; colNum < columnNames.length; colNum++) {
const columnName = columnNames[colNum];
const colInfo = columns[columnName];
let ztype = getZodPrimitiveType(colInfo.type);
if (colInfo.defaultValue) {
const { defaultValue } = colInfo;
const zSchema = getZodDefaultValue(colInfo.type, defaultValue);
if (zSchema) {
ztype = ztype.concat(zSchema);
}
}
if (colInfo.nullable) {
ztype = ztype.concat(".nullable().optional()");
}
schema = schema.concat(`${!schema ? "\n" : ""} ${columnName}: ${ztype},\n`);
}
const dashcase = tableName.split("_").join("-");
const pascalCase = tableName
.split("_")
.reduce(
(prev, curr) => prev + `${curr.at(0)?.toUpperCase()}${curr.slice(1).toLowerCase()}`,
""
);
writeFileSync(
path.join(__dirname, "../src/db/schemas", `${dashcase}.ts`),
`// Code generated by automation script, DO NOT EDIT.
// Automated by pulling database and generating zod schema
// To update. Just run npm run generate:schema
// Written by akhilmhdh.
import { z } from "zod";
import { TImmutableDBKeys } from "./models";
export const ${pascalCase}Schema = z.object({${schema}});
export type T${pascalCase} = z.infer<typeof ${pascalCase}Schema>;
export type T${pascalCase}Insert = Omit<T${pascalCase}, TImmutableDBKeys>;
export type T${pascalCase}Update = Partial<Omit<T${pascalCase}, TImmutableDBKeys>>;
`
);
// const file = readFileSync(path.join(__dirname, "../src/db/schemas/index.ts"), "utf8");
// if (!file.includes(`export * from "./${dashcase};"`)) {
// appendFileSync(
// path.join(__dirname, "../src/db/schemas/index.ts"),
// `\nexport * from "./${dashcase}";`,
// "utf8"
// );
// }
}
process.exit(0);
};
main();

8047
backend/spec.json Normal file

File diff suppressed because it is too large Load Diff

View File

@ -1,18 +0,0 @@
import { FastifyInstance, RawReplyDefaultExpression, RawRequestDefaultExpression, RawServerDefault } from "fastify";
import { Logger } from "pino";
import { ZodTypeProvider } from "@app/server/plugins/fastify-zod";
declare global {
type FastifyZodProvider = FastifyInstance<
RawServerDefault,
RawRequestDefaultExpression<RawServerDefault>,
RawReplyDefaultExpression<RawServerDefault>,
Readonly<Logger>,
ZodTypeProvider
>;
// used only for testing
const testServer: FastifyZodProvider;
const jwtAuthToken: string;
}

View File

@ -1,119 +0,0 @@
import "fastify";
import { TUsers } from "@app/db/schemas";
import { TAuditLogServiceFactory } from "@app/ee/services/audit-log/audit-log-service";
import { TCreateAuditLogDTO } from "@app/ee/services/audit-log/audit-log-types";
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { TSamlConfigServiceFactory } from "@app/ee/services/saml-config/saml-config-service";
import { TSecretApprovalPolicyServiceFactory } from "@app/ee/services/secret-approval-policy/secret-approval-policy-service";
import { TSecretApprovalRequestServiceFactory } from "@app/ee/services/secret-approval-request/secret-approval-request-service";
import { TSecretRotationServiceFactory } from "@app/ee/services/secret-rotation/secret-rotation-service";
import { TSecretScanningServiceFactory } from "@app/ee/services/secret-scanning/secret-scanning-service";
import { TSecretSnapshotServiceFactory } from "@app/ee/services/secret-snapshot/secret-snapshot-service";
import { TTrustedIpServiceFactory } from "@app/ee/services/trusted-ip/trusted-ip-service";
import { TAuthMode } from "@app/server/plugins/auth/inject-identity";
import { TApiKeyServiceFactory } from "@app/services/api-key/api-key-service";
import { TAuthLoginFactory } from "@app/services/auth/auth-login-service";
import { TAuthPasswordFactory } from "@app/services/auth/auth-password-service";
import { TAuthSignupFactory } from "@app/services/auth/auth-signup-service";
import { ActorType } from "@app/services/auth/auth-type";
import { TAuthTokenServiceFactory } from "@app/services/auth-token/auth-token-service";
import { TIdentityServiceFactory } from "@app/services/identity/identity-service";
import { TIdentityAccessTokenServiceFactory } from "@app/services/identity-access-token/identity-access-token-service";
import { TIdentityProjectServiceFactory } from "@app/services/identity-project/identity-project-service";
import { TIdentityUaServiceFactory } from "@app/services/identity-ua/identity-ua-service";
import { TIntegrationServiceFactory } from "@app/services/integration/integration-service";
import { TIntegrationAuthServiceFactory } from "@app/services/integration-auth/integration-auth-service";
import { TOrgRoleServiceFactory } from "@app/services/org/org-role-service";
import { TOrgServiceFactory } from "@app/services/org/org-service";
import { TProjectServiceFactory } from "@app/services/project/project-service";
import { TProjectBotServiceFactory } from "@app/services/project-bot/project-bot-service";
import { TProjectEnvServiceFactory } from "@app/services/project-env/project-env-service";
import { TProjectKeyServiceFactory } from "@app/services/project-key/project-key-service";
import { TProjectMembershipServiceFactory } from "@app/services/project-membership/project-membership-service";
import { TProjectRoleServiceFactory } from "@app/services/project-role/project-role-service";
import { TSecretServiceFactory } from "@app/services/secret/secret-service";
import { TSecretBlindIndexServiceFactory } from "@app/services/secret-blind-index/secret-blind-index-service";
import { TSecretFolderServiceFactory } from "@app/services/secret-folder/secret-folder-service";
import { TSecretImportServiceFactory } from "@app/services/secret-import/secret-import-service";
import { TSecretTagServiceFactory } from "@app/services/secret-tag/secret-tag-service";
import { TServiceTokenServiceFactory } from "@app/services/service-token/service-token-service";
import { TSuperAdminServiceFactory } from "@app/services/super-admin/super-admin-service";
import { TTelemetryServiceFactory } from "@app/services/telemetry/telemetry-service";
import { TUserDALFactory } from "@app/services/user/user-dal";
import { TUserServiceFactory } from "@app/services/user/user-service";
import { TWebhookServiceFactory } from "@app/services/webhook/webhook-service";
declare module "fastify" {
interface FastifyRequest {
realIp: string;
// used for mfa session authentication
mfa: {
userId: string;
user: TUsers;
};
// identity injection. depending on which kinda of token the information is filled in auth
auth: TAuthMode;
permission: {
type: ActorType;
id: string;
};
// passport data
passportUser: {
isUserCompleted: string;
providerAuthToken: string;
};
auditLogInfo: Pick<TCreateAuditLogDTO, "userAgent" | "userAgentType" | "ipAddress" | "actor">;
ssoConfig: Awaited<ReturnType<TSamlConfigServiceFactory["getSaml"]>>;
}
interface FastifyInstance {
services: {
login: TAuthLoginFactory;
password: TAuthPasswordFactory;
signup: TAuthSignupFactory;
authToken: TAuthTokenServiceFactory;
permission: TPermissionServiceFactory;
org: TOrgServiceFactory;
orgRole: TOrgRoleServiceFactory;
superAdmin: TSuperAdminServiceFactory;
user: TUserServiceFactory;
apiKey: TApiKeyServiceFactory;
project: TProjectServiceFactory;
projectMembership: TProjectMembershipServiceFactory;
projectEnv: TProjectEnvServiceFactory;
projectKey: TProjectKeyServiceFactory;
projectRole: TProjectRoleServiceFactory;
secret: TSecretServiceFactory;
secretTag: TSecretTagServiceFactory;
secretImport: TSecretImportServiceFactory;
projectBot: TProjectBotServiceFactory;
folder: TSecretFolderServiceFactory;
integration: TIntegrationServiceFactory;
integrationAuth: TIntegrationAuthServiceFactory;
webhook: TWebhookServiceFactory;
serviceToken: TServiceTokenServiceFactory;
identity: TIdentityServiceFactory;
identityAccessToken: TIdentityAccessTokenServiceFactory;
identityProject: TIdentityProjectServiceFactory;
identityUa: TIdentityUaServiceFactory;
secretApprovalPolicy: TSecretApprovalPolicyServiceFactory;
secretApprovalRequest: TSecretApprovalRequestServiceFactory;
secretRotation: TSecretRotationServiceFactory;
snapshot: TSecretSnapshotServiceFactory;
saml: TSamlConfigServiceFactory;
auditLog: TAuditLogServiceFactory;
secretScanning: TSecretScanningServiceFactory;
license: TLicenseServiceFactory;
trustedIp: TTrustedIpServiceFactory;
secretBlindIndex: TSecretBlindIndexServiceFactory;
telemetry: TTelemetryServiceFactory;
};
// this is exclusive use for middlewares in which we need to inject data
// everywhere else access using service layer
store: {
user: Pick<TUserDALFactory, "findById">;
};
}
}

View File

@ -1,343 +0,0 @@
import { Knex } from "knex";
import {
TableName,
TApiKeys,
TApiKeysInsert,
TApiKeysUpdate,
TAuditLogs,
TAuditLogsInsert,
TAuditLogsUpdate,
TAuthTokens,
TAuthTokenSessions,
TAuthTokenSessionsInsert,
TAuthTokenSessionsUpdate,
TAuthTokensInsert,
TAuthTokensUpdate,
TBackupPrivateKey,
TBackupPrivateKeyInsert,
TBackupPrivateKeyUpdate,
TGitAppInstallSessions,
TGitAppInstallSessionsInsert,
TGitAppInstallSessionsUpdate,
TGitAppOrg,
TGitAppOrgInsert,
TGitAppOrgUpdate,
TIdentities,
TIdentitiesInsert,
TIdentitiesUpdate,
TIdentityAccessTokens,
TIdentityAccessTokensInsert,
TIdentityAccessTokensUpdate,
TIdentityOrgMemberships,
TIdentityOrgMembershipsInsert,
TIdentityOrgMembershipsUpdate,
TIdentityProjectMemberships,
TIdentityProjectMembershipsInsert,
TIdentityProjectMembershipsUpdate,
TIdentityUaClientSecrets,
TIdentityUaClientSecretsInsert,
TIdentityUaClientSecretsUpdate,
TIdentityUniversalAuths,
TIdentityUniversalAuthsInsert,
TIdentityUniversalAuthsUpdate,
TIncidentContacts,
TIncidentContactsInsert,
TIncidentContactsUpdate,
TIntegrationAuths,
TIntegrationAuthsInsert,
TIntegrationAuthsUpdate,
TIntegrations,
TIntegrationsInsert,
TIntegrationsUpdate,
TOrganizations,
TOrganizationsInsert,
TOrganizationsUpdate,
TOrgBots,
TOrgBotsInsert,
TOrgBotsUpdate,
TOrgMemberships,
TOrgMembershipsInsert,
TOrgMembershipsUpdate,
TOrgRoles,
TOrgRolesInsert,
TOrgRolesUpdate,
TProjectBots,
TProjectBotsInsert,
TProjectBotsUpdate,
TProjectEnvironments,
TProjectEnvironmentsInsert,
TProjectEnvironmentsUpdate,
TProjectKeys,
TProjectKeysInsert,
TProjectKeysUpdate,
TProjectMemberships,
TProjectMembershipsInsert,
TProjectMembershipsUpdate,
TProjectRoles,
TProjectRolesInsert,
TProjectRolesUpdate,
TProjects,
TProjectsInsert,
TProjectsUpdate,
TSamlConfigs,
TSamlConfigsInsert,
TSamlConfigsUpdate,
TSecretApprovalPolicies,
TSecretApprovalPoliciesApprovers,
TSecretApprovalPoliciesApproversInsert,
TSecretApprovalPoliciesApproversUpdate,
TSecretApprovalPoliciesInsert,
TSecretApprovalPoliciesUpdate,
TSecretApprovalRequests,
TSecretApprovalRequestSecretTags,
TSecretApprovalRequestSecretTagsInsert,
TSecretApprovalRequestSecretTagsUpdate,
TSecretApprovalRequestsInsert,
TSecretApprovalRequestsReviewers,
TSecretApprovalRequestsReviewersInsert,
TSecretApprovalRequestsReviewersUpdate,
TSecretApprovalRequestsSecrets,
TSecretApprovalRequestsSecretsInsert,
TSecretApprovalRequestsSecretsUpdate,
TSecretApprovalRequestsUpdate,
TSecretBlindIndexes,
TSecretBlindIndexesInsert,
TSecretBlindIndexesUpdate,
TSecretFolders,
TSecretFoldersInsert,
TSecretFoldersUpdate,
TSecretFolderVersions,
TSecretFolderVersionsInsert,
TSecretFolderVersionsUpdate,
TSecretImports,
TSecretImportsInsert,
TSecretImportsUpdate,
TSecretRotationOutputs,
TSecretRotationOutputsInsert,
TSecretRotationOutputsUpdate,
TSecretRotations,
TSecretRotationsInsert,
TSecretRotationsUpdate,
TSecrets,
TSecretScanningGitRisks,
TSecretScanningGitRisksInsert,
TSecretScanningGitRisksUpdate,
TSecretsInsert,
TSecretSnapshotFolders,
TSecretSnapshotFoldersInsert,
TSecretSnapshotFoldersUpdate,
TSecretSnapshots,
TSecretSnapshotSecrets,
TSecretSnapshotSecretsInsert,
TSecretSnapshotSecretsUpdate,
TSecretSnapshotsInsert,
TSecretSnapshotsUpdate,
TSecretsUpdate,
TSecretTagJunction,
TSecretTagJunctionInsert,
TSecretTagJunctionUpdate,
TSecretTags,
TSecretTagsInsert,
TSecretTagsUpdate,
TSecretVersions,
TSecretVersionsInsert,
TSecretVersionsUpdate,
TSecretVersionTagJunction,
TSecretVersionTagJunctionInsert,
TSecretVersionTagJunctionUpdate,
TServiceTokens,
TServiceTokensInsert,
TServiceTokensUpdate,
TSuperAdmin,
TSuperAdminInsert,
TSuperAdminUpdate,
TTrustedIps,
TTrustedIpsInsert,
TTrustedIpsUpdate,
TUserActions,
TUserActionsInsert,
TUserActionsUpdate,
TUserEncryptionKeys,
TUserEncryptionKeysInsert,
TUserEncryptionKeysUpdate,
TUsers,
TUsersInsert,
TUsersUpdate,
TWebhooks,
TWebhooksInsert,
TWebhooksUpdate
} from "@app/db/schemas";
declare module "knex/types/tables" {
interface Tables {
[TableName.Users]: Knex.CompositeTableType<TUsers, TUsersInsert, TUsersUpdate>;
[TableName.UserEncryptionKey]: Knex.CompositeTableType<
TUserEncryptionKeys,
TUserEncryptionKeysInsert,
TUserEncryptionKeysUpdate
>;
[TableName.AuthTokens]: Knex.CompositeTableType<TAuthTokens, TAuthTokensInsert, TAuthTokensUpdate>;
[TableName.AuthTokenSession]: Knex.CompositeTableType<
TAuthTokenSessions,
TAuthTokenSessionsInsert,
TAuthTokenSessionsUpdate
>;
[TableName.BackupPrivateKey]: Knex.CompositeTableType<
TBackupPrivateKey,
TBackupPrivateKeyInsert,
TBackupPrivateKeyUpdate
>;
[TableName.Organization]: Knex.CompositeTableType<TOrganizations, TOrganizationsInsert, TOrganizationsUpdate>;
[TableName.OrgMembership]: Knex.CompositeTableType<TOrgMemberships, TOrgMembershipsInsert, TOrgMembershipsUpdate>;
[TableName.OrgRoles]: Knex.CompositeTableType<TOrgRoles, TOrgRolesInsert, TOrgRolesUpdate>;
[TableName.IncidentContact]: Knex.CompositeTableType<
TIncidentContacts,
TIncidentContactsInsert,
TIncidentContactsUpdate
>;
[TableName.UserAction]: Knex.CompositeTableType<TUserActions, TUserActionsInsert, TUserActionsUpdate>;
[TableName.SuperAdmin]: Knex.CompositeTableType<TSuperAdmin, TSuperAdminInsert, TSuperAdminUpdate>;
[TableName.ApiKey]: Knex.CompositeTableType<TApiKeys, TApiKeysInsert, TApiKeysUpdate>;
[TableName.Project]: Knex.CompositeTableType<TProjects, TProjectsInsert, TProjectsUpdate>;
[TableName.ProjectMembership]: Knex.CompositeTableType<
TProjectMemberships,
TProjectMembershipsInsert,
TProjectMembershipsUpdate
>;
[TableName.Environment]: Knex.CompositeTableType<
TProjectEnvironments,
TProjectEnvironmentsInsert,
TProjectEnvironmentsUpdate
>;
[TableName.ProjectBot]: Knex.CompositeTableType<TProjectBots, TProjectBotsInsert, TProjectBotsUpdate>;
[TableName.ProjectRoles]: Knex.CompositeTableType<TProjectRoles, TProjectRolesInsert, TProjectRolesUpdate>;
[TableName.ProjectKeys]: Knex.CompositeTableType<TProjectKeys, TProjectKeysInsert, TProjectKeysUpdate>;
[TableName.Secret]: Knex.CompositeTableType<TSecrets, TSecretsInsert, TSecretsUpdate>;
[TableName.SecretBlindIndex]: Knex.CompositeTableType<
TSecretBlindIndexes,
TSecretBlindIndexesInsert,
TSecretBlindIndexesUpdate
>;
[TableName.SecretVersion]: Knex.CompositeTableType<TSecretVersions, TSecretVersionsInsert, TSecretVersionsUpdate>;
[TableName.SecretFolder]: Knex.CompositeTableType<TSecretFolders, TSecretFoldersInsert, TSecretFoldersUpdate>;
[TableName.SecretFolderVersion]: Knex.CompositeTableType<
TSecretFolderVersions,
TSecretFolderVersionsInsert,
TSecretFolderVersionsUpdate
>;
[TableName.SecretTag]: Knex.CompositeTableType<TSecretTags, TSecretTagsInsert, TSecretTagsUpdate>;
[TableName.SecretImport]: Knex.CompositeTableType<TSecretImports, TSecretImportsInsert, TSecretImportsUpdate>;
[TableName.Integration]: Knex.CompositeTableType<TIntegrations, TIntegrationsInsert, TIntegrationsUpdate>;
[TableName.Webhook]: Knex.CompositeTableType<TWebhooks, TWebhooksInsert, TWebhooksUpdate>;
[TableName.ServiceToken]: Knex.CompositeTableType<TServiceTokens, TServiceTokensInsert, TServiceTokensUpdate>;
[TableName.IntegrationAuth]: Knex.CompositeTableType<
TIntegrationAuths,
TIntegrationAuthsInsert,
TIntegrationAuthsUpdate
>;
[TableName.Identity]: Knex.CompositeTableType<TIdentities, TIdentitiesInsert, TIdentitiesUpdate>;
[TableName.IdentityUniversalAuth]: Knex.CompositeTableType<
TIdentityUniversalAuths,
TIdentityUniversalAuthsInsert,
TIdentityUniversalAuthsUpdate
>;
[TableName.IdentityUaClientSecret]: Knex.CompositeTableType<
TIdentityUaClientSecrets,
TIdentityUaClientSecretsInsert,
TIdentityUaClientSecretsUpdate
>;
[TableName.IdentityAccessToken]: Knex.CompositeTableType<
TIdentityAccessTokens,
TIdentityAccessTokensInsert,
TIdentityAccessTokensUpdate
>;
[TableName.IdentityOrgMembership]: Knex.CompositeTableType<
TIdentityOrgMemberships,
TIdentityOrgMembershipsInsert,
TIdentityOrgMembershipsUpdate
>;
[TableName.IdentityProjectMembership]: Knex.CompositeTableType<
TIdentityProjectMemberships,
TIdentityProjectMembershipsInsert,
TIdentityProjectMembershipsUpdate
>;
[TableName.SecretApprovalPolicy]: Knex.CompositeTableType<
TSecretApprovalPolicies,
TSecretApprovalPoliciesInsert,
TSecretApprovalPoliciesUpdate
>;
[TableName.SecretApprovalPolicyApprover]: Knex.CompositeTableType<
TSecretApprovalPoliciesApprovers,
TSecretApprovalPoliciesApproversInsert,
TSecretApprovalPoliciesApproversUpdate
>;
[TableName.SecretApprovalRequest]: Knex.CompositeTableType<
TSecretApprovalRequests,
TSecretApprovalRequestsInsert,
TSecretApprovalRequestsUpdate
>;
[TableName.SecretApprovalRequestReviewer]: Knex.CompositeTableType<
TSecretApprovalRequestsReviewers,
TSecretApprovalRequestsReviewersInsert,
TSecretApprovalRequestsReviewersUpdate
>;
[TableName.SecretApprovalRequestSecret]: Knex.CompositeTableType<
TSecretApprovalRequestsSecrets,
TSecretApprovalRequestsSecretsInsert,
TSecretApprovalRequestsSecretsUpdate
>;
[TableName.SecretApprovalRequestSecretTag]: Knex.CompositeTableType<
TSecretApprovalRequestSecretTags,
TSecretApprovalRequestSecretTagsInsert,
TSecretApprovalRequestSecretTagsUpdate
>;
[TableName.SecretRotation]: Knex.CompositeTableType<
TSecretRotations,
TSecretRotationsInsert,
TSecretRotationsUpdate
>;
[TableName.SecretRotationOutput]: Knex.CompositeTableType<
TSecretRotationOutputs,
TSecretRotationOutputsInsert,
TSecretRotationOutputsUpdate
>;
[TableName.Snapshot]: Knex.CompositeTableType<TSecretSnapshots, TSecretSnapshotsInsert, TSecretSnapshotsUpdate>;
[TableName.SnapshotSecret]: Knex.CompositeTableType<
TSecretSnapshotSecrets,
TSecretSnapshotSecretsInsert,
TSecretSnapshotSecretsUpdate
>;
[TableName.SnapshotFolder]: Knex.CompositeTableType<
TSecretSnapshotFolders,
TSecretSnapshotFoldersInsert,
TSecretSnapshotFoldersUpdate
>;
[TableName.SamlConfig]: Knex.CompositeTableType<TSamlConfigs, TSamlConfigsInsert, TSamlConfigsUpdate>;
[TableName.OrgBot]: Knex.CompositeTableType<TOrgBots, TOrgBotsInsert, TOrgBotsUpdate>;
[TableName.AuditLog]: Knex.CompositeTableType<TAuditLogs, TAuditLogsInsert, TAuditLogsUpdate>;
[TableName.GitAppInstallSession]: Knex.CompositeTableType<
TGitAppInstallSessions,
TGitAppInstallSessionsInsert,
TGitAppInstallSessionsUpdate
>;
[TableName.GitAppOrg]: Knex.CompositeTableType<TGitAppOrg, TGitAppOrgInsert, TGitAppOrgUpdate>;
[TableName.SecretScanningGitRisk]: Knex.CompositeTableType<
TSecretScanningGitRisks,
TSecretScanningGitRisksInsert,
TSecretScanningGitRisksUpdate
>;
[TableName.TrustedIps]: Knex.CompositeTableType<TTrustedIps, TTrustedIpsInsert, TTrustedIpsUpdate>;
// Junction tables
[TableName.JnSecretTag]: Knex.CompositeTableType<
TSecretTagJunction,
TSecretTagJunctionInsert,
TSecretTagJunctionUpdate
>;
[TableName.SecretVersionTag]: Knex.CompositeTableType<
TSecretVersionTagJunction,
TSecretVersionTagJunctionInsert,
TSecretVersionTagJunctionUpdate
>;
}
}

View File

@ -1 +0,0 @@
declare module "passport-gitlab2";

43
backend/src/bootstrap.ts Normal file
View File

@ -0,0 +1,43 @@
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();
};

View File

@ -1,6 +0,0 @@
import Redis from "ioredis";
export const initRedisConnection = (redisUrl: string) => {
const redis = new Redis(redisUrl);
return redis;
};

173
backend/src/config/index.ts Normal file
View File

@ -0,0 +1,173 @@
import { GITLAB_URL } from "../variables";
import InfisicalClient from "infisical-node";
export const client = new InfisicalClient({
token: process.env.INFISICAL_TOKEN!
});
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;
};

View File

@ -0,0 +1,124 @@
import axios from "axios";
import axiosRetry from "axios-retry";
import {
getLicenseKeyAuthToken,
getLicenseServerKeyAuthToken,
setLicenseKeyAuthToken,
setLicenseServerKeyAuthToken,
} from "./storage";
import {
getLicenseKey,
getLicenseServerKey,
getLicenseServerUrl,
} from "./index";
// should have JWT to interact with the license server
export const licenseServerKeyRequest = axios.create();
export const licenseKeyRequest = axios.create();
export const standardRequest = axios.create();
// add retry functionality to the axios instance
axiosRetry(standardRequest, {
retries: 3,
retryDelay: axiosRetry.exponentialDelay, // exponential back-off delay between retries
retryCondition: (error) => {
// only retry if the error is a network error or a 5xx server error
return axiosRetry.isNetworkError(error) || axiosRetry.isRetryableError(error);
},
});
export const refreshLicenseServerKeyToken = async () => {
const licenseServerKey = await getLicenseServerKey();
const licenseServerUrl = await getLicenseServerUrl();
const { data: { token } } = await standardRequest.post(
`${licenseServerUrl}/api/auth/v1/license-server-login`, {},
{
headers: {
"X-API-KEY": licenseServerKey,
},
}
);
setLicenseServerKeyAuthToken(token);
return token;
}
export const refreshLicenseKeyToken = async () => {
const licenseKey = await getLicenseKey();
const licenseServerUrl = await getLicenseServerUrl();
const { data: { token } } = await standardRequest.post(
`${licenseServerUrl}/api/auth/v1/license-login`, {},
{
headers: {
"X-API-KEY": licenseKey,
},
}
);
setLicenseKeyAuthToken(token);
return token;
}
licenseServerKeyRequest.interceptors.request.use((config) => {
const token = getLicenseServerKeyAuthToken();
if (token && config.headers) {
// eslint-disable-next-line no-param-reassign
config.headers.Authorization = `Bearer ${token}`;
}
return config;
}, (err) => {
return Promise.reject(err);
});
licenseServerKeyRequest.interceptors.response.use((response) => {
return response
}, async function (err) {
const originalRequest = err.config;
if (err.response.status === 401 && !originalRequest._retry) {
originalRequest._retry = true;
// refresh
const token = await refreshLicenseServerKeyToken();
axios.defaults.headers.common["Authorization"] = "Bearer " + token;
return licenseServerKeyRequest(originalRequest);
}
return Promise.reject(err);
});
licenseKeyRequest.interceptors.request.use((config) => {
const token = getLicenseKeyAuthToken();
if (token && config.headers) {
// eslint-disable-next-line no-param-reassign
config.headers.Authorization = `Bearer ${token}`;
}
return config;
}, (err) => {
return Promise.reject(err);
});
licenseKeyRequest.interceptors.response.use((response) => {
return response
}, async function (err) {
const originalRequest = err.config;
if (err.response.status === 401 && !originalRequest._retry) {
originalRequest._retry = true;
// refresh
const token = await refreshLicenseKeyToken();
axios.defaults.headers.common["Authorization"] = "Bearer " + token;
return licenseKeyRequest(originalRequest);
}
return Promise.reject(err);
});

View File

@ -0,0 +1,24 @@
import { IServerConfig, ServerConfig } from "../models/serverConfig";
let serverConfig: IServerConfig;
export const serverConfigInit = async () => {
const cfg = await ServerConfig.findOne({});
if (!cfg) {
const cfg = new ServerConfig();
await cfg.save();
serverConfig = cfg;
} 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;
return serverConfig;
};

View File

@ -0,0 +1,30 @@
const MemoryLicenseServerKeyTokenStorage = () => {
let authToken: string;
return {
setToken: (token: string) => {
authToken = token;
},
getToken: () => authToken,
};
};
const MemoryLicenseKeyTokenStorage = () => {
let authToken: string;
return {
setToken: (token: string) => {
authToken = token;
},
getToken: () => authToken,
};
};
const licenseServerTokenStorage = MemoryLicenseServerKeyTokenStorage();
const licenseTokenStorage = MemoryLicenseKeyTokenStorage();
export const getLicenseServerKeyAuthToken = licenseServerTokenStorage.getToken;
export const setLicenseServerKeyAuthToken = licenseServerTokenStorage.setToken;
export const getLicenseKeyAuthToken = licenseTokenStorage.getToken;
export const setLicenseKeyAuthToken = licenseTokenStorage.setToken;

View File

@ -0,0 +1,100 @@
import { Request, Response } from "express";
import { getHttpsEnabled } 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 = (_req: Request, res: Response) => {
const config = getServerConfig();
return res.send({ config });
};
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
});
};

View File

@ -0,0 +1,277 @@
import { Request, Response } from "express";
import jwt from "jsonwebtoken";
import * as bigintConversion from "bigint-conversion";
// eslint-disable-next-line @typescript-eslint/no-var-requires
const jsrp = require("jsrp");
import {
LoginSRPDetail,
TokenVersion,
User
} from "../../models";
import { clearTokens, createToken, issueAuthTokens } from "../../helpers/auth";
import { checkUserDevice } from "../../helpers/user";
import { AuthTokenType } from "../../variables";
import {
BadRequestError,
UnauthorizedRequestError
} from "../../utils/errors";
import {
getAuthSecret,
getHttpsEnabled,
getJwtAuthLifetime,
} from "../../config";
import { ActorType } from "../../ee/models";
import { validateRequest } from "../../helpers/validation";
import * as reqValidator from "../../validation/auth";
declare module "jsonwebtoken" {
export interface AuthnJwtPayload extends jwt.JwtPayload {
authTokenType: AuthTokenType;
}
export interface UserIDJwtPayload extends jwt.JwtPayload {
userId: string;
refreshVersion?: number;
}
export interface IdentityAccessTokenJwtPayload extends jwt.JwtPayload {
_id: string;
clientSecretId: string;
identityAccessTokenId: string;
authTokenType: string;
}
}
/**
* Log in user step 1: Return [salt] and [serverPublicKey] as part of step 1 of SRP protocol
* @param req
* @param res
* @returns
*/
export const login1 = async (req: Request, res: Response) => {
const {
body: { email, clientPublicKey }
} = await validateRequest(reqValidator.Login1V1, req);
const user = await User.findOne({
email
}).select("+salt +verifier");
if (!user) throw new Error("Failed to find user");
const server = new jsrp.server();
server.init(
{
salt: user.salt,
verifier: user.verifier
},
async () => {
// generate server-side public key
const serverPublicKey = server.getPublicKey();
await LoginSRPDetail.findOneAndReplace(
{ email: email },
{
email: email,
clientPublicKey: clientPublicKey,
serverBInt: bigintConversion.bigintToBuf(server.bInt)
},
{ upsert: true, returnNewDocument: false }
);
return res.status(200).send({
serverPublicKey,
salt: user.salt
});
}
);
};
/**
* Log in user step 2: complete step 2 of SRP protocol and return token and their (encrypted)
* private key
* @param req
* @param res
* @returns
*/
export const login2 = async (req: Request, res: Response) => {
const {
body: { email, clientProof }
} = await validateRequest(reqValidator.Login2V1, req);
const user = await User.findOne({
email
}).select("+salt +verifier +publicKey +encryptedPrivateKey +iv +tag");
if (!user) throw new Error("Failed to find user");
const loginSRPDetailFromDB = await LoginSRPDetail.findOneAndDelete({ email: email });
if (!loginSRPDetailFromDB) {
return BadRequestError(
Error(
"It looks like some details from the first login are not found. Please try login one again"
)
);
}
const server = new jsrp.server();
server.init(
{
salt: user.salt,
verifier: user.verifier,
b: loginSRPDetailFromDB.serverBInt
},
async () => {
server.setClientPublicKey(loginSRPDetailFromDB.clientPublicKey);
// compare server and client shared keys
if (server.checkClientProof(clientProof)) {
// issue tokens
await checkUserDevice({
user,
ip: req.realIP,
userAgent: req.headers["user-agent"] ?? ""
});
const tokens = await issueAuthTokens({
userId: user._id,
ip: req.realIP,
userAgent: req.headers["user-agent"] ?? ""
});
// store (refresh) token in httpOnly cookie
res.cookie("jid", tokens.refreshToken, {
httpOnly: true,
path: "/",
sameSite: "strict",
secure: await getHttpsEnabled()
});
// return (access) token in response
return res.status(200).send({
token: tokens.token,
publicKey: user.publicKey,
encryptedPrivateKey: user.encryptedPrivateKey,
iv: user.iv,
tag: user.tag
});
}
return res.status(400).send({
message: "Failed to authenticate. Try again?"
});
}
);
};
/**
* Log out user
* @param req
* @param res
* @returns
*/
export const logout = async (req: Request, res: Response) => {
if (req.authData.actor.type === ActorType.USER && req.authData.tokenVersionId) {
await clearTokens(req.authData.tokenVersionId);
}
// clear httpOnly cookie
res.cookie("jid", "", {
httpOnly: true,
path: "/",
sameSite: "strict",
secure: (await getHttpsEnabled()) as boolean
});
return res.status(200).send({
message: "Successfully logged out."
});
};
export const revokeAllSessions = async (req: Request, res: Response) => {
await TokenVersion.updateMany(
{
user: req.user._id
},
{
$inc: {
refreshVersion: 1,
accessVersion: 1
}
}
);
return res.status(200).send({
message: "Successfully revoked all sessions."
});
};
/**
* Return user is authenticated
* @param req
* @param res
* @returns
*/
export const checkAuth = async (req: Request, res: Response) => {
return res.status(200).send({
message: "Authenticated"
});
};
/**
* Return new JWT access token by first validating the refresh token
* @param req
* @param res
* @returns
*/
export const getNewToken = async (req: Request, res: Response) => {
const refreshToken = req.cookies.jid;
if (!refreshToken)
throw BadRequestError({
message: "Failed to find refresh token in request cookies"
});
const decodedToken = <jwt.UserIDJwtPayload>jwt.verify(refreshToken, await getAuthSecret());
if (decodedToken.authTokenType !== AuthTokenType.REFRESH_TOKEN) throw UnauthorizedRequestError();
const user = await User.findOne({
_id: decodedToken.userId
}).select("+publicKey +refreshVersion +accessVersion");
if (!user) throw new Error("Failed to authenticate unfound user");
if (!user?.publicKey) throw new Error("Failed to authenticate not fully set up account");
const tokenVersion = await TokenVersion.findById(decodedToken.tokenVersionId);
if (!tokenVersion)
throw UnauthorizedRequestError({
message: "Failed to validate refresh token"
});
if (decodedToken.refreshVersion !== tokenVersion.refreshVersion)
throw BadRequestError({
message: "Failed to validate refresh token"
});
const token = createToken({
payload: {
authTokenType: AuthTokenType.ACCESS_TOKEN,
userId: decodedToken.userId,
tokenVersionId: tokenVersion._id.toString(),
accessVersion: tokenVersion.refreshVersion
},
expiresIn: await getJwtAuthLifetime(),
secret: await getAuthSecret()
});
return res.status(200).send({
token
});
};
export const handleAuthProviderCallback = (req: Request, res: Response) => {
res.redirect(`/login/provider/success?token=${encodeURIComponent(req.providerAuthToken)}`);
};

View File

@ -0,0 +1,135 @@
import { Request, Response } from "express";
import { Types } from "mongoose";
import { Bot, BotKey } from "../../models";
import { createBot } from "../../helpers/bot";
import { validateRequest } from "../../helpers/validation";
import * as reqValidator from "../../validation/bot";
import {
ProjectPermissionActions,
ProjectPermissionSub,
getAuthDataProjectPermissions
} from "../../ee/services/ProjectRoleService";
import { ForbiddenError } from "@casl/ability";
import { BadRequestError } from "../../utils/errors";
interface BotKey {
encryptedKey: string;
nonce: string;
}
/**
* Return bot for workspace with id [workspaceId]. If a workspace bot doesn't exist,
* then create and return a new bot.
* @param req
* @param res
* @returns
*/
export const getBotByWorkspaceId = async (req: Request, res: Response) => {
const {
params: { workspaceId }
} = await validateRequest(reqValidator.GetBotByWorkspaceIdV1, req);
const { permission } = await getAuthDataProjectPermissions({
authData: req.authData,
workspaceId: new Types.ObjectId(workspaceId)
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read,
ProjectPermissionSub.Integrations
);
let bot = await Bot.findOne({
workspace: workspaceId
});
if (!bot) {
// case: bot doesn't exist for workspace with id [workspaceId]
// -> create a new bot and return it
bot = await createBot({
name: "Infisical Bot",
workspaceId: new Types.ObjectId(workspaceId)
});
}
return res.status(200).send({
bot
});
};
/**
* Return bot with id [req.bot._id] with active state set to [isActive].
* @param req
* @param res
* @returns
*/
export const setBotActiveState = async (req: Request, res: Response) => {
const {
body: { botKey, isActive },
params: { botId }
} = await validateRequest(reqValidator.SetBotActiveStateV1, req);
const bot = await Bot.findById(botId);
if (!bot) {
throw BadRequestError({ message: "Bot not found" });
}
const userId = req.user._id;
const { permission } = await getAuthDataProjectPermissions({
authData: req.authData,
workspaceId: bot.workspace
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Edit,
ProjectPermissionSub.Integrations
);
if (isActive) {
// bot state set to active -> share workspace key with bot
if (!botKey?.encryptedKey || !botKey?.nonce) {
return res.status(400).send({
message: "Failed to set bot state to active - missing bot key"
});
}
await BotKey.findOneAndUpdate(
{
workspace: bot.workspace
},
{
encryptedKey: botKey.encryptedKey,
nonce: botKey.nonce,
sender: userId,
bot: bot._id,
workspace: bot.workspace
},
{
upsert: true,
new: true
}
);
} else {
// case: bot state set to inactive -> delete bot's workspace key
await BotKey.deleteOne({
bot: bot._id
});
}
const updatedBot = await Bot.findOneAndUpdate(
{
_id: bot._id
},
{
isActive
},
{
new: true
}
);
if (!updatedBot) throw new Error("Failed to update bot active state");
return res.status(200).send({
bot
});
};

View File

@ -0,0 +1,45 @@
import * as authController from "./authController";
import * as universalAuthController from "./universalAuthController";
import * as ldapController from "./ldapController";
import * as botController from "./botController";
import * as integrationAuthController from "./integrationAuthController";
import * as integrationController from "./integrationController";
import * as keyController from "./keyController";
import * as membershipController from "./membershipController";
import * as membershipOrgController from "./membershipOrgController";
import * as organizationController from "./organizationController";
import * as passwordController from "./passwordController";
import * as secretController from "./secretController";
import * as serviceTokenController from "./serviceTokenController";
import * as signupController from "./signupController";
import * as userActionController from "./userActionController";
import * as userController from "./userController";
import * as workspaceController from "./workspaceController";
import * as secretScanningController from "./secretScanningController";
import * as webhookController from "./webhookController";
import * as secretImpsController from "./secretImpsController";
import * as adminController from "./adminController";
export {
authController,
universalAuthController,
ldapController,
botController,
integrationAuthController,
integrationController,
keyController,
membershipController,
membershipOrgController,
organizationController,
passwordController,
secretController,
serviceTokenController,
signupController,
userActionController,
userController,
workspaceController,
secretScanningController,
webhookController,
secretImpsController,
adminController
};

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,322 @@
import { Request, Response } from "express";
import { Types } from "mongoose";
import { Folder, IWorkspace, Integration, IntegrationAuth } from "../../models";
import { EventService } from "../../services";
import { eventStartIntegration } from "../../events";
import { getFolderByPath } from "../../services/FolderService";
import { BadRequestError } from "../../utils/errors";
import { EEAuditLogService } from "../../ee/services";
import { EventType } from "../../ee/models";
import { syncSecretsToActiveIntegrationsQueue } from "../../queues/integrations/syncSecretsToThirdPartyServices";
import { validateRequest } from "../../helpers/validation";
import * as reqValidator from "../../validation/integration";
import {
ProjectPermissionActions,
ProjectPermissionSub,
getAuthDataProjectPermissions
} from "../../ee/services/ProjectRoleService";
import { ForbiddenError } from "@casl/ability";
/**
* Create/initialize an (empty) integration for integration authorization
* @param req
* @param res
* @returns
*/
export const createIntegration = async (req: Request, res: Response) => {
const {
body: {
isActive,
sourceEnvironment,
secretPath,
app,
path,
appId,
owner,
region,
scope,
targetService,
targetServiceId,
integrationAuthId,
targetEnvironment,
targetEnvironmentId,
metadata
}
} = await validateRequest(reqValidator.CreateIntegrationV1, req);
const integrationAuth = await IntegrationAuth.findById(integrationAuthId)
.populate<{ workspace: IWorkspace }>("workspace")
.select(
"+refreshCiphertext +refreshIV +refreshTag +accessCiphertext +accessIV +accessTag +accessExpiresAt"
);
if (!integrationAuth) throw BadRequestError({ message: "Integration auth not found" });
const { permission } = await getAuthDataProjectPermissions({
authData: req.authData,
workspaceId: integrationAuth.workspace._id
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Create,
ProjectPermissionSub.Integrations
);
const folders = await Folder.findOne({
workspace: integrationAuth.workspace._id,
environment: sourceEnvironment
});
if (folders) {
const folder = getFolderByPath(folders.nodes, secretPath);
if (!folder) {
throw BadRequestError({
message: "Folder path doesn't exist"
});
}
}
// TODO: validate [sourceEnvironment] and [targetEnvironment]
// initialize new integration after saving integration access token
const integration = await new Integration({
workspace: integrationAuth.workspace._id,
environment: sourceEnvironment,
isActive,
app,
appId,
targetEnvironment,
targetEnvironmentId,
targetService,
targetServiceId,
owner,
path,
region,
scope,
secretPath,
integration: integrationAuth.integration,
integrationAuth: new Types.ObjectId(integrationAuthId),
metadata
}).save();
if (integration) {
// trigger event - push secrets
EventService.handleEvent({
event: eventStartIntegration({
workspaceId: integration.workspace,
environment: sourceEnvironment
})
});
}
await EEAuditLogService.createAuditLog(
req.authData,
{
type: EventType.CREATE_INTEGRATION,
metadata: {
integrationId: integration._id.toString(),
integration: integration.integration,
environment: integration.environment,
secretPath,
url: integration.url,
app: integration.app,
appId: integration.appId,
targetEnvironment: integration.targetEnvironment,
targetEnvironmentId: integration.targetEnvironmentId,
targetService: integration.targetService,
targetServiceId: integration.targetServiceId,
path: integration.path,
region: integration.region
}
},
{
workspaceId: integration.workspace
}
);
return res.status(200).send({
integration
});
};
/**
* Change environment or name of integration with id [integrationId]
* @param req
* @param res
* @returns
*/
export const updateIntegration = async (req: Request, res: Response) => {
// TODO: add integration-specific validation to ensure that each
// integration has the correct fields populated in [Integration]
const {
body: {
environment,
isActive,
app,
appId,
targetEnvironment,
owner, // github-specific integration param
secretPath
},
params: { integrationId }
} = await validateRequest(reqValidator.UpdateIntegrationV1, req);
const integration = await Integration.findById(integrationId);
if (!integration) throw BadRequestError({ message: "Integration not found" });
const { permission } = await getAuthDataProjectPermissions({
authData: req.authData,
workspaceId: integration.workspace
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Edit,
ProjectPermissionSub.Integrations
);
const folders = await Folder.findOne({
workspace: integration.workspace,
environment
});
if (folders) {
const folder = getFolderByPath(folders.nodes, secretPath);
if (!folder) {
throw BadRequestError({
message: "Path for service token does not exist"
});
}
}
const updatedIntegration = await Integration.findOneAndUpdate(
{
_id: integration._id
},
{
environment,
isActive,
app,
appId,
targetEnvironment,
owner,
secretPath
},
{
new: true
}
);
if (updatedIntegration) {
// trigger event - push secrets
EventService.handleEvent({
event: eventStartIntegration({
workspaceId: updatedIntegration.workspace,
environment
})
});
}
return res.status(200).send({
integration: updatedIntegration
});
};
/**
* Delete integration with id [integrationId]
* @param req
* @param res
* @returns
*/
export const deleteIntegration = async (req: Request, res: Response) => {
const {
params: { integrationId }
} = await validateRequest(reqValidator.DeleteIntegrationV1, req);
const integration = await Integration.findById(integrationId);
if (!integration) throw BadRequestError({ message: "Integration not found" });
const { permission } = await getAuthDataProjectPermissions({
authData: req.authData,
workspaceId: integration.workspace
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Delete,
ProjectPermissionSub.Integrations
);
const deletedIntegration = await Integration.findOneAndDelete({
_id: integrationId
});
if (!deletedIntegration) throw new Error("Failed to find integration");
const numOtherIntegrationsUsingSameAuth = await Integration.countDocuments({
integrationAuth: deletedIntegration.integrationAuth,
_id: {
$nin: [deletedIntegration._id]
}
});
if (numOtherIntegrationsUsingSameAuth === 0) {
// no other integrations are using the same integration auth
// -> delete integration auth associated with the integration being deleted
await IntegrationAuth.deleteOne({
_id: deletedIntegration.integrationAuth
});
}
await EEAuditLogService.createAuditLog(
req.authData,
{
type: EventType.DELETE_INTEGRATION,
metadata: {
integrationId: integration._id.toString(),
integration: integration.integration,
environment: integration.environment,
secretPath: integration.secretPath,
url: integration.url,
app: integration.app,
appId: integration.appId,
targetEnvironment: integration.targetEnvironment,
targetEnvironmentId: integration.targetEnvironmentId,
targetService: integration.targetService,
targetServiceId: integration.targetServiceId,
path: integration.path,
region: integration.region
}
},
{
workspaceId: integration.workspace
}
);
return res.status(200).send({
integration
});
};
// Will trigger sync for all integrations within the given env and workspace id
export const manualSync = async (req: Request, res: Response) => {
const {
body: { workspaceId, environment }
} = await validateRequest(reqValidator.ManualSyncV1, req);
const { permission } = await getAuthDataProjectPermissions({
authData: req.authData,
workspaceId: new Types.ObjectId(workspaceId)
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Edit,
ProjectPermissionSub.Integrations
);
syncSecretsToActiveIntegrationsQueue({
workspaceId,
environment
});
res.status(200).send();
};

View File

@ -0,0 +1,101 @@
import { Types } from "mongoose";
import { Request, Response } from "express";
import { Key } from "../../models";
import { findMembership } from "../../helpers/membership";
import { EventType } from "../../ee/models";
import { EEAuditLogService } from "../../ee/services";
import { validateRequest } from "../../helpers/validation";
import * as reqValidator from "../../validation/key";
import {
ProjectPermissionActions,
ProjectPermissionSub,
getAuthDataProjectPermissions
} from "../../ee/services/ProjectRoleService";
import { ForbiddenError } from "@casl/ability";
/**
* Add (encrypted) copy of workspace key for workspace with id [workspaceId] for user with
* id [key.userId]
* @param req
* @param res
* @returns
*/
export const uploadKey = async (req: Request, res: Response) => {
const {
params: { workspaceId },
body: { key }
} = await validateRequest(reqValidator.UploadKeyV1, req);
const { permission } = await getAuthDataProjectPermissions({
authData: req.authData,
workspaceId: new Types.ObjectId(workspaceId)
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Edit,
ProjectPermissionSub.Member
);
// validate membership of receiver
const receiverMembership = await findMembership({
user: key.userId,
workspace: workspaceId
});
if (!receiverMembership) {
throw new Error("Failed receiver membership validation for workspace");
}
await new Key({
encryptedKey: key.encryptedKey,
nonce: key.nonce,
sender: req.user._id,
receiver: key.userId,
workspace: workspaceId
}).save();
return res.status(200).send({
message: "Successfully uploaded key to workspace"
});
};
/**
* Return latest (encrypted) copy of workspace key for user
* @param req
* @param res
* @returns
*/
export const getLatestKey = async (req: Request, res: Response) => {
const {
params: { workspaceId }
} = await validateRequest(reqValidator.GetLatestKeyV1, req);
// get latest key
const latestKey = await Key.find({
workspace: workspaceId,
receiver: req.user._id
})
.sort({ createdAt: -1 })
.limit(1)
.populate("sender", "+publicKey");
const resObj: any = {};
if (latestKey.length > 0) {
resObj["latestKey"] = latestKey[0];
await EEAuditLogService.createAuditLog(
req.authData,
{
type: EventType.GET_WORKSPACE_KEY,
metadata: {
keyId: latestKey[0]._id.toString()
}
},
{
workspaceId: new Types.ObjectId(workspaceId)
}
);
}
return res.status(200).send(resObj);
};

View File

@ -0,0 +1,233 @@
import { Request, Response } from "express";
import { Types } from "mongoose";
import { client, getSiteURL } from "../../config";
import * as reqValidator from "../../validation/ldap";
import { validateRequest } from "../../helpers/validation";
import { getLdapConfigHelper } from "../../ee/helpers/organizations";
import {
OrgPermissionActions,
OrgPermissionSubjects,
getAuthDataOrgPermissions
} from "../../ee/services/RoleService";
import { ForbiddenError } from "@casl/ability";
import { LDAPConfig } from "../../ee/models";
import { BotOrgService } from "../../services";
/**
* Return appropriate SSO endpoint after successful authentication with LDAP
* to finish inputting their master key for logging in or signing up
* @param req
* @param res
* @returns
*/
export const redirectLDAP = async (req: Request, res: Response) => {
let nextUrl;
if (req.isUserCompleted) {
nextUrl = `${await getSiteURL()}/login/sso?token=${encodeURIComponent(req.providerAuthToken)}`;
} else {
nextUrl = `${await getSiteURL()}/signup/sso?token=${encodeURIComponent(req.providerAuthToken)}`
}
return res.status(200).send({
nextUrl
});
}
/**
* Return organization LDAP configuration
* @param req
* @param res
*/
export const getLDAPConfig = async (req: Request, res: Response) => {
const {
query: { organizationId }
} = await validateRequest(reqValidator.GetLdapConfigv1, req);
const { permission } = await getAuthDataOrgPermissions({
authData: req.authData,
organizationId: new Types.ObjectId(organizationId)
});
ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionActions.Read,
OrgPermissionSubjects.Sso
);
const data = await getLdapConfigHelper({
organizationId: new Types.ObjectId(organizationId)
});
return res.status(200).send(data);
}
/**
* Update organization LDAP configuration
* @param req
* @param res
* @returns
*/
export const updateLDAPConfig = async (req: Request, res: Response) => {
const {
body: {
organizationId,
isActive,
url,
bindDN,
bindPass,
searchBase,
caCert
}
} = await validateRequest(reqValidator.UpdateLdapConfigv1, req);
const { permission } = await getAuthDataOrgPermissions({
authData: req.authData,
organizationId: new Types.ObjectId(organizationId)
});
ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionActions.Edit,
OrgPermissionSubjects.Sso
);
interface PatchUpdate {
isActive?: boolean;
url?: string;
encryptedBindDN?: string;
bindDNIV?: string;
bindDNTag?: string;
encryptedBindPass?: string;
bindPassIV?: string;
bindPassTag?: string;
searchBase?: string;
encryptedCACert?: string;
caCertIV?: string;
caCertTag?: string;
}
const update: PatchUpdate = {};
if (url) {
update.url = url;
}
if (searchBase) {
update.searchBase = searchBase;
}
if (isActive !== undefined) {
update.isActive = isActive;
}
const key = await BotOrgService.getSymmetricKey(new Types.ObjectId(organizationId));
if (bindDN) {
const {
ciphertext: encryptedBindDN,
iv: bindDNIV,
tag: bindDNTag
} = client.encryptSymmetric(bindDN, key);
update.encryptedBindDN = encryptedBindDN;
update.bindDNIV = bindDNIV;
update.bindDNTag = bindDNTag;
}
if (bindPass) {
const {
ciphertext: encryptedBindPass,
iv: bindPassIV,
tag: bindPassTag
} = client.encryptSymmetric(bindPass, key);
update.encryptedBindPass = encryptedBindPass;
update.bindPassIV = bindPassIV;
update.bindPassTag = bindPassTag;
}
if (caCert) {
const {
ciphertext: encryptedCACert,
iv: caCertIV,
tag: caCertTag
} = client.encryptSymmetric(caCert, key);
update.encryptedCACert = encryptedCACert;
update.caCertIV = caCertIV;
update.caCertTag = caCertTag;
}
const ldapConfig = await LDAPConfig.findOneAndUpdate(
{ organization: new Types.ObjectId(organizationId) },
update,
{ new: true }
);
return res.status(200).send(ldapConfig);
}
/**
* Create organization LDAP configuration
* @param req
* @param res
*/
export const createLDAPConfig = async (req: Request, res: Response) => {
const {
body: {
organizationId,
isActive,
url,
bindDN,
bindPass,
searchBase,
caCert
}
} = await validateRequest(reqValidator.CreateLdapConfigv1, req);
const { permission } = await getAuthDataOrgPermissions({
authData: req.authData,
organizationId: new Types.ObjectId(organizationId)
});
ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionActions.Create,
OrgPermissionSubjects.Sso
);
const key = await BotOrgService.getSymmetricKey(new Types.ObjectId(organizationId));
const {
ciphertext: encryptedBindDN,
iv: bindDNIV,
tag: bindDNTag
} = client.encryptSymmetric(bindDN, key);
const {
ciphertext: encryptedBindPass,
iv: bindPassIV,
tag: bindPassTag
} = client.encryptSymmetric(bindPass, key);
const {
ciphertext: encryptedCACert,
iv: caCertIV,
tag: caCertTag
} = client.encryptSymmetric(caCert, key);
const ldapConfig = await new LDAPConfig({
organization: new Types.ObjectId(organizationId),
isActive,
url,
encryptedBindDN,
bindDNIV,
bindDNTag,
encryptedBindPass,
bindPassIV,
bindPassTag,
searchBase,
encryptedCACert,
caCertIV,
caCertTag
}).save();
return res.status(200).send(ldapConfig);
}

View File

@ -0,0 +1,286 @@
import { Request, Response } from "express";
import { Types } from "mongoose";
import { IUser, Key, Membership, MembershipOrg, User, Workspace } from "../../models";
import { EventType, Role } from "../../ee/models";
import { deleteMembership as deleteMember, findMembership } from "../../helpers/membership";
import { sendMail } from "../../helpers/nodemailer";
import { ACCEPTED, ADMIN, CUSTOM, MEMBER, NO_ACCESS, VIEWER } from "../../variables";
import { getSiteURL } from "../../config";
import { EEAuditLogService, EELicenseService } from "../../ee/services";
import { validateRequest } from "../../helpers/validation";
import * as reqValidator from "../../validation/membership";
import {
ProjectPermissionActions,
ProjectPermissionSub,
getAuthDataProjectPermissions
} from "../../ee/services/ProjectRoleService";
import { ForbiddenError } from "@casl/ability";
import { BadRequestError } from "../../utils/errors";
import { InviteUserToWorkspaceV1 } from "../../validation/workspace";
/**
* Check that user is a member of workspace with id [workspaceId]
* @param req
* @param res
* @returns
*/
export const validateMembership = async (req: Request, res: Response) => {
const {
params: { workspaceId }
} = await validateRequest(reqValidator.ValidateMembershipV1, req);
// validate membership
const membership = await findMembership({
user: req.user._id,
workspace: workspaceId
});
if (!membership) {
throw new Error("Failed to validate membership");
}
return res.status(200).send({
message: "Workspace membership confirmed"
});
};
/**
* Delete membership with id [membershipId]
* @param req
* @param res
* @returns
*/
export const deleteMembership = async (req: Request, res: Response) => {
const {
params: { membershipId }
} = await validateRequest(reqValidator.DeleteMembershipV1, req);
// check if membership to delete exists
const membershipToDelete = await Membership.findOne({
_id: membershipId
}).populate<{ user: IUser }>("user");
if (!membershipToDelete) {
throw new Error("Failed to delete workspace membership that doesn't exist");
}
const { permission } = await getAuthDataProjectPermissions({
authData: req.authData,
workspaceId: membershipToDelete.workspace
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Delete,
ProjectPermissionSub.Member
);
// delete workspace membership
const deletedMembership = await deleteMember({
membershipId: membershipToDelete._id.toString()
});
await EEAuditLogService.createAuditLog(
req.authData,
{
type: EventType.REMOVE_WORKSPACE_MEMBER,
metadata: {
userId: membershipToDelete.user._id.toString(),
email: membershipToDelete.user.email
}
},
{
workspaceId: membershipToDelete.workspace
}
);
return res.status(200).send({
deletedMembership
});
};
/**
* Change and return workspace membership role
* @param req
* @param res
* @returns
*/
export const changeMembershipRole = async (req: Request, res: Response) => {
const {
body: { role },
params: { membershipId }
} = await validateRequest(reqValidator.ChangeMembershipRoleV1, req);
// validate target membership
const membershipToChangeRole = await Membership.findById(membershipId).populate<{ user: IUser }>(
"user"
);
if (!membershipToChangeRole) {
throw new Error("Failed to find membership to change role");
}
const { permission } = await getAuthDataProjectPermissions({
authData: req.authData,
workspaceId: membershipToChangeRole.workspace
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Edit,
ProjectPermissionSub.Member
);
const isCustomRole = ![ADMIN, MEMBER, VIEWER, NO_ACCESS].includes(role);
if (isCustomRole) {
const wsRole = await Role.findOne({
slug: role,
isOrgRole: false,
workspace: membershipToChangeRole.workspace
});
if (!wsRole) throw BadRequestError({ message: "Role not found" });
const plan = await EELicenseService.getPlan(wsRole.organization);
if (!plan.rbac) return res.status(400).send({
message: "Failed to assign custom role due to RBAC restriction. Upgrade plan to assign custom role to member."
});
const membership = await Membership.findByIdAndUpdate(membershipId, {
role: CUSTOM,
customRole: wsRole
});
return res.status(200).send({
membership
});
}
const membership = await Membership.findByIdAndUpdate(
membershipId,
{
$set: {
role
},
$unset: {
customRole: 1
}
},
{
new: true
}
);
await EEAuditLogService.createAuditLog(
req.authData,
{
type: EventType.UPDATE_USER_WORKSPACE_ROLE,
metadata: {
userId: membershipToChangeRole.user._id.toString(),
email: membershipToChangeRole.user.email,
oldRole: membershipToChangeRole.role,
newRole: role
}
},
{
workspaceId: membershipToChangeRole.workspace
}
);
return res.status(200).send({
membership
});
};
/**
* Add user with email [email] to workspace with id [workspaceId]
* @param req
* @param res
* @returns
*/
export const inviteUserToWorkspace = async (req: Request, res: Response) => {
const {
params: { workspaceId },
body: { email }
} = await validateRequest(InviteUserToWorkspaceV1, req);
const { permission } = await getAuthDataProjectPermissions({
authData: req.authData,
workspaceId: new Types.ObjectId(workspaceId)
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Create,
ProjectPermissionSub.Member
);
const invitee = await User.findOne({
email
}).select("+publicKey");
if (!invitee || !invitee?.publicKey) throw new Error("Failed to validate invitee");
// validate invitee's workspace membership - ensure member isn't
// already a member of the workspace
const inviteeMembership = await Membership.findOne({
user: invitee._id,
workspace: workspaceId
}).populate<{ user: IUser }>("user");
if (inviteeMembership) throw new Error("Failed to add existing member of workspace");
const workspace = await Workspace.findById(workspaceId);
if (!workspace) throw new Error("Failed to find workspace");
// validate invitee's organization membership - ensure that only
// (accepted) organization members can be added to the workspace
const membershipOrg = await MembershipOrg.findOne({
user: invitee._id,
organization: workspace.organization,
status: ACCEPTED
});
if (!membershipOrg) throw new Error("Failed to validate invitee's organization membership");
// get latest key
const latestKey = await Key.findOne({
workspace: workspaceId,
receiver: req.user._id
})
.sort({ createdAt: -1 })
.populate("sender", "+publicKey");
// create new workspace membership
await new Membership({
user: invitee._id,
workspace: workspaceId,
role: MEMBER
}).save();
await sendMail({
template: "workspaceInvitation.handlebars",
subjectLine: "Infisical workspace invitation",
recipients: [invitee.email],
substitutions: {
inviterFirstName: req.user.firstName,
inviterEmail: req.user.email,
workspaceName: workspace.name,
callback_url: (await getSiteURL()) + "/login"
}
});
await EEAuditLogService.createAuditLog(
req.authData,
{
type: EventType.ADD_WORKSPACE_MEMBER,
metadata: {
userId: invitee._id.toString(),
email: invitee.email
}
},
{
workspaceId: new Types.ObjectId(workspaceId)
}
);
return res.status(200).send({
invitee,
latestKey
});
};

View File

@ -0,0 +1,292 @@
import { Types } from "mongoose";
import { Request, Response } from "express";
import { MembershipOrg, Organization, User } from "../../models";
import { SSOConfig } from "../../ee/models";
import { deleteMembershipOrg as deleteMemberFromOrg } from "../../helpers/membershipOrg";
import { createToken } from "../../helpers/auth";
import { updateSubscriptionOrgQuantity } from "../../helpers/organization";
import { sendMail } from "../../helpers/nodemailer";
import { TokenService } from "../../services";
import { EELicenseService } from "../../ee/services";
import { ACCEPTED, AuthTokenType, INVITED, MEMBER, TOKEN_EMAIL_ORG_INVITATION } from "../../variables";
import * as reqValidator from "../../validation/membershipOrg";
import {
getAuthSecret,
getJwtSignupLifetime,
getSiteURL,
getSmtpConfigured
} from "../../config";
import { validateUserEmail } from "../../validation";
import { validateRequest } from "../../helpers/validation";
import {
OrgPermissionActions,
OrgPermissionSubjects,
getAuthDataOrgPermissions
} from "../../ee/services/RoleService";
import { ForbiddenError } from "@casl/ability";
/**
* Delete organization membership with id [membershipOrgId] from organization
* @param req
* @param res
* @returns
*/
export const deleteMembershipOrg = async (req: Request, _res: Response) => {
const {
params: { membershipOrgId }
} = await validateRequest(reqValidator.DelOrgMembershipv1, req);
// check if organization membership to delete exists
const membershipOrgToDelete = await MembershipOrg.findOne({
_id: membershipOrgId
}).populate("user");
if (!membershipOrgToDelete) {
throw new Error("Failed to delete organization membership that doesn't exist");
}
const { permission } = await getAuthDataOrgPermissions({
authData: req.authData,
organizationId: membershipOrgToDelete.organization
});
ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionActions.Delete,
OrgPermissionSubjects.Member
);
// delete organization membership
await deleteMemberFromOrg({
membershipOrgId: membershipOrgToDelete._id.toString()
});
await updateSubscriptionOrgQuantity({
organizationId: membershipOrgToDelete.organization.toString()
});
return membershipOrgToDelete;
};
/**
* Change and return organization membership role
* @param req
* @param res
* @returns
*/
export const changeMembershipOrgRole = async (req: Request, res: Response) => {
// change role for (target) organization membership with id
// [membershipOrgId]
let membershipToChangeRole;
return res.status(200).send({
membershipOrg: membershipToChangeRole
});
};
/**
* Organization invitation step 1: Send email invitation to user with email [email]
* for organization with id [organizationId] containing magic link
* @param req
* @param res
* @returns
*/
export const inviteUserToOrganization = async (req: Request, res: Response) => {
let inviteeMembershipOrg, completeInviteLink;
const {
body: { inviteeEmail, organizationId }
} = await validateRequest(reqValidator.InviteUserToOrgv1, req);
const { permission } = await getAuthDataOrgPermissions({
authData: req.authData,
organizationId: new Types.ObjectId(organizationId)
});
ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionActions.Create,
OrgPermissionSubjects.Member
);
const host = req.headers.host;
const siteUrl = `${req.protocol}://${host}`;
const plan = await EELicenseService.getPlan(new Types.ObjectId(organizationId));
const ssoConfig = await SSOConfig.findOne({
organization: new Types.ObjectId(organizationId)
});
if (ssoConfig && ssoConfig.isActive) {
// case: SAML SSO is enabled for the organization
return res.status(400).send({
message: "Failed to invite member due to SAML SSO configured for organization"
});
}
if (plan.memberLimit !== null) {
// case: limit imposed on number of members allowed
if (plan.membersUsed >= plan.memberLimit) {
// case: number of members used exceeds the number of members allowed
return res.status(400).send({
message:
"Failed to invite member due to member limit reached. Upgrade plan to invite more members."
});
}
}
const invitee = await User.findOne({
email: inviteeEmail
}).select("+publicKey");
if (invitee) {
// case: invitee is an existing user
inviteeMembershipOrg = await MembershipOrg.findOne({
user: invitee._id,
organization: organizationId
});
if (inviteeMembershipOrg && inviteeMembershipOrg.status === ACCEPTED) {
throw new Error("Failed to invite an existing member of the organization");
}
if (!inviteeMembershipOrg) {
await new MembershipOrg({
user: invitee,
inviteEmail: inviteeEmail,
organization: organizationId,
role: MEMBER,
status: INVITED
}).save();
}
} else {
// check if invitee has been invited before
inviteeMembershipOrg = await MembershipOrg.findOne({
inviteEmail: inviteeEmail,
organization: organizationId
});
if (!inviteeMembershipOrg) {
// case: invitee has never been invited before
// validate that email is not disposable
validateUserEmail(inviteeEmail);
await new MembershipOrg({
inviteEmail: inviteeEmail,
organization: organizationId,
role: MEMBER,
status: INVITED
}).save();
}
}
const organization = await Organization.findOne({ _id: organizationId });
if (organization) {
const token = await TokenService.createToken({
type: TOKEN_EMAIL_ORG_INVITATION,
email: inviteeEmail,
organizationId: organization._id
});
await sendMail({
template: "organizationInvitation.handlebars",
subjectLine: "Infisical organization invitation",
recipients: [inviteeEmail],
substitutions: {
inviterFirstName: req.user.firstName,
inviterEmail: req.user.email,
organizationName: organization.name,
email: inviteeEmail,
organizationId: organization._id.toString(),
token,
callback_url: (await getSiteURL()) + "/signupinvite"
}
});
if (!(await getSmtpConfigured())) {
completeInviteLink = `${
siteUrl + "/signupinvite"
}?token=${token}&to=${inviteeEmail}&organization_id=${organization._id}`;
}
}
await updateSubscriptionOrgQuantity({ organizationId });
return res.status(200).send({
message: `Sent an invite link to ${req.body.inviteeEmail}`,
completeInviteLink
});
};
/**
* Organization invitation step 2: Verify that code [code] was sent to email [email] as part of
* magic link and issue a temporary signup token for user to complete setting up their account
* @param req
* @param res
* @returns
*/
export const verifyUserToOrganization = async (req: Request, res: Response) => {
let user;
const {
body: { organizationId, email, code }
} = await validateRequest(reqValidator.VerifyUserToOrgv1, req);
user = await User.findOne({ email }).select("+publicKey");
const membershipOrg = await MembershipOrg.findOne({
inviteEmail: email,
status: INVITED,
organization: new Types.ObjectId(organizationId)
});
if (!membershipOrg) throw new Error("Failed to find any invitations for email");
await TokenService.validateToken({
type: TOKEN_EMAIL_ORG_INVITATION,
email,
organizationId: membershipOrg.organization,
token: code
});
if (user && user?.publicKey) {
// case: user has already completed account
// membership can be approved and redirected to login/dashboard
membershipOrg.status = ACCEPTED;
await membershipOrg.save();
await updateSubscriptionOrgQuantity({
organizationId
});
return res.status(200).send({
message: "Successfully verified email",
user
});
}
if (!user) {
// initialize user account
user = await new User({
email
}).save();
}
// generate temporary signup token
const token = createToken({
payload: {
authTokenType: AuthTokenType.SIGNUP_TOKEN,
userId: user._id.toString()
},
expiresIn: await getJwtSignupLifetime(),
secret: await getAuthSecret()
});
return res.status(200).send({
message: "Successfully verified email",
user,
token
});
};

View File

@ -0,0 +1,387 @@
import { Request, Response } from "express";
import { Types } from "mongoose";
import {
IncidentContactOrg,
Membership,
MembershipOrg,
Organization,
Workspace
} from "../../models";
import { getLicenseServerUrl, getSiteURL } from "../../config";
import { licenseServerKeyRequest } from "../../config/request";
import { validateRequest } from "../../helpers/validation";
import * as reqValidator from "../../validation/organization";
import { ACCEPTED } from "../../variables";
import {
OrgPermissionActions,
OrgPermissionSubjects,
getAuthDataOrgPermissions
} from "../../ee/services/RoleService";
import { OrganizationNotFoundError } from "../../utils/errors";
import { ForbiddenError } from "@casl/ability";
export const getOrganizations = async (req: Request, res: Response) => {
const organizations = (
await MembershipOrg.find({
user: req.user._id,
status: ACCEPTED
}).populate("organization")
).map((m) => m.organization);
return res.status(200).send({
organizations
});
};
/**
* Return organization with id [organizationId]
* @param req
* @param res
* @returns
*/
export const getOrganization = async (req: Request, res: Response) => {
const {
params: { organizationId }
} = await validateRequest(reqValidator.GetOrgv1, req);
// ensure user has membership
await getAuthDataOrgPermissions({
authData: req.authData,
organizationId: new Types.ObjectId(organizationId)
})
const organization = await Organization.findById(organizationId);
if (!organization) {
throw OrganizationNotFoundError({
message: "Failed to find organization"
});
}
return res.status(200).send({
organization
});
};
/**
* Return organization memberships for organization with id [organizationId]
* @param req
* @param res
* @returns
*/
export const getOrganizationMembers = async (req: Request, res: Response) => {
const {
params: { organizationId }
} = await validateRequest(reqValidator.GetOrgMembersv1, req);
const { permission } = await getAuthDataOrgPermissions({
authData: req.authData,
organizationId: new Types.ObjectId(organizationId)
});
ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionActions.Read,
OrgPermissionSubjects.Member
);
const users = await MembershipOrg.find({
organization: organizationId
}).populate("user", "+publicKey");
return res.status(200).send({
users
});
};
/**
* Return workspaces that user is part of in organization with id [organizationId]
* @param req
* @param res
* @returns
*/
export const getOrganizationWorkspaces = async (req: Request, res: Response) => {
const {
params: { organizationId }
} = await validateRequest(reqValidator.GetOrgWorkspacesv1, req);
const { permission } = await getAuthDataOrgPermissions({
authData: req.authData,
organizationId: new Types.ObjectId(organizationId)
})
ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionActions.Read,
OrgPermissionSubjects.Workspace
);
const workspacesSet = new Set(
(
await Workspace.find(
{
organization: organizationId
},
"_id"
)
).map((w) => w._id.toString())
);
const workspaces = (
await Membership.find({
user: req.user._id
}).populate("workspace")
)
.filter((m) => workspacesSet.has(m.workspace._id.toString()))
.map((m) => m.workspace);
return res.status(200).send({
workspaces
});
};
/**
* Change name of organization with id [organizationId] to [name]
* @param req
* @param res
* @returns
*/
export const changeOrganizationName = async (req: Request, res: Response) => {
const {
params: { organizationId },
body: { name }
} = await validateRequest(reqValidator.ChangeOrgNamev1, req);
const { permission } = await getAuthDataOrgPermissions({
authData: req.authData,
organizationId: new Types.ObjectId(organizationId)
});
ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionActions.Edit,
OrgPermissionSubjects.Settings
);
const organization = await Organization.findOneAndUpdate(
{
_id: organizationId
},
{
name
},
{
new: true
}
);
return res.status(200).send({
message: "Successfully changed organization name",
organization
});
};
/**
* Return incident contacts of organization with id [organizationId]
* @param req
* @param res
* @returns
*/
export const getOrganizationIncidentContacts = async (req: Request, res: Response) => {
const {
params: { organizationId }
} = await validateRequest(reqValidator.GetOrgIncidentContactv1, req);
const { permission } = await getAuthDataOrgPermissions({
authData: req.authData,
organizationId: new Types.ObjectId(organizationId)
});
ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionActions.Read,
OrgPermissionSubjects.IncidentAccount
);
const incidentContactsOrg = await IncidentContactOrg.find({
organization: organizationId
});
return res.status(200).send({
incidentContactsOrg
});
};
/**
* Add and return new incident contact with email [email] for organization with id [organizationId]
* @param req
* @param res
* @returns
*/
export const addOrganizationIncidentContact = async (req: Request, res: Response) => {
const {
params: { organizationId },
body: { email }
} = await validateRequest(reqValidator.CreateOrgIncideContact, req);
const { permission } = await getAuthDataOrgPermissions({
authData: req.authData,
organizationId: new Types.ObjectId(organizationId)
});
ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionActions.Create,
OrgPermissionSubjects.IncidentAccount
);
const incidentContactOrg = await IncidentContactOrg.findOneAndUpdate(
{ email, organization: organizationId },
{ email, organization: organizationId },
{ upsert: true, new: true }
);
return res.status(200).send({
incidentContactOrg
});
};
/**
* Delete incident contact with email [email] for organization with id [organizationId]
* @param req
* @param res
* @returns
*/
export const deleteOrganizationIncidentContact = async (req: Request, res: Response) => {
const {
params: { organizationId },
body: { email }
} = await validateRequest(reqValidator.DelOrgIncideContact, req);
const { permission } = await getAuthDataOrgPermissions({
authData: req.authData,
organizationId: new Types.ObjectId(organizationId)
});
ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionActions.Delete,
OrgPermissionSubjects.IncidentAccount
);
const incidentContactOrg = await IncidentContactOrg.findOneAndDelete({
email,
organization: organizationId
});
return res.status(200).send({
message: "Successfully deleted organization incident contact",
incidentContactOrg
});
};
/**
* Redirect user to billing portal or add card page depending on
* if there is a card on file
* @param req
* @param res
* @returns
*/
export const createOrganizationPortalSession = async (req: Request, res: Response) => {
const {
params: { organizationId }
} = await validateRequest(reqValidator.GetOrgPlanBillingInfov1, req);
const { permission } = await getAuthDataOrgPermissions({
authData: req.authData,
organizationId: new Types.ObjectId(organizationId)
});
ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionActions.Edit,
OrgPermissionSubjects.Billing
);
const organization = await Organization.findById(organizationId);
if (!organization) {
throw OrganizationNotFoundError({
message: "Failed to find organization"
});
}
const {
data: { pmtMethods }
} = await licenseServerKeyRequest.get(
`${await getLicenseServerUrl()}/api/license-server/v1/customers/${
organization.customerId
}/billing-details/payment-methods`
);
if (pmtMethods.length < 1) {
// case: organization has no payment method on file
// -> redirect to add payment method portal
const {
data: { url }
} = await licenseServerKeyRequest.post(
`${await getLicenseServerUrl()}/api/license-server/v1/customers/${
organization.customerId
}/billing-details/payment-methods`,
{
success_url: (await getSiteURL()) + "/dashboard",
cancel_url: (await getSiteURL()) + "/dashboard"
}
);
return res.status(200).send({ url });
} else {
// case: organization has payment method on file
// -> redirect to billing portal
const {
data: { url }
} = await licenseServerKeyRequest.post(
`${await getLicenseServerUrl()}/api/license-server/v1/customers/${
organization.customerId
}/billing-details/billing-portal`,
{
return_url: (await getSiteURL()) + "/dashboard"
}
);
return res.status(200).send({ url });
}
};
/**
* Given a org id, return the projects each member of the org belongs to
* @param req
* @param res
* @returns
*/
export const getOrganizationMembersAndTheirWorkspaces = async (req: Request, res: Response) => {
const {
params: { organizationId }
} = await validateRequest(reqValidator.GetOrgMembersv1, req);
const { permission } = await getAuthDataOrgPermissions({
authData: req.authData,
organizationId: new Types.ObjectId(organizationId)
});
ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionActions.Read,
OrgPermissionSubjects.Member
);
ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionActions.Read,
OrgPermissionSubjects.Workspace
);
const workspacesSet = (
await Workspace.find(
{
organization: organizationId
},
"_id"
)
).map((w) => w._id.toString());
const memberships = await Membership.find({
workspace: { $in: workspacesSet }
}).populate("workspace");
const userToWorkspaceIds: any = {};
memberships.forEach((membership) => {
const user = membership.user.toString();
if (userToWorkspaceIds[user]) {
userToWorkspaceIds[user].push(membership.workspace);
} else {
userToWorkspaceIds[user] = [membership.workspace];
}
});
return res.json(userToWorkspaceIds);
};

View File

@ -0,0 +1,370 @@
import { Request, Response } from "express";
// eslint-disable-next-line @typescript-eslint/no-var-requires
const jsrp = require("jsrp");
import * as bigintConversion from "bigint-conversion";
import { BackupPrivateKey, LoginSRPDetail, User } from "../../models";
import { clearTokens, createToken, sendMail } from "../../helpers";
import { TokenService } from "../../services";
import { AuthTokenType, TOKEN_EMAIL_PASSWORD_RESET } from "../../variables";
import { BadRequestError } from "../../utils/errors";
import {
getAuthSecret,
getHttpsEnabled,
getJwtSignupLifetime,
getSiteURL
} from "../../config";
import { ActorType } from "../../ee/models";
import { validateRequest } from "../../helpers/validation";
import * as reqValidator from "../../validation/auth";
/**
* Password reset step 1: Send email verification link to email [email]
* for account recovery.
* @param req
* @param res
* @returns
*/
export const emailPasswordReset = async (req: Request, res: Response) => {
const {
body: { email }
} = await validateRequest(reqValidator.EmailPasswordResetV1, req);
const user = await User.findOne({ email }).select("+publicKey");
if (!user || !user?.publicKey) {
// case: user has already completed account
return res.status(200).send({
message: "If an account exists with this email, a password reset link has been sent"
});
}
const token = await TokenService.createToken({
type: TOKEN_EMAIL_PASSWORD_RESET,
email
});
await sendMail({
template: "passwordReset.handlebars",
subjectLine: "Infisical password reset",
recipients: [email],
substitutions: {
email,
token,
callback_url: (await getSiteURL()) + "/password-reset"
}
});
return res.status(200).send({
message: "If an account exists with this email, a password reset link has been sent"
});
};
/**
* Password reset step 2: Verify email verification link sent to email [email]
* @param req
* @param res
* @returns
*/
export const emailPasswordResetVerify = async (req: Request, res: Response) => {
const {
body: { email, code }
} = await validateRequest(reqValidator.EmailPasswordResetVerifyV1, req);
const user = await User.findOne({ email }).select("+publicKey");
if (!user || !user?.publicKey) {
// case: user doesn't exist with email [email] or
// hasn't even completed their account
return res.status(403).send({
error: "Failed email verification for password reset"
});
}
await TokenService.validateToken({
type: TOKEN_EMAIL_PASSWORD_RESET,
email,
token: code
});
// generate temporary password-reset token
const token = createToken({
payload: {
authTokenType: AuthTokenType.SIGNUP_TOKEN,
userId: user._id.toString()
},
expiresIn: await getJwtSignupLifetime(),
secret: await getAuthSecret()
});
return res.status(200).send({
message: "Successfully verified email",
user,
token
});
};
/**
* Return [salt] and [serverPublicKey] as part of step 1 of SRP protocol
* @param req
* @param res
* @returns
*/
export const srp1 = async (req: Request, res: Response) => {
// return salt, serverPublicKey as part of first step of SRP protocol
const {
body: { clientPublicKey }
} = await validateRequest(reqValidator.Srp1V1, req);
const user = await User.findOne({
email: req.user.email
}).select("+salt +verifier");
if (!user) throw new Error("Failed to find user");
const server = new jsrp.server();
server.init(
{
salt: user.salt,
verifier: user.verifier
},
async () => {
// generate server-side public key
const serverPublicKey = server.getPublicKey();
await LoginSRPDetail.findOneAndReplace(
{ email: req.user.email },
{
email: req.user.email,
clientPublicKey: clientPublicKey,
serverBInt: bigintConversion.bigintToBuf(server.bInt)
},
{ upsert: true, returnNewDocument: false }
);
return res.status(200).send({
serverPublicKey,
salt: user.salt
});
}
);
};
/**
* Change account SRP authentication information for user
* Requires verifying [clientProof] as part of step 2 of SRP protocol
* as initiated in POST /srp1
* @param req
* @param res
* @returns
*/
export const changePassword = async (req: Request, res: Response) => {
const {
body: {
clientProof,
protectedKey,
protectedKeyIV,
protectedKeyTag,
encryptedPrivateKey,
encryptedPrivateKeyIV,
encryptedPrivateKeyTag,
salt,
verifier
}
} = await validateRequest(reqValidator.ChangePasswordV1, req);
const user = await User.findOne({
email: req.user.email
}).select("+salt +verifier");
if (!user) throw new Error("Failed to find user");
const loginSRPDetailFromDB = await LoginSRPDetail.findOneAndDelete({ email: req.user.email });
if (!loginSRPDetailFromDB) {
return BadRequestError(
Error(
"It looks like some details from the first login are not found. Please try login one again"
)
);
}
const server = new jsrp.server();
server.init(
{
salt: user.salt,
verifier: user.verifier,
b: loginSRPDetailFromDB.serverBInt
},
async () => {
server.setClientPublicKey(loginSRPDetailFromDB.clientPublicKey);
// compare server and client shared keys
if (server.checkClientProof(clientProof)) {
// change password
await User.findByIdAndUpdate(
req.user._id.toString(),
{
encryptionVersion: 2,
protectedKey,
protectedKeyIV,
protectedKeyTag,
encryptedPrivateKey,
iv: encryptedPrivateKeyIV,
tag: encryptedPrivateKeyTag,
salt,
verifier
},
{
new: true
}
);
if (req.authData.actor.type === ActorType.USER && req.authData.tokenVersionId) {
await clearTokens(req.authData.tokenVersionId);
}
// clear httpOnly cookie
res.cookie("jid", "", {
httpOnly: true,
path: "/",
sameSite: "strict",
secure: (await getHttpsEnabled()) as boolean
});
return res.status(200).send({
message: "Successfully changed password"
});
}
return res.status(400).send({
error: "Failed to change password. Try again?"
});
}
);
};
/**
* Create or change backup private key for user
* @param req
* @param res
* @returns
*/
export const createBackupPrivateKey = async (req: Request, res: Response) => {
// create/change backup private key
// requires verifying [clientProof] as part of second step of SRP protocol
// as initiated in /srp1
const {
body: { clientProof, encryptedPrivateKey, salt, verifier, iv, tag }
} = await validateRequest(reqValidator.CreateBackupPrivateKeyV1, req);
const user = await User.findOne({
email: req.user.email
}).select("+salt +verifier");
if (!user) throw new Error("Failed to find user");
const loginSRPDetailFromDB = await LoginSRPDetail.findOneAndDelete({ email: req.user.email });
if (!loginSRPDetailFromDB) {
return BadRequestError(
Error(
"It looks like some details from the first login are not found. Please try login one again"
)
);
}
const server = new jsrp.server();
server.init(
{
salt: user.salt,
verifier: user.verifier,
b: loginSRPDetailFromDB.serverBInt
},
async () => {
server.setClientPublicKey(loginSRPDetailFromDB.clientPublicKey);
// compare server and client shared keys
if (server.checkClientProof(clientProof)) {
// create new or replace backup private key
const backupPrivateKey = await BackupPrivateKey.findOneAndUpdate(
{ user: req.user._id },
{
user: req.user._id,
encryptedPrivateKey,
iv,
tag,
salt,
verifier
},
{ upsert: true, new: true }
).select("+user, encryptedPrivateKey");
// issue tokens
return res.status(200).send({
message: "Successfully updated backup private key",
backupPrivateKey
});
}
return res.status(400).send({
message: "Failed to update backup private key"
});
}
);
};
/**
* Return backup private key for user
* @param req
* @param res
* @returns
*/
export const getBackupPrivateKey = async (req: Request, res: Response) => {
const backupPrivateKey = await BackupPrivateKey.findOne({
user: req.user._id
}).select("+encryptedPrivateKey +iv +tag");
if (!backupPrivateKey) throw new Error("Failed to find backup private key");
return res.status(200).send({
backupPrivateKey
});
};
export const resetPassword = async (req: Request, res: Response) => {
const {
body: {
encryptedPrivateKey,
protectedKeyTag,
protectedKey,
protectedKeyIV,
salt,
verifier,
encryptedPrivateKeyIV,
encryptedPrivateKeyTag
}
} = await validateRequest(reqValidator.ResetPasswordV1, req);
await User.findByIdAndUpdate(
req.user._id.toString(),
{
encryptionVersion: 2,
protectedKey,
protectedKeyIV,
protectedKeyTag,
encryptedPrivateKey,
iv: encryptedPrivateKeyIV,
tag: encryptedPrivateKeyTag,
salt,
verifier
},
{
new: true
}
);
return res.status(200).send({
message: "Successfully reset password"
});
};

View File

@ -0,0 +1,209 @@
import { Request, Response } from "express";
import { Types } from "mongoose";
import { Key } from "../../models";
import {
pullSecrets as pull,
v1PushSecrets as push,
reformatPullSecrets
} from "../../helpers/secret";
import { pushKeys } from "../../helpers/key";
import { eventPushSecrets } from "../../events";
import { EventService } from "../../services";
import { TelemetryService } from "../../services";
interface PushSecret {
ciphertextKey: string;
ivKey: string;
tagKey: string;
hashKey: string;
ciphertextValue: string;
ivValue: string;
tagValue: string;
hashValue: string;
ciphertextComment: string;
ivComment: string;
tagComment: string;
hashComment: string;
type: "shared" | "personal";
}
/**
* Upload (encrypted) secrets to workspace with id [workspaceId]
* for environment [environment]
* @param req
* @param res
* @returns
*/
export const pushSecrets = async (req: Request, res: Response) => {
// upload (encrypted) secrets to workspace with id [workspaceId]
const postHogClient = await TelemetryService.getPostHogClient();
let { secrets }: { secrets: PushSecret[] } = req.body;
const { keys, environment, channel } = req.body;
const { workspaceId } = req.params;
// validate environment
const workspaceEnvs = req.membership.workspace.environments;
if (!workspaceEnvs.find(({ slug }: { slug: string }) => slug === environment)) {
throw new Error("Failed to validate environment");
}
// sanitize secrets
secrets = secrets.filter((s: PushSecret) => s.ciphertextKey !== "" && s.ciphertextValue !== "");
await push({
userId: req.user._id,
workspaceId,
environment,
secrets
});
await pushKeys({
userId: req.user._id,
workspaceId,
keys
});
if (postHogClient) {
postHogClient.capture({
event: "secrets pushed",
distinctId: req.user.email,
properties: {
numberOfSecrets: secrets.length,
environment,
workspaceId,
channel: channel ? channel : "cli"
}
});
}
// trigger event - push secrets
EventService.handleEvent({
event: eventPushSecrets({
workspaceId: new Types.ObjectId(workspaceId),
environment,
secretPath: "/"
})
});
return res.status(200).send({
message: "Successfully uploaded workspace secrets"
});
};
/**
* Return (encrypted) secrets for workspace with id [workspaceId]
* for environment [environment] and (encrypted) workspace key
* @param req
* @param res
* @returns
*/
export const pullSecrets = async (req: Request, res: Response) => {
let secrets;
const postHogClient = await TelemetryService.getPostHogClient();
const environment: string = req.query.environment as string;
const channel: string = req.query.channel as string;
const { workspaceId } = req.params;
// validate environment
const workspaceEnvs = req.membership.workspace.environments;
if (!workspaceEnvs.find(({ slug }: { slug: string }) => slug === environment)) {
throw new Error("Failed to validate environment");
}
secrets = await pull({
userId: req.user._id.toString(),
workspaceId,
environment,
channel: channel ? channel : "cli",
ipAddress: req.realIP
});
const key = await Key.findOne({
workspace: workspaceId,
receiver: req.user._id
})
.sort({ createdAt: -1 })
.populate("sender", "+publicKey");
if (channel !== "cli") {
secrets = reformatPullSecrets({ secrets });
}
if (postHogClient) {
// capture secrets pushed event in production
postHogClient.capture({
distinctId: req.user.email,
event: "secrets pulled",
properties: {
numberOfSecrets: secrets.length,
environment,
workspaceId,
channel: channel ? channel : "cli"
}
});
}
return res.status(200).send({
secrets,
key
});
};
/**
* Return (encrypted) secrets for workspace with id [workspaceId]
* for environment [environment] and (encrypted) workspace key
* via service token
* @param req
* @param res
* @returns
*/
export const pullSecretsServiceToken = async (req: Request, res: Response) => {
const postHogClient = await TelemetryService.getPostHogClient();
const environment: string = req.query.environment as string;
const channel: string = req.query.channel as string;
const { workspaceId } = req.params;
// validate environment
const workspaceEnvs = req.membership.workspace.environments;
if (!workspaceEnvs.find(({ slug }: { slug: string }) => slug === environment)) {
throw new Error("Failed to validate environment");
}
const secrets = await pull({
userId: req.serviceToken.user._id.toString(),
workspaceId,
environment,
channel: "cli",
ipAddress: req.realIP
});
const key = {
encryptedKey: req.serviceToken.encryptedKey,
nonce: req.serviceToken.nonce,
sender: {
publicKey: req.serviceToken.publicKey
},
receiver: req.serviceToken.user,
workspace: req.serviceToken.workspace
};
if (postHogClient) {
// capture secrets pulled event in production
postHogClient.capture({
distinctId: req.serviceToken.user.email,
event: "secrets pulled",
properties: {
numberOfSecrets: secrets.length,
environment,
workspaceId,
channel: channel ? channel : "cli"
}
});
}
return res.status(200).send({
secrets: reformatPullSecrets({ secrets }),
key
});
};

View File

@ -0,0 +1,734 @@
import { Request, Response } from "express";
import { Types } from "mongoose";
import { isValidScope } from "../../helpers";
import { Folder, IServiceTokenData, SecretImport, ServiceTokenData } from "../../models";
import { getAllImportedSecrets } from "../../services/SecretImportService";
import { getFolderByPath, getFolderWithPathFromId } from "../../services/FolderService";
import {
BadRequestError,
ResourceNotFoundError,
UnauthorizedRequestError
} from "../../utils/errors";
import { EEAuditLogService } from "../../ee/services";
import { EventType } from "../../ee/models";
import { validateRequest } from "../../helpers/validation";
import * as reqValidator from "../../validation/secretImports";
import {
ProjectPermissionActions,
ProjectPermissionSub,
getAuthDataProjectPermissions
} from "../../ee/services/ProjectRoleService";
import { ForbiddenError, subject } from "@casl/ability";
export const createSecretImp = async (req: Request, res: Response) => {
/*
#swagger.summary = 'Create secret import'
#swagger.description = 'Create secret import'
#swagger.requestBody = {
content: {
"application/json": {
"schema": {
"type": "object",
"properties": {
"workspaceId": {
"type": "string",
"description": "ID of workspace where to create secret import",
"example": "someWorkspaceId"
},
"environment": {
"type": "string",
"description": "Slug of environment where to create secret import",
"example": "dev"
},
"directory": {
"type": "string",
"description": "Path where to create secret import like / or /foo/bar. Default is /",
"example": "/foo/bar"
},
"secretImport": {
"type": "object",
"properties": {
"environment": {
"type": "string",
"description": "Slug of environment to import from",
"example": "development"
},
"secretPath": {
"type": "string",
"description": "Path where to import from like / or /foo/bar.",
"example": "/user/oauth"
}
}
}
},
"required": ["workspaceId", "environment", "directory", "secretImport"]
}
}
}
}
#swagger.responses[200] = {
content: {
"application/json": {
"schema": {
"type": "object",
"properties": {
"message": {
"type": "string",
"example": "successfully created secret import"
}
},
"description": "Confirmation of secret import creation"
}
}
}
}
#swagger.responses[400] = {
description: "Bad Request. For example, 'Secret import already exist'"
}
#swagger.responses[401] = {
description: "Unauthorized request. For example, 'Folder Permission Denied'"
}
#swagger.responses[404] = {
description: "Resource Not Found. For example, 'Failed to find folder'"
}
*/
const {
body: { workspaceId, environment, directory, secretImport }
} = await validateRequest(reqValidator.CreateSecretImportV1, req);
if (req.authData.authPayload instanceof ServiceTokenData) {
// root check
const isValidScopeAccess = isValidScope(req.authData.authPayload, environment, directory);
if (!isValidScopeAccess) {
throw UnauthorizedRequestError({ message: "Folder Permission Denied" });
}
} else {
const { permission } = await getAuthDataProjectPermissions({
authData: req.authData,
workspaceId: new Types.ObjectId(workspaceId)
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Create,
subject(ProjectPermissionSub.Secrets, { environment, secretPath: directory })
);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Create,
subject(ProjectPermissionSub.Secrets, { environment: secretImport.environment, secretPath: secretImport.secretPath })
);
}
const folders = await Folder.findOne({
workspace: workspaceId,
environment
}).lean();
if (!folders && directory !== "/")
throw ResourceNotFoundError({ message: "Failed to find folder" });
let folderId = "root";
if (folders) {
const folder = getFolderByPath(folders.nodes, directory);
if (!folder) throw BadRequestError({ message: "Folder not found" });
folderId = folder.id;
}
const importSecDoc = await SecretImport.findOne({
workspace: workspaceId,
environment,
folderId
});
if (!importSecDoc) {
const doc = new SecretImport({
workspace: workspaceId,
environment,
folderId,
imports: [{ environment: secretImport.environment, secretPath: secretImport.secretPath }]
});
await doc.save();
await EEAuditLogService.createAuditLog(
req.authData,
{
type: EventType.CREATE_SECRET_IMPORT,
metadata: {
secretImportId: doc._id.toString(),
folderId: doc.folderId.toString(),
importFromEnvironment: secretImport.environment,
importFromSecretPath: secretImport.secretPath,
importToEnvironment: environment,
importToSecretPath: directory
}
},
{
workspaceId: doc.workspace
}
);
return res.status(200).json({ message: "successfully created secret import" });
}
const doesImportExist = importSecDoc.imports.find(
(el) => el.environment === secretImport.environment && el.secretPath === secretImport.secretPath
);
if (doesImportExist) {
throw BadRequestError({ message: "Secret import already exist" });
}
importSecDoc.imports.push({
environment: secretImport.environment,
secretPath: secretImport.secretPath
});
await importSecDoc.save();
await EEAuditLogService.createAuditLog(
req.authData,
{
type: EventType.CREATE_SECRET_IMPORT,
metadata: {
secretImportId: importSecDoc._id.toString(),
folderId: importSecDoc.folderId.toString(),
importFromEnvironment: secretImport.environment,
importFromSecretPath: secretImport.secretPath,
importToEnvironment: environment,
importToSecretPath: directory
}
},
{
workspaceId: importSecDoc.workspace
}
);
return res.status(200).json({ message: "successfully created secret import" });
};
// to keep the ordering, you must pass all the imports in here not the only updated one
// this is because the order decide which import gets overriden
/**
* Update secret import
* @param req
* @param res
* @returns
*/
export const updateSecretImport = async (req: Request, res: Response) => {
/*
#swagger.summary = 'Update secret import'
#swagger.description = 'Update secret import'
#swagger.parameters['id'] = {
in: 'path',
description: 'ID of secret import to update',
required: true,
type: 'string',
example: 'import12345'
}
#swagger.requestBody = {
content: {
"application/json": {
"schema": {
"type": "object",
"properties": {
"secretImports": {
"type": "array",
"description": "List of secret imports to update to",
"items": {
"type": "object",
"properties": {
"environment": {
"type": "string",
"description": "Slug of environment to import from",
"example": "dev"
},
"secretPath": {
"type": "string",
"description": "Path where to import secrets from like / or /foo/bar",
"example": "/foo/bar"
}
},
"required": ["environment", "secretPath"]
}
}
},
"required": ["secretImports"]
}
}
}
}
#swagger.responses[200] = {
description: 'Successfully updated the secret import',
content: {
"application/json": {
"schema": {
"type": "object",
"properties": {
"message": {
"type": "string",
"example": "successfully updated secret import"
}
}
}
}
}
}
#swagger.responses[400] = {
description: 'Bad Request - Import not found',
}
#swagger.responses[403] = {
description: 'Forbidden access due to insufficient permissions',
}
#swagger.responses[401] = {
description: 'Unauthorized access due to invalid token or scope',
}
*/
const {
body: { secretImports },
params: { id }
} = await validateRequest(reqValidator.UpdateSecretImportV1, req);
const importSecDoc = await SecretImport.findById(id);
if (!importSecDoc) {
throw BadRequestError({ message: "Import not found" });
}
// check for service token validity
const folders = await Folder.findOne({
workspace: importSecDoc.workspace,
environment: importSecDoc.environment
}).lean();
let secretPath = "/";
if (folders) {
const { folderPath } = getFolderWithPathFromId(folders.nodes, importSecDoc.folderId);
secretPath = folderPath;
}
if (req.authData.authPayload instanceof ServiceTokenData) {
// token permission check
const isValidScopeAccess = isValidScope(
req.authData.authPayload,
importSecDoc.environment,
secretPath
);
if (!isValidScopeAccess) {
throw UnauthorizedRequestError({ message: "Folder Permission Denied" });
}
} else {
// non token entry check
const { permission } = await getAuthDataProjectPermissions({
authData: req.authData,
workspaceId: importSecDoc.workspace
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Edit,
subject(ProjectPermissionSub.Secrets, {
environment: importSecDoc.environment,
secretPath
})
);
secretImports.forEach(({ environment, secretPath }) => {
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Create,
subject(ProjectPermissionSub.Secrets, { environment, secretPath })
);
})
}
const orderBefore = importSecDoc.imports;
importSecDoc.imports = secretImports;
await importSecDoc.save();
await EEAuditLogService.createAuditLog(
req.authData,
{
type: EventType.UPDATE_SECRET_IMPORT,
metadata: {
importToEnvironment: importSecDoc.environment,
importToSecretPath: secretPath,
secretImportId: importSecDoc._id.toString(),
folderId: importSecDoc.folderId.toString(),
orderBefore,
orderAfter: secretImports
}
},
{
workspaceId: importSecDoc.workspace
}
);
return res.status(200).json({ message: "successfully updated secret import" });
};
/**
* Delete secret import
* @param req
* @param res
* @returns
*/
export const deleteSecretImport = async (req: Request, res: Response) => {
/*
#swagger.summary = 'Delete secret import'
#swagger.description = 'Delete secret import'
#swagger.parameters['id'] = {
in: 'path',
description: 'ID of parent secret import document from which to delete secret import',
required: true,
type: 'string',
example: '12345abcde'
}
#swagger.requestBody = {
content: {
"application/json": {
"schema": {
"type": "object",
"properties": {
"secretImportEnv": {
"type": "string",
"description": "Slug of environment of import to delete",
"example": "someWorkspaceId"
},
"secretImportPath": {
"type": "string",
"description": "Path like / or /foo/bar of import to delete",
"example": "production"
}
},
"required": ["id", "secretImportEnv", "secretImportPath"]
}
}
}
}
#swagger.responses[200] = {
content: {
"application/json": {
"schema": {
"type": "object",
"properties": {
"message": {
"type": "string",
"example": "successfully delete secret import"
}
},
"description": "Confirmation of secret import deletion"
}
}
}
}
*/
const {
params: { id },
body: { secretImportEnv, secretImportPath }
} = await validateRequest(reqValidator.DeleteSecretImportV1, req);
const importSecDoc = await SecretImport.findById(id);
if (!importSecDoc) {
throw BadRequestError({ message: "Import not found" });
}
// check for service token validity
const folders = await Folder.findOne({
workspace: importSecDoc.workspace,
environment: importSecDoc.environment
}).lean();
let secretPath = "/";
if (folders) {
const { folderPath } = getFolderWithPathFromId(folders.nodes, importSecDoc.folderId);
secretPath = folderPath;
}
if (req.authData.authPayload instanceof ServiceTokenData) {
const isValidScopeAccess = isValidScope(
req.authData.authPayload,
importSecDoc.environment,
secretPath
);
if (!isValidScopeAccess) {
throw UnauthorizedRequestError({ message: "Folder Permission Denied" });
}
} else {
const { permission } = await getAuthDataProjectPermissions({
authData: req.authData,
workspaceId: importSecDoc.workspace
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Delete,
subject(ProjectPermissionSub.Secrets, {
environment: importSecDoc.environment,
secretPath
})
);
}
importSecDoc.imports = importSecDoc.imports.filter(
({ environment, secretPath }) =>
!(environment === secretImportEnv && secretPath === secretImportPath)
);
await importSecDoc.save();
await EEAuditLogService.createAuditLog(
req.authData,
{
type: EventType.DELETE_SECRET_IMPORT,
metadata: {
secretImportId: importSecDoc._id.toString(),
folderId: importSecDoc.folderId.toString(),
importFromEnvironment: secretImportEnv,
importFromSecretPath: secretImportPath,
importToEnvironment: importSecDoc.environment,
importToSecretPath: secretPath
}
},
{
workspaceId: importSecDoc.workspace
}
);
return res.status(200).json({ message: "successfully delete secret import" });
};
/**
* Get secret imports
* @param req
* @param res
* @returns
*/
export const getSecretImports = async (req: Request, res: Response) => {
/*
#swagger.summary = 'Get secret imports'
#swagger.description = 'Get secret imports'
#swagger.parameters['workspaceId'] = {
in: 'query',
description: 'ID of workspace where to get secret imports from',
required: true,
type: 'string',
example: 'workspace12345'
}
#swagger.parameters['environment'] = {
in: 'query',
description: 'Slug of environment where to get secret imports from',
required: true,
type: 'string',
example: 'production'
}
#swagger.parameters['directory'] = {
in: 'query',
description: 'Path where to get secret imports from like / or /foo/bar. Default is /',
required: false,
type: 'string',
example: 'folder12345'
}
#swagger.responses[200] = {
description: 'Successfully retrieved secret import',
content: {
"application/json": {
"schema": {
"type": "object",
"properties": {
"secretImport": {
$ref: '#/definitions/SecretImport'
}
}
}
}
}
}
#swagger.responses[403] = {
description: 'Forbidden access due to insufficient permissions',
}
#swagger.responses[401] = {
description: 'Unauthorized access due to invalid token or scope',
}
*/
const {
query: { workspaceId, environment, directory }
} = await validateRequest(reqValidator.GetSecretImportsV1, req);
if (req.authData.authPayload instanceof ServiceTokenData) {
const isValidScopeAccess = isValidScope(req.authData.authPayload, environment, directory);
if (!isValidScopeAccess) {
throw UnauthorizedRequestError({ message: "Folder Permission Denied" });
}
} else {
const { permission } = await getAuthDataProjectPermissions({
authData: req.authData,
workspaceId: new Types.ObjectId(workspaceId)
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read,
subject(ProjectPermissionSub.Secrets, {
environment,
secretPath: directory
})
);
}
const folders = await Folder.findOne({
workspace: workspaceId,
environment
}).lean();
if (!folders && directory !== "/") throw BadRequestError({ message: "Folder not found" });
let folderId = "root";
if (folders) {
const folder = getFolderByPath(folders.nodes, directory);
if (!folder) throw BadRequestError({ message: "Folder not found" });
folderId = folder.id;
}
const importSecDoc = await SecretImport.findOne({
workspace: workspaceId,
environment,
folderId
});
if (!importSecDoc) {
return res.status(200).json({ secretImport: {} });
}
return res.status(200).json({ secretImport: importSecDoc });
};
/**
* Get all secret imports
* @param req
* @param res
* @returns
*/
export const getAllSecretsFromImport = async (req: Request, res: Response) => {
const {
query: { workspaceId, environment, directory }
} = await validateRequest(reqValidator.GetAllSecretsFromImportV1, req);
if (req.authData.authPayload instanceof ServiceTokenData) {
// check for service token validity
const isValidScopeAccess = isValidScope(req.authData.authPayload, environment, directory);
if (!isValidScopeAccess) {
throw UnauthorizedRequestError({ message: "Folder Permission Denied" });
}
} else {
const { permission } = await getAuthDataProjectPermissions({
authData: req.authData,
workspaceId: new Types.ObjectId(workspaceId)
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read,
subject(ProjectPermissionSub.Secrets, {
environment,
secretPath: directory
})
);
}
const folders = await Folder.findOne({
workspace: workspaceId,
environment
}).lean();
if (!folders && directory !== "/") throw BadRequestError({ message: "Folder not found" });
let folderId = "root";
if (folders) {
const folder = getFolderByPath(folders.nodes, directory);
if (!folder) throw BadRequestError({ message: "Folder not found" });
folderId = folder.id;
}
const importSecDoc = await SecretImport.findOne({
workspace: workspaceId,
environment,
folderId
});
if (!importSecDoc) {
return res.status(200).json({ secrets: [] });
}
let secretPath = "/";
if (folders) {
const { folderPath } = getFolderWithPathFromId(folders.nodes, importSecDoc.folderId);
secretPath = folderPath;
}
let permissionCheckFn: (env: string, secPath: string) => boolean; // used to pass as callback function to import secret
if (req.authData.authPayload instanceof ServiceTokenData) {
// check for service token validity
const isValidScopeAccess = isValidScope(
req.authData.authPayload,
importSecDoc.environment,
secretPath
);
if (!isValidScopeAccess) {
throw UnauthorizedRequestError({ message: "Folder Permission Denied" });
}
permissionCheckFn = (env: string, secPath: string) =>
isValidScope(req.authData.authPayload as IServiceTokenData, env, secPath);
} else {
const { permission } = await getAuthDataProjectPermissions({
authData: req.authData,
workspaceId: importSecDoc.workspace
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read,
subject(ProjectPermissionSub.Secrets, {
environment: importSecDoc.environment,
secretPath
})
);
permissionCheckFn = (env: string, secPath: string) =>
permission.can(
ProjectPermissionActions.Read,
subject(ProjectPermissionSub.Secrets, {
environment: env,
secretPath: secPath
})
);
}
await EEAuditLogService.createAuditLog(
req.authData,
{
type: EventType.GET_SECRET_IMPORTS,
metadata: {
environment,
secretImportId: importSecDoc._id.toString(),
folderId,
numberOfImports: importSecDoc.imports.length
}
},
{
workspaceId: importSecDoc.workspace
}
);
const secrets = await getAllImportedSecrets(
workspaceId,
environment,
folderId,
permissionCheckFn
);
return res.status(200).json({ secrets });
};

View File

@ -0,0 +1,193 @@
import { Request, Response } from "express";
import {
GitAppInstallationSession,
GitAppOrganizationInstallation,
GitRisks
} from "../../ee/models";
import crypto from "crypto";
import { Types } from "mongoose";
import { OrganizationNotFoundError, UnauthorizedRequestError } from "../../utils/errors";
import { scanGithubFullRepoForSecretLeaks } from "../../queues/secret-scanning/githubScanFullRepository";
import { getSecretScanningGitAppId, getSecretScanningPrivateKey } from "../../config";
import {
STATUS_RESOLVED_FALSE_POSITIVE,
STATUS_RESOLVED_NOT_REVOKED,
STATUS_RESOLVED_REVOKED
} from "../../ee/models/gitRisks";
import { ProbotOctokit } from "probot";
import { Organization } from "../../models";
import { validateRequest } from "../../helpers/validation";
import * as reqValidator from "../../validation/secretScanning";
import {
OrgPermissionActions,
OrgPermissionSubjects,
getAuthDataOrgPermissions
} from "../../ee/services/RoleService";
import { ForbiddenError } from "@casl/ability";
export const createInstallationSession = async (req: Request, res: Response) => {
const sessionId = crypto.randomBytes(16).toString("hex");
const {
params: { organizationId }
} = await validateRequest(reqValidator.CreateInstalLSessionv1, req);
const organization = await Organization.findById(organizationId);
if (!organization) {
throw OrganizationNotFoundError({
message: "Failed to find organization"
});
}
const { permission } = await getAuthDataOrgPermissions({
authData: req.authData,
organizationId: new Types.ObjectId(organizationId)
});
ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionActions.Create,
OrgPermissionSubjects.SecretScanning
);
await GitAppInstallationSession.findByIdAndUpdate(
organization,
{
organization: organization.id,
sessionId: sessionId,
user: new Types.ObjectId(req.user._id)
},
{ upsert: true }
).lean();
res.send({
sessionId: sessionId
});
};
export const linkInstallationToOrganization = async (req: Request, res: Response) => {
const {
body: { sessionId, installationId }
} = await validateRequest(reqValidator.LinkInstallationToOrgv1, req);
const installationSession = await GitAppInstallationSession.findOneAndDelete({
sessionId: sessionId
});
if (!installationSession) {
throw UnauthorizedRequestError();
}
const { permission } = await getAuthDataOrgPermissions({
authData: req.authData,
organizationId: installationSession.organization
});
ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionActions.Edit,
OrgPermissionSubjects.SecretScanning
);
const installationLink = await GitAppOrganizationInstallation.findOneAndUpdate(
{
organizationId: installationSession.organization
},
{
installationId: installationId,
organizationId: installationSession.organization,
user: installationSession.user
},
{
upsert: true
}
).lean();
const octokit = new ProbotOctokit({
auth: {
appId: await getSecretScanningGitAppId(),
privateKey: await getSecretScanningPrivateKey(),
installationId: installationId.toString()
}
});
const {
data: { repositories }
} = await octokit.apps.listReposAccessibleToInstallation();
for (const repository of repositories) {
scanGithubFullRepoForSecretLeaks({
organizationId: installationSession.organization.toString(),
installationId,
repository: { id: repository.id, fullName: repository.full_name }
});
}
res.json(installationLink);
};
export const getCurrentOrganizationInstallationStatus = async (req: Request, res: Response) => {
const { organizationId } = req.params;
try {
const appInstallation = await GitAppOrganizationInstallation.findOne({
organizationId: organizationId
}).lean();
if (!appInstallation) {
res.json({
appInstallationComplete: false
});
}
res.json({
appInstallationComplete: true
});
} catch {
res.json({
appInstallationComplete: false
});
}
};
export const getRisksForOrganization = async (req: Request, res: Response) => {
const {
params: { organizationId }
} = await validateRequest(reqValidator.GetOrgRisksv1, req);
const { permission } = await getAuthDataOrgPermissions({
authData: req.authData,
organizationId: new Types.ObjectId(organizationId)
});
ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionActions.Read,
OrgPermissionSubjects.SecretScanning
);
const risks = await GitRisks.find({ organization: organizationId })
.sort({ createdAt: -1 })
.lean();
res.json({
risks: risks
});
};
export const updateRisksStatus = async (req: Request, res: Response) => {
const {
params: { organizationId, riskId },
body: { status }
} = await validateRequest(reqValidator.UpdateRiskStatusv1, req);
const { permission } = await getAuthDataOrgPermissions({
authData: req.authData,
organizationId: new Types.ObjectId(organizationId)
});
ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionActions.Edit,
OrgPermissionSubjects.SecretScanning
);
const isRiskResolved =
status == STATUS_RESOLVED_FALSE_POSITIVE ||
status == STATUS_RESOLVED_REVOKED ||
status == STATUS_RESOLVED_NOT_REVOKED
? true
: false;
const risk = await GitRisks.findByIdAndUpdate(riskId, {
status: status,
isResolved: isRiskResolved
}).lean();
res.json(risk);
};

View File

@ -0,0 +1,680 @@
import { ForbiddenError, subject } from "@casl/ability";
import { Request, Response } from "express";
import { Types } from "mongoose";
import { EventType, FolderVersion } from "../../ee/models";
import { EEAuditLogService, EESecretService } from "../../ee/services";
import { isValidScope } from "../../helpers/secrets";
import { validateRequest } from "../../helpers/validation";
import { Secret, ServiceTokenData } from "../../models";
import { Folder } from "../../models/folder";
import {
appendFolder,
getAllFolderIds,
getFolderByPath,
getFolderWithPathFromId,
validateFolderName
} from "../../services/FolderService";
import {
ProjectPermissionActions,
ProjectPermissionSub,
getAuthDataProjectPermissions
} from "../../ee/services/ProjectRoleService";
import { BadRequestError, UnauthorizedRequestError } from "../../utils/errors";
import * as reqValidator from "../../validation/folders";
const ERR_FOLDER_NOT_FOUND = BadRequestError({ message: "The folder doesn't exist" });
// verify workspace id/environment
export const createFolder = async (req: Request, res: Response) => {
/*
#swagger.summary = 'Create folder'
#swagger.description = 'Create folder'
#swagger.security = [{
"apiKeyAuth": [],
"bearerAuth": []
}]
#swagger.requestBody = {
content: {
"application/json": {
"schema": {
"type": "object",
"properties": {
"workspaceId": {
"type": "string",
"description": "ID of the workspace where to create folder",
"example": "someWorkspaceId"
},
"environment": {
"type": "string",
"description": "Slug of environment where to create folder",
"example": "production"
},
"folderName": {
"type": "string",
"description": "Name of folder to create",
"example": "my_folder"
},
"directory": {
"type": "string",
"description": "Path where to create folder like / or /foo/bar. Default is /",
"example": "/foo/bar"
}
},
"required": ["workspaceId", "environment", "folderName"]
}
}
}
}
#swagger.responses[200] = {
content: {
"application/json": {
"schema": {
"type": "object",
"properties": {
"folder": {
"type": "object",
"properties": {
"id": {
"type": "string",
"description": "ID of folder",
"example": "someFolderId"
},
"name": {
"type": "string",
"description": "Name of folder",
"example": "my_folder"
},
"version": {
"type": "number",
"description": "Version of folder",
"example": 1
}
},
"description": "Details of created folder"
}
}
}
}
}
}
#swagger.responses[400] = {
description: "Bad Request. For example, 'Folder name cannot contain spaces. Only underscore and dashes'"
}
#swagger.responses[401] = {
description: "Unauthorized request. For example, 'Folder Permission Denied'"
}
*/
const {
body: { workspaceId, environment, folderName, directory }
} = await validateRequest(reqValidator.CreateFolderV1, req);
if (!validateFolderName(folderName)) {
throw BadRequestError({
message: "Folder name cannot contain spaces. Only underscore and dashes"
});
}
if (req.authData.authPayload instanceof ServiceTokenData) {
// token check
const isValidScopeAccess = isValidScope(req.authData.authPayload, environment, directory);
if (!isValidScopeAccess) {
throw UnauthorizedRequestError({ message: "Folder Permission Denied" });
}
} else {
// user check
const { permission } = await getAuthDataProjectPermissions({
authData: req.authData,
workspaceId: new Types.ObjectId(workspaceId)
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Create,
subject(ProjectPermissionSub.Secrets, { environment, secretPath: directory })
);
}
const folders = await Folder.findOne({
workspace: workspaceId,
environment
}).lean();
// space has no folders initialized
if (!folders) {
const folder = new Folder({
workspace: workspaceId,
environment,
nodes: {
id: "root",
name: "root",
version: 1,
children: []
}
});
const { parent, child } = appendFolder(folder.nodes, { folderName, directory });
await folder.save();
const folderVersion = new FolderVersion({
workspace: workspaceId,
environment,
nodes: parent
});
await folderVersion.save();
await EESecretService.takeSecretSnapshot({
workspaceId: new Types.ObjectId(workspaceId),
environment
});
await EEAuditLogService.createAuditLog(
req.authData,
{
type: EventType.CREATE_FOLDER,
metadata: {
environment,
folderId: child.id,
folderName,
folderPath: directory
}
},
{
workspaceId: new Types.ObjectId(workspaceId)
}
);
return res.json({ folder: { id: child.id, name: folderName } });
}
const { parent, child, hasCreated } = appendFolder(folders.nodes, { folderName, directory });
if (!hasCreated) return res.json({ folder: child });
await Folder.findByIdAndUpdate(folders._id, folders);
const folderVersion = new FolderVersion({
workspace: workspaceId,
environment,
nodes: parent
});
await folderVersion.save();
await EESecretService.takeSecretSnapshot({
workspaceId: new Types.ObjectId(workspaceId),
environment,
folderId: child.id
});
await EEAuditLogService.createAuditLog(
req.authData,
{
type: EventType.CREATE_FOLDER,
metadata: {
environment,
folderId: child.id,
folderName,
folderPath: directory
}
},
{
workspaceId: new Types.ObjectId(workspaceId)
}
);
return res.json({ folder: child });
};
/**
* Update folder with id [folderId]
* @param req
* @param res
* @returns
*/
export const updateFolderById = async (req: Request, res: Response) => {
/*
#swagger.summary = 'Update folder'
#swagger.description = 'Update folder'
#swagger.security = [{
"apiKeyAuth": [],
"bearerAuth": []
}]
#swagger.parameters['folderName'] = {
"description": "Name of folder to update",
"required": true,
"type": "string"
}
#swagger.requestBody = {
content: {
"application/json": {
"schema": {
"type": "object",
"properties": {
"workspaceId": {
"type": "string",
"description": "ID of workspace where to update folder",
"example": "someWorkspaceId"
},
"environment": {
"type": "string",
"description": "Slug of environment where to update folder",
"example": "production"
},
"name": {
"type": "string",
"description": "Name of folder to update to",
"example": "updated_folder_name"
},
"directory": {
"type": "string",
"description": "Path where to update folder like / or /foo/bar. Default is /",
"example": "/foo/bar"
}
},
"required": ["workspaceId", "environment", "name"]
}
}
}
}
#swagger.responses[200] = {
content: {
"application/json": {
"schema": {
"type": "object",
"properties": {
"message": {
"type": "string",
"description": "Success message",
"example": "Successfully updated folder"
},
"folder": {
"type": "object",
"properties": {
"name": {
"type": "string",
"description": "Name of updated folder",
"example": "updated_folder_name"
},
"id": {
"type": "string",
"description": "ID of created folder",
"example": "abc123"
}
},
"description": "Details of the updated folder"
}
}
}
}
}
}
#swagger.responses[400] = {
description: "Bad Request. Reasons can include 'The folder doesn't exist' or 'Folder name cannot contain spaces. Only underscore and dashes'"
}
#swagger.responses[401] = {
description: "Unauthorized request. For example, 'Folder Permission Denied'"
}
*/
const {
body: { workspaceId, environment, name, directory },
params: { folderName }
} = await validateRequest(reqValidator.UpdateFolderV1, req);
if (!validateFolderName(name)) {
throw BadRequestError({
message: "Folder name cannot contain spaces. Only underscore and dashes"
});
}
if (req.authData.authPayload instanceof ServiceTokenData) {
const isValidScopeAccess = isValidScope(req.authData.authPayload, environment, directory);
if (!isValidScopeAccess) {
throw UnauthorizedRequestError({ message: "Folder Permission Denied" });
}
} else {
const { permission } = await getAuthDataProjectPermissions({
authData: req.authData,
workspaceId: new Types.ObjectId(workspaceId)
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Edit,
subject(ProjectPermissionSub.Secrets, { environment, secretPath: directory })
);
}
const folders = await Folder.findOne({ workspace: workspaceId, environment });
if (!folders) {
throw BadRequestError({ message: "The folder doesn't exist" });
}
const parentFolder = getFolderByPath(folders.nodes, directory);
if (!parentFolder) {
throw BadRequestError({ message: "The folder doesn't exist" });
}
const folder = parentFolder.children.find(({ name }) => name === folderName);
if (!folder) throw ERR_FOLDER_NOT_FOUND;
const oldFolderName = folder.name;
parentFolder.version += 1;
folder.name = name;
await Folder.findByIdAndUpdate(folders._id, folders);
const folderVersion = new FolderVersion({
workspace: workspaceId,
environment,
nodes: parentFolder
});
await folderVersion.save();
await EESecretService.takeSecretSnapshot({
workspaceId: new Types.ObjectId(workspaceId),
environment,
folderId: parentFolder.id
});
const { folderPath } = getFolderWithPathFromId(folders.nodes, folder.id);
await EEAuditLogService.createAuditLog(
req.authData,
{
type: EventType.UPDATE_FOLDER,
metadata: {
environment,
folderId: folder.id,
oldFolderName,
newFolderName: name,
folderPath
}
},
{
workspaceId: new Types.ObjectId(workspaceId)
}
);
return res.json({
message: "Successfully updated folder",
folder: { name: folder.name, id: folder.id }
});
};
/**
* Delete folder with id [folderId]
* @param req
* @param res
* @returns
*/
export const deleteFolder = async (req: Request, res: Response) => {
/*
#swagger.summary = 'Delete folder'
#swagger.description = 'Delete folder'
#swagger.security = [{
"apiKeyAuth": [],
"bearerAuth": []
}]
#swagger.parameters['folderName'] = {
"description": "Name of folder to delete",
"required": true,
"type": "string",
"in": "path"
}
#swagger.requestBody = {
content: {
"application/json": {
"schema": {
"type": "object",
"properties": {
"workspaceId": {
"type": "string",
"description": "ID of the workspace where to delete folder",
"example": "someWorkspaceId"
},
"environment": {
"type": "string",
"description": "Slug of environment where to delete folder",
"example": "production"
},
"directory": {
"type": "string",
"description": "Path where to delete folder like / or /foo/bar. Default is /",
"example": "/foo/bar"
}
},
"required": ["workspaceId", "environment"]
}
}
}
}
#swagger.responses[200] = {
content: {
"application/json": {
"schema": {
"type": "object",
"properties": {
"message": {
"type": "string",
"description": "Success message",
"example": "successfully deleted folders"
},
"folders": {
"type": "array",
"items": {
"type": "object",
"properties": {
"id": {
"type": "string",
"description": "ID of deleted folder",
"example": "abc123"
},
"name": {
"type": "string",
"description": "Name of deleted folder",
"example": "someFolderName"
}
}
},
"description": "List of IDs and names of deleted folders"
}
}
}
}
}
}
#swagger.responses[400] = {
description: "Bad Request. Reasons can include 'The folder doesn't exist'"
}
#swagger.responses[401] = {
description: "Unauthorized request. For example, 'Folder Permission Denied'"
}
*/
const {
params: { folderName },
body: { environment, workspaceId, directory }
} = await validateRequest(reqValidator.DeleteFolderV1, req);
if (req.authData.authPayload instanceof ServiceTokenData) {
const isValidScopeAccess = isValidScope(req.authData.authPayload, environment, directory);
if (!isValidScopeAccess) {
throw UnauthorizedRequestError({ message: "Folder Permission Denied" });
}
} else {
// check that user is a member of the workspace
const { permission } = await getAuthDataProjectPermissions({
authData: req.authData,
workspaceId: new Types.ObjectId(workspaceId)
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Delete,
subject(ProjectPermissionSub.Secrets, { environment, secretPath: directory })
);
}
const folders = await Folder.findOne({ workspace: workspaceId, environment });
if (!folders) throw ERR_FOLDER_NOT_FOUND;
const parentFolder = getFolderByPath(folders.nodes, directory);
if (!parentFolder) throw ERR_FOLDER_NOT_FOUND;
const index = parentFolder.children.findIndex(({ name }) => name === folderName);
if (index === -1) throw ERR_FOLDER_NOT_FOUND;
const deletedFolder = parentFolder.children.splice(index, 1)[0];
parentFolder.version += 1;
const delFolderIds = getAllFolderIds(deletedFolder);
await Folder.findByIdAndUpdate(folders._id, folders);
const folderVersion = new FolderVersion({
workspace: workspaceId,
environment,
nodes: parentFolder
});
await folderVersion.save();
if (delFolderIds.length) {
await Secret.deleteMany({
folder: { $in: delFolderIds.map(({ id }) => id) },
workspace: workspaceId,
environment
});
}
await EESecretService.takeSecretSnapshot({
workspaceId: new Types.ObjectId(workspaceId),
environment,
folderId: parentFolder.id
});
await EEAuditLogService.createAuditLog(
req.authData,
{
type: EventType.DELETE_FOLDER,
metadata: {
environment,
folderId: deletedFolder.id,
folderName: deletedFolder.name,
folderPath: directory
}
},
{
workspaceId: new Types.ObjectId(workspaceId)
}
);
return res.send({ message: "successfully deleted folders", folders: delFolderIds });
};
/**
* Get folders for workspace with id [workspaceId] and environment [environment]
* considering directory/path [directory]
* @param req
* @param res
* @returns
*/
export const getFolders = async (req: Request, res: Response) => {
/*
#swagger.summary = 'Get folders'
#swagger.description = 'Get folders'
#swagger.security = [{
"apiKeyAuth": [],
"bearerAuth": []
}]
#swagger.parameters['workspaceId'] = {
"description": "ID of the workspace where to get folders from",
"required": true,
"type": "string",
"in": "query"
}
#swagger.parameters['environment'] = {
"description": "Slug of environment where to get folders from",
"required": true,
"type": "string",
"in": "query"
}
#swagger.parameters['directory'] = {
"description": "Path where to get fodlers from like / or /foo/bar. Default is /",
"required": false,
"type": "string",
"in": "query"
}
#swagger.responses[200] = {
content: {
"application/json": {
"schema": {
"type": "object",
"properties": {
"folders": {
"type": "array",
"items": {
"type": "object",
"properties": {
"id": {
"type": "string",
"example": "someFolderId"
},
"name": {
"type": "string",
"example": "someFolderName"
}
}
},
"description": "List of folders"
}
}
}
}
}
}
#swagger.responses[400] = {
description: "Bad Request. For instance, 'The folder doesn't exist'"
}
#swagger.responses[401] = {
description: "Unauthorized request. For example, 'Folder Permission Denied'"
}
*/
const {
query: { workspaceId, environment, directory }
} = await validateRequest(reqValidator.GetFoldersV1, req);
if (req.authData.authPayload instanceof ServiceTokenData) {
const isValidScopeAccess = isValidScope(req.authData.authPayload, environment, directory);
if (!isValidScopeAccess) {
throw UnauthorizedRequestError({ message: "Folder Permission Denied" });
}
} else {
// check that user is a member of the workspace
await getAuthDataProjectPermissions({
authData: req.authData,
workspaceId: new Types.ObjectId(workspaceId)
});
}
const folders = await Folder.findOne({ workspace: workspaceId, environment });
if (!folders) {
return res.send({ folders: [], dir: [] });
}
const folder = getFolderByPath(folders.nodes, directory);
return res.send({
folders: folder?.children?.map(({ id, name }) => ({ id, name })) || []
});
};

View File

@ -0,0 +1,75 @@
import { Request, Response } from "express";
import { ServiceToken } from "../../models";
import { createToken } from "../../helpers/auth";
import { getJwtServiceSecret } from "../../config";
/**
* Return service token on request
* @param req
* @param res
* @returns
*/
export const getServiceToken = async (req: Request, res: Response) => {
return res.status(200).send({
serviceToken: req.serviceToken,
});
};
/**
* Create and return a new service token
* @param req
* @param res
* @returns
*/
export const createServiceToken = async (req: Request, res: Response) => {
let token;
try {
const {
name,
workspaceId,
environment,
expiresIn,
publicKey,
encryptedKey,
nonce,
} = req.body;
// validate environment
const workspaceEnvs = req.membership.workspace.environments;
if (!workspaceEnvs.find(({ slug }: { slug: string }) => slug === environment)) {
throw new Error("Failed to validate environment");
}
// compute access token expiration date
const expiresAt = new Date();
expiresAt.setSeconds(expiresAt.getSeconds() + expiresIn);
const serviceToken = await new ServiceToken({
name,
user: req.user._id,
workspace: workspaceId,
environment,
expiresAt,
publicKey,
encryptedKey,
nonce,
}).save();
token = createToken({
payload: {
serviceTokenId: serviceToken._id.toString(),
workspaceId,
},
expiresIn: expiresIn,
secret: await getJwtServiceSecret(),
});
} catch (err) {
return res.status(400).send({
message: "Failed to create service token",
});
}
return res.status(200).send({
token,
});
};

View File

@ -0,0 +1,99 @@
import { Request, Response } from "express";
import { AuthMethod, User } from "../../models";
import { checkEmailVerification, sendEmailVerification } from "../../helpers/signup";
import { createToken } from "../../helpers/auth";
import {
getAuthSecret,
getJwtSignupLifetime,
getSmtpConfigured
} from "../../config";
import { validateUserEmail } from "../../validation";
import { validateRequest } from "../../helpers/validation";
import * as reqValidator from "../../validation/auth";
import { AuthTokenType } from "../../variables";
/**
* Signup step 1: Initialize account for user under email [email] and send a verification code
* to that email
* @param req
* @param res
* @returns
*/
export const beginEmailSignup = async (req: Request, res: Response) => {
const {
body: { email }
} = await validateRequest(reqValidator.BeginEmailSignUpV1, req);
// validate that email is not disposable
validateUserEmail(email);
const user = await User.findOne({ email }).select("+publicKey");
if (user && user?.publicKey) {
// case: user has already completed account
return res.status(403).send({
error: "Failed to send email verification code for complete account"
});
}
// send send verification email
await sendEmailVerification({ email });
return res.status(200).send({
message: `Sent an email verification code to ${email}`
});
};
/**
* Signup step 2: Verify that code [code] was sent to email [email] and issue
* a temporary signup token for user to complete setting up their account
* @param req
* @param res
* @returns
*/
export const verifyEmailSignup = async (req: Request, res: Response) => {
let user;
const {
body: { email, code }
} = await validateRequest(reqValidator.VerifyEmailSignUpV1, req);
// initialize user account
user = await User.findOne({ email }).select("+publicKey");
if (user && user?.publicKey) {
// case: user has already completed account
return res.status(403).send({
error: "Failed email verification for complete user"
});
}
// verify email
if (await getSmtpConfigured()) {
await checkEmailVerification({
email,
code
});
}
if (!user) {
user = await new User({
email,
authMethods: [AuthMethod.EMAIL]
}).save();
}
// generate temporary signup token
const token = createToken({
payload: {
authTokenType: AuthTokenType.SIGNUP_TOKEN,
userId: user._id.toString()
},
expiresIn: await getJwtSignupLifetime(),
secret: await getAuthSecret()
});
return res.status(200).send({
message: "Successfuly verified email",
user,
token
});
};

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,56 @@
import { Request, Response } from "express";
import { validateRequest } from "../../helpers/validation";
import { UserAction } from "../../models";
import * as reqValidator from "../../validation/action";
/**
* Add user action [action]
* @param req
* @param res
* @returns
*/
export const addUserAction = async (req: Request, res: Response) => {
// add/record new action [action] for user with id [req.user._id]
const {
body: { action }
} = await validateRequest(reqValidator.AddUserActionV1, req);
const userAction = await UserAction.findOneAndUpdate(
{
user: req.user._id,
action
},
{ user: req.user._id, action },
{
new: true,
upsert: true
}
);
return res.status(200).send({
message: "Successfully recorded user action",
userAction
});
};
/**
* Return user action [action] for user
* @param req
* @param res
* @returns
*/
export const getUserAction = async (req: Request, res: Response) => {
// get user action [action] for user with id [req.user._id]
const {
query: { action }
} = await validateRequest(reqValidator.GetUserActionV1, req);
const userAction = await UserAction.findOne({
user: req.user._id,
action
});
return res.status(200).send({
userAction
});
};

View File

@ -0,0 +1,13 @@
import { Request, Response } from "express";
/**
* Return user on request
* @param req
* @param res
* @returns
*/
export const getUser = async (req: Request, res: Response) => {
return res.status(200).send({
user: req.user,
});
};

View File

@ -0,0 +1,268 @@
import { Request, Response } from "express";
import { Types } from "mongoose";
import { client, getEncryptionKey, getRootEncryptionKey } from "../../config";
import { Webhook } from "../../models";
import { getWebhookPayload, triggerWebhookRequest } from "../../services/WebhookService";
import { BadRequestError, ResourceNotFoundError } from "../../utils/errors";
import { EEAuditLogService } from "../../ee/services";
import { EventType } from "../../ee/models";
import {
ALGORITHM_AES_256_GCM,
ENCODING_SCHEME_BASE64,
ENCODING_SCHEME_UTF8
} from "../../variables";
import { validateRequest } from "../../helpers/validation";
import * as reqValidator from "../../validation/webhooks";
import {
ProjectPermissionActions,
ProjectPermissionSub,
getAuthDataProjectPermissions
} from "../../ee/services/ProjectRoleService";
import { ForbiddenError } from "@casl/ability";
import { encryptSymmetric128BitHexKeyUTF8 } from "../../utils/crypto";
export const createWebhook = async (req: Request, res: Response) => {
const {
body: { webhookUrl, webhookSecretKey, environment, workspaceId, secretPath }
} = await validateRequest(reqValidator.CreateWebhookV1, req);
const { permission } = await getAuthDataProjectPermissions({
authData: req.authData,
workspaceId: new Types.ObjectId(workspaceId)
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Create,
ProjectPermissionSub.Webhooks
);
const webhook = new Webhook({
workspace: workspaceId,
environment,
secretPath,
url: webhookUrl
});
if (webhookSecretKey) {
const encryptionKey = await getEncryptionKey();
const rootEncryptionKey = await getRootEncryptionKey();
if (rootEncryptionKey) {
const { ciphertext, iv, tag } = client.encryptSymmetric(webhookSecretKey, rootEncryptionKey);
webhook.iv = iv;
webhook.tag = tag;
webhook.encryptedSecretKey = ciphertext;
webhook.algorithm = ALGORITHM_AES_256_GCM;
webhook.keyEncoding = ENCODING_SCHEME_BASE64;
} else if (encryptionKey) {
const { ciphertext, iv, tag } = encryptSymmetric128BitHexKeyUTF8({
plaintext: webhookSecretKey,
key: encryptionKey
});
webhook.iv = iv;
webhook.tag = tag;
webhook.encryptedSecretKey = ciphertext;
webhook.algorithm = ALGORITHM_AES_256_GCM;
webhook.keyEncoding = ENCODING_SCHEME_UTF8;
}
}
await webhook.save();
await EEAuditLogService.createAuditLog(
req.authData,
{
type: EventType.CREATE_WEBHOOK,
metadata: {
webhookId: webhook._id.toString(),
environment,
secretPath,
webhookUrl,
isDisabled: false
}
},
{
workspaceId: new Types.ObjectId(workspaceId)
}
);
return res.status(200).send({
webhook,
message: "successfully created webhook"
});
};
export const updateWebhook = async (req: Request, res: Response) => {
const {
body: { isDisabled },
params: { webhookId }
} = await validateRequest(reqValidator.UpdateWebhookV1, req);
const webhook = await Webhook.findById(webhookId);
if (!webhook) {
throw BadRequestError({ message: "Webhook not found!!" });
}
const { permission } = await getAuthDataProjectPermissions({
authData: req.authData,
workspaceId: webhook.workspace
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Edit,
ProjectPermissionSub.Webhooks
);
if (typeof isDisabled !== undefined) {
webhook.isDisabled = isDisabled;
}
await webhook.save();
await EEAuditLogService.createAuditLog(
req.authData,
{
type: EventType.UPDATE_WEBHOOK_STATUS,
metadata: {
webhookId: webhook._id.toString(),
environment: webhook.environment,
secretPath: webhook.secretPath,
webhookUrl: webhook.url,
isDisabled
}
},
{
workspaceId: webhook.workspace
}
);
return res.status(200).send({
webhook,
message: "successfully updated webhook"
});
};
export const deleteWebhook = async (req: Request, res: Response) => {
const {
params: { webhookId }
} = await validateRequest(reqValidator.DeleteWebhookV1, req);
let webhook = await Webhook.findById(webhookId);
if (!webhook) {
throw ResourceNotFoundError({ message: "Webhook not found!!" });
}
const { permission } = await getAuthDataProjectPermissions({
authData: req.authData,
workspaceId: webhook.workspace
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Delete,
ProjectPermissionSub.Webhooks
);
webhook = await Webhook.findByIdAndDelete(webhookId);
if (!webhook) {
throw ResourceNotFoundError({ message: "Webhook not found!!" });
}
await EEAuditLogService.createAuditLog(
req.authData,
{
type: EventType.DELETE_WEBHOOK,
metadata: {
webhookId: webhook._id.toString(),
environment: webhook.environment,
secretPath: webhook.secretPath,
webhookUrl: webhook.url,
isDisabled: webhook.isDisabled
}
},
{
workspaceId: webhook.workspace
}
);
return res.status(200).send({
message: "successfully removed webhook"
});
};
export const testWebhook = async (req: Request, res: Response) => {
const {
params: { webhookId }
} = await validateRequest(reqValidator.TestWebhookV1, req);
const webhook = await Webhook.findById(webhookId);
if (!webhook) {
throw BadRequestError({ message: "Webhook not found!!" });
}
const { permission } = await getAuthDataProjectPermissions({
authData: req.authData,
workspaceId: webhook.workspace
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read,
ProjectPermissionSub.Webhooks
);
try {
await triggerWebhookRequest(
webhook,
getWebhookPayload(
"test",
webhook.workspace.toString(),
webhook.environment,
webhook.secretPath
)
);
await Webhook.findByIdAndUpdate(webhookId, {
lastStatus: "success",
lastRunErrorMessage: null
});
} catch (err) {
await Webhook.findByIdAndUpdate(webhookId, {
lastStatus: "failed",
lastRunErrorMessage: (err as Error).message
});
return res.status(400).send({
message: "Failed to receive response",
error: (err as Error).message
});
}
return res.status(200).send({
message: "Successfully received response"
});
};
export const listWebhooks = async (req: Request, res: Response) => {
const {
query: { environment, workspaceId, secretPath }
} = await validateRequest(reqValidator.ListWebhooksV1, req);
const { permission } = await getAuthDataProjectPermissions({
authData: req.authData,
workspaceId: new Types.ObjectId(workspaceId)
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read,
ProjectPermissionSub.Webhooks
);
const optionalFilters: Record<string, string> = {};
if (environment) optionalFilters.environment = environment as string;
if (secretPath) optionalFilters.secretPath = secretPath as string;
const webhooks = await Webhook.find({
workspace: new Types.ObjectId(workspaceId as string),
...optionalFilters
});
return res.status(200).send({
webhooks
});
};

View File

@ -0,0 +1,359 @@
import { Types } from "mongoose";
import { Request, Response } from "express";
import {
IUser,
Integration,
IntegrationAuth,
Membership,
Organization,
ServiceToken,
Workspace
} from "../../models";
import { createWorkspace as create, deleteWorkspace as deleteWork } from "../../helpers/workspace";
import { EELicenseService } from "../../ee/services";
import { addMemberships } from "../../helpers/membership";
import { ADMIN } from "../../variables";
import { OrganizationNotFoundError } from "../../utils/errors";
import {
OrgPermissionActions,
OrgPermissionSubjects,
getAuthDataOrgPermissions
} from "../../ee/services/RoleService";
import { ForbiddenError } from "@casl/ability";
import { validateRequest } from "../../helpers/validation";
import * as reqValidator from "../../validation";
import {
ProjectPermissionActions,
ProjectPermissionSub,
getAuthDataProjectPermissions
} from "../../ee/services/ProjectRoleService";
/**
* Return public keys of members of workspace with id [workspaceId]
* @param req
* @param res
* @returns
*/
export const getWorkspacePublicKeys = async (req: Request, res: Response) => {
const {
params: { workspaceId }
} = await validateRequest(reqValidator.GetWorkspacePublicKeysV1, req);
const { permission } = await getAuthDataProjectPermissions({
authData: req.authData,
workspaceId: new Types.ObjectId(workspaceId)
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read,
ProjectPermissionSub.Member
);
const publicKeys = (
await Membership.find({
workspace: workspaceId
}).populate<{ user: IUser }>("user", "publicKey")
).map((member) => {
return {
publicKey: member.user.publicKey,
userId: member.user._id
};
});
return res.status(200).send({
publicKeys
});
};
/**
* Return memberships for workspace with id [workspaceId]
* @param req
* @param res
* @returns
*/
export const getWorkspaceMemberships = async (req: Request, res: Response) => {
const {
params: { workspaceId }
} = await validateRequest(reqValidator.GetWorkspaceMembershipsV1, req);
const { permission } = await getAuthDataProjectPermissions({
authData: req.authData,
workspaceId: new Types.ObjectId(workspaceId)
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read,
ProjectPermissionSub.Member
);
const users = await Membership.find({
workspace: workspaceId
}).populate("user", "+publicKey");
return res.status(200).send({
users
});
};
/**
* Return workspaces that user is part of
* @param req
* @param res
* @returns
*/
export const getWorkspaces = async (req: Request, res: Response) => {
const workspaces = (
await Membership.find({
user: req.user._id
}).populate("workspace")
).map((m) => m.workspace);
return res.status(200).send({
workspaces
});
};
/**
* Return workspace with id [workspaceId]
* @param req
* @param res
* @returns
*/
export const getWorkspace = async (req: Request, res: Response) => {
const {
params: { workspaceId }
} = await validateRequest(reqValidator.GetWorkspaceV1, req);
const workspace = await Workspace.findOne({
_id: workspaceId
});
return res.status(200).send({
workspace
});
};
/**
* Create new workspace named [workspaceName] under organization with id
* [organizationId] and add user as admin
* @param req
* @param res
* @returns
*/
export const createWorkspace = async (req: Request, res: Response) => {
const {
body: { organizationId, workspaceName }
} = await validateRequest(reqValidator.CreateWorkspaceV1, req);
const organization = await Organization.findById(organizationId);
if (!organization) {
throw OrganizationNotFoundError({
message: "Failed to find organization"
});
}
const { permission } = await getAuthDataOrgPermissions({
authData: req.authData,
organizationId: new Types.ObjectId(organizationId)
});
ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionActions.Create,
OrgPermissionSubjects.Workspace
);
const plan = await EELicenseService.getPlan(new Types.ObjectId(organizationId));
if (plan.workspaceLimit !== null) {
// case: limit imposed on number of workspaces allowed
if (plan.workspacesUsed >= plan.workspaceLimit) {
// case: number of workspaces used exceeds the number of workspaces allowed
return res.status(400).send({
message:
"Failed to create workspace due to plan limit reached. Upgrade plan to add more workspaces."
});
}
}
if (workspaceName.length < 1) {
throw new Error("Workspace names must be at least 1-character long");
}
// create workspace and add user as member
const workspace = await create({
name: workspaceName,
organizationId: new Types.ObjectId(organizationId)
});
await addMemberships({
userIds: [req.user._id],
workspaceId: workspace._id.toString(),
roles: [ADMIN]
});
return res.status(200).send({
workspace
});
};
/**
* Delete workspace with id [workspaceId]
* @param req
* @param res
* @returns
*/
export const deleteWorkspace = async (req: Request, res: Response) => {
const {
params: { workspaceId }
} = await validateRequest(reqValidator.DeleteWorkspaceV1, req);
const { permission } = await getAuthDataProjectPermissions({
authData: req.authData,
workspaceId: new Types.ObjectId(workspaceId)
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Delete,
ProjectPermissionSub.Workspace
);
// delete workspace
const workspace = await deleteWork({
workspaceId: new Types.ObjectId(workspaceId)
});
return res.status(200).send({
workspace
});
};
/**
* Change name of workspace with id [workspaceId] to [name]
* @param req
* @param res
* @returns
*/
export const changeWorkspaceName = async (req: Request, res: Response) => {
const {
params: { workspaceId },
body: { name }
} = await validateRequest(reqValidator.ChangeWorkspaceNameV1, req);
const { permission } = await getAuthDataProjectPermissions({
authData: req.authData,
workspaceId: new Types.ObjectId(workspaceId)
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Edit,
ProjectPermissionSub.Workspace
);
const workspace = await Workspace.findOneAndUpdate(
{
_id: workspaceId
},
{
name
},
{
new: true
}
);
return res.status(200).send({
message: "Successfully changed workspace name",
workspace
});
};
/**
* Return integrations for workspace with id [workspaceId]
* @param req
* @param res
* @returns
*/
export const getWorkspaceIntegrations = async (req: Request, res: Response) => {
const {
params: { workspaceId }
} = await validateRequest(reqValidator.GetWorkspaceIntegrationsV1, req);
const { permission } = await getAuthDataProjectPermissions({
authData: req.authData,
workspaceId: new Types.ObjectId(workspaceId)
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read,
ProjectPermissionSub.Integrations
);
const integrations = await Integration.find({
workspace: workspaceId
});
return res.status(200).send({
integrations
});
};
/**
* Return (integration) authorizations for workspace with id [workspaceId]
* @param req
* @param res
* @returns
*/
export const getWorkspaceIntegrationAuthorizations = async (req: Request, res: Response) => {
const {
params: { workspaceId }
} = await validateRequest(reqValidator.GetWorkspaceIntegrationAuthorizationsV1, req);
const { permission } = await getAuthDataProjectPermissions({
authData: req.authData,
workspaceId: new Types.ObjectId(workspaceId)
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read,
ProjectPermissionSub.Integrations
);
const authorizations = await IntegrationAuth.find({
workspace: workspaceId
});
return res.status(200).send({
authorizations
});
};
/**
* Return service service tokens for workspace [workspaceId] belonging to user
* @param req
* @param res
* @returns
*/
export const getWorkspaceServiceTokens = async (req: Request, res: Response) => {
const {
params: { workspaceId }
} = await validateRequest(reqValidator.GetWorkspaceServiceTokensV1, req);
const { permission } = await getAuthDataProjectPermissions({
authData: req.authData,
workspaceId: new Types.ObjectId(workspaceId)
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read,
ProjectPermissionSub.ServiceTokens
);
// ?? FIX.
const serviceTokens = await ServiceToken.find({
user: req.user._id,
workspace: workspaceId
});
return res.status(200).send({
serviceTokens
});
};

View File

@ -0,0 +1,315 @@
/* eslint-disable @typescript-eslint/no-var-requires */
import { Request, Response } from "express";
import jwt from "jsonwebtoken";
import * as bigintConversion from "bigint-conversion";
const jsrp = require("jsrp");
import { LoginSRPDetail, User } from "../../models";
import { createToken, issueAuthTokens } from "../../helpers/auth";
import { checkUserDevice } from "../../helpers/user";
import { sendMail } from "../../helpers/nodemailer";
import { TokenService } from "../../services";
import { BadRequestError, InternalServerError } from "../../utils/errors";
import { AuthTokenType, TOKEN_EMAIL_MFA } from "../../variables";
import { getAuthSecret, getHttpsEnabled, getJwtMfaLifetime } from "../../config";
import { validateRequest } from "../../helpers/validation";
import * as reqValidator from "../../validation/auth";
declare module "jsonwebtoken" {
export interface UserIDJwtPayload extends jwt.JwtPayload {
userId: string;
}
}
/**
* Log in user step 1: Return [salt] and [serverPublicKey] as part of step 1 of SRP protocol
* @param req
* @param res
* @returns
*/
export const login1 = async (req: Request, res: Response) => {
const { email, clientPublicKey }: { email: string; clientPublicKey: string } = req.body;
const user = await User.findOne({
email
}).select("+salt +verifier");
if (!user) throw new Error("Failed to find user");
const server = new jsrp.server();
server.init(
{
salt: user.salt,
verifier: user.verifier
},
async () => {
// generate server-side public key
const serverPublicKey = server.getPublicKey();
await LoginSRPDetail.findOneAndReplace(
{ email: email },
{
email: email,
clientPublicKey: clientPublicKey,
serverBInt: bigintConversion.bigintToBuf(server.bInt)
},
{ upsert: true, returnNewDocument: false }
);
return res.status(200).send({
serverPublicKey,
salt: user.salt
});
}
);
};
/**
* Log in user step 2: complete step 2 of SRP protocol and return token and their (encrypted)
* private key
* @param req
* @param res
* @returns
*/
export const login2 = async (req: Request, res: Response) => {
if (!req.headers["user-agent"])
throw InternalServerError({ message: "User-Agent header is required" });
const { email, clientProof } = req.body;
const user = await User.findOne({
email
}).select(
"+salt +verifier +encryptionVersion +protectedKey +protectedKeyIV +protectedKeyTag +publicKey +encryptedPrivateKey +iv +tag +devices"
);
if (!user) throw new Error("Failed to find user");
const loginSRPDetail = await LoginSRPDetail.findOneAndDelete({ email: email });
if (!loginSRPDetail) {
return BadRequestError(Error("Failed to find login details for SRP"));
}
const server = new jsrp.server();
server.init(
{
salt: user.salt,
verifier: user.verifier,
b: loginSRPDetail.serverBInt
},
async () => {
server.setClientPublicKey(loginSRPDetail.clientPublicKey);
// compare server and client shared keys
if (server.checkClientProof(clientProof)) {
if (user.isMfaEnabled) {
// case: user has MFA enabled
// generate temporary MFA token
const token = createToken({
payload: {
authTokenType: AuthTokenType.MFA_TOKEN,
userId: user._id.toString()
},
expiresIn: await getJwtMfaLifetime(),
secret: await getAuthSecret()
});
const code = await TokenService.createToken({
type: TOKEN_EMAIL_MFA,
email
});
// send MFA code [code] to [email]
await sendMail({
template: "emailMfa.handlebars",
subjectLine: "Infisical MFA code",
recipients: [email],
substitutions: {
code
}
});
return res.status(200).send({
mfaEnabled: true,
token
});
}
await checkUserDevice({
user,
ip: req.realIP,
userAgent: req.headers["user-agent"] ?? ""
});
// issue tokens
const tokens = await issueAuthTokens({
userId: user._id,
ip: req.realIP,
userAgent: req.headers["user-agent"] ?? ""
});
// store (refresh) token in httpOnly cookie
res.cookie("jid", tokens.refreshToken, {
httpOnly: true,
path: "/",
sameSite: "strict",
secure: await getHttpsEnabled()
});
// case: user does not have MFA enabled
// return (access) token in response
interface ResponseData {
mfaEnabled: boolean;
encryptionVersion: any;
protectedKey?: string;
protectedKeyIV?: string;
protectedKeyTag?: string;
token: string;
publicKey?: string;
encryptedPrivateKey?: string;
iv?: string;
tag?: string;
}
const response: ResponseData = {
mfaEnabled: false,
encryptionVersion: user.encryptionVersion,
token: tokens.token,
publicKey: user.publicKey,
encryptedPrivateKey: user.encryptedPrivateKey,
iv: user.iv,
tag: user.tag
};
if (user?.protectedKey && user?.protectedKeyIV && user?.protectedKeyTag) {
response.protectedKey = user.protectedKey;
response.protectedKeyIV = user.protectedKeyIV;
response.protectedKeyTag = user.protectedKeyTag;
}
return res.status(200).send(response);
}
return res.status(400).send({
message: "Failed to authenticate. Try again?"
});
}
);
};
/**
* Send MFA token to email [email]
* @param req
* @param res
*/
export const sendMfaToken = async (req: Request, res: Response) => {
const code = await TokenService.createToken({
type: TOKEN_EMAIL_MFA,
email: req.user.email
});
// send MFA code [code] to [email]
await sendMail({
template: "emailMfa.handlebars",
subjectLine: "Infisical MFA code",
recipients: [req.user.email],
substitutions: {
code
}
});
return res.status(200).send({
message: "Successfully sent new MFA code"
});
};
/**
* Verify MFA token [mfaToken] and issue JWT and refresh tokens if the
* MFA token [mfaToken] is valid
* @param req
* @param res
*/
export const verifyMfaToken = async (req: Request, res: Response) => {
const {
body: { mfaToken }
} = await validateRequest(reqValidator.VerifyMfaTokenV2, req);
await TokenService.validateToken({
type: TOKEN_EMAIL_MFA,
email: req.user.email,
token: mfaToken
});
const user = await User.findOne({
email: req.user.email
}).select(
"+salt +verifier +encryptionVersion +protectedKey +protectedKeyIV +protectedKeyTag +publicKey +encryptedPrivateKey +iv +tag +devices"
);
if (!user) throw new Error("Failed to find user");
await LoginSRPDetail.deleteOne({ userId: user.id });
await checkUserDevice({
user,
ip: req.realIP,
userAgent: req.headers["user-agent"] ?? ""
});
// issue tokens
const tokens = await issueAuthTokens({
userId: user._id,
ip: req.realIP,
userAgent: req.headers["user-agent"] ?? ""
});
// store (refresh) token in httpOnly cookie
res.cookie("jid", tokens.refreshToken, {
httpOnly: true,
path: "/",
sameSite: "strict",
secure: await getHttpsEnabled()
});
interface VerifyMfaTokenRes {
encryptionVersion: number;
protectedKey?: string;
protectedKeyIV?: string;
protectedKeyTag?: string;
token: string;
publicKey: string;
encryptedPrivateKey: string;
iv: string;
tag: string;
}
interface VerifyMfaTokenRes {
encryptionVersion: number;
protectedKey?: string;
protectedKeyIV?: string;
protectedKeyTag?: string;
token: string;
publicKey: string;
encryptedPrivateKey: string;
iv: string;
tag: string;
}
const resObj: VerifyMfaTokenRes = {
encryptionVersion: user.encryptionVersion,
token: tokens.token,
publicKey: user.publicKey as string,
encryptedPrivateKey: user.encryptedPrivateKey as string,
iv: user.iv as string,
tag: user.tag as string
};
if (user?.protectedKey && user?.protectedKeyIV && user?.protectedKeyTag) {
resObj.protectedKey = user.protectedKey;
resObj.protectedKeyIV = user.protectedKeyIV;
resObj.protectedKeyTag = user.protectedKeyTag;
}
return res.status(200).send(resObj);
};

View File

@ -0,0 +1,604 @@
import { Request, Response } from "express";
import { Types } from "mongoose";
import {
Folder,
Integration,
Membership,
Secret,
ServiceToken,
ServiceTokenData,
Workspace
} from "../../models";
import { EventType, SecretVersion } from "../../ee/models";
import { EEAuditLogService, EELicenseService } from "../../ee/services";
import { BadRequestError, WorkspaceNotFoundError } from "../../utils/errors";
import { validateRequest } from "../../helpers/validation";
import * as reqValidator from "../../validation/environments";
import {
ProjectPermissionActions,
ProjectPermissionSub,
getAuthDataProjectPermissions
} from "../../ee/services/ProjectRoleService";
import { ForbiddenError } from "@casl/ability";
import { SecretImport } from "../../models";
import { Webhook } from "../../models";
/**
* Create new workspace environment named [environmentName]
* with slug [environmentSlug] under workspace with id
* @param req
* @param res
* @returns
*/
export const createWorkspaceEnvironment = async (req: Request, res: Response) => {
/*
#swagger.summary = 'Create environment'
#swagger.description = 'Create environment'
#swagger.security = [{
"apiKeyAuth": [],
}]
#swagger.parameters['workspaceId'] = {
"description": "ID of workspace where to create environment",
"required": true,
"type": "string",
"in": "path"
}
#swagger.requestBody = {
content: {
"application/json": {
"schema": {
"type": "object",
"properties": {
"environmentName": {
"type": "string",
"description": "Name of the environment to create",
"example": "development"
},
"environmentSlug": {
"type": "string",
"description": "Slug of environment to create",
"example": "dev-environment"
}
},
"required": ["environmentName", "environmentSlug"]
}
}
}
}
#swagger.responses[200] = {
content: {
"application/json": {
"schema": {
"type": "object",
"properties": {
"message": {
"type": "string",
"description": "Sucess message",
"example": "Successfully created environment"
},
"workspace": {
"type": "string",
"description": "ID of workspace where environment was created",
"example": "abc123"
},
"environment": {
"type": "object",
"properties": {
"name": {
"type": "string",
"description": "Name of created environment",
"example": "Staging"
},
"slug": {
"type": "string",
"description": "Slug of created environment",
"example": "staging"
}
}
}
},
"description": "Details of the created environment"
}
}
}
}
*/
const {
params: { workspaceId },
body: { environmentName, environmentSlug }
} = await validateRequest(reqValidator.CreateWorkspaceEnvironmentV2, req);
const { permission } = await getAuthDataProjectPermissions({
authData: req.authData,
workspaceId: new Types.ObjectId(workspaceId)
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Create,
ProjectPermissionSub.Environments
);
const workspace = await Workspace.findById(workspaceId).exec();
if (!workspace) throw WorkspaceNotFoundError();
const plan = await EELicenseService.getPlan(workspace.organization);
if (plan.environmentLimit !== null) {
// case: limit imposed on number of environments allowed
if (workspace.environments.length >= plan.environmentLimit) {
// case: number of environments used exceeds the number of environments allowed
return res.status(400).send({
message:
"Failed to create environment due to environment limit reached. Upgrade plan to create more environments."
});
}
}
if (
!workspace ||
workspace?.environments.find(
({ name, slug }) => slug === environmentSlug || environmentName === name
)
) {
throw new Error("Failed to create workspace environment");
}
workspace?.environments.push({
name: environmentName,
slug: environmentSlug.toLowerCase()
});
await workspace.save();
await EELicenseService.refreshPlan(workspace.organization, new Types.ObjectId(workspaceId));
await EEAuditLogService.createAuditLog(
req.authData,
{
type: EventType.CREATE_ENVIRONMENT,
metadata: {
name: environmentName,
slug: environmentSlug
}
},
{
workspaceId: workspace._id
}
);
return res.status(200).send({
message: "Successfully created new environment",
workspace: workspaceId,
environment: {
name: environmentName,
slug: environmentSlug
}
});
};
/**
* Swaps the ordering of two environments in the database. This is purely for aesthetic purposes.
* @param req
* @param res
* @returns
*/
export const reorderWorkspaceEnvironments = async (req: Request, res: Response) => {
const {
params: { workspaceId },
body: { environmentName, environmentSlug, otherEnvironmentSlug, otherEnvironmentName }
} = await validateRequest(reqValidator.ReorderWorkspaceEnvironmentsV2, req);
const { permission } = await getAuthDataProjectPermissions({
authData: req.authData,
workspaceId: new Types.ObjectId(workspaceId)
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Edit,
ProjectPermissionSub.Environments
);
// atomic update the env to avoid conflict
const workspace = await Workspace.findById(workspaceId).exec();
if (!workspace) {
throw BadRequestError({ message: "Couldn't load workspace" });
}
const environmentIndex = workspace.environments.findIndex(
(env) => env.name === environmentName && env.slug === environmentSlug
);
const otherEnvironmentIndex = workspace.environments.findIndex(
(env) => env.name === otherEnvironmentName && env.slug === otherEnvironmentSlug
);
if (environmentIndex === -1 || otherEnvironmentIndex === -1) {
throw BadRequestError({ message: "environment or otherEnvironment couldn't be found" });
}
// swap the order of the environments
[workspace.environments[environmentIndex], workspace.environments[otherEnvironmentIndex]] = [
workspace.environments[otherEnvironmentIndex],
workspace.environments[environmentIndex]
];
await workspace.save();
return res.status(200).send({
message: "Successfully reordered environments",
workspace: workspaceId
});
};
/**
* Rename workspace environment with new name and slug of a workspace with [workspaceId]
* Old slug [oldEnvironmentSlug] must be provided
* @param req
* @param res
* @returns
*/
export const renameWorkspaceEnvironment = async (req: Request, res: Response) => {
/*
#swagger.summary = 'Update environment'
#swagger.description = 'Update environment'
#swagger.security = [{
"apiKeyAuth": [],
}]
#swagger.parameters['workspaceId'] = {
"description": "ID of workspace where to update environment",
"required": true,
"type": "string",
"in": "path"
}
#swagger.requestBody = {
content: {
"application/json": {
"schema": {
"type": "object",
"properties": {
"environmentName": {
"type": "string",
"description": "Name of environment to update to",
"example": "Staging-Renamed"
},
"environmentSlug": {
"type": "string",
"description": "Slug of environment to update to",
"example": "staging-renamed"
},
"oldEnvironmentSlug": {
"type": "string",
"description": "Current slug of environment",
"example": "staging-old"
}
},
"required": ["environmentName", "environmentSlug", "oldEnvironmentSlug"]
}
}
}
}
#swagger.responses[200] = {
content: {
"application/json": {
"schema": {
"type": "object",
"properties": {
"message": {
"type": "string",
"description": "Success message",
"example": "Successfully update environment"
},
"workspace": {
"type": "string",
"description": "ID of workspace where environment was updated",
"example": "abc123"
},
"environment": {
"type": "object",
"properties": {
"name": {
"type": "string",
"description": "Name of updated environment",
"example": "Staging-Renamed"
},
"slug": {
"type": "string",
"description": "Slug of updated environment",
"example": "staging-renamed"
}
}
}
},
"description": "Details of the renamed environment"
}
}
}
}
*/
const {
params: { workspaceId },
body: { environmentName, environmentSlug, oldEnvironmentSlug }
} = await validateRequest(reqValidator.UpdateWorkspaceEnvironmentV2, req);
const { permission } = await getAuthDataProjectPermissions({
authData: req.authData,
workspaceId: new Types.ObjectId(workspaceId)
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Edit,
ProjectPermissionSub.Environments
);
// user should pass both new slug and env name
if (!environmentSlug || !environmentName) {
throw new Error("Invalid environment given.");
}
// atomic update the env to avoid conflict
const workspace = await Workspace.findById(workspaceId).exec();
if (!workspace) {
throw new Error("Failed to create workspace environment");
}
const isEnvExist = workspace.environments.some(
({ name, slug }) =>
slug !== oldEnvironmentSlug && (name === environmentName || slug === environmentSlug)
);
if (isEnvExist) {
throw new Error("Invalid environment given");
}
const envIndex = workspace?.environments.findIndex(({ slug }) => slug === oldEnvironmentSlug);
if (envIndex === -1) {
throw new Error("Invalid environment given");
}
const oldEnvironment = workspace.environments[envIndex];
workspace.environments[envIndex].name = environmentName;
workspace.environments[envIndex].slug = environmentSlug.toLowerCase();
await workspace.save();
await Secret.updateMany(
{ workspace: workspaceId, environment: oldEnvironmentSlug },
{ environment: environmentSlug }
);
await SecretVersion.updateMany(
{ workspace: workspaceId, environment: oldEnvironmentSlug },
{ environment: environmentSlug }
);
await ServiceToken.updateMany(
{ workspace: workspaceId, environment: oldEnvironmentSlug },
{ environment: environmentSlug }
);
await ServiceTokenData.updateMany(
{
workspace: workspaceId,
"scopes.environment": oldEnvironmentSlug
},
{ $set: { "scopes.$[element].environment": environmentSlug } },
{ arrayFilters: [{ "element.environment": oldEnvironmentSlug }] }
);
await Integration.updateMany(
{ workspace: workspaceId, environment: oldEnvironmentSlug },
{ environment: environmentSlug }
);
await Folder.updateMany(
{ workspace: workspaceId, environment: oldEnvironmentSlug },
{ environment: environmentSlug }
);
await SecretImport.updateMany(
{ workspace: workspaceId, environment: oldEnvironmentSlug },
{ environment: environmentSlug }
);
await SecretImport.updateMany(
{ workspace: workspaceId, "imports.environment": oldEnvironmentSlug },
{ $set: { "imports.$[element].environment": environmentSlug } },
{ arrayFilters: [{ "element.environment": oldEnvironmentSlug }] },
);
await Webhook.updateMany(
{ workspace: workspaceId, environment: oldEnvironmentSlug },
{ environment: environmentSlug }
);
await Membership.updateMany(
{
workspace: workspaceId,
"deniedPermissions.environmentSlug": oldEnvironmentSlug
},
{ $set: { "deniedPermissions.$[element].environmentSlug": environmentSlug } },
{ arrayFilters: [{ "element.environmentSlug": oldEnvironmentSlug }] }
);
await EEAuditLogService.createAuditLog(
req.authData,
{
type: EventType.UPDATE_ENVIRONMENT,
metadata: {
oldName: oldEnvironment.name,
newName: environmentName,
oldSlug: oldEnvironment.slug,
newSlug: environmentSlug.toLowerCase()
}
},
{
workspaceId: workspace._id
}
);
return res.status(200).send({
message: "Successfully update environment",
workspace: workspaceId,
environment: {
name: environmentName,
slug: environmentSlug
}
});
};
/**
* Delete workspace environment by [environmentSlug] of workspace [workspaceId] and do the clean up
* @param req
* @param res
* @returns
*/
export const deleteWorkspaceEnvironment = async (req: Request, res: Response) => {
/*
#swagger.summary = 'Delete environment'
#swagger.description = 'Delete environment'
#swagger.security = [{
"apiKeyAuth": []
}]
#swagger.parameters['workspaceId'] = {
"description": "ID of workspace where to delete environment",
"required": true,
"type": "string",
"in": "path"
}
#swagger.requestBody = {
content: {
"application/json": {
"schema": {
"type": "object",
"properties": {
"environmentSlug": {
"type": "string",
"description": "Slug of environment to delete",
"example": "dev"
}
},
"required": ["environmentSlug"]
}
}
}
}
#swagger.responses[200] = {
content: {
"application/json": {
"schema": {
"type": "object",
"properties": {
"message": {
"type": "string",
"description": "Success message",
"example": "Successfully deleted environment"
},
"workspace": {
"type": "string",
"description": "ID of workspace where environment was deleted",
"example": "abc123"
},
"environment": {
"type": "string",
"description": "Slug of deleted environment",
"example": "dev"
}
},
"description": "Response after deleting an environment from a workspace"
}
}
}
}
*/
const {
params: { workspaceId },
body: { environmentSlug }
} = await validateRequest(reqValidator.DeleteWorkspaceEnvironmentV2, req);
const { permission } = await getAuthDataProjectPermissions({
authData: req.authData,
workspaceId: new Types.ObjectId(workspaceId)
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Delete,
ProjectPermissionSub.Environments
);
// atomic update the env to avoid conflict
const workspace = await Workspace.findById(workspaceId).exec();
if (!workspace) {
throw new Error("Failed to create workspace environment");
}
const envIndex = workspace?.environments.findIndex(({ slug }) => slug === environmentSlug);
if (envIndex === -1) {
throw new Error("Invalid environment given");
}
const oldEnvironment = workspace.environments[envIndex];
workspace.environments.splice(envIndex, 1);
await workspace.save();
// clean up
await Secret.deleteMany({
workspace: workspaceId,
environment: environmentSlug
});
await SecretVersion.deleteMany({
workspace: workspaceId,
environment: environmentSlug
});
// await ServiceToken.deleteMany({
// workspace: workspaceId,
// environment: environmentSlug,
// });
const result = await ServiceTokenData.updateMany(
{ workspace: workspaceId },
{ $pull: { scopes: { environment: environmentSlug } } }
);
if (result.modifiedCount > 0) {
await ServiceTokenData.deleteMany({ workspace: workspaceId, scopes: { $size: 0 } });
}
await Integration.deleteMany({
workspace: workspaceId,
environment: environmentSlug
});
await Membership.updateMany(
{ workspace: workspaceId },
{ $pull: { deniedPermissions: { environmentSlug: environmentSlug } } }
);
await EELicenseService.refreshPlan(workspace.organization, new Types.ObjectId(workspaceId));
await EEAuditLogService.createAuditLog(
req.authData,
{
type: EventType.DELETE_ENVIRONMENT,
metadata: {
name: oldEnvironment.name,
slug: oldEnvironment.slug
}
},
{
workspaceId: workspace._id
}
);
return res.status(200).send({
message: "Successfully deleted environment",
workspace: workspaceId,
environment: environmentSlug
});
};

View File

@ -0,0 +1,25 @@
import * as authController from "./authController";
import * as signupController from "./signupController";
import * as usersController from "./usersController";
import * as organizationsController from "./organizationsController";
import * as workspaceController from "./workspaceController";
import * as serviceTokenDataController from "./serviceTokenDataController";
import * as secretController from "./secretController";
import * as secretsController from "./secretsController";
import * as environmentController from "./environmentController";
import * as tagController from "./tagController";
import * as membershipController from "./membershipController";
export {
authController,
signupController,
usersController,
organizationsController,
workspaceController,
serviceTokenDataController,
secretController,
secretsController,
environmentController,
tagController,
membershipController
};

View File

@ -0,0 +1,107 @@
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
});
};

View File

@ -0,0 +1,505 @@
import { Request, Response } from "express";
import { Types } from "mongoose";
import {
IWorkspace,
Identity,
IdentityMembership,
IdentityMembershipOrg,
Membership,
MembershipOrg,
User,
Workspace
} from "../../models";
import { Role } from "../../ee/models";
import { deleteMembershipOrg } from "../../helpers/membershipOrg";
import {
createOrganization as create,
deleteOrganization,
updateSubscriptionOrgQuantity
} from "../../helpers/organization";
import { addMembershipsOrg } from "../../helpers/membershipOrg";
import { BadRequestError, ResourceNotFoundError, UnauthorizedRequestError } from "../../utils/errors";
import { ACCEPTED, ADMIN, CUSTOM, MEMBER, NO_ACCESS } from "../../variables";
import * as reqValidator from "../../validation/organization";
import { validateRequest } from "../../helpers/validation";
import {
OrgPermissionActions,
OrgPermissionSubjects,
getAuthDataOrgPermissions
} from "../../ee/services/RoleService";
import { EELicenseService } from "../../ee/services";
import { ForbiddenError } from "@casl/ability";
/**
* Return memberships for organization with id [organizationId]
* @param req
* @param res
*/
export const getOrganizationMemberships = async (req: Request, res: Response) => {
/*
#swagger.summary = 'Return organization user memberships'
#swagger.description = 'Return organization user memberships'
#swagger.security = [{
"apiKeyAuth": [],
"bearerAuth": []
}]
#swagger.parameters['organizationId'] = {
"description": "ID of organization",
"required": true,
"type": "string"
}
#swagger.responses[200] = {
content: {
"application/json": {
"schema": {
"type": "object",
"properties": {
"memberships": {
"type": "array",
"items": {
$ref: "#/components/schemas/MembershipOrg"
},
"description": "Memberships of organization"
}
}
}
}
}
}
*/
const {
params: { organizationId }
} = await validateRequest(reqValidator.GetOrgMembersv2, req);
const { permission } = await getAuthDataOrgPermissions({
authData: req.authData,
organizationId: new Types.ObjectId(organizationId)
});
ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionActions.Read,
OrgPermissionSubjects.Member
);
const memberships = await MembershipOrg.find({
organization: organizationId
}).populate("user", "+publicKey");
return res.status(200).send({
memberships
});
};
/**
* Update role of membership with id [membershipId] to role [role]
* @param req
* @param res
*/
export const updateOrganizationMembership = async (req: Request, res: Response) => {
/*
#swagger.summary = 'Update organization user membership'
#swagger.description = 'Update organization user membership'
#swagger.security = [{
"apiKeyAuth": [],
"bearerAuth": []
}]
#swagger.parameters['organizationId'] = {
"description": "ID of organization",
"required": true,
"type": "string"
}
#swagger.parameters['membershipId'] = {
"description": "ID of organization membership to update",
"required": true,
"type": "string"
}
#swagger.requestBody = {
"required": true,
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"role": {
"type": "string",
"description": "Role of organization membership - either owner, admin, or member",
}
}
}
}
}
}
#swagger.responses[200] = {
content: {
"application/json": {
"schema": {
"type": "object",
"properties": {
"membership": {
$ref: "#/components/schemas/MembershipOrg",
"description": "Updated organization membership"
}
}
}
}
}
}
*/
const {
params: { organizationId, membershipId },
body: { role }
} = await validateRequest(reqValidator.UpdateOrgMemberv2, req);
const { permission } = await getAuthDataOrgPermissions({
authData: req.authData,
organizationId: new Types.ObjectId(organizationId)
});
ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionActions.Edit,
OrgPermissionSubjects.Member
);
const isCustomRole = ![ADMIN, MEMBER, NO_ACCESS].includes(role);
if (isCustomRole) {
const orgRole = await Role.findOne({
slug: role,
isOrgRole: true,
organization: new Types.ObjectId(organizationId)
});
if (!orgRole) throw BadRequestError({ message: "Role not found" });
const plan = await EELicenseService.getPlan(new Types.ObjectId(organizationId));
if (!plan.rbac) return res.status(400).send({
message:
"Failed to assign custom role due to RBAC restriction. Upgrade plan to assign custom role to member."
});
const membership = await MembershipOrg.findByIdAndUpdate(membershipId, {
role: CUSTOM,
customRole: orgRole
});
return res.status(200).send({
membership
});
}
const membership = await MembershipOrg.findByIdAndUpdate(
membershipId,
{
$set: {
role
},
$unset: {
customRole: 1
}
},
{
new: true
}
);
return res.status(200).send({
membership
});
};
/**
* Delete organization membership with id [membershipId]
* @param req
* @param res
* @returns
*/
export const deleteOrganizationMembership = async (req: Request, res: Response) => {
/*
#swagger.summary = 'Delete organization user membership'
#swagger.description = 'Delete organization user membership'
#swagger.security = [{
"apiKeyAuth": [],
"bearerAuth": []
}]
#swagger.parameters['organizationId'] = {
"description": "ID of organization",
"required": true,
"type": "string"
}
#swagger.parameters['membershipId'] = {
"description": "ID of organization membership to delete",
"required": true,
"type": "string"
}
#swagger.responses[200] = {
content: {
"application/json": {
"schema": {
"type": "object",
"properties": {
"membership": {
$ref: "#/components/schemas/MembershipOrg",
"description": "Deleted organization membership"
}
}
}
}
}
}
*/
const {
params: { organizationId, membershipId }
} = await validateRequest(reqValidator.DeleteOrgMemberv2, req);
const membershipOrg = await MembershipOrg.findOne({
_id: new Types.ObjectId(membershipId),
organization: new Types.ObjectId(organizationId)
});
if (!membershipOrg) throw ResourceNotFoundError();
const { permission } = await getAuthDataOrgPermissions({
authData: req.authData,
organizationId: membershipOrg.organization
});
ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionActions.Delete,
OrgPermissionSubjects.Member
);
// delete organization membership
const membership = await deleteMembershipOrg({
membershipOrgId: membershipId
});
await updateSubscriptionOrgQuantity({
organizationId: membership.organization.toString()
});
return res.status(200).send({
membership
});
};
/**
* Return workspaces for organization with id [organizationId] that user has
* access to
* @param req
* @param res
*/
export const getOrganizationWorkspaces = async (req: Request, res: Response) => {
/*
#swagger.summary = 'Return projects in organization that user is part of'
#swagger.description = 'Return projects in organization that user is part of'
#swagger.security = [{
"apiKeyAuth": [],
"bearerAuth": []
}]
#swagger.parameters['organizationId'] = {
"description": "ID of organization",
"required": true,
"type": "string"
}
#swagger.responses[200] = {
content: {
"application/json": {
"schema": {
"type": "object",
"properties": {
"workspaces": {
"type": "array",
"items": {
$ref: "#/components/schemas/Project"
},
"description": "Projects of organization"
}
}
}
}
}
}
*/
const {
params: { organizationId }
} = await validateRequest(reqValidator.GetOrgWorkspacesv2, req);
const { permission } = await getAuthDataOrgPermissions({
authData: req.authData,
organizationId: new Types.ObjectId(organizationId)
});
ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionActions.Read,
OrgPermissionSubjects.Workspace
);
const workspacesSet = new Set(
(
await Workspace.find(
{
organization: organizationId
},
"_id"
)
).map((w) => w._id.toString())
);
let workspaces: IWorkspace[] = [];
if (req.authData.authPayload instanceof Identity) {
workspaces = (
await IdentityMembership.find({
identity: req.authData.authPayload._id
}).populate<{ workspace: IWorkspace }>("workspace")
)
.filter((m) => workspacesSet.has(m.workspace._id.toString()))
.map((m) => m.workspace);
}
if (req.authData.authPayload instanceof User) {
workspaces = (
await Membership.find({
user: req.authData.authPayload._id
}).populate<{ workspace: IWorkspace }>("workspace")
)
.filter((m) => workspacesSet.has(m.workspace._id.toString()))
.map((m) => m.workspace);
}
return res.status(200).send({
workspaces
});
};
/**
* Create new organization named [organizationName]
* and add user as owner
* @param req
* @param res
* @returns
*/
export const createOrganization = async (req: Request, res: Response) => {
const {
body: { name }
} = await validateRequest(reqValidator.CreateOrgv2, req);
// create organization and add user as member
const organization = await create({
email: req.user.email,
name
});
await addMembershipsOrg({
userIds: [req.user._id.toString()],
organizationId: organization._id.toString(),
roles: [ADMIN],
statuses: [ACCEPTED]
});
return res.status(200).send({
organization
});
};
/**
* Delete organization with id [organizationId]
* @param req
* @param res
*/
export const deleteOrganizationById = async (req: Request, res: Response) => {
const {
params: { organizationId }
} = await validateRequest(reqValidator.DeleteOrgv2, req);
const membershipOrg = await MembershipOrg.findOne({
user: req.user._id,
organization: new Types.ObjectId(organizationId),
role: ADMIN
});
if (!membershipOrg) throw UnauthorizedRequestError();
const organization = await deleteOrganization({
organizationId: new Types.ObjectId(organizationId)
});
return res.status(200).send({
organization
});
};
/**
* Return list of identity memberships for organization with id [organizationId]
* @param req
* @param res
* @returns
*/
export const getOrganizationIdentityMemberships = async (req: Request, res: Response) => {
/*
#swagger.summary = 'Return organization identity memberships'
#swagger.description = 'Return organization identity memberships'
#swagger.security = [{
"bearerAuth": []
}]
#swagger.parameters['organizationId'] = {
"description": "ID of organization",
"required": true,
"type": "string",
"in": "path"
}
#swagger.responses[200] = {
content: {
"application/json": {
"schema": {
"type": "object",
"properties": {
"identityMemberships": {
"type": "array",
"items": {
$ref: "#/components/schemas/IdentityMembershipOrg"
},
"description": "Identity memberships of organization"
}
}
}
}
}
}
*/
const {
params: { organizationId }
} = await validateRequest(reqValidator.GetOrgIdentityMembershipsV2, req);
const { permission } = await getAuthDataOrgPermissions({
authData: req.authData,
organizationId: new Types.ObjectId(organizationId)
});
ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionActions.Read,
OrgPermissionSubjects.Identity
);
const identityMemberships = await IdentityMembershipOrg.find({
organization: new Types.ObjectId(organizationId)
}).populate("identity customRole");
return res.status(200).send({
identityMemberships
});
}

View File

@ -0,0 +1,419 @@
import { Request, Response } from "express";
import mongoose, { Types } from "mongoose";
import {
CreateSecretRequestBody,
ModifySecretRequestBody,
SanitizedSecretForCreate,
SanitizedSecretModify
} from "../../types/secret";
const { ValidationError } = mongoose.Error;
import {
ValidationError as RouteValidationError,
UnauthorizedRequestError
} from "../../utils/errors";
import {
ALGORITHM_AES_256_GCM,
ENCODING_SCHEME_UTF8,
SECRET_PERSONAL,
SECRET_SHARED
} from "../../variables";
import { TelemetryService } from "../../services";
import { Secret, User } from "../../models";
import { AccountNotFoundError } from "../../utils/errors";
/**
* Create secret for workspace with id [workspaceId] and environment [environment]
* @param req
* @param res
*/
export const createSecret = async (req: Request, res: Response) => {
const postHogClient = await TelemetryService.getPostHogClient();
const secretToCreate: CreateSecretRequestBody = req.body.secret;
const { workspaceId, environment } = req.params;
const sanitizedSecret: SanitizedSecretForCreate = {
secretKeyCiphertext: secretToCreate.secretKeyCiphertext,
secretKeyIV: secretToCreate.secretKeyIV,
secretKeyTag: secretToCreate.secretKeyTag,
secretKeyHash: secretToCreate.secretKeyHash,
secretValueCiphertext: secretToCreate.secretValueCiphertext,
secretValueIV: secretToCreate.secretValueIV,
secretValueTag: secretToCreate.secretValueTag,
secretValueHash: secretToCreate.secretValueHash,
secretCommentCiphertext: secretToCreate.secretCommentCiphertext,
secretCommentIV: secretToCreate.secretCommentIV,
secretCommentTag: secretToCreate.secretCommentTag,
secretCommentHash: secretToCreate.secretCommentHash,
workspace: new Types.ObjectId(workspaceId),
environment,
type: secretToCreate.type,
user: new Types.ObjectId(req.user._id),
algorithm: ALGORITHM_AES_256_GCM,
keyEncoding: ENCODING_SCHEME_UTF8
};
const secret = await new Secret(sanitizedSecret).save();
if (postHogClient) {
postHogClient.capture({
event: "secrets added",
distinctId: req.user.email,
properties: {
numberOfSecrets: 1,
workspaceId,
environment,
channel: req.headers?.["user-agent"]?.toLowerCase().includes("mozilla") ? "web" : "cli",
userAgent: req.headers?.["user-agent"]
}
});
}
res.status(200).send({
secret
});
};
/**
* Create many secrets for workspace with id [workspaceId] and environment [environment]
* @param req
* @param res
*/
export const createSecrets = async (req: Request, res: Response) => {
const postHogClient = await TelemetryService.getPostHogClient();
const secretsToCreate: CreateSecretRequestBody[] = req.body.secrets;
const { workspaceId, environment } = req.params;
const sanitizedSecretesToCreate: SanitizedSecretForCreate[] = [];
secretsToCreate.forEach((rawSecret) => {
const safeUpdateFields: SanitizedSecretForCreate = {
secretKeyCiphertext: rawSecret.secretKeyCiphertext,
secretKeyIV: rawSecret.secretKeyIV,
secretKeyTag: rawSecret.secretKeyTag,
secretKeyHash: rawSecret.secretKeyHash,
secretValueCiphertext: rawSecret.secretValueCiphertext,
secretValueIV: rawSecret.secretValueIV,
secretValueTag: rawSecret.secretValueTag,
secretValueHash: rawSecret.secretValueHash,
secretCommentCiphertext: rawSecret.secretCommentCiphertext,
secretCommentIV: rawSecret.secretCommentIV,
secretCommentTag: rawSecret.secretCommentTag,
secretCommentHash: rawSecret.secretCommentHash,
workspace: new Types.ObjectId(workspaceId),
environment,
type: rawSecret.type,
user: new Types.ObjectId(req.user._id),
algorithm: ALGORITHM_AES_256_GCM,
keyEncoding: ENCODING_SCHEME_UTF8
};
sanitizedSecretesToCreate.push(safeUpdateFields);
});
const secrets = await Secret.insertMany(sanitizedSecretesToCreate);
if (postHogClient) {
postHogClient.capture({
event: "secrets added",
distinctId: req.user.email,
properties: {
numberOfSecrets: (secretsToCreate ?? []).length,
workspaceId,
environment,
channel: req.headers?.["user-agent"]?.toLowerCase().includes("mozilla") ? "web" : "cli",
userAgent: req.headers?.["user-agent"]
}
});
}
res.status(200).send({
secrets
});
};
/**
* Delete secrets in workspace with id [workspaceId] and environment [environment]
* @param req
* @param res
*/
export const deleteSecrets = async (req: Request, res: Response) => {
const postHogClient = await TelemetryService.getPostHogClient();
const { workspaceId, environmentName } = req.params;
const secretIdsToDelete: string[] = req.body.secretIds;
const secretIdsUserCanDelete = await Secret.find({ workspace: workspaceId, environment: environmentName }, { _id: 1 });
const secretsUserCanDeleteSet: Set<string> = new Set(
secretIdsUserCanDelete.map((objectId) => objectId._id.toString())
);
// Filter out IDs that user can delete and then map them to delete operations
const deleteOperationsToPerform = secretIdsToDelete
.filter(secretIdToDelete => {
if (!secretsUserCanDeleteSet.has(secretIdToDelete)) {
throw RouteValidationError({
message: "You cannot delete secrets that you do not have access to"
});
}
return true;
})
.map(secretIdToDelete => ({
deleteOne: { filter: { _id: new Types.ObjectId(secretIdToDelete) } }
}));
const numSecretsDeleted = deleteOperationsToPerform.length;
await Secret.bulkWrite(deleteOperationsToPerform);
if (postHogClient) {
postHogClient.capture({
event: "secrets deleted",
distinctId: req.user.email,
properties: {
numberOfSecrets: numSecretsDeleted,
environment: environmentName,
workspaceId,
channel: req.headers?.["user-agent"]?.toLowerCase().includes("mozilla") ? "web" : "cli",
userAgent: req.headers?.["user-agent"]
}
});
}
res.status(200).send();
};
/**
* Delete secret with id [secretId]
* @param req
* @param res
*/
export const deleteSecret = async (req: Request, res: Response) => {
const postHogClient = await TelemetryService.getPostHogClient();
await Secret.findByIdAndDelete(req._secret._id);
if (postHogClient) {
postHogClient.capture({
event: "secrets deleted",
distinctId: req.user.email,
properties: {
numberOfSecrets: 1,
workspaceId: req._secret.workspace.toString(),
environment: req._secret.environment,
channel: req.headers?.["user-agent"]?.toLowerCase().includes("mozilla") ? "web" : "cli",
userAgent: req.headers?.["user-agent"]
}
});
}
res.status(200).send({
secret: req._secret
});
};
/**
* Update secrets for workspace with id [workspaceId] and environment [environment]
* @param req
* @param res
* @returns
*/
export const updateSecrets = async (req: Request, res: Response) => {
const postHogClient = await TelemetryService.getPostHogClient();
const { workspaceId, environmentName } = req.params;
const secretsModificationsRequested: ModifySecretRequestBody[] = req.body.secrets;
const secretIdsUserCanModify = await Secret.find({ workspace: workspaceId, environment: environmentName }, { _id: 1 });
const secretsUserCanModifySet: Set<string> = new Set(
secretIdsUserCanModify.map((objectId) => objectId._id.toString())
);
const updateOperationsToPerform: any = [];
secretsModificationsRequested.forEach((userModifiedSecret) => {
if (secretsUserCanModifySet.has(userModifiedSecret._id.toString())) {
const sanitizedSecret: SanitizedSecretModify = {
secretKeyCiphertext: userModifiedSecret.secretKeyCiphertext,
secretKeyIV: userModifiedSecret.secretKeyIV,
secretKeyTag: userModifiedSecret.secretKeyTag,
secretKeyHash: userModifiedSecret.secretKeyHash,
secretValueCiphertext: userModifiedSecret.secretValueCiphertext,
secretValueIV: userModifiedSecret.secretValueIV,
secretValueTag: userModifiedSecret.secretValueTag,
secretValueHash: userModifiedSecret.secretValueHash,
secretCommentCiphertext: userModifiedSecret.secretCommentCiphertext,
secretCommentIV: userModifiedSecret.secretCommentIV,
secretCommentTag: userModifiedSecret.secretCommentTag,
secretCommentHash: userModifiedSecret.secretCommentHash
};
const updateOperation = {
updateOne: {
filter: { _id: userModifiedSecret._id, workspace: workspaceId },
update: { $inc: { version: 1 }, $set: sanitizedSecret }
}
};
updateOperationsToPerform.push(updateOperation);
} else {
throw UnauthorizedRequestError({
message: "You do not have permission to modify one or more of the requested secrets"
});
}
});
await Secret.bulkWrite(updateOperationsToPerform);
if (postHogClient) {
postHogClient.capture({
event: "secrets modified",
distinctId: req.user.email,
properties: {
numberOfSecrets: (secretsModificationsRequested ?? []).length,
environment: environmentName,
workspaceId,
channel: req.headers?.["user-agent"]?.toLowerCase().includes("mozilla") ? "web" : "cli",
userAgent: req.headers?.["user-agent"]
}
});
}
return res.status(200).send();
};
/**
* Update a secret within workspace with id [workspaceId] and environment [environment]
* @param req
* @param res
* @returns
*/
export const updateSecret = async (req: Request, res: Response) => {
const postHogClient = await TelemetryService.getPostHogClient();
const { workspaceId, environmentName } = req.params;
const secretModificationsRequested: ModifySecretRequestBody = req.body.secret;
await Secret.findOne({ workspace: workspaceId, environment: environmentName }, { _id: 1 });
const sanitizedSecret: SanitizedSecretModify = {
secretKeyCiphertext: secretModificationsRequested.secretKeyCiphertext,
secretKeyIV: secretModificationsRequested.secretKeyIV,
secretKeyTag: secretModificationsRequested.secretKeyTag,
secretKeyHash: secretModificationsRequested.secretKeyHash,
secretValueCiphertext: secretModificationsRequested.secretValueCiphertext,
secretValueIV: secretModificationsRequested.secretValueIV,
secretValueTag: secretModificationsRequested.secretValueTag,
secretValueHash: secretModificationsRequested.secretValueHash,
secretCommentCiphertext: secretModificationsRequested.secretCommentCiphertext,
secretCommentIV: secretModificationsRequested.secretCommentIV,
secretCommentTag: secretModificationsRequested.secretCommentTag,
secretCommentHash: secretModificationsRequested.secretCommentHash
};
const singleModificationUpdate = await Secret.updateOne(
{ _id: secretModificationsRequested._id, workspace: workspaceId },
{ $inc: { version: 1 }, $set: sanitizedSecret }
)
.catch((error) => {
if (error instanceof ValidationError) {
throw RouteValidationError({
message: "Unable to apply modifications, please try again",
stack: error.stack
});
}
throw error;
});
if (postHogClient) {
postHogClient.capture({
event: "secrets modified",
distinctId: req.user.email,
properties: {
numberOfSecrets: 1,
environment: environmentName,
workspaceId,
channel: req.headers?.["user-agent"]?.toLowerCase().includes("mozilla") ? "web" : "cli",
userAgent: req.headers?.["user-agent"]
}
});
}
return res.status(200).send(singleModificationUpdate);
};
/**
* Return secrets for workspace with id [workspaceId], environment [environment] and user
* with id [req.user._id]
* @param req
* @param res
* @returns
*/
export const getSecrets = async (req: Request, res: Response) => {
const postHogClient = await TelemetryService.getPostHogClient();
const { environment } = req.query;
const { workspaceId } = req.params;
let userId: Types.ObjectId | undefined = undefined; // used for getting personal secrets for user
let userEmail: string | undefined = undefined; // used for posthog
if (req.user) {
userId = req.user._id;
userEmail = req.user.email;
}
if (req.serviceTokenData) {
userId = req.serviceTokenData.user;
const user = await User.findById(req.serviceTokenData.user, "email");
if (!user) throw AccountNotFoundError();
userEmail = user.email;
}
const secrets = await Secret.find({
workspace: workspaceId,
environment,
$or: [{ user: userId }, { user: { $exists: false } }],
type: { $in: [SECRET_SHARED, SECRET_PERSONAL] }
})
.catch((err) => {
throw RouteValidationError({
message: "Failed to get secrets, please try again",
stack: err.stack
});
})
if (postHogClient) {
postHogClient.capture({
event: "secrets pulled",
distinctId: userEmail,
properties: {
numberOfSecrets: (secrets ?? []).length,
environment,
workspaceId,
channel: req.headers?.["user-agent"]?.toLowerCase().includes("mozilla") ? "web" : "cli",
userAgent: req.headers?.["user-agent"]
}
});
}
return res.json(secrets);
};
/**
* Return secret with id [secretId]
* @param req
* @param res
* @returns
*/
export const getSecret = async (req: Request, res: Response) => {
// if (postHogClient) {
// postHogClient.capture({
// event: 'secrets pulled',
// distinctId: req.user.email,
// properties: {
// numberOfSecrets: 1,
// workspaceId: req._secret.workspace.toString(),
// environment: req._secret.environment,
// channel: req.headers?.['user-agent']?.toLowerCase().includes('mozilla') ? 'web' : 'cli',
// userAgent: req.headers?.['user-agent']
// }
// });
// }
return res.status(200).send({
secret: req._secret
});
};

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,201 @@
import { Request, Response } from "express";
import crypto from "crypto";
import bcrypt from "bcrypt";
import { ServiceTokenData } from "../../models";
import { getSaltRounds } from "../../config";
import { BadRequestError } from "../../utils/errors";
import { ActorType, EventType } from "../../ee/models";
import { EEAuditLogService } from "../../ee/services";
import { validateRequest } from "../../helpers/validation";
import * as reqValidator from "../../validation/serviceTokenData";
import {
ProjectPermissionActions,
ProjectPermissionSub,
getAuthDataProjectPermissions
} from "../../ee/services/ProjectRoleService";
import { ForbiddenError, subject } from "@casl/ability";
import { Types } from "mongoose";
/**
* Return service token data associated with service token on request
* @param req
* @param res
* @returns
*/
export const getServiceTokenData = async (req: Request, res: Response) => {
/*
#swagger.summary = 'Return Infisical Token data'
#swagger.description = 'Return Infisical Token data'
#swagger.security = [{
"bearerAuth": []
}]
#swagger.responses[200] = {
content: {
"application/json": {
"schema": {
"type": "object",
"properties": {
"serviceTokenData": {
"type": "object",
$ref: "#/components/schemas/ServiceTokenData",
"description": "Details of service token"
}
}
}
}
}
}
*/
if (!(req.authData.authPayload instanceof ServiceTokenData))
throw BadRequestError({
message: "Failed accepted client validation for service token data"
});
const serviceTokenData = await ServiceTokenData.findById(req.authData.authPayload._id)
.select("+encryptedKey +iv +tag")
.populate("user")
.lean();
return res.status(200).json(serviceTokenData);
};
/**
* Create new service token data for workspace with id [workspaceId] and
* environment [environment].
* @param req
* @param res
* @returns
*/
export const createServiceTokenData = async (req: Request, res: Response) => {
let serviceTokenData;
const {
body: { workspaceId, permissions, tag, encryptedKey, scopes, name, expiresIn, iv }
} = await validateRequest(reqValidator.CreateServiceTokenV2, req);
const { permission } = await getAuthDataProjectPermissions({
authData: req.authData,
workspaceId: new Types.ObjectId(workspaceId)
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Create,
ProjectPermissionSub.ServiceTokens
);
scopes.forEach(({ environment, secretPath }) => {
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Create,
subject(ProjectPermissionSub.Secrets, { environment, secretPath: secretPath })
);
})
const secret = crypto.randomBytes(16).toString("hex");
const secretHash = await bcrypt.hash(secret, await getSaltRounds());
let expiresAt;
if (expiresIn) {
expiresAt = new Date();
expiresAt.setSeconds(expiresAt.getSeconds() + expiresIn);
}
let user;
if (req.authData.actor.type === ActorType.USER) {
user = req.authData.authPayload._id;
}
serviceTokenData = await new ServiceTokenData({
name,
workspace: workspaceId,
user,
scopes,
lastUsed: new Date(),
expiresAt,
secretHash,
encryptedKey,
iv,
tag,
permissions
}).save();
// return service token data without sensitive data
serviceTokenData = await ServiceTokenData.findById(serviceTokenData._id);
if (!serviceTokenData) throw new Error("Failed to find service token data");
const serviceToken = `st.${serviceTokenData._id.toString()}.${secret}`;
await EEAuditLogService.createAuditLog(
req.authData,
{
type: EventType.CREATE_SERVICE_TOKEN,
metadata: {
name,
scopes
}
},
{
workspaceId: new Types.ObjectId(workspaceId)
}
);
return res.status(200).send({
serviceToken,
serviceTokenData
});
};
/**
* Delete service token data with id [serviceTokenDataId].
* @param req
* @param res
* @returns
*/
export const deleteServiceTokenData = async (req: Request, res: Response) => {
const {
params: { serviceTokenDataId }
} = await validateRequest(reqValidator.DeleteServiceTokenV2, req);
let serviceTokenData = await ServiceTokenData.findById(serviceTokenDataId);
if (!serviceTokenData) throw BadRequestError({ message: "Service token not found" });
const { permission } = await getAuthDataProjectPermissions({
authData: req.authData,
workspaceId: serviceTokenData.workspace
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Delete,
ProjectPermissionSub.ServiceTokens
);
serviceTokenData = await ServiceTokenData.findByIdAndDelete(serviceTokenDataId);
if (!serviceTokenData)
return res.status(200).send({
message: "Failed to delete service token"
});
await EEAuditLogService.createAuditLog(
req.authData,
{
type: EventType.DELETE_SERVICE_TOKEN,
metadata: {
name: serviceTokenData.name,
scopes: serviceTokenData?.scopes
}
},
{
workspaceId: serviceTokenData.workspace
}
);
return res.status(200).send({
serviceTokenData
});
};

View File

@ -0,0 +1,262 @@
import { Request, Response } from "express";
import { MembershipOrg, User } from "../../models";
import { completeAccount } from "../../helpers/user";
import {
initializeDefaultOrg,
} from "../../helpers/signup";
import { issueAuthTokens } from "../../helpers/auth";
import { ACCEPTED, INVITED } from "../../variables";
import { standardRequest } from "../../config/request";
import { getHttpsEnabled, getLoopsApiKey } from "../../config";
import { updateSubscriptionOrgQuantity } from "../../helpers/organization";
/**
* Complete setting up user by adding their personal and auth information as part of the
* signup flow
* @param req
* @param res
* @returns
*/
export const completeAccountSignup = async (req: Request, res: Response) => {
let user;
const {
email,
firstName,
lastName,
protectedKey,
protectedKeyIV,
protectedKeyTag,
publicKey,
encryptedPrivateKey,
encryptedPrivateKeyIV,
encryptedPrivateKeyTag,
salt,
verifier,
organizationName,
}: {
email: string;
firstName: string;
lastName: string;
protectedKey: string;
protectedKeyIV: string;
protectedKeyTag: string;
publicKey: string;
encryptedPrivateKey: string;
encryptedPrivateKeyIV: string;
encryptedPrivateKeyTag: string;
salt: string;
verifier: string;
organizationName: string;
} = req.body;
// get user
user = await User.findOne({ email });
if (!user || (user && user?.publicKey)) {
// case 1: user doesn't exist.
// case 2: user has already completed account
return res.status(403).send({
error: "Failed to complete account for complete user",
});
}
// complete setting up user's account
user = await completeAccount({
userId: user._id.toString(),
firstName,
lastName,
encryptionVersion: 2,
protectedKey,
protectedKeyIV,
protectedKeyTag,
publicKey,
encryptedPrivateKey,
encryptedPrivateKeyIV,
encryptedPrivateKeyTag,
salt,
verifier,
});
if (!user)
throw new Error("Failed to complete account for non-existent user"); // ensure user is non-null
// initialize default organization and workspace
await initializeDefaultOrg({
organizationName,
user,
});
// update organization membership statuses that are
// invited to completed with user attached
const membershipsToUpdate = await MembershipOrg.find({
inviteEmail: email,
status: INVITED,
});
membershipsToUpdate.forEach(async (membership) => {
await updateSubscriptionOrgQuantity({
organizationId: membership.organization.toString(),
});
});
// update organization membership statuses that are
// invited to completed with user attached
await MembershipOrg.updateMany(
{
inviteEmail: email,
status: INVITED,
},
{
user,
status: ACCEPTED,
}
);
// issue tokens
const tokens = await issueAuthTokens({
userId: user._id,
ip: req.realIP,
userAgent: req.headers["user-agent"] ?? "",
});
const token = tokens.token;
// sending a welcome email to new users
if (await getLoopsApiKey()) {
await standardRequest.post("https://app.loops.so/api/v1/events/send", {
"email": email,
"eventName": "Sign Up",
"firstName": firstName,
"lastName": lastName,
}, {
headers: {
"Accept": "application/json",
"Authorization": "Bearer " + (await getLoopsApiKey()),
},
});
}
// store (refresh) token in httpOnly cookie
res.cookie("jid", tokens.refreshToken, {
httpOnly: true,
path: "/",
sameSite: "strict",
secure: await getHttpsEnabled(),
});
return res.status(200).send({
message: "Successfully set up account",
user,
token,
});
};
/**
* Complete setting up user by adding their personal and auth information as part of the
* invite flow
* @param req
* @param res
* @returns
*/
export const completeAccountInvite = async (req: Request, res: Response) => {
let user;
const {
email,
firstName,
lastName,
protectedKey,
protectedKeyIV,
protectedKeyTag,
publicKey,
encryptedPrivateKey,
encryptedPrivateKeyIV,
encryptedPrivateKeyTag,
salt,
verifier,
} = req.body;
// get user
user = await User.findOne({ email });
if (!user || (user && user?.publicKey)) {
// case 1: user doesn't exist.
// case 2: user has already completed account
return res.status(403).send({
error: "Failed to complete account for complete user",
});
}
const membershipOrg = await MembershipOrg.findOne({
inviteEmail: email,
status: INVITED,
});
if (!membershipOrg) throw new Error("Failed to find invitations for email");
// complete setting up user's account
user = await completeAccount({
userId: user._id.toString(),
firstName,
lastName,
encryptionVersion: 2,
protectedKey,
protectedKeyIV,
protectedKeyTag,
publicKey,
encryptedPrivateKey,
encryptedPrivateKeyIV,
encryptedPrivateKeyTag,
salt,
verifier,
});
if (!user)
throw new Error("Failed to complete account for non-existent user");
// update organization membership statuses that are
// invited to completed with user attached
const membershipsToUpdate = await MembershipOrg.find({
inviteEmail: email,
status: INVITED,
});
membershipsToUpdate.forEach(async (membership) => {
await updateSubscriptionOrgQuantity({
organizationId: membership.organization.toString(),
});
});
await MembershipOrg.updateMany(
{
inviteEmail: email,
status: INVITED,
},
{
user,
status: ACCEPTED,
}
);
// issue tokens
const tokens = await issueAuthTokens({
userId: user._id,
ip: req.realIP,
userAgent: req.headers["user-agent"] ?? "",
});
const token = tokens.token;
// store (refresh) token in httpOnly cookie
res.cookie("jid", tokens.refreshToken, {
httpOnly: true,
path: "/",
sameSite: "strict",
secure: await getHttpsEnabled(),
});
return res.status(200).send({
message: "Successfully set up account",
user,
token,
});
};

View File

@ -0,0 +1,92 @@
import { ForbiddenError } from "@casl/ability";
import { Request, Response } from "express";
import { Types } from "mongoose";
import { Secret, Tag } from "../../models";
import { BadRequestError } from "../../utils/errors";
import { validateRequest } from "../../helpers/validation";
import {
ProjectPermissionActions,
ProjectPermissionSub,
getAuthDataProjectPermissions
} from "../../ee/services/ProjectRoleService";
import * as reqValidator from "../../validation/tags";
export const createWorkspaceTag = async (req: Request, res: Response) => {
const {
body: { name, slug },
params: { workspaceId }
} = await validateRequest(reqValidator.CreateWorkspaceTagsV2, req);
const { permission } = await getAuthDataProjectPermissions({
authData: req.authData,
workspaceId: new Types.ObjectId(workspaceId)
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Create,
ProjectPermissionSub.Tags
);
const tagToCreate = {
name,
workspace: new Types.ObjectId(workspaceId),
slug,
user: new Types.ObjectId(req.user._id)
};
const createdTag = await new Tag(tagToCreate).save();
res.json(createdTag);
};
export const deleteWorkspaceTag = async (req: Request, res: Response) => {
const {
params: { tagId }
} = await validateRequest(reqValidator.DeleteWorkspaceTagsV2, req);
const tagFromDB = await Tag.findById(tagId);
if (!tagFromDB) {
throw BadRequestError();
}
const { permission } = await getAuthDataProjectPermissions({
authData: req.authData,
workspaceId: tagFromDB.workspace
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Delete,
ProjectPermissionSub.Tags
);
const result = await Tag.findByIdAndDelete(tagId);
// remove the tag from secrets
await Secret.updateMany({ tags: { $in: [tagId] } }, { $pull: { tags: tagId } });
res.json(result);
};
export const getWorkspaceTags = async (req: Request, res: Response) => {
const {
params: { workspaceId }
} = await validateRequest(reqValidator.GetWorkspaceTagsV2, req);
const { permission } = await getAuthDataProjectPermissions({
authData: req.authData,
workspaceId: new Types.ObjectId(workspaceId)
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read,
ProjectPermissionSub.Tags
);
const workspaceTags = await Tag.find({
workspace: new Types.ObjectId(workspaceId)
});
return res.json({
workspaceTags
});
};

View File

@ -0,0 +1,314 @@
import { Request, Response } from "express";
import { Types } from "mongoose";
import crypto from "crypto";
import bcrypt from "bcrypt";
import { APIKeyData, AuthMethod, MembershipOrg, TokenVersion, User } from "../../models";
import { getSaltRounds } from "../../config";
import { validateRequest } from "../../helpers/validation";
import { deleteUser } from "../../helpers/user";
import * as reqValidator from "../../validation";
/**
* Update the current user's MFA-enabled status [isMfaEnabled].
* Note: Infisical currently only supports email-based 2FA only; this will expand to
* include SMS and authenticator app modes of authentication in the future.
* @param req
* @param res
* @returns
*/
export const updateMyMfaEnabled = async (req: Request, res: Response) => {
const {
body: { isMfaEnabled }
} = await validateRequest(reqValidator.UpdateMyMfaEnabledV2, req);
req.user.isMfaEnabled = isMfaEnabled;
if (isMfaEnabled) {
// TODO: adapt this route/controller
// to work for different forms of MFA
req.user.mfaMethods = ["email"];
} else {
req.user.mfaMethods = [];
}
await req.user.save();
const user = req.user;
return res.status(200).send({
user
});
};
/**
* Update name of the current user to [firstName, lastName].
* @param req
* @param res
* @returns
*/
export const updateName = async (req: Request, res: Response) => {
const {
body: { lastName, firstName }
} = await validateRequest(reqValidator.UpdateNameV2, req);
const user = await User.findByIdAndUpdate(
req.user._id.toString(),
{
firstName,
lastName: lastName ?? ""
},
{
new: true
}
);
return res.status(200).send({
user
});
};
/**
* Update auth method of the current user to [authMethods]
* @param req
* @param res
* @returns
*/
export const updateAuthMethods = async (req: Request, res: Response) => {
const {
body: { authMethods }
} = await validateRequest(reqValidator.UpdateAuthMethodsV2, req);
const hasSamlEnabled = req.user.authMethods.some((authMethod: AuthMethod) =>
[AuthMethod.OKTA_SAML, AuthMethod.AZURE_SAML, AuthMethod.JUMPCLOUD_SAML].includes(authMethod)
);
if (hasSamlEnabled) {
return res.status(400).send({
message: "Failed to update user authentication method because SAML SSO is enforced"
});
}
const user = await User.findByIdAndUpdate(
req.user._id.toString(),
{
authMethods
},
{
new: true
}
);
return res.status(200).send({
user
});
};
/**
* Return organizations that the current user is part of.
* @param req
* @param res
*/
export const getMyOrganizations = async (req: Request, res: Response) => {
/*
#swagger.summary = 'Return organizations that current user is part of'
#swagger.description = 'Return organizations that current user is part of'
#swagger.security = [{
"apiKeyAuth": []
}]
#swagger.responses[200] = {
content: {
"application/json": {
"schema": {
"type": "object",
"properties": {
"organizations": {
"type": "array",
"items": {
$ref: "#/components/schemas/Organization"
},
"description": "Organizations that user is part of"
}
}
}
}
}
}
*/
const organizations = (
await MembershipOrg.find({
user: req.user._id
}).populate("organization")
).map((m) => m.organization);
return res.status(200).send({
organizations
});
};
/**
* Return API keys belonging to current user.
* @param req
* @param res
* @returns
*/
export const getMyAPIKeys = async (req: Request, res: Response) => {
const apiKeyData = await APIKeyData.find({
user: req.user._id
});
return res.status(200).send(apiKeyData);
};
/**
* Create new API key for current user.
* @param req
* @param res
* @returns
*/
export const createAPIKey = async (req: Request, res: Response) => {
const {
body: { name, expiresIn }
} = await validateRequest(reqValidator.CreateApiKeyV2, req);
const secret = crypto.randomBytes(16).toString("hex");
const secretHash = await bcrypt.hash(secret, await getSaltRounds());
const expiresAt = new Date();
expiresAt.setSeconds(expiresAt.getSeconds() + expiresIn);
let apiKeyData = await new APIKeyData({
name,
lastUsed: new Date(),
expiresAt,
user: req.user._id,
secretHash
}).save();
// return api key data without sensitive data
apiKeyData = (await APIKeyData.findById(apiKeyData._id)) as any;
if (!apiKeyData) throw new Error("Failed to find API key data");
const apiKey = `ak.${apiKeyData._id.toString()}.${secret}`;
return res.status(200).send({
apiKey,
apiKeyData
});
};
/**
* Delete API key with id [apiKeyDataId] belonging to current user
* @param req
* @param res
*/
export const deleteAPIKey = async (req: Request, res: Response) => {
const {
params: { apiKeyDataId }
} = await validateRequest(reqValidator.DeleteApiKeyV2, req);
const apiKeyData = await APIKeyData.findOneAndDelete({
_id: new Types.ObjectId(apiKeyDataId),
user: req.user._id
});
return res.status(200).send({
apiKeyData
});
};
/**
* Return active sessions (TokenVersion) belonging to user
* @param req
* @param res
* @returns
*/
export const getMySessions = async (req: Request, res: Response) => {
const tokenVersions = await TokenVersion.find({
user: req.user._id
});
return res.status(200).send(tokenVersions);
};
/**
* Revoke all active sessions belong to user
* @param req
* @param res
* @returns
*/
export const deleteMySessions = async (req: Request, res: Response) => {
await TokenVersion.updateMany(
{
user: req.user._id
},
{
$inc: {
refreshVersion: 1,
accessVersion: 1
}
}
);
return res.status(200).send({
message: "Successfully revoked all sessions"
});
};
/**
* Return the current user.
* @param req
* @param res
* @returns
*/
export const getMe = async (req: Request, res: Response) => {
/*
#swagger.summary = "Retrieve the current user on the request"
#swagger.description = "Retrieve the current user on the request"
#swagger.security = [{
"apiKeyAuth": []
}]
#swagger.responses[200] = {
content: {
"application/json": {
"schema": {
"type": "object",
"properties": {
"user": {
"type": "object",
$ref: "#/components/schemas/CurrentUser",
"description": "Current user on request"
}
}
}
}
}
}
*/
const user = await User.findById(req.user._id).select(
"+salt +publicKey +encryptedPrivateKey +iv +tag +encryptionVersion +protectedKey +protectedKeyIV +protectedKeyTag"
);
return res.status(200).send({
user
});
};
/**
* Delete the current user.
* @param req
* @param res
*/
export const deleteMe = async (req: Request, res: Response) => {
const user = await deleteUser({
userId: req.user._id
});
return res.status(200).send({
user
});
}

View File

@ -0,0 +1,883 @@
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
});
}

View File

@ -0,0 +1,224 @@
/* eslint-disable @typescript-eslint/no-var-requires */
import { Request, Response } from "express";
import jwt from "jsonwebtoken";
import * as bigintConversion from "bigint-conversion";
const jsrp = require("jsrp");
import { LoginSRPDetail, User } from "../../models";
import { createToken, issueAuthTokens, validateProviderAuthToken } from "../../helpers/auth";
import { checkUserDevice } from "../../helpers/user";
import { sendMail } from "../../helpers/nodemailer";
import { TokenService } from "../../services";
import { BadRequestError, InternalServerError } from "../../utils/errors";
import { AuthTokenType, TOKEN_EMAIL_MFA } from "../../variables";
import { getAuthSecret, getHttpsEnabled, getJwtMfaLifetime } from "../../config";
import { AuthMethod } from "../../models/user";
import { validateRequest } from "../../helpers/validation";
import * as reqValidator from "../../validation/auth";
declare module "jsonwebtoken" {
export interface ProviderAuthJwtPayload extends jwt.JwtPayload {
userId: string;
email: string;
authProvider: AuthMethod;
isUserCompleted: boolean;
}
}
/**
* Log in user step 1: Return [salt] and [serverPublicKey] as part of step 1 of SRP protocol
* @param req
* @param res
* @returns
*/
export const login1 = async (req: Request, res: Response) => {
const {
body: { email, clientPublicKey, providerAuthToken }
} = await validateRequest(reqValidator.Login1V3, req);
const user = await User.findOne({
email
}).select("+salt +verifier");
if (!user) throw new Error("Failed to find user");
if (!user.authMethods.includes(AuthMethod.EMAIL)) {
await validateProviderAuthToken({
email,
providerAuthToken
});
}
const server = new jsrp.server();
server.init(
{
salt: user.salt,
verifier: user.verifier
},
async () => {
// generate server-side public key
const serverPublicKey = server.getPublicKey();
await LoginSRPDetail.findOneAndReplace(
{
email: email
},
{
email,
userId: user.id,
clientPublicKey: clientPublicKey,
serverBInt: bigintConversion.bigintToBuf(server.bInt)
},
{ upsert: true, returnNewDocument: false }
);
return res.status(200).send({
serverPublicKey,
salt: user.salt
});
}
);
};
/**
* Log in user step 2: complete step 2 of SRP protocol and return token and their (encrypted)
* private key
* @param req
* @param res
* @returns
*/
export const login2 = async (req: Request, res: Response) => {
if (!req.headers["user-agent"])
throw InternalServerError({ message: "User-Agent header is required" });
const {
body: { email, providerAuthToken, clientProof }
} = await validateRequest(reqValidator.Login2V3, req);
const user = await User.findOne({
email
}).select(
"+salt +verifier +encryptionVersion +protectedKey +protectedKeyIV +protectedKeyTag +publicKey +encryptedPrivateKey +iv +tag +devices"
);
if (!user) throw new Error("Failed to find user");
if (!user.authMethods.includes(AuthMethod.EMAIL)) {
await validateProviderAuthToken({
email,
providerAuthToken
});
}
const loginSRPDetail = await LoginSRPDetail.findOneAndDelete({ email: email });
if (!loginSRPDetail) {
return BadRequestError(Error("Failed to find login details for SRP"));
}
const server = new jsrp.server();
server.init(
{
salt: user.salt,
verifier: user.verifier,
b: loginSRPDetail.serverBInt
},
async () => {
server.setClientPublicKey(loginSRPDetail.clientPublicKey);
// compare server and client shared keys
if (server.checkClientProof(clientProof)) {
if (user.isMfaEnabled) {
// case: user has MFA enabled
// generate temporary MFA token
const token = createToken({
payload: {
authTokenType: AuthTokenType.MFA_TOKEN,
userId: user._id.toString()
},
expiresIn: await getJwtMfaLifetime(),
secret: await getAuthSecret()
});
const code = await TokenService.createToken({
type: TOKEN_EMAIL_MFA,
email
});
// send MFA code [code] to [email]
await sendMail({
template: "emailMfa.handlebars",
subjectLine: "Infisical MFA code",
recipients: [user.email],
substitutions: {
code
}
});
return res.status(200).send({
mfaEnabled: true,
token
});
}
await checkUserDevice({
user,
ip: req.realIP,
userAgent: req.headers["user-agent"] ?? ""
});
// issue tokens
const tokens = await issueAuthTokens({
userId: user._id,
ip: req.realIP,
userAgent: req.headers["user-agent"] ?? ""
});
// store (refresh) token in httpOnly cookie
res.cookie("jid", tokens.refreshToken, {
httpOnly: true,
path: "/",
sameSite: "strict",
secure: await getHttpsEnabled()
});
// case: user does not have MFA enablgged
// return (access) token in response
interface ResponseData {
mfaEnabled: boolean;
encryptionVersion: any;
protectedKey?: string;
protectedKeyIV?: string;
protectedKeyTag?: string;
token: string;
publicKey?: string;
encryptedPrivateKey?: string;
iv?: string;
tag?: string;
}
const response: ResponseData = {
mfaEnabled: false,
encryptionVersion: user.encryptionVersion,
token: tokens.token,
publicKey: user.publicKey,
encryptedPrivateKey: user.encryptedPrivateKey,
iv: user.iv,
tag: user.tag
};
if (user?.protectedKey && user?.protectedKeyIV && user?.protectedKeyTag) {
response.protectedKey = user.protectedKey;
response.protectedKeyIV = user.protectedKeyIV;
response.protectedKeyTag = user.protectedKeyTag;
}
return res.status(200).send(response);
}
return res.status(400).send({
message: "Failed to authenticate. Try again?"
});
}
);
};

View File

@ -0,0 +1,13 @@
import * as usersController from "./usersController";
import * as secretsController from "./secretsController";
import * as workspacesController from "./workspacesController";
import * as authController from "./authController";
import * as signupController from "./signupController";
export {
usersController,
authController,
secretsController,
signupController,
workspacesController
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,194 @@
import jwt from "jsonwebtoken";
import { Request, Response } from "express";
import * as Sentry from "@sentry/node";
import { MembershipOrg, User } from "../../models";
import { completeAccount } from "../../helpers/user";
import { initializeDefaultOrg } from "../../helpers/signup";
import { issueAuthTokens, validateProviderAuthToken } from "../../helpers/auth";
import { ACCEPTED, AuthTokenType, INVITED } from "../../variables";
import { standardRequest } from "../../config/request";
import { getAuthSecret, getHttpsEnabled, getLoopsApiKey } from "../../config";
import { BadRequestError, UnauthorizedRequestError } from "../../utils/errors";
import { TelemetryService } from "../../services";
import { AuthMethod } from "../../models";
import { validateRequest } from "../../helpers/validation";
import * as reqValidator from "../../validation/auth";
/**
* Complete setting up user by adding their personal and auth information as part of the
* signup flow
* @param req
* @param res
* @returns
*/
export const completeAccountSignup = async (req: Request, res: Response) => {
let user, token;
try {
const {
body: {
email,
publicKey,
salt,
lastName,
verifier,
firstName,
protectedKey,
protectedKeyIV,
protectedKeyTag,
organizationName,
providerAuthToken,
attributionSource,
encryptedPrivateKey,
encryptedPrivateKeyIV,
encryptedPrivateKeyTag
}
} = await validateRequest(reqValidator.CompletedAccountSignupV3, req);
user = await User.findOne({ email });
if (!user || (user && user?.publicKey)) {
// case 1: user doesn't exist.
// case 2: user has already completed account
return res.status(403).send({
error: "Failed to complete account for complete user"
});
}
if (providerAuthToken) {
await validateProviderAuthToken({
email,
providerAuthToken
});
} else {
const [AUTH_TOKEN_TYPE, AUTH_TOKEN_VALUE] = <[string, string]>(
req.headers["authorization"]?.split(" ", 2)
) ?? [null, null];
if (AUTH_TOKEN_TYPE === null) {
throw BadRequestError({ message: "Missing Authorization Header in the request header." });
}
if (AUTH_TOKEN_TYPE.toLowerCase() !== "bearer") {
throw BadRequestError({
message: `The provided authentication type '${AUTH_TOKEN_TYPE}' is not supported.`
});
}
if (AUTH_TOKEN_VALUE === null) {
throw BadRequestError({
message: "Missing Authorization Body in the request header"
});
}
const decodedToken = <jwt.UserIDJwtPayload>(
jwt.verify(AUTH_TOKEN_VALUE, await getAuthSecret())
);
if (decodedToken.authTokenType !== AuthTokenType.SIGNUP_TOKEN) throw UnauthorizedRequestError();
if (decodedToken.userId !== user.id) throw UnauthorizedRequestError();
}
// complete setting up user's account
user = await completeAccount({
userId: user._id.toString(),
firstName,
lastName,
encryptionVersion: 2,
protectedKey,
protectedKeyIV,
protectedKeyTag,
publicKey,
encryptedPrivateKey,
encryptedPrivateKeyIV,
encryptedPrivateKeyTag,
salt,
verifier
});
if (!user) throw new Error("Failed to complete account for non-existent user"); // ensure user is non-null
// this might need to consider LDAP
const hasOrgAuthMethodEnabled = user.authMethods.some((authMethod: AuthMethod) =>
[AuthMethod.OKTA_SAML, AuthMethod.AZURE_SAML, AuthMethod.JUMPCLOUD_SAML, AuthMethod.LDAP].includes(authMethod)
);
if (!hasOrgAuthMethodEnabled) {
// TODO: modify this part
// initialize default organization and workspace
await initializeDefaultOrg({
organizationName,
user
});
}
// update organization membership statuses that are
// invited to completed with user attached
await MembershipOrg.updateMany(
{
inviteEmail: email,
status: INVITED
},
{
user,
status: ACCEPTED
}
);
// issue tokens
const tokens = await issueAuthTokens({
userId: user._id,
ip: req.realIP,
userAgent: req.headers["user-agent"] ?? ""
});
token = tokens.token;
// sending a welcome email to new users
if (await getLoopsApiKey()) {
await standardRequest.post(
"https://app.loops.so/api/v1/events/send",
{
email: email,
eventName: "Sign Up",
firstName: firstName,
lastName: lastName
},
{
headers: {
Accept: "application/json",
Authorization: "Bearer " + (await getLoopsApiKey())
}
}
);
}
// store (refresh) token in httpOnly cookie
res.cookie("jid", tokens.refreshToken, {
httpOnly: true,
path: "/",
sameSite: "strict",
secure: await getHttpsEnabled()
});
const postHogClient = await TelemetryService.getPostHogClient();
if (postHogClient) {
postHogClient.capture({
event: "User Signed Up",
distinctId: email,
properties: {
email,
...(attributionSource ? { attributionSource } : {})
}
});
}
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
return res.status(400).send({
message: "Failed to complete account setup"
});
}
return res.status(200).send({
message: "Successfully set up account",
user,
token
});
};

View File

@ -0,0 +1,18 @@
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
});
}

View File

@ -0,0 +1,142 @@
import { Request, Response } from "express";
import { Types } from "mongoose";
import { validateRequest } from "../../helpers/validation";
import { Membership, Secret, User } from "../../models";
import { SecretService } from "../../services";
import { getAuthDataProjectPermissions } from "../../ee/services/ProjectRoleService";
import { UnauthorizedRequestError } from "../../utils/errors";
import * as reqValidator from "../../validation/workspace";
/**
* Return whether or not all secrets in workspace with id [workspaceId]
* are blind-indexed
* @param req
* @param res
* @returns
*/
export const getWorkspaceBlindIndexStatus = async (req: Request, res: Response) => {
const {
params: { workspaceId }
} = await validateRequest(reqValidator.GetWorkspaceBlinkIndexStatusV3, req);
await getAuthDataProjectPermissions({
authData: req.authData,
workspaceId: new Types.ObjectId(workspaceId)
});
if (req.authData.authPayload instanceof User) {
const membership = await Membership.findOne({
user: req.authData.authPayload._id,
workspace: new Types.ObjectId(workspaceId)
});
if (!membership) throw UnauthorizedRequestError();
if (membership.role !== "admin")
throw UnauthorizedRequestError({ message: "User must be an admin" });
}
const secretsWithoutBlindIndex = await Secret.countDocuments({
workspace: new Types.ObjectId(workspaceId),
secretBlindIndex: {
$exists: false
}
});
return res.status(200).send(secretsWithoutBlindIndex === 0);
};
/**
* Get all secrets for workspace with id [workspaceId]
*/
export const getWorkspaceSecrets = async (req: Request, res: Response) => {
const {
params: { workspaceId }
} = await validateRequest(reqValidator.GetWorkspaceSecretsV3, req);
await getAuthDataProjectPermissions({
authData: req.authData,
workspaceId: new Types.ObjectId(workspaceId)
});
if (req.authData.authPayload instanceof User) {
const membership = await Membership.findOne({
user: req.authData.authPayload._id,
workspace: new Types.ObjectId(workspaceId)
});
if (!membership) throw UnauthorizedRequestError();
if (membership.role !== "admin")
throw UnauthorizedRequestError({ message: "User must be an admin" });
}
const secrets = await Secret.find({
workspace: new Types.ObjectId(workspaceId)
});
return res.status(200).send({
secrets
});
};
/**
* Update blind indices for secrets in workspace with id [workspaceId]
* @param req
* @param res
*/
export const nameWorkspaceSecrets = async (req: Request, res: Response) => {
const {
params: { workspaceId },
body: { secretsToUpdate }
} = await validateRequest(reqValidator.NameWorkspaceSecretsV3, req);
await getAuthDataProjectPermissions({
authData: req.authData,
workspaceId: new Types.ObjectId(workspaceId)
});
if (req.authData.authPayload instanceof User) {
const membership = await Membership.findOne({
user: req.authData.authPayload._id,
workspace: new Types.ObjectId(workspaceId)
});
if (!membership) throw UnauthorizedRequestError();
if (membership.role !== "admin")
throw UnauthorizedRequestError({ message: "User must be an admin" });
}
// get secret blind index salt
const salt = await SecretService.getSecretBlindIndexSalt({
workspaceId: new Types.ObjectId(workspaceId)
});
// update secret blind indices
const operations = await Promise.all(
secretsToUpdate.map(async (secretToUpdate) => {
const secretBlindIndex = await SecretService.generateSecretBlindIndexWithSalt({
secretName: secretToUpdate.secretName,
salt
});
return {
updateOne: {
filter: {
_id: new Types.ObjectId(secretToUpdate._id)
},
update: {
secretBlindIndex
}
}
};
})
);
await Secret.bulkWrite(operations);
return res.status(200).send({
message: "Successfully named workspace secrets"
});
};

View File

@ -3516,4 +3516,4 @@ zxcvbnm.com
zymuying.com
zzi.us
zzrgg.com
zzz.com
zzz.com

View File

@ -1,2 +0,0 @@
export type { TDbClient } from "./instance";
export { initDbConnection } from "./instance";

View File

@ -1,11 +0,0 @@
import knex from "knex";
export type TDbClient = ReturnType<typeof initDbConnection>;
export const initDbConnection = (dbConnectionUri: string) => {
const db = knex({
client: "pg",
connection: dbConnectionUri
});
return db;
};

View File

@ -1,39 +0,0 @@
// eslint-disable-next-line
import "ts-node/register";
import dotenv from "dotenv";
import type { Knex } from "knex";
import path from "path";
// Update with your config settings.
dotenv.config({
path: path.join(__dirname, "../../.env"),
debug: true
});
export default {
development: {
client: "postgres",
connection: process.env.DB_CONNECTION_URI,
pool: {
min: 2,
max: 10
},
seeds: {
directory: "./seeds"
},
migrations: {
tableName: "infisical_migrations"
}
},
production: {
client: "postgres",
connection: process.env.DB_CONNECTION_URI,
pool: {
min: 2,
max: 10
},
migrations: {
tableName: "infisical_migrations"
}
}
} as Knex.Config;

View File

@ -1,37 +0,0 @@
import { Knex } from "knex";
import { TableName } from "../schemas";
import {
createOnUpdateTrigger,
createUpdateAtTriggerFunction,
dropOnUpdateTrigger,
dropUpdatedAtTriggerFunction
} from "../utils";
export async function up(knex: Knex): Promise<void> {
const isTablePresent = await knex.schema.hasTable(TableName.Users);
if (!isTablePresent) {
await knex.schema.createTable(TableName.Users, (t) => {
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
t.string("email").unique().notNullable();
t.specificType("authMethods", "text[]");
t.boolean("superAdmin").defaultTo(false);
t.string("firstName");
t.string("lastName");
t.boolean("isAccepted").defaultTo(false);
t.boolean("isMfaEnabled").defaultTo(false);
t.specificType("mfaMethods", "text[]");
t.jsonb("devices");
t.timestamps(true, true, true);
});
}
// this is a one time function
await createUpdateAtTriggerFunction(knex);
await createOnUpdateTrigger(knex, TableName.Users);
}
export async function down(knex: Knex): Promise<void> {
await knex.schema.dropTableIfExists(TableName.Users);
await dropOnUpdateTrigger(knex, TableName.Users);
await dropUpdatedAtTriggerFunction(knex);
}

View File

@ -1,31 +0,0 @@
import { Knex } from "knex";
import { TableName } from "../schemas";
export async function up(knex: Knex): Promise<void> {
const isTablePresent = await knex.schema.hasTable(TableName.UserEncryptionKey);
if (!isTablePresent) {
await knex.schema.createTable(TableName.UserEncryptionKey, (t) => {
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
t.text("clientPublicKey");
t.text("serverPrivateKey");
t.integer("encryptionVersion").defaultTo(2);
t.text("protectedKey");
t.text("protectedKeyIV");
t.text("protectedKeyTag");
t.text("publicKey").notNullable();
t.text("encryptedPrivateKey").notNullable();
t.text("iv").notNullable();
t.text("tag").notNullable();
t.text("salt").notNullable();
t.text("verifier").notNullable();
// one to one relationship
t.uuid("userId").notNullable().unique();
t.foreign("userId").references("id").inTable(TableName.Users).onDelete("CASCADE");
});
}
}
export async function down(knex: Knex): Promise<void> {
await knex.schema.dropTableIfExists(TableName.UserEncryptionKey);
}

View File

@ -1,25 +0,0 @@
import { Knex } from "knex";
import { TableName } from "../schemas";
export async function up(knex: Knex): Promise<void> {
const isTablePresent = await knex.schema.hasTable(TableName.AuthTokens);
if (!isTablePresent) {
await knex.schema.createTable(TableName.AuthTokens, (t) => {
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
t.string("type").notNullable();
t.string("phoneNumber");
t.string("tokenHash").notNullable();
t.integer("triesLeft");
t.datetime("expiresAt").notNullable();
// does not need update trigger we will do it manually
t.timestamps(true, true, true);
t.uuid("userId");
t.foreign("userId").references("id").inTable(TableName.Users).onDelete("CASCADE");
});
}
}
export async function down(knex: Knex): Promise<void> {
await knex.schema.dropTableIfExists(TableName.AuthTokens);
}

View File

@ -1,29 +0,0 @@
import { Knex } from "knex";
import { TableName } from "../schemas";
import { createOnUpdateTrigger, dropOnUpdateTrigger } from "../utils";
export async function up(knex: Knex): Promise<void> {
const isTablePresent = await knex.schema.hasTable(TableName.AuthTokenSession);
if (!isTablePresent) {
await knex.schema.createTable(TableName.AuthTokenSession, (t) => {
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
t.string("ip").notNullable();
t.string("userAgent");
t.integer("refreshVersion").notNullable().defaultTo(1);
t.integer("accessVersion").notNullable().defaultTo(1);
t.datetime("lastUsed").notNullable();
// does not need update trigger we will do it manually
t.timestamps(true, true, true);
t.uuid("userId").notNullable();
t.foreign("userId").references("id").inTable(TableName.Users).onDelete("CASCADE");
});
}
// this is a one time function
await createOnUpdateTrigger(knex, TableName.AuthTokenSession);
}
export async function down(knex: Knex): Promise<void> {
await knex.schema.dropTableIfExists(TableName.AuthTokenSession);
await dropOnUpdateTrigger(knex, TableName.AuthTokenSession);
}

View File

@ -1,26 +0,0 @@
import { Knex } from "knex";
import { TableName } from "../schemas";
export async function up(knex: Knex): Promise<void> {
const doesTableExist = await knex.schema.hasTable(TableName.BackupPrivateKey);
if (!doesTableExist) {
await knex.schema.createTable(TableName.BackupPrivateKey, (t) => {
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
t.text("encryptedPrivateKey").notNullable();
t.text("iv").notNullable();
t.text("tag").notNullable();
t.string("algorithm").notNullable();
t.string("keyEncoding").notNullable();
t.text("salt").notNullable();
t.text("verifier").notNullable();
t.timestamps(true, true, true);
t.uuid("userId").notNullable().unique();
t.foreign("userId").references("id").inTable(TableName.Users).onDelete("CASCADE");
});
}
}
export async function down(knex: Knex): Promise<void> {
await knex.schema.dropTableIfExists(TableName.BackupPrivateKey);
}

View File

@ -1,35 +0,0 @@
import { Knex } from "knex";
import { TableName } from "../schemas";
import { createOnUpdateTrigger, dropOnUpdateTrigger } from "../utils";
export async function up(knex: Knex): Promise<void> {
const isTablePresent = await knex.schema.hasTable(TableName.Organization);
if (!isTablePresent) {
await knex.schema.createTable(TableName.Organization, (t) => {
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
t.string("name").notNullable();
t.string("customerId");
t.string("slug").notNullable();
// does not need update trigger we will do it manually
t.unique("slug");
t.timestamps(true, true, true);
});
await knex.schema.alterTable(TableName.AuthTokens, (t) => {
t.uuid("orgId");
t.foreign("orgId").references("id").inTable(TableName.Organization).onDelete("CASCADE");
});
}
// this is a one time function
await createOnUpdateTrigger(knex, TableName.Organization);
}
export async function down(knex: Knex): Promise<void> {
if (await knex.schema.hasColumn(TableName.AuthTokens, "orgId")) {
await knex.schema.alterTable(TableName.AuthTokens, (t) => {
t.dropColumn("orgId");
});
}
await knex.schema.dropTableIfExists(TableName.Organization);
await dropOnUpdateTrigger(knex, TableName.Organization);
}

View File

@ -1,48 +0,0 @@
import { Knex } from "knex";
import { TableName } from "../schemas";
import { OrgMembershipStatus } from "../schemas/models";
import { createOnUpdateTrigger, dropOnUpdateTrigger } from "../utils";
export async function up(knex: Knex): Promise<void> {
const isOrgRolePresent = await knex.schema.hasTable(TableName.OrgRoles);
if (!isOrgRolePresent) {
await knex.schema.createTable(TableName.OrgRoles, (t) => {
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
t.string("name").notNullable();
t.string("description");
t.string("slug").notNullable();
t.jsonb("permissions").notNullable();
// does not need update trigger we will do it manually
t.timestamps(true, true, true);
t.uuid("orgId").notNullable();
t.foreign("orgId").references("id").inTable(TableName.Organization).onDelete("CASCADE");
});
}
const isOrgTablePresent = await knex.schema.hasTable(TableName.OrgMembership);
if (!isOrgTablePresent) {
await knex.schema.createTable(TableName.OrgMembership, (t) => {
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
t.string("role").notNullable();
t.string("status").notNullable().defaultTo(OrgMembershipStatus.Invited);
t.string("inviteEmail");
// does not need update trigger we will do it manually
t.timestamps(true, true, true);
t.uuid("userId");
t.foreign("userId").references("id").inTable(TableName.Users).onDelete("CASCADE");
t.uuid("orgId").notNullable();
t.foreign("orgId").references("id").inTable(TableName.Organization).onDelete("CASCADE");
t.uuid("roleId");
t.foreign("roleId").references("id").inTable(TableName.OrgRoles);
});
}
// this is a one time function
await createOnUpdateTrigger(knex, TableName.OrgMembership);
}
export async function down(knex: Knex): Promise<void> {
await knex.schema.dropTableIfExists(TableName.OrgMembership);
await knex.schema.dropTableIfExists(TableName.OrgRoles);
await dropOnUpdateTrigger(knex, TableName.OrgMembership);
}

View File

@ -1,25 +0,0 @@
import { Knex } from "knex";
import { TableName } from "../schemas";
import { createOnUpdateTrigger, dropOnUpdateTrigger } from "../utils";
export async function up(knex: Knex): Promise<void> {
const isTablePresent = await knex.schema.hasTable(TableName.IncidentContact);
if (!isTablePresent) {
await knex.schema.createTable(TableName.IncidentContact, (t) => {
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
t.string("email").notNullable();
// does not need update trigger we will do it manually
t.timestamps(true, true, true);
t.uuid("orgId").notNullable();
t.foreign("orgId").references("id").inTable(TableName.Organization).onDelete("CASCADE");
});
}
// this is a one time function
await createOnUpdateTrigger(knex, TableName.IncidentContact);
}
export async function down(knex: Knex): Promise<void> {
await knex.schema.dropTableIfExists(TableName.IncidentContact);
await dropOnUpdateTrigger(knex, TableName.IncidentContact);
}

View File

@ -1,20 +0,0 @@
import { Knex } from "knex";
import { TableName } from "../schemas";
export async function up(knex: Knex): Promise<void> {
const isTablePresent = await knex.schema.hasTable(TableName.UserAction);
if (!isTablePresent) {
await knex.schema.createTable(TableName.UserAction, (t) => {
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
t.string("action").notNullable();
t.timestamps(true, true, true);
t.uuid("userId").notNullable();
t.foreign("userId").references("id").inTable(TableName.Users).onDelete("CASCADE");
});
}
}
export async function down(knex: Knex): Promise<void> {
await knex.schema.dropTableIfExists(TableName.UserAction);
}

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