Fix merge conflicts

This commit is contained in:
Tuan Dang
2023-07-26 23:13:33 +07:00
448 changed files with 23922 additions and 10296 deletions
+2
View File
@@ -47,11 +47,13 @@ CLIENT_ID_VERCEL=
CLIENT_ID_NETLIFY= CLIENT_ID_NETLIFY=
CLIENT_ID_GITHUB= CLIENT_ID_GITHUB=
CLIENT_ID_GITLAB= CLIENT_ID_GITLAB=
CLIENT_ID_BITBUCKET=
CLIENT_SECRET_HEROKU= CLIENT_SECRET_HEROKU=
CLIENT_SECRET_VERCEL= CLIENT_SECRET_VERCEL=
CLIENT_SECRET_NETLIFY= CLIENT_SECRET_NETLIFY=
CLIENT_SECRET_GITHUB= CLIENT_SECRET_GITHUB=
CLIENT_SECRET_GITLAB= CLIENT_SECRET_GITLAB=
CLIENT_SECRET_BITBUCKET=
CLIENT_SLUG_VERCEL= CLIENT_SLUG_VERCEL=
# Sentry (optional) for monitoring errors # Sentry (optional) for monitoring errors
+8 -8
View File
@@ -1,9 +1,9 @@
secretScanningGitApp: # secretScanningGitApp:
enabled: true # enabled: false
deploymentAnnotations: # deploymentAnnotations:
secrets.infisical.com/auto-reload: "true" # secrets.infisical.com/auto-reload: "true"
image: # image:
repository: infisical/staging_deployment_secret-scanning-git-app # repository: infisical/staging_deployment_secret-scanning-git-app
frontend: frontend:
enabled: true enabled: true
@@ -58,8 +58,8 @@ mongodbConnection:
ingress: ingress:
enabled: true enabled: true
annotations: # annotations:
kubernetes.io/ingress.class: "nginx" # kubernetes.io/ingress.class: "nginx"
# cert-manager.io/issuer: letsencrypt-nginx # cert-manager.io/issuer: letsencrypt-nginx
hostName: gamma.infisical.com ## <- Replace with your own domain hostName: gamma.infisical.com ## <- Replace with your own domain
frontend: frontend:
+1 -1
View File
@@ -135,7 +135,7 @@ jobs:
- name: Download helm values to file and upgrade gamma deploy - name: Download helm values to file and upgrade gamma deploy
run: | run: |
wget https://raw.githubusercontent.com/Infisical/infisical/main/.github/values.yaml wget https://raw.githubusercontent.com/Infisical/infisical/main/.github/values.yaml
helm upgrade infisical infisical-helm-charts/infisical --values values.yaml --wait helm upgrade infisical infisical-helm-charts/infisical --values values.yaml --wait --install
if [[ $(helm status infisical) == *"FAILED"* ]]; then if [[ $(helm status infisical) == *"FAILED"* ]]; then
echo "Helm upgrade failed" echo "Helm upgrade failed"
exit 1 exit 1
@@ -1,11 +1,17 @@
name: Release standalone docker image name: Release standalone docker image
on: [workflow_dispatch] on:
push:
tags:
- "infisical/v*.*.*"
jobs: jobs:
infisical-standalone: infisical-standalone:
name: Build infisical standalone image name: Build infisical standalone image
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Extract version from tag
id: extract_version
run: echo "::set-output name=version::${GITHUB_REF_NAME#infisical/}"
- name: ☁️ Checkout source - name: ☁️ Checkout source
uses: actions/checkout@v3 uses: actions/checkout@v3
with: with:
@@ -64,5 +70,6 @@ jobs:
tags: | tags: |
infisical/infisical:latest infisical/infisical:latest
infisical/infisical:${{ steps.commit.outputs.short }} infisical/infisical:${{ steps.commit.outputs.short }}
infisical/infisical:${{ steps.extract_version.outputs.version }}
platforms: linux/amd64,linux/arm64 platforms: linux/amd64,linux/arm64
file: Dockerfile.standalone-infisical file: Dockerfile.standalone-infisical
@@ -1,10 +1,16 @@
name: Release Docker image for K8 operator name: Release Docker image for K8 operator
on: [workflow_dispatch] on:
push:
tags:
- "infisical-k8-operator/v*.*.*"
jobs: jobs:
release: release:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Extract version from tag
id: extract_version
run: echo "::set-output name=version::${GITHUB_REF_NAME#infisical-k8-operator/}"
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- name: 🔧 Set up QEMU - name: 🔧 Set up QEMU
@@ -26,4 +32,6 @@ jobs:
context: k8-operator context: k8-operator
push: true push: true
platforms: linux/amd64,linux/arm64 platforms: linux/amd64,linux/arm64
tags: infisical/kubernetes-operator:latest tags: |
infisical/kubernetes-operator:latest
infisical/kubernetes-operator:${{ steps.extract_version.outputs.version }}
+16
View File
@@ -108,6 +108,22 @@ brews:
zsh_completion.install "completions/infisical.zsh" => "_infisical" zsh_completion.install "completions/infisical.zsh" => "_infisical"
fish_completion.install "completions/infisical.fish" fish_completion.install "completions/infisical.fish"
man1.install "manpages/infisical.1.gz" man1.install "manpages/infisical.1.gz"
- name: 'infisical@{{.Version}}'
tap:
owner: Infisical
name: homebrew-get-cli
commit_author:
name: "Infisical"
email: [email protected]
folder: Formula
homepage: "https://infisical.com"
description: "The official Infisical CLI"
install: |-
bin.install "infisical"
bash_completion.install "completions/infisical.bash" => "infisical"
zsh_completion.install "completions/infisical.zsh" => "_infisical"
fish_completion.install "completions/infisical.fish"
man1.install "manpages/infisical.1.gz"
nfpms: nfpms:
- id: infisical - id: infisical
+47 -26
View File
File diff suppressed because one or more lines are too long
+2 -6
View File
@@ -10,6 +10,7 @@
"plugin:@typescript-eslint/recommended" "plugin:@typescript-eslint/recommended"
], ],
"rules": { "rules": {
"no-empty-function": "off",
"@typescript-eslint/no-empty-function": "off", "@typescript-eslint/no-empty-function": "off",
"no-console": 2, "no-console": 2,
"quotes": [ "quotes": [
@@ -34,11 +35,6 @@
"argsIgnorePattern": "^_" "argsIgnorePattern": "^_"
} }
], ],
"sort-imports": [ "sort-imports": 1
"error",
{
"ignoreDeclarationSort": true
}
]
} }
} }
+4
View File
@@ -19,6 +19,10 @@ RUN npm ci --only-production
COPY --from=build /app . COPY --from=build /app .
RUN apk add --no-cache bash curl && curl -1sLf \
'https://dl.cloudsmith.io/public/infisical/infisical-cli/setup.alpine.sh' | bash \
&& apk add infisical=0.8.1
HEALTHCHECK --interval=10s --timeout=3s --start-period=10s \ HEALTHCHECK --interval=10s --timeout=3s --start-period=10s \
CMD node healthcheck.js CMD node healthcheck.js
+6096 -2910
View File
File diff suppressed because it is too large Load Diff
+4
View File
@@ -2,6 +2,7 @@
"dependencies": { "dependencies": {
"@aws-sdk/client-secrets-manager": "^3.319.0", "@aws-sdk/client-secrets-manager": "^3.319.0",
"@godaddy/terminus": "^4.12.0", "@godaddy/terminus": "^4.12.0",
"@node-saml/passport-saml": "^4.0.4",
"@octokit/rest": "^19.0.5", "@octokit/rest": "^19.0.5",
"@sentry/node": "^7.49.0", "@sentry/node": "^7.49.0",
"@sentry/tracing": "^7.48.0", "@sentry/tracing": "^7.48.0",
@@ -36,6 +37,7 @@
"passport": "^0.6.0", "passport": "^0.6.0",
"passport-google-oauth20": "^2.0.0", "passport-google-oauth20": "^2.0.0",
"posthog-node": "^2.6.0", "posthog-node": "^2.6.0",
"probot": "^12.3.1",
"query-string": "^7.1.3", "query-string": "^7.1.3",
"rate-limit-mongo": "^2.3.2", "rate-limit-mongo": "^2.3.2",
"rimraf": "^3.0.2", "rimraf": "^3.0.2",
@@ -89,6 +91,7 @@
"@types/node": "^18.11.3", "@types/node": "^18.11.3",
"@types/nodemailer": "^6.4.6", "@types/nodemailer": "^6.4.6",
"@types/passport": "^1.0.12", "@types/passport": "^1.0.12",
"@types/picomatch": "^2.3.0",
"@types/supertest": "^2.0.12", "@types/supertest": "^2.0.12",
"@types/swagger-jsdoc": "^6.0.1", "@types/swagger-jsdoc": "^6.0.1",
"@types/swagger-ui-express": "^4.1.3", "@types/swagger-ui-express": "^4.1.3",
@@ -102,6 +105,7 @@
"jest-junit": "^15.0.0", "jest-junit": "^15.0.0",
"nodemon": "^2.0.19", "nodemon": "^2.0.19",
"npm": "^8.19.3", "npm": "^8.19.3",
"smee-client": "^1.2.3",
"supertest": "^6.3.3", "supertest": "^6.3.3",
"ts-jest": "^29.0.3", "ts-jest": "^29.0.3",
"ts-node": "^10.9.1" "ts-node": "^10.9.1"
+8 -1
View File
@@ -10,7 +10,7 @@ export const getEncryptionKey = async () => {
return secretValue === "" ? undefined : secretValue; return secretValue === "" ? undefined : secretValue;
} }
export const getRootEncryptionKey = async () => { export const getRootEncryptionKey = async () => {
const secretValue = (await client.getSecret("ROOT_ENCRYPTION_KEY")).secretValue; const secretValue = (await client.getSecret("ROOT_ENCRYPTION_KEY")).secretValue;
return secretValue === "" ? undefined : secretValue; return secretValue === "" ? undefined : secretValue;
} }
export const getInviteOnlySignup = async () => (await client.getSecret("INVITE_ONLY_SIGNUP")).secretValue === "true" export const getInviteOnlySignup = async () => (await client.getSecret("INVITE_ONLY_SIGNUP")).secretValue === "true"
@@ -37,6 +37,7 @@ export const getClientIdNetlify = async () => (await client.getSecret("CLIENT_ID
export const getClientIdGitHub = async () => (await client.getSecret("CLIENT_ID_GITHUB")).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 getClientIdGitLab = async () => (await client.getSecret("CLIENT_ID_GITLAB")).secretValue;
export const getClientIdGoogle = async () => (await client.getSecret("CLIENT_ID_GOOGLE")).secretValue; export const getClientIdGoogle = async () => (await client.getSecret("CLIENT_ID_GOOGLE")).secretValue;
export const getClientIdBitBucket = async () => (await client.getSecret("CLIENT_ID_BITBUCKET")).secretValue;
export const getClientSecretAzure = async () => (await client.getSecret("CLIENT_SECRET_AZURE")).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 getClientSecretHeroku = async () => (await client.getSecret("CLIENT_SECRET_HEROKU")).secretValue;
export const getClientSecretVercel = async () => (await client.getSecret("CLIENT_SECRET_VERCEL")).secretValue; export const getClientSecretVercel = async () => (await client.getSecret("CLIENT_SECRET_VERCEL")).secretValue;
@@ -44,6 +45,7 @@ export const getClientSecretNetlify = async () => (await client.getSecret("CLIEN
export const getClientSecretGitHub = async () => (await client.getSecret("CLIENT_SECRET_GITHUB")).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 getClientSecretGitLab = async () => (await client.getSecret("CLIENT_SECRET_GITLAB")).secretValue;
export const getClientSecretGoogle = async () => (await client.getSecret("CLIENT_SECRET_GOOGLE")).secretValue; export const getClientSecretGoogle = async () => (await client.getSecret("CLIENT_SECRET_GOOGLE")).secretValue;
export const getClientSecretBitBucket = async () => (await client.getSecret("CLIENT_SECRET_BITBUCKET")).secretValue;
export const getClientSlugVercel = async () => (await client.getSecret("CLIENT_SLUG_VERCEL")).secretValue; export const getClientSlugVercel = async () => (await client.getSecret("CLIENT_SLUG_VERCEL")).secretValue;
export const getPostHogHost = async () => (await client.getSecret("POSTHOG_HOST")).secretValue || "https://app.posthog.com"; 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 getPostHogProjectApiKey = async () => (await client.getSecret("POSTHOG_PROJECT_API_KEY")).secretValue || "phc_nSin8j5q2zdhpFDI1ETmFNUIuTG4DwKVyIigrY10XiE";
@@ -57,6 +59,11 @@ export const getSmtpPassword = async () => (await client.getSecret("SMTP_PASSWOR
export const getSmtpFromAddress = async () => (await client.getSecret("SMTP_FROM_ADDRESS")).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 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 getLicenseKey = async () => { export const getLicenseKey = async () => {
const secretValue = (await client.getSecret("LICENSE_KEY")).secretValue; const secretValue = (await client.getSecret("LICENSE_KEY")).secretValue;
return secretValue === "" ? undefined : secretValue; return secretValue === "" ? undefined : secretValue;
+21 -15
View File
@@ -13,21 +13,27 @@ import * as signupController from "./signupController";
import * as userActionController from "./userActionController"; import * as userActionController from "./userActionController";
import * as userController from "./userController"; import * as userController from "./userController";
import * as workspaceController from "./workspaceController"; import * as workspaceController from "./workspaceController";
import * as secretScanningController from "./secretScanningController";
import * as webhookController from "./webhookController";
import * as secretImportController from "./secretImportController";
export { export {
authController, authController,
botController, botController,
integrationAuthController, integrationAuthController,
integrationController, integrationController,
keyController, keyController,
membershipController, membershipController,
membershipOrgController, membershipOrgController,
organizationController, organizationController,
passwordController, passwordController,
secretController, secretController,
serviceTokenController, serviceTokenController,
signupController, signupController,
userActionController, userActionController,
userController, userController,
workspaceController, workspaceController,
secretScanningController,
webhookController,
secretImportController
}; };
@@ -7,6 +7,7 @@ import { IntegrationService } from "../../services";
import { import {
ALGORITHM_AES_256_GCM, ALGORITHM_AES_256_GCM,
ENCODING_SCHEME_UTF8, ENCODING_SCHEME_UTF8,
INTEGRATION_BITBUCKET_API_URL,
INTEGRATION_RAILWAY_API_URL, INTEGRATION_RAILWAY_API_URL,
INTEGRATION_SET, INTEGRATION_SET,
INTEGRATION_VERCEL_API_URL, INTEGRATION_VERCEL_API_URL,
@@ -141,12 +142,14 @@ export const saveIntegrationAccessToken = async (req: Request, res: Response) =>
*/ */
export const getIntegrationAuthApps = async (req: Request, res: Response) => { export const getIntegrationAuthApps = async (req: Request, res: Response) => {
const teamId = req.query.teamId as string; const teamId = req.query.teamId as string;
const workspaceSlug = req.query.workspaceSlug as string;
const apps = await getApps({ const apps = await getApps({
integrationAuth: req.integrationAuth, integrationAuth: req.integrationAuth,
accessToken: req.accessToken, accessToken: req.accessToken,
accessId: req.accessId, accessId: req.accessId,
...(teamId && { teamId }) ...(teamId && { teamId }),
...(workspaceSlug && { workspaceSlug })
}); });
return res.status(200).send({ return res.status(200).send({
@@ -382,6 +385,66 @@ export const getIntegrationAuthRailwayServices = async (req: Request, res: Respo
}); });
}; };
/**
* Return list of workspaces allowed for Bitbucket integration
* @param req
* @param res
* @returns
*/
export const getIntegrationAuthBitBucketWorkspaces = async (req: Request, res: Response) => {
interface WorkspaceResponse {
size: number;
page: number;
pageLen: number;
next: string;
previous: string;
values: Array<Workspace>;
}
interface Workspace {
type: string;
uuid: string;
name: string;
slug: string;
is_private: boolean;
created_on: string;
updated_on: string;
}
const workspaces: Workspace[] = [];
let hasNextPage = true;
let workspaceUrl = `${INTEGRATION_BITBUCKET_API_URL}/2.0/workspaces`
while (hasNextPage) {
const { data }: { data: WorkspaceResponse } = await standardRequest.get(
workspaceUrl,
{
headers: {
Authorization: `Bearer ${req.accessToken}`,
"Accept-Encoding": "application/json"
}
}
);
if (data?.values.length > 0) {
data.values.forEach((workspace) => {
workspaces.push(workspace)
})
}
if (data.next) {
workspaceUrl = data.next
} else {
hasNextPage = false
}
}
return res.status(200).send({
workspaces
});
};
/** /**
* Delete integration authorization with id [integrationAuthId] * Delete integration authorization with id [integrationAuthId]
* @param req * @param req
@@ -2,7 +2,7 @@ import { Request, Response } from "express";
import { Types } from "mongoose"; import { Types } from "mongoose";
import { Integration } from "../../models"; import { Integration } from "../../models";
import { EventService } from "../../services"; import { EventService } from "../../services";
import { eventPushSecrets } from "../../events"; import { eventStartIntegration } from "../../events";
import Folder from "../../models/folder"; import Folder from "../../models/folder";
import { getFolderByPath } from "../../services/FolderService"; import { getFolderByPath } from "../../services/FolderService";
import { BadRequestError } from "../../utils/errors"; import { BadRequestError } from "../../utils/errors";
@@ -27,19 +27,19 @@ export const createIntegration = async (req: Request, res: Response) => {
owner, owner,
path, path,
region, region,
secretPath, secretPath
} = req.body; } = req.body;
const folders = await Folder.findOne({ const folders = await Folder.findOne({
workspace: req.integrationAuth.workspace._id, workspace: req.integrationAuth.workspace._id,
environment: sourceEnvironment, environment: sourceEnvironment
}); });
if (folders) { if (folders) {
const folder = getFolderByPath(folders.nodes, secretPath); const folder = getFolderByPath(folders.nodes, secretPath);
if (!folder) { if (!folder) {
throw BadRequestError({ throw BadRequestError({
message: "Path for service token does not exist", message: "Path for service token does not exist"
}); });
} }
} }
@@ -62,21 +62,21 @@ export const createIntegration = async (req: Request, res: Response) => {
region, region,
secretPath, secretPath,
integration: req.integrationAuth.integration, integration: req.integrationAuth.integration,
integrationAuth: new Types.ObjectId(integrationAuthId), integrationAuth: new Types.ObjectId(integrationAuthId)
}).save(); }).save();
if (integration) { if (integration) {
// trigger event - push secrets // trigger event - push secrets
EventService.handleEvent({ EventService.handleEvent({
event: eventPushSecrets({ event: eventStartIntegration({
workspaceId: integration.workspace, workspaceId: integration.workspace,
environment: sourceEnvironment, environment: sourceEnvironment
}), })
}); });
} }
return res.status(200).send({ return res.status(200).send({
integration, integration
}); });
}; };
@@ -97,26 +97,26 @@ export const updateIntegration = async (req: Request, res: Response) => {
appId, appId,
targetEnvironment, targetEnvironment,
owner, // github-specific integration param owner, // github-specific integration param
secretPath, secretPath
} = req.body; } = req.body;
const folders = await Folder.findOne({ const folders = await Folder.findOne({
workspace: req.integration.workspace, workspace: req.integration.workspace,
environment, environment
}); });
if (folders) { if (folders) {
const folder = getFolderByPath(folders.nodes, secretPath); const folder = getFolderByPath(folders.nodes, secretPath);
if (!folder) { if (!folder) {
throw BadRequestError({ throw BadRequestError({
message: "Path for service token does not exist", message: "Path for service token does not exist"
}); });
} }
} }
const integration = await Integration.findOneAndUpdate( const integration = await Integration.findOneAndUpdate(
{ {
_id: req.integration._id, _id: req.integration._id
}, },
{ {
environment, environment,
@@ -125,25 +125,25 @@ export const updateIntegration = async (req: Request, res: Response) => {
appId, appId,
targetEnvironment, targetEnvironment,
owner, owner,
secretPath, secretPath
}, },
{ {
new: true, new: true
} }
); );
if (integration) { if (integration) {
// trigger event - push secrets // trigger event - push secrets
EventService.handleEvent({ EventService.handleEvent({
event: eventPushSecrets({ event: eventStartIntegration({
workspaceId: integration.workspace, workspaceId: integration.workspace,
environment, environment
}), })
}); });
} }
return res.status(200).send({ return res.status(200).send({
integration, integration
}); });
}; };
@@ -158,12 +158,12 @@ export const deleteIntegration = async (req: Request, res: Response) => {
const { integrationId } = req.params; const { integrationId } = req.params;
const integration = await Integration.findOneAndDelete({ const integration = await Integration.findOneAndDelete({
_id: integrationId, _id: integrationId
}); });
if (!integration) throw new Error("Failed to find integration"); if (!integration) throw new Error("Failed to find integration");
return res.status(200).send({ return res.status(200).send({
integration, integration
}); });
}; };
@@ -1,6 +1,7 @@
import { Types } from "mongoose"; import { Types } from "mongoose";
import { Request, Response } from "express"; import { Request, Response } from "express";
import { MembershipOrg, Organization, User } from "../../models"; import { MembershipOrg, Organization, User } from "../../models";
import { SSOConfig } from "../../ee/models";
import { deleteMembershipOrg as deleteMemberFromOrg } from "../../helpers/membershipOrg"; import { deleteMembershipOrg as deleteMemberFromOrg } from "../../helpers/membershipOrg";
import { createToken } from "../../helpers/auth"; import { createToken } from "../../helpers/auth";
import { updateSubscriptionOrgQuantity } from "../../helpers/organization"; import { updateSubscriptionOrgQuantity } from "../../helpers/organization";
@@ -110,6 +111,18 @@ export const inviteUserToOrganization = async (req: Request, res: Response) => {
} }
const plan = await EELicenseService.getPlan(organizationId); const plan = await EELicenseService.getPlan(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) { if (plan.memberLimit !== null) {
// case: limit imposed on number of members allowed // case: limit imposed on number of members allowed
@@ -9,7 +9,7 @@ import {
import { createOrganization as create } from "../../helpers/organization"; import { createOrganization as create } from "../../helpers/organization";
import { addMembershipsOrg } from "../../helpers/membershipOrg"; import { addMembershipsOrg } from "../../helpers/membershipOrg";
import { ACCEPTED, OWNER } from "../../variables"; import { ACCEPTED, OWNER } from "../../variables";
import { getSiteURL, getLicenseServerUrl } from "../../config"; import { getLicenseServerUrl, getSiteURL } from "../../config";
import { licenseServerKeyRequest } from "../../config/request"; import { licenseServerKeyRequest } from "../../config/request";
export const getOrganizations = async (req: Request, res: Response) => { export const getOrganizations = async (req: Request, res: Response) => {
@@ -80,7 +80,8 @@ export const pushSecrets = async (req: Request, res: Response) => {
EventService.handleEvent({ EventService.handleEvent({
event: eventPushSecrets({ event: eventPushSecrets({
workspaceId: new Types.ObjectId(workspaceId), workspaceId: new Types.ObjectId(workspaceId),
environment environment,
secretPath: "/"
}) })
}); });
@@ -0,0 +1,116 @@
import { Request, Response } from "express";
import { validateMembership } from "../../helpers";
import SecretImport from "../../models/secretImports";
import { getAllImportedSecrets } from "../../services/SecretImportService";
import { BadRequestError } from "../../utils/errors";
import { ADMIN, MEMBER } from "../../variables";
export const createSecretImport = async (req: Request, res: Response) => {
const { workspaceId, environment, folderId, secretImport } = req.body;
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();
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();
return res.status(200).json({ message: "successfully created secret import" });
};
// to keep the ordering, you must pass all the imports in here not the only updated one
// this is because the order decide which import gets overriden
export const updateSecretImport = async (req: Request, res: Response) => {
const { id } = req.params;
const { secretImports } = req.body;
const importSecDoc = await SecretImport.findById(id);
if (!importSecDoc) {
throw BadRequestError({ message: "Import not found" });
}
await validateMembership({
userId: req.user._id.toString(),
workspaceId: importSecDoc.workspace,
acceptedRoles: [ADMIN, MEMBER]
});
importSecDoc.imports = secretImports;
await importSecDoc.save();
return res.status(200).json({ message: "successfully updated secret import" });
};
export const deleteSecretImport = async (req: Request, res: Response) => {
const { id } = req.params;
const { secretImportEnv, secretImportPath } = req.body;
const importSecDoc = await SecretImport.findById(id);
if (!importSecDoc) {
throw BadRequestError({ message: "Import not found" });
}
await validateMembership({
userId: req.user._id.toString(),
workspaceId: importSecDoc.workspace,
acceptedRoles: [ADMIN, MEMBER]
});
importSecDoc.imports = importSecDoc.imports.filter(
({ environment, secretPath }) =>
!(environment === secretImportEnv && secretPath === secretImportPath)
);
await importSecDoc.save();
return res.status(200).json({ message: "successfully delete secret import" });
};
export const getSecretImports = async (req: Request, res: Response) => {
const { workspaceId, environment, folderId } = req.query;
const importSecDoc = await SecretImport.findOne({
workspace: workspaceId,
environment,
folderId
});
if (!importSecDoc) {
return res.status(200).json({ secretImport: {} });
}
return res.status(200).json({ secretImport: importSecDoc });
};
export const getAllSecretsFromImport = async (req: Request, res: Response) => {
const { workspaceId, environment, folderId } = req.query as {
workspaceId: string;
environment: string;
folderId: string;
};
const importSecDoc = await SecretImport.findOne({
workspace: workspaceId,
environment,
folderId
});
if (!importSecDoc) {
return res.status(200).json({ secrets: {} });
}
const secrets = await getAllImportedSecrets(workspaceId, environment, folderId);
return res.status(200).json({ secrets });
};
@@ -0,0 +1,91 @@
import { Request, Response } from "express";
import GitAppInstallationSession from "../../models/gitAppInstallationSession";
import crypto from "crypto";
import { Types } from "mongoose";
import { UnauthorizedRequestError } from "../../utils/errors";
import GitAppOrganizationInstallation from "../../models/gitAppOrganizationInstallation";
import { MembershipOrg } from "../../models";
import GitRisks, { STATUS_RESOLVED_FALSE_POSITIVE, STATUS_RESOLVED_NOT_REVOKED, STATUS_RESOLVED_REVOKED } from "../../models/gitRisks";
export const createInstallationSession = async (req: Request, res: Response) => {
const sessionId = crypto.randomBytes(16).toString("hex");
await GitAppInstallationSession.findByIdAndUpdate(
req.organization,
{
organization: new Types.ObjectId(req.organization),
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 { installationId, sessionId } = req.body
const installationSession = await GitAppInstallationSession.findOneAndDelete({ sessionId: sessionId })
if (!installationSession) {
throw UnauthorizedRequestError()
}
const userMembership = await MembershipOrg.find({ user: req.user._id, organization: installationSession.organization })
if (!userMembership) {
throw UnauthorizedRequestError()
}
const installationLink = await GitAppOrganizationInstallation.findOneAndUpdate({
organizationId: installationSession.organization,
}, {
installationId: installationId,
organizationId: installationSession.organization,
user: installationSession.user
}, {
upsert: true
}).lean()
res.json(installationLink)
}
export const getCurrentOrganizationInstallationStatus = async (req: Request, res: Response) => {
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 { organizationId } = req.params
const risks = await GitRisks.find({ organization: organizationId }).sort({ createdAt: -1 }).lean()
res.json({
risks: risks
})
}
export const updateRisksStatus = async (req: Request, res: Response) => {
const { riskId } = req.params
const { status } = req.body
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)
}
@@ -0,0 +1,140 @@
import { Request, Response } from "express";
import { Types } from "mongoose";
import { client, getRootEncryptionKey } from "../../config";
import { validateMembership } from "../../helpers";
import Webhook from "../../models/webhooks";
import { getWebhookPayload, triggerWebhookRequest } from "../../services/WebhookService";
import { BadRequestError } from "../../utils/errors";
import { ADMIN, ALGORITHM_AES_256_GCM, ENCODING_SCHEME_BASE64, MEMBER } from "../../variables";
export const createWebhook = async (req: Request, res: Response) => {
const { webhookUrl, webhookSecretKey, environment, workspaceId, secretPath } = req.body;
const webhook = new Webhook({
workspace: workspaceId,
environment,
secretPath,
url: webhookUrl,
algorithm: ALGORITHM_AES_256_GCM,
keyEncoding: ENCODING_SCHEME_BASE64
});
if (webhookSecretKey) {
const rootEncryptionKey = await getRootEncryptionKey();
const { ciphertext, iv, tag } = client.encryptSymmetric(webhookSecretKey, rootEncryptionKey);
webhook.iv = iv;
webhook.tag = tag;
webhook.encryptedSecretKey = ciphertext;
}
await webhook.save();
return res.status(200).send({
webhook,
message: "successfully created webhook"
});
};
export const updateWebhook = async (req: Request, res: Response) => {
const { webhookId } = req.params;
const { isDisabled } = req.body;
const webhook = await Webhook.findById(webhookId);
if (!webhook) {
throw BadRequestError({ message: "Webhook not found!!" });
}
// check that user is a member of the workspace
await validateMembership({
userId: req.user._id.toString(),
workspaceId: webhook.workspace,
acceptedRoles: [ADMIN, MEMBER]
});
if (typeof isDisabled !== undefined) {
webhook.isDisabled = isDisabled;
}
await webhook.save();
return res.status(200).send({
webhook,
message: "successfully updated webhook"
});
};
export const deleteWebhook = async (req: Request, res: Response) => {
const { webhookId } = req.params;
const webhook = await Webhook.findById(webhookId);
if (!webhook) {
throw BadRequestError({ message: "Webhook not found!!" });
}
await validateMembership({
userId: req.user._id.toString(),
workspaceId: webhook.workspace,
acceptedRoles: [ADMIN, MEMBER]
});
await webhook.remove();
return res.status(200).send({
message: "successfully removed webhook"
});
};
export const testWebhook = async (req: Request, res: Response) => {
const { webhookId } = req.params;
const webhook = await Webhook.findById(webhookId);
if (!webhook) {
throw BadRequestError({ message: "Webhook not found!!" });
}
await validateMembership({
userId: req.user._id.toString(),
workspaceId: webhook.workspace,
acceptedRoles: [ADMIN, MEMBER]
});
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 { environment, workspaceId, secretPath } = req.query;
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
});
};
@@ -27,16 +27,16 @@ export const createWorkspaceEnvironment = async (
const { workspaceId } = req.params; const { workspaceId } = req.params;
const { environmentName, environmentSlug } = req.body; const { environmentName, environmentSlug } = req.body;
const workspace = await Workspace.findById(workspaceId).exec(); const workspace = await Workspace.findById(workspaceId).exec();
if (!workspace) throw WorkspaceNotFoundError(); if (!workspace) throw WorkspaceNotFoundError();
const plan = await EELicenseService.getPlan(workspace.organization.toString()); const plan = await EELicenseService.getPlan(workspace.organization.toString());
if (plan.environmentLimit !== null) { if (plan.environmentLimit !== null) {
// case: limit imposed on number of environments allowed // case: limit imposed on number of environments allowed
if (workspace.environments.length >= plan.environmentLimit) { if (workspace.environments.length >= plan.environmentLimit) {
// case: number of environments used exceeds the number of environments allowed // case: number of environments used exceeds the number of environments allowed
return res.status(400).send({ return res.status(400).send({
message: "Failed to create environment due to environment limit reached. Upgrade plan to create more environments.", message: "Failed to create environment due to environment limit reached. Upgrade plan to create more environments.",
}); });
@@ -191,14 +191,21 @@ export const deleteWorkspaceEnvironment = async (
workspace: workspaceId, workspace: workspaceId,
environment: environmentSlug, environment: environmentSlug,
}); });
await ServiceToken.deleteMany({
workspace: workspaceId, // await ServiceToken.deleteMany({
environment: environmentSlug, // workspace: workspaceId,
}); // environment: environmentSlug,
await ServiceTokenData.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({ await Integration.deleteMany({
workspace: workspaceId, workspace: workspaceId,
environment: environmentSlug, environment: environmentSlug,
@@ -9,8 +9,6 @@ import {
} from "../../types/secret"; } from "../../types/secret";
const { ValidationError } = mongoose.Error; const { ValidationError } = mongoose.Error;
import { import {
BadRequestError,
InternalServerError,
ValidationError as RouteValidationError, ValidationError as RouteValidationError,
UnauthorizedRequestError UnauthorizedRequestError
} from "../../utils/errors"; } from "../../utils/errors";
+204 -206
View File
@@ -9,7 +9,7 @@ import {
ACTION_UPDATE_SECRETS, ACTION_UPDATE_SECRETS,
ALGORITHM_AES_256_GCM, ALGORITHM_AES_256_GCM,
ENCODING_SCHEME_UTF8, ENCODING_SCHEME_UTF8,
SECRET_PERSONAL, SECRET_PERSONAL
} from "../../variables"; } from "../../variables";
import { BadRequestError, UnauthorizedRequestError } from "../../utils/errors"; import { BadRequestError, UnauthorizedRequestError } from "../../utils/errors";
import { EventService } from "../../services"; import { EventService } from "../../services";
@@ -21,7 +21,7 @@ import { PERMISSION_WRITE_SECRETS } from "../../variables";
import { import {
userHasNoAbility, userHasNoAbility,
userHasWorkspaceAccess, userHasWorkspaceAccess,
userHasWriteOnlyAbility, userHasWriteOnlyAbility
} from "../../ee/helpers/checkMembershipPermissions"; } from "../../ee/helpers/checkMembershipPermissions";
import Tag from "../../models/tag"; import Tag from "../../models/tag";
import _ from "lodash"; import _ from "lodash";
@@ -31,7 +31,11 @@ import {
getFolderByPath, getFolderByPath,
getFolderIdFromServiceToken, getFolderIdFromServiceToken,
searchByFolderId, searchByFolderId,
searchByFolderIdWithDir
} from "../../services/FolderService"; } from "../../services/FolderService";
import { isValidScope } from "../../helpers/secrets";
import path from "path";
import { getAllImportedSecrets } from "../../services/SecretImportService";
/** /**
* Peform a batch of any specified CUD secret operations * Peform a batch of any specified CUD secret operations
@@ -46,14 +50,13 @@ export const batchSecrets = async (req: Request, res: Response) => {
const { const {
workspaceId, workspaceId,
environment, environment,
requests, requests
secretPath,
}: { }: {
workspaceId: string; workspaceId: string;
environment: string; environment: string;
requests: BatchSecretRequest[]; requests: BatchSecretRequest[];
secretPath: string;
} = req.body; } = req.body;
let secretPath = req.body.secretPath as string;
let folderId = req.body.folderId as string; let folderId = req.body.folderId as string;
const createSecrets: BatchSecret[] = []; const createSecrets: BatchSecret[] = [];
@@ -63,31 +66,31 @@ export const batchSecrets = async (req: Request, res: Response) => {
// get secret blind index salt // get secret blind index salt
const salt = await SecretService.getSecretBlindIndexSalt({ const salt = await SecretService.getSecretBlindIndexSalt({
workspaceId: new Types.ObjectId(workspaceId), workspaceId: new Types.ObjectId(workspaceId)
}); });
const folders = await Folder.findOne({ workspace: workspaceId, environment }); const folders = await Folder.findOne({ workspace: workspaceId, environment });
if (folders && folderId !== "root") {
const folder = searchByFolderId(folders.nodes, folderId as string);
if (!folder) throw BadRequestError({ message: "Folder not found" });
}
if (req.authData.authPayload instanceof ServiceTokenData) { if (req.authData.authPayload instanceof ServiceTokenData) {
const { secretPath: serviceTkScopedSecretPath } = req.authData.authPayload; const isValidScopeAccess = isValidScope(req.authData.authPayload, environment, secretPath);
// in service token when not giving secretpath folderid must be root // in service token when not giving secretpath folderid must be root
// this is to avoid giving folderid when service tokens are used // this is to avoid giving folderid when service tokens are used
if ( if ((!secretPath && folderId !== "root") || (secretPath && !isValidScopeAccess)) {
(!secretPath && folderId !== "root") ||
(secretPath && secretPath !== serviceTkScopedSecretPath)
) {
throw UnauthorizedRequestError({ message: "Folder Permission Denied" }); throw UnauthorizedRequestError({ message: "Folder Permission Denied" });
} }
} }
if (secretPath) { if (secretPath) {
folderId = await getFolderIdFromServiceToken( folderId = await getFolderIdFromServiceToken(workspaceId, environment, secretPath);
workspaceId, }
environment,
secretPath if (folders && folderId !== "root") {
const folder = searchByFolderIdWithDir(folders.nodes, folderId as string);
if (!folder?.folder) throw BadRequestError({ message: "Folder not found" });
secretPath = path.join(
"/",
...folder.dir.map(({ name }) => name).filter((name) => name !== "root")
); );
} }
@@ -97,12 +100,10 @@ export const batchSecrets = async (req: Request, res: Response) => {
let secretBlindIndex = ""; let secretBlindIndex = "";
switch (request.method) { switch (request.method) {
case "POST": case "POST":
secretBlindIndex = await SecretService.generateSecretBlindIndexWithSalt( secretBlindIndex = await SecretService.generateSecretBlindIndexWithSalt({
{ secretName: request.secret.secretName,
secretName: request.secret.secretName, salt
salt, });
}
);
createSecrets.push({ createSecrets.push({
...request.secret, ...request.secret,
@@ -113,16 +114,14 @@ export const batchSecrets = async (req: Request, res: Response) => {
folder: folderId, folder: folderId,
secretBlindIndex, secretBlindIndex,
algorithm: ALGORITHM_AES_256_GCM, algorithm: ALGORITHM_AES_256_GCM,
keyEncoding: ENCODING_SCHEME_UTF8, keyEncoding: ENCODING_SCHEME_UTF8
}); });
break; break;
case "PATCH": case "PATCH":
secretBlindIndex = await SecretService.generateSecretBlindIndexWithSalt( secretBlindIndex = await SecretService.generateSecretBlindIndexWithSalt({
{ secretName: request.secret.secretName,
secretName: request.secret.secretName, salt
salt, });
}
);
updateSecrets.push({ updateSecrets.push({
...request.secret, ...request.secret,
@@ -130,7 +129,7 @@ export const batchSecrets = async (req: Request, res: Response) => {
secretBlindIndex, secretBlindIndex,
folder: folderId, folder: folderId,
algorithm: ALGORITHM_AES_256_GCM, algorithm: ALGORITHM_AES_256_GCM,
keyEncoding: ENCODING_SCHEME_UTF8, keyEncoding: ENCODING_SCHEME_UTF8
}); });
break; break;
case "DELETE": case "DELETE":
@@ -150,9 +149,9 @@ export const batchSecrets = async (req: Request, res: Response) => {
...n._doc, ...n._doc,
_id: new Types.ObjectId(), _id: new Types.ObjectId(),
secret: n._id, secret: n._id,
isDeleted: false, isDeleted: false
}; };
}), })
}); });
const addAction = (await EELogService.createAction({ const addAction = (await EELogService.createAction({
@@ -161,7 +160,7 @@ export const batchSecrets = async (req: Request, res: Response) => {
serviceAccountId: req.serviceAccount?._id, serviceAccountId: req.serviceAccount?._id,
serviceTokenDataId: req.serviceTokenData?._id, serviceTokenDataId: req.serviceTokenData?._id,
workspaceId: new Types.ObjectId(workspaceId), workspaceId: new Types.ObjectId(workspaceId),
secretIds: createdSecrets.map((n) => n._id), secretIds: createdSecrets.map((n) => n._id)
})) as IAction; })) as IAction;
actions.push(addAction); actions.push(addAction);
@@ -175,8 +174,8 @@ export const batchSecrets = async (req: Request, res: Response) => {
workspaceId, workspaceId,
folderId, folderId,
channel, channel,
userAgent: req.headers?.["user-agent"], userAgent: req.headers?.["user-agent"]
}, }
}); });
} }
} }
@@ -195,7 +194,7 @@ export const batchSecrets = async (req: Request, res: Response) => {
listedSecretsObj = req.secrets.reduce( listedSecretsObj = req.secrets.reduce(
(obj: any, secret: ISecret) => ({ (obj: any, secret: ISecret) => ({
...obj, ...obj,
[secret._id.toString()]: secret, [secret._id.toString()]: secret
}), }),
{} {}
); );
@@ -204,16 +203,16 @@ export const batchSecrets = async (req: Request, res: Response) => {
updateOne: { updateOne: {
filter: { filter: {
_id: new Types.ObjectId(u._id), _id: new Types.ObjectId(u._id),
workspace: new Types.ObjectId(workspaceId), workspace: new Types.ObjectId(workspaceId)
}, },
update: { update: {
$inc: { $inc: {
version: 1, version: 1
}, },
...u, ...u,
_id: new Types.ObjectId(u._id), _id: new Types.ObjectId(u._id)
}, }
}, }
})); }));
await Secret.bulkWrite(updateOperations); await Secret.bulkWrite(updateOperations);
@@ -240,25 +239,25 @@ export const batchSecrets = async (req: Request, res: Response) => {
algorithm: ALGORITHM_AES_256_GCM, algorithm: ALGORITHM_AES_256_GCM,
keyEncoding: ENCODING_SCHEME_UTF8, keyEncoding: ENCODING_SCHEME_UTF8,
tags: u.tags, tags: u.tags,
folder: u.folder, folder: u.folder
}) })
); );
await EESecretService.addSecretVersions({ await EESecretService.addSecretVersions({
secretVersions, secretVersions
}); });
updatedSecrets = await Secret.find({ updatedSecrets = await Secret.find({
_id: { _id: {
$in: updateSecrets.map((u) => new Types.ObjectId(u._id)), $in: updateSecrets.map((u) => new Types.ObjectId(u._id))
}, }
}); });
const updateAction = (await EELogService.createAction({ const updateAction = (await EELogService.createAction({
name: ACTION_UPDATE_SECRETS, name: ACTION_UPDATE_SECRETS,
userId: req.user._id, userId: req.user._id,
workspaceId: new Types.ObjectId(workspaceId), workspaceId: new Types.ObjectId(workspaceId),
secretIds: updatedSecrets.map((u) => u._id), secretIds: updatedSecrets.map((u) => u._id)
})) as IAction; })) as IAction;
actions.push(updateAction); actions.push(updateAction);
@@ -272,8 +271,8 @@ export const batchSecrets = async (req: Request, res: Response) => {
workspaceId, workspaceId,
folderId, folderId,
channel, channel,
userAgent: req.headers?.["user-agent"], userAgent: req.headers?.["user-agent"]
}, }
}); });
} }
} }
@@ -282,19 +281,19 @@ export const batchSecrets = async (req: Request, res: Response) => {
if (deleteSecrets.length > 0) { if (deleteSecrets.length > 0) {
await Secret.deleteMany({ await Secret.deleteMany({
_id: { _id: {
$in: deleteSecrets, $in: deleteSecrets
}, }
}); });
await EESecretService.markDeletedSecretVersions({ await EESecretService.markDeletedSecretVersions({
secretIds: deleteSecrets, secretIds: deleteSecrets
}); });
const deleteAction = (await EELogService.createAction({ const deleteAction = (await EELogService.createAction({
name: ACTION_DELETE_SECRETS, name: ACTION_DELETE_SECRETS,
userId: req.user._id, userId: req.user._id,
workspaceId: new Types.ObjectId(workspaceId), workspaceId: new Types.ObjectId(workspaceId),
secretIds: deleteSecrets, secretIds: deleteSecrets
})) as IAction; })) as IAction;
actions.push(deleteAction); actions.push(deleteAction);
@@ -307,8 +306,8 @@ export const batchSecrets = async (req: Request, res: Response) => {
environment, environment,
workspaceId, workspaceId,
channel: channel, channel: channel,
userAgent: req.headers?.["user-agent"], userAgent: req.headers?.["user-agent"]
}, }
}); });
} }
} }
@@ -320,7 +319,7 @@ export const batchSecrets = async (req: Request, res: Response) => {
workspaceId: new Types.ObjectId(workspaceId), workspaceId: new Types.ObjectId(workspaceId),
actions, actions,
channel, channel,
ipAddress: req.realIP, ipAddress: req.realIP
}); });
} }
@@ -328,14 +327,17 @@ export const batchSecrets = async (req: Request, res: Response) => {
await EventService.handleEvent({ await EventService.handleEvent({
event: eventPushSecrets({ event: eventPushSecrets({
workspaceId: new Types.ObjectId(workspaceId), workspaceId: new Types.ObjectId(workspaceId),
}), environment,
// root condition else this will be filled according to the path or folderid
secretPath: secretPath || "/"
})
}); });
// (EE) take a secret snapshot // (EE) take a secret snapshot
await EESecretService.takeSecretSnapshot({ await EESecretService.takeSecretSnapshot({
workspaceId: new Types.ObjectId(workspaceId), workspaceId: new Types.ObjectId(workspaceId),
environment, environment,
folderId, folderId
}); });
const resObj: { [key: string]: ISecret[] | string[] } = {}; const resObj: { [key: string]: ISecret[] | string[] } = {};
@@ -418,7 +420,7 @@ export const createSecrets = async (req: Request, res: Response) => {
const { const {
workspaceId, workspaceId,
environment, environment,
secretPath, secretPath
}: { }: {
workspaceId: string; workspaceId: string;
environment: string; environment: string;
@@ -435,8 +437,7 @@ export const createSecrets = async (req: Request, res: Response) => {
); );
if (!hasAccess) { if (!hasAccess) {
throw UnauthorizedRequestError({ throw UnauthorizedRequestError({
message: message: "You do not have the necessary permission(s) perform this action"
"You do not have the necessary permission(s) perform this action",
}); });
} }
} }
@@ -449,28 +450,27 @@ export const createSecrets = async (req: Request, res: Response) => {
// case: create 1 secret // case: create 1 secret
listOfSecretsToCreate = [req.body.secrets]; listOfSecretsToCreate = [req.body.secrets];
} }
if (req.authData.authPayload instanceof ServiceTokenData) { if (req.authData.authPayload instanceof ServiceTokenData) {
const { secretPath: serviceTkScopedSecretPath } = req.authData.authPayload; const isValidScopeAccess = isValidScope(
req.authData.authPayload,
environment,
secretPath || "/"
);
// in service token when not giving secretpath folderid must be root // in service token when not giving secretpath folderid must be root
// this is to avoid giving folderid when service tokens are used // this is to avoid giving folderid when service tokens are used
if ( if ((!secretPath && folderId !== "root") || (secretPath && !isValidScopeAccess)) {
(!secretPath && folderId !== "root") ||
(secretPath && secretPath !== serviceTkScopedSecretPath)
) {
throw UnauthorizedRequestError({ message: "Folder Permission Denied" }); throw UnauthorizedRequestError({ message: "Folder Permission Denied" });
} }
} }
if (secretPath) { if (secretPath) {
folderId = await getFolderIdFromServiceToken( folderId = await getFolderIdFromServiceToken(workspaceId, environment, secretPath);
workspaceId,
environment,
secretPath
);
} }
// get secret blind index salt // get secret blind index salt
const salt = await SecretService.getSecretBlindIndexSalt({ const salt = await SecretService.getSecretBlindIndexSalt({
workspaceId: new Types.ObjectId(workspaceId), workspaceId: new Types.ObjectId(workspaceId)
}); });
type secretsToCreateType = { type secretsToCreateType = {
@@ -502,15 +502,14 @@ export const createSecrets = async (req: Request, res: Response) => {
secretCommentCiphertext, secretCommentCiphertext,
secretCommentIV, secretCommentIV,
secretCommentTag, secretCommentTag,
tags, tags
}: secretsToCreateType) => { }: secretsToCreateType) => {
let secretBlindIndex; let secretBlindIndex;
if (secretName) { if (secretName) {
secretBlindIndex = secretBlindIndex = await SecretService.generateSecretBlindIndexWithSalt({
await SecretService.generateSecretBlindIndexWithSalt({ secretName,
secretName, salt
salt, });
});
} }
return { return {
@@ -532,22 +531,24 @@ export const createSecrets = async (req: Request, res: Response) => {
secretCommentTag, secretCommentTag,
algorithm: ALGORITHM_AES_256_GCM, algorithm: ALGORITHM_AES_256_GCM,
keyEncoding: ENCODING_SCHEME_UTF8, keyEncoding: ENCODING_SCHEME_UTF8,
tags, tags
}; };
} }
) )
); );
const newlyCreatedSecrets: ISecret[] = ( const newlyCreatedSecrets: ISecret[] = (await Secret.insertMany(secretsToInsert)).map(
await Secret.insertMany(secretsToInsert) (insertedSecret) => insertedSecret.toObject()
).map((insertedSecret) => insertedSecret.toObject()); );
setTimeout(async () => { setTimeout(async () => {
// trigger event - push secrets // trigger event - push secrets
await EventService.handleEvent({ await EventService.handleEvent({
event: eventPushSecrets({ event: eventPushSecrets({
workspaceId: new Types.ObjectId(workspaceId), workspaceId: new Types.ObjectId(workspaceId),
}), environment,
secretPath: secretPath || "/"
})
}); });
}, 5000); }, 5000);
@@ -567,7 +568,7 @@ export const createSecrets = async (req: Request, res: Response) => {
secretKeyTag, secretKeyTag,
secretValueCiphertext, secretValueCiphertext,
secretValueIV, secretValueIV,
secretValueTag, secretValueTag
}) => }) =>
new SecretVersion({ new SecretVersion({
secret: _id, secret: _id,
@@ -586,9 +587,9 @@ export const createSecrets = async (req: Request, res: Response) => {
secretValueTag, secretValueTag,
folder: folderId, folder: folderId,
algorithm: ALGORITHM_AES_256_GCM, algorithm: ALGORITHM_AES_256_GCM,
keyEncoding: ENCODING_SCHEME_UTF8, keyEncoding: ENCODING_SCHEME_UTF8
}) })
), )
}); });
const addAction = await EELogService.createAction({ const addAction = await EELogService.createAction({
@@ -597,7 +598,7 @@ export const createSecrets = async (req: Request, res: Response) => {
serviceAccountId: req.serviceAccount?._id, serviceAccountId: req.serviceAccount?._id,
serviceTokenDataId: req.serviceTokenData?._id, serviceTokenDataId: req.serviceTokenData?._id,
workspaceId: new Types.ObjectId(workspaceId), workspaceId: new Types.ObjectId(workspaceId),
secretIds: newlyCreatedSecrets.map((n) => n._id), secretIds: newlyCreatedSecrets.map((n) => n._id)
}); });
// (EE) create (audit) log // (EE) create (audit) log
@@ -609,14 +610,14 @@ export const createSecrets = async (req: Request, res: Response) => {
workspaceId: new Types.ObjectId(workspaceId), workspaceId: new Types.ObjectId(workspaceId),
actions: [addAction], actions: [addAction],
channel, channel,
ipAddress: req.realIP, ipAddress: req.realIP
})); }));
// (EE) take a secret snapshot // (EE) take a secret snapshot
await EESecretService.takeSecretSnapshot({ await EESecretService.takeSecretSnapshot({
workspaceId: new Types.ObjectId(workspaceId), workspaceId: new Types.ObjectId(workspaceId),
environment, environment,
folderId, folderId
}); });
const postHogClient = await TelemetryService.getPostHogClient(); const postHogClient = await TelemetryService.getPostHogClient();
@@ -624,7 +625,7 @@ export const createSecrets = async (req: Request, res: Response) => {
postHogClient.capture({ postHogClient.capture({
event: "secrets added", event: "secrets added",
distinctId: await TelemetryService.getDistinctId({ distinctId: await TelemetryService.getDistinctId({
authData: req.authData, authData: req.authData
}), }),
properties: { properties: {
numberOfSecrets: listOfSecretsToCreate.length, numberOfSecrets: listOfSecretsToCreate.length,
@@ -632,13 +633,13 @@ export const createSecrets = async (req: Request, res: Response) => {
workspaceId, workspaceId,
channel: channel, channel: channel,
folderId, folderId,
userAgent: req.headers?.["user-agent"], userAgent: req.headers?.["user-agent"]
}, }
}); });
} }
return res.status(200).send({ return res.status(200).send({
secrets: newlyCreatedSecrets, secrets: newlyCreatedSecrets
}); });
}; };
@@ -690,16 +691,13 @@ export const getSecrets = async (req: Request, res: Response) => {
} }
*/ */
const { tagSlugs, secretPath } = req.query; const { tagSlugs, secretPath, include_imports } = req.query;
let { folderId } = req.query; let { folderId } = req.query;
const workspaceId = req.query.workspaceId as string; const workspaceId = req.query.workspaceId as string;
const environment = req.query.environment as string; const environment = req.query.environment as string;
const folders = await Folder.findOne({ workspace: workspaceId, environment }); const folders = await Folder.findOne({ workspace: workspaceId, environment });
if ( if ((!folders && folderId && folderId !== "root") || (!folders && secretPath)) {
(!folders && folderId && folderId !== "root") ||
(!folders && secretPath)
) {
res.send({ secrets: [] }); res.send({ secrets: [] });
return; return;
} }
@@ -712,13 +710,15 @@ export const getSecrets = async (req: Request, res: Response) => {
} }
if (req.authData.authPayload instanceof ServiceTokenData) { if (req.authData.authPayload instanceof ServiceTokenData) {
const { secretPath: serviceTkScopedSecretPath } = req.authData.authPayload; const isValidScopeAccess = isValidScope(
req.authData.authPayload,
environment,
(secretPath as string) || "/"
);
// in service token when not giving secretpath folderid must be root // in service token when not giving secretpath folderid must be root
// this is to avoid giving folderid when service tokens are used // this is to avoid giving folderid when service tokens are used
if ( if ((!secretPath && folderId !== "root") || (secretPath && !isValidScopeAccess)) {
(!secretPath && folderId !== "root") ||
(secretPath && secretPath !== serviceTkScopedSecretPath)
) {
throw UnauthorizedRequestError({ message: "Folder Permission Denied" }); throw UnauthorizedRequestError({ message: "Folder Permission Denied" });
} }
} }
@@ -738,8 +738,7 @@ export const getSecrets = async (req: Request, res: Response) => {
// query tags table to get all tags ids for the tag names for the given workspace // query tags table to get all tags ids for the tag names for the given workspace
let tagIds = []; let tagIds = [];
const tagNamesList = const tagNamesList = typeof tagSlugs === "string" && tagSlugs !== "" ? tagSlugs.split(",") : [];
typeof tagSlugs === "string" && tagSlugs !== "" ? tagSlugs.split(",") : [];
if (tagNamesList != undefined && tagNamesList.length != 0) { if (tagNamesList != undefined && tagNamesList.length != 0) {
const workspaceFromDB = await Tag.find({ workspace: workspaceId }); const workspaceFromDB = await Tag.find({ workspace: workspaceId });
tagIds = _.map(tagNamesList, (tagName: string) => { tagIds = _.map(tagNamesList, (tagName: string) => {
@@ -762,8 +761,7 @@ export const getSecrets = async (req: Request, res: Response) => {
); );
if (hasNoAccess) { if (hasNoAccess) {
throw UnauthorizedRequestError({ throw UnauthorizedRequestError({
message: message: "You do not have the necessary permission(s) perform this action"
"You do not have the necessary permission(s) perform this action",
}); });
} }
@@ -773,8 +771,8 @@ export const getSecrets = async (req: Request, res: Response) => {
folder: folderId, folder: folderId,
$or: [ $or: [
{ user: req.user._id }, // personal secrets for this user { user: req.user._id }, // personal secrets for this user
{ user: { $exists: false } }, // shared secrets from workspace { user: { $exists: false } } // shared secrets from workspace
], ]
}; };
if (tagIds.length > 0) { if (tagIds.length > 0) {
@@ -801,8 +799,8 @@ export const getSecrets = async (req: Request, res: Response) => {
environment, environment,
$or: [ $or: [
{ user: userId }, // personal secrets for this user { user: userId }, // personal secrets for this user
{ user: { $exists: false } }, // shared secrets from workspace { user: { $exists: false } } // shared secrets from workspace
], ]
}; };
if (tagIds.length > 0) { if (tagIds.length > 0) {
@@ -820,7 +818,7 @@ export const getSecrets = async (req: Request, res: Response) => {
workspace: workspaceId, workspace: workspaceId,
environment, environment,
folder: folderId, folder: folderId,
user: { $exists: false }, // shared secrets only from workspace user: { $exists: false } // shared secrets only from workspace
}; };
if (tagIds.length > 0) { if (tagIds.length > 0) {
@@ -830,6 +828,12 @@ export const getSecrets = async (req: Request, res: Response) => {
secrets = await Secret.find(secretQuery).populate("tags"); secrets = await Secret.find(secretQuery).populate("tags");
} }
// TODO(akhilmhdh) - secret-imp change this to org type
let importedSecrets: any[] = [];
if (include_imports === "true") {
importedSecrets = await getAllImportedSecrets(workspaceId, environment, folderId as string);
}
const channel = getChannelFromUserAgent(req.headers["user-agent"]); const channel = getChannelFromUserAgent(req.headers["user-agent"]);
const readAction = await EELogService.createAction({ const readAction = await EELogService.createAction({
@@ -838,7 +842,7 @@ export const getSecrets = async (req: Request, res: Response) => {
serviceAccountId: req.serviceAccount?._id, serviceAccountId: req.serviceAccount?._id,
serviceTokenDataId: req.serviceTokenData?._id, serviceTokenDataId: req.serviceTokenData?._id,
workspaceId: new Types.ObjectId(workspaceId as string), workspaceId: new Types.ObjectId(workspaceId as string),
secretIds: secrets.map((n: any) => n._id), secretIds: secrets.map((n: any) => n._id)
}); });
readAction && readAction &&
@@ -849,7 +853,7 @@ export const getSecrets = async (req: Request, res: Response) => {
workspaceId: new Types.ObjectId(workspaceId as string), workspaceId: new Types.ObjectId(workspaceId as string),
actions: [readAction], actions: [readAction],
channel, channel,
ipAddress: req.realIP, ipAddress: req.realIP
})); }));
const postHogClient = await TelemetryService.getPostHogClient(); const postHogClient = await TelemetryService.getPostHogClient();
@@ -857,7 +861,7 @@ export const getSecrets = async (req: Request, res: Response) => {
postHogClient.capture({ postHogClient.capture({
event: "secrets pulled", event: "secrets pulled",
distinctId: await TelemetryService.getDistinctId({ distinctId: await TelemetryService.getDistinctId({
authData: req.authData, authData: req.authData
}), }),
properties: { properties: {
numberOfSecrets: secrets.length, numberOfSecrets: secrets.length,
@@ -865,13 +869,14 @@ export const getSecrets = async (req: Request, res: Response) => {
workspaceId, workspaceId,
channel, channel,
folderId, folderId,
userAgent: req.headers?.["user-agent"], userAgent: req.headers?.["user-agent"]
}, }
}); });
} }
return res.status(200).send({ return res.status(200).send({
secrets, secrets,
...(include_imports && { imports: importedSecrets })
}); });
}; };
@@ -925,9 +930,7 @@ export const updateSecrets = async (req: Request, res: Response) => {
} }
} }
*/ */
const channel = req.headers?.["user-agent"]?.toLowerCase().includes("mozilla") const channel = req.headers?.["user-agent"]?.toLowerCase().includes("mozilla") ? "web" : "cli";
? "web"
: "cli";
interface PatchSecret { interface PatchSecret {
id: string; id: string;
@@ -943,51 +946,47 @@ export const updateSecrets = async (req: Request, res: Response) => {
tags: string[]; tags: string[];
} }
const updateOperationsToPerform = req.body.secrets.map( const updateOperationsToPerform = req.body.secrets.map((secret: PatchSecret) => {
(secret: PatchSecret) => { const {
const { secretKeyCiphertext,
secretKeyCiphertext, secretKeyIV,
secretKeyIV, secretKeyTag,
secretKeyTag, secretValueCiphertext,
secretValueCiphertext, secretValueIV,
secretValueIV, secretValueTag,
secretValueTag, secretCommentCiphertext,
secretCommentCiphertext, secretCommentIV,
secretCommentIV, secretCommentTag,
secretCommentTag, tags
tags, } = secret;
} = secret;
return { return {
updateOne: { updateOne: {
filter: { _id: new Types.ObjectId(secret.id) }, filter: { _id: new Types.ObjectId(secret.id) },
update: { update: {
$inc: { $inc: {
version: 1, version: 1
},
secretKeyCiphertext,
secretKeyIV,
secretKeyTag,
secretValueCiphertext,
secretValueIV,
secretValueTag,
algorithm: ALGORITHM_AES_256_GCM,
keyEncoding: ENCODING_SCHEME_UTF8,
tags,
...(secretCommentCiphertext !== undefined &&
secretCommentIV &&
secretCommentTag
? {
secretCommentCiphertext,
secretCommentIV,
secretCommentTag,
}
: {}),
}, },
}, secretKeyCiphertext,
}; secretKeyIV,
} secretKeyTag,
); secretValueCiphertext,
secretValueIV,
secretValueTag,
algorithm: ALGORITHM_AES_256_GCM,
keyEncoding: ENCODING_SCHEME_UTF8,
tags,
...(secretCommentCiphertext !== undefined && secretCommentIV && secretCommentTag
? {
secretCommentCiphertext,
secretCommentIV,
secretCommentTag
}
: {})
}
}
};
});
await Secret.bulkWrite(updateOperationsToPerform); await Secret.bulkWrite(updateOperationsToPerform);
@@ -1009,7 +1008,7 @@ export const updateSecrets = async (req: Request, res: Response) => {
secretCommentCiphertext, secretCommentCiphertext,
secretCommentIV, secretCommentIV,
secretCommentTag, secretCommentTag,
tags, tags
} = secretModificationsBySecretId[secret._id.toString()]; } = secretModificationsBySecretId[secret._id.toString()];
return { return {
@@ -1018,9 +1017,7 @@ export const updateSecrets = async (req: Request, res: Response) => {
workspace: secret.workspace, workspace: secret.workspace,
type: secret.type, type: secret.type,
environment: secret.environment, environment: secret.environment,
secretKeyCiphertext: secretKeyCiphertext secretKeyCiphertext: secretKeyCiphertext ? secretKeyCiphertext : secret.secretKeyCiphertext,
? secretKeyCiphertext
: secret.secretKeyCiphertext,
secretKeyIV: secretKeyIV ? secretKeyIV : secret.secretKeyIV, secretKeyIV: secretKeyIV ? secretKeyIV : secret.secretKeyIV,
secretKeyTag: secretKeyTag ? secretKeyTag : secret.secretKeyTag, secretKeyTag: secretKeyTag ? secretKeyTag : secret.secretKeyTag,
secretValueCiphertext: secretValueCiphertext secretValueCiphertext: secretValueCiphertext
@@ -1031,17 +1028,13 @@ export const updateSecrets = async (req: Request, res: Response) => {
secretCommentCiphertext: secretCommentCiphertext secretCommentCiphertext: secretCommentCiphertext
? secretCommentCiphertext ? secretCommentCiphertext
: secret.secretCommentCiphertext, : secret.secretCommentCiphertext,
secretCommentIV: secretCommentIV secretCommentIV: secretCommentIV ? secretCommentIV : secret.secretCommentIV,
? secretCommentIV secretCommentTag: secretCommentTag ? secretCommentTag : secret.secretCommentTag,
: secret.secretCommentIV,
secretCommentTag: secretCommentTag
? secretCommentTag
: secret.secretCommentTag,
tags: tags ? tags : secret.tags, tags: tags ? tags : secret.tags,
algorithm: ALGORITHM_AES_256_GCM, algorithm: ALGORITHM_AES_256_GCM,
keyEncoding: ENCODING_SCHEME_UTF8, keyEncoding: ENCODING_SCHEME_UTF8
}; };
}), })
}; };
await EESecretService.addSecretVersions(secretVersions); await EESecretService.addSecretVersions(secretVersions);
@@ -1059,13 +1052,16 @@ export const updateSecrets = async (req: Request, res: Response) => {
Object.keys(workspaceSecretObj).forEach(async (key) => { Object.keys(workspaceSecretObj).forEach(async (key) => {
// trigger event - push secrets // trigger event - push secrets
setTimeout(async () => { // This route is not used anymore thus keep it commented out as it does not expose environment
await EventService.handleEvent({ // it will end up creating a lot of requests from the server
event: eventPushSecrets({ // setTimeout(async () => {
workspaceId: new Types.ObjectId(key), // await EventService.handleEvent({
}), // event: eventPushSecrets({
}); // workspaceId: new Types.ObjectId(key),
}, 10000); // environment,
// })
// });
// }, 10000);
const updateAction = await EELogService.createAction({ const updateAction = await EELogService.createAction({
name: ACTION_UPDATE_SECRETS, name: ACTION_UPDATE_SECRETS,
@@ -1073,7 +1069,7 @@ export const updateSecrets = async (req: Request, res: Response) => {
serviceAccountId: req.serviceAccount?._id, serviceAccountId: req.serviceAccount?._id,
serviceTokenDataId: req.serviceTokenData?._id, serviceTokenDataId: req.serviceTokenData?._id,
workspaceId: new Types.ObjectId(key), workspaceId: new Types.ObjectId(key),
secretIds: workspaceSecretObj[key].map((secret: ISecret) => secret._id), secretIds: workspaceSecretObj[key].map((secret: ISecret) => secret._id)
}); });
// (EE) create (audit) log // (EE) create (audit) log
@@ -1085,7 +1081,7 @@ export const updateSecrets = async (req: Request, res: Response) => {
workspaceId: new Types.ObjectId(key), workspaceId: new Types.ObjectId(key),
actions: [updateAction], actions: [updateAction],
channel, channel,
ipAddress: req.realIP, ipAddress: req.realIP
})); }));
// (EE) take a secret snapshot // (EE) take a secret snapshot
@@ -1101,15 +1097,15 @@ export const updateSecrets = async (req: Request, res: Response) => {
postHogClient.capture({ postHogClient.capture({
event: "secrets modified", event: "secrets modified",
distinctId: await TelemetryService.getDistinctId({ distinctId: await TelemetryService.getDistinctId({
authData: req.authData, authData: req.authData
}), }),
properties: { properties: {
numberOfSecrets: workspaceSecretObj[key].length, numberOfSecrets: workspaceSecretObj[key].length,
environment: workspaceSecretObj[key][0].environment, environment: workspaceSecretObj[key][0].environment,
workspaceId: key, workspaceId: key,
channel: channel, channel: channel,
userAgent: req.headers?.["user-agent"], userAgent: req.headers?.["user-agent"]
}, }
}); });
} }
}); });
@@ -1117,9 +1113,9 @@ export const updateSecrets = async (req: Request, res: Response) => {
return res.status(200).send({ return res.status(200).send({
secrets: await Secret.find({ secrets: await Secret.find({
_id: { _id: {
$in: req.secrets.map((secret: ISecret) => secret._id), $in: req.secrets.map((secret: ISecret) => secret._id)
}, }
}), })
}); });
}; };
@@ -1179,12 +1175,12 @@ export const deleteSecrets = async (req: Request, res: Response) => {
await Secret.deleteMany({ await Secret.deleteMany({
_id: { _id: {
$in: toDelete, $in: toDelete
}, }
}); });
await EESecretService.markDeletedSecretVersions({ await EESecretService.markDeletedSecretVersions({
secretIds: toDelete, secretIds: toDelete
}); });
// group secrets into workspaces so deleted secrets can // group secrets into workspaces so deleted secrets can
@@ -1200,18 +1196,20 @@ export const deleteSecrets = async (req: Request, res: Response) => {
Object.keys(workspaceSecretObj).forEach(async (key) => { Object.keys(workspaceSecretObj).forEach(async (key) => {
// trigger event - push secrets // trigger event - push secrets
await EventService.handleEvent({ // DEPRECIATED(akhilmhdh): as this would cause server to send so many request
event: eventPushSecrets({ // and this route is not used anymore thus like snapshot keeping it commented out
workspaceId: new Types.ObjectId(key), // await EventService.handleEvent({
}), // event: eventPushSecrets({
}); // workspaceId: new Types.ObjectId(key)
// })
// });
const deleteAction = await EELogService.createAction({ const deleteAction = await EELogService.createAction({
name: ACTION_DELETE_SECRETS, name: ACTION_DELETE_SECRETS,
userId: req.user?._id, userId: req.user?._id,
serviceAccountId: req.serviceAccount?._id, serviceAccountId: req.serviceAccount?._id,
serviceTokenDataId: req.serviceTokenData?._id, serviceTokenDataId: req.serviceTokenData?._id,
workspaceId: new Types.ObjectId(key), workspaceId: new Types.ObjectId(key),
secretIds: workspaceSecretObj[key].map((secret: ISecret) => secret._id), secretIds: workspaceSecretObj[key].map((secret: ISecret) => secret._id)
}); });
// (EE) create (audit) log // (EE) create (audit) log
@@ -1223,7 +1221,7 @@ export const deleteSecrets = async (req: Request, res: Response) => {
workspaceId: new Types.ObjectId(key), workspaceId: new Types.ObjectId(key),
actions: [deleteAction], actions: [deleteAction],
channel, channel,
ipAddress: req.realIP, ipAddress: req.realIP
})); }));
// (EE) take a secret snapshot // (EE) take a secret snapshot
@@ -1237,20 +1235,20 @@ export const deleteSecrets = async (req: Request, res: Response) => {
postHogClient.capture({ postHogClient.capture({
event: "secrets deleted", event: "secrets deleted",
distinctId: await TelemetryService.getDistinctId({ distinctId: await TelemetryService.getDistinctId({
authData: req.authData, authData: req.authData
}), }),
properties: { properties: {
numberOfSecrets: workspaceSecretObj[key].length, numberOfSecrets: workspaceSecretObj[key].length,
environment: workspaceSecretObj[key][0].environment, environment: workspaceSecretObj[key][0].environment,
workspaceId: key, workspaceId: key,
channel: channel, channel: channel,
userAgent: req.headers?.["user-agent"], userAgent: req.headers?.["user-agent"]
}, }
}); });
} }
}); });
return res.status(200).send({ return res.status(200).send({
secrets: req.secrets, secrets: req.secrets
}); });
}; };
@@ -2,14 +2,9 @@ import { Request, Response } from "express";
import crypto from "crypto"; import crypto from "crypto";
import bcrypt from "bcrypt"; import bcrypt from "bcrypt";
import { ServiceAccount, ServiceTokenData, User } from "../../models"; import { ServiceAccount, ServiceTokenData, User } from "../../models";
import { import { AUTH_MODE_JWT, AUTH_MODE_SERVICE_ACCOUNT } from "../../variables";
AUTH_MODE_JWT,
AUTH_MODE_SERVICE_ACCOUNT,
} from "../../variables";
import { getSaltRounds } from "../../config"; import { getSaltRounds } from "../../config";
import { BadRequestError } from "../../utils/errors"; import { BadRequestError } from "../../utils/errors";
import Folder from "../../models/folder";
import { getFolderByPath } from "../../services/FolderService";
/** /**
* Return service token data associated with service token on request * Return service token data associated with service token on request
@@ -46,14 +41,13 @@ export const getServiceTokenData = async (req: Request, res: Response) => {
if (!(req.authData.authPayload instanceof ServiceTokenData)) if (!(req.authData.authPayload instanceof ServiceTokenData))
throw BadRequestError({ throw BadRequestError({
message: "Failed accepted client validation for service token data", message: "Failed accepted client validation for service token data"
}); });
const serviceTokenData = await ServiceTokenData.findById( const serviceTokenData = await ServiceTokenData.findById(req.authData.authPayload._id)
req.authData.authPayload._id
)
.select("+encryptedKey +iv +tag") .select("+encryptedKey +iv +tag")
.populate("user").lean(); .populate("user")
.lean();
return res.status(200).json(serviceTokenData); return res.status(200).json(serviceTokenData);
}; };
@@ -68,29 +62,7 @@ export const getServiceTokenData = async (req: Request, res: Response) => {
export const createServiceTokenData = async (req: Request, res: Response) => { export const createServiceTokenData = async (req: Request, res: Response) => {
let serviceTokenData; let serviceTokenData;
const { const { name, workspaceId, encryptedKey, iv, tag, expiresIn, permissions, scopes } = req.body;
name,
workspaceId,
environment,
encryptedKey,
iv,
tag,
expiresIn,
secretPath,
permissions,
} = req.body;
const folders = await Folder.findOne({
workspace: workspaceId,
environment,
});
if (folders) {
const folder = getFolderByPath(folders.nodes, secretPath);
if (folder == undefined) {
throw BadRequestError({ message: "Path for service token does not exist" })
}
}
const secret = crypto.randomBytes(16).toString("hex"); const secret = crypto.randomBytes(16).toString("hex");
const secretHash = await bcrypt.hash(secret, await getSaltRounds()); const secretHash = await bcrypt.hash(secret, await getSaltRounds());
@@ -103,10 +75,7 @@ export const createServiceTokenData = async (req: Request, res: Response) => {
let user, serviceAccount; let user, serviceAccount;
if ( if (req.authData.authMode === AUTH_MODE_JWT && req.authData.authPayload instanceof User) {
req.authData.authMode === AUTH_MODE_JWT &&
req.authData.authPayload instanceof User
) {
user = req.authData.authPayload._id; user = req.authData.authPayload._id;
} }
@@ -120,17 +89,16 @@ export const createServiceTokenData = async (req: Request, res: Response) => {
serviceTokenData = await new ServiceTokenData({ serviceTokenData = await new ServiceTokenData({
name, name,
workspace: workspaceId, workspace: workspaceId,
environment,
user, user,
serviceAccount, serviceAccount,
scopes,
lastUsed: new Date(), lastUsed: new Date(),
expiresAt, expiresAt,
secretHash, secretHash,
encryptedKey, encryptedKey,
iv, iv,
tag, tag,
secretPath, permissions
permissions,
}).save(); }).save();
// return service token data without sensitive data // return service token data without sensitive data
@@ -142,7 +110,7 @@ export const createServiceTokenData = async (req: Request, res: Response) => {
return res.status(200).send({ return res.status(200).send({
serviceToken, serviceToken,
serviceTokenData, serviceTokenData
}); });
}; };
@@ -155,11 +123,9 @@ export const createServiceTokenData = async (req: Request, res: Response) => {
export const deleteServiceTokenData = async (req: Request, res: Response) => { export const deleteServiceTokenData = async (req: Request, res: Response) => {
const { serviceTokenDataId } = req.params; const { serviceTokenDataId } = req.params;
const serviceTokenData = await ServiceTokenData.findByIdAndDelete( const serviceTokenData = await ServiceTokenData.findByIdAndDelete(serviceTokenDataId);
serviceTokenDataId
);
return res.status(200).send({ return res.status(200).send({
serviceTokenData, serviceTokenData
}); });
}; };
@@ -18,7 +18,7 @@ import { updateSubscriptionOrgQuantity } from "../../helpers/organization";
* @returns * @returns
*/ */
export const completeAccountSignup = async (req: Request, res: Response) => { export const completeAccountSignup = async (req: Request, res: Response) => {
let user, token, refreshToken; let user;
const { const {
email, email,
firstName, firstName,
@@ -119,7 +119,7 @@ export const completeAccountSignup = async (req: Request, res: Response) => {
userAgent: req.headers["user-agent"] ?? "", userAgent: req.headers["user-agent"] ?? "",
}); });
token = tokens.token; const token = tokens.token;
// sending a welcome email to new users // sending a welcome email to new users
if (await getLoopsApiKey()) { if (await getLoopsApiKey()) {
@@ -159,7 +159,7 @@ export const completeAccountSignup = async (req: Request, res: Response) => {
* @returns * @returns
*/ */
export const completeAccountInvite = async (req: Request, res: Response) => { export const completeAccountInvite = async (req: Request, res: Response) => {
let user, token, refreshToken; let user;
const { const {
email, email,
firstName, firstName,
@@ -244,7 +244,7 @@ export const completeAccountInvite = async (req: Request, res: Response) => {
userAgent: req.headers["user-agent"] ?? "", userAgent: req.headers["user-agent"] ?? "",
}); });
token = tokens.token; const token = tokens.token;
// store (refresh) token in httpOnly cookie // store (refresh) token in httpOnly cookie
res.cookie("jid", tokens.refreshToken, { res.cookie("jid", tokens.refreshToken, {
+65 -3
View File
@@ -3,10 +3,11 @@ import { Types } from "mongoose";
import crypto from "crypto"; import crypto from "crypto";
import bcrypt from "bcrypt"; import bcrypt from "bcrypt";
import { import {
MembershipOrg,
User,
APIKeyData, APIKeyData,
TokenVersion AuthProvider,
MembershipOrg,
TokenVersion,
User
} from "../../models"; } from "../../models";
import { getSaltRounds } from "../../config"; import { getSaltRounds } from "../../config";
@@ -80,6 +81,67 @@ export const updateMyMfaEnabled = async (req: Request, res: Response) => {
}); });
} }
/**
* Update name of the current user to [firstName, lastName].
* @param req
* @param res
* @returns
*/
export const updateName = async (req: Request, res: Response) => {
const {
firstName,
lastName
}: {
firstName: string;
lastName: string;
} = req.body;
const user = await User.findByIdAndUpdate(
req.user._id.toString(),
{
firstName,
lastName: lastName ?? ""
},
{
new: true
}
);
return res.status(200).send({
user,
});
}
/**
* Update auth provider of the current user to [authProvider]
* @param req
* @param res
* @returns
*/
export const updateAuthProvider = async (req: Request, res: Response) => {
const {
authProvider
} = req.body;
if (req.user?.authProvider === AuthProvider.OKTA_SAML) 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(),
{
authProvider
},
{
new: true
}
);
return res.status(200).send({
user
});
}
/** /**
* Return organizations that the current user is part of. * Return organizations that the current user is part of.
* @param req * @param req
+91 -104
View File
@@ -1,34 +1,29 @@
import { Request, Response } from "express"; import { Request, Response } from "express";
import { Types } from "mongoose"; import { Types } from "mongoose";
import { Key, Membership, ServiceTokenData, Workspace } from "../../models";
import { import {
Key, pullSecrets as pull,
Membership, v2PushSecrets as push,
ServiceTokenData, reformatPullSecrets
Workspace,
} from "../../models";
import {
pullSecrets as pull,
v2PushSecrets as push,
reformatPullSecrets,
} from "../../helpers/secret"; } from "../../helpers/secret";
import { pushKeys } from "../../helpers/key"; import { pushKeys } from "../../helpers/key";
import { EventService, TelemetryService } from "../../services"; import { EventService, TelemetryService } from "../../services";
import { eventPushSecrets } from "../../events"; import { eventPushSecrets } from "../../events";
interface V2PushSecret { interface V2PushSecret {
type: string; // personal or shared type: string; // personal or shared
secretKeyCiphertext: string; secretKeyCiphertext: string;
secretKeyIV: string; secretKeyIV: string;
secretKeyTag: string; secretKeyTag: string;
secretKeyHash: string; secretKeyHash: string;
secretValueCiphertext: string; secretValueCiphertext: string;
secretValueIV: string; secretValueIV: string;
secretValueTag: string; secretValueTag: string;
secretValueHash: string; secretValueHash: string;
secretCommentCiphertext?: string; secretCommentCiphertext?: string;
secretCommentIV?: string; secretCommentIV?: string;
secretCommentTag?: string; secretCommentTag?: string;
secretCommentHash?: string; secretCommentHash?: string;
} }
/** /**
@@ -39,7 +34,7 @@ interface V2PushSecret {
* @returns * @returns
*/ */
export const pushWorkspaceSecrets = async (req: Request, res: Response) => { export const pushWorkspaceSecrets = async (req: Request, res: Response) => {
// upload (encrypted) secrets to workspace with id [workspaceId] // upload (encrypted) secrets to workspace with id [workspaceId]
const postHogClient = await TelemetryService.getPostHogClient(); const postHogClient = await TelemetryService.getPostHogClient();
let { secrets }: { secrets: V2PushSecret[] } = req.body; let { secrets }: { secrets: V2PushSecret[] } = req.body;
const { keys, environment, channel } = req.body; const { keys, environment, channel } = req.body;
@@ -62,13 +57,13 @@ export const pushWorkspaceSecrets = async (req: Request, res: Response) => {
environment, environment,
secrets, secrets,
channel: channel ? channel : "cli", channel: channel ? channel : "cli",
ipAddress: req.realIP, ipAddress: req.realIP
}); });
await pushKeys({ await pushKeys({
userId: req.user._id, userId: req.user._id,
workspaceId, workspaceId,
keys, keys
}); });
if (postHogClient) { if (postHogClient) {
@@ -79,8 +74,8 @@ export const pushWorkspaceSecrets = async (req: Request, res: Response) => {
numberOfSecrets: secrets.length, numberOfSecrets: secrets.length,
environment, environment,
workspaceId, workspaceId,
channel: channel ? channel : "cli", channel: channel ? channel : "cli"
}, }
}); });
} }
@@ -89,12 +84,13 @@ export const pushWorkspaceSecrets = async (req: Request, res: Response) => {
event: eventPushSecrets({ event: eventPushSecrets({
workspaceId: new Types.ObjectId(workspaceId), workspaceId: new Types.ObjectId(workspaceId),
environment, environment,
}), secretPath: "/"
})
}); });
return res.status(200).send({ return res.status(200).send({
message: "Successfully uploaded workspace secrets", message: "Successfully uploaded workspace secrets"
}); });
}; };
/** /**
@@ -105,7 +101,7 @@ export const pushWorkspaceSecrets = async (req: Request, res: Response) => {
* @returns * @returns
*/ */
export const pullSecrets = async (req: Request, res: Response) => { export const pullSecrets = async (req: Request, res: Response) => {
let secrets; let secrets;
const postHogClient = await TelemetryService.getPostHogClient(); const postHogClient = await TelemetryService.getPostHogClient();
const environment: string = req.query.environment as string; const environment: string = req.query.environment as string;
const channel: string = req.query.channel as string; const channel: string = req.query.channel as string;
@@ -128,7 +124,7 @@ export const pullSecrets = async (req: Request, res: Response) => {
workspaceId, workspaceId,
environment, environment,
channel: channel ? channel : "cli", channel: channel ? channel : "cli",
ipAddress: req.realIP, ipAddress: req.realIP
}); });
if (channel !== "cli") { if (channel !== "cli") {
@@ -144,18 +140,18 @@ export const pullSecrets = async (req: Request, res: Response) => {
numberOfSecrets: secrets.length, numberOfSecrets: secrets.length,
environment, environment,
workspaceId, workspaceId,
channel: channel ? channel : "cli", channel: channel ? channel : "cli"
}, }
}); });
} }
return res.status(200).send({ return res.status(200).send({
secrets, secrets
}); });
}; };
export const getWorkspaceKey = async (req: Request, res: Response) => { export const getWorkspaceKey = async (req: Request, res: Response) => {
/* /*
#swagger.summary = 'Return encrypted project key' #swagger.summary = 'Return encrypted project key'
#swagger.description = 'Return encrypted project key' #swagger.description = 'Return encrypted project key'
@@ -183,43 +179,37 @@ export const getWorkspaceKey = async (req: Request, res: Response) => {
} }
} }
*/ */
let key;
const { workspaceId } = req.params; const { workspaceId } = req.params;
key = await Key.findOne({ const key = await Key.findOne({
workspace: workspaceId, workspace: workspaceId,
receiver: req.user._id, receiver: req.user._id
}).populate("sender", "+publicKey"); }).populate("sender", "+publicKey");
if (!key) throw new Error("Failed to find workspace key"); if (!key) throw new Error("Failed to find workspace key");
return res.status(200).json(key); return res.status(200).json(key);
} };
export const getWorkspaceServiceTokenData = async ( export const getWorkspaceServiceTokenData = async (req: Request, res: Response) => {
req: Request,
res: Response
) => {
const { workspaceId } = req.params; const { workspaceId } = req.params;
const serviceTokenData = await ServiceTokenData const serviceTokenData = await ServiceTokenData.find({
.find({ workspace: workspaceId
workspace: workspaceId, }).select("+encryptedKey +iv +tag");
})
.select("+encryptedKey +iv +tag");
return res.status(200).send({ return res.status(200).send({
serviceTokenData, serviceTokenData
}); });
} };
/** /**
* Return memberships for workspace with id [workspaceId] * Return memberships for workspace with id [workspaceId]
* @param req * @param req
* @param res * @param res
* @returns * @returns
*/ */
export const getWorkspaceMemberships = async (req: Request, res: Response) => { export const getWorkspaceMemberships = async (req: Request, res: Response) => {
/* /*
#swagger.summary = 'Return project memberships' #swagger.summary = 'Return project memberships'
#swagger.description = 'Return project memberships' #swagger.description = 'Return project memberships'
@@ -255,22 +245,22 @@ export const getWorkspaceMemberships = async (req: Request, res: Response) => {
const { workspaceId } = req.params; const { workspaceId } = req.params;
const memberships = await Membership.find({ const memberships = await Membership.find({
workspace: workspaceId, workspace: workspaceId
}).populate("user", "+publicKey"); }).populate("user", "+publicKey");
return res.status(200).send({ return res.status(200).send({
memberships, memberships
}); });
} };
/** /**
* Update role of membership with id [membershipId] to role [role] * Update role of membership with id [membershipId] to role [role]
* @param req * @param req
* @param res * @param res
* @returns * @returns
*/ */
export const updateWorkspaceMembership = async (req: Request, res: Response) => { export const updateWorkspaceMembership = async (req: Request, res: Response) => {
/* /*
#swagger.summary = 'Update project membership' #swagger.summary = 'Update project membership'
#swagger.description = 'Update project membership' #swagger.description = 'Update project membership'
@@ -323,33 +313,32 @@ export const updateWorkspaceMembership = async (req: Request, res: Response) =>
} }
} }
*/ */
const { const { membershipId } = req.params;
membershipId,
} = req.params;
const { role } = req.body; const { role } = req.body;
const membership = await Membership.findByIdAndUpdate( const membership = await Membership.findByIdAndUpdate(
membershipId, membershipId,
{ {
role, role
}, { },
new: true, {
new: true
} }
); );
return res.status(200).send({ return res.status(200).send({
membership, membership
}); });
} };
/** /**
* Delete workspace membership with id [membershipId] * Delete workspace membership with id [membershipId]
* @param req * @param req
* @param res * @param res
* @returns * @returns
*/ */
export const deleteWorkspaceMembership = async (req: Request, res: Response) => { export const deleteWorkspaceMembership = async (req: Request, res: Response) => {
/* /*
#swagger.summary = 'Delete project membership' #swagger.summary = 'Delete project membership'
#swagger.description = 'Delete project membership' #swagger.description = 'Delete project membership'
@@ -385,23 +374,21 @@ export const deleteWorkspaceMembership = async (req: Request, res: Response) =>
} }
} }
*/ */
const { const { membershipId } = req.params;
membershipId,
} = req.params;
const membership = await Membership.findByIdAndDelete(membershipId); const membership = await Membership.findByIdAndDelete(membershipId);
if (!membership) throw new Error("Failed to delete workspace membership"); if (!membership) throw new Error("Failed to delete workspace membership");
await Key.deleteMany({ await Key.deleteMany({
receiver: membership.user, receiver: membership.user,
workspace: membership.workspace, workspace: membership.workspace
}); });
return res.status(200).send({ return res.status(200).send({
membership, membership
}); });
} };
/** /**
* Change autoCapitilzation Rule of workspace * Change autoCapitilzation Rule of workspace
@@ -415,18 +402,18 @@ export const toggleAutoCapitalization = async (req: Request, res: Response) => {
const workspace = await Workspace.findOneAndUpdate( const workspace = await Workspace.findOneAndUpdate(
{ {
_id: workspaceId, _id: workspaceId
}, },
{ {
autoCapitalization, autoCapitalization
}, },
{ {
new: true, new: true
} }
); );
return res.status(200).send({ return res.status(200).send({
message: "Successfully changed autoCapitalization setting", message: "Successfully changed autoCapitalization setting",
workspace, workspace
}); });
}; };
+2 -2
View File
@@ -56,7 +56,7 @@ export const login1 = async (req: Request, res: Response) => {
if (!user) throw new Error("Failed to find user"); if (!user) throw new Error("Failed to find user");
if (user.authProvider) { if (user.authProvider && user.authProvider !== AuthProvider.EMAIL) {
await validateProviderAuthToken({ await validateProviderAuthToken({
email, email,
user, user,
@@ -117,7 +117,7 @@ export const login2 = async (req: Request, res: Response) => {
if (!user) throw new Error("Failed to find user"); if (!user) throw new Error("Failed to find user");
if (user.authProvider) { if (user.authProvider && user.authProvider !== AuthProvider.EMAIL) {
await validateProviderAuthToken({ await validateProviderAuthToken({
email, email,
user, user,
+134 -77
View File
@@ -3,8 +3,15 @@ import { Types } from "mongoose";
import { EventService, SecretService } from "../../services"; import { EventService, SecretService } from "../../services";
import { eventPushSecrets } from "../../events"; import { eventPushSecrets } from "../../events";
import { BotService } from "../../services"; import { BotService } from "../../services";
import { repackageSecretToRaw } from "../../helpers/secrets"; import { containsGlobPatterns, repackageSecretToRaw } from "../../helpers/secrets";
import { encryptSymmetric128BitHexKeyUTF8 } from "../../utils/crypto"; import { encryptSymmetric128BitHexKeyUTF8 } from "../../utils/crypto";
import { getAllImportedSecrets } from "../../services/SecretImportService";
import Folder from "../../models/folder";
import { getFolderByPath } from "../../services/FolderService";
import { BadRequestError } from "../../utils/errors";
import { IServiceTokenData } from "../../models";
import { requireWorkspaceAuth } from "../../middleware";
import { ADMIN, MEMBER, PERMISSION_READ_SECRETS } from "../../variables";
/** /**
* Return secrets for workspace with id [workspaceId] and environment * Return secrets for workspace with id [workspaceId] and environment
@@ -13,30 +20,79 @@ import { encryptSymmetric128BitHexKeyUTF8 } from "../../utils/crypto";
* @param res * @param res
*/ */
export const getSecretsRaw = async (req: Request, res: Response) => { export const getSecretsRaw = async (req: Request, res: Response) => {
const workspaceId = req.query.workspaceId as string; let workspaceId = req.query.workspaceId as string;
const environment = req.query.environment as string; let environment = req.query.environment as string;
const secretPath = req.query.secretPath as string; let secretPath = req.query.secretPath as string;
const includeImports = req.query.include_imports as string;
// if the service token has single scope, it will get all secrets for that scope by default
const serviceTokenDetails: IServiceTokenData = req?.serviceTokenData;
if (serviceTokenDetails) {
if (
serviceTokenDetails.scopes.length == 1 &&
!containsGlobPatterns(serviceTokenDetails.scopes[0].secretPath)
) {
const scope = serviceTokenDetails.scopes[0];
secretPath = scope.secretPath;
environment = scope.environment;
workspaceId = serviceTokenDetails.workspace.toString();
} else {
requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER],
locationWorkspaceId: "query",
locationEnvironment: "query",
requiredPermissions: [PERMISSION_READ_SECRETS],
requireBlindIndicesEnabled: true,
requireE2EEOff: true
});
}
}
const secrets = await SecretService.getSecrets({ const secrets = await SecretService.getSecrets({
workspaceId: new Types.ObjectId(workspaceId), workspaceId: new Types.ObjectId(workspaceId),
environment, environment,
secretPath, secretPath,
authData: req.authData, authData: req.authData
}); });
const key = await BotService.getWorkspaceKeyWithBot({ const key = await BotService.getWorkspaceKeyWithBot({
workspaceId: new Types.ObjectId(workspaceId), workspaceId: new Types.ObjectId(workspaceId)
}); });
if (includeImports === "true") {
const folders = await Folder.findOne({ workspace: workspaceId, environment });
let folderId = "root";
// if folder exist get it and replace folderid with new one
if (folders) {
const folder = getFolderByPath(folders.nodes, secretPath as string);
if (!folder) {
throw BadRequestError({ message: "Folder not found" });
}
folderId = folder.id;
}
const importedSecrets = await getAllImportedSecrets(workspaceId, environment, folderId);
return res.status(200).send({
secrets: secrets.map((secret) =>
repackageSecretToRaw({
secret,
key
})
),
imports: importedSecrets.map((el) => ({
...el,
secrets: el.secrets.map((secret) => repackageSecretToRaw({ secret, key }))
}))
});
}
return res.status(200).send({ return res.status(200).send({
secrets: secrets.map((secret) => { secrets: secrets.map((secret) => {
const rep = repackageSecretToRaw({ const rep = repackageSecretToRaw({
secret, secret,
key, key
}); });
return rep; return rep;
}), })
}); });
}; };
@@ -58,54 +114,47 @@ export const getSecretByNameRaw = async (req: Request, res: Response) => {
environment, environment,
type, type,
secretPath, secretPath,
authData: req.authData, authData: req.authData
}); });
const key = await BotService.getWorkspaceKeyWithBot({ const key = await BotService.getWorkspaceKeyWithBot({
workspaceId: new Types.ObjectId(workspaceId), workspaceId: new Types.ObjectId(workspaceId)
}); });
return res.status(200).send({ return res.status(200).send({
secret: repackageSecretToRaw({ secret: repackageSecretToRaw({
secret, secret,
key, key
}), })
}); });
}; };
/** /**
* Create secret with name [secretName] in plaintext * Create secret with name [secretName] in plaintext
* @param req * @param req
* @param res * @param res
*/ */
export const createSecretRaw = async (req: Request, res: Response) => { export const createSecretRaw = async (req: Request, res: Response) => {
const { secretName } = req.params; const { secretName } = req.params;
const { const { workspaceId, environment, type, secretValue, secretComment, secretPath = "/" } = req.body;
workspaceId,
environment,
type,
secretValue,
secretComment,
secretPath = "/",
} = req.body;
const key = await BotService.getWorkspaceKeyWithBot({ const key = await BotService.getWorkspaceKeyWithBot({
workspaceId: new Types.ObjectId(workspaceId), workspaceId: new Types.ObjectId(workspaceId)
}); });
const secretKeyEncrypted = encryptSymmetric128BitHexKeyUTF8({ const secretKeyEncrypted = encryptSymmetric128BitHexKeyUTF8({
plaintext: secretName, plaintext: secretName,
key, key
}); });
const secretValueEncrypted = encryptSymmetric128BitHexKeyUTF8({ const secretValueEncrypted = encryptSymmetric128BitHexKeyUTF8({
plaintext: secretValue, plaintext: secretValue,
key, key
}); });
const secretCommentEncrypted = encryptSymmetric128BitHexKeyUTF8({ const secretCommentEncrypted = encryptSymmetric128BitHexKeyUTF8({
plaintext: secretComment, plaintext: secretComment,
key, key
}); });
const secret = await SecretService.createSecret({ const secret = await SecretService.createSecret({
@@ -123,14 +172,15 @@ export const createSecretRaw = async (req: Request, res: Response) => {
secretPath, secretPath,
secretCommentCiphertext: secretCommentEncrypted.ciphertext, secretCommentCiphertext: secretCommentEncrypted.ciphertext,
secretCommentIV: secretCommentEncrypted.iv, secretCommentIV: secretCommentEncrypted.iv,
secretCommentTag: secretCommentEncrypted.tag, secretCommentTag: secretCommentEncrypted.tag
}); });
await EventService.handleEvent({ await EventService.handleEvent({
event: eventPushSecrets({ event: eventPushSecrets({
workspaceId: new Types.ObjectId(workspaceId), workspaceId: new Types.ObjectId(workspaceId),
environment, environment,
}), secretPath
})
}); });
const secretWithoutBlindIndex = secret.toObject(); const secretWithoutBlindIndex = secret.toObject();
@@ -139,10 +189,10 @@ export const createSecretRaw = async (req: Request, res: Response) => {
return res.status(200).send({ return res.status(200).send({
secret: repackageSecretToRaw({ secret: repackageSecretToRaw({
secret: secretWithoutBlindIndex, secret: secretWithoutBlindIndex,
key, key
}), })
}); });
} };
/** /**
* Update secret with name [secretName] * Update secret with name [secretName]
@@ -151,21 +201,15 @@ export const createSecretRaw = async (req: Request, res: Response) => {
*/ */
export const updateSecretByNameRaw = async (req: Request, res: Response) => { export const updateSecretByNameRaw = async (req: Request, res: Response) => {
const { secretName } = req.params; const { secretName } = req.params;
const { const { workspaceId, environment, type, secretValue, secretPath = "/" } = req.body;
workspaceId,
environment,
type,
secretValue,
secretPath = "/",
} = req.body;
const key = await BotService.getWorkspaceKeyWithBot({ const key = await BotService.getWorkspaceKeyWithBot({
workspaceId: new Types.ObjectId(workspaceId), workspaceId: new Types.ObjectId(workspaceId)
}); });
const secretValueEncrypted = encryptSymmetric128BitHexKeyUTF8({ const secretValueEncrypted = encryptSymmetric128BitHexKeyUTF8({
plaintext: secretValue, plaintext: secretValue,
key, key
}); });
const secret = await SecretService.updateSecret({ const secret = await SecretService.updateSecret({
@@ -177,21 +221,22 @@ export const updateSecretByNameRaw = async (req: Request, res: Response) => {
secretValueCiphertext: secretValueEncrypted.ciphertext, secretValueCiphertext: secretValueEncrypted.ciphertext,
secretValueIV: secretValueEncrypted.iv, secretValueIV: secretValueEncrypted.iv,
secretValueTag: secretValueEncrypted.tag, secretValueTag: secretValueEncrypted.tag,
secretPath, secretPath
}); });
await EventService.handleEvent({ await EventService.handleEvent({
event: eventPushSecrets({ event: eventPushSecrets({
workspaceId: new Types.ObjectId(workspaceId), workspaceId: new Types.ObjectId(workspaceId),
environment, environment,
}), secretPath
})
}); });
return res.status(200).send({ return res.status(200).send({
secret: repackageSecretToRaw({ secret: repackageSecretToRaw({
secret, secret,
key, key
}), })
}); });
}; };
@@ -202,12 +247,7 @@ export const updateSecretByNameRaw = async (req: Request, res: Response) => {
*/ */
export const deleteSecretByNameRaw = async (req: Request, res: Response) => { export const deleteSecretByNameRaw = async (req: Request, res: Response) => {
const { secretName } = req.params; const { secretName } = req.params;
const { const { workspaceId, environment, type, secretPath = "/" } = req.body;
workspaceId,
environment,
type,
secretPath = "/",
} = req.body;
const { secret } = await SecretService.deleteSecret({ const { secret } = await SecretService.deleteSecret({
secretName, secretName,
@@ -215,25 +255,26 @@ export const deleteSecretByNameRaw = async (req: Request, res: Response) => {
environment, environment,
type, type,
authData: req.authData, authData: req.authData,
secretPath, secretPath
}); });
await EventService.handleEvent({ await EventService.handleEvent({
event: eventPushSecrets({ event: eventPushSecrets({
workspaceId: new Types.ObjectId(workspaceId), workspaceId: new Types.ObjectId(workspaceId),
environment, environment,
}), secretPath
})
}); });
const key = await BotService.getWorkspaceKeyWithBot({ const key = await BotService.getWorkspaceKeyWithBot({
workspaceId: new Types.ObjectId(workspaceId), workspaceId: new Types.ObjectId(workspaceId)
}); });
return res.status(200).send({ return res.status(200).send({
secret: repackageSecretToRaw({ secret: repackageSecretToRaw({
secret, secret,
key, key
}), })
}); });
}; };
@@ -247,16 +288,35 @@ export const getSecrets = async (req: Request, res: Response) => {
const workspaceId = req.query.workspaceId as string; const workspaceId = req.query.workspaceId as string;
const environment = req.query.environment as string; const environment = req.query.environment as string;
const secretPath = req.query.secretPath as string; const secretPath = req.query.secretPath as string;
const includeImports = req.query.include_imports as string;
const secrets = await SecretService.getSecrets({ const secrets = await SecretService.getSecrets({
workspaceId: new Types.ObjectId(workspaceId), workspaceId: new Types.ObjectId(workspaceId),
environment, environment,
secretPath, secretPath,
authData: req.authData, authData: req.authData
}); });
if (includeImports === "true") {
const folders = await Folder.findOne({ workspace: workspaceId, environment });
let folderId = "root";
// if folder exist get it and replace folderid with new one
if (folders) {
const folder = getFolderByPath(folders.nodes, secretPath as string);
if (!folder) {
throw BadRequestError({ message: "Folder not found" });
}
folderId = folder.id;
}
const importedSecrets = await getAllImportedSecrets(workspaceId, environment, folderId);
return res.status(200).send({
secrets,
imports: importedSecrets
});
}
return res.status(200).send({ return res.status(200).send({
secrets, secrets
}); });
}; };
@@ -278,11 +338,11 @@ export const getSecretByName = async (req: Request, res: Response) => {
environment, environment,
type, type,
secretPath, secretPath,
authData: req.authData, authData: req.authData
}); });
return res.status(200).send({ return res.status(200).send({
secret, secret
}); });
}; };
@@ -306,7 +366,7 @@ export const createSecret = async (req: Request, res: Response) => {
secretCommentCiphertext, secretCommentCiphertext,
secretCommentIV, secretCommentIV,
secretCommentTag, secretCommentTag,
secretPath = "/", secretPath = "/"
} = req.body; } = req.body;
const secret = await SecretService.createSecret({ const secret = await SecretService.createSecret({
@@ -324,25 +384,25 @@ export const createSecret = async (req: Request, res: Response) => {
secretPath, secretPath,
secretCommentCiphertext, secretCommentCiphertext,
secretCommentIV, secretCommentIV,
secretCommentTag, secretCommentTag
}); });
await EventService.handleEvent({ await EventService.handleEvent({
event: eventPushSecrets({ event: eventPushSecrets({
workspaceId: new Types.ObjectId(workspaceId), workspaceId: new Types.ObjectId(workspaceId),
environment, environment,
}), secretPath
})
}); });
const secretWithoutBlindIndex = secret.toObject(); const secretWithoutBlindIndex = secret.toObject();
delete secretWithoutBlindIndex.secretBlindIndex; delete secretWithoutBlindIndex.secretBlindIndex;
return res.status(200).send({ return res.status(200).send({
secret: secretWithoutBlindIndex, secret: secretWithoutBlindIndex
}); });
}; };
/** /**
* Update secret with name [secretName] * Update secret with name [secretName]
* @param req * @param req
@@ -357,7 +417,7 @@ export const updateSecretByName = async (req: Request, res: Response) => {
secretValueCiphertext, secretValueCiphertext,
secretValueIV, secretValueIV,
secretValueTag, secretValueTag,
secretPath = "/", secretPath = "/"
} = req.body; } = req.body;
const secret = await SecretService.updateSecret({ const secret = await SecretService.updateSecret({
@@ -369,18 +429,19 @@ export const updateSecretByName = async (req: Request, res: Response) => {
secretValueCiphertext, secretValueCiphertext,
secretValueIV, secretValueIV,
secretValueTag, secretValueTag,
secretPath, secretPath
}); });
await EventService.handleEvent({ await EventService.handleEvent({
event: eventPushSecrets({ event: eventPushSecrets({
workspaceId: new Types.ObjectId(workspaceId), workspaceId: new Types.ObjectId(workspaceId),
environment, environment,
}), secretPath
})
}); });
return res.status(200).send({ return res.status(200).send({
secret, secret
}); });
}; };
@@ -391,12 +452,7 @@ export const updateSecretByName = async (req: Request, res: Response) => {
*/ */
export const deleteSecretByName = async (req: Request, res: Response) => { export const deleteSecretByName = async (req: Request, res: Response) => {
const { secretName } = req.params; const { secretName } = req.params;
const { const { workspaceId, environment, type, secretPath = "/" } = req.body;
workspaceId,
environment,
type,
secretPath = "/",
} = req.body;
const { secret } = await SecretService.deleteSecret({ const { secret } = await SecretService.deleteSecret({
secretName, secretName,
@@ -404,17 +460,18 @@ export const deleteSecretByName = async (req: Request, res: Response) => {
environment, environment,
type, type,
authData: req.authData, authData: req.authData,
secretPath, secretPath
}); });
await EventService.handleEvent({ await EventService.handleEvent({
event: eventPushSecrets({ event: eventPushSecrets({
workspaceId: new Types.ObjectId(workspaceId), workspaceId: new Types.ObjectId(workspaceId),
environment, environment,
}), secretPath
})
}); });
return res.status(200).send({ return res.status(200).send({
secret, secret
}); });
}; };
@@ -12,6 +12,7 @@ import { standardRequest } from "../../config/request";
import { getHttpsEnabled, getJwtSignupSecret, getLoopsApiKey } from "../../config"; import { getHttpsEnabled, getJwtSignupSecret, getLoopsApiKey } from "../../config";
import { BadRequestError } from "../../utils/errors"; import { BadRequestError } from "../../utils/errors";
import { TelemetryService } from "../../services"; import { TelemetryService } from "../../services";
import { AuthProvider } from "../../models";
/** /**
* Complete setting up user by adding their personal and auth information as part of the * Complete setting up user by adding their personal and auth information as part of the
@@ -116,11 +117,13 @@ export const completeAccountSignup = async (req: Request, res: Response) => {
if (!user) if (!user)
throw new Error("Failed to complete account for non-existent user"); // ensure user is non-null throw new Error("Failed to complete account for non-existent user"); // ensure user is non-null
// initialize default organization and workspace if (user.authProvider !== AuthProvider.OKTA_SAML) {
await initializeDefaultOrg({ // initialize default organization and workspace
organizationName, await initializeDefaultOrg({
user, organizationName,
}); user,
});
}
// update organization membership statuses that are // update organization membership statuses that are
// invited to completed with user attached // invited to completed with user attached
@@ -174,7 +177,7 @@ export const completeAccountSignup = async (req: Request, res: Response) => {
distinctId: email, distinctId: email,
properties: { properties: {
email, email,
attributionSource, ...(attributionSource ? { attributionSource } : {})
}, },
}); });
} }
+4
View File
@@ -1,6 +1,8 @@
import * as secretController from "./secretController"; import * as secretController from "./secretController";
import * as secretSnapshotController from "./secretSnapshotController"; import * as secretSnapshotController from "./secretSnapshotController";
import * as organizationsController from "./organizationsController"; import * as organizationsController from "./organizationsController";
import * as ssoController from "./ssoController";
import * as usersController from "./usersController";
import * as workspaceController from "./workspaceController"; import * as workspaceController from "./workspaceController";
import * as actionController from "./actionController"; import * as actionController from "./actionController";
import * as membershipController from "./membershipController"; import * as membershipController from "./membershipController";
@@ -10,6 +12,8 @@ export {
secretController, secretController,
secretSnapshotController, secretSnapshotController,
organizationsController, organizationsController,
ssoController,
usersController,
workspaceController, workspaceController,
actionController, actionController,
membershipController, membershipController,
@@ -27,6 +27,30 @@ export const getOrganizationPlan = async (req: Request, res: Response) => {
}); });
} }
/**
* Return checkout url for pro trial
* @param req
* @param res
* @returns
*/
export const startOrganizationTrial = async (req: Request, res: Response) => {
const { organizationId } = req.params;
const { success_url } = req.body;
const { data: { url } } = await licenseServerKeyRequest.post(
`${await getLicenseServerUrl()}/api/license-server/v1/customers/${req.organization.customerId}/session/trial`,
{
success_url
}
);
EELicenseService.delPlan(organizationId);
return res.status(200).send({
url
});
}
/** /**
* Return the organization's current plan's billing info * Return the organization's current plan's billing info
* @param req * @param req
@@ -154,6 +178,12 @@ export const addOrganizationTaxId = async (req: Request, res: Response) => {
return res.status(200).send(data); return res.status(200).send(data);
} }
/**
* Delete tax id with id [taxId] from organization tax ids on file
* @param req
* @param res
* @returns
*/
export const deleteOrganizationTaxId = async (req: Request, res: Response) => { export const deleteOrganizationTaxId = async (req: Request, res: Response) => {
const { taxId } = req.params; const { taxId } = req.params;
@@ -164,6 +194,12 @@ export const deleteOrganizationTaxId = async (req: Request, res: Response) => {
return res.status(200).send(data); return res.status(200).send(data);
} }
/**
* Return organization's invoices on file
* @param req
* @param res
* @returns
*/
export const getOrganizationInvoices = async (req: Request, res: Response) => { export const getOrganizationInvoices = async (req: Request, res: Response) => {
const { data: { invoices } } = await licenseServerKeyRequest.get( const { data: { invoices } } = await licenseServerKeyRequest.get(
`${await getLicenseServerUrl()}/api/license-server/v1/customers/${req.organization.customerId}/invoices` `${await getLicenseServerUrl()}/api/license-server/v1/customers/${req.organization.customerId}/invoices`
@@ -0,0 +1,267 @@
import { Request, Response } from "express";
import { Types } from "mongoose";
import { BotOrgService } from "../../../services";
import { SSOConfig } from "../../models";
import {
MembershipOrg,
User
} from "../../../models";
import { getSSOConfigHelper } from "../../helpers/organizations";
import { client } from "../../../config";
import { ResourceNotFoundError } from "../../../utils/errors";
import { getSiteURL } from "../../../config";
import { EELicenseService } from "../../services";
/**
* Redirect user to appropriate SSO endpoint after successful authentication
* to finish inputting their master key for logging in or signing up
* @param req
* @param res
* @returns
*/
export const redirectSSO = async (req: Request, res: Response) => {
if (req.isUserCompleted) {
return res.redirect(`${await getSiteURL()}/login/sso?token=${encodeURIComponent(req.providerAuthToken)}`);
}
return res.redirect(`${await getSiteURL()}/signup/sso?token=${encodeURIComponent(req.providerAuthToken)}`);
}
/**
* Return organization SAML SSO configuration
* @param req
* @param res
* @returns
*/
export const getSSOConfig = async (req: Request, res: Response) => {
const organizationId = req.query.organizationId as string;
const data = await getSSOConfigHelper({
organizationId: new Types.ObjectId(organizationId)
});
return res.status(200).send(data);
}
/**
* Update organization SAML SSO configuration
* @param req
* @param res
* @returns
*/
export const updateSSOConfig = async (req: Request, res: Response) => {
const {
organizationId,
authProvider,
isActive,
entryPoint,
issuer,
cert,
audience
} = req.body;
const plan = await EELicenseService.getPlan(organizationId);
if (!plan.samlSSO) return res.status(400).send({
message: "Failed to update SAML SSO configuration due to plan restriction. Upgrade plan to update SSO configuration."
});
interface PatchUpdate {
authProvider?: string;
isActive?: boolean;
encryptedEntryPoint?: string;
entryPointIV?: string;
entryPointTag?: string;
encryptedIssuer?: string;
issuerIV?: string;
issuerTag?: string;
encryptedCert?: string;
certIV?: string;
certTag?: string;
encryptedAudience?: string;
audienceIV?: string;
audienceTag?: string;
}
const update: PatchUpdate = {};
if (authProvider) {
update.authProvider = authProvider;
}
if (isActive !== undefined) {
update.isActive = isActive;
}
const key = await BotOrgService.getSymmetricKey(
new Types.ObjectId(organizationId)
);
if (entryPoint) {
const {
ciphertext: encryptedEntryPoint,
iv: entryPointIV,
tag: entryPointTag
} = client.encryptSymmetric(entryPoint, key);
update.encryptedEntryPoint = encryptedEntryPoint;
update.entryPointIV = entryPointIV;
update.entryPointTag = entryPointTag;
}
if (issuer) {
const {
ciphertext: encryptedIssuer,
iv: issuerIV,
tag: issuerTag
} = client.encryptSymmetric(issuer, key);
update.encryptedIssuer = encryptedIssuer;
update.issuerIV = issuerIV;
update.issuerTag = issuerTag;
}
if (cert) {
const {
ciphertext: encryptedCert,
iv: certIV,
tag: certTag
} = client.encryptSymmetric(cert, key);
update.encryptedCert = encryptedCert;
update.certIV = certIV;
update.certTag = certTag;
}
if (audience) {
const {
ciphertext: encryptedAudience,
iv: audienceIV,
tag: audienceTag
} = client.encryptSymmetric(audience, key);
update.encryptedAudience = encryptedAudience;
update.audienceIV = audienceIV;
update.audienceTag = audienceTag;
}
const ssoConfig = await SSOConfig.findOneAndUpdate(
{
organization: new Types.ObjectId(organizationId)
},
update,
{
new: true
}
);
if (!ssoConfig) throw ResourceNotFoundError({
message: "Failed to find SSO config to update"
});
if (update.isActive !== undefined) {
const membershipOrgs = await MembershipOrg.find({
organization: new Types.ObjectId(organizationId)
}).select("user");
if (update.isActive) {
await User.updateMany(
{
_id: {
$in: membershipOrgs.map((membershipOrg) => membershipOrg.user)
}
},
{
authProvider: ssoConfig.authProvider
}
);
} else {
await User.updateMany(
{
_id: {
$in: membershipOrgs.map((membershipOrg) => membershipOrg.user)
}
},
{
$unset: {
authProvider: 1
}
}
);
}
}
return res.status(200).send(ssoConfig);
}
/**
* Create organization SAML SSO configuration
* @param req
* @param res
* @returns
*/
export const createSSOConfig = async (req: Request, res: Response) => {
const {
organizationId,
authProvider,
isActive,
entryPoint,
issuer,
cert,
audience
} = req.body;
const plan = await EELicenseService.getPlan(organizationId);
if (!plan.samlSSO) return res.status(400).send({
message: "Failed to create SAML SSO configuration due to plan restriction. Upgrade plan to add SSO configuration."
});
const key = await BotOrgService.getSymmetricKey(
new Types.ObjectId(organizationId)
);
const {
ciphertext: encryptedEntryPoint,
iv: entryPointIV,
tag: entryPointTag
} = client.encryptSymmetric(entryPoint, key);
const {
ciphertext: encryptedIssuer,
iv: issuerIV,
tag: issuerTag
} = client.encryptSymmetric(issuer, key);
const {
ciphertext: encryptedCert,
iv: certIV,
tag: certTag
} = client.encryptSymmetric(cert, key);
const {
ciphertext: encryptedAudience,
iv: audienceIV,
tag: audienceTag
} = client.encryptSymmetric(audience, key);
const ssoConfig = await new SSOConfig({
organization: new Types.ObjectId(organizationId),
authProvider,
isActive,
encryptedEntryPoint,
entryPointIV,
entryPointTag,
encryptedIssuer,
issuerIV,
issuerTag,
encryptedCert,
certIV,
certTag,
encryptedAudience,
audienceIV,
audienceTag
}).save();
return res.status(200).send(ssoConfig);
}
@@ -0,0 +1,13 @@
import { Request, Response } from "express";
/**
* Return the ip address of the current user
* @param req
* @param res
* @returns
*/
export const getMyIp = (req: Request, res: Response) => {
return res.status(200).send({
ip: req.authData.authIP
});
}
@@ -3,16 +3,20 @@ import { PipelineStage, Types } from "mongoose";
import { Secret } from "../../../models"; import { Secret } from "../../../models";
import { import {
FolderVersion, FolderVersion,
IPType,
ISecretVersion, ISecretVersion,
Log, Log,
SecretSnapshot, SecretSnapshot,
SecretVersion, SecretVersion,
TFolderRootVersionSchema, TFolderRootVersionSchema,
TrustedIP
} from "../../models"; } from "../../models";
import { EESecretService } from "../../services"; import { EESecretService } from "../../services";
import { getLatestSecretVersionIds } from "../../helpers/secretVersion"; import { getLatestSecretVersionIds } from "../../helpers/secretVersion";
import Folder, { TFolderSchema } from "../../../models/folder"; import Folder, { TFolderSchema } from "../../../models/folder";
import { searchByFolderId } from "../../../services/FolderService"; import { searchByFolderId } from "../../../services/FolderService";
import { EELicenseService } from "../../services";
import { extractIPDetails, isValidIpOrCidr } from "../../../utils/ip";
/** /**
* Return secret snapshots for workspace with id [workspaceId] * Return secret snapshots for workspace with id [workspaceId]
@@ -588,3 +592,147 @@ export const getWorkspaceLogs = async (req: Request, res: Response) => {
logs, logs,
}); });
}; };
/**
* Return trusted ips for workspace with id [workspaceId]
* @param req
* @param res
*/
export const getWorkspaceTrustedIps = async (req: Request, res: Response) => {
const { workspaceId } = req.params;
const trustedIps = await TrustedIP.find({
workspace: new Types.ObjectId(workspaceId)
});
return res.status(200).send({
trustedIps
});
}
/**
* Add a trusted ip to workspace with id [workspaceId]
* @param req
* @param res
*/
export const addWorkspaceTrustedIp = async (req: Request, res: Response) => {
const { workspaceId } = req.params;
const {
ipAddress: ip,
comment,
isActive
} = req.body;
const plan = await EELicenseService.getPlan(req.workspace.organization.toString());
if (!plan.ipAllowlisting) return res.status(400).send({
message: "Failed to add IP access range due to plan restriction. Upgrade plan to add IP access range."
});
const isValidIPOrCidr = isValidIpOrCidr(ip);
if (!isValidIPOrCidr) return res.status(400).send({
message: "The IP is not a valid IPv4, IPv6, or CIDR block"
});
const { ipAddress, type, prefix } = extractIPDetails(ip);
const trustedIp = await new TrustedIP({
workspace: new Types.ObjectId(workspaceId),
ipAddress,
type,
prefix,
isActive,
comment,
}).save();
return res.status(200).send({
trustedIp
});
}
/**
* Update trusted ip with id [trustedIpId] workspace with id [workspaceId]
* @param req
* @param res
*/
export const updateWorkspaceTrustedIp = async (req: Request, res: Response) => {
const { workspaceId, trustedIpId } = req.params;
const {
ipAddress: ip,
comment
} = req.body;
const plan = await EELicenseService.getPlan(req.workspace.organization.toString());
if (!plan.ipAllowlisting) return res.status(400).send({
message: "Failed to update IP access range due to plan restriction. Upgrade plan to update IP access range."
});
const isValidIPOrCidr = isValidIpOrCidr(ip);
if (!isValidIPOrCidr) return res.status(400).send({
message: "The IP is not a valid IPv4, IPv6, or CIDR block"
});
const { ipAddress, type, prefix } = extractIPDetails(ip);
const updateObject: {
ipAddress: string;
type: IPType;
comment: string;
prefix?: number;
$unset?: {
prefix: number;
}
} = {
ipAddress,
type,
comment
};
if (prefix !== undefined) {
updateObject.prefix = prefix;
} else {
updateObject.$unset = { prefix: 1 };
}
const trustedIp = await TrustedIP.findOneAndUpdate(
{
_id: new Types.ObjectId(trustedIpId),
workspace: new Types.ObjectId(workspaceId),
},
updateObject,
{
new: true
}
);
return res.status(200).send({
trustedIp
});
}
/**
* Delete IP access range from workspace with id [workspaceId]
* @param req
* @param res
*/
export const deleteWorkspaceTrustedIp = async (req: Request, res: Response) => {
const { workspaceId, trustedIpId } = req.params;
const plan = await EELicenseService.getPlan(req.workspace.organization.toString());
if (!plan.ipAllowlisting) return res.status(400).send({
message: "Failed to delete IP access range due to plan restriction. Upgrade plan to delete IP access range."
});
const trustedIp = await TrustedIP.findOneAndDelete({
_id: new Types.ObjectId(trustedIpId),
workspace: new Types.ObjectId(workspaceId)
});
return res.status(200).send({
trustedIp
});
}
+72
View File
@@ -0,0 +1,72 @@
import { Types } from "mongoose";
import {
SSOConfig
} from "../models";
import {
BotOrgService
} from "../../services";
import { client } from "../../config";
import { ValidationError } from "../../utils/errors";
export const getSSOConfigHelper = async ({
organizationId,
ssoConfigId
}: {
organizationId?: Types.ObjectId;
ssoConfigId?: Types.ObjectId;
}) => {
if (!organizationId && !ssoConfigId) throw ValidationError({
message: "Getting SSO data requires either id of organization or SSO data"
});
const ssoConfig = await SSOConfig.findOne({
...(organizationId ? { organization: organizationId } : {}),
...(ssoConfigId ? { _id: ssoConfigId } : {})
});
if (!ssoConfig) throw new Error("Failed to find organization SSO data");
const key = await BotOrgService.getSymmetricKey(
ssoConfig.organization
);
const entryPoint = client.decryptSymmetric(
ssoConfig.encryptedEntryPoint,
key,
ssoConfig.entryPointIV,
ssoConfig.entryPointTag
);
const issuer = client.decryptSymmetric(
ssoConfig.encryptedIssuer,
key,
ssoConfig.issuerIV,
ssoConfig.issuerTag
);
const cert = client.decryptSymmetric(
ssoConfig.encryptedCert,
key,
ssoConfig.certIV,
ssoConfig.certTag
);
const audience = client.decryptSymmetric(
ssoConfig.encryptedAudience,
key,
ssoConfig.audienceIV,
ssoConfig.audienceTag
);
return ({
_id: ssoConfig._id,
organization: ssoConfig.organization,
authProvider: ssoConfig.authProvider,
isActive: ssoConfig.isActive,
entryPoint,
issuer,
cert,
audience
});
}
-2
View File
@@ -1,7 +1,5 @@
import requireLicenseAuth from "./requireLicenseAuth";
import requireSecretSnapshotAuth from "./requireSecretSnapshotAuth"; import requireSecretSnapshotAuth from "./requireSecretSnapshotAuth";
export { export {
requireLicenseAuth,
requireSecretSnapshotAuth, requireSecretSnapshotAuth,
} }
@@ -1,23 +0,0 @@
import { NextFunction, Request, Response } from "express";
/**
* Validate if organization hosting meets license requirements to
* access a license-specific route.
* @param {Object} obj
* @param {String[]} obj.acceptedTiers
*/
const requireLicenseAuth = ({
acceptedTiers,
}: {
acceptedTiers: string[];
}) => {
return async (req: Request, res: Response, next: NextFunction) => {
try {
} catch (err) {
}
}
}
export default requireLicenseAuth;
+1 -3
View File
@@ -66,6 +66,4 @@ const actionSchema = new Schema<IAction>(
} }
); );
const Action = model<IAction>("Action", actionSchema); export const Action = model<IAction>("Action", actionSchema);
export default Action;
+2 -4
View File
@@ -52,9 +52,7 @@ const folderRootVersionSchema = new Schema<TFolderRootVersionSchema>(
} }
); );
const FolderVersion = model<TFolderRootVersionSchema>( export const FolderVersion = model<TFolderRootVersionSchema>(
"FolderVersion", "FolderVersion",
folderRootVersionSchema folderRootVersionSchema
); );
export default FolderVersion;
+7 -18
View File
@@ -1,18 +1,7 @@
import SecretSnapshot, { ISecretSnapshot } from "./secretSnapshot"; export * from "./secretSnapshot";
import SecretVersion, { ISecretVersion } from "./secretVersion"; export * from "./secretVersion";
import FolderVersion, { TFolderRootVersionSchema } from "./folderVersion"; export * from "./folderVersion";
import Log, { ILog } from "./log"; export * from "./log";
import Action, { IAction } from "./action"; export * from "./action";
export * from "./ssoConfig";
export { export * from "./trustedIp";
SecretSnapshot,
ISecretSnapshot,
SecretVersion,
ISecretVersion,
FolderVersion,
TFolderRootVersionSchema,
Log,
ILog,
Action,
IAction,
};
+5 -6
View File
@@ -63,11 +63,10 @@ const logSchema = new Schema<ILog>(
ipAddress: { ipAddress: {
type: String, type: String,
}, },
}, { },
timestamps: true, {
} timestamps: true,
}
); );
const Log = model<ILog>("Log", logSchema); export const Log = model<ILog>("Log", logSchema);
export default Log;
+2 -4
View File
@@ -46,9 +46,7 @@ const secretSnapshotSchema = new Schema<ISecretSnapshot>(
} }
); );
const SecretSnapshot = model<ISecretSnapshot>( export const SecretSnapshot = model<ISecretSnapshot>(
"SecretSnapshot", "SecretSnapshot",
secretSnapshotSchema secretSnapshotSchema
); );
export default SecretSnapshot;
+2 -4
View File
@@ -124,9 +124,7 @@ const secretVersionSchema = new Schema<ISecretVersion>(
} }
); );
const SecretVersion = model<ISecretVersion>( export const SecretVersion = model<ISecretVersion>(
"SecretVersion", "SecretVersion",
secretVersionSchema secretVersionSchema
); );
export default SecretVersion;
+80
View File
@@ -0,0 +1,80 @@
import { Schema, Types, model } from "mongoose";
export interface ISSOConfig {
organization: Types.ObjectId;
authProvider: "okta-saml"
isActive: boolean;
encryptedEntryPoint: string;
entryPointIV: string;
entryPointTag: string;
encryptedIssuer: string;
issuerIV: string;
issuerTag: string;
encryptedCert: string;
certIV: string;
certTag: string;
encryptedAudience: string;
audienceIV: string;
audienceTag: string;
}
const ssoConfigSchema = new Schema<ISSOConfig>(
{
organization: {
type: Schema.Types.ObjectId,
ref: "Organization"
},
authProvider: {
type: String,
enum: [
"okta-saml"
],
required: true
},
isActive: {
type: Boolean,
required: true
},
encryptedEntryPoint: {
type: String
},
entryPointIV: {
type: String
},
entryPointTag: {
type: String
},
encryptedIssuer: {
type: String
},
issuerIV: {
type: String
},
issuerTag: {
type: String
},
encryptedCert: {
type: String
},
certIV: {
type: String
},
certTag: {
type: String
},
encryptedAudience: {
type: String
},
audienceIV: {
type: String
},
audienceTag: {
type: String
}
},
{
timestamps: true
}
);
export const SSOConfig = model<ISSOConfig>("SSOConfig", ssoConfigSchema);
+54
View File
@@ -0,0 +1,54 @@
import { Schema, Types, model } from "mongoose";
export enum IPType {
IPV4 = "ipv4",
IPV6 = "ipv6"
}
export interface ITrustedIP {
_id: Types.ObjectId;
workspace: Types.ObjectId;
ipAddress: string;
type: "ipv4" | "ipv6", // either IPv4/IPv6 address or network IPv4/IPv6 address
isActive: boolean;
comment: string;
prefix?: number; // CIDR
}
const trustedIpSchema = new Schema<ITrustedIP>(
{
workspace: {
type: Schema.Types.ObjectId,
ref: "Workspace",
required: true
},
ipAddress: {
type: String,
required: true
},
type: {
type: String,
enum: [
IPType.IPV4,
IPType.IPV6
],
required: true
},
prefix: {
type: Number,
required: false
},
isActive: {
type: Boolean,
required: true
},
comment: {
type: String
}
},
{
timestamps: true
}
);
export const TrustedIP = model<ITrustedIP>("TrustedIP", trustedIpSchema);
+4
View File
@@ -1,6 +1,8 @@
import secret from "./secret"; import secret from "./secret";
import secretSnapshot from "./secretSnapshot"; import secretSnapshot from "./secretSnapshot";
import organizations from "./organizations"; import organizations from "./organizations";
import sso from "./sso";
import users from "./users";
import workspace from "./workspace"; import workspace from "./workspace";
import action from "./action"; import action from "./action";
import cloudProducts from "./cloudProducts"; import cloudProducts from "./cloudProducts";
@@ -9,6 +11,8 @@ export {
secret, secret,
secretSnapshot, secretSnapshot,
organizations, organizations,
sso,
users,
workspace, workspace,
action, action,
cloudProducts, cloudProducts,
+15
View File
@@ -41,6 +41,21 @@ router.get(
organizationsController.getOrganizationPlan organizationsController.getOrganizationPlan
); );
router.post(
"/:organizationId/session/trial",
requireAuth({
acceptedAuthModes: ["jwt"],
}),
requireOrganizationAuth({
acceptedRoles: [OWNER, ADMIN, MEMBER],
acceptedStatuses: [ACCEPTED],
}),
param("organizationId").exists().trim(),
body("success_url").exists().trim(),
validateRequest,
organizationsController.startOrganizationTrial
);
router.get( router.get(
"/:organizationId/plan/billing", "/:organizationId/plan/billing",
requireAuth({ requireAuth({
+121
View File
@@ -0,0 +1,121 @@
import express from "express";
const router = express.Router();
import passport from "passport";
import {
requireAuth,
requireOrganizationAuth,
validateRequest,
} from "../../../middleware";
import { body, query } from "express-validator";
import { ssoController } from "../../controllers/v1";
import { authLimiter } from "../../../helpers/rateLimiter";
import {
ACCEPTED,
ADMIN,
OWNER
} from "../../../variables";
router.get(
"/redirect/google",
authLimiter,
(req, res, next) => {
passport.authenticate("google", {
scope: ["profile", "email"],
session: false,
...(req.query.callback_port ? {
state: req.query.callback_port as string
} : {})
})(req, res, next);
}
);
router.get(
"/google",
passport.authenticate("google", {
failureRedirect: "/login/provider/error",
session: false
}),
ssoController.redirectSSO
);
router.get(
"/redirect/saml2/:ssoIdentifier",
authLimiter,
(req, res, next) => {
const options = {
failureRedirect: "/",
additionalParams: {
RelayState: req.query.callback_port ?? ""
},
};
passport.authenticate("saml", options)(req, res, next);
}
);
router.post("/saml2/:ssoIdentifier",
passport.authenticate("saml", {
failureRedirect: "/login/provider/error",
failureFlash: true,
session: false
}),
ssoController.redirectSSO
);
router.get(
"/config",
requireAuth({
acceptedAuthModes: ["jwt"],
}),
requireOrganizationAuth({
acceptedRoles: [OWNER, ADMIN],
acceptedStatuses: [ACCEPTED],
locationOrganizationId: "query"
}),
query("organizationId").exists().trim(),
validateRequest,
ssoController.getSSOConfig
);
router.post(
"/config",
requireAuth({
acceptedAuthModes: ["jwt"],
}),
requireOrganizationAuth({
acceptedRoles: [OWNER, ADMIN],
acceptedStatuses: [ACCEPTED],
locationOrganizationId: "body"
}),
body("organizationId").exists().trim(),
body("authProvider").exists().isString(),
body("isActive").exists().isBoolean(),
body("entryPoint").exists().isString(),
body("issuer").exists().isString(),
body("cert").exists().isString(),
body("audience").exists().isString(),
validateRequest,
ssoController.createSSOConfig
);
router.patch(
"/config",
requireAuth({
acceptedAuthModes: ["jwt"],
}),
requireOrganizationAuth({
acceptedRoles: [OWNER, ADMIN],
acceptedStatuses: [ACCEPTED],
locationOrganizationId: "body"
}),
body("organizationId").exists().trim(),
body("authProvider").optional().isString(),
body("isActive").optional().isBoolean(),
body("entryPoint").optional().isString(),
body("issuer").optional().isString(),
body("cert").optional().isString(),
body("audience").optional().isString(),
validateRequest,
ssoController.updateSSOConfig
);
export default router;
+17
View File
@@ -0,0 +1,17 @@
import express from "express";
const router = express.Router();
import {
requireAuth
} from "../../../middleware";
import { AUTH_MODE_API_KEY, AUTH_MODE_JWT } from "../../../variables";
import { usersController } from "../../controllers/v1";
router.get(
"/me/ip",
requireAuth({
acceptedAuthModes: [AUTH_MODE_JWT, AUTH_MODE_API_KEY],
}),
usersController.getMyIp
);
export default router;
+72 -5
View File
@@ -6,13 +6,18 @@ import {
validateRequest, validateRequest,
} from "../../../middleware"; } from "../../../middleware";
import { body, param, query } from "express-validator"; import { body, param, query } from "express-validator";
import { ADMIN, MEMBER } from "../../../variables"; import {
ADMIN,
AUTH_MODE_API_KEY,
AUTH_MODE_JWT,
MEMBER
} from "../../../variables";
import { workspaceController } from "../../controllers/v1"; import { workspaceController } from "../../controllers/v1";
router.get( router.get(
"/:workspaceId/secret-snapshots", "/:workspaceId/secret-snapshots",
requireAuth({ requireAuth({
acceptedAuthModes: ["jwt", "apiKey"], acceptedAuthModes: [AUTH_MODE_JWT, AUTH_MODE_API_KEY],
}), }),
requireWorkspaceAuth({ requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER], acceptedRoles: [ADMIN, MEMBER],
@@ -30,7 +35,7 @@ router.get(
router.get( router.get(
"/:workspaceId/secret-snapshots/count", "/:workspaceId/secret-snapshots/count",
requireAuth({ requireAuth({
acceptedAuthModes: ["jwt"], acceptedAuthModes: [AUTH_MODE_JWT],
}), }),
requireWorkspaceAuth({ requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER], acceptedRoles: [ADMIN, MEMBER],
@@ -46,7 +51,7 @@ router.get(
router.post( router.post(
"/:workspaceId/secret-snapshots/rollback", "/:workspaceId/secret-snapshots/rollback",
requireAuth({ requireAuth({
acceptedAuthModes: ["jwt", "apiKey"], acceptedAuthModes: [AUTH_MODE_JWT, AUTH_MODE_API_KEY],
}), }),
requireWorkspaceAuth({ requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER], acceptedRoles: [ADMIN, MEMBER],
@@ -63,7 +68,7 @@ router.post(
router.get( router.get(
"/:workspaceId/logs", "/:workspaceId/logs",
requireAuth({ requireAuth({
acceptedAuthModes: ["jwt", "apiKey"], acceptedAuthModes: [AUTH_MODE_JWT, AUTH_MODE_API_KEY],
}), }),
requireWorkspaceAuth({ requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER], acceptedRoles: [ADMIN, MEMBER],
@@ -79,4 +84,66 @@ router.get(
workspaceController.getWorkspaceLogs workspaceController.getWorkspaceLogs
); );
router.get(
"/:workspaceId/trusted-ips",
param("workspaceId").exists().isString().trim(),
requireAuth({
acceptedAuthModes: [AUTH_MODE_JWT],
}),
requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER],
locationWorkspaceId: "params",
}),
workspaceController.getWorkspaceTrustedIps
);
router.post(
"/:workspaceId/trusted-ips",
param("workspaceId").exists().isString().trim(),
body("ipAddress").exists().isString().trim(),
body("comment").default("").isString().trim(),
body("isActive").exists().isBoolean(),
validateRequest,
requireAuth({
acceptedAuthModes: [AUTH_MODE_JWT],
}),
requireWorkspaceAuth({
acceptedRoles: [ADMIN],
locationWorkspaceId: "params",
}),
workspaceController.addWorkspaceTrustedIp
);
router.patch(
"/:workspaceId/trusted-ips/:trustedIpId",
param("workspaceId").exists().isString().trim(),
param("trustedIpId").exists().isString().trim(),
body("ipAddress").isString().trim().default(""),
body("comment").default("").isString().trim(),
validateRequest,
requireAuth({
acceptedAuthModes: [AUTH_MODE_JWT],
}),
requireWorkspaceAuth({
acceptedRoles: [ADMIN],
locationWorkspaceId: "params",
}),
workspaceController.updateWorkspaceTrustedIp
);
router.delete(
"/:workspaceId/trusted-ips/:trustedIpId",
param("workspaceId").exists().isString().trim(),
param("trustedIpId").exists().isString().trim(),
validateRequest,
requireAuth({
acceptedAuthModes: [AUTH_MODE_JWT],
}),
requireWorkspaceAuth({
acceptedRoles: [ADMIN],
locationWorkspaceId: "params",
}),
workspaceController.deleteWorkspaceTrustedIp
);
export default router; export default router;
+15 -3
View File
@@ -26,12 +26,15 @@ interface FeatureSet {
environmentsUsed: number; environmentsUsed: number;
secretVersioning: boolean; secretVersioning: boolean;
pitRecovery: boolean; pitRecovery: boolean;
ipAllowlisting: boolean;
rbac: boolean; rbac: boolean;
customRateLimits: boolean; customRateLimits: boolean;
customAlerts: boolean; customAlerts: boolean;
auditLogs: boolean; auditLogs: boolean;
status: 'incomplete' | 'incomplete_expired' | 'trialing' | 'active' | 'past_due' | 'canceled' | 'unpaid' | null; samlSSO: boolean;
status: "incomplete" | "incomplete_expired" | "trialing" | "active" | "past_due" | "canceled" | "unpaid" | null;
trial_end: number | null; trial_end: number | null;
has_used_trial: boolean;
} }
/** /**
@@ -58,12 +61,15 @@ class EELicenseService {
environmentsUsed: 0, environmentsUsed: 0,
secretVersioning: true, secretVersioning: true,
pitRecovery: false, pitRecovery: false,
ipAllowlisting: false,
rbac: true, rbac: true,
customRateLimits: true, customRateLimits: true,
customAlerts: true, customAlerts: true,
auditLogs: false, auditLogs: false,
samlSSO: false,
status: null, status: null,
trial_end: null trial_end: null,
has_used_trial: true
} }
public localFeatureSet: NodeCache; public localFeatureSet: NodeCache;
@@ -71,7 +77,7 @@ class EELicenseService {
constructor() { constructor() {
this._isLicenseValid = true; this._isLicenseValid = true;
this.localFeatureSet = new NodeCache({ this.localFeatureSet = new NodeCache({
stdTTL: 300, stdTTL: 60,
}); });
} }
@@ -112,6 +118,12 @@ class EELicenseService {
await this.getPlan(organizationId, workspaceId); await this.getPlan(organizationId, workspaceId);
} }
} }
public async delPlan(organizationId: string) {
if (this.instanceType === "cloud") {
this.localFeatureSet.del(`${organizationId}-`);
}
}
public async initGlobalFeatureSet() { public async initGlobalFeatureSet() {
const licenseServerKey = await getLicenseServerKey(); const licenseServerKey = await getLicenseServerKey();
+3 -4
View File
@@ -1,5 +1,4 @@
import { eventPushSecrets } from "./secret" import { eventPushSecrets } from "./secret";
import { eventStartIntegration } from "./integration";
export { export { eventPushSecrets, eventStartIntegration };
eventPushSecrets,
}
+23
View File
@@ -0,0 +1,23 @@
import { Types } from "mongoose";
import { EVENT_START_INTEGRATION } from "../variables";
/*
* Return event for starting integrations
* @param {Object} obj
* @param {String} obj.workspaceId - id of workspace to push secrets to
* @returns
*/
export const eventStartIntegration = ({
workspaceId,
environment
}: {
workspaceId: Types.ObjectId;
environment: string;
}) => {
return {
name: EVENT_START_INTEGRATION,
workspaceId,
environment,
payload: {}
};
};
+33 -43
View File
@@ -1,64 +1,54 @@
import { Types } from "mongoose"; import { Types } from "mongoose";
import { import { EVENT_PULL_SECRETS, EVENT_PUSH_SECRETS } from "../variables";
EVENT_PULL_SECRETS,
EVENT_PUSH_SECRETS,
} from "../variables";
interface PushSecret { interface PushSecret {
ciphertextKey: string; ciphertextKey: string;
ivKey: string; ivKey: string;
tagKey: string; tagKey: string;
hashKey: string; hashKey: string;
ciphertextValue: string; ciphertextValue: string;
ivValue: string; ivValue: string;
tagValue: string; tagValue: string;
hashValue: string; hashValue: string;
type: "shared" | "personal"; type: "shared" | "personal";
} }
/** /**
* Return event for pushing secrets * Return event for pushing secrets
* @param {Object} obj * @param {Object} obj
* @param {String} obj.workspaceId - id of workspace to push secrets to * @param {String} obj.workspaceId - id of workspace to push secrets to
* @returns * @returns
*/ */
const eventPushSecrets = ({ const eventPushSecrets = ({
workspaceId,
environment,
secretPath
}: {
workspaceId: Types.ObjectId;
environment: string;
secretPath: string;
}) => {
return {
name: EVENT_PUSH_SECRETS,
workspaceId, workspaceId,
environment, environment,
}: { secretPath,
workspaceId: Types.ObjectId; payload: {}
environment?: string; };
}) => { };
return ({
name: EVENT_PUSH_SECRETS,
workspaceId,
environment,
payload: {
},
});
}
/** /**
* Return event for pulling secrets * Return event for pulling secrets
* @param {Object} obj * @param {Object} obj
* @param {String} obj.workspaceId - id of workspace to pull secrets from * @param {String} obj.workspaceId - id of workspace to pull secrets from
* @returns * @returns
*/ */
const eventPullSecrets = ({ const eventPullSecrets = ({ workspaceId }: { workspaceId: string }) => {
return {
name: EVENT_PULL_SECRETS,
workspaceId, workspaceId,
}: { payload: {}
workspaceId: string; };
}) => { };
return ({
name: EVENT_PULL_SECRETS,
workspaceId,
payload: {
}, export { eventPushSecrets };
});
}
export {
eventPushSecrets,
}
+134
View File
@@ -0,0 +1,134 @@
import { Types } from "mongoose";
import { client, getEncryptionKey, getRootEncryptionKey } from "../config";
import { BotOrg } from "../models";
import { decryptSymmetric128BitHexKeyUTF8 } from "../utils/crypto";
import {
ALGORITHM_AES_256_GCM,
ENCODING_SCHEME_BASE64,
ENCODING_SCHEME_UTF8
} from "../variables";
import { InternalServerError } from "../utils/errors";
import { encryptSymmetric128BitHexKeyUTF8, generateKeyPair } from "../utils/crypto";
/**
* Create a bot with name [name] for organization with id [organizationId]
* @param {Object} obj
* @param {String} obj.name - name of bot
* @param {String} obj.organizationId - id of organization that bot belongs to
*/
export const createBotOrg = async ({
name,
organizationId,
}: {
name: string;
organizationId: Types.ObjectId;
}) => {
const encryptionKey = await getEncryptionKey();
const rootEncryptionKey = await getRootEncryptionKey();
const { publicKey, privateKey } = generateKeyPair();
const key = client.createSymmetricKey();
if (rootEncryptionKey) {
const {
ciphertext: encryptedPrivateKey,
iv: privateKeyIV,
tag: privateKeyTag
} = client.encryptSymmetric(privateKey, rootEncryptionKey);
const {
ciphertext: encryptedSymmetricKey,
iv: symmetricKeyIV,
tag: symmetricKeyTag
} = client.encryptSymmetric(key, rootEncryptionKey);
return await new BotOrg({
name,
organization: organizationId,
publicKey,
encryptedSymmetricKey,
symmetricKeyIV,
symmetricKeyTag,
symmetricKeyAlgorithm: ALGORITHM_AES_256_GCM,
symmetricKeyKeyEncoding: ENCODING_SCHEME_BASE64,
encryptedPrivateKey,
privateKeyIV,
privateKeyTag,
privateKeyAlgorithm: ALGORITHM_AES_256_GCM,
privateKeyKeyEncoding: ENCODING_SCHEME_BASE64
}).save();
} else if (encryptionKey) {
const {
ciphertext: encryptedPrivateKey,
iv: privateKeyIV,
tag: privateKeyTag
} = encryptSymmetric128BitHexKeyUTF8({
plaintext: privateKey,
key: encryptionKey
});
const {
ciphertext: encryptedSymmetricKey,
iv: symmetricKeyIV,
tag: symmetricKeyTag
} = encryptSymmetric128BitHexKeyUTF8({
plaintext: key,
key: encryptionKey
});
return await new BotOrg({
name,
organization: organizationId,
publicKey,
encryptedSymmetricKey,
symmetricKeyIV,
symmetricKeyTag,
symmetricKeyAlgorithm: ALGORITHM_AES_256_GCM,
symmetricKeyKeyEncoding: ENCODING_SCHEME_UTF8,
encryptedPrivateKey,
privateKeyIV,
privateKeyTag,
privateKeyAlgorithm: ALGORITHM_AES_256_GCM,
privateKeyKeyEncoding: ENCODING_SCHEME_UTF8
}).save();
}
throw InternalServerError({
message: "Failed to create new organization bot due to missing encryption key",
});
};
export const getSymmetricKeyHelper = async (organizationId: Types.ObjectId) => {
const rootEncryptionKey = await getRootEncryptionKey();
const encryptionKey = await getEncryptionKey();
const botOrg = await BotOrg.findOne({
organization: organizationId
});
if (!botOrg) throw new Error("Failed to find organization bot");
if (rootEncryptionKey && botOrg.symmetricKeyKeyEncoding == ENCODING_SCHEME_BASE64) {
const key = client.decryptSymmetric(
botOrg.encryptedSymmetricKey,
rootEncryptionKey,
botOrg.symmetricKeyIV,
botOrg.symmetricKeyTag
);
return key;
} else if (encryptionKey && botOrg.symmetricKeyKeyEncoding === ENCODING_SCHEME_UTF8) {
const key = decryptSymmetric128BitHexKeyUTF8({
ciphertext: botOrg.encryptedSymmetricKey,
iv: botOrg.symmetricKeyIV,
tag: botOrg.symmetricKeyTag,
key: encryptionKey
});
return key;
}
throw InternalServerError({
message: "Failed to match encryption key with organization bot symmetric key encoding"
});
}
+21 -10
View File
@@ -1,12 +1,14 @@
import { Types } from "mongoose"; import { Types } from "mongoose";
import { Bot } from "../models"; import { Bot } from "../models";
import { EVENT_PUSH_SECRETS } from "../variables"; import { EVENT_PUSH_SECRETS, EVENT_START_INTEGRATION } from "../variables";
import { IntegrationService } from "../services"; import { IntegrationService } from "../services";
import { triggerWebhook } from "../services/WebhookService";
interface Event { interface Event {
name: string; name: string;
workspaceId: Types.ObjectId; workspaceId: Types.ObjectId;
environment?: string; environment?: string;
secretPath?: string;
payload: any; payload: any;
} }
@@ -19,22 +21,31 @@ interface Event {
* @param {Object} obj.event.payload - payload of event (depends on event) * @param {Object} obj.event.payload - payload of event (depends on event)
*/ */
export const handleEventHelper = async ({ event }: { event: Event }) => { export const handleEventHelper = async ({ event }: { event: Event }) => {
const { workspaceId, environment } = event; const { workspaceId, environment, secretPath } = event;
// TODO: moduralize bot check into separate function // TODO: moduralize bot check into separate function
const bot = await Bot.findOne({ const bot = await Bot.findOne({
workspace: workspaceId, workspace: workspaceId,
isActive: true, isActive: true
}); });
if (!bot) return;
switch (event.name) { switch (event.name) {
case EVENT_PUSH_SECRETS: case EVENT_PUSH_SECRETS:
IntegrationService.syncIntegrations({ if (bot) {
workspaceId, await IntegrationService.syncIntegrations({
environment, workspaceId,
}); environment
});
}
triggerWebhook(workspaceId.toString(), environment || "", secretPath || "");
break;
case EVENT_START_INTEGRATION:
if (bot) {
IntegrationService.syncIntegrations({
workspaceId,
environment
});
}
break; break;
} }
}; };
+41 -34
View File
@@ -9,6 +9,7 @@ import {
INTEGRATION_VERCEL, INTEGRATION_VERCEL,
} from "../variables"; } from "../variables";
import { UnauthorizedRequestError } from "../utils/errors"; import { UnauthorizedRequestError } from "../utils/errors";
import * as Sentry from "@sentry/node";
interface Update { interface Update {
workspace: string; workspace: string;
@@ -115,46 +116,52 @@ export const syncIntegrationsHelper = async ({
workspaceId: Types.ObjectId; workspaceId: Types.ObjectId;
environment?: string; environment?: string;
}) => { }) => {
const integrations = await Integration.find({ try {
workspace: workspaceId, const integrations = await Integration.find({
...(environment workspace: workspaceId,
? { ...(environment
? {
environment, environment,
} }
: {}), : {}),
isActive: true, isActive: true,
app: { $ne: null }, app: { $ne: null },
});
// for each workspace integration, sync/push secrets
// to that integration
for await (const integration of integrations) {
// get workspace, environment (shared) secrets
const secrets = await BotService.getSecrets({
// issue here?
workspaceId: integration.workspace,
environment: integration.environment,
secretPath: integration.secretPath,
}); });
const integrationAuth = await IntegrationAuth.findById( // for each workspace integration, sync/push secrets
integration.integrationAuth // to that integration
); for await (const integration of integrations) {
if (!integrationAuth) throw new Error("Failed to find integration auth"); // get workspace, environment (shared) secrets
const secrets = await BotService.getSecrets({
// issue here?
workspaceId: integration.workspace,
environment: integration.environment,
secretPath: integration.secretPath,
});
// get integration auth access token const integrationAuth = await IntegrationAuth.findById(
const access = await getIntegrationAuthAccessHelper({ integration.integrationAuth
integrationAuthId: integration.integrationAuth, );
}); if (!integrationAuth) throw new Error("Failed to find integration auth");
// sync secrets to integration // get integration auth access token
await syncSecrets({ const access = await getIntegrationAuthAccessHelper({
integration, integrationAuthId: integration.integrationAuth,
integrationAuth, });
secrets,
accessId: access.accessId === undefined ? null : access.accessId, // sync secrets to integration
accessToken: access.accessToken, await syncSecrets({
}); integration,
integrationAuth,
secrets,
accessId: access.accessId === undefined ? null : access.accessId,
accessToken: access.accessToken,
});
}
} catch (err) {
Sentry.captureException(err);
console.log(`syncIntegrationsHelper: failed with [workspaceId=${workspaceId}] [environment=${environment}]`, err) // eslint-disable-line no-use-before-define
throw err
} }
}; };
+10
View File
@@ -14,6 +14,9 @@ import {
licenseKeyRequest, licenseKeyRequest,
licenseServerKeyRequest, licenseServerKeyRequest,
} from "../config/request"; } from "../config/request";
import {
createBotOrg
} from "./botOrg";
/** /**
* Create an organization with name [name] * Create an organization with name [name]
@@ -29,6 +32,7 @@ export const createOrganization = async ({
name: string; name: string;
email: string; email: string;
}) => { }) => {
const licenseServerKey = await getLicenseServerKey(); const licenseServerKey = await getLicenseServerKey();
let organization; let organization;
@@ -52,6 +56,12 @@ export const createOrganization = async ({
}).save(); }).save();
} }
// initialize bot for organization
await createBotOrg({
name,
organizationId: organization._id
});
return organization; return organization;
}; };
+151 -171
View File
@@ -4,13 +4,14 @@ import {
DeleteSecretParams, DeleteSecretParams,
GetSecretParams, GetSecretParams,
GetSecretsParams, GetSecretsParams,
UpdateSecretParams, UpdateSecretParams
} from "../interfaces/services/SecretService"; } from "../interfaces/services/SecretService";
import { import {
ISecret, ISecret,
IServiceTokenData,
Secret, Secret,
SecretBlindIndexData, SecretBlindIndexData,
ServiceTokenData, ServiceTokenData
} from "../models"; } from "../models";
import { SecretVersion } from "../ee/models"; import { SecretVersion } from "../ee/models";
import { import {
@@ -18,7 +19,7 @@ import {
InternalServerError, InternalServerError,
SecretBlindIndexDataNotFoundError, SecretBlindIndexDataNotFoundError,
SecretNotFoundError, SecretNotFoundError,
UnauthorizedRequestError, UnauthorizedRequestError
} from "../utils/errors"; } from "../utils/errors";
import { import {
ACTION_ADD_SECRETS, ACTION_ADD_SECRETS,
@@ -29,51 +30,65 @@ import {
ENCODING_SCHEME_BASE64, ENCODING_SCHEME_BASE64,
ENCODING_SCHEME_UTF8, ENCODING_SCHEME_UTF8,
SECRET_PERSONAL, SECRET_PERSONAL,
SECRET_SHARED, SECRET_SHARED
} from "../variables"; } from "../variables";
import crypto from "crypto"; import crypto from "crypto";
import * as argon2 from "argon2"; import * as argon2 from "argon2";
import { import {
decryptSymmetric128BitHexKeyUTF8, decryptSymmetric128BitHexKeyUTF8,
encryptSymmetric128BitHexKeyUTF8, encryptSymmetric128BitHexKeyUTF8
} from "../utils/crypto"; } from "../utils/crypto";
import { TelemetryService } from "../services"; import { TelemetryService } from "../services";
import { client, getEncryptionKey, getRootEncryptionKey } from "../config"; import { client, getEncryptionKey, getRootEncryptionKey } from "../config";
import { EELogService, EESecretService } from "../ee/services"; import { EELogService, EESecretService } from "../ee/services";
import { import { getAuthDataPayloadIdObj, getAuthDataPayloadUserObj } from "../utils/auth";
getAuthDataPayloadIdObj,
getAuthDataPayloadUserObj,
} from "../utils/auth";
import { getFolderIdFromServiceToken } from "../services/FolderService"; import { getFolderIdFromServiceToken } from "../services/FolderService";
import picomatch from "picomatch";
import path from "path";
export const isValidScope = (
authPayload: IServiceTokenData,
environment: string,
secretPath: string
) => {
const { scopes: tkScopes } = authPayload;
const validScope = tkScopes.find(
(scope) =>
picomatch.isMatch(secretPath, scope.secretPath, { strictSlashes: false }) &&
scope.environment === environment
);
return Boolean(validScope);
};
export function containsGlobPatterns(secretPath: string) {
const globChars = ["*", "?", "[", "]", "{", "}", "**"];
const normalizedPath = path.normalize(secretPath);
return globChars.some(char => normalizedPath.includes(char));
}
/** /**
* Returns an object containing secret [secret] but with its value, key, comment decrypted. * Returns an object containing secret [secret] but with its value, key, comment decrypted.
* *
* Precondition: the workspace for secret [secret] must have E2EE disabled * Precondition: the workspace for secret [secret] must have E2EE disabled
* @param {ISecret} secret - secret to repackage to raw * @param {ISecret} secret - secret to repackage to raw
* @param {String} key - symmetric key to use to decrypt secret * @param {String} key - symmetric key to use to decrypt secret
* @returns * @returns
*/ */
export const repackageSecretToRaw = ({ export const repackageSecretToRaw = ({ secret, key }: { secret: ISecret; key: string }) => {
secret,
key,
}: {
secret: ISecret;
key: string;
}) => {
const secretKey = decryptSymmetric128BitHexKeyUTF8({ const secretKey = decryptSymmetric128BitHexKeyUTF8({
ciphertext: secret.secretKeyCiphertext, ciphertext: secret.secretKeyCiphertext,
iv: secret.secretKeyIV, iv: secret.secretKeyIV,
tag: secret.secretKeyTag, tag: secret.secretKeyTag,
key, key
}); });
const secretValue = decryptSymmetric128BitHexKeyUTF8({ const secretValue = decryptSymmetric128BitHexKeyUTF8({
ciphertext: secret.secretValueCiphertext, ciphertext: secret.secretValueCiphertext,
iv: secret.secretValueIV, iv: secret.secretValueIV,
tag: secret.secretValueTag, tag: secret.secretValueTag,
key, key
}); });
let secretComment = ""; let secretComment = "";
@@ -83,11 +98,11 @@ export const repackageSecretToRaw = ({
ciphertext: secret.secretCommentCiphertext, ciphertext: secret.secretCommentCiphertext,
iv: secret.secretCommentIV, iv: secret.secretCommentIV,
tag: secret.secretCommentTag, tag: secret.secretCommentTag,
key, key
}); });
} }
return ({ return {
_id: secret._id, _id: secret._id,
version: secret.version, version: secret.version,
workspace: secret.workspace, workspace: secret.workspace,
@@ -96,9 +111,9 @@ export const repackageSecretToRaw = ({
user: secret.user, user: secret.user,
secretKey, secretKey,
secretValue, secretValue,
secretComment, secretComment
}); };
} };
/** /**
* Create secret blind index data containing encrypted blind index [salt] * Create secret blind index data containing encrypted blind index [salt]
@@ -107,7 +122,7 @@ export const repackageSecretToRaw = ({
* @param {Types.ObjectId} obj.workspaceId * @param {Types.ObjectId} obj.workspaceId
*/ */
export const createSecretBlindIndexDataHelper = async ({ export const createSecretBlindIndexDataHelper = async ({
workspaceId, workspaceId
}: { }: {
workspaceId: Types.ObjectId; workspaceId: Types.ObjectId;
}) => { }) => {
@@ -121,7 +136,7 @@ export const createSecretBlindIndexDataHelper = async ({
const { const {
ciphertext: encryptedSaltCiphertext, ciphertext: encryptedSaltCiphertext,
iv: saltIV, iv: saltIV,
tag: saltTag, tag: saltTag
} = client.encryptSymmetric(salt, rootEncryptionKey); } = client.encryptSymmetric(salt, rootEncryptionKey);
return await new SecretBlindIndexData({ return await new SecretBlindIndexData({
@@ -130,16 +145,16 @@ export const createSecretBlindIndexDataHelper = async ({
saltIV, saltIV,
saltTag, saltTag,
algorithm: ALGORITHM_AES_256_GCM, algorithm: ALGORITHM_AES_256_GCM,
keyEncoding: ENCODING_SCHEME_BASE64, keyEncoding: ENCODING_SCHEME_BASE64
}).save(); }).save();
} else { } else {
const { const {
ciphertext: encryptedSaltCiphertext, ciphertext: encryptedSaltCiphertext,
iv: saltIV, iv: saltIV,
tag: saltTag, tag: saltTag
} = encryptSymmetric128BitHexKeyUTF8({ } = encryptSymmetric128BitHexKeyUTF8({
plaintext: salt, plaintext: salt,
key: encryptionKey, key: encryptionKey
}); });
return await new SecretBlindIndexData({ return await new SecretBlindIndexData({
@@ -148,7 +163,7 @@ export const createSecretBlindIndexDataHelper = async ({
saltIV, saltIV,
saltTag, saltTag,
algorithm: ALGORITHM_AES_256_GCM, algorithm: ALGORITHM_AES_256_GCM,
keyEncoding: ENCODING_SCHEME_UTF8, keyEncoding: ENCODING_SCHEME_UTF8
}).save(); }).save();
} }
}; };
@@ -160,7 +175,7 @@ export const createSecretBlindIndexDataHelper = async ({
* @returns * @returns
*/ */
export const getSecretBlindIndexSaltHelper = async ({ export const getSecretBlindIndexSaltHelper = async ({
workspaceId, workspaceId
}: { }: {
workspaceId: Types.ObjectId; workspaceId: Types.ObjectId;
}) => { }) => {
@@ -168,36 +183,30 @@ export const getSecretBlindIndexSaltHelper = async ({
const rootEncryptionKey = await getRootEncryptionKey(); const rootEncryptionKey = await getRootEncryptionKey();
const secretBlindIndexData = await SecretBlindIndexData.findOne({ const secretBlindIndexData = await SecretBlindIndexData.findOne({
workspace: workspaceId, workspace: workspaceId
}).select("+algorithm +keyEncoding"); }).select("+algorithm +keyEncoding");
if (!secretBlindIndexData) throw SecretBlindIndexDataNotFoundError(); if (!secretBlindIndexData) throw SecretBlindIndexDataNotFoundError();
if ( if (rootEncryptionKey && secretBlindIndexData.keyEncoding === ENCODING_SCHEME_BASE64) {
rootEncryptionKey &&
secretBlindIndexData.keyEncoding === ENCODING_SCHEME_BASE64
) {
return client.decryptSymmetric( return client.decryptSymmetric(
secretBlindIndexData.encryptedSaltCiphertext, secretBlindIndexData.encryptedSaltCiphertext,
rootEncryptionKey, rootEncryptionKey,
secretBlindIndexData.saltIV, secretBlindIndexData.saltIV,
secretBlindIndexData.saltTag secretBlindIndexData.saltTag
); );
} else if ( } else if (encryptionKey && secretBlindIndexData.keyEncoding === ENCODING_SCHEME_UTF8) {
encryptionKey &&
secretBlindIndexData.keyEncoding === ENCODING_SCHEME_UTF8
) {
// decrypt workspace salt // decrypt workspace salt
return decryptSymmetric128BitHexKeyUTF8({ return decryptSymmetric128BitHexKeyUTF8({
ciphertext: secretBlindIndexData.encryptedSaltCiphertext, ciphertext: secretBlindIndexData.encryptedSaltCiphertext,
iv: secretBlindIndexData.saltIV, iv: secretBlindIndexData.saltIV,
tag: secretBlindIndexData.saltTag, tag: secretBlindIndexData.saltTag,
key: encryptionKey, key: encryptionKey
}); });
} }
throw InternalServerError({ throw InternalServerError({
message: "Failed to obtain workspace salt needed for secret blind indexing", message: "Failed to obtain workspace salt needed for secret blind indexing"
}); });
}; };
@@ -210,7 +219,7 @@ export const getSecretBlindIndexSaltHelper = async ({
*/ */
export const generateSecretBlindIndexWithSaltHelper = async ({ export const generateSecretBlindIndexWithSaltHelper = async ({
secretName, secretName,
salt, salt
}: { }: {
secretName: string; secretName: string;
salt: string; salt: string;
@@ -224,7 +233,7 @@ export const generateSecretBlindIndexWithSaltHelper = async ({
memoryCost: 65536, // default pool of 64 MiB per thread. memoryCost: 65536, // default pool of 64 MiB per thread.
hashLength: 32, hashLength: 32,
parallelism: 1, parallelism: 1,
raw: true, raw: true
}) })
).toString("base64"); ).toString("base64");
@@ -240,7 +249,7 @@ export const generateSecretBlindIndexWithSaltHelper = async ({
*/ */
export const generateSecretBlindIndexHelper = async ({ export const generateSecretBlindIndexHelper = async ({
secretName, secretName,
workspaceId, workspaceId
}: { }: {
secretName: string; secretName: string;
workspaceId: Types.ObjectId; workspaceId: Types.ObjectId;
@@ -250,16 +259,13 @@ export const generateSecretBlindIndexHelper = async ({
const rootEncryptionKey = await getRootEncryptionKey(); const rootEncryptionKey = await getRootEncryptionKey();
const secretBlindIndexData = await SecretBlindIndexData.findOne({ const secretBlindIndexData = await SecretBlindIndexData.findOne({
workspace: workspaceId, workspace: workspaceId
}).select("+algorithm +keyEncoding"); }).select("+algorithm +keyEncoding");
if (!secretBlindIndexData) throw SecretBlindIndexDataNotFoundError(); if (!secretBlindIndexData) throw SecretBlindIndexDataNotFoundError();
let salt; let salt;
if ( if (rootEncryptionKey && secretBlindIndexData.keyEncoding === ENCODING_SCHEME_BASE64) {
rootEncryptionKey &&
secretBlindIndexData.keyEncoding === ENCODING_SCHEME_BASE64
) {
salt = client.decryptSymmetric( salt = client.decryptSymmetric(
secretBlindIndexData.encryptedSaltCiphertext, secretBlindIndexData.encryptedSaltCiphertext,
rootEncryptionKey, rootEncryptionKey,
@@ -269,32 +275,29 @@ export const generateSecretBlindIndexHelper = async ({
const secretBlindIndex = await generateSecretBlindIndexWithSaltHelper({ const secretBlindIndex = await generateSecretBlindIndexWithSaltHelper({
secretName, secretName,
salt, salt
}); });
return secretBlindIndex; return secretBlindIndex;
} else if ( } else if (encryptionKey && secretBlindIndexData.keyEncoding === ENCODING_SCHEME_UTF8) {
encryptionKey &&
secretBlindIndexData.keyEncoding === ENCODING_SCHEME_UTF8
) {
// decrypt workspace salt // decrypt workspace salt
salt = decryptSymmetric128BitHexKeyUTF8({ salt = decryptSymmetric128BitHexKeyUTF8({
ciphertext: secretBlindIndexData.encryptedSaltCiphertext, ciphertext: secretBlindIndexData.encryptedSaltCiphertext,
iv: secretBlindIndexData.saltIV, iv: secretBlindIndexData.saltIV,
tag: secretBlindIndexData.saltTag, tag: secretBlindIndexData.saltTag,
key: encryptionKey, key: encryptionKey
}); });
const secretBlindIndex = await generateSecretBlindIndexWithSaltHelper({ const secretBlindIndex = await generateSecretBlindIndexWithSaltHelper({
secretName, secretName,
salt, salt
}); });
return secretBlindIndex; return secretBlindIndex;
} }
throw InternalServerError({ throw InternalServerError({
message: "Failed to generate secret blind index", message: "Failed to generate secret blind index"
}); });
}; };
@@ -323,38 +326,32 @@ export const createSecretHelper = async ({
secretCommentCiphertext, secretCommentCiphertext,
secretCommentIV, secretCommentIV,
secretCommentTag, secretCommentTag,
secretPath = "/", secretPath = "/"
}: CreateSecretParams) => { }: CreateSecretParams) => {
const secretBlindIndex = await generateSecretBlindIndexHelper({ const secretBlindIndex = await generateSecretBlindIndexHelper({
secretName, secretName,
workspaceId: new Types.ObjectId(workspaceId), workspaceId: new Types.ObjectId(workspaceId)
}); });
// if using service token filter towards the folderId by secretpath // if using service token filter towards the folderId by secretpath
if (authData.authPayload instanceof ServiceTokenData) { if (authData.authPayload instanceof ServiceTokenData) {
const { secretPath: serviceTkScopedSecretPath } = authData.authPayload; if (!isValidScope(authData.authPayload, environment, secretPath)) {
if (secretPath !== serviceTkScopedSecretPath) {
throw UnauthorizedRequestError({ message: "Folder Permission Denied" }); throw UnauthorizedRequestError({ message: "Folder Permission Denied" });
} }
} }
const folderId = await getFolderIdFromServiceToken( const folderId = await getFolderIdFromServiceToken(workspaceId, environment, secretPath);
workspaceId,
environment,
secretPath
);
const exists = await Secret.exists({ const exists = await Secret.exists({
secretBlindIndex, secretBlindIndex,
workspace: new Types.ObjectId(workspaceId), workspace: new Types.ObjectId(workspaceId),
folder: folderId, folder: folderId,
type, type,
...(type === SECRET_PERSONAL ? getAuthDataPayloadUserObj(authData) : {}), ...(type === SECRET_PERSONAL ? getAuthDataPayloadUserObj(authData) : {})
}); });
if (exists) if (exists)
throw BadRequestError({ throw BadRequestError({
message: "Failed to create secret that already exists", message: "Failed to create secret that already exists"
}); });
if (type === SECRET_PERSONAL) { if (type === SECRET_PERSONAL) {
@@ -365,13 +362,12 @@ export const createSecretHelper = async ({
secretBlindIndex, secretBlindIndex,
folder: folderId, folder: folderId,
workspace: new Types.ObjectId(workspaceId), workspace: new Types.ObjectId(workspaceId),
type: SECRET_SHARED, type: SECRET_SHARED
}); });
if (!exists) if (!exists)
throw BadRequestError({ throw BadRequestError({
message: message: "Failed to create personal secret override for no corresponding shared secret"
"Failed to create personal secret override for no corresponding shared secret",
}); });
} }
@@ -394,7 +390,7 @@ export const createSecretHelper = async ({
secretCommentTag, secretCommentTag,
folder: folderId, folder: folderId,
algorithm: ALGORITHM_AES_256_GCM, algorithm: ALGORITHM_AES_256_GCM,
keyEncoding: ENCODING_SCHEME_UTF8, keyEncoding: ENCODING_SCHEME_UTF8
}).save(); }).save();
const secretVersion = new SecretVersion({ const secretVersion = new SecretVersion({
@@ -414,12 +410,12 @@ export const createSecretHelper = async ({
secretValueIV, secretValueIV,
secretValueTag, secretValueTag,
algorithm: ALGORITHM_AES_256_GCM, algorithm: ALGORITHM_AES_256_GCM,
keyEncoding: ENCODING_SCHEME_UTF8, keyEncoding: ENCODING_SCHEME_UTF8
}); });
// (EE) add version for new secret // (EE) add version for new secret
await EESecretService.addSecretVersions({ await EESecretService.addSecretVersions({
secretVersions: [secretVersion], secretVersions: [secretVersion]
}); });
// (EE) create (audit) log // (EE) create (audit) log
@@ -427,7 +423,7 @@ export const createSecretHelper = async ({
name: ACTION_ADD_SECRETS, name: ACTION_ADD_SECRETS,
...getAuthDataPayloadIdObj(authData), ...getAuthDataPayloadIdObj(authData),
workspaceId, workspaceId,
secretIds: [secret._id], secretIds: [secret._id]
}); });
action && action &&
@@ -436,14 +432,14 @@ export const createSecretHelper = async ({
workspaceId, workspaceId,
actions: [action], actions: [action],
channel: authData.authChannel, channel: authData.authChannel,
ipAddress: authData.authIP, ipAddress: authData.authIP
})); }));
// (EE) take a secret snapshot // (EE) take a secret snapshot
await EESecretService.takeSecretSnapshot({ await EESecretService.takeSecretSnapshot({
workspaceId, workspaceId,
environment, environment,
folderId, folderId
}); });
const postHogClient = await TelemetryService.getPostHogClient(); const postHogClient = await TelemetryService.getPostHogClient();
@@ -452,7 +448,7 @@ export const createSecretHelper = async ({
postHogClient.capture({ postHogClient.capture({
event: "secrets added", event: "secrets added",
distinctId: await TelemetryService.getDistinctId({ distinctId: await TelemetryService.getDistinctId({
authData, authData
}), }),
properties: { properties: {
numberOfSecrets: 1, numberOfSecrets: 1,
@@ -460,8 +456,8 @@ export const createSecretHelper = async ({
workspaceId, workspaceId,
folderId, folderId,
channel: authData.authChannel, channel: authData.authChannel,
userAgent: authData.authUserAgent, userAgent: authData.authUserAgent
}, }
}); });
} }
@@ -480,21 +476,16 @@ export const getSecretsHelper = async ({
workspaceId, workspaceId,
environment, environment,
authData, authData,
secretPath = "/", secretPath = "/"
}: GetSecretsParams) => { }: GetSecretsParams) => {
let secrets: ISecret[] = []; let secrets: ISecret[] = [];
// if using service token filter towards the folderId by secretpath // if using service token filter towards the folderId by secretpath
if (authData.authPayload instanceof ServiceTokenData) { if (authData.authPayload instanceof ServiceTokenData) {
const { secretPath: serviceTkScopedSecretPath } = authData.authPayload; if (!isValidScope(authData.authPayload, environment, secretPath)) {
if (secretPath !== serviceTkScopedSecretPath) {
throw UnauthorizedRequestError({ message: "Folder Permission Denied" }); throw UnauthorizedRequestError({ message: "Folder Permission Denied" });
} }
} }
const folderId = await getFolderIdFromServiceToken( const folderId = await getFolderIdFromServiceToken(workspaceId, environment, secretPath);
workspaceId,
environment,
secretPath
);
// get personal secrets first // get personal secrets first
secrets = await Secret.find({ secrets = await Secret.find({
@@ -502,8 +493,10 @@ export const getSecretsHelper = async ({
environment, environment,
folder: folderId, folder: folderId,
type: SECRET_PERSONAL, type: SECRET_PERSONAL,
...getAuthDataPayloadUserObj(authData), ...getAuthDataPayloadUserObj(authData)
}).populate("tags").lean(); })
.populate("tags")
.lean();
// concat with shared secrets // concat with shared secrets
secrets = secrets.concat( secrets = secrets.concat(
@@ -513,9 +506,11 @@ export const getSecretsHelper = async ({
folder: folderId, folder: folderId,
type: SECRET_SHARED, type: SECRET_SHARED,
secretBlindIndex: { secretBlindIndex: {
$nin: secrets.map((secret) => secret.secretBlindIndex), $nin: secrets.map((secret) => secret.secretBlindIndex)
}, }
}).populate("tags").lean() })
.populate("tags")
.lean()
); );
// (EE) create (audit) log // (EE) create (audit) log
@@ -523,7 +518,7 @@ export const getSecretsHelper = async ({
name: ACTION_READ_SECRETS, name: ACTION_READ_SECRETS,
...getAuthDataPayloadIdObj(authData), ...getAuthDataPayloadIdObj(authData),
workspaceId, workspaceId,
secretIds: secrets.map((secret) => secret._id), secretIds: secrets.map((secret) => secret._id)
}); });
action && action &&
@@ -532,7 +527,7 @@ export const getSecretsHelper = async ({
workspaceId, workspaceId,
actions: [action], actions: [action],
channel: authData.authChannel, channel: authData.authChannel,
ipAddress: authData.authIP, ipAddress: authData.authIP
})); }));
const postHogClient = await TelemetryService.getPostHogClient(); const postHogClient = await TelemetryService.getPostHogClient();
@@ -541,7 +536,7 @@ export const getSecretsHelper = async ({
postHogClient.capture({ postHogClient.capture({
event: "secrets pulled", event: "secrets pulled",
distinctId: await TelemetryService.getDistinctId({ distinctId: await TelemetryService.getDistinctId({
authData, authData
}), }),
properties: { properties: {
numberOfSecrets: secrets.length, numberOfSecrets: secrets.length,
@@ -549,8 +544,8 @@ export const getSecretsHelper = async ({
workspaceId, workspaceId,
folderId, folderId,
channel: authData.authChannel, channel: authData.authChannel,
userAgent: authData.authUserAgent, userAgent: authData.authUserAgent
}, }
}); });
} }
@@ -573,25 +568,20 @@ export const getSecretHelper = async ({
environment, environment,
type, type,
authData, authData,
secretPath = "/", secretPath = "/"
}: GetSecretParams) => { }: GetSecretParams) => {
const secretBlindIndex = await generateSecretBlindIndexHelper({ const secretBlindIndex = await generateSecretBlindIndexHelper({
secretName, secretName,
workspaceId: new Types.ObjectId(workspaceId), workspaceId: new Types.ObjectId(workspaceId)
}); });
let secret: ISecret | null = null; let secret: ISecret | null = null;
// if using service token filter towards the folderId by secretpath // if using service token filter towards the folderId by secretpath
if (authData.authPayload instanceof ServiceTokenData) { if (authData.authPayload instanceof ServiceTokenData) {
const { secretPath: serviceTkScopedSecretPath } = authData.authPayload; if (!isValidScope(authData.authPayload, environment, secretPath)) {
if (secretPath !== serviceTkScopedSecretPath) {
throw UnauthorizedRequestError({ message: "Folder Permission Denied" }); throw UnauthorizedRequestError({ message: "Folder Permission Denied" });
} }
} }
const folderId = await getFolderIdFromServiceToken( const folderId = await getFolderIdFromServiceToken(workspaceId, environment, secretPath);
workspaceId,
environment,
secretPath
);
// try getting personal secret first (if exists) // try getting personal secret first (if exists)
secret = await Secret.findOne({ secret = await Secret.findOne({
@@ -600,7 +590,7 @@ export const getSecretHelper = async ({
environment, environment,
folder: folderId, folder: folderId,
type: type ?? SECRET_PERSONAL, type: type ?? SECRET_PERSONAL,
...(type === SECRET_PERSONAL ? getAuthDataPayloadUserObj(authData) : {}), ...(type === SECRET_PERSONAL ? getAuthDataPayloadUserObj(authData) : {})
}).lean(); }).lean();
if (!secret) { if (!secret) {
@@ -611,7 +601,7 @@ export const getSecretHelper = async ({
workspace: new Types.ObjectId(workspaceId), workspace: new Types.ObjectId(workspaceId),
environment, environment,
folder: folderId, folder: folderId,
type: SECRET_SHARED, type: SECRET_SHARED
}).lean(); }).lean();
} }
@@ -622,7 +612,7 @@ export const getSecretHelper = async ({
name: ACTION_READ_SECRETS, name: ACTION_READ_SECRETS,
...getAuthDataPayloadIdObj(authData), ...getAuthDataPayloadIdObj(authData),
workspaceId, workspaceId,
secretIds: [secret._id], secretIds: [secret._id]
}); });
action && action &&
@@ -631,7 +621,7 @@ export const getSecretHelper = async ({
workspaceId, workspaceId,
actions: [action], actions: [action],
channel: authData.authChannel, channel: authData.authChannel,
ipAddress: authData.authIP, ipAddress: authData.authIP
})); }));
const postHogClient = await TelemetryService.getPostHogClient(); const postHogClient = await TelemetryService.getPostHogClient();
@@ -640,7 +630,7 @@ export const getSecretHelper = async ({
postHogClient.capture({ postHogClient.capture({
event: "secrets pull", event: "secrets pull",
distinctId: await TelemetryService.getDistinctId({ distinctId: await TelemetryService.getDistinctId({
authData, authData
}), }),
properties: { properties: {
numberOfSecrets: 1, numberOfSecrets: 1,
@@ -648,8 +638,8 @@ export const getSecretHelper = async ({
workspaceId, workspaceId,
folderId, folderId,
channel: authData.authChannel, channel: authData.authChannel,
userAgent: authData.authUserAgent, userAgent: authData.authUserAgent
}, }
}); });
} }
@@ -679,26 +669,21 @@ export const updateSecretHelper = async ({
secretValueCiphertext, secretValueCiphertext,
secretValueIV, secretValueIV,
secretValueTag, secretValueTag,
secretPath, secretPath
}: UpdateSecretParams) => { }: UpdateSecretParams) => {
const secretBlindIndex = await generateSecretBlindIndexHelper({ const secretBlindIndex = await generateSecretBlindIndexHelper({
secretName, secretName,
workspaceId: new Types.ObjectId(workspaceId), workspaceId: new Types.ObjectId(workspaceId)
}); });
let secret: ISecret | null = null; let secret: ISecret | null = null;
// if using service token filter towards the folderId by secretpath // if using service token filter towards the folderId by secretpath
if (authData.authPayload instanceof ServiceTokenData) { if (authData.authPayload instanceof ServiceTokenData) {
const { secretPath: serviceTkScopedSecretPath } = authData.authPayload; if (!isValidScope(authData.authPayload, environment, secretPath)) {
if (secretPath !== serviceTkScopedSecretPath) {
throw UnauthorizedRequestError({ message: "Folder Permission Denied" }); throw UnauthorizedRequestError({ message: "Folder Permission Denied" });
} }
} }
const folderId = await getFolderIdFromServiceToken( const folderId = await getFolderIdFromServiceToken(workspaceId, environment, secretPath);
workspaceId,
environment,
secretPath
);
if (type === SECRET_SHARED) { if (type === SECRET_SHARED) {
// case: update shared secret // case: update shared secret
@@ -708,16 +693,16 @@ export const updateSecretHelper = async ({
workspace: new Types.ObjectId(workspaceId), workspace: new Types.ObjectId(workspaceId),
environment, environment,
folder: folderId, folder: folderId,
type, type
}, },
{ {
secretValueCiphertext, secretValueCiphertext,
secretValueIV, secretValueIV,
secretValueTag, secretValueTag,
$inc: { version: 1 }, $inc: { version: 1 }
}, },
{ {
new: true, new: true
} }
); );
} else { } else {
@@ -730,16 +715,16 @@ export const updateSecretHelper = async ({
environment, environment,
type, type,
folder: folderId, folder: folderId,
...getAuthDataPayloadUserObj(authData), ...getAuthDataPayloadUserObj(authData)
}, },
{ {
secretValueCiphertext, secretValueCiphertext,
secretValueIV, secretValueIV,
secretValueTag, secretValueTag,
$inc: { version: 1 }, $inc: { version: 1 }
}, },
{ {
new: true, new: true
} }
); );
} }
@@ -763,12 +748,12 @@ export const updateSecretHelper = async ({
secretValueIV, secretValueIV,
secretValueTag, secretValueTag,
algorithm: ALGORITHM_AES_256_GCM, algorithm: ALGORITHM_AES_256_GCM,
keyEncoding: ENCODING_SCHEME_UTF8, keyEncoding: ENCODING_SCHEME_UTF8
}); });
// (EE) add version for new secret // (EE) add version for new secret
await EESecretService.addSecretVersions({ await EESecretService.addSecretVersions({
secretVersions: [secretVersion], secretVersions: [secretVersion]
}); });
// (EE) create (audit) log // (EE) create (audit) log
@@ -776,7 +761,7 @@ export const updateSecretHelper = async ({
name: ACTION_UPDATE_SECRETS, name: ACTION_UPDATE_SECRETS,
...getAuthDataPayloadIdObj(authData), ...getAuthDataPayloadIdObj(authData),
workspaceId, workspaceId,
secretIds: [secret._id], secretIds: [secret._id]
}); });
action && action &&
@@ -785,14 +770,14 @@ export const updateSecretHelper = async ({
workspaceId, workspaceId,
actions: [action], actions: [action],
channel: authData.authChannel, channel: authData.authChannel,
ipAddress: authData.authIP, ipAddress: authData.authIP
})); }));
// (EE) take a secret snapshot // (EE) take a secret snapshot
await EESecretService.takeSecretSnapshot({ await EESecretService.takeSecretSnapshot({
workspaceId, workspaceId,
environment, environment,
folderId: secret?.folder, folderId: secret?.folder
}); });
const postHogClient = await TelemetryService.getPostHogClient(); const postHogClient = await TelemetryService.getPostHogClient();
@@ -801,7 +786,7 @@ export const updateSecretHelper = async ({
postHogClient.capture({ postHogClient.capture({
event: "secrets modified", event: "secrets modified",
distinctId: await TelemetryService.getDistinctId({ distinctId: await TelemetryService.getDistinctId({
authData, authData
}), }),
properties: { properties: {
numberOfSecrets: 1, numberOfSecrets: 1,
@@ -809,8 +794,8 @@ export const updateSecretHelper = async ({
workspaceId, workspaceId,
folderId, folderId,
channel: authData.authChannel, channel: authData.authChannel,
userAgent: authData.authUserAgent, userAgent: authData.authUserAgent
}, }
}); });
} }
@@ -833,26 +818,20 @@ export const deleteSecretHelper = async ({
environment, environment,
type, type,
authData, authData,
secretPath = "/", secretPath = "/"
}: DeleteSecretParams) => { }: DeleteSecretParams) => {
const secretBlindIndex = await generateSecretBlindIndexHelper({ const secretBlindIndex = await generateSecretBlindIndexHelper({
secretName, secretName,
workspaceId: new Types.ObjectId(workspaceId), workspaceId: new Types.ObjectId(workspaceId)
}); });
// if using service token filter towards the folderId by secretpath // if using service token filter towards the folderId by secretpath
if (authData.authPayload instanceof ServiceTokenData) { if (authData.authPayload instanceof ServiceTokenData) {
const { secretPath: serviceTkScopedSecretPath } = authData.authPayload; if (!isValidScope(authData.authPayload, environment, secretPath)) {
if (secretPath !== serviceTkScopedSecretPath) {
throw UnauthorizedRequestError({ message: "Folder Permission Denied" }); throw UnauthorizedRequestError({ message: "Folder Permission Denied" });
} }
} }
const folderId = await getFolderIdFromServiceToken( const folderId = await getFolderIdFromServiceToken(workspaceId, environment, secretPath);
workspaceId,
environment,
secretPath
);
let secrets: ISecret[] = []; let secrets: ISecret[] = [];
let secret: ISecret | null = null; let secret: ISecret | null = null;
@@ -862,7 +841,7 @@ export const deleteSecretHelper = async ({
secretBlindIndex, secretBlindIndex,
workspaceId: new Types.ObjectId(workspaceId), workspaceId: new Types.ObjectId(workspaceId),
environment, environment,
folder: folderId, folder: folderId
}).lean(); }).lean();
secret = await Secret.findOneAndDelete({ secret = await Secret.findOneAndDelete({
@@ -870,14 +849,14 @@ export const deleteSecretHelper = async ({
workspaceId: new Types.ObjectId(workspaceId), workspaceId: new Types.ObjectId(workspaceId),
environment, environment,
type, type,
folder: folderId, folder: folderId
}).lean(); }).lean();
await Secret.deleteMany({ await Secret.deleteMany({
secretBlindIndex, secretBlindIndex,
workspaceId: new Types.ObjectId(workspaceId), workspaceId: new Types.ObjectId(workspaceId),
environment, environment,
folder: folderId, folder: folderId
}); });
} else { } else {
secret = await Secret.findOneAndDelete({ secret = await Secret.findOneAndDelete({
@@ -886,7 +865,7 @@ export const deleteSecretHelper = async ({
workspaceId: new Types.ObjectId(workspaceId), workspaceId: new Types.ObjectId(workspaceId),
environment, environment,
type, type,
...getAuthDataPayloadUserObj(authData), ...getAuthDataPayloadUserObj(authData)
}).lean(); }).lean();
if (secret) { if (secret) {
@@ -897,7 +876,7 @@ export const deleteSecretHelper = async ({
if (!secret) throw SecretNotFoundError(); if (!secret) throw SecretNotFoundError();
await EESecretService.markDeletedSecretVersions({ await EESecretService.markDeletedSecretVersions({
secretIds: secrets.map((secret) => secret._id), secretIds: secrets.map((secret) => secret._id)
}); });
// (EE) create (audit) log // (EE) create (audit) log
@@ -905,22 +884,23 @@ export const deleteSecretHelper = async ({
name: ACTION_DELETE_SECRETS, name: ACTION_DELETE_SECRETS,
...getAuthDataPayloadIdObj(authData), ...getAuthDataPayloadIdObj(authData),
workspaceId, workspaceId,
secretIds: secrets.map((secret) => secret._id), secretIds: secrets.map((secret) => secret._id)
}); });
action && (await EELogService.createLog({ action &&
...getAuthDataPayloadIdObj(authData), (await EELogService.createLog({
workspaceId, ...getAuthDataPayloadIdObj(authData),
actions: [action], workspaceId,
channel: authData.authChannel, actions: [action],
ipAddress: authData.authIP, channel: authData.authChannel,
})); ipAddress: authData.authIP
}));
// (EE) take a secret snapshot // (EE) take a secret snapshot
await EESecretService.takeSecretSnapshot({ await EESecretService.takeSecretSnapshot({
workspaceId, workspaceId,
environment, environment,
folderId: secret?.folder, folderId: secret?.folder
}); });
const postHogClient = await TelemetryService.getPostHogClient(); const postHogClient = await TelemetryService.getPostHogClient();
@@ -929,7 +909,7 @@ export const deleteSecretHelper = async ({
postHogClient.capture({ postHogClient.capture({
event: "secrets deleted", event: "secrets deleted",
distinctId: await TelemetryService.getDistinctId({ distinctId: await TelemetryService.getDistinctId({
authData, authData
}), }),
properties: { properties: {
numberOfSecrets: secrets.length, numberOfSecrets: secrets.length,
@@ -937,13 +917,13 @@ export const deleteSecretHelper = async ({
workspaceId, workspaceId,
folderId, folderId,
channel: authData.authChannel, channel: authData.authChannel,
userAgent: authData.authUserAgent, userAgent: authData.authUserAgent
}, }
}); });
} }
return ({ return {
secrets, secrets,
secret, secret
}); };
}; };
+14
View File
@@ -5,6 +5,10 @@ import {
Secret, Secret,
Workspace, Workspace,
} from "../models"; } from "../models";
import {
IPType,
TrustedIP
} from "../ee/models";
import { createBot } from "../helpers/bot"; import { createBot } from "../helpers/bot";
import { EELicenseService } from "../ee/services"; import { EELicenseService } from "../ee/services";
import { SecretService } from "../services"; import { SecretService } from "../services";
@@ -40,6 +44,16 @@ export const createWorkspace = async ({
await SecretService.createSecretBlindIndexData({ await SecretService.createSecretBlindIndexData({
workspaceId: workspace._id, workspaceId: workspace._id,
}); });
// initialize default trusted ip of 0.0.0.0/0
await new TrustedIP({
workspace: workspace._id,
ipAddress: "0.0.0.0",
type: IPType.IPV4,
prefix: 0,
isActive: true,
comment: ""
}).save()
await EELicenseService.refreshPlan(organizationId); await EELicenseService.refreshPlan(organizationId);
+46 -14
View File
@@ -5,11 +5,12 @@ import express from "express";
require("express-async-errors"); require("express-async-errors");
import helmet from "helmet"; import helmet from "helmet";
import cors from "cors"; import cors from "cors";
import { DatabaseService } from "./services"; import { DatabaseService, GithubSecretScanningService } from "./services";
import { EELicenseService } from "./ee/services"; import { EELicenseService } from "./ee/services";
import { setUpHealthEndpoint } from "./services/health"; import { setUpHealthEndpoint } from "./services/health";
import cookieParser from "cookie-parser"; import cookieParser from "cookie-parser";
import swaggerUi = require("swagger-ui-express"); import swaggerUi = require("swagger-ui-express");
import { Probot, createNodeMiddleware } from "probot";
// eslint-disable-next-line @typescript-eslint/no-var-requires // eslint-disable-next-line @typescript-eslint/no-var-requires
const swaggerFile = require("../spec.json"); const swaggerFile = require("../spec.json");
// eslint-disable-next-line @typescript-eslint/no-var-requires // eslint-disable-next-line @typescript-eslint/no-var-requires
@@ -18,8 +19,10 @@ import {
action as eeActionRouter, action as eeActionRouter,
cloudProducts as eeCloudProductsRouter, cloudProducts as eeCloudProductsRouter,
organizations as eeOrganizationsRouter, organizations as eeOrganizationsRouter,
sso as eeSSORouter,
secret as eeSecretRouter, secret as eeSecretRouter,
secretSnapshot as eeSecretSnapshotRouter, secretSnapshot as eeSecretSnapshotRouter,
users as eeUsersRouter,
workspace as eeWorkspaceRouter, workspace as eeWorkspaceRouter,
} from "./ee/routes/v1"; } from "./ee/routes/v1";
import { import {
@@ -33,41 +36,46 @@ import {
membership as v1MembershipRouter, membership as v1MembershipRouter,
organization as v1OrganizationRouter, organization as v1OrganizationRouter,
password as v1PasswordRouter, password as v1PasswordRouter,
secretImport as v1SecretImportRouter,
secret as v1SecretRouter, secret as v1SecretRouter,
secretScanning as v1SecretScanningRouter,
secretsFolder as v1SecretsFolder, secretsFolder as v1SecretsFolder,
serviceToken as v1ServiceTokenRouter, serviceToken as v1ServiceTokenRouter,
signup as v1SignupRouter, signup as v1SignupRouter,
userAction as v1UserActionRouter, userAction as v1UserActionRouter,
user as v1UserRouter, user as v1UserRouter,
workspace as v1WorkspaceRouter, webhooks as v1WebhooksRouter,
workspace as v1WorkspaceRouter
} from "./routes/v1"; } from "./routes/v1";
import { import {
signup as v2SignupRouter,
auth as v2AuthRouter, auth as v2AuthRouter,
users as v2UsersRouter, environment as v2EnvironmentRouter,
organizations as v2OrganizationsRouter, organizations as v2OrganizationsRouter,
workspace as v2WorkspaceRouter,
secret as v2SecretRouter, // begin to phase out secret as v2SecretRouter, // begin to phase out
secrets as v2SecretsRouter, secrets as v2SecretsRouter,
serviceTokenData as v2ServiceTokenDataRouter,
serviceAccounts as v2ServiceAccountsRouter, serviceAccounts as v2ServiceAccountsRouter,
environment as v2EnvironmentRouter, serviceTokenData as v2ServiceTokenDataRouter,
signup as v2SignupRouter,
tags as v2TagsRouter, tags as v2TagsRouter,
users as v2UsersRouter,
workspace as v2WorkspaceRouter,
} from "./routes/v2"; } from "./routes/v2";
import { import {
auth as v3AuthRouter, auth as v3AuthRouter,
secrets as v3SecretsRouter, secrets as v3SecretsRouter,
signup as v3SignupRouter, signup as v3SignupRouter,
workspaces as v3WorkspacesRouter, workspaces as v3WorkspacesRouter
} from "./routes/v3"; } from "./routes/v3";
import { healthCheck } from "./routes/status"; import { healthCheck } from "./routes/status";
import { getLogger } from "./utils/logger"; import { getLogger } from "./utils/logger";
import { RouteNotFoundError } from "./utils/errors"; import { RouteNotFoundError } from "./utils/errors";
import { requestErrorHandler } from "./middleware/requestErrorHandler"; import { requestErrorHandler } from "./middleware/requestErrorHandler";
import { getNodeEnv, getPort, getSiteURL } from "./config"; import { getNodeEnv, getPort, getSecretScanningGitAppId, getSecretScanningPrivateKey, getSecretScanningWebhookProxy, getSecretScanningWebhookSecret, getSiteURL } from "./config";
import { setup } from "./utils/setup"; import { setup } from "./utils/setup";
const SmeeClient = require('smee-client') // eslint-disable-line
const main = async () => { const main = async () => {
await setup(); await setup();
await EELicenseService.initGlobalFeatureSet(); await EELicenseService.initGlobalFeatureSet();
@@ -75,14 +83,35 @@ const main = async () => {
const app = express(); const app = express();
app.enable("trust proxy"); app.enable("trust proxy");
app.use(express.json()); app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.use(cookieParser()); app.use(cookieParser());
app.use( app.use(
cors({ cors({
credentials: true, credentials: true,
origin: await getSiteURL(), origin: await getSiteURL()
}) })
); );
if (await getSecretScanningGitAppId() && await getSecretScanningWebhookSecret() && await getSecretScanningPrivateKey()) {
const probot = new Probot({
appId: await getSecretScanningGitAppId(),
privateKey: await getSecretScanningPrivateKey(),
secret: await getSecretScanningWebhookSecret(),
});
if ((await getNodeEnv()) != "production") {
const smee = new SmeeClient({
source: await getSecretScanningWebhookProxy(),
target: "http://backend:4000/ss-webhook",
logger: console
})
smee.start()
}
app.use(createNodeMiddleware(GithubSecretScanningService, { probot, webhooksPath: "/ss-webhook" })); // secret scanning webhook
}
if ((await getNodeEnv()) === "production") { if ((await getNodeEnv()) === "production") {
// enable app-wide rate-limiting + helmet security // enable app-wide rate-limiting + helmet security
// in production // in production
@@ -101,9 +130,11 @@ const main = async () => {
// (EE) routes // (EE) routes
app.use("/api/v1/secret", eeSecretRouter); app.use("/api/v1/secret", eeSecretRouter);
app.use("/api/v1/secret-snapshot", eeSecretSnapshotRouter); app.use("/api/v1/secret-snapshot", eeSecretSnapshotRouter);
app.use("/api/v1/users", eeUsersRouter);
app.use("/api/v1/workspace", eeWorkspaceRouter); app.use("/api/v1/workspace", eeWorkspaceRouter);
app.use("/api/v1/action", eeActionRouter); app.use("/api/v1/action", eeActionRouter);
app.use("/api/v1/organizations", eeOrganizationsRouter); app.use("/api/v1/organizations", eeOrganizationsRouter);
app.use("/api/v1/sso", eeSSORouter);
app.use("/api/v1/cloud-products", eeCloudProductsRouter); app.use("/api/v1/cloud-products", eeCloudProductsRouter);
// v1 routes (default) // v1 routes (default)
@@ -124,6 +155,9 @@ const main = async () => {
app.use("/api/v1/integration", v1IntegrationRouter); app.use("/api/v1/integration", v1IntegrationRouter);
app.use("/api/v1/integration-auth", v1IntegrationAuthRouter); app.use("/api/v1/integration-auth", v1IntegrationAuthRouter);
app.use("/api/v1/folders", v1SecretsFolder); app.use("/api/v1/folders", v1SecretsFolder);
app.use("/api/v1/secret-scanning", v1SecretScanningRouter);
app.use("/api/v1/webhooks", v1WebhooksRouter);
app.use("/api/v1/secret-imports", v1SecretImportRouter);
// v2 routes (improvements) // v2 routes (improvements)
app.use("/api/v2/signup", v2SignupRouter); app.use("/api/v2/signup", v2SignupRouter);
@@ -155,7 +189,7 @@ const main = async () => {
if (res.headersSent) return next(); if (res.headersSent) return next();
next( next(
RouteNotFoundError({ RouteNotFoundError({
message: `The requested source '(${req.method})${req.url}' was not found`, message: `The requested source '(${req.method})${req.url}' was not found`
}) })
); );
}); });
@@ -163,9 +197,7 @@ const main = async () => {
app.use(requestErrorHandler); app.use(requestErrorHandler);
const server = app.listen(await getPort(), async () => { const server = app.listen(await getPort(), async () => {
(await getLogger("backend-main")).info( (await getLogger("backend-main")).info(`Server started listening at port ${await getPort()}`);
`Server started listening at port ${await getPort()}`
);
}); });
// await createTestUserForDevelopment(); // await createTestUserForDevelopment();
+258 -22
View File
@@ -1,16 +1,21 @@
import { Octokit } from "@octokit/rest";
import { IIntegrationAuth } from "../models";
import { standardRequest } from "../config/request";
import { import {
INTEGRATION_AWS_PARAMETER_STORE, INTEGRATION_AWS_PARAMETER_STORE,
INTEGRATION_AWS_SECRET_MANAGER, INTEGRATION_AWS_SECRET_MANAGER,
INTEGRATION_AZURE_KEY_VAULT, INTEGRATION_AZURE_KEY_VAULT,
INTEGRATION_BITBUCKET,
INTEGRATION_BITBUCKET_API_URL,
INTEGRATION_CHECKLY, INTEGRATION_CHECKLY,
INTEGRATION_CHECKLY_API_URL, INTEGRATION_CHECKLY_API_URL,
INTEGRATION_CIRCLECI, INTEGRATION_CIRCLECI,
INTEGRATION_CIRCLECI_API_URL, INTEGRATION_CIRCLECI_API_URL,
INTEGRATION_CLOUDFLARE_PAGES, INTEGRATION_CLOUDFLARE_PAGES,
INTEGRATION_CLOUDFLARE_PAGES_API_URL, INTEGRATION_CLOUDFLARE_PAGES_API_URL,
INTEGRATION_CLOUD_66,
INTEGRATION_CLOUD_66_API_URL,
INTEGRATION_CODEFRESH,
INTEGRATION_CODEFRESH_API_URL,
INTEGRATION_DIGITAL_OCEAN_API_URL,
INTEGRATION_DIGITAL_OCEAN_APP_PLATFORM,
INTEGRATION_FLYIO, INTEGRATION_FLYIO,
INTEGRATION_FLYIO_API_URL, INTEGRATION_FLYIO_API_URL,
INTEGRATION_GITHUB, INTEGRATION_GITHUB,
@@ -33,8 +38,11 @@ import {
INTEGRATION_TRAVISCI, INTEGRATION_TRAVISCI,
INTEGRATION_TRAVISCI_API_URL, INTEGRATION_TRAVISCI_API_URL,
INTEGRATION_VERCEL, INTEGRATION_VERCEL,
INTEGRATION_VERCEL_API_URL, INTEGRATION_VERCEL_API_URL
} from "../variables"; } from "../variables";
import { IIntegrationAuth } from "../models";
import { Octokit } from "@octokit/rest";
import { standardRequest } from "../config/request";
interface App { interface App {
name: string; name: string;
@@ -56,11 +64,13 @@ const getApps = async ({
accessToken, accessToken,
accessId, accessId,
teamId, teamId,
workspaceSlug,
}: { }: {
integrationAuth: IIntegrationAuth; integrationAuth: IIntegrationAuth;
accessToken: string; accessToken: string;
accessId?: string; accessId?: string;
teamId?: string; teamId?: string;
workspaceSlug?: string;
}) => { }) => {
let apps: App[] = []; let apps: App[] = [];
switch (integrationAuth.integration) { switch (integrationAuth.integration) {
@@ -153,6 +163,27 @@ const getApps = async ({
accountId: accessId accountId: accessId
}) })
break; break;
case INTEGRATION_BITBUCKET:
apps = await getAppsBitBucket({
accessToken,
workspaceSlug
});
break;
case INTEGRATION_CODEFRESH:
apps = await getAppsCodefresh({
accessToken,
});
break;
case INTEGRATION_DIGITAL_OCEAN_APP_PLATFORM:
apps = await getAppsDigitalOceanAppPlatform({
accessToken
});
break;
case INTEGRATION_CLOUD_66:
apps = await getAppsCloud66({
accessToken,
});
break;
} }
return apps; return apps;
@@ -204,10 +235,10 @@ const getAppsVercel = async ({
}, },
...(integrationAuth?.teamId ...(integrationAuth?.teamId
? { ? {
params: { params: {
teamId: integrationAuth.teamId, teamId: integrationAuth.teamId,
}, },
} }
: {}), : {}),
}) })
).data; ).data;
@@ -740,15 +771,76 @@ const getAppsCheckly = async ({ accessToken }: { accessToken: string }) => {
* @returns {Object[]} apps - Cloudflare Pages projects * @returns {Object[]} apps - Cloudflare Pages projects
* @returns {String} apps.name - name of Cloudflare Pages project * @returns {String} apps.name - name of Cloudflare Pages project
*/ */
const getAppsCloudflarePages = async ({ const getAppsCloudflarePages = async ({
accessToken, accessToken,
accountId accountId
}: { }: {
accessToken: string; accessToken: string;
accountId?: string; accountId?: string;
}) => { }) => {
const { data } = await standardRequest.get( const { data } = await standardRequest.get(
`${INTEGRATION_CLOUDFLARE_PAGES_API_URL}/client/v4/accounts/${accountId}/pages/projects`, `${INTEGRATION_CLOUDFLARE_PAGES_API_URL}/client/v4/accounts/${accountId}/pages/projects`,
{
headers: {
Authorization: `Bearer ${accessToken}`,
"Accept": "application/json",
},
}
);
const apps = data.result.map((a: any) => {
return {
name: a.name,
appId: a.id,
};
});
return apps;
}
/**
* Return list of repositories for the BitBucket integration based on provided BitBucket workspace
* @param {Object} obj
* @param {String} obj.accessToken - access token for BitBucket API
* @param {String} obj.workspaceSlug - Workspace identifier for fetching BitBucket repositories
* @returns {Object[]} apps - BitBucket repositories
* @returns {String} apps.name - name of BitBucket repository
*/
const getAppsBitBucket = async ({
accessToken,
workspaceSlug,
}: {
accessToken: string;
workspaceSlug?: string;
}) => {
interface RepositoriesResponse {
size: number;
page: number;
pageLen: number;
next: string;
previous: string;
values: Array<Repository>;
}
interface Repository {
type: string;
uuid: string;
name: string;
is_private: boolean;
created_on: string;
updated_on: string;
}
if (!workspaceSlug) {
return []
}
const repositories: Repository[] = [];
let hasNextPage = true;
let repositoriesUrl = `${INTEGRATION_BITBUCKET_API_URL}/2.0/repositories/${workspaceSlug}`
while (hasNextPage) {
const { data }: { data: RepositoriesResponse } = await standardRequest.get(
repositoriesUrl,
{ {
headers: { headers: {
Authorization: `Bearer ${accessToken}`, Authorization: `Bearer ${accessToken}`,
@@ -757,13 +849,157 @@ const getAppsCloudflarePages = async ({
} }
); );
const apps = data.result.map((a: any) => { if (data?.values.length > 0) {
return { data.values.forEach((repository) => {
name: a.name, repositories.push(repository)
appId: a.id, })
}; }
});
return apps; if (data.next) {
repositoriesUrl = data.next
} else {
hasNextPage = false
}
}
const apps = repositories.map((repository) => {
return {
name: repository.name,
appId: repository.uuid,
};
});
return apps;
} }
/**
* Return list of projects for Supabase integration
* @param {Object} obj
* @param {String} obj.accessToken - access token for Supabase API
* @returns {Object[]} apps - names of Supabase apps
* @returns {String} apps.name - name of Supabase app
*/
const getAppsCodefresh = async ({
accessToken,
}: {
accessToken: string;
}) => {
const res = (
await standardRequest.get(`${INTEGRATION_CODEFRESH_API_URL}/projects`, {
headers: {
Authorization: `Bearer ${accessToken}`,
"Accept-Encoding": "application/json",
},
})
).data;
const apps = res.projects.map((a: any) => ({
name: a.projectName,
appId: a.id,
}));
return apps;
};
/**
* Return list of applications for DigitalOcean App Platform integration
* @param {Object} obj
* @param {String} obj.accessToken - personal access token for DigitalOcean
* @returns {Object[]} apps - names of DigitalOcean apps
* @returns {String} apps.name - name of DigitalOcean app
* @returns {String} apps.appId - id of DigitalOcean app
*/
const getAppsDigitalOceanAppPlatform = async ({ accessToken }: { accessToken: string }) => {
interface DigitalOceanApp {
id: string;
owner_uuid: string;
spec: Spec;
}
interface Spec {
name: string;
region: string;
envs: Env[];
}
interface Env {
key: string;
value: string;
scope: string;
}
const res = (
await standardRequest.get(`${INTEGRATION_DIGITAL_OCEAN_API_URL}/v2/apps`, {
headers: {
Authorization: `Bearer ${accessToken}`,
"Accept-Encoding": "application/json"
}
})
).data;
return (res.apps ?? []).map((a: DigitalOceanApp) => ({
name: a.spec.name,
appId: a.id
}));
}
/**
* Return list of applications for Cloud66 integration
* @param {Object} obj
* @param {String} obj.accessToken - personal access token for Cloud66 API
* @returns {Object[]} apps - Cloud66 apps
* @returns {String} apps.name - name of Cloud66 app
* @returns {String} apps.appId - uid of Cloud66 app
*/
const getAppsCloud66 = async ({ accessToken }: { accessToken: string }) => {
interface Cloud66Apps {
uid: string;
name: string;
account_id: number;
git: string;
git_branch: string;
environment: string;
cloud: string;
fqdn: string;
language: string;
framework: string;
status: number;
health: number;
last_activity: string;
last_activity_iso: string;
maintenance_mode: boolean;
has_loadbalancer: boolean;
created_at: string;
updated_at: string;
deploy_directory: string;
cloud_status: string;
backend: string;
version: string;
revision: string;
is_busy: boolean;
account_name: string;
is_cluster: boolean;
is_inside_cluster: boolean;
cluster_name: any;
application_address: string;
configstore_namespace: string;
}
const stacks = (
await standardRequest.get(`${INTEGRATION_CLOUD_66_API_URL}/3/stacks`, {
headers: {
Authorization: `Bearer ${accessToken}`,
"Accept-Encoding": "application/json"
}
})
).data.response as Cloud66Apps[]
const apps = stacks.map((app) => ({
name: app.name,
appId: app.uid
}));
return apps;
};
export { getApps }; export { getApps };
+58
View File
@@ -2,6 +2,8 @@ import { standardRequest } from "../config/request";
import { import {
INTEGRATION_AZURE_KEY_VAULT, INTEGRATION_AZURE_KEY_VAULT,
INTEGRATION_AZURE_TOKEN_URL, INTEGRATION_AZURE_TOKEN_URL,
INTEGRATION_BITBUCKET,
INTEGRATION_BITBUCKET_TOKEN_URL,
INTEGRATION_GITHUB, INTEGRATION_GITHUB,
INTEGRATION_GITHUB_TOKEN_URL, INTEGRATION_GITHUB_TOKEN_URL,
INTEGRATION_GITLAB, INTEGRATION_GITLAB,
@@ -15,11 +17,13 @@ import {
} from "../variables"; } from "../variables";
import { import {
getClientIdAzure, getClientIdAzure,
getClientIdBitBucket,
getClientIdGitHub, getClientIdGitHub,
getClientIdGitLab, getClientIdGitLab,
getClientIdNetlify, getClientIdNetlify,
getClientIdVercel, getClientIdVercel,
getClientSecretAzure, getClientSecretAzure,
getClientSecretBitBucket,
getClientSecretGitHub, getClientSecretGitHub,
getClientSecretGitLab, getClientSecretGitLab,
getClientSecretHeroku, getClientSecretHeroku,
@@ -78,6 +82,15 @@ interface ExchangeCodeGitlabResponse {
created_at: number; created_at: number;
} }
interface ExchangeCodeBitBucketResponse {
access_token: string;
token_type: string;
expires_in: number;
refresh_token: string;
scopes: string;
state: string;
}
/** /**
* Return [accessToken], [accessExpiresAt], and [refreshToken] for OAuth2 * Return [accessToken], [accessExpiresAt], and [refreshToken] for OAuth2
* code-token exchange for integration named [integration] * code-token exchange for integration named [integration]
@@ -129,6 +142,12 @@ const exchangeCode = async ({
obj = await exchangeCodeGitlab({ obj = await exchangeCodeGitlab({
code, code,
}); });
break;
case INTEGRATION_BITBUCKET:
obj = await exchangeCodeBitBucket({
code,
});
break;
} }
return obj; return obj;
@@ -347,4 +366,43 @@ const exchangeCodeGitlab = async ({ code }: { code: string }) => {
}; };
}; };
/**
* Return [accessToken], [accessExpiresAt], and [refreshToken] for BitBucket
* code-token exchange
* @param {Object} obj1
* @param {Object} obj1.code - code for code-token exchange
* @returns {Object} obj2
* @returns {String} obj2.accessToken - access token for BitBucket API
* @returns {String} obj2.refreshToken - refresh token for BitBucket API
* @returns {Date} obj2.accessExpiresAt - date of expiration for access token
*/
const exchangeCodeBitBucket = async ({ code }: { code: string }) => {
const accessExpiresAt = new Date();
const res: ExchangeCodeBitBucketResponse = (
await standardRequest.post(
INTEGRATION_BITBUCKET_TOKEN_URL,
new URLSearchParams({
grant_type: "authorization_code",
code: code,
client_id: await getClientIdBitBucket(),
client_secret: await getClientSecretBitBucket(),
redirect_uri: `${await getSiteURL()}/integrations/bitbucket/oauth2/callback`,
} as any),
{
headers: {
"Accept-Encoding": "application/json",
},
}
)
).data;
accessExpiresAt.setSeconds(accessExpiresAt.getSeconds() + res.expires_in);
return {
accessToken: res.access_token,
refreshToken: res.refresh_token,
accessExpiresAt,
};
};
export { exchangeCode }; export { exchangeCode };
+60
View File
@@ -2,6 +2,8 @@ import { standardRequest } from "../config/request";
import { IIntegrationAuth } from "../models"; import { IIntegrationAuth } from "../models";
import { import {
INTEGRATION_AZURE_KEY_VAULT, INTEGRATION_AZURE_KEY_VAULT,
INTEGRATION_BITBUCKET,
INTEGRATION_BITBUCKET_TOKEN_URL,
INTEGRATION_GITLAB, INTEGRATION_GITLAB,
INTEGRATION_HEROKU, INTEGRATION_HEROKU,
} from "../variables"; } from "../variables";
@@ -13,8 +15,10 @@ import {
import { IntegrationService } from "../services"; import { IntegrationService } from "../services";
import { import {
getClientIdAzure, getClientIdAzure,
getClientIdBitBucket,
getClientIdGitLab, getClientIdGitLab,
getClientSecretAzure, getClientSecretAzure,
getClientSecretBitBucket,
getClientSecretGitLab, getClientSecretGitLab,
getClientSecretHeroku, getClientSecretHeroku,
getSiteURL, getSiteURL,
@@ -46,6 +50,15 @@ interface RefreshTokenGitLabResponse {
created_at: number; created_at: number;
} }
interface RefreshTokenBitBucketResponse {
access_token: string;
token_type: string;
expires_in: number;
refresh_token: string;
scopes: string;
state: string;
}
/** /**
* Return new access token by exchanging refresh token [refreshToken] for integration * Return new access token by exchanging refresh token [refreshToken] for integration
* named [integration] * named [integration]
@@ -83,6 +96,11 @@ const exchangeRefresh = async ({
refreshToken, refreshToken,
}); });
break; break;
case INTEGRATION_BITBUCKET:
tokenDetails = await exchangeRefreshBitBucket({
refreshToken,
});
break;
default: default:
throw new Error("Failed to exchange token for incompatible integration"); throw new Error("Failed to exchange token for incompatible integration");
} }
@@ -218,4 +236,46 @@ const exchangeRefreshGitLab = async ({
}; };
}; };
/**
* Return new access token by exchanging refresh token [refreshToken] for the
* BitBucket integration
* @param {Object} obj
* @param {String} obj.refreshToken - refresh token to use to get new access token for BitBucket
* @returns
*/
const exchangeRefreshBitBucket = async ({
refreshToken,
}: {
refreshToken: string;
}) => {
const accessExpiresAt = new Date();
const {
data,
}: {
data: RefreshTokenBitBucketResponse;
} = await standardRequest.post(
INTEGRATION_BITBUCKET_TOKEN_URL,
new URLSearchParams({
grant_type: "refresh_token",
refresh_token: refreshToken,
client_id: await getClientIdBitBucket(),
client_secret: await getClientSecretBitBucket(),
redirect_uri: `${await getSiteURL()}/integrations/bitbucket/oauth2/callback`,
} as any),
{
headers: {
"Accept-Encoding": "application/json",
},
}
);
accessExpiresAt.setSeconds(accessExpiresAt.getSeconds() + data.expires_in);
return {
accessToken: data.access_token,
refreshToken: data.refresh_token,
accessExpiresAt,
};
};
export { exchangeRefresh }; export { exchangeRefresh };
+1 -2
View File
@@ -18,7 +18,6 @@ const revokeAccess = async ({
integrationAuth: IIntegrationAuth; integrationAuth: IIntegrationAuth;
accessToken: string; accessToken: string;
}) => { }) => {
let deletedIntegrationAuth;
// add any integration-specific revocation logic // add any integration-specific revocation logic
switch (integrationAuth.integration) { switch (integrationAuth.integration) {
case INTEGRATION_HEROKU: case INTEGRATION_HEROKU:
@@ -33,7 +32,7 @@ const revokeAccess = async ({
break; break;
} }
deletedIntegrationAuth = await IntegrationAuth.findOneAndDelete({ const deletedIntegrationAuth = await IntegrationAuth.findOneAndDelete({
_id: integrationAuth._id, _id: integrationAuth._id,
}); });
File diff suppressed because it is too large Load Diff
+9 -4
View File
@@ -1,4 +1,5 @@
import jwt from "jsonwebtoken"; import jwt from "jsonwebtoken";
import { Types } from "mongoose";
import { NextFunction, Request, Response } from "express"; import { NextFunction, Request, Response } from "express";
import { import {
getAuthAPIKeyPayload, getAuthAPIKeyPayload,
@@ -51,6 +52,10 @@ const requireAuth = ({
}); });
let authPayload: IUser | IServiceAccount | IServiceTokenData; let authPayload: IUser | IServiceAccount | IServiceTokenData;
let authUserPayload: {
user: IUser;
tokenVersionId: Types.ObjectId;
};
switch (authMode) { switch (authMode) {
case AUTH_MODE_SERVICE_ACCOUNT: case AUTH_MODE_SERVICE_ACCOUNT:
authPayload = await getAuthSAAKPayload({ authPayload = await getAuthSAAKPayload({
@@ -71,12 +76,12 @@ const requireAuth = ({
req.user = authPayload; req.user = authPayload;
break; break;
default: default:
const { user, tokenVersionId } = await getAuthUserPayload({ authUserPayload = await getAuthUserPayload({
authTokenValue, authTokenValue,
}); });
authPayload = user; authPayload = authUserPayload.user;
req.user = user; req.user = authUserPayload.user;
req.tokenVersionId = tokenVersionId; req.tokenVersionId = authUserPayload.tokenVersionId;
break; break;
} }
@@ -18,6 +18,7 @@ const requireWorkspaceAuth = ({
requiredPermissions = [], requiredPermissions = [],
requireBlindIndicesEnabled = false, requireBlindIndicesEnabled = false,
requireE2EEOff = false, requireE2EEOff = false,
checkIPAllowlist = false
}: { }: {
acceptedRoles: Array<"admin" | "member">; acceptedRoles: Array<"admin" | "member">;
locationWorkspaceId: req; locationWorkspaceId: req;
@@ -25,6 +26,7 @@ const requireWorkspaceAuth = ({
requiredPermissions?: string[]; requiredPermissions?: string[];
requireBlindIndicesEnabled?: boolean; requireBlindIndicesEnabled?: boolean;
requireE2EEOff?: boolean; requireE2EEOff?: boolean;
checkIPAllowlist?: boolean;
}) => { }) => {
return async (req: Request, res: Response, next: NextFunction) => { return async (req: Request, res: Response, next: NextFunction) => {
const workspaceId = req[locationWorkspaceId]?.workspaceId; const workspaceId = req[locationWorkspaceId]?.workspaceId;
@@ -39,6 +41,7 @@ const requireWorkspaceAuth = ({
requiredPermissions, requiredPermissions,
requireBlindIndicesEnabled, requireBlindIndicesEnabled,
requireE2EEOff, requireE2EEOff,
checkIPAllowlist
}); });
if (membership) { if (membership) {
+98
View File
@@ -0,0 +1,98 @@
import { Schema, Types, model } from "mongoose";
import {
ALGORITHM_AES_256_GCM,
ENCODING_SCHEME_BASE64,
ENCODING_SCHEME_UTF8,
} from "../variables";
export interface IBotOrg {
_id: Types.ObjectId;
name: string;
organization: Types.ObjectId;
publicKey: string;
encryptedSymmetricKey: string;
symmetricKeyIV: string;
symmetricKeyTag: string;
symmetricKeyAlgorithm: "aes-256-gcm";
symmetricKeyKeyEncoding: "base64" | "utf8";
encryptedPrivateKey: string;
privateKeyIV: string;
privateKeyTag: string;
privateKeyAlgorithm: "aes-256-gcm";
privateKeyKeyEncoding: "base64" | "utf8";
}
const botOrgSchema = new Schema<IBotOrg>(
{
name: {
type: String,
required: true,
},
organization: {
type: Schema.Types.ObjectId,
ref: "Organization",
required: true,
},
publicKey: {
type: String,
required: true,
},
encryptedSymmetricKey: {
type: String,
required: true
},
symmetricKeyIV: {
type: String,
required: true
},
symmetricKeyTag: {
type: String,
required: true
},
symmetricKeyAlgorithm: {
type: String,
enum: [ALGORITHM_AES_256_GCM],
required: true
},
symmetricKeyKeyEncoding: {
type: String,
enum: [
ENCODING_SCHEME_UTF8,
ENCODING_SCHEME_BASE64,
],
required: true
},
encryptedPrivateKey: {
type: String,
required: true
},
privateKeyIV: {
type: String,
required: true
},
privateKeyTag: {
type: String,
required: true
},
privateKeyAlgorithm: {
type: String,
enum: [ALGORITHM_AES_256_GCM],
required: true
},
privateKeyKeyEncoding: {
type: String,
enum: [
ENCODING_SCHEME_UTF8,
ENCODING_SCHEME_BASE64,
],
required: true
},
},
{
timestamps: true,
}
);
const BotOrg = model<IBotOrg>("BotOrg", botOrgSchema);
export default BotOrg;
@@ -0,0 +1,34 @@
import { Schema, Types, model } from "mongoose";
type GitAppInstallationSession = {
id: string;
sessionId: string;
organization: Types.ObjectId;
user: Types.ObjectId;
}
const gitAppInstallationSession = new Schema<GitAppInstallationSession>({
id: {
required: true,
type: String,
},
sessionId: {
type: String,
required: true,
unique: true
},
organization: {
type: Schema.Types.ObjectId,
required: true,
unique: true
},
user: {
type: Schema.Types.ObjectId,
ref: "User"
}
});
const GitAppInstallationSession = model<GitAppInstallationSession>("git_app_installation_session", gitAppInstallationSession);
export default GitAppInstallationSession;
@@ -0,0 +1,31 @@
import { Schema, model } from "mongoose";
type Installation = {
installationId: string
organizationId: string
user: Schema.Types.ObjectId
};
const gitAppOrganizationInstallation = new Schema<Installation>({
installationId: {
type: String,
required: true,
unique: true
},
organizationId: {
type: String,
required: true,
unique: true
},
user: {
type: Schema.Types.ObjectId,
ref: "User",
required: true,
}
});
const GitAppOrganizationInstallation = model<Installation>("git_app_organization_installation", gitAppOrganizationInstallation);
export default GitAppOrganizationInstallation;
+152
View File
@@ -0,0 +1,152 @@
import { Schema, model } from "mongoose";
export const STATUS_RESOLVED_FALSE_POSITIVE = "RESOLVED_FALSE_POSITIVE";
export const STATUS_RESOLVED_REVOKED = "RESOLVED_REVOKED";
export const STATUS_RESOLVED_NOT_REVOKED = "RESOLVED_NOT_REVOKED";
export const STATUS_UNRESOLVED = "UNRESOLVED";
export type GitRisks = {
id: string;
description: string;
startLine: string;
endLine: string;
startColumn: string;
endColumn: string;
match: string;
secret: string;
file: string;
symlinkFile: string;
commit: string;
entropy: string;
author: string;
email: string;
date: string;
message: string;
tags: string[];
ruleID: string;
fingerprint: string;
fingerPrintWithoutCommitId: string
isFalsePositive: boolean; // New field for marking risks as false positives
isResolved: boolean; // New field for marking risks as resolved
riskOwner: string | null; // New field for setting a risk owner (nullable string)
installationId: string,
repositoryId: string,
repositoryLink: string
repositoryFullName: string
status: string
pusher: {
name: string,
email: string
},
organization: Schema.Types.ObjectId,
}
const gitRisks = new Schema<GitRisks>({
id: {
type: String,
},
description: {
type: String,
},
startLine: {
type: String,
},
endLine: {
type: String,
},
startColumn: {
type: String,
},
endColumn: {
type: String,
},
file: {
type: String,
},
symlinkFile: {
type: String,
},
commit: {
type: String,
},
entropy: {
type: String,
},
author: {
type: String,
},
email: {
type: String,
},
date: {
type: String,
},
message: {
type: String,
},
tags: {
type: [String],
},
ruleID: {
type: String,
},
fingerprint: {
type: String,
unique: true
},
fingerPrintWithoutCommitId: {
type: String,
},
isFalsePositive: {
type: Boolean,
default: false
},
isResolved: {
type: Boolean,
default: false
},
riskOwner: {
type: String,
default: null
},
installationId: {
type: String,
require: true
},
repositoryId: {
type: String
},
repositoryLink: {
type: String
},
repositoryFullName: {
type: String
},
pusher: {
name: {
type: String
},
email: {
type: String
},
},
organization: {
type: Schema.Types.ObjectId,
ref: "Organization",
},
status: {
type: String,
enum: [
STATUS_RESOLVED_FALSE_POSITIVE,
STATUS_RESOLVED_REVOKED,
STATUS_RESOLVED_NOT_REVOKED,
STATUS_UNRESOLVED
],
default: STATUS_UNRESOLVED
}
}, { timestamps: true });
const GitRisks = model<GitRisks>("GitRisks", gitRisks);
export default GitRisks;
+7 -1
View File
@@ -1,5 +1,6 @@
import BackupPrivateKey, { IBackupPrivateKey } from "./backupPrivateKey"; import BackupPrivateKey, { IBackupPrivateKey } from "./backupPrivateKey";
import Bot, { IBot } from "./bot"; import Bot, { IBot } from "./bot";
import BotOrg, { IBotOrg } from "./botOrg";
import BotKey, { IBotKey } from "./botKey"; import BotKey, { IBotKey } from "./botKey";
import IncidentContactOrg, { IIncidentContactOrg } from "./incidentContactOrg"; import IncidentContactOrg, { IIncidentContactOrg } from "./incidentContactOrg";
import Integration, { IIntegration } from "./integration"; import Integration, { IIntegration } from "./integration";
@@ -16,13 +17,14 @@ import ServiceAccountKey, { IServiceAccountKey } from "./serviceAccountKey"; //
import ServiceAccountOrganizationPermission, { IServiceAccountOrganizationPermission } from "./serviceAccountOrganizationPermission"; // new import ServiceAccountOrganizationPermission, { IServiceAccountOrganizationPermission } from "./serviceAccountOrganizationPermission"; // new
import ServiceAccountWorkspacePermission, { IServiceAccountWorkspacePermission } from "./serviceAccountWorkspacePermission"; // new import ServiceAccountWorkspacePermission, { IServiceAccountWorkspacePermission } from "./serviceAccountWorkspacePermission"; // new
import TokenData, { ITokenData } from "./tokenData"; import TokenData, { ITokenData } from "./tokenData";
import User,{ AuthProvider, IUser } from "./user"; import User, { AuthProvider, IUser } from "./user";
import UserAction, { IUserAction } from "./userAction"; import UserAction, { IUserAction } from "./userAction";
import Workspace, { IWorkspace } from "./workspace"; import Workspace, { IWorkspace } from "./workspace";
import ServiceTokenData, { IServiceTokenData } from "./serviceTokenData"; import ServiceTokenData, { IServiceTokenData } from "./serviceTokenData";
import APIKeyData, { IAPIKeyData } from "./apiKeyData"; import APIKeyData, { IAPIKeyData } from "./apiKeyData";
import LoginSRPDetail, { ILoginSRPDetail } from "./loginSRPDetail"; import LoginSRPDetail, { ILoginSRPDetail } from "./loginSRPDetail";
import TokenVersion, { ITokenVersion } from "./tokenVersion"; import TokenVersion, { ITokenVersion } from "./tokenVersion";
import GitRisks, { STATUS_RESOLVED_FALSE_POSITIVE } from "./gitRisks";
export { export {
AuthProvider, AuthProvider,
@@ -30,6 +32,8 @@ export {
IBackupPrivateKey, IBackupPrivateKey,
Bot, Bot,
IBot, IBot,
BotOrg,
IBotOrg,
BotKey, BotKey,
IBotKey, IBotKey,
IncidentContactOrg, IncidentContactOrg,
@@ -76,4 +80,6 @@ export {
ILoginSRPDetail, ILoginSRPDetail,
TokenVersion, TokenVersion,
ITokenVersion, ITokenVersion,
GitRisks,
STATUS_RESOLVED_FALSE_POSITIVE
}; };
+15 -3
View File
@@ -1,11 +1,14 @@
import { Schema, Types, model } from "mongoose";
import { import {
INTEGRATION_AWS_PARAMETER_STORE, INTEGRATION_AWS_PARAMETER_STORE,
INTEGRATION_AWS_SECRET_MANAGER, INTEGRATION_AWS_SECRET_MANAGER,
INTEGRATION_AZURE_KEY_VAULT, INTEGRATION_AZURE_KEY_VAULT,
INTEGRATION_BITBUCKET,
INTEGRATION_CHECKLY, INTEGRATION_CHECKLY,
INTEGRATION_CIRCLECI, INTEGRATION_CIRCLECI,
INTEGRATION_CLOUDFLARE_PAGES, INTEGRATION_CLOUDFLARE_PAGES,
INTEGRATION_CLOUD_66,
INTEGRATION_CODEFRESH,
INTEGRATION_DIGITAL_OCEAN_APP_PLATFORM,
INTEGRATION_FLYIO, INTEGRATION_FLYIO,
INTEGRATION_GITHUB, INTEGRATION_GITHUB,
INTEGRATION_GITLAB, INTEGRATION_GITLAB,
@@ -18,8 +21,9 @@ import {
INTEGRATION_SUPABASE, INTEGRATION_SUPABASE,
INTEGRATION_TERRAFORM_CLOUD, INTEGRATION_TERRAFORM_CLOUD,
INTEGRATION_TRAVISCI, INTEGRATION_TRAVISCI,
INTEGRATION_VERCEL, INTEGRATION_VERCEL
} from "../variables"; } from "../variables";
import { Schema, Types, model } from "mongoose";
export interface IIntegration { export interface IIntegration {
_id: Types.ObjectId; _id: Types.ObjectId;
@@ -56,7 +60,11 @@ export interface IIntegration {
| "checkly" | "checkly"
| "terraform-cloud" | "terraform-cloud"
| "hashicorp-vault" | "hashicorp-vault"
| "cloudflare-pages"; | "cloudflare-pages"
| "bitbucket"
| "codefresh"
| "digital-ocean-app-platform"
| "cloud-66"
integrationAuth: Types.ObjectId; integrationAuth: Types.ObjectId;
} }
@@ -147,6 +155,10 @@ const integrationSchema = new Schema<IIntegration>(
INTEGRATION_TERRAFORM_CLOUD, INTEGRATION_TERRAFORM_CLOUD,
INTEGRATION_HASHICORP_VAULT, INTEGRATION_HASHICORP_VAULT,
INTEGRATION_CLOUDFLARE_PAGES, INTEGRATION_CLOUDFLARE_PAGES,
INTEGRATION_BITBUCKET,
INTEGRATION_DIGITAL_OCEAN_APP_PLATFORM,
INTEGRATION_CODEFRESH,
INTEGRATION_CLOUD_66,
], ],
required: true, required: true,
}, },
+32 -2
View File
@@ -1,4 +1,3 @@
import { Document, Schema, Types, model } from "mongoose";
import { import {
ALGORITHM_AES_256_GCM, ALGORITHM_AES_256_GCM,
ENCODING_SCHEME_BASE64, ENCODING_SCHEME_BASE64,
@@ -6,8 +5,12 @@ import {
INTEGRATION_AWS_PARAMETER_STORE, INTEGRATION_AWS_PARAMETER_STORE,
INTEGRATION_AWS_SECRET_MANAGER, INTEGRATION_AWS_SECRET_MANAGER,
INTEGRATION_AZURE_KEY_VAULT, INTEGRATION_AZURE_KEY_VAULT,
INTEGRATION_BITBUCKET,
INTEGRATION_CIRCLECI, INTEGRATION_CIRCLECI,
INTEGRATION_CLOUDFLARE_PAGES, INTEGRATION_CLOUDFLARE_PAGES,
INTEGRATION_CLOUD_66,
INTEGRATION_CODEFRESH,
INTEGRATION_DIGITAL_OCEAN_APP_PLATFORM,
INTEGRATION_FLYIO, INTEGRATION_FLYIO,
INTEGRATION_GITHUB, INTEGRATION_GITHUB,
INTEGRATION_GITLAB, INTEGRATION_GITLAB,
@@ -22,11 +25,34 @@ import {
INTEGRATION_TRAVISCI, INTEGRATION_TRAVISCI,
INTEGRATION_VERCEL INTEGRATION_VERCEL
} from "../variables"; } from "../variables";
import { Document, Schema, Types, model } from "mongoose";
export interface IIntegrationAuth extends Document { export interface IIntegrationAuth extends Document {
_id: Types.ObjectId; _id: Types.ObjectId;
workspace: Types.ObjectId; workspace: Types.ObjectId;
integration: "heroku" | "vercel" | "netlify" | "github" | "gitlab" | "render" | "railway" | "flyio" | "azure-key-vault" | "laravel-forge" | "circleci" | "travisci" | "supabase" | "aws-parameter-store" | "aws-secret-manager" | "terraform-cloud" | "checkly" | "cloudflare-pages"; integration:
| "heroku"
| "vercel"
| "netlify"
| "github"
| "gitlab"
| "render"
| "railway"
| "flyio"
| "azure-key-vault"
| "laravel-forge"
| "circleci"
| "travisci"
| "supabase"
| "aws-parameter-store"
| "aws-secret-manager"
| "checkly"
| "cloudflare-pages"
| "codefresh"
| "digital-ocean-app-platform"
| "bitbucket"
| "cloud-66"
| "terraform-cloud";
teamId: string; teamId: string;
accountId: string; accountId: string;
url: string; url: string;
@@ -73,6 +99,10 @@ const integrationAuthSchema = new Schema<IIntegrationAuth>(
INTEGRATION_TERRAFORM_CLOUD, INTEGRATION_TERRAFORM_CLOUD,
INTEGRATION_HASHICORP_VAULT, INTEGRATION_HASHICORP_VAULT,
INTEGRATION_CLOUDFLARE_PAGES, INTEGRATION_CLOUDFLARE_PAGES,
INTEGRATION_BITBUCKET,
INTEGRATION_DIGITAL_OCEAN_APP_PLATFORM,
INTEGRATION_CODEFRESH,
INTEGRATION_CLOUD_66,
], ],
required: true, required: true,
}, },
+52
View File
@@ -0,0 +1,52 @@
import { Schema, Types, model } from "mongoose";
export interface ISecretImports {
_id: Types.ObjectId;
workspace: Types.ObjectId;
environment: string;
folderId: string;
imports: Array<{
environment: string;
secretPath: string;
}>;
}
const secretImportSchema = new Schema<ISecretImports>(
{
workspace: {
type: Schema.Types.ObjectId,
ref: "Workspace",
required: true
},
environment: {
type: String,
required: true
},
folderId: {
type: String,
required: true,
default: "root"
},
imports: {
type: [
{
environment: {
type: String,
required: true
},
secretPath: {
type: String,
required: true
}
}
],
default: []
}
},
{
timestamps: true
}
);
const SecretImport = model<ISecretImports>("SecretImports", secretImportSchema);
export default SecretImport;
+33 -27
View File
@@ -4,7 +4,10 @@ export interface IServiceTokenData extends Document {
_id: Types.ObjectId; _id: Types.ObjectId;
name: string; name: string;
workspace: Types.ObjectId; workspace: Types.ObjectId;
environment: string; scopes: Array<{
environment: string;
secretPath: string;
}>;
user: Types.ObjectId; user: Types.ObjectId;
serviceAccount: Types.ObjectId; serviceAccount: Types.ObjectId;
lastUsed: Date; lastUsed: Date;
@@ -13,7 +16,6 @@ export interface IServiceTokenData extends Document {
encryptedKey: string; encryptedKey: string;
iv: string; iv: string;
tag: string; tag: string;
secretPath: string;
permissions: string[]; permissions: string[];
} }
@@ -21,68 +23,72 @@ const serviceTokenDataSchema = new Schema<IServiceTokenData>(
{ {
name: { name: {
type: String, type: String,
required: true, required: true
}, },
workspace: { workspace: {
type: Schema.Types.ObjectId, type: Schema.Types.ObjectId,
ref: "Workspace", ref: "Workspace",
required: true, required: true
}, },
environment: { scopes: {
type: String, type: [
required: true, {
environment: {
type: String,
required: true
},
secretPath: {
type: String,
default: "/",
required: true
}
}
],
required: true
}, },
user: { user: {
type: Schema.Types.ObjectId, type: Schema.Types.ObjectId,
ref: "User", ref: "User",
required: true, required: true
}, },
serviceAccount: { serviceAccount: {
type: Schema.Types.ObjectId, type: Schema.Types.ObjectId,
ref: "ServiceAccount", ref: "ServiceAccount"
}, },
lastUsed: { lastUsed: {
type: Date, type: Date
}, },
expiresAt: { expiresAt: {
type: Date, type: Date
}, },
secretHash: { secretHash: {
type: String, type: String,
required: true, required: true,
select: false, select: false
}, },
encryptedKey: { encryptedKey: {
type: String, type: String,
select: false, select: false
}, },
iv: { iv: {
type: String, type: String,
select: false, select: false
}, },
tag: { tag: {
type: String, type: String,
select: false, select: false
}, },
permissions: { permissions: {
type: [String], type: [String],
enum: ["read", "write"], enum: ["read", "write"],
default: ["read"], default: ["read"]
}, }
secretPath: {
type: String,
default: "/",
required: true,
},
}, },
{ {
timestamps: true, timestamps: true
} }
); );
const ServiceTokenData = model<IServiceTokenData>( const ServiceTokenData = model<IServiceTokenData>("ServiceTokenData", serviceTokenDataSchema);
"ServiceTokenData",
serviceTokenDataSchema
);
export default ServiceTokenData; export default ServiceTokenData;
+2
View File
@@ -1,7 +1,9 @@
import { Document, Schema, Types, model } from "mongoose"; import { Document, Schema, Types, model } from "mongoose";
export enum AuthProvider { export enum AuthProvider {
EMAIL = "email",
GOOGLE = "google", GOOGLE = "google",
OKTA_SAML = "okta-saml"
} }
export interface IUser extends Document { export interface IUser extends Document {
+85
View File
@@ -0,0 +1,85 @@
import { Document, Schema, Types, model } from "mongoose";
import { ALGORITHM_AES_256_GCM, ENCODING_SCHEME_BASE64, ENCODING_SCHEME_UTF8 } from "../variables";
export interface IWebhook extends Document {
_id: Types.ObjectId;
workspace: Types.ObjectId;
environment: string;
secretPath: string;
url: string;
lastStatus: "success" | "failed";
lastRunErrorMessage?: string;
isDisabled: boolean;
encryptedSecretKey: string;
iv: string;
tag: string;
algorithm: "aes-256-gcm";
keyEncoding: "base64" | "utf8";
}
const WebhookSchema = new Schema<IWebhook>(
{
workspace: {
type: Schema.Types.ObjectId,
ref: "Workspace",
required: true
},
environment: {
type: String,
required: true
},
secretPath: {
type: String,
required: true,
default: "/"
},
url: {
type: String,
required: true
},
lastStatus: {
type: String,
enum: ["success", "failed"]
},
lastRunErrorMessage: {
type: String
},
isDisabled: {
type: Boolean,
default: false
},
// used for webhook signature
encryptedSecretKey: {
type: String,
select: false
},
iv: {
type: String,
select: false
},
tag: {
type: String,
select: false
},
algorithm: {
// the encryption algorithm used
type: String,
enum: [ALGORITHM_AES_256_GCM],
required: true,
select: false
},
keyEncoding: {
type: String,
enum: [ENCODING_SCHEME_UTF8, ENCODING_SCHEME_BASE64],
required: true,
select: false
}
},
{
timestamps: true
}
);
const Webhook = model<IWebhook>("Webhook", WebhookSchema);
export default Webhook;
+2 -1
View File
@@ -1,5 +1,5 @@
import express, { Request, Response } from "express"; import express, { Request, Response } from "express";
import { getSmtpConfigured } from "../../config"; import { getSecretScanningGitAppId, getSecretScanningPrivateKey, getSecretScanningWebhookSecret, getSmtpConfigured } from "../../config";
const router = express.Router(); const router = express.Router();
@@ -10,6 +10,7 @@ router.get(
date: new Date(), date: new Date(),
message: "Ok", message: "Ok",
emailConfigured: await getSmtpConfigured(), emailConfigured: await getSmtpConfigured(),
secretScanningConfigured: await getSecretScanningGitAppId() && await getSecretScanningWebhookSecret() && await getSecretScanningPrivateKey(),
}) })
} }
); );
-16
View File
@@ -1,7 +1,6 @@
import express from "express"; import express from "express";
const router = express.Router(); const router = express.Router();
import { body } from "express-validator"; import { body } from "express-validator";
import passport from "passport";
import { requireAuth, validateRequest } from "../../middleware"; import { requireAuth, validateRequest } from "../../middleware";
import { authController } from "../../controllers/v1"; import { authController } from "../../controllers/v1";
import { authLimiter } from "../../helpers/rateLimiter"; import { authLimiter } from "../../helpers/rateLimiter";
@@ -44,21 +43,6 @@ router.post(
authController.checkAuth authController.checkAuth
); );
router.get(
"/redirect/google",
authLimiter,
passport.authenticate("google", {
scope: ["profile", "email"],
session: false,
}),
);
router.get(
"/callback/google",
passport.authenticate("google", { failureRedirect: "/login/provider/error", session: false }),
authController.handleAuthProviderCallback,
);
router.get( router.get(
"/common-passwords", "/common-passwords",
authLimiter, authLimiter,
+23 -17
View File
@@ -15,23 +15,29 @@ import password from "./password";
import integration from "./integration"; import integration from "./integration";
import integrationAuth from "./integrationAuth"; import integrationAuth from "./integrationAuth";
import secretsFolder from "./secretsFolder"; import secretsFolder from "./secretsFolder";
import secretScanning from "./secretScanning";
import webhooks from "./webhook";
import secretImport from "./secretImport";
export { export {
signup, signup,
auth, auth,
bot, bot,
user, user,
userAction, userAction,
organization, organization,
workspace, workspace,
membershipOrg, membershipOrg,
membership, membership,
key, key,
inviteOrg, inviteOrg,
secret, secret,
serviceToken, serviceToken,
password, password,
integration, integration,
integrationAuth, integrationAuth,
secretsFolder, secretsFolder,
secretScanning,
webhooks,
secretImport
}; };
+14
View File
@@ -81,6 +81,7 @@ router.get(
}), }),
param("integrationAuthId"), param("integrationAuthId"),
query("teamId"), query("teamId"),
query("workspaceSlug"),
validateRequest, validateRequest,
integrationAuthController.getIntegrationAuthApps integrationAuthController.getIntegrationAuthApps
); );
@@ -141,6 +142,19 @@ router.get(
integrationAuthController.getIntegrationAuthRailwayServices integrationAuthController.getIntegrationAuthRailwayServices
); );
router.get(
"/:integrationAuthId/bitbucket/workspaces",
requireAuth({
acceptedAuthModes: [AUTH_MODE_JWT],
}),
requireIntegrationAuthorizationAuth({
acceptedRoles: [ADMIN, MEMBER],
}),
param("integrationAuthId").exists().isString(),
validateRequest,
integrationAuthController.getIntegrationAuthBitBucketWorkspaces
);
router.delete( router.delete(
"/:integrationAuthId", "/:integrationAuthId",
requireAuth({ requireAuth({
+84
View File
@@ -0,0 +1,84 @@
import express from "express";
const router = express.Router();
import { body, param, query } from "express-validator";
import { secretImportController } from "../../controllers/v1";
import { requireAuth, requireWorkspaceAuth, validateRequest } from "../../middleware";
import { ADMIN, AUTH_MODE_JWT, MEMBER } from "../../variables";
router.post(
"/",
requireAuth({
acceptedAuthModes: [AUTH_MODE_JWT]
}),
requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER],
locationWorkspaceId: "body"
}),
body("workspaceId").exists().isString().trim().notEmpty(),
body("environment").exists().isString().trim().notEmpty(),
body("folderId").default("root").isString().trim(),
body("secretImport").exists().isObject(),
body("secretImport.environment").isString().exists().trim(),
body("secretImport.secretPath").isString().exists().trim(),
validateRequest,
secretImportController.createSecretImport
);
router.put(
"/:id",
requireAuth({
acceptedAuthModes: [AUTH_MODE_JWT]
}),
param("id").exists().isString().trim(),
body("secretImports").exists().isArray(),
body("secretImports.*.environment").isString().exists().trim(),
body("secretImports.*.secretPath").isString().exists().trim(),
validateRequest,
secretImportController.updateSecretImport
);
router.delete(
"/:id",
requireAuth({
acceptedAuthModes: [AUTH_MODE_JWT]
}),
param("id").exists().isString().trim(),
body("secretImportPath").isString().exists().trim(),
body("secretImportEnv").isString().exists().trim(),
validateRequest,
secretImportController.deleteSecretImport
);
router.get(
"/",
requireAuth({
acceptedAuthModes: [AUTH_MODE_JWT]
}),
requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER],
locationWorkspaceId: "query"
}),
query("workspaceId").exists().isString().trim().notEmpty(),
query("environment").exists().isString().trim().notEmpty(),
query("folderId").default("root").isString().trim(),
validateRequest,
secretImportController.getSecretImports
);
router.get(
"/secrets",
requireAuth({
acceptedAuthModes: [AUTH_MODE_JWT]
}),
requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER],
locationWorkspaceId: "query"
}),
query("workspaceId").exists().isString().trim().notEmpty(),
query("environment").exists().isString().trim().notEmpty(),
query("folderId").default("root").isString().trim(),
validateRequest,
secretImportController.getAllSecretsFromImport
);
export default router;
+81
View File
@@ -0,0 +1,81 @@
import express from "express";
const router = express.Router();
import {
requireAuth,
requireOrganizationAuth,
validateRequest,
} from "../../middleware";
import { body, param } from "express-validator";
import { createInstallationSession, getCurrentOrganizationInstallationStatus, getRisksForOrganization, linkInstallationToOrganization, updateRisksStatus } from "../../controllers/v1/secretScanningController";
import { ACCEPTED, ADMIN, MEMBER, OWNER } from "../../variables";
router.post(
"/create-installation-session/organization/:organizationId",
requireAuth({
acceptedAuthModes: ["jwt"],
}),
param("organizationId").exists().trim(),
requireOrganizationAuth({
acceptedRoles: [OWNER, ADMIN, MEMBER],
acceptedStatuses: [ACCEPTED],
}),
validateRequest,
createInstallationSession
);
router.post(
"/link-installation",
requireAuth({
acceptedAuthModes: ["jwt"],
}),
body("installationId").exists().trim(),
body("sessionId").exists().trim(),
validateRequest,
linkInstallationToOrganization
);
router.get(
"/installation-status/organization/:organizationId",
requireAuth({
acceptedAuthModes: ["jwt"],
}),
param("organizationId").exists().trim(),
requireOrganizationAuth({
acceptedRoles: [OWNER, ADMIN, MEMBER],
acceptedStatuses: [ACCEPTED],
}),
validateRequest,
getCurrentOrganizationInstallationStatus
);
router.get(
"/organization/:organizationId/risks",
requireAuth({
acceptedAuthModes: ["jwt"],
}),
param("organizationId").exists().trim(),
requireOrganizationAuth({
acceptedRoles: [OWNER, ADMIN, MEMBER],
acceptedStatuses: [ACCEPTED],
}),
validateRequest,
getRisksForOrganization
);
router.post(
"/organization/:organizationId/risks/:riskId/status",
requireAuth({
acceptedAuthModes: ["jwt"],
}),
param("organizationId").exists().trim(),
param("riskId").exists().trim(),
body("status").exists(),
requireOrganizationAuth({
acceptedRoles: [OWNER, ADMIN, MEMBER],
acceptedStatuses: [ACCEPTED],
}),
validateRequest,
updateRisksStatus
);
export default router;
+75
View File
@@ -0,0 +1,75 @@
import express from "express";
const router = express.Router();
import { requireAuth, requireWorkspaceAuth, validateRequest } from "../../middleware";
import { body, param, query } from "express-validator";
import { ADMIN, AUTH_MODE_JWT, MEMBER } from "../../variables";
import { webhookController } from "../../controllers/v1";
router.post(
"/",
requireAuth({
acceptedAuthModes: [AUTH_MODE_JWT]
}),
requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER],
locationWorkspaceId: "body",
locationEnvironment: "body"
}),
body("workspaceId").exists().isString().trim(),
body("environment").exists().isString().trim(),
body("webhookUrl").exists().isString().isURL().trim(),
body("webhookSecretKey").isString().trim(),
body("secretPath").default("/").isString().trim(),
validateRequest,
webhookController.createWebhook
);
router.patch(
"/:webhookId",
requireAuth({
acceptedAuthModes: [AUTH_MODE_JWT]
}),
param("webhookId").exists().isString().trim(),
body("isDisabled").default(false).isBoolean(),
validateRequest,
webhookController.updateWebhook
);
router.post(
"/:webhookId/test",
requireAuth({
acceptedAuthModes: [AUTH_MODE_JWT]
}),
param("webhookId").exists().isString().trim(),
validateRequest,
webhookController.testWebhook
);
router.delete(
"/:webhookId",
requireAuth({
acceptedAuthModes: [AUTH_MODE_JWT]
}),
param("webhookId").exists().isString().trim(),
validateRequest,
webhookController.deleteWebhook
);
router.get(
"/",
requireAuth({
acceptedAuthModes: [AUTH_MODE_JWT]
}),
requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER],
locationWorkspaceId: "query",
locationEnvironment: "query"
}),
query("workspaceId").exists().isString().trim(),
query("environment").optional().isString().trim(),
query("secretPath").optional().isString().trim(),
validateRequest,
webhookController.listWebhooks
);
export default router;
+21 -45
View File
@@ -5,7 +5,7 @@ import {
requireAuth, requireAuth,
requireSecretsAuth, requireSecretsAuth,
requireWorkspaceAuth, requireWorkspaceAuth,
validateRequest, validateRequest
} from "../../middleware"; } from "../../middleware";
import { validateClientForSecrets } from "../../validation"; import { validateClientForSecrets } from "../../validation";
import { body, query } from "express-validator"; import { body, query } from "express-validator";
@@ -20,22 +20,18 @@ import {
PERMISSION_READ_SECRETS, PERMISSION_READ_SECRETS,
PERMISSION_WRITE_SECRETS, PERMISSION_WRITE_SECRETS,
SECRET_PERSONAL, SECRET_PERSONAL,
SECRET_SHARED, SECRET_SHARED
} from "../../variables"; } from "../../variables";
import { BatchSecretRequest } from "../../types/secret"; import { BatchSecretRequest } from "../../types/secret";
router.post( router.post(
"/batch", "/batch",
requireAuth({ requireAuth({
acceptedAuthModes: [ acceptedAuthModes: [AUTH_MODE_JWT, AUTH_MODE_API_KEY, AUTH_MODE_SERVICE_TOKEN]
AUTH_MODE_JWT,
AUTH_MODE_API_KEY,
AUTH_MODE_SERVICE_TOKEN,
],
}), }),
requireWorkspaceAuth({ requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER], acceptedRoles: [ADMIN, MEMBER],
locationWorkspaceId: "body", locationWorkspaceId: "body"
}), }),
body("workspaceId").exists().isString().trim(), body("workspaceId").exists().isString().trim(),
body("folderId").default("root").isString().trim(), body("folderId").default("root").isString().trim(),
@@ -52,10 +48,8 @@ router.post(
if (secretIds.length > 0) { if (secretIds.length > 0) {
req.secrets = await validateClientForSecrets({ req.secrets = await validateClientForSecrets({
authData: req.authData, authData: req.authData,
secretIds: secretIds.map( secretIds: secretIds.map((secretId: string) => new Types.ObjectId(secretId)),
(secretId: string) => new Types.ObjectId(secretId) requiredPermissions: []
),
requiredPermissions: [],
}); });
} }
} }
@@ -76,14 +70,11 @@ router.post(
.custom((value) => { .custom((value) => {
if (Array.isArray(value)) { if (Array.isArray(value)) {
// case: create multiple secrets // case: create multiple secrets
if (value.length === 0) if (value.length === 0) throw new Error("secrets cannot be an empty array");
throw new Error("secrets cannot be an empty array");
for (const secret of value) { for (const secret of value) {
if ( if (
!secret.type || !secret.type ||
!( !(secret.type === SECRET_PERSONAL || secret.type === SECRET_SHARED) ||
secret.type === SECRET_PERSONAL || secret.type === SECRET_SHARED
) ||
!secret.secretKeyCiphertext || !secret.secretKeyCiphertext ||
!secret.secretKeyIV || !secret.secretKeyIV ||
!secret.secretKeyTag || !secret.secretKeyTag ||
@@ -108,9 +99,7 @@ router.post(
!value.secretValueIV || !value.secretValueIV ||
!value.secretValueTag !value.secretValueTag
) { ) {
throw new Error( throw new Error("secrets object is missing required secret properties");
"secrets object is missing required secret properties"
);
} }
} else { } else {
throw new Error("secrets must be an object or an array of objects"); throw new Error("secrets must be an object or an array of objects");
@@ -120,17 +109,13 @@ router.post(
}), }),
validateRequest, validateRequest,
requireAuth({ requireAuth({
acceptedAuthModes: [ acceptedAuthModes: [AUTH_MODE_JWT, AUTH_MODE_API_KEY, AUTH_MODE_SERVICE_TOKEN]
AUTH_MODE_JWT,
AUTH_MODE_API_KEY,
AUTH_MODE_SERVICE_TOKEN,
],
}), }),
requireWorkspaceAuth({ requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER], acceptedRoles: [ADMIN, MEMBER],
locationWorkspaceId: "body", locationWorkspaceId: "body",
locationEnvironment: "body", locationEnvironment: "body",
requiredPermissions: [PERMISSION_WRITE_SECRETS], requiredPermissions: [PERMISSION_WRITE_SECRETS]
}), }),
secretsController.createSecrets secretsController.createSecrets
); );
@@ -142,20 +127,21 @@ router.get(
query("tagSlugs"), query("tagSlugs"),
query("folderId").default("root").isString().trim(), query("folderId").default("root").isString().trim(),
query("secretPath").optional().isString().trim(), query("secretPath").optional().isString().trim(),
query("include_imports").optional().default(false).isBoolean(),
validateRequest, validateRequest,
requireAuth({ requireAuth({
acceptedAuthModes: [ acceptedAuthModes: [
AUTH_MODE_JWT, AUTH_MODE_JWT,
AUTH_MODE_API_KEY, AUTH_MODE_API_KEY,
AUTH_MODE_SERVICE_TOKEN, AUTH_MODE_SERVICE_TOKEN,
AUTH_MODE_SERVICE_ACCOUNT, AUTH_MODE_SERVICE_ACCOUNT
], ]
}), }),
requireWorkspaceAuth({ requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER], acceptedRoles: [ADMIN, MEMBER],
locationWorkspaceId: "query", locationWorkspaceId: "query",
locationEnvironment: "query", locationEnvironment: "query",
requiredPermissions: [PERMISSION_READ_SECRETS], requiredPermissions: [PERMISSION_READ_SECRETS]
}), }),
secretsController.getSecrets secretsController.getSecrets
); );
@@ -167,8 +153,7 @@ router.patch(
.custom((value) => { .custom((value) => {
if (Array.isArray(value)) { if (Array.isArray(value)) {
// case: update multiple secrets // case: update multiple secrets
if (value.length === 0) if (value.length === 0) throw new Error("secrets cannot be an empty array");
throw new Error("secrets cannot be an empty array");
for (const secret of value) { for (const secret of value) {
if (!secret.id) { if (!secret.id) {
throw new Error("Each secret must contain a ID property"); throw new Error("Each secret must contain a ID property");
@@ -187,15 +172,11 @@ router.patch(
}), }),
validateRequest, validateRequest,
requireAuth({ requireAuth({
acceptedAuthModes: [ acceptedAuthModes: [AUTH_MODE_JWT, AUTH_MODE_API_KEY, AUTH_MODE_SERVICE_TOKEN]
AUTH_MODE_JWT,
AUTH_MODE_API_KEY,
AUTH_MODE_SERVICE_TOKEN,
],
}), }),
requireSecretsAuth({ requireSecretsAuth({
acceptedRoles: [ADMIN, MEMBER], acceptedRoles: [ADMIN, MEMBER],
requiredPermissions: [PERMISSION_WRITE_SECRETS], requiredPermissions: [PERMISSION_WRITE_SECRETS]
}), }),
secretsController.updateSecrets secretsController.updateSecrets
); );
@@ -210,8 +191,7 @@ router.delete(
if (Array.isArray(value)) { if (Array.isArray(value)) {
// case: delete multiple secrets // case: delete multiple secrets
if (value.length === 0) if (value.length === 0) throw new Error("secrets cannot be an empty array");
throw new Error("secrets cannot be an empty array");
return value.every((id: string) => typeof id === "string"); return value.every((id: string) => typeof id === "string");
} }
@@ -221,15 +201,11 @@ router.delete(
.isEmpty(), .isEmpty(),
validateRequest, validateRequest,
requireAuth({ requireAuth({
acceptedAuthModes: [ acceptedAuthModes: [AUTH_MODE_JWT, AUTH_MODE_API_KEY, AUTH_MODE_SERVICE_TOKEN]
AUTH_MODE_JWT,
AUTH_MODE_API_KEY,
AUTH_MODE_SERVICE_TOKEN,
],
}), }),
requireSecretsAuth({ requireSecretsAuth({
acceptedRoles: [ADMIN, MEMBER], acceptedRoles: [ADMIN, MEMBER],
requiredPermissions: [PERMISSION_WRITE_SECRETS], requiredPermissions: [PERMISSION_WRITE_SECRETS]
}), }),
secretsController.deleteSecrets secretsController.deleteSecrets
); );
+12 -15
View File
@@ -4,7 +4,7 @@ import {
requireAuth, requireAuth,
requireServiceTokenDataAuth, requireServiceTokenDataAuth,
requireWorkspaceAuth, requireWorkspaceAuth,
validateRequest, validateRequest
} from "../../middleware"; } from "../../middleware";
import { body, param } from "express-validator"; import { body, param } from "express-validator";
import { import {
@@ -13,14 +13,14 @@ import {
AUTH_MODE_SERVICE_ACCOUNT, AUTH_MODE_SERVICE_ACCOUNT,
AUTH_MODE_SERVICE_TOKEN, AUTH_MODE_SERVICE_TOKEN,
MEMBER, MEMBER,
PERMISSION_WRITE_SECRETS, PERMISSION_WRITE_SECRETS
} from "../../variables"; } from "../../variables";
import { serviceTokenDataController } from "../../controllers/v2"; import { serviceTokenDataController } from "../../controllers/v2";
router.get( router.get(
"/", "/",
requireAuth({ requireAuth({
acceptedAuthModes: [AUTH_MODE_SERVICE_TOKEN], acceptedAuthModes: [AUTH_MODE_SERVICE_TOKEN]
}), }),
serviceTokenDataController.getServiceTokenData serviceTokenDataController.getServiceTokenData
); );
@@ -28,33 +28,30 @@ router.get(
router.post( router.post(
"/", "/",
requireAuth({ requireAuth({
acceptedAuthModes: [AUTH_MODE_JWT, AUTH_MODE_SERVICE_ACCOUNT], acceptedAuthModes: [AUTH_MODE_JWT, AUTH_MODE_SERVICE_ACCOUNT]
}), }),
requireWorkspaceAuth({ requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER], acceptedRoles: [ADMIN, MEMBER],
locationWorkspaceId: "body", locationWorkspaceId: "body",
locationEnvironment: "body", locationEnvironment: "body",
requiredPermissions: [PERMISSION_WRITE_SECRETS], requiredPermissions: [PERMISSION_WRITE_SECRETS]
}), }),
body("name").exists().isString().trim(), body("name").exists().isString().trim(),
body("workspaceId").exists().isString().trim(), body("workspaceId").exists().isString().trim(),
body("environment").exists().isString().trim(), body("scopes").exists().isArray(),
body("scopes.*.environment").exists().isString().trim(),
body("scopes.*.secretPath").exists().isString().trim(),
body("encryptedKey").exists().isString().trim(), body("encryptedKey").exists().isString().trim(),
body("iv").exists().isString().trim(), body("iv").exists().isString().trim(),
body("secretPath").isString().default("/").trim(),
body("tag").exists().isString().trim(), body("tag").exists().isString().trim(),
body("expiresIn").exists().isNumeric(), // measured in ms body("expiresIn").exists().isNumeric(), // measured in ms
body("permissions") body("permissions")
.isArray({ min: 1 }) .isArray({ min: 1 })
.custom((value: string[]) => { .custom((value: string[]) => {
const allowedPermissions = ["read", "write"]; const allowedPermissions = ["read", "write"];
const invalidValues = value.filter( const invalidValues = value.filter((v) => !allowedPermissions.includes(v));
(v) => !allowedPermissions.includes(v)
);
if (invalidValues.length > 0) { if (invalidValues.length > 0) {
throw new Error( throw new Error(`permissions contains invalid values: ${invalidValues.join(", ")}`);
`permissions contains invalid values: ${invalidValues.join(", ")}`
);
} }
return true; return true;
@@ -66,10 +63,10 @@ router.post(
router.delete( router.delete(
"/:serviceTokenDataId", "/:serviceTokenDataId",
requireAuth({ requireAuth({
acceptedAuthModes: [AUTH_MODE_JWT], acceptedAuthModes: [AUTH_MODE_JWT]
}), }),
requireServiceTokenDataAuth({ requireServiceTokenDataAuth({
acceptedRoles: [ADMIN, MEMBER], acceptedRoles: [ADMIN, MEMBER]
}), }),
param("serviceTokenDataId").exists().trim(), param("serviceTokenDataId").exists().trim(),
validateRequest, validateRequest,
+29 -2
View File
@@ -10,6 +10,9 @@ import {
AUTH_MODE_API_KEY, AUTH_MODE_API_KEY,
AUTH_MODE_JWT, AUTH_MODE_JWT,
} from "../../variables"; } from "../../variables";
import {
AuthProvider
} from "../../models";
router.get( router.get(
"/me", "/me",
@@ -29,6 +32,30 @@ router.patch(
usersController.updateMyMfaEnabled usersController.updateMyMfaEnabled
); );
router.patch(
"/me/name",
requireAuth({
acceptedAuthModes: [AUTH_MODE_JWT, AUTH_MODE_API_KEY],
}),
body("firstName").exists().isString(),
body("lastName").isString(),
validateRequest,
usersController.updateName
);
router.patch(
"/me/auth-provider",
requireAuth({
acceptedAuthModes: [AUTH_MODE_JWT, AUTH_MODE_API_KEY],
}),
body("authProvider").exists().isString().isIn([
AuthProvider.EMAIL,
AuthProvider.GOOGLE
]),
validateRequest,
usersController.updateAuthProvider
);
router.get( router.get(
"/me/organizations", "/me/organizations",
requireAuth({ requireAuth({
@@ -66,7 +93,7 @@ router.delete(
usersController.deleteAPIKey usersController.deleteAPIKey
); );
router.get( // new router.get(
"/me/sessions", "/me/sessions",
requireAuth({ requireAuth({
acceptedAuthModes: [AUTH_MODE_JWT], acceptedAuthModes: [AUTH_MODE_JWT],
@@ -74,7 +101,7 @@ router.get( // new
usersController.getMySessions usersController.getMySessions
); );
router.delete( // new router.delete(
"/me/sessions", "/me/sessions",
requireAuth({ requireAuth({
acceptedAuthModes: [AUTH_MODE_JWT], acceptedAuthModes: [AUTH_MODE_JWT],
+34 -36
View File
@@ -1,10 +1,6 @@
import express from "express"; import express from "express";
const router = express.Router(); const router = express.Router();
import { import { requireAuth, requireWorkspaceAuth, validateRequest } from "../../middleware";
requireAuth,
requireWorkspaceAuth,
validateRequest,
} from "../../middleware";
import { body, param, query } from "express-validator"; import { body, param, query } from "express-validator";
import { secretsController } from "../../controllers/v3"; import { secretsController } from "../../controllers/v3";
import { import {
@@ -17,30 +13,23 @@ import {
PERMISSION_READ_SECRETS, PERMISSION_READ_SECRETS,
PERMISSION_WRITE_SECRETS, PERMISSION_WRITE_SECRETS,
SECRET_PERSONAL, SECRET_PERSONAL,
SECRET_SHARED, SECRET_SHARED
} from "../../variables"; } from "../../variables";
router.get( router.get(
"/raw", "/raw",
query("workspaceId").exists().isString().trim(), query("workspaceId").optional().isString().trim(),
query("environment").exists().isString().trim(), query("environment").optional().isString().trim(),
query("secretPath").default("/").isString().trim(), query("secretPath").default("/").isString().trim(),
query("include_imports").optional().isBoolean().default(false),
validateRequest, validateRequest,
requireAuth({ requireAuth({
acceptedAuthModes: [ acceptedAuthModes: [
AUTH_MODE_JWT, AUTH_MODE_JWT,
AUTH_MODE_API_KEY, AUTH_MODE_API_KEY,
AUTH_MODE_SERVICE_TOKEN, AUTH_MODE_SERVICE_TOKEN,
AUTH_MODE_SERVICE_ACCOUNT, AUTH_MODE_SERVICE_ACCOUNT
], ]
}),
requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER],
locationWorkspaceId: "query",
locationEnvironment: "query",
requiredPermissions: [PERMISSION_READ_SECRETS],
requireBlindIndicesEnabled: true,
requireE2EEOff: true,
}), }),
secretsController.getSecretsRaw secretsController.getSecretsRaw
); );
@@ -58,8 +47,8 @@ router.get(
AUTH_MODE_JWT, AUTH_MODE_JWT,
AUTH_MODE_API_KEY, AUTH_MODE_API_KEY,
AUTH_MODE_SERVICE_TOKEN, AUTH_MODE_SERVICE_TOKEN,
AUTH_MODE_SERVICE_ACCOUNT, AUTH_MODE_SERVICE_ACCOUNT
], ]
}), }),
requireWorkspaceAuth({ requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER], acceptedRoles: [ADMIN, MEMBER],
@@ -68,6 +57,7 @@ router.get(
requiredPermissions: [PERMISSION_READ_SECRETS], requiredPermissions: [PERMISSION_READ_SECRETS],
requireBlindIndicesEnabled: true, requireBlindIndicesEnabled: true,
requireE2EEOff: true, requireE2EEOff: true,
checkIPAllowlist: true
}), }),
secretsController.getSecretByNameRaw secretsController.getSecretByNameRaw
); );
@@ -86,8 +76,8 @@ router.post(
AUTH_MODE_JWT, AUTH_MODE_JWT,
AUTH_MODE_API_KEY, AUTH_MODE_API_KEY,
AUTH_MODE_SERVICE_TOKEN, AUTH_MODE_SERVICE_TOKEN,
AUTH_MODE_SERVICE_ACCOUNT, AUTH_MODE_SERVICE_ACCOUNT
], ]
}), }),
requireWorkspaceAuth({ requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER], acceptedRoles: [ADMIN, MEMBER],
@@ -96,6 +86,7 @@ router.post(
requiredPermissions: [PERMISSION_WRITE_SECRETS], requiredPermissions: [PERMISSION_WRITE_SECRETS],
requireBlindIndicesEnabled: true, requireBlindIndicesEnabled: true,
requireE2EEOff: true, requireE2EEOff: true,
checkIPAllowlist: true
}), }),
secretsController.createSecretRaw secretsController.createSecretRaw
); );
@@ -114,8 +105,8 @@ router.patch(
AUTH_MODE_JWT, AUTH_MODE_JWT,
AUTH_MODE_API_KEY, AUTH_MODE_API_KEY,
AUTH_MODE_SERVICE_TOKEN, AUTH_MODE_SERVICE_TOKEN,
AUTH_MODE_SERVICE_ACCOUNT, AUTH_MODE_SERVICE_ACCOUNT
], ]
}), }),
requireWorkspaceAuth({ requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER], acceptedRoles: [ADMIN, MEMBER],
@@ -124,6 +115,7 @@ router.patch(
requiredPermissions: [PERMISSION_WRITE_SECRETS], requiredPermissions: [PERMISSION_WRITE_SECRETS],
requireBlindIndicesEnabled: true, requireBlindIndicesEnabled: true,
requireE2EEOff: true, requireE2EEOff: true,
checkIPAllowlist: true
}), }),
secretsController.updateSecretByNameRaw secretsController.updateSecretByNameRaw
); );
@@ -141,8 +133,8 @@ router.delete(
AUTH_MODE_JWT, AUTH_MODE_JWT,
AUTH_MODE_API_KEY, AUTH_MODE_API_KEY,
AUTH_MODE_SERVICE_TOKEN, AUTH_MODE_SERVICE_TOKEN,
AUTH_MODE_SERVICE_ACCOUNT, AUTH_MODE_SERVICE_ACCOUNT
], ]
}), }),
requireWorkspaceAuth({ requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER], acceptedRoles: [ADMIN, MEMBER],
@@ -151,6 +143,7 @@ router.delete(
requiredPermissions: [PERMISSION_WRITE_SECRETS], requiredPermissions: [PERMISSION_WRITE_SECRETS],
requireBlindIndicesEnabled: true, requireBlindIndicesEnabled: true,
requireE2EEOff: true, requireE2EEOff: true,
checkIPAllowlist: true
}), }),
secretsController.deleteSecretByNameRaw secretsController.deleteSecretByNameRaw
); );
@@ -166,8 +159,8 @@ router.get(
AUTH_MODE_JWT, AUTH_MODE_JWT,
AUTH_MODE_API_KEY, AUTH_MODE_API_KEY,
AUTH_MODE_SERVICE_TOKEN, AUTH_MODE_SERVICE_TOKEN,
AUTH_MODE_SERVICE_ACCOUNT, AUTH_MODE_SERVICE_ACCOUNT
], ]
}), }),
requireWorkspaceAuth({ requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER], acceptedRoles: [ADMIN, MEMBER],
@@ -176,6 +169,7 @@ router.get(
requiredPermissions: [PERMISSION_READ_SECRETS], requiredPermissions: [PERMISSION_READ_SECRETS],
requireBlindIndicesEnabled: true, requireBlindIndicesEnabled: true,
requireE2EEOff: false, requireE2EEOff: false,
checkIPAllowlist: true
}), }),
secretsController.getSecrets secretsController.getSecrets
); );
@@ -201,8 +195,8 @@ router.post(
AUTH_MODE_JWT, AUTH_MODE_JWT,
AUTH_MODE_API_KEY, AUTH_MODE_API_KEY,
AUTH_MODE_SERVICE_TOKEN, AUTH_MODE_SERVICE_TOKEN,
AUTH_MODE_SERVICE_ACCOUNT, AUTH_MODE_SERVICE_ACCOUNT
], ]
}), }),
requireWorkspaceAuth({ requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER], acceptedRoles: [ADMIN, MEMBER],
@@ -211,6 +205,7 @@ router.post(
requiredPermissions: [PERMISSION_WRITE_SECRETS], requiredPermissions: [PERMISSION_WRITE_SECRETS],
requireBlindIndicesEnabled: true, requireBlindIndicesEnabled: true,
requireE2EEOff: false, requireE2EEOff: false,
checkIPAllowlist: true
}), }),
secretsController.createSecret secretsController.createSecret
); );
@@ -228,8 +223,8 @@ router.get(
AUTH_MODE_JWT, AUTH_MODE_JWT,
AUTH_MODE_API_KEY, AUTH_MODE_API_KEY,
AUTH_MODE_SERVICE_TOKEN, AUTH_MODE_SERVICE_TOKEN,
AUTH_MODE_SERVICE_ACCOUNT, AUTH_MODE_SERVICE_ACCOUNT
], ]
}), }),
requireWorkspaceAuth({ requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER], acceptedRoles: [ADMIN, MEMBER],
@@ -237,6 +232,7 @@ router.get(
locationEnvironment: "query", locationEnvironment: "query",
requiredPermissions: [PERMISSION_READ_SECRETS], requiredPermissions: [PERMISSION_READ_SECRETS],
requireBlindIndicesEnabled: true, requireBlindIndicesEnabled: true,
checkIPAllowlist: true
}), }),
secretsController.getSecretByName secretsController.getSecretByName
); );
@@ -257,8 +253,8 @@ router.patch(
AUTH_MODE_JWT, AUTH_MODE_JWT,
AUTH_MODE_API_KEY, AUTH_MODE_API_KEY,
AUTH_MODE_SERVICE_TOKEN, AUTH_MODE_SERVICE_TOKEN,
AUTH_MODE_SERVICE_ACCOUNT, AUTH_MODE_SERVICE_ACCOUNT
], ]
}), }),
requireWorkspaceAuth({ requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER], acceptedRoles: [ADMIN, MEMBER],
@@ -267,6 +263,7 @@ router.patch(
requiredPermissions: [PERMISSION_WRITE_SECRETS], requiredPermissions: [PERMISSION_WRITE_SECRETS],
requireBlindIndicesEnabled: true, requireBlindIndicesEnabled: true,
requireE2EEOff: false, requireE2EEOff: false,
checkIPAllowlist: true
}), }),
secretsController.updateSecretByName secretsController.updateSecretByName
); );
@@ -284,8 +281,8 @@ router.delete(
AUTH_MODE_JWT, AUTH_MODE_JWT,
AUTH_MODE_API_KEY, AUTH_MODE_API_KEY,
AUTH_MODE_SERVICE_TOKEN, AUTH_MODE_SERVICE_TOKEN,
AUTH_MODE_SERVICE_ACCOUNT, AUTH_MODE_SERVICE_ACCOUNT
], ]
}), }),
requireWorkspaceAuth({ requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER], acceptedRoles: [ADMIN, MEMBER],
@@ -294,6 +291,7 @@ router.delete(
requiredPermissions: [PERMISSION_WRITE_SECRETS], requiredPermissions: [PERMISSION_WRITE_SECRETS],
requireBlindIndicesEnabled: true, requireBlindIndicesEnabled: true,
requireE2EEOff: false, requireE2EEOff: false,
checkIPAllowlist: true
}), }),
secretsController.deleteSecretByName secretsController.deleteSecretByName
); );
+2 -1
View File
@@ -21,7 +21,8 @@ router.post(
body("salt").exists().isString().trim().notEmpty(), body("salt").exists().isString().trim().notEmpty(),
body("verifier").exists().isString().trim().notEmpty(), body("verifier").exists().isString().trim().notEmpty(),
body("organizationName").exists().isString().trim().notEmpty(), body("organizationName").exists().isString().trim().notEmpty(),
body("providerAuthToken").isString().trim().optional({nullable: true}), body("providerAuthToken").isString().trim().optional({ nullable: true }),
body("attributionSource").optional().isString().trim(),
validateRequest, validateRequest,
signupController.completeAccountSignup, signupController.completeAccountSignup,
); );
+12
View File
@@ -0,0 +1,12 @@
import { Types } from "mongoose";
import { getSymmetricKeyHelper } from "../helpers/botOrg";
// TODO: DOCstrings
class BotOrgService {
static async getSymmetricKey(organizationId: Types.ObjectId) {
return await getSymmetricKeyHelper(organizationId);
}
}
export default BotOrgService;
@@ -0,0 +1,250 @@
import { Probot } from "probot";
import { exec } from "child_process";
import { mkdir, readFile, rm, writeFile } from "fs";
import { tmpdir } from "os";
import { join } from "path"
import GitRisks from "../models/gitRisks";
import GitAppOrganizationInstallation from "../models/gitAppOrganizationInstallation";
import MembershipOrg from "../models/membershipOrg";
import { ADMIN, OWNER } from "../variables";
import User from "../models/user";
import { sendMail } from "../helpers";
import TelemetryService from "./TelemetryService";
type SecretMatch = {
Description: string;
StartLine: number;
EndLine: number;
StartColumn: number;
EndColumn: number;
Match: string;
Secret: string;
File: string;
SymlinkFile: string;
Commit: string;
Entropy: number;
Author: string;
Email: string;
Date: string;
Message: string;
Tags: string[];
RuleID: string;
Fingerprint: string;
FingerPrintWithoutCommitId: string
};
export default async (app: Probot) => {
app.on("installation.deleted", async (context) => {
const { payload } = context;
const { installation, repositories } = payload;
if (installation.repository_selection == "all") {
await GitRisks.deleteMany({ installationId: installation.id })
await GitAppOrganizationInstallation.deleteOne({ installationId: installation.id })
} else {
if (repositories) {
for (const repository of repositories) {
await GitRisks.deleteMany({ repositoryId: repository.id })
}
}
}
})
app.on("push", async (context) => {
const { payload } = context;
const { commits, repository, installation, pusher } = payload;
const [owner, repo] = repository.full_name.split("/");
if (!commits || !repository || !installation || !pusher) {
return
}
const installationLinkToOrgExists = await GitAppOrganizationInstallation.findOne({ installationId: installation?.id }).lean()
if (!installationLinkToOrgExists) {
return
}
const allFindingsByFingerprint: { [key: string]: SecretMatch; } = {}
for (const commit of commits) {
for (const filepath of [...commit.added, ...commit.modified]) {
try {
const fileContentsResponse = await context.octokit.repos.getContent({
owner,
repo,
path: filepath,
});
const data: any = fileContentsResponse.data;
const fileContent = Buffer.from(data.content, "base64").toString();
const findings = await scanContentAndGetFindings(`\n${fileContent}`) // extra line to count lines correctly
for (const finding of findings) {
const fingerPrintWithCommitId = `${commit.id}:${filepath}:${finding.RuleID}:${finding.StartLine}`
const fingerPrintWithoutCommitId = `${filepath}:${finding.RuleID}:${finding.StartLine}`
finding.Fingerprint = fingerPrintWithCommitId
finding.FingerPrintWithoutCommitId = fingerPrintWithoutCommitId
finding.Commit = commit.id
finding.File = filepath
finding.Author = commit.author.name
finding.Email = commit?.author?.email ? commit?.author?.email : ""
allFindingsByFingerprint[fingerPrintWithCommitId] = finding
}
} catch (error) {
console.error(`Error fetching content for ${filepath}`, error); // eslint-disable-line
}
}
}
// change to update
for (const key in allFindingsByFingerprint) {
const risk = await GitRisks.findOneAndUpdate({ fingerprint: allFindingsByFingerprint[key].Fingerprint },
{
...convertKeysToLowercase(allFindingsByFingerprint[key]),
installationId: installation.id,
organization: installationLinkToOrgExists.organizationId,
repositoryFullName: repository.full_name,
repositoryId: repository.id
}, {
upsert: true
}).lean()
}
// get emails of admins
const adminsOfWork = await MembershipOrg.find({
organization: installationLinkToOrgExists.organizationId,
$or: [
{ role: OWNER },
{ role: ADMIN }
]
}).lean()
const userEmails = await User.find({
_id: {
$in: [adminsOfWork.map(orgMembership => orgMembership.user)]
}
}).select("email").lean()
const adminOrOwnerEmails = userEmails.map(userObject => userObject.email)
const usersToNotify = pusher?.email ? [pusher.email, ...adminOrOwnerEmails] : [...adminOrOwnerEmails]
if (Object.keys(allFindingsByFingerprint).length) {
await sendMail({
template: "secretLeakIncident.handlebars",
subjectLine: `Incident alert: leaked secrets found in Github repository ${repository.full_name}`,
recipients: usersToNotify,
substitutions: {
numberOfSecrets: Object.keys(allFindingsByFingerprint).length,
pusher_email: pusher.email,
pusher_name: pusher.name
}
});
}
const postHogClient = await TelemetryService.getPostHogClient();
if (postHogClient) {
postHogClient.capture({
event: "cloud secret scan",
distinctId: pusher.email,
properties: {
numberOfCommitsScanned: commits.length,
numberOfRisksFound: Object.keys(allFindingsByFingerprint).length,
}
});
}
});
};
async function scanContentAndGetFindings(textContent: string): Promise<SecretMatch[]> {
const tempFolder = await createTempFolder();
const filePath = join(tempFolder, "content.txt");
const findingsPath = join(tempFolder, "findings.json");
try {
await writeTextToFile(filePath, textContent);
await runInfisicalScan(filePath, findingsPath);
const findingsData = await readFindingsFile(findingsPath);
return JSON.parse(findingsData);
} finally {
await deleteTempFolder(tempFolder);
}
}
function createTempFolder(): Promise<string> {
return new Promise((resolve, reject) => {
const tempDir = tmpdir()
const tempFolderName = Math.random().toString(36).substring(2);
const tempFolderPath = join(tempDir, tempFolderName);
mkdir(tempFolderPath, (err: any) => {
if (err) {
reject(err);
} else {
resolve(tempFolderPath);
}
});
});
}
function writeTextToFile(filePath: string, content: string): Promise<void> {
return new Promise((resolve, reject) => {
writeFile(filePath, content, (err) => {
if (err) {
reject(err);
} else {
resolve();
}
});
});
}
function runInfisicalScan(inputPath: string, outputPath: string): Promise<void> {
return new Promise((resolve, reject) => {
const command = `cat "${inputPath}" | infisical scan --exit-code=77 --pipe -r "${outputPath}"`;
exec(command, (error) => {
if (error && error.code != 77) {
reject(error);
} else {
resolve();
}
});
});
}
function readFindingsFile(filePath: string): Promise<string> {
return new Promise((resolve, reject) => {
readFile(filePath, "utf8", (err, data) => {
if (err) {
reject(err);
} else {
resolve(data);
}
});
});
}
function deleteTempFolder(folderPath: string): Promise<void> {
return new Promise((resolve, reject) => {
rm(folderPath, { recursive: true }, (err) => {
if (err) {
reject(err);
} else {
resolve();
}
});
});
}
function convertKeysToLowercase<T>(obj: T): T {
const convertedObj = {} as T;
for (const key in obj) {
if (Object.prototype.hasOwnProperty.call(obj, key)) {
const lowercaseKey = key.charAt(0).toLowerCase() + key.slice(1);
convertedObj[lowercaseKey as keyof T] = obj[key];
}
}
return convertedObj;
}
@@ -0,0 +1,87 @@
import { Types } from "mongoose";
import Folder from "../models/folder";
import Secret, { ISecret } from "../models/secret";
import SecretImport from "../models/secretImports";
import { getFolderByPath } from "./FolderService";
type TSecretImportFid = { environment: string; folderId: string; secretPath: string };
export const getAllImportedSecrets = async (
workspaceId: string,
environment: string,
folderId = "root"
) => {
const secImports = await SecretImport.findOne({
workspace: workspaceId,
environment,
folderId
});
if (!secImports) return [];
if (secImports.imports.length === 0) return [];
const importedEnv: Record<string, boolean> = {}; // to get folders from all environment
secImports.imports.forEach((el) => (importedEnv[el.environment] = true));
const folders = await Folder.find({
workspace: workspaceId,
environment: { $in: Object.keys(importedEnv) }
});
const importedSecByFid: TSecretImportFid[] = [];
secImports.imports.forEach((el) => {
const folder = folders.find((fl) => fl.environment === el.environment);
if (folder) {
const secPathFolder = getFolderByPath(folder.nodes, el.secretPath);
if (secPathFolder)
importedSecByFid.push({
environment: el.environment,
folderId: secPathFolder.id,
secretPath: el.secretPath
});
} else {
if (el.secretPath === "/") {
// this happens when importing with a fresh env without any folders
importedSecByFid.push({ environment: el.environment, folderId: "root", secretPath: "/" });
}
}
});
if (importedSecByFid.length === 0) return [];
const secsGroupedByRef = await Secret.aggregate([
{
$match: {
workspace: new Types.ObjectId(workspaceId),
type: "shared"
}
},
{
$group: {
_id: {
environment: "$environment",
folderId: "$folder"
},
secrets: { $push: "$$ROOT" }
}
},
{
$match: {
$or: importedSecByFid.map(({ environment, folderId: fid }) => ({
"_id.environment": environment,
"_id.folderId": fid
}))
}
}
]);
// now let stitch together secrets.
const importedSecrets: Array<TSecretImportFid & { secrets: ISecret[] }> = [];
importedSecByFid.forEach(({ environment, folderId, secretPath }) => {
const secretsGrouped = secsGroupedByRef.find(
(el) => el._id.environment === environment && el._id.folderId === folderId
);
if (secretsGrouped) {
importedSecrets.push({ secretPath, folderId, environment, secrets: secretsGrouped.secrets });
}
});
return importedSecrets;
};
+93
View File
@@ -0,0 +1,93 @@
import axios from "axios";
import crypto from "crypto";
import { Types } from "mongoose";
import picomatch from "picomatch";
import { client, getRootEncryptionKey } from "../config";
import Webhook, { IWebhook } from "../models/webhooks";
export const triggerWebhookRequest = async (
{ url, encryptedSecretKey, iv, tag }: IWebhook,
payload: Record<string, unknown>
) => {
const headers: Record<string, string> = {};
payload["timestamp"] = Date.now();
if (encryptedSecretKey) {
const rootEncryptionKey = await getRootEncryptionKey();
const secretKey = client.decryptSymmetric(encryptedSecretKey, rootEncryptionKey, iv, tag);
const webhookSign = crypto
.createHmac("sha256", secretKey)
.update(JSON.stringify(payload))
.digest("hex");
headers["x-infisical-signature"] = `t=${payload["timestamp"]};${webhookSign}`;
}
const req = await axios.post(url, payload, { headers });
return req;
};
export const getWebhookPayload = (
eventName: string,
workspaceId: string,
environment: string,
secretPath?: string
) => ({
event: eventName,
project: {
workspaceId,
environment,
secretPath
}
});
export const triggerWebhook = async (
workspaceId: string,
environment: string,
secretPath: string
) => {
const webhooks = await Webhook.find({ workspace: workspaceId, environment, isDisabled: false });
// TODO(akhilmhdh): implement retry policy later, for that a cron job based approach is needed
// for exponential backoff
const toBeTriggeredHooks = webhooks.filter(({ secretPath: hookSecretPath }) =>
picomatch.isMatch(secretPath, hookSecretPath, { strictSlashes: false })
);
const webhooksTriggered = await Promise.allSettled(
toBeTriggeredHooks.map((hook) =>
triggerWebhookRequest(
hook,
getWebhookPayload("secrets.modified", workspaceId, environment, secretPath)
)
)
);
const successWebhooks: Types.ObjectId[] = [];
const failedWebhooks: Array<{ id: Types.ObjectId; error: string }> = [];
webhooksTriggered.forEach((data, index) => {
if (data.status === "rejected") {
failedWebhooks.push({ id: toBeTriggeredHooks[index]._id, error: data.reason.message });
return;
}
successWebhooks.push(toBeTriggeredHooks[index]._id);
});
// dont remove the workspaceid and environment filter. its used to reduce the dataset before $in check
await Webhook.bulkWrite([
{
updateMany: {
filter: { workspace: workspaceId, environment, _id: { $in: successWebhooks } },
update: { lastStatus: "success", lastRunErrorMessage: null }
}
},
...failedWebhooks.map(({ id, error }) => ({
updateOne: {
filter: {
workspace: workspaceId,
environment,
_id: id
},
update: {
lastStatus: "failed",
lastRunErrorMessage: error
}
}
}))
]);
};
+12 -8
View File
@@ -2,17 +2,21 @@ import DatabaseService from "./DatabaseService";
// import { logTelemetryMessage, getPostHogClient } from './TelemetryService'; // import { logTelemetryMessage, getPostHogClient } from './TelemetryService';
import TelemetryService from "./TelemetryService"; import TelemetryService from "./TelemetryService";
import BotService from "./BotService"; import BotService from "./BotService";
import BotOrgService from "./BotOrgService";
import EventService from "./EventService"; import EventService from "./EventService";
import IntegrationService from "./IntegrationService"; import IntegrationService from "./IntegrationService";
import TokenService from "./TokenService"; import TokenService from "./TokenService";
import SecretService from "./SecretService"; import SecretService from "./SecretService";
import GithubSecretScanningService from "./GithubSecretScanningService"
export { export {
TelemetryService, TelemetryService,
DatabaseService, DatabaseService,
BotService, BotService,
EventService, BotOrgService,
IntegrationService, EventService,
TokenService, IntegrationService,
SecretService, TokenService,
} SecretService,
GithubSecretScanningService
}
+13 -3
View File
@@ -2,9 +2,10 @@ import nodemailer from "nodemailer";
import { import {
SMTP_HOST_GMAIL, SMTP_HOST_GMAIL,
SMTP_HOST_MAILGUN, SMTP_HOST_MAILGUN,
SMTP_HOST_OFFICE365,
SMTP_HOST_SENDGRID, SMTP_HOST_SENDGRID,
SMTP_HOST_SOCKETLABS, SMTP_HOST_SOCKETLABS,
SMTP_HOST_ZOHOMAIL, SMTP_HOST_ZOHOMAIL
} from "../variables"; } from "../variables";
import SMTPConnection from "nodemailer/lib/smtp-connection"; import SMTPConnection from "nodemailer/lib/smtp-connection";
import * as Sentry from "@sentry/node"; import * as Sentry from "@sentry/node";
@@ -15,6 +16,7 @@ import {
getSmtpSecure, getSmtpSecure,
getSmtpUsername, getSmtpUsername,
} from "../config"; } from "../config";
import { getLogger } from "../utils/logger";
export const initSmtp = async () => { export const initSmtp = async () => {
const mailOpts: SMTPConnection.Options = { const mailOpts: SMTPConnection.Options = {
@@ -58,6 +60,12 @@ export const initSmtp = async () => {
ciphers: "TLSv1.2", ciphers: "TLSv1.2",
} }
break; break;
case SMTP_HOST_OFFICE365:
mailOpts.requireTLS = true;
mailOpts.tls = {
ciphers: "TLSv1.2"
}
break;
default: default:
if ((await getSmtpHost()).includes("amazonaws.com")) { if ((await getSmtpHost()).includes("amazonaws.com")) {
mailOpts.tls = { mailOpts.tls = {
@@ -73,10 +81,12 @@ export const initSmtp = async () => {
const transporter = nodemailer.createTransport(mailOpts); const transporter = nodemailer.createTransport(mailOpts);
transporter transporter
.verify() .verify()
.then((err) => { .then(async () => {
Sentry.setUser(null); Sentry.setUser(null);
Sentry.captureMessage("SMTP - Successfully connected"); Sentry.captureMessage("SMTP - Successfully connected");
console.log("SMTP - Successfully connected") (await getLogger("backend-main")).info(
"SMTP - Successfully connected"
);
}) })
.catch(async (err) => { .catch(async (err) => {
Sentry.setUser(null); Sentry.setUser(null);
@@ -0,0 +1,25 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="x-ua-compatible" content="ie=edge">
<title>Incident alert: secret leaked</title>
</head>
<body>
<h3>Infisical has uncovered {{numberOfSecrets}} secret(s) from your recent push</h3>
<p><a href="https://app.infisical.com/secret-scanning"><strong>View leaked secrets</strong></a></p>
<p>You are receiving this notification because one or more secret leaks have been detected in a recent commit pushed
by {{pusher_name}} ({{pusher_email}}). If
these are test secrets, please add `infisical-scan:ignore` at the end of the line containing the secret as comment
in the given programming. This will prevent future notifications from being sent out for those secret(s).</p>
<p>If these are production secrets, please rotate them immediately.</p>
<p>Once you have taken action, be sure to update the status of the risk in your <a
href="https://app.infisical.com/">Infisical
dashboard</a>.</p>
</body>
</html>

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