mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-16 13:56:43 +00:00
Fix merge conflicts
This commit is contained in:
@@ -10,7 +10,7 @@ export const getEncryptionKey = async () => {
|
||||
return secretValue === "" ? undefined : secretValue;
|
||||
}
|
||||
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;
|
||||
}
|
||||
export const getInviteOnlySignup = async () => (await client.getSecret("INVITE_ONLY_SIGNUP")).secretValue === "true"
|
||||
@@ -57,6 +57,11 @@ export const getSmtpPassword = async () => (await client.getSecret("SMTP_PASSWOR
|
||||
export const getSmtpFromAddress = async () => (await client.getSecret("SMTP_FROM_ADDRESS")).secretValue;
|
||||
export const getSmtpFromName = async () => (await client.getSecret("SMTP_FROM_NAME")).secretValue || "Infisical";
|
||||
|
||||
export const getSecretScanningWebhookProxy = async () => (await client.getSecret("SECRET_SCANNING_WEBHOOK_PROXY")).secretValue;
|
||||
export const getSecretScanningWebhookSecret = async () => (await client.getSecret("SECRET_SCANNING_WEBHOOK_SECRET")).secretValue;
|
||||
export const getSecretScanningGitAppId = async () => (await client.getSecret("SECRET_SCANNING_GIT_APP_ID")).secretValue;
|
||||
export const getSecretScanningPrivateKey = async () => (await client.getSecret("SECRET_SCANNING_PRIVATE_KEY")).secretValue;
|
||||
|
||||
export const getLicenseKey = async () => {
|
||||
const secretValue = (await client.getSecret("LICENSE_KEY")).secretValue;
|
||||
return secretValue === "" ? undefined : secretValue;
|
||||
@@ -82,4 +87,4 @@ export const getHttpsEnabled = async () => {
|
||||
}
|
||||
|
||||
return (await client.getSecret("HTTPS_ENABLED")).secretValue === "true" && true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,21 +13,27 @@ import * as signupController from "./signupController";
|
||||
import * as userActionController from "./userActionController";
|
||||
import * as userController from "./userController";
|
||||
import * as workspaceController from "./workspaceController";
|
||||
import * as secretScanningController from "./secretScanningController";
|
||||
import * as webhookController from "./webhookController";
|
||||
import * as secretImportController from "./secretImportController";
|
||||
|
||||
export {
|
||||
authController,
|
||||
botController,
|
||||
integrationAuthController,
|
||||
integrationController,
|
||||
keyController,
|
||||
membershipController,
|
||||
membershipOrgController,
|
||||
organizationController,
|
||||
passwordController,
|
||||
secretController,
|
||||
serviceTokenController,
|
||||
signupController,
|
||||
userActionController,
|
||||
userController,
|
||||
workspaceController,
|
||||
authController,
|
||||
botController,
|
||||
integrationAuthController,
|
||||
integrationController,
|
||||
keyController,
|
||||
membershipController,
|
||||
membershipOrgController,
|
||||
organizationController,
|
||||
passwordController,
|
||||
secretController,
|
||||
serviceTokenController,
|
||||
signupController,
|
||||
userActionController,
|
||||
userController,
|
||||
workspaceController,
|
||||
secretScanningController,
|
||||
webhookController,
|
||||
secretImportController
|
||||
};
|
||||
|
||||
@@ -2,7 +2,7 @@ import { Request, Response } from "express";
|
||||
import { Types } from "mongoose";
|
||||
import { Integration } from "../../models";
|
||||
import { EventService } from "../../services";
|
||||
import { eventPushSecrets } from "../../events";
|
||||
import { eventPushSecrets, eventStartIntegration } from "../../events";
|
||||
import Folder from "../../models/folder";
|
||||
import { getFolderByPath } from "../../services/FolderService";
|
||||
import { BadRequestError } from "../../utils/errors";
|
||||
@@ -27,19 +27,19 @@ export const createIntegration = async (req: Request, res: Response) => {
|
||||
owner,
|
||||
path,
|
||||
region,
|
||||
secretPath,
|
||||
secretPath
|
||||
} = req.body;
|
||||
|
||||
const folders = await Folder.findOne({
|
||||
workspace: req.integrationAuth.workspace._id,
|
||||
environment: sourceEnvironment,
|
||||
environment: sourceEnvironment
|
||||
});
|
||||
|
||||
if (folders) {
|
||||
const folder = getFolderByPath(folders.nodes, secretPath);
|
||||
if (!folder) {
|
||||
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,
|
||||
secretPath,
|
||||
integration: req.integrationAuth.integration,
|
||||
integrationAuth: new Types.ObjectId(integrationAuthId),
|
||||
integrationAuth: new Types.ObjectId(integrationAuthId)
|
||||
}).save();
|
||||
|
||||
if (integration) {
|
||||
// trigger event - push secrets
|
||||
EventService.handleEvent({
|
||||
event: eventPushSecrets({
|
||||
event: eventStartIntegration({
|
||||
workspaceId: integration.workspace,
|
||||
environment: sourceEnvironment,
|
||||
}),
|
||||
environment: sourceEnvironment
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
return res.status(200).send({
|
||||
integration,
|
||||
integration
|
||||
});
|
||||
};
|
||||
|
||||
@@ -97,26 +97,26 @@ export const updateIntegration = async (req: Request, res: Response) => {
|
||||
appId,
|
||||
targetEnvironment,
|
||||
owner, // github-specific integration param
|
||||
secretPath,
|
||||
secretPath
|
||||
} = req.body;
|
||||
|
||||
const folders = await Folder.findOne({
|
||||
workspace: req.integration.workspace,
|
||||
environment,
|
||||
environment
|
||||
});
|
||||
|
||||
if (folders) {
|
||||
const folder = getFolderByPath(folders.nodes, secretPath);
|
||||
if (!folder) {
|
||||
throw BadRequestError({
|
||||
message: "Path for service token does not exist",
|
||||
message: "Path for service token does not exist"
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const integration = await Integration.findOneAndUpdate(
|
||||
{
|
||||
_id: req.integration._id,
|
||||
_id: req.integration._id
|
||||
},
|
||||
{
|
||||
environment,
|
||||
@@ -125,25 +125,25 @@ export const updateIntegration = async (req: Request, res: Response) => {
|
||||
appId,
|
||||
targetEnvironment,
|
||||
owner,
|
||||
secretPath,
|
||||
secretPath
|
||||
},
|
||||
{
|
||||
new: true,
|
||||
new: true
|
||||
}
|
||||
);
|
||||
|
||||
if (integration) {
|
||||
// trigger event - push secrets
|
||||
EventService.handleEvent({
|
||||
event: eventPushSecrets({
|
||||
event: eventStartIntegration({
|
||||
workspaceId: integration.workspace,
|
||||
environment,
|
||||
}),
|
||||
environment
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
return res.status(200).send({
|
||||
integration,
|
||||
integration
|
||||
});
|
||||
};
|
||||
|
||||
@@ -158,12 +158,12 @@ export const deleteIntegration = async (req: Request, res: Response) => {
|
||||
const { integrationId } = req.params;
|
||||
|
||||
const integration = await Integration.findOneAndDelete({
|
||||
_id: integrationId,
|
||||
_id: integrationId
|
||||
});
|
||||
|
||||
if (!integration) throw new Error("Failed to find integration");
|
||||
|
||||
return res.status(200).send({
|
||||
integration,
|
||||
integration
|
||||
});
|
||||
};
|
||||
|
||||
@@ -80,7 +80,8 @@ export const pushSecrets = async (req: Request, res: Response) => {
|
||||
EventService.handleEvent({
|
||||
event: eventPushSecrets({
|
||||
workspaceId: new Types.ObjectId(workspaceId),
|
||||
environment
|
||||
environment,
|
||||
secretPath: "/"
|
||||
})
|
||||
});
|
||||
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
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
|
||||
});
|
||||
};
|
||||
@@ -1,72 +0,0 @@
|
||||
import { Request, Response } from "express";
|
||||
import crypto from "crypto";
|
||||
import bcrypt from "bcrypt";
|
||||
import { APIKeyData } from "../../models";
|
||||
import { getSaltRounds } from "../../config";
|
||||
|
||||
/**
|
||||
* Return API key data for user with id [req.user_id]
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
export const getAPIKeyData = async (req: Request, res: Response) => {
|
||||
const apiKeyData = await APIKeyData.find({
|
||||
user: req.user._id,
|
||||
});
|
||||
|
||||
return res.status(200).send({
|
||||
apiKeyData,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Create new API key data for user with id [req.user._id]
|
||||
* @param req
|
||||
* @param res
|
||||
*/
|
||||
export const createAPIKeyData = async (req: Request, res: Response) => {
|
||||
const { name, expiresIn } = req.body;
|
||||
|
||||
const secret = crypto.randomBytes(16).toString("hex");
|
||||
const secretHash = await bcrypt.hash(secret, await getSaltRounds());
|
||||
|
||||
const expiresAt = new Date();
|
||||
expiresAt.setSeconds(expiresAt.getSeconds() + expiresIn);
|
||||
|
||||
let apiKeyData = await new APIKeyData({
|
||||
name,
|
||||
lastUsed: new Date(),
|
||||
expiresAt,
|
||||
user: req.user._id,
|
||||
secretHash,
|
||||
}).save();
|
||||
|
||||
// return api key data without sensitive data
|
||||
// FIX: fix this any
|
||||
apiKeyData = (await APIKeyData.findById(apiKeyData._id)) as any;
|
||||
|
||||
if (!apiKeyData) throw new Error("Failed to find API key data");
|
||||
|
||||
const apiKey = `ak.${apiKeyData._id.toString()}.${secret}`;
|
||||
|
||||
return res.status(200).send({
|
||||
apiKey,
|
||||
apiKeyData,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Delete API key data with id [apiKeyDataId].
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
export const deleteAPIKeyData = async (req: Request, res: Response) => {
|
||||
const { apiKeyDataId } = req.params;
|
||||
const apiKeyData = await APIKeyData.findByIdAndDelete(apiKeyDataId);
|
||||
|
||||
return res.status(200).send({
|
||||
apiKeyData,
|
||||
});
|
||||
};
|
||||
@@ -4,7 +4,6 @@ import * as usersController from "./usersController";
|
||||
import * as organizationsController from "./organizationsController";
|
||||
import * as workspaceController from "./workspaceController";
|
||||
import * as serviceTokenDataController from "./serviceTokenDataController";
|
||||
import * as apiKeyDataController from "./apiKeyDataController";
|
||||
import * as secretController from "./secretController";
|
||||
import * as secretsController from "./secretsController";
|
||||
import * as serviceAccountsController from "./serviceAccountsController";
|
||||
@@ -18,7 +17,6 @@ export {
|
||||
organizationsController,
|
||||
workspaceController,
|
||||
serviceTokenDataController,
|
||||
apiKeyDataController,
|
||||
secretController,
|
||||
secretsController,
|
||||
serviceAccountsController,
|
||||
|
||||
@@ -290,7 +290,7 @@ export const updateSecret = async (req: Request, res: Response) => {
|
||||
const { workspaceId, environmentName } = req.params;
|
||||
const secretModificationsRequested: ModifySecretRequestBody = req.body.secret;
|
||||
|
||||
const secretIdUserCanModify = await Secret.findOne({ workspace: workspaceId, environment: environmentName }, { _id: 1 });
|
||||
await Secret.findOne({ workspace: workspaceId, environment: environmentName }, { _id: 1 });
|
||||
|
||||
const sanitizedSecret: SanitizedSecretModify = {
|
||||
secretKeyCiphertext: secretModificationsRequested.secretKeyCiphertext,
|
||||
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
ACTION_UPDATE_SECRETS,
|
||||
ALGORITHM_AES_256_GCM,
|
||||
ENCODING_SCHEME_UTF8,
|
||||
SECRET_PERSONAL,
|
||||
SECRET_PERSONAL
|
||||
} from "../../variables";
|
||||
import { BadRequestError, UnauthorizedRequestError } from "../../utils/errors";
|
||||
import { EventService } from "../../services";
|
||||
@@ -21,7 +21,7 @@ import { PERMISSION_WRITE_SECRETS } from "../../variables";
|
||||
import {
|
||||
userHasNoAbility,
|
||||
userHasWorkspaceAccess,
|
||||
userHasWriteOnlyAbility,
|
||||
userHasWriteOnlyAbility
|
||||
} from "../../ee/helpers/checkMembershipPermissions";
|
||||
import Tag from "../../models/tag";
|
||||
import _ from "lodash";
|
||||
@@ -31,7 +31,11 @@ import {
|
||||
getFolderByPath,
|
||||
getFolderIdFromServiceToken,
|
||||
searchByFolderId,
|
||||
searchByFolderIdWithDir
|
||||
} 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
|
||||
@@ -46,14 +50,13 @@ export const batchSecrets = async (req: Request, res: Response) => {
|
||||
const {
|
||||
workspaceId,
|
||||
environment,
|
||||
requests,
|
||||
secretPath,
|
||||
requests
|
||||
}: {
|
||||
workspaceId: string;
|
||||
environment: string;
|
||||
requests: BatchSecretRequest[];
|
||||
secretPath: string;
|
||||
} = req.body;
|
||||
let secretPath = req.body.secretPath as string;
|
||||
let folderId = req.body.folderId as string;
|
||||
|
||||
const createSecrets: BatchSecret[] = [];
|
||||
@@ -63,31 +66,31 @@ export const batchSecrets = async (req: Request, res: Response) => {
|
||||
|
||||
// get secret blind index salt
|
||||
const salt = await SecretService.getSecretBlindIndexSalt({
|
||||
workspaceId: new Types.ObjectId(workspaceId),
|
||||
workspaceId: new Types.ObjectId(workspaceId)
|
||||
});
|
||||
|
||||
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) {
|
||||
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
|
||||
// this is to avoid giving folderid when service tokens are used
|
||||
if (
|
||||
(!secretPath && folderId !== "root") ||
|
||||
(secretPath && secretPath !== serviceTkScopedSecretPath)
|
||||
) {
|
||||
if ((!secretPath && folderId !== "root") || (secretPath && !isValidScopeAccess)) {
|
||||
throw UnauthorizedRequestError({ message: "Folder Permission Denied" });
|
||||
}
|
||||
}
|
||||
|
||||
if (secretPath) {
|
||||
folderId = await getFolderIdFromServiceToken(
|
||||
workspaceId,
|
||||
environment,
|
||||
secretPath
|
||||
folderId = await getFolderIdFromServiceToken(workspaceId, environment, secretPath);
|
||||
}
|
||||
|
||||
if (folders && folderId !== "root") {
|
||||
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 = "";
|
||||
switch (request.method) {
|
||||
case "POST":
|
||||
secretBlindIndex = await SecretService.generateSecretBlindIndexWithSalt(
|
||||
{
|
||||
secretName: request.secret.secretName,
|
||||
salt,
|
||||
}
|
||||
);
|
||||
secretBlindIndex = await SecretService.generateSecretBlindIndexWithSalt({
|
||||
secretName: request.secret.secretName,
|
||||
salt
|
||||
});
|
||||
|
||||
createSecrets.push({
|
||||
...request.secret,
|
||||
@@ -113,16 +114,14 @@ export const batchSecrets = async (req: Request, res: Response) => {
|
||||
folder: folderId,
|
||||
secretBlindIndex,
|
||||
algorithm: ALGORITHM_AES_256_GCM,
|
||||
keyEncoding: ENCODING_SCHEME_UTF8,
|
||||
keyEncoding: ENCODING_SCHEME_UTF8
|
||||
});
|
||||
break;
|
||||
case "PATCH":
|
||||
secretBlindIndex = await SecretService.generateSecretBlindIndexWithSalt(
|
||||
{
|
||||
secretName: request.secret.secretName,
|
||||
salt,
|
||||
}
|
||||
);
|
||||
secretBlindIndex = await SecretService.generateSecretBlindIndexWithSalt({
|
||||
secretName: request.secret.secretName,
|
||||
salt
|
||||
});
|
||||
|
||||
updateSecrets.push({
|
||||
...request.secret,
|
||||
@@ -130,7 +129,7 @@ export const batchSecrets = async (req: Request, res: Response) => {
|
||||
secretBlindIndex,
|
||||
folder: folderId,
|
||||
algorithm: ALGORITHM_AES_256_GCM,
|
||||
keyEncoding: ENCODING_SCHEME_UTF8,
|
||||
keyEncoding: ENCODING_SCHEME_UTF8
|
||||
});
|
||||
break;
|
||||
case "DELETE":
|
||||
@@ -150,9 +149,9 @@ export const batchSecrets = async (req: Request, res: Response) => {
|
||||
...n._doc,
|
||||
_id: new Types.ObjectId(),
|
||||
secret: n._id,
|
||||
isDeleted: false,
|
||||
isDeleted: false
|
||||
};
|
||||
}),
|
||||
})
|
||||
});
|
||||
|
||||
const addAction = (await EELogService.createAction({
|
||||
@@ -161,7 +160,7 @@ export const batchSecrets = async (req: Request, res: Response) => {
|
||||
serviceAccountId: req.serviceAccount?._id,
|
||||
serviceTokenDataId: req.serviceTokenData?._id,
|
||||
workspaceId: new Types.ObjectId(workspaceId),
|
||||
secretIds: createdSecrets.map((n) => n._id),
|
||||
secretIds: createdSecrets.map((n) => n._id)
|
||||
})) as IAction;
|
||||
actions.push(addAction);
|
||||
|
||||
@@ -175,8 +174,8 @@ export const batchSecrets = async (req: Request, res: Response) => {
|
||||
workspaceId,
|
||||
folderId,
|
||||
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(
|
||||
(obj: any, secret: ISecret) => ({
|
||||
...obj,
|
||||
[secret._id.toString()]: secret,
|
||||
[secret._id.toString()]: secret
|
||||
}),
|
||||
{}
|
||||
);
|
||||
@@ -204,16 +203,16 @@ export const batchSecrets = async (req: Request, res: Response) => {
|
||||
updateOne: {
|
||||
filter: {
|
||||
_id: new Types.ObjectId(u._id),
|
||||
workspace: new Types.ObjectId(workspaceId),
|
||||
workspace: new Types.ObjectId(workspaceId)
|
||||
},
|
||||
update: {
|
||||
$inc: {
|
||||
version: 1,
|
||||
version: 1
|
||||
},
|
||||
...u,
|
||||
_id: new Types.ObjectId(u._id),
|
||||
},
|
||||
},
|
||||
_id: new Types.ObjectId(u._id)
|
||||
}
|
||||
}
|
||||
}));
|
||||
|
||||
await Secret.bulkWrite(updateOperations);
|
||||
@@ -240,25 +239,25 @@ export const batchSecrets = async (req: Request, res: Response) => {
|
||||
algorithm: ALGORITHM_AES_256_GCM,
|
||||
keyEncoding: ENCODING_SCHEME_UTF8,
|
||||
tags: u.tags,
|
||||
folder: u.folder,
|
||||
folder: u.folder
|
||||
})
|
||||
);
|
||||
|
||||
await EESecretService.addSecretVersions({
|
||||
secretVersions,
|
||||
secretVersions
|
||||
});
|
||||
|
||||
updatedSecrets = await Secret.find({
|
||||
_id: {
|
||||
$in: updateSecrets.map((u) => new Types.ObjectId(u._id)),
|
||||
},
|
||||
$in: updateSecrets.map((u) => new Types.ObjectId(u._id))
|
||||
}
|
||||
});
|
||||
|
||||
const updateAction = (await EELogService.createAction({
|
||||
name: ACTION_UPDATE_SECRETS,
|
||||
userId: req.user._id,
|
||||
workspaceId: new Types.ObjectId(workspaceId),
|
||||
secretIds: updatedSecrets.map((u) => u._id),
|
||||
secretIds: updatedSecrets.map((u) => u._id)
|
||||
})) as IAction;
|
||||
actions.push(updateAction);
|
||||
|
||||
@@ -272,8 +271,8 @@ export const batchSecrets = async (req: Request, res: Response) => {
|
||||
workspaceId,
|
||||
folderId,
|
||||
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) {
|
||||
await Secret.deleteMany({
|
||||
_id: {
|
||||
$in: deleteSecrets,
|
||||
},
|
||||
$in: deleteSecrets
|
||||
}
|
||||
});
|
||||
|
||||
await EESecretService.markDeletedSecretVersions({
|
||||
secretIds: deleteSecrets,
|
||||
secretIds: deleteSecrets
|
||||
});
|
||||
|
||||
const deleteAction = (await EELogService.createAction({
|
||||
name: ACTION_DELETE_SECRETS,
|
||||
userId: req.user._id,
|
||||
workspaceId: new Types.ObjectId(workspaceId),
|
||||
secretIds: deleteSecrets,
|
||||
secretIds: deleteSecrets
|
||||
})) as IAction;
|
||||
actions.push(deleteAction);
|
||||
|
||||
@@ -307,8 +306,8 @@ export const batchSecrets = async (req: Request, res: Response) => {
|
||||
environment,
|
||||
workspaceId,
|
||||
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),
|
||||
actions,
|
||||
channel,
|
||||
ipAddress: req.realIP,
|
||||
ipAddress: req.realIP
|
||||
});
|
||||
}
|
||||
|
||||
@@ -328,14 +327,17 @@ export const batchSecrets = async (req: Request, res: Response) => {
|
||||
await EventService.handleEvent({
|
||||
event: eventPushSecrets({
|
||||
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
|
||||
await EESecretService.takeSecretSnapshot({
|
||||
workspaceId: new Types.ObjectId(workspaceId),
|
||||
environment,
|
||||
folderId,
|
||||
folderId
|
||||
});
|
||||
|
||||
const resObj: { [key: string]: ISecret[] | string[] } = {};
|
||||
@@ -418,7 +420,7 @@ export const createSecrets = async (req: Request, res: Response) => {
|
||||
const {
|
||||
workspaceId,
|
||||
environment,
|
||||
secretPath,
|
||||
secretPath
|
||||
}: {
|
||||
workspaceId: string;
|
||||
environment: string;
|
||||
@@ -435,8 +437,7 @@ export const createSecrets = async (req: Request, res: Response) => {
|
||||
);
|
||||
if (!hasAccess) {
|
||||
throw UnauthorizedRequestError({
|
||||
message:
|
||||
"You do not have the necessary permission(s) perform this action",
|
||||
message: "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
|
||||
listOfSecretsToCreate = [req.body.secrets];
|
||||
}
|
||||
|
||||
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
|
||||
// this is to avoid giving folderid when service tokens are used
|
||||
if (
|
||||
(!secretPath && folderId !== "root") ||
|
||||
(secretPath && secretPath !== serviceTkScopedSecretPath)
|
||||
) {
|
||||
if ((!secretPath && folderId !== "root") || (secretPath && !isValidScopeAccess)) {
|
||||
throw UnauthorizedRequestError({ message: "Folder Permission Denied" });
|
||||
}
|
||||
}
|
||||
if (secretPath) {
|
||||
folderId = await getFolderIdFromServiceToken(
|
||||
workspaceId,
|
||||
environment,
|
||||
secretPath
|
||||
);
|
||||
folderId = await getFolderIdFromServiceToken(workspaceId, environment, secretPath);
|
||||
}
|
||||
|
||||
// get secret blind index salt
|
||||
const salt = await SecretService.getSecretBlindIndexSalt({
|
||||
workspaceId: new Types.ObjectId(workspaceId),
|
||||
workspaceId: new Types.ObjectId(workspaceId)
|
||||
});
|
||||
|
||||
type secretsToCreateType = {
|
||||
@@ -502,15 +502,14 @@ export const createSecrets = async (req: Request, res: Response) => {
|
||||
secretCommentCiphertext,
|
||||
secretCommentIV,
|
||||
secretCommentTag,
|
||||
tags,
|
||||
tags
|
||||
}: secretsToCreateType) => {
|
||||
let secretBlindIndex;
|
||||
if (secretName) {
|
||||
secretBlindIndex =
|
||||
await SecretService.generateSecretBlindIndexWithSalt({
|
||||
secretName,
|
||||
salt,
|
||||
});
|
||||
secretBlindIndex = await SecretService.generateSecretBlindIndexWithSalt({
|
||||
secretName,
|
||||
salt
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -532,22 +531,24 @@ export const createSecrets = async (req: Request, res: Response) => {
|
||||
secretCommentTag,
|
||||
algorithm: ALGORITHM_AES_256_GCM,
|
||||
keyEncoding: ENCODING_SCHEME_UTF8,
|
||||
tags,
|
||||
tags
|
||||
};
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
const newlyCreatedSecrets: ISecret[] = (
|
||||
await Secret.insertMany(secretsToInsert)
|
||||
).map((insertedSecret) => insertedSecret.toObject());
|
||||
const newlyCreatedSecrets: ISecret[] = (await Secret.insertMany(secretsToInsert)).map(
|
||||
(insertedSecret) => insertedSecret.toObject()
|
||||
);
|
||||
|
||||
setTimeout(async () => {
|
||||
// trigger event - push secrets
|
||||
await EventService.handleEvent({
|
||||
event: eventPushSecrets({
|
||||
workspaceId: new Types.ObjectId(workspaceId),
|
||||
}),
|
||||
environment,
|
||||
secretPath: secretPath || "/"
|
||||
})
|
||||
});
|
||||
}, 5000);
|
||||
|
||||
@@ -567,7 +568,7 @@ export const createSecrets = async (req: Request, res: Response) => {
|
||||
secretKeyTag,
|
||||
secretValueCiphertext,
|
||||
secretValueIV,
|
||||
secretValueTag,
|
||||
secretValueTag
|
||||
}) =>
|
||||
new SecretVersion({
|
||||
secret: _id,
|
||||
@@ -586,9 +587,9 @@ export const createSecrets = async (req: Request, res: Response) => {
|
||||
secretValueTag,
|
||||
folder: folderId,
|
||||
algorithm: ALGORITHM_AES_256_GCM,
|
||||
keyEncoding: ENCODING_SCHEME_UTF8,
|
||||
keyEncoding: ENCODING_SCHEME_UTF8
|
||||
})
|
||||
),
|
||||
)
|
||||
});
|
||||
|
||||
const addAction = await EELogService.createAction({
|
||||
@@ -597,7 +598,7 @@ export const createSecrets = async (req: Request, res: Response) => {
|
||||
serviceAccountId: req.serviceAccount?._id,
|
||||
serviceTokenDataId: req.serviceTokenData?._id,
|
||||
workspaceId: new Types.ObjectId(workspaceId),
|
||||
secretIds: newlyCreatedSecrets.map((n) => n._id),
|
||||
secretIds: newlyCreatedSecrets.map((n) => n._id)
|
||||
});
|
||||
|
||||
// (EE) create (audit) log
|
||||
@@ -609,14 +610,14 @@ export const createSecrets = async (req: Request, res: Response) => {
|
||||
workspaceId: new Types.ObjectId(workspaceId),
|
||||
actions: [addAction],
|
||||
channel,
|
||||
ipAddress: req.realIP,
|
||||
ipAddress: req.realIP
|
||||
}));
|
||||
|
||||
// (EE) take a secret snapshot
|
||||
await EESecretService.takeSecretSnapshot({
|
||||
workspaceId: new Types.ObjectId(workspaceId),
|
||||
environment,
|
||||
folderId,
|
||||
folderId
|
||||
});
|
||||
|
||||
const postHogClient = await TelemetryService.getPostHogClient();
|
||||
@@ -624,7 +625,7 @@ export const createSecrets = async (req: Request, res: Response) => {
|
||||
postHogClient.capture({
|
||||
event: "secrets added",
|
||||
distinctId: await TelemetryService.getDistinctId({
|
||||
authData: req.authData,
|
||||
authData: req.authData
|
||||
}),
|
||||
properties: {
|
||||
numberOfSecrets: listOfSecretsToCreate.length,
|
||||
@@ -632,13 +633,13 @@ export const createSecrets = async (req: Request, res: Response) => {
|
||||
workspaceId,
|
||||
channel: channel,
|
||||
folderId,
|
||||
userAgent: req.headers?.["user-agent"],
|
||||
},
|
||||
userAgent: req.headers?.["user-agent"]
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
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;
|
||||
const workspaceId = req.query.workspaceId as string;
|
||||
const environment = req.query.environment as string;
|
||||
|
||||
const folders = await Folder.findOne({ workspace: workspaceId, environment });
|
||||
if (
|
||||
(!folders && folderId && folderId !== "root") ||
|
||||
(!folders && secretPath)
|
||||
) {
|
||||
if ((!folders && folderId && folderId !== "root") || (!folders && secretPath)) {
|
||||
res.send({ secrets: [] });
|
||||
return;
|
||||
}
|
||||
@@ -712,13 +710,15 @@ export const getSecrets = async (req: Request, res: Response) => {
|
||||
}
|
||||
|
||||
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
|
||||
// this is to avoid giving folderid when service tokens are used
|
||||
if (
|
||||
(!secretPath && folderId !== "root") ||
|
||||
(secretPath && secretPath !== serviceTkScopedSecretPath)
|
||||
) {
|
||||
if ((!secretPath && folderId !== "root") || (secretPath && !isValidScopeAccess)) {
|
||||
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
|
||||
let tagIds = [];
|
||||
const tagNamesList =
|
||||
typeof tagSlugs === "string" && tagSlugs !== "" ? tagSlugs.split(",") : [];
|
||||
const tagNamesList = typeof tagSlugs === "string" && tagSlugs !== "" ? tagSlugs.split(",") : [];
|
||||
if (tagNamesList != undefined && tagNamesList.length != 0) {
|
||||
const workspaceFromDB = await Tag.find({ workspace: workspaceId });
|
||||
tagIds = _.map(tagNamesList, (tagName: string) => {
|
||||
@@ -762,8 +761,7 @@ export const getSecrets = async (req: Request, res: Response) => {
|
||||
);
|
||||
if (hasNoAccess) {
|
||||
throw UnauthorizedRequestError({
|
||||
message:
|
||||
"You do not have the necessary permission(s) perform this action",
|
||||
message: "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,
|
||||
$or: [
|
||||
{ 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) {
|
||||
@@ -801,8 +799,8 @@ export const getSecrets = async (req: Request, res: Response) => {
|
||||
environment,
|
||||
$or: [
|
||||
{ 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) {
|
||||
@@ -820,7 +818,7 @@ export const getSecrets = async (req: Request, res: Response) => {
|
||||
workspace: workspaceId,
|
||||
environment,
|
||||
folder: folderId,
|
||||
user: { $exists: false }, // shared secrets only from workspace
|
||||
user: { $exists: false } // shared secrets only from workspace
|
||||
};
|
||||
|
||||
if (tagIds.length > 0) {
|
||||
@@ -830,6 +828,12 @@ export const getSecrets = async (req: Request, res: Response) => {
|
||||
secrets = await Secret.find(secretQuery).populate("tags");
|
||||
}
|
||||
|
||||
// TODO(akhilmhdh) - secret-imp change this to org type
|
||||
let importedSecrets: any[] = [];
|
||||
if (include_imports) {
|
||||
importedSecrets = await getAllImportedSecrets(workspaceId, environment, folderId as string);
|
||||
}
|
||||
|
||||
const channel = getChannelFromUserAgent(req.headers["user-agent"]);
|
||||
|
||||
const readAction = await EELogService.createAction({
|
||||
@@ -838,7 +842,7 @@ export const getSecrets = async (req: Request, res: Response) => {
|
||||
serviceAccountId: req.serviceAccount?._id,
|
||||
serviceTokenDataId: req.serviceTokenData?._id,
|
||||
workspaceId: new Types.ObjectId(workspaceId as string),
|
||||
secretIds: secrets.map((n: any) => n._id),
|
||||
secretIds: secrets.map((n: any) => n._id)
|
||||
});
|
||||
|
||||
readAction &&
|
||||
@@ -849,7 +853,7 @@ export const getSecrets = async (req: Request, res: Response) => {
|
||||
workspaceId: new Types.ObjectId(workspaceId as string),
|
||||
actions: [readAction],
|
||||
channel,
|
||||
ipAddress: req.realIP,
|
||||
ipAddress: req.realIP
|
||||
}));
|
||||
|
||||
const postHogClient = await TelemetryService.getPostHogClient();
|
||||
@@ -857,7 +861,7 @@ export const getSecrets = async (req: Request, res: Response) => {
|
||||
postHogClient.capture({
|
||||
event: "secrets pulled",
|
||||
distinctId: await TelemetryService.getDistinctId({
|
||||
authData: req.authData,
|
||||
authData: req.authData
|
||||
}),
|
||||
properties: {
|
||||
numberOfSecrets: secrets.length,
|
||||
@@ -865,13 +869,14 @@ export const getSecrets = async (req: Request, res: Response) => {
|
||||
workspaceId,
|
||||
channel,
|
||||
folderId,
|
||||
userAgent: req.headers?.["user-agent"],
|
||||
},
|
||||
userAgent: req.headers?.["user-agent"]
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return res.status(200).send({
|
||||
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")
|
||||
? "web"
|
||||
: "cli";
|
||||
const channel = req.headers?.["user-agent"]?.toLowerCase().includes("mozilla") ? "web" : "cli";
|
||||
|
||||
interface PatchSecret {
|
||||
id: string;
|
||||
@@ -943,51 +946,47 @@ export const updateSecrets = async (req: Request, res: Response) => {
|
||||
tags: string[];
|
||||
}
|
||||
|
||||
const updateOperationsToPerform = req.body.secrets.map(
|
||||
(secret: PatchSecret) => {
|
||||
const {
|
||||
secretKeyCiphertext,
|
||||
secretKeyIV,
|
||||
secretKeyTag,
|
||||
secretValueCiphertext,
|
||||
secretValueIV,
|
||||
secretValueTag,
|
||||
secretCommentCiphertext,
|
||||
secretCommentIV,
|
||||
secretCommentTag,
|
||||
tags,
|
||||
} = secret;
|
||||
const updateOperationsToPerform = req.body.secrets.map((secret: PatchSecret) => {
|
||||
const {
|
||||
secretKeyCiphertext,
|
||||
secretKeyIV,
|
||||
secretKeyTag,
|
||||
secretValueCiphertext,
|
||||
secretValueIV,
|
||||
secretValueTag,
|
||||
secretCommentCiphertext,
|
||||
secretCommentIV,
|
||||
secretCommentTag,
|
||||
tags
|
||||
} = secret;
|
||||
|
||||
return {
|
||||
updateOne: {
|
||||
filter: { _id: new Types.ObjectId(secret.id) },
|
||||
update: {
|
||||
$inc: {
|
||||
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,
|
||||
}
|
||||
: {}),
|
||||
return {
|
||||
updateOne: {
|
||||
filter: { _id: new Types.ObjectId(secret.id) },
|
||||
update: {
|
||||
$inc: {
|
||||
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
|
||||
}
|
||||
: {})
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
await Secret.bulkWrite(updateOperationsToPerform);
|
||||
|
||||
@@ -1009,7 +1008,7 @@ export const updateSecrets = async (req: Request, res: Response) => {
|
||||
secretCommentCiphertext,
|
||||
secretCommentIV,
|
||||
secretCommentTag,
|
||||
tags,
|
||||
tags
|
||||
} = secretModificationsBySecretId[secret._id.toString()];
|
||||
|
||||
return {
|
||||
@@ -1018,9 +1017,7 @@ export const updateSecrets = async (req: Request, res: Response) => {
|
||||
workspace: secret.workspace,
|
||||
type: secret.type,
|
||||
environment: secret.environment,
|
||||
secretKeyCiphertext: secretKeyCiphertext
|
||||
? secretKeyCiphertext
|
||||
: secret.secretKeyCiphertext,
|
||||
secretKeyCiphertext: secretKeyCiphertext ? secretKeyCiphertext : secret.secretKeyCiphertext,
|
||||
secretKeyIV: secretKeyIV ? secretKeyIV : secret.secretKeyIV,
|
||||
secretKeyTag: secretKeyTag ? secretKeyTag : secret.secretKeyTag,
|
||||
secretValueCiphertext: secretValueCiphertext
|
||||
@@ -1031,17 +1028,13 @@ export const updateSecrets = async (req: Request, res: Response) => {
|
||||
secretCommentCiphertext: secretCommentCiphertext
|
||||
? secretCommentCiphertext
|
||||
: secret.secretCommentCiphertext,
|
||||
secretCommentIV: secretCommentIV
|
||||
? secretCommentIV
|
||||
: secret.secretCommentIV,
|
||||
secretCommentTag: secretCommentTag
|
||||
? secretCommentTag
|
||||
: secret.secretCommentTag,
|
||||
secretCommentIV: secretCommentIV ? secretCommentIV : secret.secretCommentIV,
|
||||
secretCommentTag: secretCommentTag ? secretCommentTag : secret.secretCommentTag,
|
||||
tags: tags ? tags : secret.tags,
|
||||
algorithm: ALGORITHM_AES_256_GCM,
|
||||
keyEncoding: ENCODING_SCHEME_UTF8,
|
||||
keyEncoding: ENCODING_SCHEME_UTF8
|
||||
};
|
||||
}),
|
||||
})
|
||||
};
|
||||
|
||||
await EESecretService.addSecretVersions(secretVersions);
|
||||
@@ -1059,13 +1052,16 @@ export const updateSecrets = async (req: Request, res: Response) => {
|
||||
|
||||
Object.keys(workspaceSecretObj).forEach(async (key) => {
|
||||
// trigger event - push secrets
|
||||
setTimeout(async () => {
|
||||
await EventService.handleEvent({
|
||||
event: eventPushSecrets({
|
||||
workspaceId: new Types.ObjectId(key),
|
||||
}),
|
||||
});
|
||||
}, 10000);
|
||||
// This route is not used anymore thus keep it commented out as it does not expose environment
|
||||
// it will end up creating a lot of requests from the server
|
||||
// setTimeout(async () => {
|
||||
// await EventService.handleEvent({
|
||||
// event: eventPushSecrets({
|
||||
// workspaceId: new Types.ObjectId(key),
|
||||
// environment,
|
||||
// })
|
||||
// });
|
||||
// }, 10000);
|
||||
|
||||
const updateAction = await EELogService.createAction({
|
||||
name: ACTION_UPDATE_SECRETS,
|
||||
@@ -1073,7 +1069,7 @@ export const updateSecrets = async (req: Request, res: Response) => {
|
||||
serviceAccountId: req.serviceAccount?._id,
|
||||
serviceTokenDataId: req.serviceTokenData?._id,
|
||||
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
|
||||
@@ -1085,7 +1081,7 @@ export const updateSecrets = async (req: Request, res: Response) => {
|
||||
workspaceId: new Types.ObjectId(key),
|
||||
actions: [updateAction],
|
||||
channel,
|
||||
ipAddress: req.realIP,
|
||||
ipAddress: req.realIP
|
||||
}));
|
||||
|
||||
// (EE) take a secret snapshot
|
||||
@@ -1101,15 +1097,15 @@ export const updateSecrets = async (req: Request, res: Response) => {
|
||||
postHogClient.capture({
|
||||
event: "secrets modified",
|
||||
distinctId: await TelemetryService.getDistinctId({
|
||||
authData: req.authData,
|
||||
authData: req.authData
|
||||
}),
|
||||
properties: {
|
||||
numberOfSecrets: workspaceSecretObj[key].length,
|
||||
environment: workspaceSecretObj[key][0].environment,
|
||||
workspaceId: key,
|
||||
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({
|
||||
secrets: await Secret.find({
|
||||
_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({
|
||||
_id: {
|
||||
$in: toDelete,
|
||||
},
|
||||
$in: toDelete
|
||||
}
|
||||
});
|
||||
|
||||
await EESecretService.markDeletedSecretVersions({
|
||||
secretIds: toDelete,
|
||||
secretIds: toDelete
|
||||
});
|
||||
|
||||
// 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) => {
|
||||
// trigger event - push secrets
|
||||
await EventService.handleEvent({
|
||||
event: eventPushSecrets({
|
||||
workspaceId: new Types.ObjectId(key),
|
||||
}),
|
||||
});
|
||||
// DEPRECIATED(akhilmhdh): as this would cause server to send so many request
|
||||
// and this route is not used anymore thus like snapshot keeping it commented out
|
||||
// await EventService.handleEvent({
|
||||
// event: eventPushSecrets({
|
||||
// workspaceId: new Types.ObjectId(key)
|
||||
// })
|
||||
// });
|
||||
const deleteAction = await EELogService.createAction({
|
||||
name: ACTION_DELETE_SECRETS,
|
||||
userId: req.user?._id,
|
||||
serviceAccountId: req.serviceAccount?._id,
|
||||
serviceTokenDataId: req.serviceTokenData?._id,
|
||||
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
|
||||
@@ -1223,7 +1221,7 @@ export const deleteSecrets = async (req: Request, res: Response) => {
|
||||
workspaceId: new Types.ObjectId(key),
|
||||
actions: [deleteAction],
|
||||
channel,
|
||||
ipAddress: req.realIP,
|
||||
ipAddress: req.realIP
|
||||
}));
|
||||
|
||||
// (EE) take a secret snapshot
|
||||
@@ -1237,20 +1235,20 @@ export const deleteSecrets = async (req: Request, res: Response) => {
|
||||
postHogClient.capture({
|
||||
event: "secrets deleted",
|
||||
distinctId: await TelemetryService.getDistinctId({
|
||||
authData: req.authData,
|
||||
authData: req.authData
|
||||
}),
|
||||
properties: {
|
||||
numberOfSecrets: workspaceSecretObj[key].length,
|
||||
environment: workspaceSecretObj[key][0].environment,
|
||||
workspaceId: key,
|
||||
channel: channel,
|
||||
userAgent: req.headers?.["user-agent"],
|
||||
},
|
||||
userAgent: req.headers?.["user-agent"]
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return res.status(200).send({
|
||||
secrets: req.secrets,
|
||||
secrets: req.secrets
|
||||
});
|
||||
};
|
||||
|
||||
@@ -2,10 +2,7 @@ import { Request, Response } from "express";
|
||||
import crypto from "crypto";
|
||||
import bcrypt from "bcrypt";
|
||||
import { ServiceAccount, ServiceTokenData, User } from "../../models";
|
||||
import {
|
||||
AUTH_MODE_JWT,
|
||||
AUTH_MODE_SERVICE_ACCOUNT,
|
||||
} from "../../variables";
|
||||
import { AUTH_MODE_JWT, AUTH_MODE_SERVICE_ACCOUNT } from "../../variables";
|
||||
import { getSaltRounds } from "../../config";
|
||||
import { BadRequestError } from "../../utils/errors";
|
||||
import Folder from "../../models/folder";
|
||||
@@ -46,14 +43,13 @@ export const getServiceTokenData = async (req: Request, res: Response) => {
|
||||
|
||||
if (!(req.authData.authPayload instanceof ServiceTokenData))
|
||||
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(
|
||||
req.authData.authPayload._id
|
||||
)
|
||||
const serviceTokenData = await ServiceTokenData.findById(req.authData.authPayload._id)
|
||||
.select("+encryptedKey +iv +tag")
|
||||
.populate("user").lean();
|
||||
.populate("user")
|
||||
.lean();
|
||||
|
||||
return res.status(200).json(serviceTokenData);
|
||||
};
|
||||
@@ -68,29 +64,7 @@ export const getServiceTokenData = async (req: Request, res: Response) => {
|
||||
export const createServiceTokenData = async (req: Request, res: Response) => {
|
||||
let serviceTokenData;
|
||||
|
||||
const {
|
||||
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 { name, workspaceId, encryptedKey, iv, tag, expiresIn, permissions, scopes } = req.body;
|
||||
|
||||
const secret = crypto.randomBytes(16).toString("hex");
|
||||
const secretHash = await bcrypt.hash(secret, await getSaltRounds());
|
||||
@@ -103,10 +77,7 @@ export const createServiceTokenData = async (req: Request, res: Response) => {
|
||||
|
||||
let user, serviceAccount;
|
||||
|
||||
if (
|
||||
req.authData.authMode === AUTH_MODE_JWT &&
|
||||
req.authData.authPayload instanceof User
|
||||
) {
|
||||
if (req.authData.authMode === AUTH_MODE_JWT && req.authData.authPayload instanceof User) {
|
||||
user = req.authData.authPayload._id;
|
||||
}
|
||||
|
||||
@@ -120,17 +91,16 @@ export const createServiceTokenData = async (req: Request, res: Response) => {
|
||||
serviceTokenData = await new ServiceTokenData({
|
||||
name,
|
||||
workspace: workspaceId,
|
||||
environment,
|
||||
user,
|
||||
serviceAccount,
|
||||
scopes,
|
||||
lastUsed: new Date(),
|
||||
expiresAt,
|
||||
secretHash,
|
||||
encryptedKey,
|
||||
iv,
|
||||
tag,
|
||||
secretPath,
|
||||
permissions,
|
||||
permissions
|
||||
}).save();
|
||||
|
||||
// return service token data without sensitive data
|
||||
@@ -142,7 +112,7 @@ export const createServiceTokenData = async (req: Request, res: Response) => {
|
||||
|
||||
return res.status(200).send({
|
||||
serviceToken,
|
||||
serviceTokenData,
|
||||
serviceTokenData
|
||||
});
|
||||
};
|
||||
|
||||
@@ -155,11 +125,9 @@ export const createServiceTokenData = async (req: Request, res: Response) => {
|
||||
export const deleteServiceTokenData = async (req: Request, res: Response) => {
|
||||
const { serviceTokenDataId } = req.params;
|
||||
|
||||
const serviceTokenData = await ServiceTokenData.findByIdAndDelete(
|
||||
serviceTokenDataId
|
||||
);
|
||||
const serviceTokenData = await ServiceTokenData.findByIdAndDelete(serviceTokenDataId);
|
||||
|
||||
return res.status(200).send({
|
||||
serviceTokenData,
|
||||
serviceTokenData
|
||||
});
|
||||
};
|
||||
|
||||
@@ -3,7 +3,6 @@ import { Types } from "mongoose";
|
||||
import { Membership, Secret } from "../../models";
|
||||
import Tag from "../../models/tag";
|
||||
import { BadRequestError, UnauthorizedRequestError } from "../../utils/errors";
|
||||
import { MongoError } from "mongodb";
|
||||
|
||||
export const createWorkspaceTag = async (req: Request, res: Response) => {
|
||||
const { workspaceId } = req.params;
|
||||
@@ -16,7 +15,7 @@ export const createWorkspaceTag = async (req: Request, res: Response) => {
|
||||
user: new Types.ObjectId(req.user._id),
|
||||
};
|
||||
|
||||
const createdTag = await new Tag(tagToCreate);
|
||||
const createdTag = await new Tag(tagToCreate).save();
|
||||
|
||||
res.json(createdTag);
|
||||
};
|
||||
@@ -49,7 +48,11 @@ export const deleteWorkspaceTag = async (req: Request, res: Response) => {
|
||||
|
||||
export const getWorkspaceTags = async (req: Request, res: Response) => {
|
||||
const { workspaceId } = req.params;
|
||||
const workspaceTags = await Tag.find({ workspace: workspaceId });
|
||||
|
||||
const workspaceTags = await Tag.find({
|
||||
workspace: new Types.ObjectId(workspaceId)
|
||||
});
|
||||
|
||||
return res.json({
|
||||
workspaceTags
|
||||
});
|
||||
|
||||
@@ -1,8 +1,14 @@
|
||||
import { Request, Response } from "express";
|
||||
import { Types } from "mongoose";
|
||||
import crypto from "crypto";
|
||||
import bcrypt from "bcrypt";
|
||||
import {
|
||||
APIKeyData,
|
||||
MembershipOrg,
|
||||
User,
|
||||
TokenVersion,
|
||||
User
|
||||
} from "../../models";
|
||||
import { getSaltRounds } from "../../config";
|
||||
|
||||
/**
|
||||
* Return the current user.
|
||||
@@ -74,6 +80,26 @@ export const updateMyMfaEnabled = async (req: Request, res: Response) => {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the current user's name [firstName, lastName].
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
export const updateName = async (req: Request, res: Response) => {
|
||||
const { firstName, lastName }: { firstName: string; lastName: string; } = req.body;
|
||||
req.user.firstName = firstName;
|
||||
req.user.lastName = lastName || "";
|
||||
|
||||
await req.user.save();
|
||||
|
||||
const user = req.user;
|
||||
|
||||
return res.status(200).send({
|
||||
user,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Return organizations that the current user is part of.
|
||||
* @param req
|
||||
@@ -117,3 +143,106 @@ export const getMyOrganizations = async (req: Request, res: Response) => {
|
||||
organizations,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Return API keys belonging to current user.
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
export const getMyAPIKeys = async (req: Request, res: Response) => {
|
||||
const apiKeyData = await APIKeyData.find({
|
||||
user: req.user._id,
|
||||
});
|
||||
|
||||
return res.status(200).send(apiKeyData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create new API key for current user.
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
export const createAPIKey = async (req: Request, res: Response) => {
|
||||
const { name, expiresIn } = req.body;
|
||||
|
||||
const secret = crypto.randomBytes(16).toString("hex");
|
||||
const secretHash = await bcrypt.hash(secret, await getSaltRounds());
|
||||
|
||||
const expiresAt = new Date();
|
||||
expiresAt.setSeconds(expiresAt.getSeconds() + expiresIn);
|
||||
|
||||
let apiKeyData = await new APIKeyData({
|
||||
name,
|
||||
lastUsed: new Date(),
|
||||
expiresAt,
|
||||
user: req.user._id,
|
||||
secretHash,
|
||||
}).save();
|
||||
|
||||
// return api key data without sensitive data
|
||||
apiKeyData = (await APIKeyData.findById(apiKeyData._id)) as any;
|
||||
|
||||
if (!apiKeyData) throw new Error("Failed to find API key data");
|
||||
|
||||
const apiKey = `ak.${apiKeyData._id.toString()}.${secret}`;
|
||||
|
||||
return res.status(200).send({
|
||||
apiKey,
|
||||
apiKeyData,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete API key with id [apiKeyDataId] belonging to current user
|
||||
* @param req
|
||||
* @param res
|
||||
*/
|
||||
export const deleteAPIKey = async (req: Request, res: Response) => {
|
||||
const { apiKeyDataId } = req.params;
|
||||
|
||||
const apiKeyData = await APIKeyData.findOneAndDelete({
|
||||
_id: new Types.ObjectId(apiKeyDataId),
|
||||
user: req.user._id
|
||||
});
|
||||
|
||||
return res.status(200).send({
|
||||
apiKeyData
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Return active sessions (TokenVersion) belonging to user
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
export const getMySessions = async (req: Request, res: Response) => {
|
||||
const tokenVersions = await TokenVersion.find({
|
||||
user: req.user._id
|
||||
});
|
||||
|
||||
return res.status(200).send(tokenVersions);
|
||||
}
|
||||
|
||||
/**
|
||||
* Revoke all active sessions belong to user
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
export const deleteMySessions = async (req: Request, res: Response) => {
|
||||
await TokenVersion.updateMany({
|
||||
user: req.user._id,
|
||||
}, {
|
||||
$inc: {
|
||||
refreshVersion: 1,
|
||||
accessVersion: 1,
|
||||
},
|
||||
});
|
||||
|
||||
return res.status(200).send({
|
||||
message: "Successfully revoked all sessions"
|
||||
});
|
||||
}
|
||||
@@ -1,34 +1,29 @@
|
||||
import { Request, Response } from "express";
|
||||
import { Types } from "mongoose";
|
||||
import { Key, Membership, ServiceTokenData, Workspace } from "../../models";
|
||||
import {
|
||||
Key,
|
||||
Membership,
|
||||
ServiceTokenData,
|
||||
Workspace,
|
||||
} from "../../models";
|
||||
import {
|
||||
pullSecrets as pull,
|
||||
v2PushSecrets as push,
|
||||
reformatPullSecrets,
|
||||
pullSecrets as pull,
|
||||
v2PushSecrets as push,
|
||||
reformatPullSecrets
|
||||
} from "../../helpers/secret";
|
||||
import { pushKeys } from "../../helpers/key";
|
||||
import { EventService, TelemetryService } from "../../services";
|
||||
import { eventPushSecrets } from "../../events";
|
||||
|
||||
interface V2PushSecret {
|
||||
type: string; // personal or shared
|
||||
secretKeyCiphertext: string;
|
||||
secretKeyIV: string;
|
||||
secretKeyTag: string;
|
||||
secretKeyHash: string;
|
||||
secretValueCiphertext: string;
|
||||
secretValueIV: string;
|
||||
secretValueTag: string;
|
||||
secretValueHash: string;
|
||||
secretCommentCiphertext?: string;
|
||||
secretCommentIV?: string;
|
||||
secretCommentTag?: string;
|
||||
secretCommentHash?: string;
|
||||
type: string; // personal or shared
|
||||
secretKeyCiphertext: string;
|
||||
secretKeyIV: string;
|
||||
secretKeyTag: string;
|
||||
secretKeyHash: string;
|
||||
secretValueCiphertext: string;
|
||||
secretValueIV: string;
|
||||
secretValueTag: string;
|
||||
secretValueHash: string;
|
||||
secretCommentCiphertext?: string;
|
||||
secretCommentIV?: string;
|
||||
secretCommentTag?: string;
|
||||
secretCommentHash?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -39,7 +34,7 @@ interface V2PushSecret {
|
||||
* @returns
|
||||
*/
|
||||
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();
|
||||
let { secrets }: { secrets: V2PushSecret[] } = req.body;
|
||||
const { keys, environment, channel } = req.body;
|
||||
@@ -62,13 +57,13 @@ export const pushWorkspaceSecrets = async (req: Request, res: Response) => {
|
||||
environment,
|
||||
secrets,
|
||||
channel: channel ? channel : "cli",
|
||||
ipAddress: req.realIP,
|
||||
ipAddress: req.realIP
|
||||
});
|
||||
|
||||
await pushKeys({
|
||||
userId: req.user._id,
|
||||
workspaceId,
|
||||
keys,
|
||||
keys
|
||||
});
|
||||
|
||||
if (postHogClient) {
|
||||
@@ -79,8 +74,8 @@ export const pushWorkspaceSecrets = async (req: Request, res: Response) => {
|
||||
numberOfSecrets: secrets.length,
|
||||
environment,
|
||||
workspaceId,
|
||||
channel: channel ? channel : "cli",
|
||||
},
|
||||
channel: channel ? channel : "cli"
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -89,12 +84,13 @@ export const pushWorkspaceSecrets = async (req: Request, res: Response) => {
|
||||
event: eventPushSecrets({
|
||||
workspaceId: new Types.ObjectId(workspaceId),
|
||||
environment,
|
||||
}),
|
||||
secretPath: "/"
|
||||
})
|
||||
});
|
||||
|
||||
return res.status(200).send({
|
||||
message: "Successfully uploaded workspace secrets",
|
||||
});
|
||||
return res.status(200).send({
|
||||
message: "Successfully uploaded workspace secrets"
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -105,7 +101,7 @@ export const pushWorkspaceSecrets = async (req: Request, res: Response) => {
|
||||
* @returns
|
||||
*/
|
||||
export const pullSecrets = async (req: Request, res: Response) => {
|
||||
let secrets;
|
||||
let secrets;
|
||||
const postHogClient = await TelemetryService.getPostHogClient();
|
||||
const environment: string = req.query.environment as string;
|
||||
const channel: string = req.query.channel as string;
|
||||
@@ -128,7 +124,7 @@ export const pullSecrets = async (req: Request, res: Response) => {
|
||||
workspaceId,
|
||||
environment,
|
||||
channel: channel ? channel : "cli",
|
||||
ipAddress: req.realIP,
|
||||
ipAddress: req.realIP
|
||||
});
|
||||
|
||||
if (channel !== "cli") {
|
||||
@@ -144,18 +140,18 @@ export const pullSecrets = async (req: Request, res: Response) => {
|
||||
numberOfSecrets: secrets.length,
|
||||
environment,
|
||||
workspaceId,
|
||||
channel: channel ? channel : "cli",
|
||||
},
|
||||
channel: channel ? channel : "cli"
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return res.status(200).send({
|
||||
secrets,
|
||||
});
|
||||
return res.status(200).send({
|
||||
secrets
|
||||
});
|
||||
};
|
||||
|
||||
export const getWorkspaceKey = async (req: Request, res: Response) => {
|
||||
/*
|
||||
/*
|
||||
#swagger.summary = 'Return encrypted project key'
|
||||
#swagger.description = 'Return encrypted project key'
|
||||
|
||||
@@ -183,43 +179,38 @@ export const getWorkspaceKey = async (req: Request, res: Response) => {
|
||||
}
|
||||
}
|
||||
*/
|
||||
let key;
|
||||
let key;
|
||||
const { workspaceId } = req.params;
|
||||
|
||||
key = await Key.findOne({
|
||||
workspace: workspaceId,
|
||||
receiver: req.user._id,
|
||||
receiver: req.user._id
|
||||
}).populate("sender", "+publicKey");
|
||||
|
||||
if (!key) throw new Error("Failed to find workspace key");
|
||||
|
||||
return res.status(200).json(key);
|
||||
}
|
||||
export const getWorkspaceServiceTokenData = async (
|
||||
req: Request,
|
||||
res: Response
|
||||
) => {
|
||||
return res.status(200).json(key);
|
||||
};
|
||||
export const getWorkspaceServiceTokenData = async (req: Request, res: Response) => {
|
||||
const { workspaceId } = req.params;
|
||||
|
||||
const serviceTokenData = await ServiceTokenData
|
||||
.find({
|
||||
workspace: workspaceId,
|
||||
})
|
||||
.select("+encryptedKey +iv +tag");
|
||||
const serviceTokenData = await ServiceTokenData.find({
|
||||
workspace: workspaceId
|
||||
}).select("+encryptedKey +iv +tag");
|
||||
|
||||
return res.status(200).send({
|
||||
serviceTokenData,
|
||||
});
|
||||
}
|
||||
return res.status(200).send({
|
||||
serviceTokenData
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Return memberships for workspace with id [workspaceId]
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
export const getWorkspaceMemberships = async (req: Request, res: Response) => {
|
||||
/*
|
||||
/*
|
||||
#swagger.summary = 'Return project memberships'
|
||||
#swagger.description = 'Return project memberships'
|
||||
|
||||
@@ -255,22 +246,22 @@ export const getWorkspaceMemberships = async (req: Request, res: Response) => {
|
||||
const { workspaceId } = req.params;
|
||||
|
||||
const memberships = await Membership.find({
|
||||
workspace: workspaceId,
|
||||
workspace: workspaceId
|
||||
}).populate("user", "+publicKey");
|
||||
|
||||
return res.status(200).send({
|
||||
memberships,
|
||||
});
|
||||
}
|
||||
return res.status(200).send({
|
||||
memberships
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Update role of membership with id [membershipId] to role [role]
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
export const updateWorkspaceMembership = async (req: Request, res: Response) => {
|
||||
/*
|
||||
/*
|
||||
#swagger.summary = 'Update project membership'
|
||||
#swagger.description = 'Update project membership'
|
||||
|
||||
@@ -323,33 +314,32 @@ export const updateWorkspaceMembership = async (req: Request, res: Response) =>
|
||||
}
|
||||
}
|
||||
*/
|
||||
const {
|
||||
membershipId,
|
||||
} = req.params;
|
||||
const { membershipId } = req.params;
|
||||
const { role } = req.body;
|
||||
|
||||
|
||||
const membership = await Membership.findByIdAndUpdate(
|
||||
membershipId,
|
||||
{
|
||||
role,
|
||||
}, {
|
||||
new: true,
|
||||
role
|
||||
},
|
||||
{
|
||||
new: true
|
||||
}
|
||||
);
|
||||
|
||||
return res.status(200).send({
|
||||
membership,
|
||||
});
|
||||
}
|
||||
return res.status(200).send({
|
||||
membership
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Delete workspace membership with id [membershipId]
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
export const deleteWorkspaceMembership = async (req: Request, res: Response) => {
|
||||
/*
|
||||
/*
|
||||
#swagger.summary = 'Delete project membership'
|
||||
#swagger.description = 'Delete project membership'
|
||||
|
||||
@@ -385,23 +375,21 @@ export const deleteWorkspaceMembership = async (req: Request, res: Response) =>
|
||||
}
|
||||
}
|
||||
*/
|
||||
const {
|
||||
membershipId,
|
||||
} = req.params;
|
||||
|
||||
const { membershipId } = req.params;
|
||||
|
||||
const membership = await Membership.findByIdAndDelete(membershipId);
|
||||
|
||||
|
||||
if (!membership) throw new Error("Failed to delete workspace membership");
|
||||
|
||||
|
||||
await Key.deleteMany({
|
||||
receiver: membership.user,
|
||||
workspace: membership.workspace,
|
||||
workspace: membership.workspace
|
||||
});
|
||||
|
||||
return res.status(200).send({
|
||||
membership,
|
||||
});
|
||||
}
|
||||
|
||||
return res.status(200).send({
|
||||
membership
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Change autoCapitilzation Rule of workspace
|
||||
@@ -415,18 +403,18 @@ export const toggleAutoCapitalization = async (req: Request, res: Response) => {
|
||||
|
||||
const workspace = await Workspace.findOneAndUpdate(
|
||||
{
|
||||
_id: workspaceId,
|
||||
_id: workspaceId
|
||||
},
|
||||
{
|
||||
autoCapitalization,
|
||||
autoCapitalization
|
||||
},
|
||||
{
|
||||
new: true,
|
||||
new: true
|
||||
}
|
||||
);
|
||||
|
||||
return res.status(200).send({
|
||||
message: "Successfully changed autoCapitalization setting",
|
||||
workspace,
|
||||
});
|
||||
return res.status(200).send({
|
||||
message: "Successfully changed autoCapitalization setting",
|
||||
workspace
|
||||
});
|
||||
};
|
||||
|
||||
@@ -5,6 +5,10 @@ import { eventPushSecrets } from "../../events";
|
||||
import { BotService } from "../../services";
|
||||
import { repackageSecretToRaw } from "../../helpers/secrets";
|
||||
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";
|
||||
|
||||
/**
|
||||
* Return secrets for workspace with id [workspaceId] and environment
|
||||
@@ -16,27 +20,53 @@ export const getSecretsRaw = async (req: Request, res: Response) => {
|
||||
const workspaceId = req.query.workspaceId as string;
|
||||
const environment = req.query.environment as string;
|
||||
const secretPath = req.query.secretPath as string;
|
||||
const includeImports = req.query.include_imports as string;
|
||||
|
||||
const secrets = await SecretService.getSecrets({
|
||||
workspaceId: new Types.ObjectId(workspaceId),
|
||||
environment,
|
||||
secretPath,
|
||||
authData: req.authData,
|
||||
authData: req.authData
|
||||
});
|
||||
|
||||
const key = await BotService.getWorkspaceKeyWithBot({
|
||||
workspaceId: new Types.ObjectId(workspaceId),
|
||||
workspaceId: new Types.ObjectId(workspaceId)
|
||||
});
|
||||
|
||||
if (includeImports) {
|
||||
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({
|
||||
secrets: secrets.map((secret) => {
|
||||
const rep = repackageSecretToRaw({
|
||||
secret,
|
||||
key,
|
||||
key
|
||||
});
|
||||
|
||||
return rep;
|
||||
}),
|
||||
})
|
||||
});
|
||||
};
|
||||
|
||||
@@ -58,54 +88,47 @@ export const getSecretByNameRaw = async (req: Request, res: Response) => {
|
||||
environment,
|
||||
type,
|
||||
secretPath,
|
||||
authData: req.authData,
|
||||
authData: req.authData
|
||||
});
|
||||
|
||||
const key = await BotService.getWorkspaceKeyWithBot({
|
||||
workspaceId: new Types.ObjectId(workspaceId),
|
||||
workspaceId: new Types.ObjectId(workspaceId)
|
||||
});
|
||||
|
||||
return res.status(200).send({
|
||||
secret: repackageSecretToRaw({
|
||||
secret,
|
||||
key,
|
||||
}),
|
||||
key
|
||||
})
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Create secret with name [secretName] in plaintext
|
||||
* @param req
|
||||
* @param res
|
||||
* @param res
|
||||
*/
|
||||
export const createSecretRaw = async (req: Request, res: Response) => {
|
||||
const { secretName } = req.params;
|
||||
const {
|
||||
workspaceId,
|
||||
environment,
|
||||
type,
|
||||
secretValue,
|
||||
secretComment,
|
||||
secretPath = "/",
|
||||
} = req.body;
|
||||
const { workspaceId, environment, type, secretValue, secretComment, secretPath = "/" } = req.body;
|
||||
|
||||
const key = await BotService.getWorkspaceKeyWithBot({
|
||||
workspaceId: new Types.ObjectId(workspaceId),
|
||||
workspaceId: new Types.ObjectId(workspaceId)
|
||||
});
|
||||
|
||||
const secretKeyEncrypted = encryptSymmetric128BitHexKeyUTF8({
|
||||
plaintext: secretName,
|
||||
key,
|
||||
key
|
||||
});
|
||||
|
||||
const secretValueEncrypted = encryptSymmetric128BitHexKeyUTF8({
|
||||
plaintext: secretValue,
|
||||
key,
|
||||
key
|
||||
});
|
||||
|
||||
const secretCommentEncrypted = encryptSymmetric128BitHexKeyUTF8({
|
||||
plaintext: secretComment,
|
||||
key,
|
||||
key
|
||||
});
|
||||
|
||||
const secret = await SecretService.createSecret({
|
||||
@@ -123,14 +146,15 @@ export const createSecretRaw = async (req: Request, res: Response) => {
|
||||
secretPath,
|
||||
secretCommentCiphertext: secretCommentEncrypted.ciphertext,
|
||||
secretCommentIV: secretCommentEncrypted.iv,
|
||||
secretCommentTag: secretCommentEncrypted.tag,
|
||||
secretCommentTag: secretCommentEncrypted.tag
|
||||
});
|
||||
|
||||
await EventService.handleEvent({
|
||||
event: eventPushSecrets({
|
||||
workspaceId: new Types.ObjectId(workspaceId),
|
||||
environment,
|
||||
}),
|
||||
secretPath
|
||||
})
|
||||
});
|
||||
|
||||
const secretWithoutBlindIndex = secret.toObject();
|
||||
@@ -139,10 +163,10 @@ export const createSecretRaw = async (req: Request, res: Response) => {
|
||||
return res.status(200).send({
|
||||
secret: repackageSecretToRaw({
|
||||
secret: secretWithoutBlindIndex,
|
||||
key,
|
||||
}),
|
||||
key
|
||||
})
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Update secret with name [secretName]
|
||||
@@ -151,21 +175,15 @@ export const createSecretRaw = async (req: Request, res: Response) => {
|
||||
*/
|
||||
export const updateSecretByNameRaw = async (req: Request, res: Response) => {
|
||||
const { secretName } = req.params;
|
||||
const {
|
||||
workspaceId,
|
||||
environment,
|
||||
type,
|
||||
secretValue,
|
||||
secretPath = "/",
|
||||
} = req.body;
|
||||
const { workspaceId, environment, type, secretValue, secretPath = "/" } = req.body;
|
||||
|
||||
const key = await BotService.getWorkspaceKeyWithBot({
|
||||
workspaceId: new Types.ObjectId(workspaceId),
|
||||
workspaceId: new Types.ObjectId(workspaceId)
|
||||
});
|
||||
|
||||
const secretValueEncrypted = encryptSymmetric128BitHexKeyUTF8({
|
||||
plaintext: secretValue,
|
||||
key,
|
||||
key
|
||||
});
|
||||
|
||||
const secret = await SecretService.updateSecret({
|
||||
@@ -177,21 +195,22 @@ export const updateSecretByNameRaw = async (req: Request, res: Response) => {
|
||||
secretValueCiphertext: secretValueEncrypted.ciphertext,
|
||||
secretValueIV: secretValueEncrypted.iv,
|
||||
secretValueTag: secretValueEncrypted.tag,
|
||||
secretPath,
|
||||
secretPath
|
||||
});
|
||||
|
||||
await EventService.handleEvent({
|
||||
event: eventPushSecrets({
|
||||
workspaceId: new Types.ObjectId(workspaceId),
|
||||
environment,
|
||||
}),
|
||||
secretPath
|
||||
})
|
||||
});
|
||||
|
||||
return res.status(200).send({
|
||||
secret: repackageSecretToRaw({
|
||||
secret,
|
||||
key,
|
||||
}),
|
||||
key
|
||||
})
|
||||
});
|
||||
};
|
||||
|
||||
@@ -202,12 +221,7 @@ export const updateSecretByNameRaw = async (req: Request, res: Response) => {
|
||||
*/
|
||||
export const deleteSecretByNameRaw = async (req: Request, res: Response) => {
|
||||
const { secretName } = req.params;
|
||||
const {
|
||||
workspaceId,
|
||||
environment,
|
||||
type,
|
||||
secretPath = "/",
|
||||
} = req.body;
|
||||
const { workspaceId, environment, type, secretPath = "/" } = req.body;
|
||||
|
||||
const { secret } = await SecretService.deleteSecret({
|
||||
secretName,
|
||||
@@ -215,25 +229,26 @@ export const deleteSecretByNameRaw = async (req: Request, res: Response) => {
|
||||
environment,
|
||||
type,
|
||||
authData: req.authData,
|
||||
secretPath,
|
||||
secretPath
|
||||
});
|
||||
|
||||
await EventService.handleEvent({
|
||||
event: eventPushSecrets({
|
||||
workspaceId: new Types.ObjectId(workspaceId),
|
||||
environment,
|
||||
}),
|
||||
secretPath
|
||||
})
|
||||
});
|
||||
|
||||
const key = await BotService.getWorkspaceKeyWithBot({
|
||||
workspaceId: new Types.ObjectId(workspaceId),
|
||||
workspaceId: new Types.ObjectId(workspaceId)
|
||||
});
|
||||
|
||||
return res.status(200).send({
|
||||
secret: repackageSecretToRaw({
|
||||
secret,
|
||||
key,
|
||||
}),
|
||||
key
|
||||
})
|
||||
});
|
||||
};
|
||||
|
||||
@@ -247,16 +262,35 @@ export const getSecrets = async (req: Request, res: Response) => {
|
||||
const workspaceId = req.query.workspaceId as string;
|
||||
const environment = req.query.environment as string;
|
||||
const secretPath = req.query.secretPath as string;
|
||||
const includeImports = req.query.include_imports as string;
|
||||
|
||||
const secrets = await SecretService.getSecrets({
|
||||
workspaceId: new Types.ObjectId(workspaceId),
|
||||
environment,
|
||||
secretPath,
|
||||
authData: req.authData,
|
||||
authData: req.authData
|
||||
});
|
||||
|
||||
if (includeImports) {
|
||||
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({
|
||||
secrets,
|
||||
secrets
|
||||
});
|
||||
};
|
||||
|
||||
@@ -278,11 +312,11 @@ export const getSecretByName = async (req: Request, res: Response) => {
|
||||
environment,
|
||||
type,
|
||||
secretPath,
|
||||
authData: req.authData,
|
||||
authData: req.authData
|
||||
});
|
||||
|
||||
return res.status(200).send({
|
||||
secret,
|
||||
secret
|
||||
});
|
||||
};
|
||||
|
||||
@@ -306,7 +340,7 @@ export const createSecret = async (req: Request, res: Response) => {
|
||||
secretCommentCiphertext,
|
||||
secretCommentIV,
|
||||
secretCommentTag,
|
||||
secretPath = "/",
|
||||
secretPath = "/"
|
||||
} = req.body;
|
||||
|
||||
const secret = await SecretService.createSecret({
|
||||
@@ -324,25 +358,25 @@ export const createSecret = async (req: Request, res: Response) => {
|
||||
secretPath,
|
||||
secretCommentCiphertext,
|
||||
secretCommentIV,
|
||||
secretCommentTag,
|
||||
secretCommentTag
|
||||
});
|
||||
|
||||
await EventService.handleEvent({
|
||||
event: eventPushSecrets({
|
||||
workspaceId: new Types.ObjectId(workspaceId),
|
||||
environment,
|
||||
}),
|
||||
secretPath
|
||||
})
|
||||
});
|
||||
|
||||
const secretWithoutBlindIndex = secret.toObject();
|
||||
delete secretWithoutBlindIndex.secretBlindIndex;
|
||||
|
||||
return res.status(200).send({
|
||||
secret: secretWithoutBlindIndex,
|
||||
secret: secretWithoutBlindIndex
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Update secret with name [secretName]
|
||||
* @param req
|
||||
@@ -357,7 +391,7 @@ export const updateSecretByName = async (req: Request, res: Response) => {
|
||||
secretValueCiphertext,
|
||||
secretValueIV,
|
||||
secretValueTag,
|
||||
secretPath = "/",
|
||||
secretPath = "/"
|
||||
} = req.body;
|
||||
|
||||
const secret = await SecretService.updateSecret({
|
||||
@@ -369,18 +403,19 @@ export const updateSecretByName = async (req: Request, res: Response) => {
|
||||
secretValueCiphertext,
|
||||
secretValueIV,
|
||||
secretValueTag,
|
||||
secretPath,
|
||||
secretPath
|
||||
});
|
||||
|
||||
await EventService.handleEvent({
|
||||
event: eventPushSecrets({
|
||||
workspaceId: new Types.ObjectId(workspaceId),
|
||||
environment,
|
||||
}),
|
||||
secretPath
|
||||
})
|
||||
});
|
||||
|
||||
return res.status(200).send({
|
||||
secret,
|
||||
secret
|
||||
});
|
||||
};
|
||||
|
||||
@@ -391,12 +426,7 @@ export const updateSecretByName = async (req: Request, res: Response) => {
|
||||
*/
|
||||
export const deleteSecretByName = async (req: Request, res: Response) => {
|
||||
const { secretName } = req.params;
|
||||
const {
|
||||
workspaceId,
|
||||
environment,
|
||||
type,
|
||||
secretPath = "/",
|
||||
} = req.body;
|
||||
const { workspaceId, environment, type, secretPath = "/" } = req.body;
|
||||
|
||||
const { secret } = await SecretService.deleteSecret({
|
||||
secretName,
|
||||
@@ -404,17 +434,18 @@ export const deleteSecretByName = async (req: Request, res: Response) => {
|
||||
environment,
|
||||
type,
|
||||
authData: req.authData,
|
||||
secretPath,
|
||||
secretPath
|
||||
});
|
||||
|
||||
await EventService.handleEvent({
|
||||
event: eventPushSecrets({
|
||||
workspaceId: new Types.ObjectId(workspaceId),
|
||||
environment,
|
||||
}),
|
||||
secretPath
|
||||
})
|
||||
});
|
||||
|
||||
return res.status(200).send({
|
||||
secret,
|
||||
secret
|
||||
});
|
||||
};
|
||||
|
||||
@@ -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
|
||||
* @param req
|
||||
|
||||
@@ -54,23 +54,20 @@ export const getSecretVersions = async (req: Request, res: Response) => {
|
||||
}
|
||||
}
|
||||
*/
|
||||
const { secretId, workspaceId, environment, folderId } = req.params;
|
||||
const { secretId } = req.params;
|
||||
|
||||
const offset: number = parseInt(req.query.offset as string);
|
||||
const limit: number = parseInt(req.query.limit as string);
|
||||
|
||||
const secretVersions = await SecretVersion.find({
|
||||
secret: secretId,
|
||||
workspace: workspaceId,
|
||||
environment,
|
||||
folder: folderId,
|
||||
secret: secretId
|
||||
})
|
||||
.sort({ createdAt: -1 })
|
||||
.skip(offset)
|
||||
.limit(limit);
|
||||
|
||||
return res.status(200).send({
|
||||
secretVersions,
|
||||
secretVersions
|
||||
});
|
||||
};
|
||||
|
||||
@@ -135,7 +132,7 @@ export const rollbackSecretVersion = async (req: Request, res: Response) => {
|
||||
// validate secret version
|
||||
const oldSecretVersion = await SecretVersion.findOne({
|
||||
secret: secretId,
|
||||
version,
|
||||
version
|
||||
}).select("+secretBlindIndex");
|
||||
|
||||
if (!oldSecretVersion) throw new Error("Failed to find secret version");
|
||||
@@ -154,7 +151,7 @@ export const rollbackSecretVersion = async (req: Request, res: Response) => {
|
||||
secretValueTag,
|
||||
algorithm,
|
||||
folder,
|
||||
keyEncoding,
|
||||
keyEncoding
|
||||
} = oldSecretVersion;
|
||||
|
||||
// update secret
|
||||
@@ -162,7 +159,7 @@ export const rollbackSecretVersion = async (req: Request, res: Response) => {
|
||||
secretId,
|
||||
{
|
||||
$inc: {
|
||||
version: 1,
|
||||
version: 1
|
||||
},
|
||||
workspace,
|
||||
type,
|
||||
@@ -177,10 +174,10 @@ export const rollbackSecretVersion = async (req: Request, res: Response) => {
|
||||
secretValueTag,
|
||||
folderId: folder,
|
||||
algorithm,
|
||||
keyEncoding,
|
||||
keyEncoding
|
||||
},
|
||||
{
|
||||
new: true,
|
||||
new: true
|
||||
}
|
||||
);
|
||||
|
||||
@@ -204,17 +201,17 @@ export const rollbackSecretVersion = async (req: Request, res: Response) => {
|
||||
secretValueTag,
|
||||
folder,
|
||||
algorithm,
|
||||
keyEncoding,
|
||||
keyEncoding
|
||||
}).save();
|
||||
|
||||
// take secret snapshot
|
||||
await EESecretService.takeSecretSnapshot({
|
||||
workspaceId: secret.workspace,
|
||||
environment,
|
||||
folderId: folder,
|
||||
folderId: folder
|
||||
});
|
||||
|
||||
return res.status(200).send({
|
||||
secret,
|
||||
secret
|
||||
});
|
||||
};
|
||||
|
||||
@@ -41,6 +41,21 @@ router.get(
|
||||
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(
|
||||
"/:organizationId/plan/billing",
|
||||
requireAuth({
|
||||
|
||||
@@ -30,6 +30,9 @@ interface FeatureSet {
|
||||
customRateLimits: boolean;
|
||||
customAlerts: boolean;
|
||||
auditLogs: boolean;
|
||||
status: 'incomplete' | 'incomplete_expired' | 'trialing' | 'active' | 'past_due' | 'canceled' | 'unpaid' | null;
|
||||
trial_end: number | null;
|
||||
has_used_trial: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -60,6 +63,9 @@ class EELicenseService {
|
||||
customRateLimits: true,
|
||||
customAlerts: true,
|
||||
auditLogs: false,
|
||||
status: null,
|
||||
trial_end: null,
|
||||
has_used_trial: true
|
||||
}
|
||||
|
||||
public localFeatureSet: NodeCache;
|
||||
@@ -67,7 +73,7 @@ class EELicenseService {
|
||||
constructor() {
|
||||
this._isLicenseValid = true;
|
||||
this.localFeatureSet = new NodeCache({
|
||||
stdTTL: 300,
|
||||
stdTTL: 60,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -108,6 +114,12 @@ class EELicenseService {
|
||||
await this.getPlan(organizationId, workspaceId);
|
||||
}
|
||||
}
|
||||
|
||||
public async delPlan(organizationId: string) {
|
||||
if (this.instanceType === "cloud") {
|
||||
this.localFeatureSet.del(`${organizationId}-`);
|
||||
}
|
||||
}
|
||||
|
||||
public async initGlobalFeatureSet() {
|
||||
const licenseServerKey = await getLicenseServerKey();
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { eventPushSecrets } from "./secret"
|
||||
import { eventPushSecrets } from "./secret";
|
||||
import { eventStartIntegration } from "./integration";
|
||||
|
||||
export {
|
||||
eventPushSecrets,
|
||||
}
|
||||
export { eventPushSecrets, eventStartIntegration };
|
||||
|
||||
@@ -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: {}
|
||||
};
|
||||
};
|
||||
@@ -1,64 +1,54 @@
|
||||
import { Types } from "mongoose";
|
||||
import {
|
||||
EVENT_PULL_SECRETS,
|
||||
EVENT_PUSH_SECRETS,
|
||||
} from "../variables";
|
||||
import { EVENT_PULL_SECRETS, EVENT_PUSH_SECRETS } from "../variables";
|
||||
|
||||
interface PushSecret {
|
||||
ciphertextKey: string;
|
||||
ivKey: string;
|
||||
tagKey: string;
|
||||
hashKey: string;
|
||||
ciphertextValue: string;
|
||||
ivValue: string;
|
||||
tagValue: string;
|
||||
hashValue: string;
|
||||
type: "shared" | "personal";
|
||||
ciphertextKey: string;
|
||||
ivKey: string;
|
||||
tagKey: string;
|
||||
hashKey: string;
|
||||
ciphertextValue: string;
|
||||
ivValue: string;
|
||||
tagValue: string;
|
||||
hashValue: string;
|
||||
type: "shared" | "personal";
|
||||
}
|
||||
|
||||
/**
|
||||
* Return event for pushing secrets
|
||||
* @param {Object} obj
|
||||
* @param {String} obj.workspaceId - id of workspace to push secrets to
|
||||
* @returns
|
||||
* @returns
|
||||
*/
|
||||
const eventPushSecrets = ({
|
||||
workspaceId,
|
||||
environment,
|
||||
secretPath
|
||||
}: {
|
||||
workspaceId: Types.ObjectId;
|
||||
environment: string;
|
||||
secretPath: string;
|
||||
}) => {
|
||||
return {
|
||||
name: EVENT_PUSH_SECRETS,
|
||||
workspaceId,
|
||||
environment,
|
||||
}: {
|
||||
workspaceId: Types.ObjectId;
|
||||
environment?: string;
|
||||
}) => {
|
||||
return ({
|
||||
name: EVENT_PUSH_SECRETS,
|
||||
workspaceId,
|
||||
environment,
|
||||
payload: {
|
||||
|
||||
},
|
||||
});
|
||||
}
|
||||
secretPath,
|
||||
payload: {}
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Return event for pulling secrets
|
||||
* @param {Object} obj
|
||||
* @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: string;
|
||||
}) => {
|
||||
return ({
|
||||
name: EVENT_PULL_SECRETS,
|
||||
workspaceId,
|
||||
payload: {
|
||||
payload: {}
|
||||
};
|
||||
};
|
||||
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export {
|
||||
eventPushSecrets,
|
||||
}
|
||||
export { eventPushSecrets };
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
import { Types } from "mongoose";
|
||||
import { Bot } from "../models";
|
||||
import { EVENT_PUSH_SECRETS } from "../variables";
|
||||
import { EVENT_PUSH_SECRETS, EVENT_START_INTEGRATION } from "../variables";
|
||||
import { IntegrationService } from "../services";
|
||||
import { triggerWebhook } from "../services/WebhookService";
|
||||
|
||||
interface Event {
|
||||
name: string;
|
||||
workspaceId: Types.ObjectId;
|
||||
environment?: string;
|
||||
secretPath?: string;
|
||||
payload: any;
|
||||
}
|
||||
|
||||
@@ -19,22 +21,31 @@ interface Event {
|
||||
* @param {Object} obj.event.payload - payload of event (depends on event)
|
||||
*/
|
||||
export const handleEventHelper = async ({ event }: { event: Event }) => {
|
||||
const { workspaceId, environment } = event;
|
||||
const { workspaceId, environment, secretPath } = event;
|
||||
|
||||
// TODO: moduralize bot check into separate function
|
||||
const bot = await Bot.findOne({
|
||||
workspace: workspaceId,
|
||||
isActive: true,
|
||||
isActive: true
|
||||
});
|
||||
|
||||
if (!bot) return;
|
||||
|
||||
switch (event.name) {
|
||||
case EVENT_PUSH_SECRETS:
|
||||
IntegrationService.syncIntegrations({
|
||||
workspaceId,
|
||||
environment,
|
||||
});
|
||||
if (bot) {
|
||||
await IntegrationService.syncIntegrations({
|
||||
workspaceId,
|
||||
environment
|
||||
});
|
||||
}
|
||||
triggerWebhook(workspaceId.toString(), environment || "", secretPath || "");
|
||||
break;
|
||||
case EVENT_START_INTEGRATION:
|
||||
if (bot) {
|
||||
IntegrationService.syncIntegrations({
|
||||
workspaceId,
|
||||
environment
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
@@ -45,6 +45,7 @@ export const createOrganization = async ({
|
||||
name,
|
||||
customerId
|
||||
}).save();
|
||||
|
||||
} else {
|
||||
organization = await new Organization({
|
||||
name,
|
||||
|
||||
+143
-171
@@ -4,13 +4,14 @@ import {
|
||||
DeleteSecretParams,
|
||||
GetSecretParams,
|
||||
GetSecretsParams,
|
||||
UpdateSecretParams,
|
||||
UpdateSecretParams
|
||||
} from "../interfaces/services/SecretService";
|
||||
import {
|
||||
ISecret,
|
||||
IServiceTokenData,
|
||||
Secret,
|
||||
SecretBlindIndexData,
|
||||
ServiceTokenData,
|
||||
ServiceTokenData
|
||||
} from "../models";
|
||||
import { SecretVersion } from "../ee/models";
|
||||
import {
|
||||
@@ -18,7 +19,7 @@ import {
|
||||
InternalServerError,
|
||||
SecretBlindIndexDataNotFoundError,
|
||||
SecretNotFoundError,
|
||||
UnauthorizedRequestError,
|
||||
UnauthorizedRequestError
|
||||
} from "../utils/errors";
|
||||
import {
|
||||
ACTION_ADD_SECRETS,
|
||||
@@ -29,51 +30,57 @@ import {
|
||||
ENCODING_SCHEME_BASE64,
|
||||
ENCODING_SCHEME_UTF8,
|
||||
SECRET_PERSONAL,
|
||||
SECRET_SHARED,
|
||||
SECRET_SHARED
|
||||
} from "../variables";
|
||||
import crypto from "crypto";
|
||||
import * as argon2 from "argon2";
|
||||
import {
|
||||
decryptSymmetric128BitHexKeyUTF8,
|
||||
encryptSymmetric128BitHexKeyUTF8,
|
||||
encryptSymmetric128BitHexKeyUTF8
|
||||
} from "../utils/crypto";
|
||||
import { TelemetryService } from "../services";
|
||||
import { client, getEncryptionKey, getRootEncryptionKey } from "../config";
|
||||
import { EELogService, EESecretService } from "../ee/services";
|
||||
import {
|
||||
getAuthDataPayloadIdObj,
|
||||
getAuthDataPayloadUserObj,
|
||||
} from "../utils/auth";
|
||||
import { getAuthDataPayloadIdObj, getAuthDataPayloadUserObj } from "../utils/auth";
|
||||
import { getFolderIdFromServiceToken } from "../services/FolderService";
|
||||
import picomatch from "picomatch";
|
||||
|
||||
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);
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns an object containing secret [secret] but with its value, key, comment decrypted.
|
||||
*
|
||||
*
|
||||
* Precondition: the workspace for secret [secret] must have E2EE disabled
|
||||
* @param {ISecret} secret - secret to repackage to raw
|
||||
* @param {String} key - symmetric key to use to decrypt secret
|
||||
* @returns
|
||||
* @returns
|
||||
*/
|
||||
export const repackageSecretToRaw = ({
|
||||
secret,
|
||||
key,
|
||||
}: {
|
||||
secret: ISecret;
|
||||
key: string;
|
||||
}) => {
|
||||
|
||||
export const repackageSecretToRaw = ({ secret, key }: { secret: ISecret; key: string }) => {
|
||||
const secretKey = decryptSymmetric128BitHexKeyUTF8({
|
||||
ciphertext: secret.secretKeyCiphertext,
|
||||
iv: secret.secretKeyIV,
|
||||
tag: secret.secretKeyTag,
|
||||
key,
|
||||
key
|
||||
});
|
||||
|
||||
const secretValue = decryptSymmetric128BitHexKeyUTF8({
|
||||
ciphertext: secret.secretValueCiphertext,
|
||||
iv: secret.secretValueIV,
|
||||
tag: secret.secretValueTag,
|
||||
key,
|
||||
key
|
||||
});
|
||||
|
||||
let secretComment = "";
|
||||
@@ -83,11 +90,11 @@ export const repackageSecretToRaw = ({
|
||||
ciphertext: secret.secretCommentCiphertext,
|
||||
iv: secret.secretCommentIV,
|
||||
tag: secret.secretCommentTag,
|
||||
key,
|
||||
key
|
||||
});
|
||||
}
|
||||
|
||||
return ({
|
||||
return {
|
||||
_id: secret._id,
|
||||
version: secret.version,
|
||||
workspace: secret.workspace,
|
||||
@@ -96,9 +103,9 @@ export const repackageSecretToRaw = ({
|
||||
user: secret.user,
|
||||
secretKey,
|
||||
secretValue,
|
||||
secretComment,
|
||||
});
|
||||
}
|
||||
secretComment
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Create secret blind index data containing encrypted blind index [salt]
|
||||
@@ -107,7 +114,7 @@ export const repackageSecretToRaw = ({
|
||||
* @param {Types.ObjectId} obj.workspaceId
|
||||
*/
|
||||
export const createSecretBlindIndexDataHelper = async ({
|
||||
workspaceId,
|
||||
workspaceId
|
||||
}: {
|
||||
workspaceId: Types.ObjectId;
|
||||
}) => {
|
||||
@@ -121,7 +128,7 @@ export const createSecretBlindIndexDataHelper = async ({
|
||||
const {
|
||||
ciphertext: encryptedSaltCiphertext,
|
||||
iv: saltIV,
|
||||
tag: saltTag,
|
||||
tag: saltTag
|
||||
} = client.encryptSymmetric(salt, rootEncryptionKey);
|
||||
|
||||
return await new SecretBlindIndexData({
|
||||
@@ -130,16 +137,16 @@ export const createSecretBlindIndexDataHelper = async ({
|
||||
saltIV,
|
||||
saltTag,
|
||||
algorithm: ALGORITHM_AES_256_GCM,
|
||||
keyEncoding: ENCODING_SCHEME_BASE64,
|
||||
keyEncoding: ENCODING_SCHEME_BASE64
|
||||
}).save();
|
||||
} else {
|
||||
const {
|
||||
ciphertext: encryptedSaltCiphertext,
|
||||
iv: saltIV,
|
||||
tag: saltTag,
|
||||
tag: saltTag
|
||||
} = encryptSymmetric128BitHexKeyUTF8({
|
||||
plaintext: salt,
|
||||
key: encryptionKey,
|
||||
key: encryptionKey
|
||||
});
|
||||
|
||||
return await new SecretBlindIndexData({
|
||||
@@ -148,7 +155,7 @@ export const createSecretBlindIndexDataHelper = async ({
|
||||
saltIV,
|
||||
saltTag,
|
||||
algorithm: ALGORITHM_AES_256_GCM,
|
||||
keyEncoding: ENCODING_SCHEME_UTF8,
|
||||
keyEncoding: ENCODING_SCHEME_UTF8
|
||||
}).save();
|
||||
}
|
||||
};
|
||||
@@ -160,7 +167,7 @@ export const createSecretBlindIndexDataHelper = async ({
|
||||
* @returns
|
||||
*/
|
||||
export const getSecretBlindIndexSaltHelper = async ({
|
||||
workspaceId,
|
||||
workspaceId
|
||||
}: {
|
||||
workspaceId: Types.ObjectId;
|
||||
}) => {
|
||||
@@ -168,36 +175,30 @@ export const getSecretBlindIndexSaltHelper = async ({
|
||||
const rootEncryptionKey = await getRootEncryptionKey();
|
||||
|
||||
const secretBlindIndexData = await SecretBlindIndexData.findOne({
|
||||
workspace: workspaceId,
|
||||
workspace: workspaceId
|
||||
}).select("+algorithm +keyEncoding");
|
||||
|
||||
if (!secretBlindIndexData) throw SecretBlindIndexDataNotFoundError();
|
||||
|
||||
if (
|
||||
rootEncryptionKey &&
|
||||
secretBlindIndexData.keyEncoding === ENCODING_SCHEME_BASE64
|
||||
) {
|
||||
if (rootEncryptionKey && secretBlindIndexData.keyEncoding === ENCODING_SCHEME_BASE64) {
|
||||
return client.decryptSymmetric(
|
||||
secretBlindIndexData.encryptedSaltCiphertext,
|
||||
rootEncryptionKey,
|
||||
secretBlindIndexData.saltIV,
|
||||
secretBlindIndexData.saltTag
|
||||
);
|
||||
} else if (
|
||||
encryptionKey &&
|
||||
secretBlindIndexData.keyEncoding === ENCODING_SCHEME_UTF8
|
||||
) {
|
||||
} else if (encryptionKey && secretBlindIndexData.keyEncoding === ENCODING_SCHEME_UTF8) {
|
||||
// decrypt workspace salt
|
||||
return decryptSymmetric128BitHexKeyUTF8({
|
||||
ciphertext: secretBlindIndexData.encryptedSaltCiphertext,
|
||||
iv: secretBlindIndexData.saltIV,
|
||||
tag: secretBlindIndexData.saltTag,
|
||||
key: encryptionKey,
|
||||
key: encryptionKey
|
||||
});
|
||||
}
|
||||
|
||||
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 +211,7 @@ export const getSecretBlindIndexSaltHelper = async ({
|
||||
*/
|
||||
export const generateSecretBlindIndexWithSaltHelper = async ({
|
||||
secretName,
|
||||
salt,
|
||||
salt
|
||||
}: {
|
||||
secretName: string;
|
||||
salt: string;
|
||||
@@ -224,7 +225,7 @@ export const generateSecretBlindIndexWithSaltHelper = async ({
|
||||
memoryCost: 65536, // default pool of 64 MiB per thread.
|
||||
hashLength: 32,
|
||||
parallelism: 1,
|
||||
raw: true,
|
||||
raw: true
|
||||
})
|
||||
).toString("base64");
|
||||
|
||||
@@ -240,7 +241,7 @@ export const generateSecretBlindIndexWithSaltHelper = async ({
|
||||
*/
|
||||
export const generateSecretBlindIndexHelper = async ({
|
||||
secretName,
|
||||
workspaceId,
|
||||
workspaceId
|
||||
}: {
|
||||
secretName: string;
|
||||
workspaceId: Types.ObjectId;
|
||||
@@ -250,16 +251,13 @@ export const generateSecretBlindIndexHelper = async ({
|
||||
const rootEncryptionKey = await getRootEncryptionKey();
|
||||
|
||||
const secretBlindIndexData = await SecretBlindIndexData.findOne({
|
||||
workspace: workspaceId,
|
||||
workspace: workspaceId
|
||||
}).select("+algorithm +keyEncoding");
|
||||
|
||||
if (!secretBlindIndexData) throw SecretBlindIndexDataNotFoundError();
|
||||
|
||||
let salt;
|
||||
if (
|
||||
rootEncryptionKey &&
|
||||
secretBlindIndexData.keyEncoding === ENCODING_SCHEME_BASE64
|
||||
) {
|
||||
if (rootEncryptionKey && secretBlindIndexData.keyEncoding === ENCODING_SCHEME_BASE64) {
|
||||
salt = client.decryptSymmetric(
|
||||
secretBlindIndexData.encryptedSaltCiphertext,
|
||||
rootEncryptionKey,
|
||||
@@ -269,32 +267,29 @@ export const generateSecretBlindIndexHelper = async ({
|
||||
|
||||
const secretBlindIndex = await generateSecretBlindIndexWithSaltHelper({
|
||||
secretName,
|
||||
salt,
|
||||
salt
|
||||
});
|
||||
|
||||
return secretBlindIndex;
|
||||
} else if (
|
||||
encryptionKey &&
|
||||
secretBlindIndexData.keyEncoding === ENCODING_SCHEME_UTF8
|
||||
) {
|
||||
} else if (encryptionKey && secretBlindIndexData.keyEncoding === ENCODING_SCHEME_UTF8) {
|
||||
// decrypt workspace salt
|
||||
salt = decryptSymmetric128BitHexKeyUTF8({
|
||||
ciphertext: secretBlindIndexData.encryptedSaltCiphertext,
|
||||
iv: secretBlindIndexData.saltIV,
|
||||
tag: secretBlindIndexData.saltTag,
|
||||
key: encryptionKey,
|
||||
key: encryptionKey
|
||||
});
|
||||
|
||||
const secretBlindIndex = await generateSecretBlindIndexWithSaltHelper({
|
||||
secretName,
|
||||
salt,
|
||||
salt
|
||||
});
|
||||
|
||||
return secretBlindIndex;
|
||||
}
|
||||
|
||||
throw InternalServerError({
|
||||
message: "Failed to generate secret blind index",
|
||||
message: "Failed to generate secret blind index"
|
||||
});
|
||||
};
|
||||
|
||||
@@ -323,38 +318,32 @@ export const createSecretHelper = async ({
|
||||
secretCommentCiphertext,
|
||||
secretCommentIV,
|
||||
secretCommentTag,
|
||||
secretPath = "/",
|
||||
secretPath = "/"
|
||||
}: CreateSecretParams) => {
|
||||
|
||||
const secretBlindIndex = await generateSecretBlindIndexHelper({
|
||||
secretName,
|
||||
workspaceId: new Types.ObjectId(workspaceId),
|
||||
workspaceId: new Types.ObjectId(workspaceId)
|
||||
});
|
||||
|
||||
// if using service token filter towards the folderId by secretpath
|
||||
if (authData.authPayload instanceof ServiceTokenData) {
|
||||
const { secretPath: serviceTkScopedSecretPath } = authData.authPayload;
|
||||
if (secretPath !== serviceTkScopedSecretPath) {
|
||||
if (!isValidScope(authData.authPayload, environment, secretPath)) {
|
||||
throw UnauthorizedRequestError({ message: "Folder Permission Denied" });
|
||||
}
|
||||
}
|
||||
const folderId = await getFolderIdFromServiceToken(
|
||||
workspaceId,
|
||||
environment,
|
||||
secretPath
|
||||
);
|
||||
const folderId = await getFolderIdFromServiceToken(workspaceId, environment, secretPath);
|
||||
|
||||
const exists = await Secret.exists({
|
||||
secretBlindIndex,
|
||||
workspace: new Types.ObjectId(workspaceId),
|
||||
folder: folderId,
|
||||
type,
|
||||
...(type === SECRET_PERSONAL ? getAuthDataPayloadUserObj(authData) : {}),
|
||||
...(type === SECRET_PERSONAL ? getAuthDataPayloadUserObj(authData) : {})
|
||||
});
|
||||
|
||||
if (exists)
|
||||
throw BadRequestError({
|
||||
message: "Failed to create secret that already exists",
|
||||
message: "Failed to create secret that already exists"
|
||||
});
|
||||
|
||||
if (type === SECRET_PERSONAL) {
|
||||
@@ -365,13 +354,12 @@ export const createSecretHelper = async ({
|
||||
secretBlindIndex,
|
||||
folder: folderId,
|
||||
workspace: new Types.ObjectId(workspaceId),
|
||||
type: SECRET_SHARED,
|
||||
type: SECRET_SHARED
|
||||
});
|
||||
|
||||
if (!exists)
|
||||
throw BadRequestError({
|
||||
message:
|
||||
"Failed to create personal secret override for no corresponding shared secret",
|
||||
message: "Failed to create personal secret override for no corresponding shared secret"
|
||||
});
|
||||
}
|
||||
|
||||
@@ -394,7 +382,7 @@ export const createSecretHelper = async ({
|
||||
secretCommentTag,
|
||||
folder: folderId,
|
||||
algorithm: ALGORITHM_AES_256_GCM,
|
||||
keyEncoding: ENCODING_SCHEME_UTF8,
|
||||
keyEncoding: ENCODING_SCHEME_UTF8
|
||||
}).save();
|
||||
|
||||
const secretVersion = new SecretVersion({
|
||||
@@ -414,12 +402,12 @@ export const createSecretHelper = async ({
|
||||
secretValueIV,
|
||||
secretValueTag,
|
||||
algorithm: ALGORITHM_AES_256_GCM,
|
||||
keyEncoding: ENCODING_SCHEME_UTF8,
|
||||
keyEncoding: ENCODING_SCHEME_UTF8
|
||||
});
|
||||
|
||||
// (EE) add version for new secret
|
||||
await EESecretService.addSecretVersions({
|
||||
secretVersions: [secretVersion],
|
||||
secretVersions: [secretVersion]
|
||||
});
|
||||
|
||||
// (EE) create (audit) log
|
||||
@@ -427,7 +415,7 @@ export const createSecretHelper = async ({
|
||||
name: ACTION_ADD_SECRETS,
|
||||
...getAuthDataPayloadIdObj(authData),
|
||||
workspaceId,
|
||||
secretIds: [secret._id],
|
||||
secretIds: [secret._id]
|
||||
});
|
||||
|
||||
action &&
|
||||
@@ -436,14 +424,14 @@ export const createSecretHelper = async ({
|
||||
workspaceId,
|
||||
actions: [action],
|
||||
channel: authData.authChannel,
|
||||
ipAddress: authData.authIP,
|
||||
ipAddress: authData.authIP
|
||||
}));
|
||||
|
||||
// (EE) take a secret snapshot
|
||||
await EESecretService.takeSecretSnapshot({
|
||||
workspaceId,
|
||||
environment,
|
||||
folderId,
|
||||
folderId
|
||||
});
|
||||
|
||||
const postHogClient = await TelemetryService.getPostHogClient();
|
||||
@@ -452,7 +440,7 @@ export const createSecretHelper = async ({
|
||||
postHogClient.capture({
|
||||
event: "secrets added",
|
||||
distinctId: await TelemetryService.getDistinctId({
|
||||
authData,
|
||||
authData
|
||||
}),
|
||||
properties: {
|
||||
numberOfSecrets: 1,
|
||||
@@ -460,8 +448,8 @@ export const createSecretHelper = async ({
|
||||
workspaceId,
|
||||
folderId,
|
||||
channel: authData.authChannel,
|
||||
userAgent: authData.authUserAgent,
|
||||
},
|
||||
userAgent: authData.authUserAgent
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -480,21 +468,16 @@ export const getSecretsHelper = async ({
|
||||
workspaceId,
|
||||
environment,
|
||||
authData,
|
||||
secretPath = "/",
|
||||
secretPath = "/"
|
||||
}: GetSecretsParams) => {
|
||||
let secrets: ISecret[] = [];
|
||||
// if using service token filter towards the folderId by secretpath
|
||||
if (authData.authPayload instanceof ServiceTokenData) {
|
||||
const { secretPath: serviceTkScopedSecretPath } = authData.authPayload;
|
||||
if (secretPath !== serviceTkScopedSecretPath) {
|
||||
if (!isValidScope(authData.authPayload, environment, secretPath)) {
|
||||
throw UnauthorizedRequestError({ message: "Folder Permission Denied" });
|
||||
}
|
||||
}
|
||||
const folderId = await getFolderIdFromServiceToken(
|
||||
workspaceId,
|
||||
environment,
|
||||
secretPath
|
||||
);
|
||||
const folderId = await getFolderIdFromServiceToken(workspaceId, environment, secretPath);
|
||||
|
||||
// get personal secrets first
|
||||
secrets = await Secret.find({
|
||||
@@ -502,8 +485,10 @@ export const getSecretsHelper = async ({
|
||||
environment,
|
||||
folder: folderId,
|
||||
type: SECRET_PERSONAL,
|
||||
...getAuthDataPayloadUserObj(authData),
|
||||
}).populate("tags").lean();
|
||||
...getAuthDataPayloadUserObj(authData)
|
||||
})
|
||||
.populate("tags")
|
||||
.lean();
|
||||
|
||||
// concat with shared secrets
|
||||
secrets = secrets.concat(
|
||||
@@ -513,9 +498,11 @@ export const getSecretsHelper = async ({
|
||||
folder: folderId,
|
||||
type: SECRET_SHARED,
|
||||
secretBlindIndex: {
|
||||
$nin: secrets.map((secret) => secret.secretBlindIndex),
|
||||
},
|
||||
}).populate("tags").lean()
|
||||
$nin: secrets.map((secret) => secret.secretBlindIndex)
|
||||
}
|
||||
})
|
||||
.populate("tags")
|
||||
.lean()
|
||||
);
|
||||
|
||||
// (EE) create (audit) log
|
||||
@@ -523,7 +510,7 @@ export const getSecretsHelper = async ({
|
||||
name: ACTION_READ_SECRETS,
|
||||
...getAuthDataPayloadIdObj(authData),
|
||||
workspaceId,
|
||||
secretIds: secrets.map((secret) => secret._id),
|
||||
secretIds: secrets.map((secret) => secret._id)
|
||||
});
|
||||
|
||||
action &&
|
||||
@@ -532,7 +519,7 @@ export const getSecretsHelper = async ({
|
||||
workspaceId,
|
||||
actions: [action],
|
||||
channel: authData.authChannel,
|
||||
ipAddress: authData.authIP,
|
||||
ipAddress: authData.authIP
|
||||
}));
|
||||
|
||||
const postHogClient = await TelemetryService.getPostHogClient();
|
||||
@@ -541,7 +528,7 @@ export const getSecretsHelper = async ({
|
||||
postHogClient.capture({
|
||||
event: "secrets pulled",
|
||||
distinctId: await TelemetryService.getDistinctId({
|
||||
authData,
|
||||
authData
|
||||
}),
|
||||
properties: {
|
||||
numberOfSecrets: secrets.length,
|
||||
@@ -549,8 +536,8 @@ export const getSecretsHelper = async ({
|
||||
workspaceId,
|
||||
folderId,
|
||||
channel: authData.authChannel,
|
||||
userAgent: authData.authUserAgent,
|
||||
},
|
||||
userAgent: authData.authUserAgent
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -573,25 +560,20 @@ export const getSecretHelper = async ({
|
||||
environment,
|
||||
type,
|
||||
authData,
|
||||
secretPath = "/",
|
||||
secretPath = "/"
|
||||
}: GetSecretParams) => {
|
||||
const secretBlindIndex = await generateSecretBlindIndexHelper({
|
||||
secretName,
|
||||
workspaceId: new Types.ObjectId(workspaceId),
|
||||
workspaceId: new Types.ObjectId(workspaceId)
|
||||
});
|
||||
let secret: ISecret | null = null;
|
||||
// if using service token filter towards the folderId by secretpath
|
||||
if (authData.authPayload instanceof ServiceTokenData) {
|
||||
const { secretPath: serviceTkScopedSecretPath } = authData.authPayload;
|
||||
if (secretPath !== serviceTkScopedSecretPath) {
|
||||
if (!isValidScope(authData.authPayload, environment, secretPath)) {
|
||||
throw UnauthorizedRequestError({ message: "Folder Permission Denied" });
|
||||
}
|
||||
}
|
||||
const folderId = await getFolderIdFromServiceToken(
|
||||
workspaceId,
|
||||
environment,
|
||||
secretPath
|
||||
);
|
||||
const folderId = await getFolderIdFromServiceToken(workspaceId, environment, secretPath);
|
||||
|
||||
// try getting personal secret first (if exists)
|
||||
secret = await Secret.findOne({
|
||||
@@ -600,7 +582,7 @@ export const getSecretHelper = async ({
|
||||
environment,
|
||||
folder: folderId,
|
||||
type: type ?? SECRET_PERSONAL,
|
||||
...(type === SECRET_PERSONAL ? getAuthDataPayloadUserObj(authData) : {}),
|
||||
...(type === SECRET_PERSONAL ? getAuthDataPayloadUserObj(authData) : {})
|
||||
}).lean();
|
||||
|
||||
if (!secret) {
|
||||
@@ -611,7 +593,7 @@ export const getSecretHelper = async ({
|
||||
workspace: new Types.ObjectId(workspaceId),
|
||||
environment,
|
||||
folder: folderId,
|
||||
type: SECRET_SHARED,
|
||||
type: SECRET_SHARED
|
||||
}).lean();
|
||||
}
|
||||
|
||||
@@ -622,7 +604,7 @@ export const getSecretHelper = async ({
|
||||
name: ACTION_READ_SECRETS,
|
||||
...getAuthDataPayloadIdObj(authData),
|
||||
workspaceId,
|
||||
secretIds: [secret._id],
|
||||
secretIds: [secret._id]
|
||||
});
|
||||
|
||||
action &&
|
||||
@@ -631,7 +613,7 @@ export const getSecretHelper = async ({
|
||||
workspaceId,
|
||||
actions: [action],
|
||||
channel: authData.authChannel,
|
||||
ipAddress: authData.authIP,
|
||||
ipAddress: authData.authIP
|
||||
}));
|
||||
|
||||
const postHogClient = await TelemetryService.getPostHogClient();
|
||||
@@ -640,7 +622,7 @@ export const getSecretHelper = async ({
|
||||
postHogClient.capture({
|
||||
event: "secrets pull",
|
||||
distinctId: await TelemetryService.getDistinctId({
|
||||
authData,
|
||||
authData
|
||||
}),
|
||||
properties: {
|
||||
numberOfSecrets: 1,
|
||||
@@ -648,8 +630,8 @@ export const getSecretHelper = async ({
|
||||
workspaceId,
|
||||
folderId,
|
||||
channel: authData.authChannel,
|
||||
userAgent: authData.authUserAgent,
|
||||
},
|
||||
userAgent: authData.authUserAgent
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -679,26 +661,21 @@ export const updateSecretHelper = async ({
|
||||
secretValueCiphertext,
|
||||
secretValueIV,
|
||||
secretValueTag,
|
||||
secretPath,
|
||||
secretPath
|
||||
}: UpdateSecretParams) => {
|
||||
const secretBlindIndex = await generateSecretBlindIndexHelper({
|
||||
secretName,
|
||||
workspaceId: new Types.ObjectId(workspaceId),
|
||||
workspaceId: new Types.ObjectId(workspaceId)
|
||||
});
|
||||
|
||||
let secret: ISecret | null = null;
|
||||
// if using service token filter towards the folderId by secretpath
|
||||
if (authData.authPayload instanceof ServiceTokenData) {
|
||||
const { secretPath: serviceTkScopedSecretPath } = authData.authPayload;
|
||||
if (secretPath !== serviceTkScopedSecretPath) {
|
||||
if (!isValidScope(authData.authPayload, environment, secretPath)) {
|
||||
throw UnauthorizedRequestError({ message: "Folder Permission Denied" });
|
||||
}
|
||||
}
|
||||
const folderId = await getFolderIdFromServiceToken(
|
||||
workspaceId,
|
||||
environment,
|
||||
secretPath
|
||||
);
|
||||
const folderId = await getFolderIdFromServiceToken(workspaceId, environment, secretPath);
|
||||
|
||||
if (type === SECRET_SHARED) {
|
||||
// case: update shared secret
|
||||
@@ -708,16 +685,16 @@ export const updateSecretHelper = async ({
|
||||
workspace: new Types.ObjectId(workspaceId),
|
||||
environment,
|
||||
folder: folderId,
|
||||
type,
|
||||
type
|
||||
},
|
||||
{
|
||||
secretValueCiphertext,
|
||||
secretValueIV,
|
||||
secretValueTag,
|
||||
$inc: { version: 1 },
|
||||
$inc: { version: 1 }
|
||||
},
|
||||
{
|
||||
new: true,
|
||||
new: true
|
||||
}
|
||||
);
|
||||
} else {
|
||||
@@ -730,16 +707,16 @@ export const updateSecretHelper = async ({
|
||||
environment,
|
||||
type,
|
||||
folder: folderId,
|
||||
...getAuthDataPayloadUserObj(authData),
|
||||
...getAuthDataPayloadUserObj(authData)
|
||||
},
|
||||
{
|
||||
secretValueCiphertext,
|
||||
secretValueIV,
|
||||
secretValueTag,
|
||||
$inc: { version: 1 },
|
||||
$inc: { version: 1 }
|
||||
},
|
||||
{
|
||||
new: true,
|
||||
new: true
|
||||
}
|
||||
);
|
||||
}
|
||||
@@ -763,12 +740,12 @@ export const updateSecretHelper = async ({
|
||||
secretValueIV,
|
||||
secretValueTag,
|
||||
algorithm: ALGORITHM_AES_256_GCM,
|
||||
keyEncoding: ENCODING_SCHEME_UTF8,
|
||||
keyEncoding: ENCODING_SCHEME_UTF8
|
||||
});
|
||||
|
||||
// (EE) add version for new secret
|
||||
await EESecretService.addSecretVersions({
|
||||
secretVersions: [secretVersion],
|
||||
secretVersions: [secretVersion]
|
||||
});
|
||||
|
||||
// (EE) create (audit) log
|
||||
@@ -776,7 +753,7 @@ export const updateSecretHelper = async ({
|
||||
name: ACTION_UPDATE_SECRETS,
|
||||
...getAuthDataPayloadIdObj(authData),
|
||||
workspaceId,
|
||||
secretIds: [secret._id],
|
||||
secretIds: [secret._id]
|
||||
});
|
||||
|
||||
action &&
|
||||
@@ -785,14 +762,14 @@ export const updateSecretHelper = async ({
|
||||
workspaceId,
|
||||
actions: [action],
|
||||
channel: authData.authChannel,
|
||||
ipAddress: authData.authIP,
|
||||
ipAddress: authData.authIP
|
||||
}));
|
||||
|
||||
// (EE) take a secret snapshot
|
||||
await EESecretService.takeSecretSnapshot({
|
||||
workspaceId,
|
||||
environment,
|
||||
folderId: secret?.folder,
|
||||
folderId: secret?.folder
|
||||
});
|
||||
|
||||
const postHogClient = await TelemetryService.getPostHogClient();
|
||||
@@ -801,7 +778,7 @@ export const updateSecretHelper = async ({
|
||||
postHogClient.capture({
|
||||
event: "secrets modified",
|
||||
distinctId: await TelemetryService.getDistinctId({
|
||||
authData,
|
||||
authData
|
||||
}),
|
||||
properties: {
|
||||
numberOfSecrets: 1,
|
||||
@@ -809,8 +786,8 @@ export const updateSecretHelper = async ({
|
||||
workspaceId,
|
||||
folderId,
|
||||
channel: authData.authChannel,
|
||||
userAgent: authData.authUserAgent,
|
||||
},
|
||||
userAgent: authData.authUserAgent
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -833,26 +810,20 @@ export const deleteSecretHelper = async ({
|
||||
environment,
|
||||
type,
|
||||
authData,
|
||||
secretPath = "/",
|
||||
secretPath = "/"
|
||||
}: DeleteSecretParams) => {
|
||||
const secretBlindIndex = await generateSecretBlindIndexHelper({
|
||||
secretName,
|
||||
workspaceId: new Types.ObjectId(workspaceId),
|
||||
workspaceId: new Types.ObjectId(workspaceId)
|
||||
});
|
||||
|
||||
// if using service token filter towards the folderId by secretpath
|
||||
if (authData.authPayload instanceof ServiceTokenData) {
|
||||
const { secretPath: serviceTkScopedSecretPath } = authData.authPayload;
|
||||
|
||||
if (secretPath !== serviceTkScopedSecretPath) {
|
||||
if (!isValidScope(authData.authPayload, environment, secretPath)) {
|
||||
throw UnauthorizedRequestError({ message: "Folder Permission Denied" });
|
||||
}
|
||||
}
|
||||
const folderId = await getFolderIdFromServiceToken(
|
||||
workspaceId,
|
||||
environment,
|
||||
secretPath
|
||||
);
|
||||
const folderId = await getFolderIdFromServiceToken(workspaceId, environment, secretPath);
|
||||
|
||||
let secrets: ISecret[] = [];
|
||||
let secret: ISecret | null = null;
|
||||
@@ -862,7 +833,7 @@ export const deleteSecretHelper = async ({
|
||||
secretBlindIndex,
|
||||
workspaceId: new Types.ObjectId(workspaceId),
|
||||
environment,
|
||||
folder: folderId,
|
||||
folder: folderId
|
||||
}).lean();
|
||||
|
||||
secret = await Secret.findOneAndDelete({
|
||||
@@ -870,14 +841,14 @@ export const deleteSecretHelper = async ({
|
||||
workspaceId: new Types.ObjectId(workspaceId),
|
||||
environment,
|
||||
type,
|
||||
folder: folderId,
|
||||
folder: folderId
|
||||
}).lean();
|
||||
|
||||
await Secret.deleteMany({
|
||||
secretBlindIndex,
|
||||
workspaceId: new Types.ObjectId(workspaceId),
|
||||
environment,
|
||||
folder: folderId,
|
||||
folder: folderId
|
||||
});
|
||||
} else {
|
||||
secret = await Secret.findOneAndDelete({
|
||||
@@ -886,7 +857,7 @@ export const deleteSecretHelper = async ({
|
||||
workspaceId: new Types.ObjectId(workspaceId),
|
||||
environment,
|
||||
type,
|
||||
...getAuthDataPayloadUserObj(authData),
|
||||
...getAuthDataPayloadUserObj(authData)
|
||||
}).lean();
|
||||
|
||||
if (secret) {
|
||||
@@ -897,7 +868,7 @@ export const deleteSecretHelper = async ({
|
||||
if (!secret) throw SecretNotFoundError();
|
||||
|
||||
await EESecretService.markDeletedSecretVersions({
|
||||
secretIds: secrets.map((secret) => secret._id),
|
||||
secretIds: secrets.map((secret) => secret._id)
|
||||
});
|
||||
|
||||
// (EE) create (audit) log
|
||||
@@ -905,22 +876,23 @@ export const deleteSecretHelper = async ({
|
||||
name: ACTION_DELETE_SECRETS,
|
||||
...getAuthDataPayloadIdObj(authData),
|
||||
workspaceId,
|
||||
secretIds: secrets.map((secret) => secret._id),
|
||||
secretIds: secrets.map((secret) => secret._id)
|
||||
});
|
||||
|
||||
action && (await EELogService.createLog({
|
||||
...getAuthDataPayloadIdObj(authData),
|
||||
workspaceId,
|
||||
actions: [action],
|
||||
channel: authData.authChannel,
|
||||
ipAddress: authData.authIP,
|
||||
}));
|
||||
action &&
|
||||
(await EELogService.createLog({
|
||||
...getAuthDataPayloadIdObj(authData),
|
||||
workspaceId,
|
||||
actions: [action],
|
||||
channel: authData.authChannel,
|
||||
ipAddress: authData.authIP
|
||||
}));
|
||||
|
||||
// (EE) take a secret snapshot
|
||||
await EESecretService.takeSecretSnapshot({
|
||||
workspaceId,
|
||||
environment,
|
||||
folderId: secret?.folder,
|
||||
folderId: secret?.folder
|
||||
});
|
||||
|
||||
const postHogClient = await TelemetryService.getPostHogClient();
|
||||
@@ -929,7 +901,7 @@ export const deleteSecretHelper = async ({
|
||||
postHogClient.capture({
|
||||
event: "secrets deleted",
|
||||
distinctId: await TelemetryService.getDistinctId({
|
||||
authData,
|
||||
authData
|
||||
}),
|
||||
properties: {
|
||||
numberOfSecrets: secrets.length,
|
||||
@@ -937,13 +909,13 @@ export const deleteSecretHelper = async ({
|
||||
workspaceId,
|
||||
folderId,
|
||||
channel: authData.authChannel,
|
||||
userAgent: authData.authUserAgent,
|
||||
},
|
||||
userAgent: authData.authUserAgent
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return ({
|
||||
return {
|
||||
secrets,
|
||||
secret,
|
||||
});
|
||||
secret
|
||||
};
|
||||
};
|
||||
|
||||
+39
-14
@@ -5,11 +5,12 @@ import express from "express";
|
||||
require("express-async-errors");
|
||||
import helmet from "helmet";
|
||||
import cors from "cors";
|
||||
import { DatabaseService } from "./services";
|
||||
import { DatabaseService, GithubSecretScanningService } from "./services";
|
||||
import { EELicenseService } from "./ee/services";
|
||||
import { setUpHealthEndpoint } from "./services/health";
|
||||
import cookieParser from "cookie-parser";
|
||||
import swaggerUi = require("swagger-ui-express");
|
||||
import { Probot, createNodeMiddleware } from "probot";
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const swaggerFile = require("../spec.json");
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
@@ -20,7 +21,7 @@ import {
|
||||
organizations as eeOrganizationsRouter,
|
||||
secret as eeSecretRouter,
|
||||
secretSnapshot as eeSecretSnapshotRouter,
|
||||
workspace as eeWorkspaceRouter,
|
||||
workspace as eeWorkspaceRouter
|
||||
} from "./ee/routes/v1";
|
||||
import {
|
||||
auth as v1AuthRouter,
|
||||
@@ -34,41 +35,45 @@ import {
|
||||
organization as v1OrganizationRouter,
|
||||
password as v1PasswordRouter,
|
||||
secret as v1SecretRouter,
|
||||
secretScanning as v1SecretScanningRouter,
|
||||
secretsFolder as v1SecretsFolder,
|
||||
serviceToken as v1ServiceTokenRouter,
|
||||
signup as v1SignupRouter,
|
||||
userAction as v1UserActionRouter,
|
||||
user as v1UserRouter,
|
||||
workspace as v1WorkspaceRouter,
|
||||
webhooks as v1WebhooksRouter,
|
||||
secretImport as v1SecretImportRouter
|
||||
} from "./routes/v1";
|
||||
import {
|
||||
signup as v2SignupRouter,
|
||||
auth as v2AuthRouter,
|
||||
users as v2UsersRouter,
|
||||
organizations as v2OrganizationsRouter,
|
||||
signup as v2SignupRouter,
|
||||
users as v2UsersRouter,
|
||||
workspace as v2WorkspaceRouter,
|
||||
secret as v2SecretRouter, // begin to phase out
|
||||
secrets as v2SecretsRouter,
|
||||
serviceTokenData as v2ServiceTokenDataRouter,
|
||||
serviceAccounts as v2ServiceAccountsRouter,
|
||||
apiKeyData as v2APIKeyDataRouter,
|
||||
environment as v2EnvironmentRouter,
|
||||
tags as v2TagsRouter,
|
||||
tags as v2TagsRouter
|
||||
} from "./routes/v2";
|
||||
import {
|
||||
auth as v3AuthRouter,
|
||||
secrets as v3SecretsRouter,
|
||||
signup as v3SignupRouter,
|
||||
workspaces as v3WorkspacesRouter,
|
||||
workspaces as v3WorkspacesRouter
|
||||
} from "./routes/v3";
|
||||
import { healthCheck } from "./routes/status";
|
||||
import { getLogger } from "./utils/logger";
|
||||
import { RouteNotFoundError } from "./utils/errors";
|
||||
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";
|
||||
const SmeeClient = require('smee-client') // eslint-disable-line
|
||||
|
||||
const main = async () => {
|
||||
|
||||
await setup();
|
||||
|
||||
await EELicenseService.initGlobalFeatureSet();
|
||||
@@ -80,10 +85,30 @@ const main = async () => {
|
||||
app.use(
|
||||
cors({
|
||||
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") {
|
||||
// enable app-wide rate-limiting + helmet security
|
||||
// in production
|
||||
@@ -125,6 +150,9 @@ const main = async () => {
|
||||
app.use("/api/v1/integration", v1IntegrationRouter);
|
||||
app.use("/api/v1/integration-auth", v1IntegrationAuthRouter);
|
||||
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)
|
||||
app.use("/api/v2/signup", v2SignupRouter);
|
||||
@@ -138,7 +166,6 @@ const main = async () => {
|
||||
app.use("/api/v2/secrets", v2SecretsRouter); // note: in the process of moving to v3/secrets
|
||||
app.use("/api/v2/service-token", v2ServiceTokenDataRouter);
|
||||
app.use("/api/v2/service-accounts", v2ServiceAccountsRouter); // new
|
||||
app.use("/api/v2/api-key", v2APIKeyDataRouter);
|
||||
|
||||
// v3 routes (experimental)
|
||||
app.use("/api/v3/auth", v3AuthRouter);
|
||||
@@ -157,7 +184,7 @@ const main = async () => {
|
||||
if (res.headersSent) return next();
|
||||
next(
|
||||
RouteNotFoundError({
|
||||
message: `The requested source '(${req.method})${req.url}' was not found`,
|
||||
message: `The requested source '(${req.method})${req.url}' was not found`
|
||||
})
|
||||
);
|
||||
});
|
||||
@@ -165,9 +192,7 @@ const main = async () => {
|
||||
app.use(requestErrorHandler);
|
||||
|
||||
const server = app.listen(await getPort(), async () => {
|
||||
(await getLogger("backend-main")).info(
|
||||
`Server started listening at port ${await getPort()}`
|
||||
);
|
||||
(await getLogger("backend-main")).info(`Server started listening at port ${await getPort()}`);
|
||||
});
|
||||
|
||||
// await createTestUserForDevelopment();
|
||||
|
||||
@@ -9,15 +9,17 @@ import {
|
||||
INTEGRATION_CHECKLY_API_URL,
|
||||
INTEGRATION_CIRCLECI,
|
||||
INTEGRATION_CIRCLECI_API_URL,
|
||||
INTEGRATION_CLOUDFLARE_PAGES,
|
||||
INTEGRATION_CLOUDFLARE_PAGES_API_URL,
|
||||
INTEGRATION_FLYIO,
|
||||
INTEGRATION_FLYIO_API_URL,
|
||||
INTEGRATION_GITHUB,
|
||||
INTEGRATION_GITLAB,
|
||||
INTEGRATION_CLOUDFLARE_PAGES,
|
||||
INTEGRATION_CLOUDFLARE_PAGES_API_URL,
|
||||
INTEGRATION_GITLAB_API_URL,
|
||||
INTEGRATION_HEROKU,
|
||||
INTEGRATION_HEROKU_API_URL,
|
||||
INTEGRATION_LARAVELFORGE,
|
||||
INTEGRATION_LARAVELFORGE_API_URL,
|
||||
INTEGRATION_NETLIFY,
|
||||
INTEGRATION_NETLIFY_API_URL,
|
||||
INTEGRATION_RAILWAY,
|
||||
@@ -119,6 +121,12 @@ const getApps = async ({
|
||||
accessToken,
|
||||
});
|
||||
break;
|
||||
case INTEGRATION_LARAVELFORGE:
|
||||
apps = await getAppsLaravelForge({
|
||||
accessToken,
|
||||
serverId: accessId
|
||||
});
|
||||
break;
|
||||
case INTEGRATION_TRAVISCI:
|
||||
apps = await getAppsTravisCI({
|
||||
accessToken,
|
||||
@@ -406,6 +414,40 @@ const getAppsRailway = async ({ accessToken }: { accessToken: string }) => {
|
||||
return apps;
|
||||
};
|
||||
|
||||
/**
|
||||
* Return list of sites for Laravel Forge integration
|
||||
* @param {Object} obj
|
||||
* @param {String} obj.accessToken - access token for Laravel Forge API
|
||||
* @param {String} obj.serverId - server id of Laravel Forge
|
||||
* @returns {Object[]} apps - names and ids of Laravel Forge sites
|
||||
* @returns {String} apps.name - name of Laravel Forge sites
|
||||
* @returns {String} apps.appId - id of Laravel Forge sites
|
||||
*/
|
||||
const getAppsLaravelForge = async ({
|
||||
accessToken,
|
||||
serverId
|
||||
}: {
|
||||
accessToken: string;
|
||||
serverId?: string;
|
||||
}) => {
|
||||
const res = (
|
||||
await standardRequest.get(`${INTEGRATION_LARAVELFORGE_API_URL}/api/v1/servers/${serverId}/sites`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
Accept: "application/json",
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
})
|
||||
).data.sites;
|
||||
|
||||
const apps = res.map((a: any) => ({
|
||||
name: a.name,
|
||||
appId: a.id,
|
||||
}));
|
||||
|
||||
return apps;
|
||||
};
|
||||
|
||||
/**
|
||||
* Return list of apps for Fly.io integration
|
||||
* @param {Object} obj
|
||||
|
||||
@@ -18,6 +18,8 @@ import {
|
||||
INTEGRATION_CHECKLY_API_URL,
|
||||
INTEGRATION_CIRCLECI,
|
||||
INTEGRATION_CIRCLECI_API_URL,
|
||||
INTEGRATION_CLOUDFLARE_PAGES,
|
||||
INTEGRATION_CLOUDFLARE_PAGES_API_URL,
|
||||
INTEGRATION_FLYIO,
|
||||
INTEGRATION_FLYIO_API_URL,
|
||||
INTEGRATION_GITHUB,
|
||||
@@ -26,6 +28,8 @@ import {
|
||||
INTEGRATION_HASHICORP_VAULT,
|
||||
INTEGRATION_HEROKU,
|
||||
INTEGRATION_HEROKU_API_URL,
|
||||
INTEGRATION_LARAVELFORGE,
|
||||
INTEGRATION_LARAVELFORGE_API_URL,
|
||||
INTEGRATION_NETLIFY,
|
||||
INTEGRATION_NETLIFY_API_URL,
|
||||
INTEGRATION_RAILWAY,
|
||||
@@ -34,8 +38,6 @@ import {
|
||||
INTEGRATION_RENDER_API_URL,
|
||||
INTEGRATION_SUPABASE,
|
||||
INTEGRATION_SUPABASE_API_URL,
|
||||
INTEGRATION_CLOUDFLARE_PAGES,
|
||||
INTEGRATION_CLOUDFLARE_PAGES_API_URL,
|
||||
INTEGRATION_TRAVISCI,
|
||||
INTEGRATION_TRAVISCI_API_URL,
|
||||
INTEGRATION_VERCEL,
|
||||
@@ -156,6 +158,14 @@ const syncSecrets = async ({
|
||||
accessToken,
|
||||
});
|
||||
break;
|
||||
case INTEGRATION_LARAVELFORGE:
|
||||
await syncSecretsLaravelForge({
|
||||
integration,
|
||||
secrets,
|
||||
accessId,
|
||||
accessToken,
|
||||
});
|
||||
break;
|
||||
case INTEGRATION_TRAVISCI:
|
||||
await syncSecretsTravisCI({
|
||||
integration,
|
||||
@@ -229,7 +239,7 @@ const syncSecrets = async ({
|
||||
accessToken,
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -1176,6 +1186,48 @@ const syncSecretsRender = async ({
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Sync/push [secrets] to Laravel Forge sites with id [integration.appId]
|
||||
* @param {Object} obj
|
||||
* @param {IIntegration} obj.integration - integration details
|
||||
* @param {Object} obj.secrets - secrets to push to integration (object where keys are secret keys and values are secret values)
|
||||
* @param {String} obj.accessToken - access token for Laravel Forge integration
|
||||
*/
|
||||
const syncSecretsLaravelForge = async ({
|
||||
integration,
|
||||
secrets,
|
||||
accessId,
|
||||
accessToken,
|
||||
}: {
|
||||
integration: IIntegration;
|
||||
secrets: any;
|
||||
accessId: string | null;
|
||||
accessToken: string;
|
||||
}) => {
|
||||
|
||||
function transformObjectToString(obj: any) {
|
||||
let result = "";
|
||||
for (const key in obj) {
|
||||
result += `${key}=${obj[key]}\n`;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
await standardRequest.put(
|
||||
`${INTEGRATION_LARAVELFORGE_API_URL}/api/v1/servers/${accessId}/sites/${integration.appId}/env`,
|
||||
{
|
||||
content: transformObjectToString(secrets),
|
||||
},
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
Accept: "application/json",
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Sync/push [secrets] to Railway project with id [integration.appId]
|
||||
* @param {Object} obj
|
||||
@@ -1883,7 +1935,7 @@ const syncSecretsCloudflarePages = async ({
|
||||
}
|
||||
)
|
||||
)
|
||||
.data.result['deployment_configs'][integration.targetEnvironment]['env_vars'];
|
||||
.data.result['deployment_configs'][integration.targetEnvironment]['env_vars'];
|
||||
|
||||
// copy the secrets object, so we can set deleted keys to null
|
||||
const secretsObj: any = { ...secrets };
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import * as Sentry from "@sentry/node";
|
||||
import { ErrorRequestHandler } from "express";
|
||||
import { InternalServerError } from "../utils/errors";
|
||||
import { TokenExpiredError } from "jsonwebtoken";
|
||||
import { InternalServerError, UnauthorizedRequestError } from "../utils/errors";
|
||||
import { getLogger } from "../utils/logger";
|
||||
import RequestError, { LogLevel } from "../utils/requestError";
|
||||
import { getNodeEnv } from "../config";
|
||||
@@ -19,7 +20,9 @@ export const requestErrorHandler: ErrorRequestHandler = async (
|
||||
}
|
||||
|
||||
//TODO: Find better way to type check for error. In current setting you need to cast type to get the functions and variables from RequestError
|
||||
if (!(error instanceof RequestError)) {
|
||||
if (error instanceof TokenExpiredError) {
|
||||
error = UnauthorizedRequestError({ stack: error.stack, message: "Token expired" });
|
||||
} else if (!(error instanceof RequestError)) {
|
||||
error = InternalServerError({
|
||||
context: { exception: error.message },
|
||||
stack: error.stack,
|
||||
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -16,13 +16,14 @@ import ServiceAccountKey, { IServiceAccountKey } from "./serviceAccountKey"; //
|
||||
import ServiceAccountOrganizationPermission, { IServiceAccountOrganizationPermission } from "./serviceAccountOrganizationPermission"; // new
|
||||
import ServiceAccountWorkspacePermission, { IServiceAccountWorkspacePermission } from "./serviceAccountWorkspacePermission"; // new
|
||||
import TokenData, { ITokenData } from "./tokenData";
|
||||
import User,{ AuthProvider, IUser } from "./user";
|
||||
import User, { AuthProvider, IUser } from "./user";
|
||||
import UserAction, { IUserAction } from "./userAction";
|
||||
import Workspace, { IWorkspace } from "./workspace";
|
||||
import ServiceTokenData, { IServiceTokenData } from "./serviceTokenData";
|
||||
import APIKeyData, { IAPIKeyData } from "./apiKeyData";
|
||||
import LoginSRPDetail, { ILoginSRPDetail } from "./loginSRPDetail";
|
||||
import TokenVersion, { ITokenVersion } from "./tokenVersion";
|
||||
import GitRisks, { STATUS_RESOLVED_FALSE_POSITIVE } from "./gitRisks";
|
||||
|
||||
export {
|
||||
AuthProvider,
|
||||
@@ -76,4 +77,6 @@ export {
|
||||
ILoginSRPDetail,
|
||||
TokenVersion,
|
||||
ITokenVersion,
|
||||
GitRisks,
|
||||
STATUS_RESOLVED_FALSE_POSITIVE
|
||||
};
|
||||
|
||||
@@ -5,16 +5,17 @@ import {
|
||||
INTEGRATION_AZURE_KEY_VAULT,
|
||||
INTEGRATION_CHECKLY,
|
||||
INTEGRATION_CIRCLECI,
|
||||
INTEGRATION_CLOUDFLARE_PAGES,
|
||||
INTEGRATION_FLYIO,
|
||||
INTEGRATION_GITHUB,
|
||||
INTEGRATION_GITLAB,
|
||||
INTEGRATION_HASHICORP_VAULT,
|
||||
INTEGRATION_HEROKU,
|
||||
INTEGRATION_LARAVELFORGE,
|
||||
INTEGRATION_NETLIFY,
|
||||
INTEGRATION_RAILWAY,
|
||||
INTEGRATION_RENDER,
|
||||
INTEGRATION_SUPABASE,
|
||||
INTEGRATION_CLOUDFLARE_PAGES,
|
||||
INTEGRATION_TRAVISCI,
|
||||
INTEGRATION_VERCEL,
|
||||
INTEGRATION_CODEFRESH
|
||||
@@ -49,6 +50,7 @@ export interface IIntegration {
|
||||
| "railway"
|
||||
| "flyio"
|
||||
| "circleci"
|
||||
| "laravel-forge"
|
||||
| "travisci"
|
||||
| "supabase"
|
||||
| "checkly"
|
||||
@@ -138,6 +140,7 @@ const integrationSchema = new Schema<IIntegration>(
|
||||
INTEGRATION_RAILWAY,
|
||||
INTEGRATION_FLYIO,
|
||||
INTEGRATION_CIRCLECI,
|
||||
INTEGRATION_LARAVELFORGE,
|
||||
INTEGRATION_TRAVISCI,
|
||||
INTEGRATION_SUPABASE,
|
||||
INTEGRATION_CHECKLY,
|
||||
|
||||
@@ -7,16 +7,17 @@ import {
|
||||
INTEGRATION_AWS_SECRET_MANAGER,
|
||||
INTEGRATION_AZURE_KEY_VAULT,
|
||||
INTEGRATION_CIRCLECI,
|
||||
INTEGRATION_CLOUDFLARE_PAGES,
|
||||
INTEGRATION_FLYIO,
|
||||
INTEGRATION_GITHUB,
|
||||
INTEGRATION_GITLAB,
|
||||
INTEGRATION_HASHICORP_VAULT,
|
||||
INTEGRATION_HEROKU,
|
||||
INTEGRATION_LARAVELFORGE,
|
||||
INTEGRATION_NETLIFY,
|
||||
INTEGRATION_RAILWAY,
|
||||
INTEGRATION_RENDER,
|
||||
INTEGRATION_SUPABASE,
|
||||
INTEGRATION_CLOUDFLARE_PAGES,
|
||||
INTEGRATION_TRAVISCI,
|
||||
INTEGRATION_VERCEL,
|
||||
INTEGRATION_CODEFRESH
|
||||
@@ -25,7 +26,7 @@ import {
|
||||
export interface IIntegrationAuth extends Document {
|
||||
_id: Types.ObjectId;
|
||||
workspace: Types.ObjectId;
|
||||
integration: 'heroku' | 'vercel' | 'netlify' | 'github' | 'gitlab' | 'render' | 'railway' | 'flyio' | 'azure-key-vault' | 'circleci' | 'travisci' | 'supabase' | 'aws-parameter-store' | 'aws-secret-manager' | 'checkly' | 'cloudflare-pages' | 'codefresh';
|
||||
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';
|
||||
teamId: string;
|
||||
accountId: string;
|
||||
url: string;
|
||||
@@ -66,6 +67,7 @@ const integrationAuthSchema = new Schema<IIntegrationAuth>(
|
||||
INTEGRATION_RAILWAY,
|
||||
INTEGRATION_FLYIO,
|
||||
INTEGRATION_CIRCLECI,
|
||||
INTEGRATION_LARAVELFORGE,
|
||||
INTEGRATION_TRAVISCI,
|
||||
INTEGRATION_SUPABASE,
|
||||
INTEGRATION_HASHICORP_VAULT,
|
||||
|
||||
@@ -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;
|
||||
@@ -4,7 +4,10 @@ export interface IServiceTokenData extends Document {
|
||||
_id: Types.ObjectId;
|
||||
name: string;
|
||||
workspace: Types.ObjectId;
|
||||
environment: string;
|
||||
scopes: Array<{
|
||||
environment: string;
|
||||
secretPath: string;
|
||||
}>;
|
||||
user: Types.ObjectId;
|
||||
serviceAccount: Types.ObjectId;
|
||||
lastUsed: Date;
|
||||
@@ -13,7 +16,6 @@ export interface IServiceTokenData extends Document {
|
||||
encryptedKey: string;
|
||||
iv: string;
|
||||
tag: string;
|
||||
secretPath: string;
|
||||
permissions: string[];
|
||||
}
|
||||
|
||||
@@ -21,68 +23,72 @@ const serviceTokenDataSchema = new Schema<IServiceTokenData>(
|
||||
{
|
||||
name: {
|
||||
type: String,
|
||||
required: true,
|
||||
required: true
|
||||
},
|
||||
workspace: {
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: "Workspace",
|
||||
required: true,
|
||||
required: true
|
||||
},
|
||||
environment: {
|
||||
type: String,
|
||||
required: true,
|
||||
scopes: {
|
||||
type: [
|
||||
{
|
||||
environment: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
secretPath: {
|
||||
type: String,
|
||||
default: "/",
|
||||
required: true
|
||||
}
|
||||
}
|
||||
],
|
||||
required: true
|
||||
},
|
||||
user: {
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: "User",
|
||||
required: true,
|
||||
required: true
|
||||
},
|
||||
serviceAccount: {
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: "ServiceAccount",
|
||||
ref: "ServiceAccount"
|
||||
},
|
||||
lastUsed: {
|
||||
type: Date,
|
||||
type: Date
|
||||
},
|
||||
expiresAt: {
|
||||
type: Date,
|
||||
type: Date
|
||||
},
|
||||
secretHash: {
|
||||
type: String,
|
||||
required: true,
|
||||
select: false,
|
||||
select: false
|
||||
},
|
||||
encryptedKey: {
|
||||
type: String,
|
||||
select: false,
|
||||
select: false
|
||||
},
|
||||
iv: {
|
||||
type: String,
|
||||
select: false,
|
||||
select: false
|
||||
},
|
||||
tag: {
|
||||
type: String,
|
||||
select: false,
|
||||
select: false
|
||||
},
|
||||
permissions: {
|
||||
type: [String],
|
||||
enum: ["read", "write"],
|
||||
default: ["read"],
|
||||
},
|
||||
secretPath: {
|
||||
type: String,
|
||||
default: "/",
|
||||
required: true,
|
||||
},
|
||||
default: ["read"]
|
||||
}
|
||||
},
|
||||
{
|
||||
timestamps: true,
|
||||
timestamps: true
|
||||
}
|
||||
);
|
||||
|
||||
const ServiceTokenData = model<IServiceTokenData>(
|
||||
"ServiceTokenData",
|
||||
serviceTokenDataSchema
|
||||
);
|
||||
const ServiceTokenData = model<IServiceTokenData>("ServiceTokenData", serviceTokenDataSchema);
|
||||
|
||||
export default ServiceTokenData;
|
||||
|
||||
@@ -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;
|
||||
@@ -1,5 +1,5 @@
|
||||
import express, { Request, Response } from "express";
|
||||
import { getSmtpConfigured } from "../../config";
|
||||
import { getSecretScanningGitAppId, getSecretScanningPrivateKey, getSecretScanningWebhookSecret, getSmtpConfigured } from "../../config";
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
@@ -10,6 +10,7 @@ router.get(
|
||||
date: new Date(),
|
||||
message: "Ok",
|
||||
emailConfigured: await getSmtpConfigured(),
|
||||
secretScanningConfigured: await getSecretScanningGitAppId() && await getSecretScanningWebhookSecret() && await getSecretScanningPrivateKey(),
|
||||
})
|
||||
}
|
||||
);
|
||||
|
||||
@@ -15,23 +15,29 @@ import password from "./password";
|
||||
import integration from "./integration";
|
||||
import integrationAuth from "./integrationAuth";
|
||||
import secretsFolder from "./secretsFolder";
|
||||
import secretScanning from "./secretScanning";
|
||||
import webhooks from "./webhook";
|
||||
import secretImport from "./secretImport";
|
||||
|
||||
export {
|
||||
signup,
|
||||
auth,
|
||||
bot,
|
||||
user,
|
||||
userAction,
|
||||
organization,
|
||||
workspace,
|
||||
membershipOrg,
|
||||
membership,
|
||||
key,
|
||||
inviteOrg,
|
||||
secret,
|
||||
serviceToken,
|
||||
password,
|
||||
integration,
|
||||
integrationAuth,
|
||||
secretsFolder,
|
||||
signup,
|
||||
auth,
|
||||
bot,
|
||||
user,
|
||||
userAction,
|
||||
organization,
|
||||
workspace,
|
||||
membershipOrg,
|
||||
membership,
|
||||
key,
|
||||
inviteOrg,
|
||||
secret,
|
||||
serviceToken,
|
||||
password,
|
||||
integration,
|
||||
integrationAuth,
|
||||
secretsFolder,
|
||||
secretScanning,
|
||||
webhooks,
|
||||
secretImport
|
||||
};
|
||||
|
||||
@@ -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.updateSecretImport
|
||||
);
|
||||
|
||||
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;
|
||||
@@ -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;
|
||||
@@ -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, AUTH_MODE_SERVICE_ACCOUNT, 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;
|
||||
@@ -1,42 +0,0 @@
|
||||
import express from "express";
|
||||
const router = express.Router();
|
||||
import { body, param } from "express-validator";
|
||||
import {
|
||||
requireAuth,
|
||||
validateRequest,
|
||||
} from "../../middleware";
|
||||
import { apiKeyDataController } from "../../controllers/v2";
|
||||
import {
|
||||
AUTH_MODE_JWT,
|
||||
} from "../../variables";
|
||||
|
||||
router.get(
|
||||
"/",
|
||||
requireAuth({
|
||||
acceptedAuthModes: [AUTH_MODE_JWT],
|
||||
}),
|
||||
apiKeyDataController.getAPIKeyData
|
||||
);
|
||||
|
||||
router.post(
|
||||
"/",
|
||||
requireAuth({
|
||||
acceptedAuthModes: [AUTH_MODE_JWT],
|
||||
}),
|
||||
body("name").exists().trim(),
|
||||
body("expiresIn"), // measured in ms
|
||||
validateRequest,
|
||||
apiKeyDataController.createAPIKeyData
|
||||
);
|
||||
|
||||
router.delete(
|
||||
"/:apiKeyDataId",
|
||||
requireAuth({
|
||||
acceptedAuthModes: [AUTH_MODE_JWT],
|
||||
}),
|
||||
param("apiKeyDataId").exists().trim(),
|
||||
validateRequest,
|
||||
apiKeyDataController.deleteAPIKeyData
|
||||
);
|
||||
|
||||
export default router;
|
||||
@@ -7,7 +7,6 @@ import secret from "./secret"; // deprecated
|
||||
import secrets from "./secrets";
|
||||
import serviceTokenData from "./serviceTokenData";
|
||||
import serviceAccounts from "./serviceAccounts";
|
||||
import apiKeyData from "./apiKeyData";
|
||||
import environment from "./environment"
|
||||
import tags from "./tags"
|
||||
|
||||
@@ -21,7 +20,6 @@ export {
|
||||
secrets,
|
||||
serviceTokenData,
|
||||
serviceAccounts,
|
||||
apiKeyData,
|
||||
environment,
|
||||
tags,
|
||||
}
|
||||
@@ -5,7 +5,7 @@ import {
|
||||
requireAuth,
|
||||
requireSecretsAuth,
|
||||
requireWorkspaceAuth,
|
||||
validateRequest,
|
||||
validateRequest
|
||||
} from "../../middleware";
|
||||
import { validateClientForSecrets } from "../../validation";
|
||||
import { body, query } from "express-validator";
|
||||
@@ -20,22 +20,18 @@ import {
|
||||
PERMISSION_READ_SECRETS,
|
||||
PERMISSION_WRITE_SECRETS,
|
||||
SECRET_PERSONAL,
|
||||
SECRET_SHARED,
|
||||
SECRET_SHARED
|
||||
} from "../../variables";
|
||||
import { BatchSecretRequest } from "../../types/secret";
|
||||
|
||||
router.post(
|
||||
"/batch",
|
||||
requireAuth({
|
||||
acceptedAuthModes: [
|
||||
AUTH_MODE_JWT,
|
||||
AUTH_MODE_API_KEY,
|
||||
AUTH_MODE_SERVICE_TOKEN,
|
||||
],
|
||||
acceptedAuthModes: [AUTH_MODE_JWT, AUTH_MODE_API_KEY, AUTH_MODE_SERVICE_TOKEN]
|
||||
}),
|
||||
requireWorkspaceAuth({
|
||||
acceptedRoles: [ADMIN, MEMBER],
|
||||
locationWorkspaceId: "body",
|
||||
locationWorkspaceId: "body"
|
||||
}),
|
||||
body("workspaceId").exists().isString().trim(),
|
||||
body("folderId").default("root").isString().trim(),
|
||||
@@ -52,10 +48,8 @@ router.post(
|
||||
if (secretIds.length > 0) {
|
||||
req.secrets = await validateClientForSecrets({
|
||||
authData: req.authData,
|
||||
secretIds: secretIds.map(
|
||||
(secretId: string) => new Types.ObjectId(secretId)
|
||||
),
|
||||
requiredPermissions: [],
|
||||
secretIds: secretIds.map((secretId: string) => new Types.ObjectId(secretId)),
|
||||
requiredPermissions: []
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -76,14 +70,11 @@ router.post(
|
||||
.custom((value) => {
|
||||
if (Array.isArray(value)) {
|
||||
// case: create multiple secrets
|
||||
if (value.length === 0)
|
||||
throw new Error("secrets cannot be an empty array");
|
||||
if (value.length === 0) throw new Error("secrets cannot be an empty array");
|
||||
for (const secret of value) {
|
||||
if (
|
||||
!secret.type ||
|
||||
!(
|
||||
secret.type === SECRET_PERSONAL || secret.type === SECRET_SHARED
|
||||
) ||
|
||||
!(secret.type === SECRET_PERSONAL || secret.type === SECRET_SHARED) ||
|
||||
!secret.secretKeyCiphertext ||
|
||||
!secret.secretKeyIV ||
|
||||
!secret.secretKeyTag ||
|
||||
@@ -108,9 +99,7 @@ router.post(
|
||||
!value.secretValueIV ||
|
||||
!value.secretValueTag
|
||||
) {
|
||||
throw new Error(
|
||||
"secrets object is missing required secret properties"
|
||||
);
|
||||
throw new Error("secrets object is missing required secret properties");
|
||||
}
|
||||
} else {
|
||||
throw new Error("secrets must be an object or an array of objects");
|
||||
@@ -120,17 +109,13 @@ router.post(
|
||||
}),
|
||||
validateRequest,
|
||||
requireAuth({
|
||||
acceptedAuthModes: [
|
||||
AUTH_MODE_JWT,
|
||||
AUTH_MODE_API_KEY,
|
||||
AUTH_MODE_SERVICE_TOKEN,
|
||||
],
|
||||
acceptedAuthModes: [AUTH_MODE_JWT, AUTH_MODE_API_KEY, AUTH_MODE_SERVICE_TOKEN]
|
||||
}),
|
||||
requireWorkspaceAuth({
|
||||
acceptedRoles: [ADMIN, MEMBER],
|
||||
locationWorkspaceId: "body",
|
||||
locationEnvironment: "body",
|
||||
requiredPermissions: [PERMISSION_WRITE_SECRETS],
|
||||
requiredPermissions: [PERMISSION_WRITE_SECRETS]
|
||||
}),
|
||||
secretsController.createSecrets
|
||||
);
|
||||
@@ -142,20 +127,21 @@ router.get(
|
||||
query("tagSlugs"),
|
||||
query("folderId").default("root").isString().trim(),
|
||||
query("secretPath").optional().isString().trim(),
|
||||
query("include_imports").optional().default(false).isBoolean(),
|
||||
validateRequest,
|
||||
requireAuth({
|
||||
acceptedAuthModes: [
|
||||
AUTH_MODE_JWT,
|
||||
AUTH_MODE_API_KEY,
|
||||
AUTH_MODE_SERVICE_TOKEN,
|
||||
AUTH_MODE_SERVICE_ACCOUNT,
|
||||
],
|
||||
AUTH_MODE_SERVICE_ACCOUNT
|
||||
]
|
||||
}),
|
||||
requireWorkspaceAuth({
|
||||
acceptedRoles: [ADMIN, MEMBER],
|
||||
locationWorkspaceId: "query",
|
||||
locationEnvironment: "query",
|
||||
requiredPermissions: [PERMISSION_READ_SECRETS],
|
||||
requiredPermissions: [PERMISSION_READ_SECRETS]
|
||||
}),
|
||||
secretsController.getSecrets
|
||||
);
|
||||
@@ -167,8 +153,7 @@ router.patch(
|
||||
.custom((value) => {
|
||||
if (Array.isArray(value)) {
|
||||
// case: update multiple secrets
|
||||
if (value.length === 0)
|
||||
throw new Error("secrets cannot be an empty array");
|
||||
if (value.length === 0) throw new Error("secrets cannot be an empty array");
|
||||
for (const secret of value) {
|
||||
if (!secret.id) {
|
||||
throw new Error("Each secret must contain a ID property");
|
||||
@@ -187,15 +172,11 @@ router.patch(
|
||||
}),
|
||||
validateRequest,
|
||||
requireAuth({
|
||||
acceptedAuthModes: [
|
||||
AUTH_MODE_JWT,
|
||||
AUTH_MODE_API_KEY,
|
||||
AUTH_MODE_SERVICE_TOKEN,
|
||||
],
|
||||
acceptedAuthModes: [AUTH_MODE_JWT, AUTH_MODE_API_KEY, AUTH_MODE_SERVICE_TOKEN]
|
||||
}),
|
||||
requireSecretsAuth({
|
||||
acceptedRoles: [ADMIN, MEMBER],
|
||||
requiredPermissions: [PERMISSION_WRITE_SECRETS],
|
||||
requiredPermissions: [PERMISSION_WRITE_SECRETS]
|
||||
}),
|
||||
secretsController.updateSecrets
|
||||
);
|
||||
@@ -210,8 +191,7 @@ router.delete(
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
// case: delete multiple secrets
|
||||
if (value.length === 0)
|
||||
throw new Error("secrets cannot be an empty array");
|
||||
if (value.length === 0) throw new Error("secrets cannot be an empty array");
|
||||
return value.every((id: string) => typeof id === "string");
|
||||
}
|
||||
|
||||
@@ -221,15 +201,11 @@ router.delete(
|
||||
.isEmpty(),
|
||||
validateRequest,
|
||||
requireAuth({
|
||||
acceptedAuthModes: [
|
||||
AUTH_MODE_JWT,
|
||||
AUTH_MODE_API_KEY,
|
||||
AUTH_MODE_SERVICE_TOKEN,
|
||||
],
|
||||
acceptedAuthModes: [AUTH_MODE_JWT, AUTH_MODE_API_KEY, AUTH_MODE_SERVICE_TOKEN]
|
||||
}),
|
||||
requireSecretsAuth({
|
||||
acceptedRoles: [ADMIN, MEMBER],
|
||||
requiredPermissions: [PERMISSION_WRITE_SECRETS],
|
||||
requiredPermissions: [PERMISSION_WRITE_SECRETS]
|
||||
}),
|
||||
secretsController.deleteSecrets
|
||||
);
|
||||
|
||||
@@ -4,7 +4,7 @@ import {
|
||||
requireAuth,
|
||||
requireServiceTokenDataAuth,
|
||||
requireWorkspaceAuth,
|
||||
validateRequest,
|
||||
validateRequest
|
||||
} from "../../middleware";
|
||||
import { body, param } from "express-validator";
|
||||
import {
|
||||
@@ -13,14 +13,14 @@ import {
|
||||
AUTH_MODE_SERVICE_ACCOUNT,
|
||||
AUTH_MODE_SERVICE_TOKEN,
|
||||
MEMBER,
|
||||
PERMISSION_WRITE_SECRETS,
|
||||
PERMISSION_WRITE_SECRETS
|
||||
} from "../../variables";
|
||||
import { serviceTokenDataController } from "../../controllers/v2";
|
||||
|
||||
router.get(
|
||||
"/",
|
||||
requireAuth({
|
||||
acceptedAuthModes: [AUTH_MODE_SERVICE_TOKEN],
|
||||
acceptedAuthModes: [AUTH_MODE_SERVICE_TOKEN]
|
||||
}),
|
||||
serviceTokenDataController.getServiceTokenData
|
||||
);
|
||||
@@ -28,33 +28,30 @@ router.get(
|
||||
router.post(
|
||||
"/",
|
||||
requireAuth({
|
||||
acceptedAuthModes: [AUTH_MODE_JWT, AUTH_MODE_SERVICE_ACCOUNT],
|
||||
acceptedAuthModes: [AUTH_MODE_JWT, AUTH_MODE_SERVICE_ACCOUNT]
|
||||
}),
|
||||
requireWorkspaceAuth({
|
||||
acceptedRoles: [ADMIN, MEMBER],
|
||||
locationWorkspaceId: "body",
|
||||
locationEnvironment: "body",
|
||||
requiredPermissions: [PERMISSION_WRITE_SECRETS],
|
||||
requiredPermissions: [PERMISSION_WRITE_SECRETS]
|
||||
}),
|
||||
body("name").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("iv").exists().isString().trim(),
|
||||
body("secretPath").isString().default("/").trim(),
|
||||
body("tag").exists().isString().trim(),
|
||||
body("expiresIn").exists().isNumeric(), // measured in ms
|
||||
body("permissions")
|
||||
.isArray({ min: 1 })
|
||||
.custom((value: string[]) => {
|
||||
const allowedPermissions = ["read", "write"];
|
||||
const invalidValues = value.filter(
|
||||
(v) => !allowedPermissions.includes(v)
|
||||
);
|
||||
const invalidValues = value.filter((v) => !allowedPermissions.includes(v));
|
||||
if (invalidValues.length > 0) {
|
||||
throw new Error(
|
||||
`permissions contains invalid values: ${invalidValues.join(", ")}`
|
||||
);
|
||||
throw new Error(`permissions contains invalid values: ${invalidValues.join(", ")}`);
|
||||
}
|
||||
|
||||
return true;
|
||||
@@ -66,10 +63,10 @@ router.post(
|
||||
router.delete(
|
||||
"/:serviceTokenDataId",
|
||||
requireAuth({
|
||||
acceptedAuthModes: [AUTH_MODE_JWT],
|
||||
acceptedAuthModes: [AUTH_MODE_JWT]
|
||||
}),
|
||||
requireServiceTokenDataAuth({
|
||||
acceptedRoles: [ADMIN, MEMBER],
|
||||
acceptedRoles: [ADMIN, MEMBER]
|
||||
}),
|
||||
param("serviceTokenDataId").exists().trim(),
|
||||
validateRequest,
|
||||
|
||||
@@ -4,7 +4,7 @@ import {
|
||||
requireAuth,
|
||||
validateRequest,
|
||||
} from "../../middleware";
|
||||
import { body } from "express-validator";
|
||||
import { body, param } from "express-validator";
|
||||
import { usersController } from "../../controllers/v2";
|
||||
import {
|
||||
AUTH_MODE_API_KEY,
|
||||
@@ -29,6 +29,16 @@ router.patch(
|
||||
usersController.updateMyMfaEnabled
|
||||
);
|
||||
|
||||
router.patch(
|
||||
"/me/name",
|
||||
requireAuth({
|
||||
acceptedAuthModes: [AUTH_MODE_JWT, AUTH_MODE_API_KEY],
|
||||
}),
|
||||
body("firstName").exists(),
|
||||
validateRequest,
|
||||
usersController.updateName
|
||||
);
|
||||
|
||||
router.get(
|
||||
"/me/organizations",
|
||||
requireAuth({
|
||||
@@ -37,4 +47,49 @@ router.get(
|
||||
usersController.getMyOrganizations
|
||||
);
|
||||
|
||||
router.get(
|
||||
"/me/api-keys",
|
||||
requireAuth({
|
||||
acceptedAuthModes: [AUTH_MODE_JWT],
|
||||
}),
|
||||
usersController.getMyAPIKeys
|
||||
);
|
||||
|
||||
router.post(
|
||||
"/me/api-keys",
|
||||
requireAuth({
|
||||
acceptedAuthModes: [AUTH_MODE_JWT],
|
||||
}),
|
||||
body("name").exists().isString().trim(),
|
||||
body("expiresIn").isNumeric(),
|
||||
validateRequest,
|
||||
usersController.createAPIKey
|
||||
);
|
||||
|
||||
router.delete(
|
||||
"/me/api-keys/:apiKeyDataId",
|
||||
requireAuth({
|
||||
acceptedAuthModes: [AUTH_MODE_JWT],
|
||||
}),
|
||||
param("apiKeyDataId").exists().trim(),
|
||||
validateRequest,
|
||||
usersController.deleteAPIKey
|
||||
);
|
||||
|
||||
router.get( // new
|
||||
"/me/sessions",
|
||||
requireAuth({
|
||||
acceptedAuthModes: [AUTH_MODE_JWT],
|
||||
}),
|
||||
usersController.getMySessions
|
||||
);
|
||||
|
||||
router.delete( // new
|
||||
"/me/sessions",
|
||||
requireAuth({
|
||||
acceptedAuthModes: [AUTH_MODE_JWT],
|
||||
}),
|
||||
usersController.deleteMySessions
|
||||
);
|
||||
|
||||
export default router;
|
||||
@@ -1,10 +1,6 @@
|
||||
import express from "express";
|
||||
const router = express.Router();
|
||||
import {
|
||||
requireAuth,
|
||||
requireWorkspaceAuth,
|
||||
validateRequest,
|
||||
} from "../../middleware";
|
||||
import { requireAuth, requireWorkspaceAuth, validateRequest } from "../../middleware";
|
||||
import { body, param, query } from "express-validator";
|
||||
import { secretsController } from "../../controllers/v3";
|
||||
import {
|
||||
@@ -17,7 +13,7 @@ import {
|
||||
PERMISSION_READ_SECRETS,
|
||||
PERMISSION_WRITE_SECRETS,
|
||||
SECRET_PERSONAL,
|
||||
SECRET_SHARED,
|
||||
SECRET_SHARED
|
||||
} from "../../variables";
|
||||
|
||||
router.get(
|
||||
@@ -25,14 +21,15 @@ router.get(
|
||||
query("workspaceId").exists().isString().trim(),
|
||||
query("environment").exists().isString().trim(),
|
||||
query("secretPath").default("/").isString().trim(),
|
||||
query("include_imports").isBoolean().default(false),
|
||||
validateRequest,
|
||||
requireAuth({
|
||||
acceptedAuthModes: [
|
||||
AUTH_MODE_JWT,
|
||||
AUTH_MODE_API_KEY,
|
||||
AUTH_MODE_SERVICE_TOKEN,
|
||||
AUTH_MODE_SERVICE_ACCOUNT,
|
||||
],
|
||||
AUTH_MODE_SERVICE_ACCOUNT
|
||||
]
|
||||
}),
|
||||
requireWorkspaceAuth({
|
||||
acceptedRoles: [ADMIN, MEMBER],
|
||||
@@ -40,7 +37,7 @@ router.get(
|
||||
locationEnvironment: "query",
|
||||
requiredPermissions: [PERMISSION_READ_SECRETS],
|
||||
requireBlindIndicesEnabled: true,
|
||||
requireE2EEOff: true,
|
||||
requireE2EEOff: true
|
||||
}),
|
||||
secretsController.getSecretsRaw
|
||||
);
|
||||
@@ -58,8 +55,8 @@ router.get(
|
||||
AUTH_MODE_JWT,
|
||||
AUTH_MODE_API_KEY,
|
||||
AUTH_MODE_SERVICE_TOKEN,
|
||||
AUTH_MODE_SERVICE_ACCOUNT,
|
||||
],
|
||||
AUTH_MODE_SERVICE_ACCOUNT
|
||||
]
|
||||
}),
|
||||
requireWorkspaceAuth({
|
||||
acceptedRoles: [ADMIN, MEMBER],
|
||||
@@ -67,7 +64,7 @@ router.get(
|
||||
locationEnvironment: "query",
|
||||
requiredPermissions: [PERMISSION_READ_SECRETS],
|
||||
requireBlindIndicesEnabled: true,
|
||||
requireE2EEOff: true,
|
||||
requireE2EEOff: true
|
||||
}),
|
||||
secretsController.getSecretByNameRaw
|
||||
);
|
||||
@@ -86,8 +83,8 @@ router.post(
|
||||
AUTH_MODE_JWT,
|
||||
AUTH_MODE_API_KEY,
|
||||
AUTH_MODE_SERVICE_TOKEN,
|
||||
AUTH_MODE_SERVICE_ACCOUNT,
|
||||
],
|
||||
AUTH_MODE_SERVICE_ACCOUNT
|
||||
]
|
||||
}),
|
||||
requireWorkspaceAuth({
|
||||
acceptedRoles: [ADMIN, MEMBER],
|
||||
@@ -95,7 +92,7 @@ router.post(
|
||||
locationEnvironment: "body",
|
||||
requiredPermissions: [PERMISSION_WRITE_SECRETS],
|
||||
requireBlindIndicesEnabled: true,
|
||||
requireE2EEOff: true,
|
||||
requireE2EEOff: true
|
||||
}),
|
||||
secretsController.createSecretRaw
|
||||
);
|
||||
@@ -114,8 +111,8 @@ router.patch(
|
||||
AUTH_MODE_JWT,
|
||||
AUTH_MODE_API_KEY,
|
||||
AUTH_MODE_SERVICE_TOKEN,
|
||||
AUTH_MODE_SERVICE_ACCOUNT,
|
||||
],
|
||||
AUTH_MODE_SERVICE_ACCOUNT
|
||||
]
|
||||
}),
|
||||
requireWorkspaceAuth({
|
||||
acceptedRoles: [ADMIN, MEMBER],
|
||||
@@ -123,7 +120,7 @@ router.patch(
|
||||
locationEnvironment: "body",
|
||||
requiredPermissions: [PERMISSION_WRITE_SECRETS],
|
||||
requireBlindIndicesEnabled: true,
|
||||
requireE2EEOff: true,
|
||||
requireE2EEOff: true
|
||||
}),
|
||||
secretsController.updateSecretByNameRaw
|
||||
);
|
||||
@@ -141,8 +138,8 @@ router.delete(
|
||||
AUTH_MODE_JWT,
|
||||
AUTH_MODE_API_KEY,
|
||||
AUTH_MODE_SERVICE_TOKEN,
|
||||
AUTH_MODE_SERVICE_ACCOUNT,
|
||||
],
|
||||
AUTH_MODE_SERVICE_ACCOUNT
|
||||
]
|
||||
}),
|
||||
requireWorkspaceAuth({
|
||||
acceptedRoles: [ADMIN, MEMBER],
|
||||
@@ -150,7 +147,7 @@ router.delete(
|
||||
locationEnvironment: "body",
|
||||
requiredPermissions: [PERMISSION_WRITE_SECRETS],
|
||||
requireBlindIndicesEnabled: true,
|
||||
requireE2EEOff: true,
|
||||
requireE2EEOff: true
|
||||
}),
|
||||
secretsController.deleteSecretByNameRaw
|
||||
);
|
||||
@@ -166,8 +163,8 @@ router.get(
|
||||
AUTH_MODE_JWT,
|
||||
AUTH_MODE_API_KEY,
|
||||
AUTH_MODE_SERVICE_TOKEN,
|
||||
AUTH_MODE_SERVICE_ACCOUNT,
|
||||
],
|
||||
AUTH_MODE_SERVICE_ACCOUNT
|
||||
]
|
||||
}),
|
||||
requireWorkspaceAuth({
|
||||
acceptedRoles: [ADMIN, MEMBER],
|
||||
@@ -175,7 +172,7 @@ router.get(
|
||||
locationEnvironment: "query",
|
||||
requiredPermissions: [PERMISSION_READ_SECRETS],
|
||||
requireBlindIndicesEnabled: true,
|
||||
requireE2EEOff: false,
|
||||
requireE2EEOff: false
|
||||
}),
|
||||
secretsController.getSecrets
|
||||
);
|
||||
@@ -201,8 +198,8 @@ router.post(
|
||||
AUTH_MODE_JWT,
|
||||
AUTH_MODE_API_KEY,
|
||||
AUTH_MODE_SERVICE_TOKEN,
|
||||
AUTH_MODE_SERVICE_ACCOUNT,
|
||||
],
|
||||
AUTH_MODE_SERVICE_ACCOUNT
|
||||
]
|
||||
}),
|
||||
requireWorkspaceAuth({
|
||||
acceptedRoles: [ADMIN, MEMBER],
|
||||
@@ -210,7 +207,7 @@ router.post(
|
||||
locationEnvironment: "body",
|
||||
requiredPermissions: [PERMISSION_WRITE_SECRETS],
|
||||
requireBlindIndicesEnabled: true,
|
||||
requireE2EEOff: false,
|
||||
requireE2EEOff: false
|
||||
}),
|
||||
secretsController.createSecret
|
||||
);
|
||||
@@ -228,15 +225,15 @@ router.get(
|
||||
AUTH_MODE_JWT,
|
||||
AUTH_MODE_API_KEY,
|
||||
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,
|
||||
requireBlindIndicesEnabled: true
|
||||
}),
|
||||
secretsController.getSecretByName
|
||||
);
|
||||
@@ -257,8 +254,8 @@ router.patch(
|
||||
AUTH_MODE_JWT,
|
||||
AUTH_MODE_API_KEY,
|
||||
AUTH_MODE_SERVICE_TOKEN,
|
||||
AUTH_MODE_SERVICE_ACCOUNT,
|
||||
],
|
||||
AUTH_MODE_SERVICE_ACCOUNT
|
||||
]
|
||||
}),
|
||||
requireWorkspaceAuth({
|
||||
acceptedRoles: [ADMIN, MEMBER],
|
||||
@@ -266,7 +263,7 @@ router.patch(
|
||||
locationEnvironment: "body",
|
||||
requiredPermissions: [PERMISSION_WRITE_SECRETS],
|
||||
requireBlindIndicesEnabled: true,
|
||||
requireE2EEOff: false,
|
||||
requireE2EEOff: false
|
||||
}),
|
||||
secretsController.updateSecretByName
|
||||
);
|
||||
@@ -284,8 +281,8 @@ router.delete(
|
||||
AUTH_MODE_JWT,
|
||||
AUTH_MODE_API_KEY,
|
||||
AUTH_MODE_SERVICE_TOKEN,
|
||||
AUTH_MODE_SERVICE_ACCOUNT,
|
||||
],
|
||||
AUTH_MODE_SERVICE_ACCOUNT
|
||||
]
|
||||
}),
|
||||
requireWorkspaceAuth({
|
||||
acceptedRoles: [ADMIN, MEMBER],
|
||||
@@ -293,7 +290,7 @@ router.delete(
|
||||
locationEnvironment: "body",
|
||||
requiredPermissions: [PERMISSION_WRITE_SECRETS],
|
||||
requireBlindIndicesEnabled: true,
|
||||
requireE2EEOff: false,
|
||||
requireE2EEOff: false
|
||||
}),
|
||||
secretsController.deleteSecretByName
|
||||
);
|
||||
|
||||
@@ -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,82 @@
|
||||
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
|
||||
});
|
||||
}
|
||||
});
|
||||
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;
|
||||
};
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}))
|
||||
]);
|
||||
};
|
||||
@@ -6,13 +6,15 @@ import EventService from "./EventService";
|
||||
import IntegrationService from "./IntegrationService";
|
||||
import TokenService from "./TokenService";
|
||||
import SecretService from "./SecretService";
|
||||
import GithubSecretScanningService from "./GithubSecretScanningService"
|
||||
|
||||
export {
|
||||
TelemetryService,
|
||||
DatabaseService,
|
||||
BotService,
|
||||
EventService,
|
||||
IntegrationService,
|
||||
TokenService,
|
||||
SecretService,
|
||||
}
|
||||
TelemetryService,
|
||||
DatabaseService,
|
||||
BotService,
|
||||
EventService,
|
||||
IntegrationService,
|
||||
TokenService,
|
||||
SecretService,
|
||||
GithubSecretScanningService
|
||||
};
|
||||
@@ -2,9 +2,10 @@ import nodemailer from "nodemailer";
|
||||
import {
|
||||
SMTP_HOST_GMAIL,
|
||||
SMTP_HOST_MAILGUN,
|
||||
SMTP_HOST_OFFICE365,
|
||||
SMTP_HOST_SENDGRID,
|
||||
SMTP_HOST_SOCKETLABS,
|
||||
SMTP_HOST_ZOHOMAIL,
|
||||
SMTP_HOST_ZOHOMAIL
|
||||
} from "../variables";
|
||||
import SMTPConnection from "nodemailer/lib/smtp-connection";
|
||||
import * as Sentry from "@sentry/node";
|
||||
@@ -15,6 +16,7 @@ import {
|
||||
getSmtpSecure,
|
||||
getSmtpUsername,
|
||||
} from "../config";
|
||||
import { getLogger } from "../utils/logger";
|
||||
|
||||
export const initSmtp = async () => {
|
||||
const mailOpts: SMTPConnection.Options = {
|
||||
@@ -58,6 +60,12 @@ export const initSmtp = async () => {
|
||||
ciphers: "TLSv1.2",
|
||||
}
|
||||
break;
|
||||
case SMTP_HOST_OFFICE365:
|
||||
mailOpts.requireTLS = true;
|
||||
mailOpts.tls = {
|
||||
ciphers: "TLSv1.2"
|
||||
}
|
||||
break;
|
||||
default:
|
||||
if ((await getSmtpHost()).includes("amazonaws.com")) {
|
||||
mailOpts.tls = {
|
||||
@@ -73,10 +81,12 @@ export const initSmtp = async () => {
|
||||
const transporter = nodemailer.createTransport(mailOpts);
|
||||
transporter
|
||||
.verify()
|
||||
.then((err) => {
|
||||
.then(async () => {
|
||||
Sentry.setUser(null);
|
||||
Sentry.captureMessage("SMTP - Successfully connected");
|
||||
console.log("SMTP - Successfully connected")
|
||||
(await getLogger("backend-main")).info(
|
||||
"SMTP - Successfully connected"
|
||||
);
|
||||
})
|
||||
.catch(async (err) => {
|
||||
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>
|
||||
@@ -27,7 +27,7 @@ export const UnauthorizedRequestError = (error?: Partial<RequestErrorContext>) =
|
||||
context: error?.context,
|
||||
stack: error?.stack,
|
||||
});
|
||||
|
||||
|
||||
export const ForbiddenRequestError = (error?: Partial<RequestErrorContext>) => new RequestError({
|
||||
logLevel: error?.logLevel ?? LogLevel.INFO,
|
||||
statusCode: error?.statusCode ?? 403,
|
||||
@@ -46,6 +46,15 @@ export const BadRequestError = (error?: Partial<RequestErrorContext>) => new Req
|
||||
stack: error?.stack,
|
||||
});
|
||||
|
||||
export const ResourceNotFound = (error?: Partial<RequestErrorContext>) => new RequestError({
|
||||
logLevel: error?.logLevel ?? LogLevel.INFO,
|
||||
statusCode: error?.statusCode ?? 404,
|
||||
type: error?.type ?? "resource_not_found",
|
||||
message: error?.message ?? "The requested resource is not found",
|
||||
context: error?.context,
|
||||
stack: error?.stack,
|
||||
});
|
||||
|
||||
export const InternalServerError = (error?: Partial<RequestErrorContext>) => new RequestError({
|
||||
logLevel: error?.logLevel ?? LogLevel.ERROR,
|
||||
statusCode: error?.statusCode ?? 500,
|
||||
@@ -229,6 +238,6 @@ export const BotNotFoundError = (error?: Partial<RequestErrorContext>) => new Re
|
||||
message: error?.message ?? "The requested bot was not found",
|
||||
context: error?.context,
|
||||
stack: error?.stack,
|
||||
})
|
||||
})
|
||||
|
||||
//* ----->[MISC ERRORS]<-----
|
||||
|
||||
@@ -13,14 +13,14 @@ import {
|
||||
Secret,
|
||||
SecretBlindIndexData,
|
||||
ServiceTokenData,
|
||||
Workspace,
|
||||
Workspace
|
||||
} from "../../models";
|
||||
import { generateKeyPair } from "../../utils/crypto";
|
||||
import { client, getEncryptionKey, getRootEncryptionKey } from "../../config";
|
||||
import {
|
||||
ALGORITHM_AES_256_GCM,
|
||||
ENCODING_SCHEME_BASE64,
|
||||
ENCODING_SCHEME_UTF8,
|
||||
ENCODING_SCHEME_UTF8
|
||||
} from "../../variables";
|
||||
import { InternalServerError } from "../errors";
|
||||
|
||||
@@ -29,10 +29,7 @@ import { InternalServerError } from "../errors";
|
||||
* corresponding secret versions
|
||||
*/
|
||||
export const backfillSecretVersions = async () => {
|
||||
await Secret.updateMany(
|
||||
{ version: { $exists: false } },
|
||||
{ $set: { version: 1 } }
|
||||
);
|
||||
await Secret.updateMany({ version: { $exists: false } }, { $set: { version: 1 } });
|
||||
|
||||
const unversionedSecrets: ISecret[] = await Secret.aggregate([
|
||||
{
|
||||
@@ -40,14 +37,14 @@ export const backfillSecretVersions = async () => {
|
||||
from: "secretversions",
|
||||
localField: "_id",
|
||||
foreignField: "secret",
|
||||
as: "versions",
|
||||
},
|
||||
as: "versions"
|
||||
}
|
||||
},
|
||||
{
|
||||
$match: {
|
||||
versions: { $size: 0 },
|
||||
},
|
||||
},
|
||||
versions: { $size: 0 }
|
||||
}
|
||||
}
|
||||
]);
|
||||
|
||||
if (unversionedSecrets.length > 0) {
|
||||
@@ -62,9 +59,9 @@ export const backfillSecretVersions = async () => {
|
||||
workspace: s.workspace,
|
||||
environment: s.environment,
|
||||
algorithm: ALGORITHM_AES_256_GCM,
|
||||
keyEncoding: ENCODING_SCHEME_UTF8,
|
||||
keyEncoding: ENCODING_SCHEME_UTF8
|
||||
})
|
||||
),
|
||||
)
|
||||
});
|
||||
}
|
||||
console.log("Migration: Secret version migration v1 complete");
|
||||
@@ -80,8 +77,8 @@ export const backfillBots = async () => {
|
||||
const workspaceIdsWithBot = await Bot.distinct("workspace");
|
||||
const workspaceIdsToAddBot = await Workspace.distinct("_id", {
|
||||
_id: {
|
||||
$nin: workspaceIdsWithBot,
|
||||
},
|
||||
$nin: workspaceIdsWithBot
|
||||
}
|
||||
});
|
||||
|
||||
if (workspaceIdsToAddBot.length === 0) return;
|
||||
@@ -94,7 +91,7 @@ export const backfillBots = async () => {
|
||||
const {
|
||||
ciphertext: encryptedPrivateKey,
|
||||
iv,
|
||||
tag,
|
||||
tag
|
||||
} = client.encryptSymmetric(privateKey, rootEncryptionKey);
|
||||
|
||||
return new Bot({
|
||||
@@ -106,16 +103,16 @@ export const backfillBots = async () => {
|
||||
iv,
|
||||
tag,
|
||||
algorithm: ALGORITHM_AES_256_GCM,
|
||||
keyEncoding: ENCODING_SCHEME_BASE64,
|
||||
keyEncoding: ENCODING_SCHEME_BASE64
|
||||
});
|
||||
} else if (encryptionKey) {
|
||||
const {
|
||||
ciphertext: encryptedPrivateKey,
|
||||
iv,
|
||||
tag,
|
||||
tag
|
||||
} = encryptSymmetric128BitHexKeyUTF8({
|
||||
plaintext: privateKey,
|
||||
key: encryptionKey,
|
||||
key: encryptionKey
|
||||
});
|
||||
|
||||
return new Bot({
|
||||
@@ -127,13 +124,12 @@ export const backfillBots = async () => {
|
||||
iv,
|
||||
tag,
|
||||
algorithm: ALGORITHM_AES_256_GCM,
|
||||
keyEncoding: ENCODING_SCHEME_UTF8,
|
||||
keyEncoding: ENCODING_SCHEME_UTF8
|
||||
});
|
||||
}
|
||||
|
||||
throw InternalServerError({
|
||||
message:
|
||||
"Failed to backfill workspace bots due to missing encryption key",
|
||||
message: "Failed to backfill workspace bots due to missing encryption key"
|
||||
});
|
||||
})
|
||||
);
|
||||
@@ -149,13 +145,11 @@ export const backfillSecretBlindIndexData = async () => {
|
||||
const encryptionKey = await getEncryptionKey();
|
||||
const rootEncryptionKey = await getRootEncryptionKey();
|
||||
|
||||
const workspaceIdsBlindIndexed = await SecretBlindIndexData.distinct(
|
||||
"workspace"
|
||||
);
|
||||
const workspaceIdsBlindIndexed = await SecretBlindIndexData.distinct("workspace");
|
||||
const workspaceIdsToBlindIndex = await Workspace.distinct("_id", {
|
||||
_id: {
|
||||
$nin: workspaceIdsBlindIndexed,
|
||||
},
|
||||
$nin: workspaceIdsBlindIndexed
|
||||
}
|
||||
});
|
||||
|
||||
if (workspaceIdsToBlindIndex.length === 0) return;
|
||||
@@ -168,7 +162,7 @@ export const backfillSecretBlindIndexData = async () => {
|
||||
const {
|
||||
ciphertext: encryptedSaltCiphertext,
|
||||
iv: saltIV,
|
||||
tag: saltTag,
|
||||
tag: saltTag
|
||||
} = client.encryptSymmetric(salt, rootEncryptionKey);
|
||||
|
||||
return new SecretBlindIndexData({
|
||||
@@ -177,16 +171,16 @@ export const backfillSecretBlindIndexData = async () => {
|
||||
saltIV,
|
||||
saltTag,
|
||||
algorithm: ALGORITHM_AES_256_GCM,
|
||||
keyEncoding: ENCODING_SCHEME_BASE64,
|
||||
keyEncoding: ENCODING_SCHEME_BASE64
|
||||
});
|
||||
} else if (encryptionKey) {
|
||||
const {
|
||||
ciphertext: encryptedSaltCiphertext,
|
||||
iv: saltIV,
|
||||
tag: saltTag,
|
||||
tag: saltTag
|
||||
} = encryptSymmetric128BitHexKeyUTF8({
|
||||
plaintext: salt,
|
||||
key: encryptionKey,
|
||||
key: encryptionKey
|
||||
});
|
||||
|
||||
return new SecretBlindIndexData({
|
||||
@@ -195,13 +189,12 @@ export const backfillSecretBlindIndexData = async () => {
|
||||
saltIV,
|
||||
saltTag,
|
||||
algorithm: ALGORITHM_AES_256_GCM,
|
||||
keyEncoding: ENCODING_SCHEME_UTF8,
|
||||
keyEncoding: ENCODING_SCHEME_UTF8
|
||||
});
|
||||
}
|
||||
|
||||
throw InternalServerError({
|
||||
message:
|
||||
"Failed to backfill secret blind index data due to missing encryption key",
|
||||
message: "Failed to backfill secret blind index data due to missing encryption key"
|
||||
});
|
||||
})
|
||||
);
|
||||
@@ -219,17 +212,17 @@ export const backfillEncryptionMetadata = async () => {
|
||||
await Secret.updateMany(
|
||||
{
|
||||
algorithm: {
|
||||
$exists: false,
|
||||
$exists: false
|
||||
},
|
||||
keyEncoding: {
|
||||
$exists: false,
|
||||
},
|
||||
$exists: false
|
||||
}
|
||||
},
|
||||
{
|
||||
$set: {
|
||||
algorithm: ALGORITHM_AES_256_GCM,
|
||||
keyEncoding: ENCODING_SCHEME_UTF8,
|
||||
},
|
||||
keyEncoding: ENCODING_SCHEME_UTF8
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
@@ -237,17 +230,17 @@ export const backfillEncryptionMetadata = async () => {
|
||||
await SecretVersion.updateMany(
|
||||
{
|
||||
algorithm: {
|
||||
$exists: false,
|
||||
$exists: false
|
||||
},
|
||||
keyEncoding: {
|
||||
$exists: false,
|
||||
},
|
||||
$exists: false
|
||||
}
|
||||
},
|
||||
{
|
||||
$set: {
|
||||
algorithm: ALGORITHM_AES_256_GCM,
|
||||
keyEncoding: ENCODING_SCHEME_UTF8,
|
||||
},
|
||||
keyEncoding: ENCODING_SCHEME_UTF8
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
@@ -255,17 +248,17 @@ export const backfillEncryptionMetadata = async () => {
|
||||
await SecretBlindIndexData.updateMany(
|
||||
{
|
||||
algorithm: {
|
||||
$exists: false,
|
||||
$exists: false
|
||||
},
|
||||
keyEncoding: {
|
||||
$exists: false,
|
||||
},
|
||||
$exists: false
|
||||
}
|
||||
},
|
||||
{
|
||||
$set: {
|
||||
algorithm: ALGORITHM_AES_256_GCM,
|
||||
keyEncoding: ENCODING_SCHEME_UTF8,
|
||||
},
|
||||
keyEncoding: ENCODING_SCHEME_UTF8
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
@@ -273,17 +266,17 @@ export const backfillEncryptionMetadata = async () => {
|
||||
await Bot.updateMany(
|
||||
{
|
||||
algorithm: {
|
||||
$exists: false,
|
||||
$exists: false
|
||||
},
|
||||
keyEncoding: {
|
||||
$exists: false,
|
||||
},
|
||||
$exists: false
|
||||
}
|
||||
},
|
||||
{
|
||||
$set: {
|
||||
algorithm: ALGORITHM_AES_256_GCM,
|
||||
keyEncoding: ENCODING_SCHEME_UTF8,
|
||||
},
|
||||
keyEncoding: ENCODING_SCHEME_UTF8
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
@@ -291,17 +284,17 @@ export const backfillEncryptionMetadata = async () => {
|
||||
await BackupPrivateKey.updateMany(
|
||||
{
|
||||
algorithm: {
|
||||
$exists: false,
|
||||
$exists: false
|
||||
},
|
||||
keyEncoding: {
|
||||
$exists: false,
|
||||
},
|
||||
$exists: false
|
||||
}
|
||||
},
|
||||
{
|
||||
$set: {
|
||||
algorithm: ALGORITHM_AES_256_GCM,
|
||||
keyEncoding: ENCODING_SCHEME_UTF8,
|
||||
},
|
||||
keyEncoding: ENCODING_SCHEME_UTF8
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
@@ -309,17 +302,17 @@ export const backfillEncryptionMetadata = async () => {
|
||||
await IntegrationAuth.updateMany(
|
||||
{
|
||||
algorithm: {
|
||||
$exists: false,
|
||||
$exists: false
|
||||
},
|
||||
keyEncoding: {
|
||||
$exists: false,
|
||||
},
|
||||
$exists: false
|
||||
}
|
||||
},
|
||||
{
|
||||
$set: {
|
||||
algorithm: ALGORITHM_AES_256_GCM,
|
||||
keyEncoding: ENCODING_SCHEME_UTF8,
|
||||
},
|
||||
keyEncoding: ENCODING_SCHEME_UTF8
|
||||
}
|
||||
}
|
||||
);
|
||||
};
|
||||
@@ -328,26 +321,26 @@ export const backfillSecretFolders = async () => {
|
||||
await Secret.updateMany(
|
||||
{
|
||||
folder: {
|
||||
$exists: false,
|
||||
},
|
||||
$exists: false
|
||||
}
|
||||
},
|
||||
{
|
||||
$set: {
|
||||
folder: "root",
|
||||
},
|
||||
folder: "root"
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
await SecretVersion.updateMany(
|
||||
{
|
||||
folder: {
|
||||
$exists: false,
|
||||
},
|
||||
$exists: false
|
||||
}
|
||||
},
|
||||
{
|
||||
$set: {
|
||||
folder: "root",
|
||||
},
|
||||
folder: "root"
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
@@ -355,20 +348,20 @@ export const backfillSecretFolders = async () => {
|
||||
await SecretVersion.updateMany(
|
||||
{
|
||||
tags: {
|
||||
$exists: false,
|
||||
},
|
||||
$exists: false
|
||||
}
|
||||
},
|
||||
{
|
||||
$set: {
|
||||
tags: [],
|
||||
},
|
||||
tags: []
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
let secretSnapshots = await SecretSnapshot.find({
|
||||
environment: {
|
||||
$exists: false,
|
||||
},
|
||||
$exists: false
|
||||
}
|
||||
})
|
||||
.populate<{ secretVersions: ISecretVersion[] }>("secretVersions")
|
||||
.limit(50);
|
||||
@@ -377,8 +370,7 @@ export const backfillSecretFolders = async () => {
|
||||
for (const secSnapshot of secretSnapshots) {
|
||||
const groupSnapByEnv: Record<string, Array<ISecretVersion>> = {};
|
||||
secSnapshot.secretVersions.forEach((secVer) => {
|
||||
if (!groupSnapByEnv?.[secVer.environment])
|
||||
groupSnapByEnv[secVer.environment] = [];
|
||||
if (!groupSnapByEnv?.[secVer.environment]) groupSnapByEnv[secVer.environment] = [];
|
||||
groupSnapByEnv[secVer.environment].push(secVer);
|
||||
});
|
||||
|
||||
@@ -390,7 +382,7 @@ export const backfillSecretFolders = async () => {
|
||||
...secSnapshot.toObject({ virtuals: false }),
|
||||
_id: new Types.ObjectId(),
|
||||
environment: snapEnv,
|
||||
secretVersions: secretIdsOfEnvGroup,
|
||||
secretVersions: secretIdsOfEnvGroup
|
||||
};
|
||||
});
|
||||
|
||||
@@ -400,8 +392,8 @@ export const backfillSecretFolders = async () => {
|
||||
|
||||
secretSnapshots = await SecretSnapshot.find({
|
||||
environment: {
|
||||
$exists: false,
|
||||
},
|
||||
$exists: false
|
||||
}
|
||||
})
|
||||
.populate<{ secretVersions: ISecretVersion[] }>("secretVersions")
|
||||
.limit(50);
|
||||
@@ -414,13 +406,13 @@ export const backfillServiceToken = async () => {
|
||||
await ServiceTokenData.updateMany(
|
||||
{
|
||||
secretPath: {
|
||||
$exists: false,
|
||||
},
|
||||
$exists: false
|
||||
}
|
||||
},
|
||||
{
|
||||
$set: {
|
||||
secretPath: "/",
|
||||
},
|
||||
secretPath: "/"
|
||||
}
|
||||
}
|
||||
);
|
||||
console.log("Migration: Service token migration v1 complete");
|
||||
@@ -430,14 +422,33 @@ export const backfillIntegration = async () => {
|
||||
await Integration.updateMany(
|
||||
{
|
||||
secretPath: {
|
||||
$exists: false,
|
||||
},
|
||||
$exists: false
|
||||
}
|
||||
},
|
||||
{
|
||||
$set: {
|
||||
secretPath: "/",
|
||||
},
|
||||
secretPath: "/"
|
||||
}
|
||||
}
|
||||
);
|
||||
console.log("Migration: Integration migration v1 complete");
|
||||
};
|
||||
|
||||
export const backfillServiceTokenMultiScope = async () => {
|
||||
await ServiceTokenData.updateMany(
|
||||
{
|
||||
scopes: {
|
||||
$exists: false
|
||||
}
|
||||
},
|
||||
[
|
||||
{
|
||||
$set: {
|
||||
scopes: [{ environment: "$environment", secretPath: "$secretPath" }]
|
||||
}
|
||||
}
|
||||
]
|
||||
);
|
||||
|
||||
console.log("Migration: Service token migration v2 complete");
|
||||
};
|
||||
|
||||
@@ -14,17 +14,15 @@ import {
|
||||
backfillSecretFolders,
|
||||
backfillSecretVersions,
|
||||
backfillServiceToken,
|
||||
backfillServiceTokenMultiScope
|
||||
} from "./backfillData";
|
||||
import {
|
||||
reencryptBotPrivateKeys,
|
||||
reencryptSecretBlindIndexDataSalts,
|
||||
} from "./reencryptData";
|
||||
import { reencryptBotPrivateKeys, reencryptSecretBlindIndexDataSalts } from "./reencryptData";
|
||||
import {
|
||||
getClientIdGoogle,
|
||||
getClientSecretGoogle,
|
||||
getMongoURL,
|
||||
getNodeEnv,
|
||||
getSentryDSN,
|
||||
getSentryDSN
|
||||
} from "../../config";
|
||||
import { initializePassport } from "../auth";
|
||||
|
||||
@@ -79,6 +77,7 @@ export const setup = async () => {
|
||||
await backfillSecretFolders();
|
||||
await backfillServiceToken();
|
||||
await backfillIntegration();
|
||||
await backfillServiceTokenMultiScope();
|
||||
|
||||
// re-encrypt any data previously encrypted under server hex 128-bit ENCRYPTION_KEY
|
||||
// to base64 256-bit ROOT_ENCRYPTION_KEY
|
||||
@@ -90,7 +89,7 @@ export const setup = async () => {
|
||||
dsn: await getSentryDSN(),
|
||||
tracesSampleRate: 1.0,
|
||||
debug: (await getNodeEnv()) === "production" ? false : true,
|
||||
environment: await getNodeEnv(),
|
||||
environment: await getNodeEnv()
|
||||
});
|
||||
|
||||
await createTestUserForDevelopment();
|
||||
|
||||
@@ -1,22 +1,19 @@
|
||||
import { Types } from "mongoose";
|
||||
import {
|
||||
ISecret,
|
||||
IServiceAccount,
|
||||
IServiceTokenData,
|
||||
IUser,
|
||||
ServiceAccount,
|
||||
ServiceTokenData,
|
||||
User,
|
||||
ISecret,
|
||||
IServiceAccount,
|
||||
IServiceTokenData,
|
||||
IUser,
|
||||
ServiceAccount,
|
||||
ServiceTokenData,
|
||||
User
|
||||
} from "../models";
|
||||
import {
|
||||
ServiceTokenDataNotFoundError,
|
||||
UnauthorizedRequestError,
|
||||
} from "../utils/errors";
|
||||
import { ServiceTokenDataNotFoundError, UnauthorizedRequestError } from "../utils/errors";
|
||||
import {
|
||||
AUTH_MODE_API_KEY,
|
||||
AUTH_MODE_JWT,
|
||||
AUTH_MODE_SERVICE_ACCOUNT,
|
||||
AUTH_MODE_SERVICE_TOKEN,
|
||||
AUTH_MODE_API_KEY,
|
||||
AUTH_MODE_JWT,
|
||||
AUTH_MODE_SERVICE_ACCOUNT,
|
||||
AUTH_MODE_SERVICE_TOKEN
|
||||
} from "../variables";
|
||||
import { validateUserClientForWorkspace } from "./user";
|
||||
import { validateServiceAccountClientForWorkspace } from "./serviceAccount";
|
||||
@@ -30,65 +27,71 @@ import { validateServiceAccountClientForWorkspace } from "./serviceAccount";
|
||||
* @param {Array<'admin' | 'member'>} obj.acceptedRoles - accepted workspace roles
|
||||
*/
|
||||
export const validateClientForServiceTokenData = async ({
|
||||
authData,
|
||||
serviceTokenDataId,
|
||||
acceptedRoles,
|
||||
authData,
|
||||
serviceTokenDataId,
|
||||
acceptedRoles
|
||||
}: {
|
||||
authData: {
|
||||
authMode: string;
|
||||
authPayload: IUser | IServiceAccount | IServiceTokenData;
|
||||
};
|
||||
serviceTokenDataId: Types.ObjectId;
|
||||
acceptedRoles: Array<"admin" | "member">;
|
||||
authData: {
|
||||
authMode: string;
|
||||
authPayload: IUser | IServiceAccount | IServiceTokenData;
|
||||
};
|
||||
serviceTokenDataId: Types.ObjectId;
|
||||
acceptedRoles: Array<"admin" | "member">;
|
||||
}) => {
|
||||
const serviceTokenData = await ServiceTokenData
|
||||
.findById(serviceTokenDataId)
|
||||
.select("+encryptedKey +iv +tag")
|
||||
.populate<{ user: IUser }>("user");
|
||||
const serviceTokenData = await ServiceTokenData.findById(serviceTokenDataId)
|
||||
.select("+encryptedKey +iv +tag")
|
||||
.populate<{ user: IUser }>("user");
|
||||
|
||||
if (!serviceTokenData) throw ServiceTokenDataNotFoundError({
|
||||
message: "Failed to find service token data",
|
||||
if (!serviceTokenData)
|
||||
throw ServiceTokenDataNotFoundError({
|
||||
message: "Failed to find service token data"
|
||||
});
|
||||
|
||||
if (authData.authMode === AUTH_MODE_JWT && authData.authPayload instanceof User) {
|
||||
await validateUserClientForWorkspace({
|
||||
user: authData.authPayload,
|
||||
workspaceId: serviceTokenData.workspace,
|
||||
acceptedRoles,
|
||||
});
|
||||
|
||||
return serviceTokenData;
|
||||
}
|
||||
if (authData.authMode === AUTH_MODE_JWT && authData.authPayload instanceof User) {
|
||||
await validateUserClientForWorkspace({
|
||||
user: authData.authPayload,
|
||||
workspaceId: serviceTokenData.workspace,
|
||||
acceptedRoles
|
||||
});
|
||||
|
||||
if (authData.authMode === AUTH_MODE_SERVICE_ACCOUNT && authData.authPayload instanceof ServiceAccount) {
|
||||
await validateServiceAccountClientForWorkspace({
|
||||
serviceAccount: authData.authPayload,
|
||||
workspaceId: serviceTokenData.workspace,
|
||||
});
|
||||
|
||||
return serviceTokenData;
|
||||
}
|
||||
return serviceTokenData;
|
||||
}
|
||||
|
||||
if (authData.authMode === AUTH_MODE_SERVICE_TOKEN && authData.authPayload instanceof ServiceTokenData) {
|
||||
throw UnauthorizedRequestError({
|
||||
message: "Failed service token authorization for service token data",
|
||||
});
|
||||
}
|
||||
if (
|
||||
authData.authMode === AUTH_MODE_SERVICE_ACCOUNT &&
|
||||
authData.authPayload instanceof ServiceAccount
|
||||
) {
|
||||
await validateServiceAccountClientForWorkspace({
|
||||
serviceAccount: authData.authPayload,
|
||||
workspaceId: serviceTokenData.workspace
|
||||
});
|
||||
|
||||
if (authData.authMode === AUTH_MODE_API_KEY && authData.authPayload instanceof User) {
|
||||
await validateUserClientForWorkspace({
|
||||
user: authData.authPayload,
|
||||
workspaceId: serviceTokenData.workspace,
|
||||
acceptedRoles,
|
||||
});
|
||||
|
||||
return serviceTokenData;
|
||||
}
|
||||
|
||||
return serviceTokenData;
|
||||
}
|
||||
|
||||
if (
|
||||
authData.authMode === AUTH_MODE_SERVICE_TOKEN &&
|
||||
authData.authPayload instanceof ServiceTokenData
|
||||
) {
|
||||
throw UnauthorizedRequestError({
|
||||
message: "Failed client authorization for service token data",
|
||||
message: "Failed service token authorization for service token data"
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (authData.authMode === AUTH_MODE_API_KEY && authData.authPayload instanceof User) {
|
||||
await validateUserClientForWorkspace({
|
||||
user: authData.authPayload,
|
||||
workspaceId: serviceTokenData.workspace,
|
||||
acceptedRoles
|
||||
});
|
||||
|
||||
return serviceTokenData;
|
||||
}
|
||||
|
||||
throw UnauthorizedRequestError({
|
||||
message: "Failed client authorization for service token data"
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Validate that service token (client) can access workspace
|
||||
@@ -101,42 +104,42 @@ export const validateClientForServiceTokenData = async ({
|
||||
* @param {String[]} requiredPermissions - required permissions as part of the endpoint
|
||||
*/
|
||||
export const validateServiceTokenDataClientForWorkspace = async ({
|
||||
serviceTokenData,
|
||||
workspaceId,
|
||||
environment,
|
||||
requiredPermissions,
|
||||
serviceTokenData,
|
||||
workspaceId,
|
||||
environment,
|
||||
requiredPermissions
|
||||
}: {
|
||||
serviceTokenData: IServiceTokenData;
|
||||
workspaceId: Types.ObjectId;
|
||||
environment?: string;
|
||||
requiredPermissions?: string[];
|
||||
serviceTokenData: IServiceTokenData;
|
||||
workspaceId: Types.ObjectId;
|
||||
environment?: string;
|
||||
requiredPermissions?: string[];
|
||||
}) => {
|
||||
if (!serviceTokenData.workspace.equals(workspaceId)) {
|
||||
// case: invalid workspaceId passed
|
||||
if (!serviceTokenData.workspace.equals(workspaceId)) {
|
||||
// case: invalid workspaceId passed
|
||||
throw UnauthorizedRequestError({
|
||||
message: "Failed service token authorization for the given workspace"
|
||||
});
|
||||
}
|
||||
|
||||
if (environment) {
|
||||
// case: environment is specified
|
||||
|
||||
if (!serviceTokenData.scopes.find(({ environment: tkEnv }) => tkEnv === environment)) {
|
||||
// case: invalid environment passed
|
||||
throw UnauthorizedRequestError({
|
||||
message: "Failed service token authorization for the given workspace environment"
|
||||
});
|
||||
}
|
||||
|
||||
requiredPermissions?.forEach((permission) => {
|
||||
if (!serviceTokenData.permissions.includes(permission)) {
|
||||
throw UnauthorizedRequestError({
|
||||
message: "Failed service token authorization for the given workspace",
|
||||
message: `Failed service token authorization for the given workspace environment action: ${permission}`
|
||||
});
|
||||
}
|
||||
|
||||
if (environment) {
|
||||
// case: environment is specified
|
||||
|
||||
if (serviceTokenData.environment !== environment) {
|
||||
// case: invalid environment passed
|
||||
throw UnauthorizedRequestError({
|
||||
message: "Failed service token authorization for the given workspace environment",
|
||||
});
|
||||
}
|
||||
|
||||
requiredPermissions?.forEach((permission) => {
|
||||
if (!serviceTokenData.permissions.includes(permission)) {
|
||||
throw UnauthorizedRequestError({
|
||||
message: `Failed service token authorization for the given workspace environment action: ${permission}`,
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Validate that service token (client) can access secrets
|
||||
@@ -147,36 +150,35 @@ export const validateServiceTokenDataClientForWorkspace = async ({
|
||||
* @param {string[]} requiredPermissions - required permissions as part of the endpoint
|
||||
*/
|
||||
export const validateServiceTokenDataClientForSecrets = async ({
|
||||
serviceTokenData,
|
||||
secrets,
|
||||
requiredPermissions,
|
||||
serviceTokenData,
|
||||
secrets,
|
||||
requiredPermissions
|
||||
}: {
|
||||
serviceTokenData: IServiceTokenData;
|
||||
secrets: ISecret[];
|
||||
requiredPermissions?: string[];
|
||||
serviceTokenData: IServiceTokenData;
|
||||
secrets: ISecret[];
|
||||
requiredPermissions?: string[];
|
||||
}) => {
|
||||
secrets.forEach((secret: ISecret) => {
|
||||
if (!serviceTokenData.workspace.equals(secret.workspace)) {
|
||||
// case: invalid workspaceId passed
|
||||
throw UnauthorizedRequestError({
|
||||
message: "Failed service token authorization for the given workspace"
|
||||
});
|
||||
}
|
||||
|
||||
secrets.forEach((secret: ISecret) => {
|
||||
if (!serviceTokenData.workspace.equals(secret.workspace)) {
|
||||
// case: invalid workspaceId passed
|
||||
throw UnauthorizedRequestError({
|
||||
message: "Failed service token authorization for the given workspace",
|
||||
});
|
||||
}
|
||||
|
||||
if (serviceTokenData.environment !== secret.environment) {
|
||||
// case: invalid environment passed
|
||||
throw UnauthorizedRequestError({
|
||||
message: "Failed service token authorization for the given workspace environment",
|
||||
});
|
||||
}
|
||||
|
||||
requiredPermissions?.forEach((permission) => {
|
||||
if (!serviceTokenData.permissions.includes(permission)) {
|
||||
throw UnauthorizedRequestError({
|
||||
message: `Failed service token authorization for the given workspace environment action: ${permission}`,
|
||||
});
|
||||
}
|
||||
if (!serviceTokenData.scopes.find(({ environment: tkEnv }) => tkEnv === secret.environment)) {
|
||||
// case: invalid environment passed
|
||||
throw UnauthorizedRequestError({
|
||||
message: "Failed service token authorization for the given workspace environment"
|
||||
});
|
||||
}
|
||||
|
||||
requiredPermissions?.forEach((permission) => {
|
||||
if (!serviceTokenData.permissions.includes(permission)) {
|
||||
throw UnauthorizedRequestError({
|
||||
message: `Failed service token authorization for the given workspace environment action: ${permission}`
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
export const EVENT_PUSH_SECRETS = "pushSecrets";
|
||||
export const EVENT_PULL_SECRETS = "pullSecrets";
|
||||
export const EVENT_PULL_SECRETS = "pullSecrets";
|
||||
export const EVENT_START_INTEGRATION = "startIntegration";
|
||||
|
||||
@@ -19,6 +19,7 @@ export const INTEGRATION_GITLAB = "gitlab";
|
||||
export const INTEGRATION_RENDER = "render";
|
||||
export const INTEGRATION_RAILWAY = "railway";
|
||||
export const INTEGRATION_FLYIO = "flyio";
|
||||
export const INTEGRATION_LARAVELFORGE = "laravel-forge"
|
||||
export const INTEGRATION_CIRCLECI = "circleci";
|
||||
export const INTEGRATION_TRAVISCI = "travisci";
|
||||
export const INTEGRATION_SUPABASE = 'supabase';
|
||||
@@ -26,7 +27,6 @@ export const INTEGRATION_CHECKLY = 'checkly';
|
||||
export const INTEGRATION_HASHICORP_VAULT = 'hashicorp-vault';
|
||||
export const INTEGRATION_CLOUDFLARE_PAGES = 'cloudflare-pages';
|
||||
export const INTEGRATION_CODEFRESH = 'codefresh';
|
||||
|
||||
export const INTEGRATION_SET = new Set([
|
||||
INTEGRATION_AZURE_KEY_VAULT,
|
||||
INTEGRATION_HEROKU,
|
||||
@@ -37,6 +37,7 @@ export const INTEGRATION_SET = new Set([
|
||||
INTEGRATION_RENDER,
|
||||
INTEGRATION_FLYIO,
|
||||
INTEGRATION_CIRCLECI,
|
||||
INTEGRATION_LARAVELFORGE,
|
||||
INTEGRATION_TRAVISCI,
|
||||
INTEGRATION_SUPABASE,
|
||||
INTEGRATION_CHECKLY,
|
||||
@@ -69,6 +70,7 @@ export const INTEGRATION_FLYIO_API_URL = "https://api.fly.io/graphql";
|
||||
export const INTEGRATION_CIRCLECI_API_URL = "https://circleci.com/api";
|
||||
export const INTEGRATION_TRAVISCI_API_URL = "https://api.travis-ci.com";
|
||||
export const INTEGRATION_SUPABASE_API_URL = 'https://api.supabase.com';
|
||||
export const INTEGRATION_LARAVELFORGE_API_URL = "https://forge.laravel.com";
|
||||
export const INTEGRATION_CHECKLY_API_URL = 'https://api.checklyhq.com';
|
||||
export const INTEGRATION_CLOUDFLARE_PAGES_API_URL = 'https://api.cloudflare.com';
|
||||
export const INTEGRATION_CODEFRESH_API_URL = 'https://g.codefresh.io/api';
|
||||
@@ -148,6 +150,15 @@ export const getIntegrationOptions = async () => {
|
||||
clientId: "",
|
||||
docsLink: "",
|
||||
},
|
||||
{
|
||||
name: "Laravel Forge",
|
||||
slug: "laravel-forge",
|
||||
image: "Laravel Forge.png",
|
||||
isAvailable: true,
|
||||
type: "pat",
|
||||
clientId: "",
|
||||
docsLink: "",
|
||||
},
|
||||
{
|
||||
name: "AWS Secret Manager",
|
||||
slug: "aws-secret-manager",
|
||||
@@ -225,15 +236,16 @@ export const getIntegrationOptions = async () => {
|
||||
slug: "gcp",
|
||||
image: "Google Cloud Platform.png",
|
||||
isAvailable: false,
|
||||
type: '',
|
||||
clientId: '',
|
||||
docsLink: ''
|
||||
type: "",
|
||||
clientId: "",
|
||||
docsLink: ""
|
||||
},
|
||||
{
|
||||
name: 'Cloudflare Pages',
|
||||
slug: 'cloudflare-pages',
|
||||
image: 'Cloudflare.png',
|
||||
name: "Cloudflare Pages",
|
||||
slug: "cloudflare-pages",
|
||||
image: "Cloudflare.png",
|
||||
isAvailable: true,
|
||||
<<<<<<< HEAD
|
||||
type: 'pat',
|
||||
clientId: '',
|
||||
docsLink: ''
|
||||
@@ -247,7 +259,13 @@ export const getIntegrationOptions = async () => {
|
||||
clientId: "",
|
||||
docsLink: "",
|
||||
},
|
||||
=======
|
||||
type: "pat",
|
||||
clientId: "",
|
||||
docsLink: ""
|
||||
}
|
||||
>>>>>>> origin
|
||||
]
|
||||
|
||||
return INTEGRATION_OPTIONS;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,3 +3,4 @@ export const SMTP_HOST_MAILGUN = "smtp.mailgun.org";
|
||||
export const SMTP_HOST_SOCKETLABS = "smtp.socketlabs.com";
|
||||
export const SMTP_HOST_ZOHOMAIL = "smtp.zoho.com";
|
||||
export const SMTP_HOST_GMAIL = "smtp.gmail.com";
|
||||
export const SMTP_HOST_OFFICE365 = "smtp.office365.com";
|
||||
Reference in New Issue
Block a user