Fix merge conflicts

This commit is contained in:
Tuan Dang
2023-07-21 20:49:41 +07:00
217 changed files with 11802 additions and 20564 deletions

View File

@@ -105,36 +105,6 @@ jobs:
platforms: linux/amd64,linux/arm64 platforms: linux/amd64,linux/arm64
build-args: | build-args: |
POSTHOG_API_KEY=${{ secrets.PUBLIC_POSTHOG_API_KEY }} POSTHOG_API_KEY=${{ secrets.PUBLIC_POSTHOG_API_KEY }}
secret-scanning-git-app:
name: Build secret scanning git app
runs-on: ubuntu-latest
steps:
- name: ☁️ Checkout source
uses: actions/checkout@v3
- name: Save commit hashes for tag
id: commit
uses: pr-mpt/actions-commit-hash@v2
- name: 🔧 Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: 🐋 Login to Docker Hub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Set up Depot CLI
uses: depot/setup-action@v1
- name: 🏗️ Build secret scanning git app and push
uses: depot/build-push-action@v1
with:
project: 64mmf0n610
push: true
token: ${{ secrets.DEPOT_PROJECT_TOKEN }}
context: secret-engine
tags: |
infisical/staging_deployment_secret-scanning-git-app:${{ steps.commit.outputs.short }}
infisical/staging_deployment_secret-scanning-git-app:latest
platforms: linux/amd64,linux/arm64
gamma-deployment: gamma-deployment:
name: Deploy to gamma name: Deploy to gamma
runs-on: ubuntu-latest runs-on: ubuntu-latest

View File

@@ -10,7 +10,6 @@
"plugin:@typescript-eslint/recommended" "plugin:@typescript-eslint/recommended"
], ],
"rules": { "rules": {
"@typescript-eslint/no-empty-function": "off",
"no-console": 2, "no-console": 2,
"quotes": [ "quotes": [
"error", "error",
@@ -34,11 +33,6 @@
"argsIgnorePattern": "^_" "argsIgnorePattern": "^_"
} }
], ],
"sort-imports": [ "sort-imports": 1
"error",
{
"ignoreDeclarationSort": true
}
]
} }
} }

View File

@@ -19,6 +19,10 @@ RUN npm ci --only-production
COPY --from=build /app . COPY --from=build /app .
RUN apk add --no-cache bash curl && curl -1sLf \
'https://dl.cloudsmith.io/public/infisical/infisical-cli/setup.alpine.sh' | bash \
&& apk add infisical=0.8.1
HEALTHCHECK --interval=10s --timeout=3s --start-period=10s \ HEALTHCHECK --interval=10s --timeout=3s --start-period=10s \
CMD node healthcheck.js CMD node healthcheck.js

9235
backend/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -2,6 +2,7 @@
"dependencies": { "dependencies": {
"@aws-sdk/client-secrets-manager": "^3.319.0", "@aws-sdk/client-secrets-manager": "^3.319.0",
"@godaddy/terminus": "^4.12.0", "@godaddy/terminus": "^4.12.0",
"@node-saml/passport-saml": "^4.0.4",
"@octokit/rest": "^19.0.5", "@octokit/rest": "^19.0.5",
"@sentry/node": "^7.49.0", "@sentry/node": "^7.49.0",
"@sentry/tracing": "^7.48.0", "@sentry/tracing": "^7.48.0",
@@ -36,6 +37,7 @@
"passport": "^0.6.0", "passport": "^0.6.0",
"passport-google-oauth20": "^2.0.0", "passport-google-oauth20": "^2.0.0",
"posthog-node": "^2.6.0", "posthog-node": "^2.6.0",
"probot": "^12.3.1",
"query-string": "^7.1.3", "query-string": "^7.1.3",
"rate-limit-mongo": "^2.3.2", "rate-limit-mongo": "^2.3.2",
"rimraf": "^3.0.2", "rimraf": "^3.0.2",
@@ -103,6 +105,7 @@
"jest-junit": "^15.0.0", "jest-junit": "^15.0.0",
"nodemon": "^2.0.19", "nodemon": "^2.0.19",
"npm": "^8.19.3", "npm": "^8.19.3",
"smee-client": "^1.2.3",
"supertest": "^6.3.3", "supertest": "^6.3.3",
"ts-jest": "^29.0.3", "ts-jest": "^29.0.3",
"ts-node": "^10.9.1" "ts-node": "^10.9.1"

View File

@@ -10,7 +10,7 @@ export const getEncryptionKey = async () => {
return secretValue === "" ? undefined : secretValue; return secretValue === "" ? undefined : secretValue;
} }
export const getRootEncryptionKey = async () => { export const getRootEncryptionKey = async () => {
const secretValue = (await client.getSecret("ROOT_ENCRYPTION_KEY")).secretValue; const secretValue = (await client.getSecret("ROOT_ENCRYPTION_KEY")).secretValue;
return secretValue === "" ? undefined : secretValue; return secretValue === "" ? undefined : secretValue;
} }
export const getInviteOnlySignup = async () => (await client.getSecret("INVITE_ONLY_SIGNUP")).secretValue === "true" export const getInviteOnlySignup = async () => (await client.getSecret("INVITE_ONLY_SIGNUP")).secretValue === "true"
@@ -59,6 +59,11 @@ export const getSmtpPassword = async () => (await client.getSecret("SMTP_PASSWOR
export const getSmtpFromAddress = async () => (await client.getSecret("SMTP_FROM_ADDRESS")).secretValue; export const getSmtpFromAddress = async () => (await client.getSecret("SMTP_FROM_ADDRESS")).secretValue;
export const getSmtpFromName = async () => (await client.getSecret("SMTP_FROM_NAME")).secretValue || "Infisical"; export const getSmtpFromName = async () => (await client.getSecret("SMTP_FROM_NAME")).secretValue || "Infisical";
export const getSecretScanningWebhookProxy = async () => (await client.getSecret("SECRET_SCANNING_WEBHOOK_PROXY")).secretValue;
export const getSecretScanningWebhookSecret = async () => (await client.getSecret("SECRET_SCANNING_WEBHOOK_SECRET")).secretValue;
export const getSecretScanningGitAppId = async () => (await client.getSecret("SECRET_SCANNING_GIT_APP_ID")).secretValue;
export const getSecretScanningPrivateKey = async () => (await client.getSecret("SECRET_SCANNING_PRIVATE_KEY")).secretValue;
export const getLicenseKey = async () => { export const getLicenseKey = async () => {
const secretValue = (await client.getSecret("LICENSE_KEY")).secretValue; const secretValue = (await client.getSecret("LICENSE_KEY")).secretValue;
return secretValue === "" ? undefined : secretValue; return secretValue === "" ? undefined : secretValue;

View File

@@ -15,6 +15,7 @@ import * as userController from "./userController";
import * as workspaceController from "./workspaceController"; import * as workspaceController from "./workspaceController";
import * as secretScanningController from "./secretScanningController"; import * as secretScanningController from "./secretScanningController";
import * as webhookController from "./webhookController"; import * as webhookController from "./webhookController";
import * as secretImportController from "./secretImportController";
export { export {
authController, authController,
@@ -33,5 +34,6 @@ export {
userController, userController,
workspaceController, workspaceController,
secretScanningController, secretScanningController,
webhookController webhookController,
secretImportController
}; };

View File

@@ -2,7 +2,7 @@ import { Request, Response } from "express";
import { Types } from "mongoose"; import { Types } from "mongoose";
import { Integration } from "../../models"; import { Integration } from "../../models";
import { EventService } from "../../services"; import { EventService } from "../../services";
import { eventPushSecrets, eventStartIntegration } from "../../events"; import { eventStartIntegration } from "../../events";
import Folder from "../../models/folder"; import Folder from "../../models/folder";
import { getFolderByPath } from "../../services/FolderService"; import { getFolderByPath } from "../../services/FolderService";
import { BadRequestError } from "../../utils/errors"; import { BadRequestError } from "../../utils/errors";

View File

@@ -9,7 +9,7 @@ import {
import { createOrganization as create } from "../../helpers/organization"; import { createOrganization as create } from "../../helpers/organization";
import { addMembershipsOrg } from "../../helpers/membershipOrg"; import { addMembershipsOrg } from "../../helpers/membershipOrg";
import { ACCEPTED, OWNER } from "../../variables"; import { ACCEPTED, OWNER } from "../../variables";
import { getSiteURL, getLicenseServerUrl } from "../../config"; import { getLicenseServerUrl, getSiteURL } from "../../config";
import { licenseServerKeyRequest } from "../../config/request"; import { licenseServerKeyRequest } from "../../config/request";
export const getOrganizations = async (req: Request, res: Response) => { export const getOrganizations = async (req: Request, res: Response) => {

View File

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

View File

@@ -5,7 +5,7 @@ import { Types } from "mongoose";
import { UnauthorizedRequestError } from "../../utils/errors"; import { UnauthorizedRequestError } from "../../utils/errors";
import GitAppOrganizationInstallation from "../../models/gitAppOrganizationInstallation"; import GitAppOrganizationInstallation from "../../models/gitAppOrganizationInstallation";
import { MembershipOrg } from "../../models"; import { MembershipOrg } from "../../models";
import GitRisks, { STATUS_UNRESOLVED } from "../../models/gitRisks"; import GitRisks, { STATUS_RESOLVED_FALSE_POSITIVE, STATUS_RESOLVED_NOT_REVOKED, STATUS_RESOLVED_REVOKED } from "../../models/gitRisks";
export const createInstallationSession = async (req: Request, res: Response) => { export const createInstallationSession = async (req: Request, res: Response) => {
const sessionId = crypto.randomBytes(16).toString("hex"); const sessionId = crypto.randomBytes(16).toString("hex");
@@ -72,7 +72,7 @@ export const getCurrentOrganizationInstallationStatus = async (req: Request, res
export const getRisksForOrganization = async (req: Request, res: Response) => { export const getRisksForOrganization = async (req: Request, res: Response) => {
const { organizationId } = req.params const { organizationId } = req.params
const risks = await GitRisks.find({ organization: organizationId, status: STATUS_UNRESOLVED }).lean() const risks = await GitRisks.find({ organization: organizationId }).sort({ createdAt: -1 }).lean()
res.json({ res.json({
risks: risks risks: risks
}) })
@@ -81,9 +81,11 @@ export const getRisksForOrganization = async (req: Request, res: Response) => {
export const updateRisksStatus = async (req: Request, res: Response) => { export const updateRisksStatus = async (req: Request, res: Response) => {
const { riskId } = req.params const { riskId } = req.params
const { status } = req.body const { status } = req.body
const risks = await GitRisks.findByIdAndUpdate(riskId, { const isRiskResolved = status == STATUS_RESOLVED_FALSE_POSITIVE || status == STATUS_RESOLVED_REVOKED || status == STATUS_RESOLVED_NOT_REVOKED ? true : false
sttaus: status const risk = await GitRisks.findByIdAndUpdate(riskId, {
status: status,
isResolved: isRiskResolved
}).lean() }).lean()
res.json(risks) res.json(risk)
} }

View File

@@ -27,16 +27,16 @@ export const createWorkspaceEnvironment = async (
const { workspaceId } = req.params; const { workspaceId } = req.params;
const { environmentName, environmentSlug } = req.body; const { environmentName, environmentSlug } = req.body;
const workspace = await Workspace.findById(workspaceId).exec(); const workspace = await Workspace.findById(workspaceId).exec();
if (!workspace) throw WorkspaceNotFoundError(); if (!workspace) throw WorkspaceNotFoundError();
const plan = await EELicenseService.getPlan(workspace.organization.toString()); const plan = await EELicenseService.getPlan(workspace.organization.toString());
if (plan.environmentLimit !== null) { if (plan.environmentLimit !== null) {
// case: limit imposed on number of environments allowed // case: limit imposed on number of environments allowed
if (workspace.environments.length >= plan.environmentLimit) { if (workspace.environments.length >= plan.environmentLimit) {
// case: number of environments used exceeds the number of environments allowed // case: number of environments used exceeds the number of environments allowed
return res.status(400).send({ return res.status(400).send({
message: "Failed to create environment due to environment limit reached. Upgrade plan to create more environments.", message: "Failed to create environment due to environment limit reached. Upgrade plan to create more environments.",
}); });
@@ -191,14 +191,21 @@ export const deleteWorkspaceEnvironment = async (
workspace: workspaceId, workspace: workspaceId,
environment: environmentSlug, environment: environmentSlug,
}); });
await ServiceToken.deleteMany({
workspace: workspaceId, // await ServiceToken.deleteMany({
environment: environmentSlug, // workspace: workspaceId,
}); // environment: environmentSlug,
await ServiceTokenData.deleteMany({ // });
workspace: workspaceId,
environment: environmentSlug, const result = await ServiceTokenData.updateMany(
}); { workspace: workspaceId },
{ $pull: { scopes: { environment: environmentSlug } } }
);
if (result.modifiedCount > 0) {
await ServiceTokenData.deleteMany({ workspace: workspaceId, scopes: { $size: 0 } });
}
await Integration.deleteMany({ await Integration.deleteMany({
workspace: workspaceId, workspace: workspaceId,
environment: environmentSlug, environment: environmentSlug,

View File

@@ -9,8 +9,6 @@ import {
} from "../../types/secret"; } from "../../types/secret";
const { ValidationError } = mongoose.Error; const { ValidationError } = mongoose.Error;
import { import {
BadRequestError,
InternalServerError,
ValidationError as RouteValidationError, ValidationError as RouteValidationError,
UnauthorizedRequestError UnauthorizedRequestError
} from "../../utils/errors"; } from "../../utils/errors";

View File

@@ -35,6 +35,7 @@ import {
} from "../../services/FolderService"; } from "../../services/FolderService";
import { isValidScope } from "../../helpers/secrets"; import { isValidScope } from "../../helpers/secrets";
import path from "path"; import path from "path";
import { getAllImportedSecrets } from "../../services/SecretImportService";
/** /**
* Peform a batch of any specified CUD secret operations * Peform a batch of any specified CUD secret operations
@@ -690,7 +691,7 @@ export const getSecrets = async (req: Request, res: Response) => {
} }
*/ */
const { tagSlugs, secretPath } = req.query; const { tagSlugs, secretPath, include_imports } = req.query;
let { folderId } = req.query; let { folderId } = req.query;
const workspaceId = req.query.workspaceId as string; const workspaceId = req.query.workspaceId as string;
const environment = req.query.environment as string; const environment = req.query.environment as string;
@@ -827,6 +828,12 @@ export const getSecrets = async (req: Request, res: Response) => {
secrets = await Secret.find(secretQuery).populate("tags"); secrets = await Secret.find(secretQuery).populate("tags");
} }
// TODO(akhilmhdh) - secret-imp change this to org type
let importedSecrets: any[] = [];
if (include_imports) {
importedSecrets = await getAllImportedSecrets(workspaceId, environment, folderId as string);
}
const channel = getChannelFromUserAgent(req.headers["user-agent"]); const channel = getChannelFromUserAgent(req.headers["user-agent"]);
const readAction = await EELogService.createAction({ const readAction = await EELogService.createAction({
@@ -868,7 +875,8 @@ export const getSecrets = async (req: Request, res: Response) => {
} }
return res.status(200).send({ return res.status(200).send({
secrets secrets,
...(include_imports && { imports: importedSecrets })
}); });
}; };

View File

@@ -5,8 +5,6 @@ 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 { getSaltRounds } from "../../config";
import { BadRequestError } from "../../utils/errors"; import { BadRequestError } from "../../utils/errors";
import Folder from "../../models/folder";
import { getFolderByPath } from "../../services/FolderService";
/** /**
* Return service token data associated with service token on request * Return service token data associated with service token on request

View File

@@ -18,7 +18,7 @@ import { updateSubscriptionOrgQuantity } from "../../helpers/organization";
* @returns * @returns
*/ */
export const completeAccountSignup = async (req: Request, res: Response) => { export const completeAccountSignup = async (req: Request, res: Response) => {
let user, token, refreshToken; let user;
const { const {
email, email,
firstName, firstName,
@@ -119,7 +119,7 @@ export const completeAccountSignup = async (req: Request, res: Response) => {
userAgent: req.headers["user-agent"] ?? "", userAgent: req.headers["user-agent"] ?? "",
}); });
token = tokens.token; const token = tokens.token;
// sending a welcome email to new users // sending a welcome email to new users
if (await getLoopsApiKey()) { if (await getLoopsApiKey()) {
@@ -159,7 +159,7 @@ export const completeAccountSignup = async (req: Request, res: Response) => {
* @returns * @returns
*/ */
export const completeAccountInvite = async (req: Request, res: Response) => { export const completeAccountInvite = async (req: Request, res: Response) => {
let user, token, refreshToken; let user;
const { const {
email, email,
firstName, firstName,
@@ -244,7 +244,7 @@ export const completeAccountInvite = async (req: Request, res: Response) => {
userAgent: req.headers["user-agent"] ?? "", userAgent: req.headers["user-agent"] ?? "",
}); });
token = tokens.token; const token = tokens.token;
// store (refresh) token in httpOnly cookie // store (refresh) token in httpOnly cookie
res.cookie("jid", tokens.refreshToken, { res.cookie("jid", tokens.refreshToken, {

View File

@@ -3,10 +3,10 @@ import { Types } from "mongoose";
import crypto from "crypto"; import crypto from "crypto";
import bcrypt from "bcrypt"; import bcrypt from "bcrypt";
import { import {
MembershipOrg,
User,
APIKeyData, APIKeyData,
TokenVersion MembershipOrg,
TokenVersion,
User
} from "../../models"; } from "../../models";
import { getSaltRounds } from "../../config"; import { getSaltRounds } from "../../config";
@@ -80,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. * Return organizations that the current user is part of.
* @param req * @param req

View File

@@ -179,10 +179,9 @@ export const getWorkspaceKey = async (req: Request, res: Response) => {
} }
} }
*/ */
let key;
const { workspaceId } = req.params; const { workspaceId } = req.params;
key = await Key.findOne({ const key = await Key.findOne({
workspace: workspaceId, workspace: workspaceId,
receiver: req.user._id receiver: req.user._id
}).populate("sender", "+publicKey"); }).populate("sender", "+publicKey");

View File

@@ -5,6 +5,10 @@ import { eventPushSecrets } from "../../events";
import { BotService } from "../../services"; import { BotService } from "../../services";
import { repackageSecretToRaw } from "../../helpers/secrets"; import { repackageSecretToRaw } from "../../helpers/secrets";
import { encryptSymmetric128BitHexKeyUTF8 } from "../../utils/crypto"; import { encryptSymmetric128BitHexKeyUTF8 } from "../../utils/crypto";
import { getAllImportedSecrets } from "../../services/SecretImportService";
import Folder from "../../models/folder";
import { getFolderByPath } from "../../services/FolderService";
import { BadRequestError } from "../../utils/errors";
/** /**
* Return secrets for workspace with id [workspaceId] and environment * Return secrets for workspace with id [workspaceId] and environment
@@ -16,6 +20,7 @@ export const getSecretsRaw = async (req: Request, res: Response) => {
const workspaceId = req.query.workspaceId as string; const workspaceId = req.query.workspaceId as string;
const environment = req.query.environment as string; const environment = req.query.environment as string;
const secretPath = req.query.secretPath as string; const secretPath = req.query.secretPath as string;
const includeImports = req.query.include_imports as string;
const secrets = await SecretService.getSecrets({ const secrets = await SecretService.getSecrets({
workspaceId: new Types.ObjectId(workspaceId), workspaceId: new Types.ObjectId(workspaceId),
@@ -28,13 +33,38 @@ export const getSecretsRaw = async (req: Request, res: Response) => {
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({ return res.status(200).send({
secrets: secrets.map((secret) => { secrets: secrets.map((secret) => {
const rep = repackageSecretToRaw({ const rep = repackageSecretToRaw({
secret, secret,
key key
}); });
return rep; return rep;
}) })
}); });
@@ -232,6 +262,7 @@ export const getSecrets = async (req: Request, res: Response) => {
const workspaceId = req.query.workspaceId as string; const workspaceId = req.query.workspaceId as string;
const environment = req.query.environment as string; const environment = req.query.environment as string;
const secretPath = req.query.secretPath as string; const secretPath = req.query.secretPath as string;
const includeImports = req.query.include_imports as string;
const secrets = await SecretService.getSecrets({ const secrets = await SecretService.getSecrets({
workspaceId: new Types.ObjectId(workspaceId), workspaceId: new Types.ObjectId(workspaceId),
@@ -240,6 +271,24 @@ export const getSecrets = async (req: Request, res: Response) => {
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({ return res.status(200).send({
secrets secrets
}); });

View File

@@ -12,6 +12,7 @@ import { standardRequest } from "../../config/request";
import { getHttpsEnabled, getJwtSignupSecret, getLoopsApiKey } from "../../config"; import { getHttpsEnabled, getJwtSignupSecret, getLoopsApiKey } from "../../config";
import { BadRequestError } from "../../utils/errors"; import { BadRequestError } from "../../utils/errors";
import { TelemetryService } from "../../services"; import { TelemetryService } from "../../services";
import { AuthProvider } from "../../models";
/** /**
* Complete setting up user by adding their personal and auth information as part of the * Complete setting up user by adding their personal and auth information as part of the
@@ -116,11 +117,13 @@ export const completeAccountSignup = async (req: Request, res: Response) => {
if (!user) if (!user)
throw new Error("Failed to complete account for non-existent user"); // ensure user is non-null throw new Error("Failed to complete account for non-existent user"); // ensure user is non-null
// initialize default organization and workspace if (user.authProvider !== AuthProvider.OKTA_SAML) {
await initializeDefaultOrg({ // initialize default organization and workspace
organizationName, await initializeDefaultOrg({
user, organizationName,
}); user,
});
}
// update organization membership statuses that are // update organization membership statuses that are
// invited to completed with user attached // invited to completed with user attached
@@ -174,7 +177,7 @@ export const completeAccountSignup = async (req: Request, res: Response) => {
distinctId: email, distinctId: email,
properties: { properties: {
email, email,
attributionSource, ...(attributionSource ? { attributionSource } : {})
}, },
}); });
} }

View File

@@ -1,6 +1,7 @@
import * as secretController from "./secretController"; import * as secretController from "./secretController";
import * as secretSnapshotController from "./secretSnapshotController"; import * as secretSnapshotController from "./secretSnapshotController";
import * as organizationsController from "./organizationsController"; import * as organizationsController from "./organizationsController";
import * as ssoController from "./ssoController";
import * as workspaceController from "./workspaceController"; import * as workspaceController from "./workspaceController";
import * as actionController from "./actionController"; import * as actionController from "./actionController";
import * as membershipController from "./membershipController"; import * as membershipController from "./membershipController";
@@ -10,6 +11,7 @@ export {
secretController, secretController,
secretSnapshotController, secretSnapshotController,
organizationsController, organizationsController,
ssoController,
workspaceController, workspaceController,
actionController, actionController,
membershipController, membershipController,

View File

@@ -178,6 +178,12 @@ export const addOrganizationTaxId = async (req: Request, res: Response) => {
return res.status(200).send(data); return res.status(200).send(data);
} }
/**
* Delete tax id with id [taxId] from organization tax ids on file
* @param req
* @param res
* @returns
*/
export const deleteOrganizationTaxId = async (req: Request, res: Response) => { export const deleteOrganizationTaxId = async (req: Request, res: Response) => {
const { taxId } = req.params; const { taxId } = req.params;
@@ -188,6 +194,12 @@ export const deleteOrganizationTaxId = async (req: Request, res: Response) => {
return res.status(200).send(data); return res.status(200).send(data);
} }
/**
* Return organization's invoices on file
* @param req
* @param res
* @returns
*/
export const getOrganizationInvoices = async (req: Request, res: Response) => { export const getOrganizationInvoices = async (req: Request, res: Response) => {
const { data: { invoices } } = await licenseServerKeyRequest.get( const { data: { invoices } } = await licenseServerKeyRequest.get(
`${await getLicenseServerUrl()}/api/license-server/v1/customers/${req.organization.customerId}/invoices` `${await getLicenseServerUrl()}/api/license-server/v1/customers/${req.organization.customerId}/invoices`

View File

@@ -0,0 +1,254 @@
import { Request, Response } from "express";
import { Types } from "mongoose";
import { BotOrgService } from "../../../services";
import { SSOConfig } from "../../models";
import {
MembershipOrg,
User
} from "../../../models";
import { getSSOConfigHelper } from "../../helpers/organizations";
import { client } from "../../../config";
import { ResourceNotFoundError } from "../../../utils/errors";
import { getSiteURL } from "../../../config";
/**
* Redirect user to appropriate SSO endpoint after successful authentication
* to finish inputting their master key for logging in or signing up
* @param req
* @param res
* @returns
*/
export const redirectSSO = async (req: Request, res: Response) => {
if (req.isUserCompleted) {
return res.redirect(`${await getSiteURL()}/login/sso?token=${encodeURIComponent(req.providerAuthToken)}`);
}
return res.redirect(`${await getSiteURL()}/signup/sso?token=${encodeURIComponent(req.providerAuthToken)}`);
}
/**
* Return organization SAML SSO configuration
* @param req
* @param res
* @returns
*/
export const getSSOConfig = async (req: Request, res: Response) => {
const organizationId = req.query.organizationId as string;
const data = await getSSOConfigHelper({
organizationId: new Types.ObjectId(organizationId)
});
return res.status(200).send(data);
}
/**
* Update organization SAML SSO configuration
* @param req
* @param res
* @returns
*/
export const updateSSOConfig = async (req: Request, res: Response) => {
const {
organizationId,
authProvider,
isActive,
entryPoint,
issuer,
cert,
audience
} = req.body;
interface PatchUpdate {
authProvider?: string;
isActive?: boolean;
encryptedEntryPoint?: string;
entryPointIV?: string;
entryPointTag?: string;
encryptedIssuer?: string;
issuerIV?: string;
issuerTag?: string;
encryptedCert?: string;
certIV?: string;
certTag?: string;
encryptedAudience?: string;
audienceIV?: string;
audienceTag?: string;
}
const update: PatchUpdate = {};
if (authProvider) {
update.authProvider = authProvider;
}
if (isActive !== undefined) {
update.isActive = isActive;
}
const key = await BotOrgService.getSymmetricKey(
new Types.ObjectId(organizationId)
);
if (entryPoint) {
const {
ciphertext: encryptedEntryPoint,
iv: entryPointIV,
tag: entryPointTag
} = client.encryptSymmetric(entryPoint, key);
update.encryptedEntryPoint = encryptedEntryPoint;
update.entryPointIV = entryPointIV;
update.entryPointTag = entryPointTag;
}
if (issuer) {
const {
ciphertext: encryptedIssuer,
iv: issuerIV,
tag: issuerTag
} = client.encryptSymmetric(issuer, key);
update.encryptedIssuer = encryptedIssuer;
update.issuerIV = issuerIV;
update.issuerTag = issuerTag;
}
if (cert) {
const {
ciphertext: encryptedCert,
iv: certIV,
tag: certTag
} = client.encryptSymmetric(cert, key);
update.encryptedCert = encryptedCert;
update.certIV = certIV;
update.certTag = certTag;
}
if (audience) {
const {
ciphertext: encryptedAudience,
iv: audienceIV,
tag: audienceTag
} = client.encryptSymmetric(audience, key);
update.encryptedAudience = encryptedAudience;
update.audienceIV = audienceIV;
update.audienceTag = audienceTag;
}
const ssoConfig = await SSOConfig.findOneAndUpdate(
{
organization: new Types.ObjectId(organizationId)
},
update,
{
new: true
}
);
if (!ssoConfig) throw ResourceNotFoundError({
message: "Failed to find SSO config to update"
});
if (update.isActive !== undefined) {
const membershipOrgs = await MembershipOrg.find({
organization: new Types.ObjectId(organizationId)
}).select("user");
if (update.isActive) {
await User.updateMany(
{
_id: {
$in: membershipOrgs.map((membershipOrg) => membershipOrg.user)
}
},
{
authProvider: ssoConfig.authProvider
}
);
} else {
await User.updateMany(
{
_id: {
$in: membershipOrgs.map((membershipOrg) => membershipOrg.user)
}
},
{
$unset: {
authProvider: 1
}
}
);
}
}
return res.status(200).send(ssoConfig);
}
/**
* Create organization SAML SSO configuration
* @param req
* @param res
* @returns
*/
export const createSSOConfig = async (req: Request, res: Response) => {
const {
organizationId,
authProvider,
isActive,
entryPoint,
issuer,
cert,
audience
} = req.body;
const key = await BotOrgService.getSymmetricKey(
new Types.ObjectId(organizationId)
);
const {
ciphertext: encryptedEntryPoint,
iv: entryPointIV,
tag: entryPointTag
} = client.encryptSymmetric(entryPoint, key);
const {
ciphertext: encryptedIssuer,
iv: issuerIV,
tag: issuerTag
} = client.encryptSymmetric(issuer, key);
const {
ciphertext: encryptedCert,
iv: certIV,
tag: certTag
} = client.encryptSymmetric(cert, key);
const {
ciphertext: encryptedAudience,
iv: audienceIV,
tag: audienceTag
} = client.encryptSymmetric(audience, key);
const ssoConfig = await new SSOConfig({
organization: new Types.ObjectId(organizationId),
authProvider,
isActive,
encryptedEntryPoint,
entryPointIV,
entryPointTag,
encryptedIssuer,
issuerIV,
issuerTag,
encryptedCert,
certIV,
certTag,
encryptedAudience,
audienceIV,
audienceTag
}).save();
return res.status(200).send(ssoConfig);
}

View File

@@ -0,0 +1,72 @@
import { Types } from "mongoose";
import {
SSOConfig
} from "../models";
import {
BotOrgService
} from "../../services";
import { client } from "../../config";
import { ValidationError } from "../../utils/errors";
export const getSSOConfigHelper = async ({
organizationId,
ssoConfigId
}: {
organizationId?: Types.ObjectId;
ssoConfigId?: Types.ObjectId;
}) => {
if (!organizationId && !ssoConfigId) throw ValidationError({
message: "Getting SSO data requires either id of organization or SSO data"
});
const ssoConfig = await SSOConfig.findOne({
...(organizationId ? { organization: organizationId } : {}),
...(ssoConfigId ? { _id: ssoConfigId } : {})
});
if (!ssoConfig) throw new Error("Failed to find organization SSO data");
const key = await BotOrgService.getSymmetricKey(
ssoConfig.organization
);
const entryPoint = client.decryptSymmetric(
ssoConfig.encryptedEntryPoint,
key,
ssoConfig.entryPointIV,
ssoConfig.entryPointTag
);
const issuer = client.decryptSymmetric(
ssoConfig.encryptedIssuer,
key,
ssoConfig.issuerIV,
ssoConfig.issuerTag
);
const cert = client.decryptSymmetric(
ssoConfig.encryptedCert,
key,
ssoConfig.certIV,
ssoConfig.certTag
);
const audience = client.decryptSymmetric(
ssoConfig.encryptedAudience,
key,
ssoConfig.audienceIV,
ssoConfig.audienceTag
);
return ({
_id: ssoConfig._id,
organization: ssoConfig.organization,
authProvider: ssoConfig.authProvider,
isActive: ssoConfig.isActive,
entryPoint,
issuer,
cert,
audience
});
}

View File

@@ -1,7 +1,5 @@
import requireLicenseAuth from "./requireLicenseAuth";
import requireSecretSnapshotAuth from "./requireSecretSnapshotAuth"; import requireSecretSnapshotAuth from "./requireSecretSnapshotAuth";
export { export {
requireLicenseAuth,
requireSecretSnapshotAuth, requireSecretSnapshotAuth,
} }

View File

@@ -1,23 +0,0 @@
import { NextFunction, Request, Response } from "express";
/**
* Validate if organization hosting meets license requirements to
* access a license-specific route.
* @param {Object} obj
* @param {String[]} obj.acceptedTiers
*/
const requireLicenseAuth = ({
acceptedTiers,
}: {
acceptedTiers: string[];
}) => {
return async (req: Request, res: Response, next: NextFunction) => {
try {
} catch (err) {
}
}
}
export default requireLicenseAuth;

View File

@@ -3,6 +3,7 @@ import SecretVersion, { ISecretVersion } from "./secretVersion";
import FolderVersion, { TFolderRootVersionSchema } from "./folderVersion"; import FolderVersion, { TFolderRootVersionSchema } from "./folderVersion";
import Log, { ILog } from "./log"; import Log, { ILog } from "./log";
import Action, { IAction } from "./action"; import Action, { IAction } from "./action";
import SSOConfig, { ISSOConfig } from "./ssoConfig";
export { export {
SecretSnapshot, SecretSnapshot,
@@ -15,4 +16,6 @@ export {
ILog, ILog,
Action, Action,
IAction, IAction,
SSOConfig,
ISSOConfig
}; };

View File

@@ -63,9 +63,10 @@ const logSchema = new Schema<ILog>(
ipAddress: { ipAddress: {
type: String, type: String,
}, },
}, { },
timestamps: true, {
} timestamps: true,
}
); );
const Log = model<ILog>("Log", logSchema); const Log = model<ILog>("Log", logSchema);

View File

@@ -0,0 +1,82 @@
import { Schema, Types, model } from "mongoose";
export interface ISSOConfig {
organization: Types.ObjectId;
authProvider: "okta-saml"
isActive: boolean;
encryptedEntryPoint: string;
entryPointIV: string;
entryPointTag: string;
encryptedIssuer: string;
issuerIV: string;
issuerTag: string;
encryptedCert: string;
certIV: string;
certTag: string;
encryptedAudience: string;
audienceIV: string;
audienceTag: string;
}
const ssoConfigSchema = new Schema<ISSOConfig>(
{
organization: {
type: Schema.Types.ObjectId,
ref: "Organization"
},
authProvider: {
type: String,
enum: [
"okta-saml"
],
required: true
},
isActive: {
type: Boolean,
required: true
},
encryptedEntryPoint: {
type: String
},
entryPointIV: {
type: String
},
entryPointTag: {
type: String
},
encryptedIssuer: {
type: String
},
issuerIV: {
type: String
},
issuerTag: {
type: String
},
encryptedCert: {
type: String
},
certIV: {
type: String
},
certTag: {
type: String
},
encryptedAudience: {
type: String
},
audienceIV: {
type: String
},
audienceTag: {
type: String
}
},
{
timestamps: true
}
);
const SSOConfig = model<ISSOConfig>("SSOConfig", ssoConfigSchema);
export default SSOConfig;

View File

@@ -1,6 +1,7 @@
import secret from "./secret"; import secret from "./secret";
import secretSnapshot from "./secretSnapshot"; import secretSnapshot from "./secretSnapshot";
import organizations from "./organizations"; import organizations from "./organizations";
import sso from "./sso";
import workspace from "./workspace"; import workspace from "./workspace";
import action from "./action"; import action from "./action";
import cloudProducts from "./cloudProducts"; import cloudProducts from "./cloudProducts";
@@ -9,6 +10,7 @@ export {
secret, secret,
secretSnapshot, secretSnapshot,
organizations, organizations,
sso,
workspace, workspace,
action, action,
cloudProducts, cloudProducts,

View File

@@ -0,0 +1,110 @@
import express from "express";
const router = express.Router();
import passport from "passport";
import {
requireAuth,
requireOrganizationAuth,
validateRequest,
} from "../../../middleware";
import { body, query } from "express-validator";
import { ssoController } from "../../controllers/v1";
import { authLimiter } from "../../../helpers/rateLimiter";
import {
ACCEPTED,
OWNER,
ADMIN
} from "../../../variables";
router.get(
"/redirect/google",
authLimiter,
passport.authenticate("google", {
scope: ["profile", "email"],
session: false,
})
);
router.get(
"/google",
passport.authenticate("google", {
failureRedirect: "/login/provider/error",
session: false
}),
ssoController.redirectSSO
);
router.get(
"/redirect/saml2/:ssoIdentifier",
authLimiter,
passport.authenticate("saml", {
failureRedirect: "/login/fail"
})
);
router.post("/saml2/:ssoIdentifier",
passport.authenticate("saml", {
failureRedirect: "/login/provider/error",
failureFlash: true,
session: false
}),
ssoController.redirectSSO
);
router.get(
"/config",
requireAuth({
acceptedAuthModes: ["jwt"],
}),
requireOrganizationAuth({
acceptedRoles: [OWNER, ADMIN],
acceptedStatuses: [ACCEPTED],
locationOrganizationId: "query"
}),
query("organizationId").exists().trim(),
validateRequest,
ssoController.getSSOConfig
);
router.post(
"/config",
requireAuth({
acceptedAuthModes: ["jwt"],
}),
requireOrganizationAuth({
acceptedRoles: [OWNER, ADMIN],
acceptedStatuses: [ACCEPTED],
locationOrganizationId: "body"
}),
body("organizationId").exists().trim(),
body("authProvider").exists().isString(),
body("isActive").exists().isBoolean(),
body("entryPoint").exists().isString(),
body("issuer").exists().isString(),
body("cert").exists().isString(),
body("audience").exists().isString(),
validateRequest,
ssoController.createSSOConfig
);
router.patch(
"/config",
requireAuth({
acceptedAuthModes: ["jwt"],
}),
requireOrganizationAuth({
acceptedRoles: [OWNER, ADMIN],
acceptedStatuses: [ACCEPTED],
locationOrganizationId: "body"
}),
body("organizationId").exists().trim(),
body("authProvider").optional().isString(),
body("isActive").optional().isBoolean(),
body("entryPoint").optional().isString(),
body("issuer").optional().isString(),
body("cert").optional().isString(),
body("audience").optional().isString(),
validateRequest,
ssoController.updateSSOConfig
);
export default router;

View File

@@ -30,7 +30,8 @@ interface FeatureSet {
customRateLimits: boolean; customRateLimits: boolean;
customAlerts: boolean; customAlerts: boolean;
auditLogs: boolean; auditLogs: boolean;
status: 'incomplete' | 'incomplete_expired' | 'trialing' | 'active' | 'past_due' | 'canceled' | 'unpaid' | null; samlSSO: boolean;
status: "incomplete" | "incomplete_expired" | "trialing" | "active" | "past_due" | "canceled" | "unpaid" | null;
trial_end: number | null; trial_end: number | null;
has_used_trial: boolean; has_used_trial: boolean;
} }
@@ -63,6 +64,7 @@ class EELicenseService {
customRateLimits: true, customRateLimits: true,
customAlerts: true, customAlerts: true,
auditLogs: false, auditLogs: false,
samlSSO: false,
status: null, status: null,
trial_end: null, trial_end: null,
has_used_trial: true has_used_trial: true

View File

@@ -0,0 +1,46 @@
import { Types } from "mongoose";
import { client, getEncryptionKey, getRootEncryptionKey } from "../config";
import { BotOrg } from "../models";
import { decryptSymmetric128BitHexKeyUTF8 } from "../utils/crypto";
import {
ENCODING_SCHEME_BASE64,
ENCODING_SCHEME_UTF8
} from "../variables";
import { InternalServerError } from "../utils/errors";
// TODO: DOCstrings
export const getSymmetricKeyHelper = async (organizationId: Types.ObjectId) => {
const rootEncryptionKey = await getRootEncryptionKey();
const encryptionKey = await getEncryptionKey();
const botOrg = await BotOrg.findOne({
organization: organizationId
});
if (!botOrg) throw new Error("Failed to find organization bot");
if (rootEncryptionKey && botOrg.symmetricKeyKeyEncoding == ENCODING_SCHEME_BASE64) {
const key = client.decryptSymmetric(
botOrg.encryptedSymmetricKey,
rootEncryptionKey,
botOrg.symmetricKeyIV,
botOrg.symmetricKeyTag
);
return key;
} else if (encryptionKey && botOrg.symmetricKeyKeyEncoding === ENCODING_SCHEME_UTF8) {
const key = decryptSymmetric128BitHexKeyUTF8({
ciphertext: botOrg.encryptedSymmetricKey,
iv: botOrg.symmetricKeyIV,
tag: botOrg.symmetricKeyTag,
key: encryptionKey
});
return key;
}
throw InternalServerError({
message: "Failed to match encryption key with organization bot symmetric key encoding"
});
}

View File

@@ -5,11 +5,12 @@ import express from "express";
require("express-async-errors"); require("express-async-errors");
import helmet from "helmet"; import helmet from "helmet";
import cors from "cors"; import cors from "cors";
import { DatabaseService } from "./services"; import { DatabaseService, GithubSecretScanningService } from "./services";
import { EELicenseService } from "./ee/services"; import { EELicenseService } from "./ee/services";
import { setUpHealthEndpoint } from "./services/health"; import { setUpHealthEndpoint } from "./services/health";
import cookieParser from "cookie-parser"; import cookieParser from "cookie-parser";
import swaggerUi = require("swagger-ui-express"); import swaggerUi = require("swagger-ui-express");
import { Probot, createNodeMiddleware } from "probot";
// eslint-disable-next-line @typescript-eslint/no-var-requires // eslint-disable-next-line @typescript-eslint/no-var-requires
const swaggerFile = require("../spec.json"); const swaggerFile = require("../spec.json");
// eslint-disable-next-line @typescript-eslint/no-var-requires // eslint-disable-next-line @typescript-eslint/no-var-requires
@@ -18,6 +19,7 @@ import {
action as eeActionRouter, action as eeActionRouter,
cloudProducts as eeCloudProductsRouter, cloudProducts as eeCloudProductsRouter,
organizations as eeOrganizationsRouter, organizations as eeOrganizationsRouter,
sso as eeSSORouter,
secret as eeSecretRouter, secret as eeSecretRouter,
secretSnapshot as eeSecretSnapshotRouter, secretSnapshot as eeSecretSnapshotRouter,
workspace as eeWorkspaceRouter workspace as eeWorkspaceRouter
@@ -40,21 +42,22 @@ import {
signup as v1SignupRouter, signup as v1SignupRouter,
userAction as v1UserActionRouter, userAction as v1UserActionRouter,
user as v1UserRouter, user as v1UserRouter,
webhooks as v1WebhooksRouter,
workspace as v1WorkspaceRouter, workspace as v1WorkspaceRouter,
webhooks as v1WebhooksRouter secretImport as v1SecretImportRouter
} from "./routes/v1"; } from "./routes/v1";
import { import {
auth as v2AuthRouter, auth as v2AuthRouter,
signup as v2SignupRouter, environment as v2EnvironmentRouter,
users as v2UsersRouter,
organizations as v2OrganizationsRouter, organizations as v2OrganizationsRouter,
workspace as v2WorkspaceRouter,
secret as v2SecretRouter, // begin to phase out secret as v2SecretRouter, // begin to phase out
secrets as v2SecretsRouter, secrets as v2SecretsRouter,
serviceTokenData as v2ServiceTokenDataRouter,
serviceAccounts as v2ServiceAccountsRouter, serviceAccounts as v2ServiceAccountsRouter,
environment as v2EnvironmentRouter, serviceTokenData as v2ServiceTokenDataRouter,
tags as v2TagsRouter signup as v2SignupRouter,
tags as v2TagsRouter,
users as v2UsersRouter,
workspace as v2WorkspaceRouter,
} from "./routes/v2"; } from "./routes/v2";
import { import {
auth as v3AuthRouter, auth as v3AuthRouter,
@@ -66,10 +69,12 @@ import { healthCheck } from "./routes/status";
import { getLogger } from "./utils/logger"; import { getLogger } from "./utils/logger";
import { RouteNotFoundError } from "./utils/errors"; import { RouteNotFoundError } from "./utils/errors";
import { requestErrorHandler } from "./middleware/requestErrorHandler"; import { requestErrorHandler } from "./middleware/requestErrorHandler";
import { getNodeEnv, getPort, getSiteURL } from "./config"; import { getNodeEnv, getPort, getSecretScanningGitAppId, getSecretScanningPrivateKey, getSecretScanningWebhookProxy, getSecretScanningWebhookSecret, getSiteURL } from "./config";
import { setup } from "./utils/setup"; import { setup } from "./utils/setup";
const SmeeClient = require('smee-client') // eslint-disable-line
const main = async () => { const main = async () => {
await setup(); await setup();
await EELicenseService.initGlobalFeatureSet(); await EELicenseService.initGlobalFeatureSet();
@@ -77,6 +82,7 @@ const main = async () => {
const app = express(); const app = express();
app.enable("trust proxy"); app.enable("trust proxy");
app.use(express.json()); app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.use(cookieParser()); app.use(cookieParser());
app.use( app.use(
cors({ cors({
@@ -85,6 +91,26 @@ const main = async () => {
}) })
); );
if (await getSecretScanningGitAppId() && await getSecretScanningWebhookSecret() && await getSecretScanningPrivateKey()) {
const probot = new Probot({
appId: await getSecretScanningGitAppId(),
privateKey: await getSecretScanningPrivateKey(),
secret: await getSecretScanningWebhookSecret(),
});
if ((await getNodeEnv()) != "production") {
const smee = new SmeeClient({
source: await getSecretScanningWebhookProxy(),
target: "http://backend:4000/ss-webhook",
logger: console
})
smee.start()
}
app.use(createNodeMiddleware(GithubSecretScanningService, { probot, webhooksPath: "/ss-webhook" })); // secret scanning webhook
}
if ((await getNodeEnv()) === "production") { if ((await getNodeEnv()) === "production") {
// enable app-wide rate-limiting + helmet security // enable app-wide rate-limiting + helmet security
// in production // in production
@@ -106,6 +132,7 @@ const main = async () => {
app.use("/api/v1/workspace", eeWorkspaceRouter); app.use("/api/v1/workspace", eeWorkspaceRouter);
app.use("/api/v1/action", eeActionRouter); app.use("/api/v1/action", eeActionRouter);
app.use("/api/v1/organizations", eeOrganizationsRouter); app.use("/api/v1/organizations", eeOrganizationsRouter);
app.use("/api/v1/sso", eeSSORouter);
app.use("/api/v1/cloud-products", eeCloudProductsRouter); app.use("/api/v1/cloud-products", eeCloudProductsRouter);
// v1 routes (default) // v1 routes (default)
@@ -128,6 +155,7 @@ const main = async () => {
app.use("/api/v1/folders", v1SecretsFolder); app.use("/api/v1/folders", v1SecretsFolder);
app.use("/api/v1/secret-scanning", v1SecretScanningRouter); app.use("/api/v1/secret-scanning", v1SecretScanningRouter);
app.use("/api/v1/webhooks", v1WebhooksRouter); app.use("/api/v1/webhooks", v1WebhooksRouter);
app.use("/api/v1/secret-imports", v1SecretImportRouter);
// v2 routes (improvements) // v2 routes (improvements)
app.use("/api/v2/signup", v2SignupRouter); app.use("/api/v2/signup", v2SignupRouter);

View File

@@ -34,6 +34,8 @@ import {
INTEGRATION_TRAVISCI_API_URL, INTEGRATION_TRAVISCI_API_URL,
INTEGRATION_VERCEL, INTEGRATION_VERCEL,
INTEGRATION_VERCEL_API_URL, INTEGRATION_VERCEL_API_URL,
INTEGRATION_CODEFRESH,
INTEGRATION_CODEFRESH_API_URL
} from "../variables"; } from "../variables";
interface App { interface App {
@@ -153,7 +155,13 @@ const getApps = async ({
apps = await getAppsBitBucket({ apps = await getAppsBitBucket({
accessToken, accessToken,
workspaceSlug workspaceSlug
}) });
break;
case INTEGRATION_CODEFRESH:
apps = await getAppsCodefresh({
accessToken,
});
break;
} }
return apps; return apps;
@@ -205,10 +213,10 @@ const getAppsVercel = async ({
}, },
...(integrationAuth?.teamId ...(integrationAuth?.teamId
? { ? {
params: { params: {
teamId: integrationAuth.teamId, teamId: integrationAuth.teamId,
}, },
} }
: {}), : {}),
}) })
).data; ).data;
@@ -704,30 +712,30 @@ const getAppsCheckly = async ({ accessToken }: { accessToken: string }) => {
* @returns {Object[]} apps - Cloudflare Pages projects * @returns {Object[]} apps - Cloudflare Pages projects
* @returns {String} apps.name - name of Cloudflare Pages project * @returns {String} apps.name - name of Cloudflare Pages project
*/ */
const getAppsCloudflarePages = async ({ const getAppsCloudflarePages = async ({
accessToken, accessToken,
accountId accountId
}: { }: {
accessToken: string; accessToken: string;
accountId?: string; accountId?: string;
}) => { }) => {
const { data } = await standardRequest.get( const { data } = await standardRequest.get(
`${INTEGRATION_CLOUDFLARE_PAGES_API_URL}/client/v4/accounts/${accountId}/pages/projects`, `${INTEGRATION_CLOUDFLARE_PAGES_API_URL}/client/v4/accounts/${accountId}/pages/projects`,
{ {
headers: { headers: {
Authorization: `Bearer ${accessToken}`, Authorization: `Bearer ${accessToken}`,
"Accept": "application/json", "Accept": "application/json",
}, },
} }
); );
const apps = data.result.map((a: any) => { const apps = data.result.map((a: any) => {
return { return {
name: a.name, name: a.name,
appId: a.id, appId: a.id,
}; };
}); });
return apps; return apps;
} }
/** /**
@@ -766,7 +774,7 @@ const getAppsBitBucket = async ({
if (!workspaceSlug) { if (!workspaceSlug) {
return [] return []
} }
const repositories: Repository[] = []; const repositories: Repository[] = [];
let hasNextPage = true; let hasNextPage = true;
let repositoriesUrl = `${INTEGRATION_BITBUCKET_API_URL}/2.0/repositories/${workspaceSlug}` let repositoriesUrl = `${INTEGRATION_BITBUCKET_API_URL}/2.0/repositories/${workspaceSlug}`
@@ -804,4 +812,34 @@ const getAppsBitBucket = async ({
return apps; return apps;
} }
/**
* Return list of projects for Supabase integration
* @param {Object} obj
* @param {String} obj.accessToken - access token for Supabase API
* @returns {Object[]} apps - names of Supabase apps
* @returns {String} apps.name - name of Supabase app
*/
const getAppsCodefresh = async ({
accessToken,
}: {
accessToken: string;
}) => {
const res = (
await standardRequest.get(`${INTEGRATION_CODEFRESH_API_URL}/projects`, {
headers: {
Authorization: `Bearer ${accessToken}`,
"Accept-Encoding": "application/json",
},
})
).data;
const apps = res.projects.map((a: any) => ({
name: a.projectName,
appId: a.id,
}));
return apps;
};
export { getApps }; export { getApps };

View File

@@ -146,7 +146,8 @@ const exchangeCode = async ({
case INTEGRATION_BITBUCKET: case INTEGRATION_BITBUCKET:
obj = await exchangeCodeBitBucket({ obj = await exchangeCodeBitBucket({
code, code,
}) });
break;
} }
return obj; return obj;

View File

@@ -18,7 +18,6 @@ const revokeAccess = async ({
integrationAuth: IIntegrationAuth; integrationAuth: IIntegrationAuth;
accessToken: string; accessToken: string;
}) => { }) => {
let deletedIntegrationAuth;
// add any integration-specific revocation logic // add any integration-specific revocation logic
switch (integrationAuth.integration) { switch (integrationAuth.integration) {
case INTEGRATION_HEROKU: case INTEGRATION_HEROKU:
@@ -33,7 +32,7 @@ const revokeAccess = async ({
break; break;
} }
deletedIntegrationAuth = await IntegrationAuth.findOneAndDelete({ const deletedIntegrationAuth = await IntegrationAuth.findOneAndDelete({
_id: integrationAuth._id, _id: integrationAuth._id,
}); });

View File

@@ -1,7 +1,7 @@
import _ from "lodash"; import _ from "lodash";
import AWS from "aws-sdk"; import AWS from "aws-sdk";
import { import {
CreateSecretCommand, CreateSecretCommand,
GetSecretValueCommand, GetSecretValueCommand,
ResourceNotFoundException, ResourceNotFoundException,
SecretsManagerClient, SecretsManagerClient,
@@ -44,8 +44,10 @@ import {
INTEGRATION_TRAVISCI_API_URL, INTEGRATION_TRAVISCI_API_URL,
INTEGRATION_VERCEL, INTEGRATION_VERCEL,
INTEGRATION_VERCEL_API_URL, INTEGRATION_VERCEL_API_URL,
INTEGRATION_CODEFRESH,
INTEGRATION_CODEFRESH_API_URL
} from "../variables"; } from "../variables";
import { standardRequest} from "../config/request"; import { standardRequest } from "../config/request";
/** /**
* Sync/push [secrets] to [app] in integration named [integration] * Sync/push [secrets] to [app] in integration named [integration]
@@ -175,11 +177,39 @@ const syncSecrets = async ({
break; break;
case INTEGRATION_SUPABASE: case INTEGRATION_SUPABASE:
await syncSecretsSupabase({ await syncSecretsSupabase({
integration, integration,
secrets, secrets,
accessToken, accessToken,
}); });
break; break;
case INTEGRATION_FLYIO:
await syncSecretsFlyio({
integration,
secrets,
accessToken,
});
break;
case INTEGRATION_CIRCLECI:
await syncSecretsCircleCI({
integration,
secrets,
accessToken,
});
break;
case INTEGRATION_TRAVISCI:
await syncSecretsTravisCI({
integration,
secrets,
accessToken,
});
break;
case INTEGRATION_SUPABASE:
await syncSecretsSupabase({
integration,
secrets,
accessToken,
});
break;
case INTEGRATION_CHECKLY: case INTEGRATION_CHECKLY:
await syncSecretsCheckly({ await syncSecretsCheckly({
integration, integration,
@@ -198,10 +228,17 @@ const syncSecrets = async ({
break; break;
case INTEGRATION_CLOUDFLARE_PAGES: case INTEGRATION_CLOUDFLARE_PAGES:
await syncSecretsCloudflarePages({ await syncSecretsCloudflarePages({
integration, integration,
secrets, secrets,
accessId, accessId,
accessToken accessToken
});
break;
case INTEGRATION_CODEFRESH:
await syncSecretsCodefresh({
integration,
secrets,
accessToken,
}); });
break; break;
case INTEGRATION_BITBUCKET: case INTEGRATION_BITBUCKET:
@@ -240,11 +277,11 @@ const syncSecretsAzureKeyVault = async ({
recoverableDays: number; recoverableDays: number;
} }
} }
interface AzureKeyVaultSecret extends GetAzureKeyVaultSecret { interface AzureKeyVaultSecret extends GetAzureKeyVaultSecret {
key: string; key: string;
} }
/** /**
* Return all secrets from Azure Key Vault by paginating through URL [url] * Return all secrets from Azure Key Vault by paginating through URL [url]
* @param {String} url - pagination URL to get next set of secrets from Azure Key Vault * @param {String} url - pagination URL to get next set of secrets from Azure Key Vault
@@ -258,23 +295,23 @@ const syncSecretsAzureKeyVault = async ({
Authorization: `Bearer ${accessToken}`, Authorization: `Bearer ${accessToken}`,
}, },
}); });
result = result.concat(res.data.value); result = result.concat(res.data.value);
url = res.data.nextLink; url = res.data.nextLink;
} }
return result; return result;
} }
const getAzureKeyVaultSecrets = await paginateAzureKeyVaultSecrets(`${integration.app}/secrets?api-version=7.3`); const getAzureKeyVaultSecrets = await paginateAzureKeyVaultSecrets(`${integration.app}/secrets?api-version=7.3`);
let lastSlashIndex: number; let lastSlashIndex: number;
const res = (await Promise.all(getAzureKeyVaultSecrets.map(async (getAzureKeyVaultSecret) => { const res = (await Promise.all(getAzureKeyVaultSecrets.map(async (getAzureKeyVaultSecret) => {
if (!lastSlashIndex) { if (!lastSlashIndex) {
lastSlashIndex = getAzureKeyVaultSecret.id.lastIndexOf("/"); lastSlashIndex = getAzureKeyVaultSecret.id.lastIndexOf("/");
} }
const azureKeyVaultSecret = await standardRequest.get(`${getAzureKeyVaultSecret.id}?api-version=7.3`, { const azureKeyVaultSecret = await standardRequest.get(`${getAzureKeyVaultSecret.id}?api-version=7.3`, {
headers: { headers: {
"Authorization": `Bearer ${accessToken}`, "Authorization": `Bearer ${accessToken}`,
@@ -286,11 +323,11 @@ const syncSecretsAzureKeyVault = async ({
key: getAzureKeyVaultSecret.id.substring(lastSlashIndex + 1), key: getAzureKeyVaultSecret.id.substring(lastSlashIndex + 1),
}); });
}))) })))
.reduce((obj: any, secret: any) => ({ .reduce((obj: any, secret: any) => ({
...obj, ...obj,
[secret.key]: secret, [secret.key]: secret,
}), {}); }), {});
const setSecrets: { const setSecrets: {
key: string; key: string;
value: string; value: string;
@@ -314,9 +351,9 @@ const syncSecretsAzureKeyVault = async ({
} }
} }
}); });
const deleteSecrets: AzureKeyVaultSecret[] = []; const deleteSecrets: AzureKeyVaultSecret[] = [];
Object.keys(res).forEach((key) => { Object.keys(res).forEach((key) => {
const underscoredKey = key.replace(/-/g, "_"); const underscoredKey = key.replace(/-/g, "_");
if (!(underscoredKey in secrets)) { if (!(underscoredKey in secrets)) {
@@ -337,7 +374,7 @@ const syncSecretsAzureKeyVault = async ({
}) => { }) => {
let isSecretSet = false; let isSecretSet = false;
let maxTries = 6; let maxTries = 6;
while (!isSecretSet && maxTries > 0) { while (!isSecretSet && maxTries > 0) {
// try to set secret // try to set secret
try { try {
@@ -354,7 +391,7 @@ const syncSecretsAzureKeyVault = async ({
); );
isSecretSet = true; isSecretSet = true;
} catch (err) { } catch (err) {
const error: any = err; const error: any = err;
if (error?.response?.data?.error?.innererror?.code === "ObjectIsDeletedButRecoverable") { if (error?.response?.data?.error?.innererror?.code === "ObjectIsDeletedButRecoverable") {
@@ -374,7 +411,7 @@ const syncSecretsAzureKeyVault = async ({
} }
} }
} }
// Sync/push set secrets // Sync/push set secrets
for await (const setSecret of setSecrets) { for await (const setSecret of setSecrets) {
const { key, value } = setSecret; const { key, value } = setSecret;
@@ -385,7 +422,7 @@ const syncSecretsAzureKeyVault = async ({
accessToken, accessToken,
}); });
} }
for await (const deleteSecret of deleteSecrets) { for await (const deleteSecret of deleteSecrets) {
const { key } = deleteSecret; const { key } = deleteSecret;
await standardRequest.delete(`${integration.app}/secrets/${key}?api-version=7.3`, { await standardRequest.delete(`${integration.app}/secrets/${key}?api-version=7.3`, {
@@ -427,7 +464,7 @@ const syncSecretsAWSParameterStore = async ({
apiVersion: "2014-11-06", apiVersion: "2014-11-06",
region: integration.region, region: integration.region,
}); });
const params = { const params = {
Path: integration.path, Path: integration.path,
Recursive: true, Recursive: true,
@@ -435,61 +472,61 @@ const syncSecretsAWSParameterStore = async ({
}; };
const parameterList = (await ssm.getParametersByPath(params).promise()).Parameters const parameterList = (await ssm.getParametersByPath(params).promise()).Parameters
let awsParameterStoreSecretsObj: { let awsParameterStoreSecretsObj: {
[key: string]: any // TODO: fix type [key: string]: any // TODO: fix type
} = {}; } = {};
if (parameterList) { if (parameterList) {
awsParameterStoreSecretsObj = parameterList.reduce((obj: any, secret: any) => ({ awsParameterStoreSecretsObj = parameterList.reduce((obj: any, secret: any) => ({
...obj, ...obj,
[secret.Name.split("/").pop()]: secret, [secret.Name.split("/").pop()]: secret,
}), {}); }), {});
} }
// Identify secrets to create // Identify secrets to create
Object.keys(secrets).map(async (key) => { Object.keys(secrets).map(async (key) => {
if (!(key in awsParameterStoreSecretsObj)) { if (!(key in awsParameterStoreSecretsObj)) {
// case: secret does not exist in AWS parameter store // case: secret does not exist in AWS parameter store
// -> create secret // -> create secret
await ssm.putParameter({
Name: `${integration.path}${key}`,
Type: "SecureString",
Value: secrets[key],
Overwrite: true,
}).promise();
} else {
// case: secret exists in AWS parameter store
if (awsParameterStoreSecretsObj[key].Value !== secrets[key]) {
// case: secret value doesn't match one in AWS parameter store
// -> update secret
await ssm.putParameter({ await ssm.putParameter({
Name: `${integration.path}${key}`, Name: `${integration.path}${key}`,
Type: "SecureString", Type: "SecureString",
Value: secrets[key], Value: secrets[key],
Overwrite: true, Overwrite: true,
}).promise(); }).promise();
} else {
// case: secret exists in AWS parameter store
if (awsParameterStoreSecretsObj[key].Value !== secrets[key]) {
// case: secret value doesn't match one in AWS parameter store
// -> update secret
await ssm.putParameter({
Name: `${integration.path}${key}`,
Type: "SecureString",
Value: secrets[key],
Overwrite: true,
}).promise();
}
} }
}
}); });
// Identify secrets to delete // Identify secrets to delete
Object.keys(awsParameterStoreSecretsObj).map(async (key) => { Object.keys(awsParameterStoreSecretsObj).map(async (key) => {
if (!(key in secrets)) { if (!(key in secrets)) {
// case: // case:
// -> delete secret // -> delete secret
await ssm.deleteParameter({ await ssm.deleteParameter({
Name: awsParameterStoreSecretsObj[key].Name, Name: awsParameterStoreSecretsObj[key].Name,
}).promise(); }).promise();
} }
}); });
AWS.config.update({ AWS.config.update({
region: undefined, region: undefined,
accessKeyId: undefined, accessKeyId: undefined,
secretAccessKey: undefined, secretAccessKey: undefined,
}); });
} }
/** /**
@@ -520,7 +557,7 @@ const syncSecretsAWSSecretManager = async ({
accessKeyId: accessId, accessKeyId: accessId,
secretAccessKey: accessToken, secretAccessKey: accessToken,
}); });
secretsManager = new SecretsManagerClient({ secretsManager = new SecretsManagerClient({
region: integration.region, region: integration.region,
credentials: { credentials: {
@@ -534,13 +571,13 @@ const syncSecretsAWSSecretManager = async ({
SecretId: integration.app, SecretId: integration.app,
}) })
); );
let awsSecretManagerSecretObj: { [key: string]: any } = {}; let awsSecretManagerSecretObj: { [key: string]: any } = {};
if (awsSecretManagerSecret?.SecretString) { if (awsSecretManagerSecret?.SecretString) {
awsSecretManagerSecretObj = JSON.parse(awsSecretManagerSecret.SecretString); awsSecretManagerSecretObj = JSON.parse(awsSecretManagerSecret.SecretString);
} }
if (!_.isEqual(awsSecretManagerSecretObj, secrets)) { if (!_.isEqual(awsSecretManagerSecretObj, secrets)) {
await secretsManager.send(new UpdateSecretCommand({ await secretsManager.send(new UpdateSecretCommand({
SecretId: integration.app, SecretId: integration.app,
@@ -552,19 +589,19 @@ const syncSecretsAWSSecretManager = async ({
region: undefined, region: undefined,
accessKeyId: undefined, accessKeyId: undefined,
secretAccessKey: undefined, secretAccessKey: undefined,
}); });
} catch (err) { } catch (err) {
if (err instanceof ResourceNotFoundException && secretsManager) { if (err instanceof ResourceNotFoundException && secretsManager) {
await secretsManager.send(new CreateSecretCommand({ await secretsManager.send(new CreateSecretCommand({
Name: integration.app, Name: integration.app,
SecretString: JSON.stringify(secrets), SecretString: JSON.stringify(secrets),
})); }));
} }
AWS.config.update({ AWS.config.update({
region: undefined, region: undefined,
accessKeyId: undefined, accessKeyId: undefined,
secretAccessKey: undefined, secretAccessKey: undefined,
}); });
} }
} }
@@ -647,36 +684,36 @@ const syncSecretsVercel = async ({
decrypt: "true", decrypt: "true",
...(integrationAuth?.teamId ...(integrationAuth?.teamId
? { ? {
teamId: integrationAuth.teamId, teamId: integrationAuth.teamId,
} }
: {}), : {}),
}; };
const vercelSecrets: VercelSecret[] = (await standardRequest.get( const vercelSecrets: VercelSecret[] = (await standardRequest.get(
`${INTEGRATION_VERCEL_API_URL}/v9/projects/${integration.app}/env`, `${INTEGRATION_VERCEL_API_URL}/v9/projects/${integration.app}/env`,
{ {
params, params,
headers: { headers: {
Authorization: `Bearer ${accessToken}`, Authorization: `Bearer ${accessToken}`,
"Accept-Encoding": "application/json", "Accept-Encoding": "application/json",
}, },
} }
)) ))
.data .data
.envs .envs
.filter((secret: VercelSecret) => { .filter((secret: VercelSecret) => {
if (!secret.target.includes(integration.targetEnvironment)) { if (!secret.target.includes(integration.targetEnvironment)) {
// case: secret does not have the same target environment // case: secret does not have the same target environment
return false; return false;
} }
if (integration.targetEnvironment === "preview" && integration.path && integration.path !== secret.gitBranch) { if (integration.targetEnvironment === "preview" && integration.path && integration.path !== secret.gitBranch) {
// case: secret on preview environment does not have same target git branch // case: secret on preview environment does not have same target git branch
return false; return false;
} }
return true; return true;
}); });
const res: { [key: string]: VercelSecret } = {}; const res: { [key: string]: VercelSecret } = {};
@@ -684,14 +721,14 @@ const syncSecretsVercel = async ({
if (vercelSecret.type === "encrypted") { if (vercelSecret.type === "encrypted") {
// case: secret is encrypted -> need to decrypt // case: secret is encrypted -> need to decrypt
const decryptedSecret = (await standardRequest.get( const decryptedSecret = (await standardRequest.get(
`${INTEGRATION_VERCEL_API_URL}/v9/projects/${integration.app}/env/${vercelSecret.id}`, `${INTEGRATION_VERCEL_API_URL}/v9/projects/${integration.app}/env/${vercelSecret.id}`,
{ {
params, params,
headers: { headers: {
Authorization: `Bearer ${accessToken}`, Authorization: `Bearer ${accessToken}`,
"Accept-Encoding": "application/json", "Accept-Encoding": "application/json",
}, },
} }
)).data; )).data;
res[vercelSecret.key] = decryptedSecret; res[vercelSecret.key] = decryptedSecret;
@@ -699,7 +736,7 @@ const syncSecretsVercel = async ({
res[vercelSecret.key] = vercelSecret; res[vercelSecret.key] = vercelSecret;
} }
} }
const updateSecrets: VercelSecret[] = []; const updateSecrets: VercelSecret[] = [];
const deleteSecrets: VercelSecret[] = []; const deleteSecrets: VercelSecret[] = [];
const newSecrets: VercelSecret[] = []; const newSecrets: VercelSecret[] = [];
@@ -730,9 +767,9 @@ const syncSecretsVercel = async ({
key: key, key: key,
value: secrets[key], value: secrets[key],
type: res[key].type, type: res[key].type,
target: res[key].target.includes(integration.targetEnvironment) target: res[key].target.includes(integration.targetEnvironment)
? [...res[key].target] ? [...res[key].target]
: [...res[key].target, integration.targetEnvironment], : [...res[key].target, integration.targetEnvironment],
...(integration.path ? { ...(integration.path ? {
gitBranch: integration.path, gitBranch: integration.path,
} : {}), } : {}),
@@ -782,7 +819,7 @@ const syncSecretsVercel = async ({
}, },
} }
); );
} }
} }
for await (const secret of deleteSecrets) { for await (const secret of deleteSecrets) {
@@ -795,7 +832,7 @@ const syncSecretsVercel = async ({
"Accept-Encoding": "application/json", "Accept-Encoding": "application/json",
}, },
} }
); );
} }
}; };
@@ -1382,7 +1419,7 @@ const syncSecretsCircleCI = async ({
integration: IIntegration; integration: IIntegration;
secrets: any; secrets: any;
accessToken: string; accessToken: string;
}) => { }) => {
const circleciOrganizationDetail = ( const circleciOrganizationDetail = (
await standardRequest.get(`${INTEGRATION_CIRCLECI_API_URL}/v2/me/collaborations`, { await standardRequest.get(`${INTEGRATION_CIRCLECI_API_URL}/v2/me/collaborations`, {
headers: { headers: {
@@ -1469,13 +1506,13 @@ const syncSecretsTravisCI = async ({
} }
) )
) )
.data .data
?.env_vars ?.env_vars
.reduce((obj: any, secret: any) => ({ .reduce((obj: any, secret: any) => ({
...obj, ...obj,
[secret.name]: secret, [secret.name]: secret,
}), {}); }), {});
// add secrets // add secrets
for await (const key of Object.keys(secrets)) { for await (const key of Object.keys(secrets)) {
if (!(key in getSecretsRes)) { if (!(key in getSecretsRes)) {
@@ -1520,7 +1557,7 @@ const syncSecretsTravisCI = async ({
} }
for await (const key of Object.keys(getSecretsRes)) { for await (const key of Object.keys(getSecretsRes)) {
if (!(key in secrets)){ if (!(key in secrets)) {
// delete secret // delete secret
await standardRequest.delete( await standardRequest.delete(
`${INTEGRATION_TRAVISCI_API_URL}/settings/env_vars/${getSecretsRes[key].id}?repository_id=${getSecretsRes[key].repository_id}`, `${INTEGRATION_TRAVISCI_API_URL}/settings/env_vars/${getSecretsRes[key].id}?repository_id=${getSecretsRes[key].repository_id}`,
@@ -1565,29 +1602,29 @@ const syncSecretsGitLab = async ({
"Authorization": `Bearer ${accessToken}`, "Authorization": `Bearer ${accessToken}`,
"Accept-Encoding": "application/json", "Accept-Encoding": "application/json",
}; };
let allEnvVariables: GitLabSecret[] = []; let allEnvVariables: GitLabSecret[] = [];
let url: string | null = `${gitLabApiUrl}?per_page=100`; let url: string | null = `${gitLabApiUrl}?per_page=100`;
while (url) { while (url) {
const response: any = await standardRequest.get(url, { headers }); const response: any = await standardRequest.get(url, { headers });
allEnvVariables = [...allEnvVariables, ...response.data]; allEnvVariables = [...allEnvVariables, ...response.data];
const linkHeader = response.headers.link; const linkHeader = response.headers.link;
const nextLink = linkHeader?.split(",").find((part: string) => part.includes('rel="next"')); const nextLink = linkHeader?.split(",").find((part: string) => part.includes('rel="next"'));
if (nextLink) { if (nextLink) {
url = nextLink.trim().split(";")[0].slice(1, -1); url = nextLink.trim().split(";")[0].slice(1, -1);
} else { } else {
url = null; url = null;
} }
} }
return allEnvVariables; return allEnvVariables;
}; };
const allEnvVariables = await getAllEnvVariables(integration?.appId, accessToken); const allEnvVariables = await getAllEnvVariables(integration?.appId, accessToken);
const getSecretsRes: GitLabSecret[] = allEnvVariables.filter((secret: GitLabSecret) => const getSecretsRes: GitLabSecret[] = allEnvVariables.filter((secret: GitLabSecret) =>
secret.environment_scope === integration.targetEnvironment secret.environment_scope === integration.targetEnvironment
); );
@@ -1669,8 +1706,8 @@ const syncSecretsSupabase = async ({
`${INTEGRATION_SUPABASE_API_URL}/v1/projects/${integration.appId}/secrets`, `${INTEGRATION_SUPABASE_API_URL}/v1/projects/${integration.appId}/secrets`,
{ {
headers: { headers: {
Authorization: `Bearer ${accessToken}`, Authorization: `Bearer ${accessToken}`,
"Accept-Encoding": "application/json", "Accept-Encoding": "application/json",
}, },
} }
); );
@@ -1679,8 +1716,8 @@ const syncSecretsSupabase = async ({
const modifiedFormatForSecretInjection = Object.keys(secrets).map( const modifiedFormatForSecretInjection = Object.keys(secrets).map(
(key) => { (key) => {
return { return {
name: key, name: key,
value: secrets[key], value: secrets[key],
}; };
} }
); );
@@ -1690,8 +1727,8 @@ const syncSecretsSupabase = async ({
modifiedFormatForSecretInjection, modifiedFormatForSecretInjection,
{ {
headers: { headers: {
Authorization: `Bearer ${accessToken}`, Authorization: `Bearer ${accessToken}`,
"Accept-Encoding": "application/json", "Accept-Encoding": "application/json",
}, },
} }
); );
@@ -1699,7 +1736,7 @@ const syncSecretsSupabase = async ({
const secretsToDelete: any = []; const secretsToDelete: any = [];
getSecretsRes?.forEach((secretObj: any) => { getSecretsRes?.forEach((secretObj: any) => {
if (!(secretObj.name in secrets)) { if (!(secretObj.name in secrets)) {
secretsToDelete.push(secretObj.name); secretsToDelete.push(secretObj.name);
} }
}); });
@@ -1746,18 +1783,18 @@ const syncSecretsCheckly = async ({
} }
) )
) )
.data .data
.reduce((obj: any, secret: any) => ({ .reduce((obj: any, secret: any) => ({
...obj, ...obj,
[secret.key]: secret.value, [secret.key]: secret.value,
}), {}); }), {});
// add secrets // add secrets
for await (const key of Object.keys(secrets)) { for await (const key of Object.keys(secrets)) {
if (!(key in getSecretsRes)) { if (!(key in getSecretsRes)) {
// case: secret does not exist in checkly // case: secret does not exist in checkly
// -> add secret // -> add secret
await standardRequest.post( await standardRequest.post(
`${INTEGRATION_CHECKLY_API_URL}/v1/variables`, `${INTEGRATION_CHECKLY_API_URL}/v1/variables`,
{ {
@@ -1776,7 +1813,7 @@ const syncSecretsCheckly = async ({
} else { } else {
// case: secret exists in checkly // case: secret exists in checkly
// -> update/set secret // -> update/set secret
if (secrets[key] !== getSecretsRes[key]) { if (secrets[key] !== getSecretsRes[key]) {
await standardRequest.put( await standardRequest.put(
`${INTEGRATION_CHECKLY_API_URL}/v1/variables/${key}`, `${INTEGRATION_CHECKLY_API_URL}/v1/variables/${key}`,
@@ -1797,7 +1834,7 @@ const syncSecretsCheckly = async ({
} }
for await (const key of Object.keys(getSecretsRes)) { for await (const key of Object.keys(getSecretsRes)) {
if (!(key in secrets)){ if (!(key in secrets)) {
// delete secret // delete secret
await standardRequest.delete( await standardRequest.delete(
`${INTEGRATION_CHECKLY_API_URL}/v1/variables/${key}`, `${INTEGRATION_CHECKLY_API_URL}/v1/variables/${key}`,
@@ -1834,13 +1871,13 @@ const syncSecretsHashiCorpVault = async ({
accessToken: string; accessToken: string;
}) => { }) => {
if (!accessId) return; if (!accessId) return;
interface LoginAppRoleRes { interface LoginAppRoleRes {
auth: { auth: {
client_token: string; client_token: string;
} }
} }
// get Vault client token (could be optimized) // get Vault client token (could be optimized)
const { data }: { data: LoginAppRoleRes } = await standardRequest.post( const { data }: { data: LoginAppRoleRes } = await standardRequest.post(
`${integrationAuth.url}/v1/auth/approle/login`, `${integrationAuth.url}/v1/auth/approle/login`,
@@ -1854,7 +1891,7 @@ const syncSecretsHashiCorpVault = async ({
}, },
} }
); );
const clientToken = data.auth.client_token; const clientToken = data.auth.client_token;
await standardRequest.post( await standardRequest.post(
@@ -1882,46 +1919,46 @@ const syncSecretsHashiCorpVault = async ({
* @param {String} obj.accessToken - API token for Cloudflare * @param {String} obj.accessToken - API token for Cloudflare
*/ */
const syncSecretsCloudflarePages = async ({ const syncSecretsCloudflarePages = async ({
integration, integration,
secrets, secrets,
accessId, accessId,
accessToken, accessToken,
}: { }: {
integration: IIntegration; integration: IIntegration;
secrets: any; secrets: any;
accessId: string | null; accessId: string | null;
accessToken: string; accessToken: string;
}) => { }) => {
// get secrets from cloudflare pages // get secrets from cloudflare pages
const getSecretsRes = ( const getSecretsRes = (
await standardRequest.get( await standardRequest.get(
`${INTEGRATION_CLOUDFLARE_PAGES_API_URL}/client/v4/accounts/${accessId}/pages/projects/${integration.app}`, `${INTEGRATION_CLOUDFLARE_PAGES_API_URL}/client/v4/accounts/${accessId}/pages/projects/${integration.app}`,
{ {
headers: { headers: {
Authorization: `Bearer ${accessToken}`, Authorization: `Bearer ${accessToken}`,
"Accept": "application/json", "Accept": "application/json",
}, },
} }
) )
) )
.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 // copy the secrets object, so we can set deleted keys to null
const secretsObj: any = {...secrets}; const secretsObj: any = { ...secrets };
for (const [key, val] of Object.entries(secretsObj)) { for (const [key, val] of Object.entries(secretsObj)) {
secretsObj[key] = { type: "secret_text", value: val }; secretsObj[key] = { type: "secret_text", value: val };
} }
if (getSecretsRes) { if (getSecretsRes) {
for await (const key of Object.keys(getSecretsRes)) { for await (const key of Object.keys(getSecretsRes)) {
if (!(key in secrets)) { if (!(key in secrets)) {
// case: secret does not exist in infisical // case: secret does not exist in infisical
// -> delete secret from cloudflare pages // -> delete secret from cloudflare pages
secretsObj[key] = null; secretsObj[key] = null;
}
} }
}
} }
const data = { const data = {
@@ -1933,14 +1970,14 @@ const syncSecretsCloudflarePages = async ({
}; };
await standardRequest.patch( await standardRequest.patch(
`${INTEGRATION_CLOUDFLARE_PAGES_API_URL}/client/v4/accounts/${accessId}/pages/projects/${integration.app}`, `${INTEGRATION_CLOUDFLARE_PAGES_API_URL}/client/v4/accounts/${accessId}/pages/projects/${integration.app}`,
data, data,
{ {
headers: { headers: {
Authorization: `Bearer ${accessToken}`, Authorization: `Bearer ${accessToken}`,
"Accept": "application/json", "Accept": "application/json",
}, },
} }
); );
} }
@@ -1961,7 +1998,6 @@ const syncSecretsBitBucket = async ({
secrets: any; secrets: any;
accessToken: string; accessToken: string;
}) => { }) => {
interface VariablesResponse { interface VariablesResponse {
size: number; size: number;
page: number; page: number;
@@ -2060,4 +2096,37 @@ const syncSecretsBitBucket = async ({
} }
} }
/*
* Sync/push [secrets] to Codefresh with name [integration.app]
* @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 Codefresh integration
*/
const syncSecretsCodefresh = async ({
integration,
secrets,
accessToken,
}: {
integration: IIntegration;
secrets: any;
accessToken: string;
}) => {
await standardRequest.patch(
`${INTEGRATION_CODEFRESH_API_URL}/projects/${integration.appId}`,
{
variables: Object.keys(secrets).map((key) => ({
key,
value: secrets[key]
}))
},
{
headers: {
Authorization: `Bearer ${accessToken}`,
"Accept": "application/json",
},
}
);
};
export { syncSecrets }; export { syncSecrets };

View File

@@ -1,4 +1,5 @@
import jwt from "jsonwebtoken"; import jwt from "jsonwebtoken";
import { Types } from "mongoose";
import { NextFunction, Request, Response } from "express"; import { NextFunction, Request, Response } from "express";
import { import {
getAuthAPIKeyPayload, getAuthAPIKeyPayload,
@@ -51,6 +52,10 @@ const requireAuth = ({
}); });
let authPayload: IUser | IServiceAccount | IServiceTokenData; let authPayload: IUser | IServiceAccount | IServiceTokenData;
let authUserPayload: {
user: IUser;
tokenVersionId: Types.ObjectId;
};
switch (authMode) { switch (authMode) {
case AUTH_MODE_SERVICE_ACCOUNT: case AUTH_MODE_SERVICE_ACCOUNT:
authPayload = await getAuthSAAKPayload({ authPayload = await getAuthSAAKPayload({
@@ -71,12 +76,12 @@ const requireAuth = ({
req.user = authPayload; req.user = authPayload;
break; break;
default: default:
const { user, tokenVersionId } = await getAuthUserPayload({ authUserPayload = await getAuthUserPayload({
authTokenValue, authTokenValue,
}); });
authPayload = user; authPayload = authUserPayload.user;
req.user = user; req.user = authUserPayload.user;
req.tokenVersionId = tokenVersionId; req.tokenVersionId = authUserPayload.tokenVersionId;
break; break;
} }

View File

@@ -0,0 +1,98 @@
import { Schema, Types, model } from "mongoose";
import {
ALGORITHM_AES_256_GCM,
ENCODING_SCHEME_BASE64,
ENCODING_SCHEME_UTF8,
} from "../variables";
export interface IBotOrg {
_id: Types.ObjectId;
name: string;
organization: Types.ObjectId;
publicKey: string;
encryptedSymmetricKey: string;
symmetricKeyIV: string;
symmetricKeyTag: string;
symmetricKeyAlgorithm: "aes-256-gcm";
symmetricKeyKeyEncoding: "base64" | "utf8";
encryptedPrivateKey: string;
privateKeyIV: string;
privateKeyTag: string;
privateKeyAlgorithm: "aes-256-gcm";
privateKeyKeyEncoding: "base64" | "utf8";
}
const botOrgSchema = new Schema<IBotOrg>(
{
name: {
type: String,
required: true,
},
organization: {
type: Schema.Types.ObjectId,
ref: "Organization",
required: true,
},
publicKey: {
type: String,
required: true,
},
encryptedSymmetricKey: {
type: String,
required: true
},
symmetricKeyIV: {
type: String,
required: true
},
symmetricKeyTag: {
type: String,
required: true
},
symmetricKeyAlgorithm: {
type: String,
enum: [ALGORITHM_AES_256_GCM],
required: true
},
symmetricKeyKeyEncoding: {
type: String,
enum: [
ENCODING_SCHEME_UTF8,
ENCODING_SCHEME_BASE64,
],
required: true
},
encryptedPrivateKey: {
type: String,
required: true
},
privateKeyIV: {
type: String,
required: true
},
privateKeyTag: {
type: String,
required: true
},
privateKeyAlgorithm: {
type: String,
enum: [ALGORITHM_AES_256_GCM],
required: true
},
privateKeyKeyEncoding: {
type: String,
enum: [
ENCODING_SCHEME_UTF8,
ENCODING_SCHEME_BASE64,
],
required: true
},
},
{
timestamps: true,
}
);
const BotOrg = model<IBotOrg>("BotOrg", botOrgSchema);
export default BotOrg;

View File

@@ -25,6 +25,7 @@ export type GitRisks = {
tags: string[]; tags: string[];
ruleID: string; ruleID: string;
fingerprint: string; fingerprint: string;
fingerPrintWithoutCommitId: string
isFalsePositive: boolean; // New field for marking risks as false positives isFalsePositive: boolean; // New field for marking risks as false positives
isResolved: boolean; // New field for marking risks as resolved isResolved: boolean; // New field for marking risks as resolved
@@ -94,6 +95,9 @@ const gitRisks = new Schema<GitRisks>({
type: String, type: String,
unique: true unique: true
}, },
fingerPrintWithoutCommitId: {
type: String,
},
isFalsePositive: { isFalsePositive: {
type: Boolean, type: Boolean,
default: false default: false

View File

@@ -1,5 +1,6 @@
import BackupPrivateKey, { IBackupPrivateKey } from "./backupPrivateKey"; import BackupPrivateKey, { IBackupPrivateKey } from "./backupPrivateKey";
import Bot, { IBot } from "./bot"; import Bot, { IBot } from "./bot";
import BotOrg, { IBotOrg } from "./botOrg";
import BotKey, { IBotKey } from "./botKey"; import BotKey, { IBotKey } from "./botKey";
import IncidentContactOrg, { IIncidentContactOrg } from "./incidentContactOrg"; import IncidentContactOrg, { IIncidentContactOrg } from "./incidentContactOrg";
import Integration, { IIntegration } from "./integration"; import Integration, { IIntegration } from "./integration";
@@ -16,13 +17,14 @@ import ServiceAccountKey, { IServiceAccountKey } from "./serviceAccountKey"; //
import ServiceAccountOrganizationPermission, { IServiceAccountOrganizationPermission } from "./serviceAccountOrganizationPermission"; // new import ServiceAccountOrganizationPermission, { IServiceAccountOrganizationPermission } from "./serviceAccountOrganizationPermission"; // new
import ServiceAccountWorkspacePermission, { IServiceAccountWorkspacePermission } from "./serviceAccountWorkspacePermission"; // new import ServiceAccountWorkspacePermission, { IServiceAccountWorkspacePermission } from "./serviceAccountWorkspacePermission"; // new
import TokenData, { ITokenData } from "./tokenData"; import TokenData, { ITokenData } from "./tokenData";
import User,{ AuthProvider, IUser } from "./user"; import User, { AuthProvider, IUser } from "./user";
import UserAction, { IUserAction } from "./userAction"; import UserAction, { IUserAction } from "./userAction";
import Workspace, { IWorkspace } from "./workspace"; import Workspace, { IWorkspace } from "./workspace";
import ServiceTokenData, { IServiceTokenData } from "./serviceTokenData"; import ServiceTokenData, { IServiceTokenData } from "./serviceTokenData";
import APIKeyData, { IAPIKeyData } from "./apiKeyData"; import APIKeyData, { IAPIKeyData } from "./apiKeyData";
import LoginSRPDetail, { ILoginSRPDetail } from "./loginSRPDetail"; import LoginSRPDetail, { ILoginSRPDetail } from "./loginSRPDetail";
import TokenVersion, { ITokenVersion } from "./tokenVersion"; import TokenVersion, { ITokenVersion } from "./tokenVersion";
import GitRisks, { STATUS_RESOLVED_FALSE_POSITIVE } from "./gitRisks";
export { export {
AuthProvider, AuthProvider,
@@ -30,6 +32,8 @@ export {
IBackupPrivateKey, IBackupPrivateKey,
Bot, Bot,
IBot, IBot,
BotOrg,
IBotOrg,
BotKey, BotKey,
IBotKey, IBotKey,
IncidentContactOrg, IncidentContactOrg,
@@ -76,4 +80,6 @@ export {
ILoginSRPDetail, ILoginSRPDetail,
TokenVersion, TokenVersion,
ITokenVersion, ITokenVersion,
GitRisks,
STATUS_RESOLVED_FALSE_POSITIVE
}; };

View File

@@ -19,6 +19,7 @@ import {
INTEGRATION_SUPABASE, INTEGRATION_SUPABASE,
INTEGRATION_TRAVISCI, INTEGRATION_TRAVISCI,
INTEGRATION_VERCEL, INTEGRATION_VERCEL,
INTEGRATION_CODEFRESH
} from "../variables"; } from "../variables";
export interface IIntegration { export interface IIntegration {
@@ -56,7 +57,8 @@ export interface IIntegration {
| "checkly" | "checkly"
| "hashicorp-vault" | "hashicorp-vault"
| "cloudflare-pages" | "cloudflare-pages"
| "bitbucket"; | "bitbucket"
| "codefresh"
integrationAuth: Types.ObjectId; integrationAuth: Types.ObjectId;
} }
@@ -147,6 +149,7 @@ const integrationSchema = new Schema<IIntegration>(
INTEGRATION_HASHICORP_VAULT, INTEGRATION_HASHICORP_VAULT,
INTEGRATION_CLOUDFLARE_PAGES, INTEGRATION_CLOUDFLARE_PAGES,
INTEGRATION_BITBUCKET, INTEGRATION_BITBUCKET,
INTEGRATION_CODEFRESH
], ],
required: true, required: true,
}, },

View File

@@ -20,7 +20,8 @@ import {
INTEGRATION_RENDER, INTEGRATION_RENDER,
INTEGRATION_SUPABASE, INTEGRATION_SUPABASE,
INTEGRATION_TRAVISCI, INTEGRATION_TRAVISCI,
INTEGRATION_VERCEL INTEGRATION_VERCEL,
INTEGRATION_CODEFRESH
} from "../variables"; } from "../variables";
export interface IIntegrationAuth extends Document { export interface IIntegrationAuth extends Document {
@@ -44,6 +45,7 @@ export interface IIntegrationAuth extends Document {
| "aws-secret-manager" | "aws-secret-manager"
| "checkly" | "checkly"
| "cloudflare-pages" | "cloudflare-pages"
| "codefresh"
| "bitbucket"; | "bitbucket";
teamId: string; teamId: string;
accountId: string; accountId: string;
@@ -90,7 +92,8 @@ const integrationAuthSchema = new Schema<IIntegrationAuth>(
INTEGRATION_SUPABASE, INTEGRATION_SUPABASE,
INTEGRATION_HASHICORP_VAULT, INTEGRATION_HASHICORP_VAULT,
INTEGRATION_CLOUDFLARE_PAGES, INTEGRATION_CLOUDFLARE_PAGES,
INTEGRATION_BITBUCKET INTEGRATION_BITBUCKET,
INTEGRATION_CODEFRESH
], ],
required: true, required: true,
}, },

View File

@@ -0,0 +1,52 @@
import { Schema, Types, model } from "mongoose";
export interface ISecretImports {
_id: Types.ObjectId;
workspace: Types.ObjectId;
environment: string;
folderId: string;
imports: Array<{
environment: string;
secretPath: string;
}>;
}
const secretImportSchema = new Schema<ISecretImports>(
{
workspace: {
type: Schema.Types.ObjectId,
ref: "Workspace",
required: true
},
environment: {
type: String,
required: true
},
folderId: {
type: String,
required: true,
default: "root"
},
imports: {
type: [
{
environment: {
type: String,
required: true
},
secretPath: {
type: String,
required: true
}
}
],
default: []
}
},
{
timestamps: true
}
);
const SecretImport = model<ISecretImports>("SecretImports", secretImportSchema);
export default SecretImport;

View File

@@ -2,6 +2,7 @@ import { Document, Schema, Types, model } from "mongoose";
export enum AuthProvider { export enum AuthProvider {
GOOGLE = "google", GOOGLE = "google",
OKTA_SAML = "okta-saml"
} }
export interface IUser extends Document { export interface IUser extends Document {

View File

@@ -1,5 +1,5 @@
import express, { Request, Response } from "express"; import express, { Request, Response } from "express";
import { getSmtpConfigured } from "../../config"; import { getSecretScanningGitAppId, getSecretScanningPrivateKey, getSecretScanningWebhookSecret, getSmtpConfigured } from "../../config";
const router = express.Router(); const router = express.Router();
@@ -10,6 +10,7 @@ router.get(
date: new Date(), date: new Date(),
message: "Ok", message: "Ok",
emailConfigured: await getSmtpConfigured(), emailConfigured: await getSmtpConfigured(),
secretScanningConfigured: await getSecretScanningGitAppId() && await getSecretScanningWebhookSecret() && await getSecretScanningPrivateKey(),
}) })
} }
); );

View File

@@ -1,7 +1,6 @@
import express from "express"; import express from "express";
const router = express.Router(); const router = express.Router();
import { body } from "express-validator"; import { body } from "express-validator";
import passport from "passport";
import { requireAuth, validateRequest } from "../../middleware"; import { requireAuth, validateRequest } from "../../middleware";
import { authController } from "../../controllers/v1"; import { authController } from "../../controllers/v1";
import { authLimiter } from "../../helpers/rateLimiter"; import { authLimiter } from "../../helpers/rateLimiter";
@@ -44,21 +43,6 @@ router.post(
authController.checkAuth authController.checkAuth
); );
router.get(
"/redirect/google",
authLimiter,
passport.authenticate("google", {
scope: ["profile", "email"],
session: false,
}),
);
router.get(
"/callback/google",
passport.authenticate("google", { failureRedirect: "/login/provider/error", session: false }),
authController.handleAuthProviderCallback,
);
router.get( router.get(
"/common-passwords", "/common-passwords",
authLimiter, authLimiter,

View File

@@ -17,6 +17,7 @@ import integrationAuth from "./integrationAuth";
import secretsFolder from "./secretsFolder"; import secretsFolder from "./secretsFolder";
import secretScanning from "./secretScanning"; import secretScanning from "./secretScanning";
import webhooks from "./webhook"; import webhooks from "./webhook";
import secretImport from "./secretImport";
export { export {
signup, signup,
@@ -37,5 +38,6 @@ export {
integrationAuth, integrationAuth,
secretsFolder, secretsFolder,
secretScanning, secretScanning,
webhooks webhooks,
secretImport
}; };

View File

@@ -0,0 +1,84 @@
import express from "express";
const router = express.Router();
import { body, param, query } from "express-validator";
import { secretImportController } from "../../controllers/v1";
import { requireAuth, requireWorkspaceAuth, validateRequest } from "../../middleware";
import { ADMIN, AUTH_MODE_JWT, MEMBER } from "../../variables";
router.post(
"/",
requireAuth({
acceptedAuthModes: [AUTH_MODE_JWT]
}),
requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER],
locationWorkspaceId: "body"
}),
body("workspaceId").exists().isString().trim().notEmpty(),
body("environment").exists().isString().trim().notEmpty(),
body("folderId").default("root").isString().trim(),
body("secretImport").exists().isObject(),
body("secretImport.environment").isString().exists().trim(),
body("secretImport.secretPath").isString().exists().trim(),
validateRequest,
secretImportController.createSecretImport
);
router.put(
"/:id",
requireAuth({
acceptedAuthModes: [AUTH_MODE_JWT]
}),
param("id").exists().isString().trim(),
body("secretImports").exists().isArray(),
body("secretImports.*.environment").isString().exists().trim(),
body("secretImports.*.secretPath").isString().exists().trim(),
validateRequest,
secretImportController.updateSecretImport
);
router.delete(
"/:id",
requireAuth({
acceptedAuthModes: [AUTH_MODE_JWT]
}),
param("id").exists().isString().trim(),
body("secretImportPath").isString().exists().trim(),
body("secretImportEnv").isString().exists().trim(),
validateRequest,
secretImportController.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;

View File

@@ -69,6 +69,7 @@ router.post(
}), }),
param("organizationId").exists().trim(), param("organizationId").exists().trim(),
param("riskId").exists().trim(), param("riskId").exists().trim(),
body("status").exists(),
requireOrganizationAuth({ requireOrganizationAuth({
acceptedRoles: [OWNER, ADMIN, MEMBER], acceptedRoles: [OWNER, ADMIN, MEMBER],
acceptedStatuses: [ACCEPTED], acceptedStatuses: [ACCEPTED],

View File

@@ -2,7 +2,7 @@ import express from "express";
const router = express.Router(); const router = express.Router();
import { requireAuth, requireWorkspaceAuth, validateRequest } from "../../middleware"; import { requireAuth, requireWorkspaceAuth, validateRequest } from "../../middleware";
import { body, param, query } from "express-validator"; import { body, param, query } from "express-validator";
import { ADMIN, AUTH_MODE_JWT, AUTH_MODE_SERVICE_ACCOUNT, MEMBER } from "../../variables"; import { ADMIN, AUTH_MODE_JWT, MEMBER } from "../../variables";
import { webhookController } from "../../controllers/v1"; import { webhookController } from "../../controllers/v1";
router.post( router.post(

View File

@@ -127,6 +127,7 @@ router.get(
query("tagSlugs"), query("tagSlugs"),
query("folderId").default("root").isString().trim(), query("folderId").default("root").isString().trim(),
query("secretPath").optional().isString().trim(), query("secretPath").optional().isString().trim(),
query("include_imports").optional().default(false).isBoolean(),
validateRequest, validateRequest,
requireAuth({ requireAuth({
acceptedAuthModes: [ acceptedAuthModes: [

View File

@@ -29,6 +29,16 @@ router.patch(
usersController.updateMyMfaEnabled usersController.updateMyMfaEnabled
); );
router.patch(
"/me/name",
requireAuth({
acceptedAuthModes: [AUTH_MODE_JWT, AUTH_MODE_API_KEY],
}),
body("firstName").exists(),
validateRequest,
usersController.updateName
);
router.get( router.get(
"/me/organizations", "/me/organizations",
requireAuth({ requireAuth({

View File

@@ -1,10 +1,6 @@
import express from "express"; import express from "express";
const router = express.Router(); const router = express.Router();
import { import { requireAuth, requireWorkspaceAuth, validateRequest } from "../../middleware";
requireAuth,
requireWorkspaceAuth,
validateRequest,
} from "../../middleware";
import { body, param, query } from "express-validator"; import { body, param, query } from "express-validator";
import { secretsController } from "../../controllers/v3"; import { secretsController } from "../../controllers/v3";
import { import {
@@ -17,7 +13,7 @@ import {
PERMISSION_READ_SECRETS, PERMISSION_READ_SECRETS,
PERMISSION_WRITE_SECRETS, PERMISSION_WRITE_SECRETS,
SECRET_PERSONAL, SECRET_PERSONAL,
SECRET_SHARED, SECRET_SHARED
} from "../../variables"; } from "../../variables";
router.get( router.get(
@@ -25,14 +21,15 @@ router.get(
query("workspaceId").exists().isString().trim(), query("workspaceId").exists().isString().trim(),
query("environment").exists().isString().trim(), query("environment").exists().isString().trim(),
query("secretPath").default("/").isString().trim(), query("secretPath").default("/").isString().trim(),
query("include_imports").optional().isBoolean().default(false),
validateRequest, validateRequest,
requireAuth({ requireAuth({
acceptedAuthModes: [ acceptedAuthModes: [
AUTH_MODE_JWT, AUTH_MODE_JWT,
AUTH_MODE_API_KEY, AUTH_MODE_API_KEY,
AUTH_MODE_SERVICE_TOKEN, AUTH_MODE_SERVICE_TOKEN,
AUTH_MODE_SERVICE_ACCOUNT, AUTH_MODE_SERVICE_ACCOUNT
], ]
}), }),
requireWorkspaceAuth({ requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER], acceptedRoles: [ADMIN, MEMBER],
@@ -40,7 +37,7 @@ router.get(
locationEnvironment: "query", locationEnvironment: "query",
requiredPermissions: [PERMISSION_READ_SECRETS], requiredPermissions: [PERMISSION_READ_SECRETS],
requireBlindIndicesEnabled: true, requireBlindIndicesEnabled: true,
requireE2EEOff: true, requireE2EEOff: true
}), }),
secretsController.getSecretsRaw secretsController.getSecretsRaw
); );
@@ -58,8 +55,8 @@ router.get(
AUTH_MODE_JWT, AUTH_MODE_JWT,
AUTH_MODE_API_KEY, AUTH_MODE_API_KEY,
AUTH_MODE_SERVICE_TOKEN, AUTH_MODE_SERVICE_TOKEN,
AUTH_MODE_SERVICE_ACCOUNT, AUTH_MODE_SERVICE_ACCOUNT
], ]
}), }),
requireWorkspaceAuth({ requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER], acceptedRoles: [ADMIN, MEMBER],
@@ -67,7 +64,7 @@ router.get(
locationEnvironment: "query", locationEnvironment: "query",
requiredPermissions: [PERMISSION_READ_SECRETS], requiredPermissions: [PERMISSION_READ_SECRETS],
requireBlindIndicesEnabled: true, requireBlindIndicesEnabled: true,
requireE2EEOff: true, requireE2EEOff: true
}), }),
secretsController.getSecretByNameRaw secretsController.getSecretByNameRaw
); );
@@ -86,8 +83,8 @@ router.post(
AUTH_MODE_JWT, AUTH_MODE_JWT,
AUTH_MODE_API_KEY, AUTH_MODE_API_KEY,
AUTH_MODE_SERVICE_TOKEN, AUTH_MODE_SERVICE_TOKEN,
AUTH_MODE_SERVICE_ACCOUNT, AUTH_MODE_SERVICE_ACCOUNT
], ]
}), }),
requireWorkspaceAuth({ requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER], acceptedRoles: [ADMIN, MEMBER],
@@ -95,7 +92,7 @@ router.post(
locationEnvironment: "body", locationEnvironment: "body",
requiredPermissions: [PERMISSION_WRITE_SECRETS], requiredPermissions: [PERMISSION_WRITE_SECRETS],
requireBlindIndicesEnabled: true, requireBlindIndicesEnabled: true,
requireE2EEOff: true, requireE2EEOff: true
}), }),
secretsController.createSecretRaw secretsController.createSecretRaw
); );
@@ -114,8 +111,8 @@ router.patch(
AUTH_MODE_JWT, AUTH_MODE_JWT,
AUTH_MODE_API_KEY, AUTH_MODE_API_KEY,
AUTH_MODE_SERVICE_TOKEN, AUTH_MODE_SERVICE_TOKEN,
AUTH_MODE_SERVICE_ACCOUNT, AUTH_MODE_SERVICE_ACCOUNT
], ]
}), }),
requireWorkspaceAuth({ requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER], acceptedRoles: [ADMIN, MEMBER],
@@ -123,7 +120,7 @@ router.patch(
locationEnvironment: "body", locationEnvironment: "body",
requiredPermissions: [PERMISSION_WRITE_SECRETS], requiredPermissions: [PERMISSION_WRITE_SECRETS],
requireBlindIndicesEnabled: true, requireBlindIndicesEnabled: true,
requireE2EEOff: true, requireE2EEOff: true
}), }),
secretsController.updateSecretByNameRaw secretsController.updateSecretByNameRaw
); );
@@ -141,8 +138,8 @@ router.delete(
AUTH_MODE_JWT, AUTH_MODE_JWT,
AUTH_MODE_API_KEY, AUTH_MODE_API_KEY,
AUTH_MODE_SERVICE_TOKEN, AUTH_MODE_SERVICE_TOKEN,
AUTH_MODE_SERVICE_ACCOUNT, AUTH_MODE_SERVICE_ACCOUNT
], ]
}), }),
requireWorkspaceAuth({ requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER], acceptedRoles: [ADMIN, MEMBER],
@@ -150,7 +147,7 @@ router.delete(
locationEnvironment: "body", locationEnvironment: "body",
requiredPermissions: [PERMISSION_WRITE_SECRETS], requiredPermissions: [PERMISSION_WRITE_SECRETS],
requireBlindIndicesEnabled: true, requireBlindIndicesEnabled: true,
requireE2EEOff: true, requireE2EEOff: true
}), }),
secretsController.deleteSecretByNameRaw secretsController.deleteSecretByNameRaw
); );
@@ -166,8 +163,8 @@ router.get(
AUTH_MODE_JWT, AUTH_MODE_JWT,
AUTH_MODE_API_KEY, AUTH_MODE_API_KEY,
AUTH_MODE_SERVICE_TOKEN, AUTH_MODE_SERVICE_TOKEN,
AUTH_MODE_SERVICE_ACCOUNT, AUTH_MODE_SERVICE_ACCOUNT
], ]
}), }),
requireWorkspaceAuth({ requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER], acceptedRoles: [ADMIN, MEMBER],
@@ -175,7 +172,7 @@ router.get(
locationEnvironment: "query", locationEnvironment: "query",
requiredPermissions: [PERMISSION_READ_SECRETS], requiredPermissions: [PERMISSION_READ_SECRETS],
requireBlindIndicesEnabled: true, requireBlindIndicesEnabled: true,
requireE2EEOff: false, requireE2EEOff: false
}), }),
secretsController.getSecrets secretsController.getSecrets
); );
@@ -201,8 +198,8 @@ router.post(
AUTH_MODE_JWT, AUTH_MODE_JWT,
AUTH_MODE_API_KEY, AUTH_MODE_API_KEY,
AUTH_MODE_SERVICE_TOKEN, AUTH_MODE_SERVICE_TOKEN,
AUTH_MODE_SERVICE_ACCOUNT, AUTH_MODE_SERVICE_ACCOUNT
], ]
}), }),
requireWorkspaceAuth({ requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER], acceptedRoles: [ADMIN, MEMBER],
@@ -210,7 +207,7 @@ router.post(
locationEnvironment: "body", locationEnvironment: "body",
requiredPermissions: [PERMISSION_WRITE_SECRETS], requiredPermissions: [PERMISSION_WRITE_SECRETS],
requireBlindIndicesEnabled: true, requireBlindIndicesEnabled: true,
requireE2EEOff: false, requireE2EEOff: false
}), }),
secretsController.createSecret secretsController.createSecret
); );
@@ -228,15 +225,15 @@ router.get(
AUTH_MODE_JWT, AUTH_MODE_JWT,
AUTH_MODE_API_KEY, AUTH_MODE_API_KEY,
AUTH_MODE_SERVICE_TOKEN, AUTH_MODE_SERVICE_TOKEN,
AUTH_MODE_SERVICE_ACCOUNT, AUTH_MODE_SERVICE_ACCOUNT
], ]
}), }),
requireWorkspaceAuth({ requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER], acceptedRoles: [ADMIN, MEMBER],
locationWorkspaceId: "query", locationWorkspaceId: "query",
locationEnvironment: "query", locationEnvironment: "query",
requiredPermissions: [PERMISSION_READ_SECRETS], requiredPermissions: [PERMISSION_READ_SECRETS],
requireBlindIndicesEnabled: true, requireBlindIndicesEnabled: true
}), }),
secretsController.getSecretByName secretsController.getSecretByName
); );
@@ -257,8 +254,8 @@ router.patch(
AUTH_MODE_JWT, AUTH_MODE_JWT,
AUTH_MODE_API_KEY, AUTH_MODE_API_KEY,
AUTH_MODE_SERVICE_TOKEN, AUTH_MODE_SERVICE_TOKEN,
AUTH_MODE_SERVICE_ACCOUNT, AUTH_MODE_SERVICE_ACCOUNT
], ]
}), }),
requireWorkspaceAuth({ requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER], acceptedRoles: [ADMIN, MEMBER],
@@ -266,7 +263,7 @@ router.patch(
locationEnvironment: "body", locationEnvironment: "body",
requiredPermissions: [PERMISSION_WRITE_SECRETS], requiredPermissions: [PERMISSION_WRITE_SECRETS],
requireBlindIndicesEnabled: true, requireBlindIndicesEnabled: true,
requireE2EEOff: false, requireE2EEOff: false
}), }),
secretsController.updateSecretByName secretsController.updateSecretByName
); );
@@ -284,8 +281,8 @@ router.delete(
AUTH_MODE_JWT, AUTH_MODE_JWT,
AUTH_MODE_API_KEY, AUTH_MODE_API_KEY,
AUTH_MODE_SERVICE_TOKEN, AUTH_MODE_SERVICE_TOKEN,
AUTH_MODE_SERVICE_ACCOUNT, AUTH_MODE_SERVICE_ACCOUNT
], ]
}), }),
requireWorkspaceAuth({ requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER], acceptedRoles: [ADMIN, MEMBER],
@@ -293,7 +290,7 @@ router.delete(
locationEnvironment: "body", locationEnvironment: "body",
requiredPermissions: [PERMISSION_WRITE_SECRETS], requiredPermissions: [PERMISSION_WRITE_SECRETS],
requireBlindIndicesEnabled: true, requireBlindIndicesEnabled: true,
requireE2EEOff: false, requireE2EEOff: false
}), }),
secretsController.deleteSecretByName secretsController.deleteSecretByName
); );

View File

@@ -21,7 +21,8 @@ router.post(
body("salt").exists().isString().trim().notEmpty(), body("salt").exists().isString().trim().notEmpty(),
body("verifier").exists().isString().trim().notEmpty(), body("verifier").exists().isString().trim().notEmpty(),
body("organizationName").exists().isString().trim().notEmpty(), body("organizationName").exists().isString().trim().notEmpty(),
body("providerAuthToken").isString().trim().optional({nullable: true}), body("providerAuthToken").isString().trim().optional({ nullable: true }),
body("attributionSource").optional().isString().trim(),
validateRequest, validateRequest,
signupController.completeAccountSignup, signupController.completeAccountSignup,
); );

View File

@@ -0,0 +1,12 @@
import { Types } from "mongoose";
import { getSymmetricKeyHelper } from "../helpers/botOrg";
// TODO: DOCstrings
class BotOrgService {
static async getSymmetricKey(organizationId: Types.ObjectId) {
return await getSymmetricKeyHelper(organizationId);
}
}
export default BotOrgService;

View File

@@ -1,15 +1,15 @@
import { Probot } from "probot"; import { Probot } from "probot";
import { exec } from "child_process"; import { exec } from "child_process";
import { writeFile, readFile, rm, mkdir, } from "fs"; import { mkdir, readFile, rm, writeFile } from "fs";
import { tmpdir } from "os"; import { tmpdir } from "os";
import { join } from "path" import { join } from "path"
import mongoose from "mongoose"; import GitRisks from "../models/gitRisks";
import GitRisks, { STATUS_RESOLVED_FALSE_POSITIVE } from "./models/gitRisks"; import GitAppOrganizationInstallation from "../models/gitAppOrganizationInstallation";
import GitAppOrganizationInstallation from "./models/gitAppOrganizationInstallation"; import MembershipOrg from "../models/membershipOrg";
import { sendMail, setTransporter } from "./helper/nodemailer"; import { ADMIN, OWNER } from "../variables";
import { initSmtp } from "./service/smtp"; import User from "../models/user";
import MembershipOrg, { ADMIN, OWNER } from "./models/membershipOrg"; import { sendMail } from "../helpers";
import User from "./models/user"; import TelemetryService from "./TelemetryService";
type SecretMatch = { type SecretMatch = {
Description: string; Description: string;
@@ -30,19 +30,10 @@ type SecretMatch = {
Tags: string[]; Tags: string[];
RuleID: string; RuleID: string;
Fingerprint: string; Fingerprint: string;
FingerPrintWithoutCommitId: string
}; };
export = async (app: Probot) => { export default async (app: Probot) => {
// connect to DB
initDatabase()
setTransporter(await initSmtp());
app.on("installation.created", async (context) => {
const { payload } = context;
// console.log("payload==>", payload.installation.repository_selection)
})
app.on("installation.deleted", async (context) => { app.on("installation.deleted", async (context) => {
const { payload } = context; const { payload } = context;
const { installation, repositories } = payload; const { installation, repositories } = payload;
@@ -50,8 +41,10 @@ export = async (app: Probot) => {
await GitRisks.deleteMany({ installationId: installation.id }) await GitRisks.deleteMany({ installationId: installation.id })
await GitAppOrganizationInstallation.deleteOne({ installationId: installation.id }) await GitAppOrganizationInstallation.deleteOne({ installationId: installation.id })
} else { } else {
for (const repository of repositories) { if (repositories) {
await GitRisks.deleteMany({ repositoryId: repository.id }) for (const repository of repositories) {
await GitRisks.deleteMany({ repositoryId: repository.id })
}
} }
} }
}) })
@@ -59,9 +52,13 @@ export = async (app: Probot) => {
app.on("push", async (context) => { app.on("push", async (context) => {
const { payload } = context; const { payload } = context;
const { commits, repository, installation, pusher } = payload; const { commits, repository, installation, pusher } = payload;
const [owner, repo] = repository.full_name.split('/'); const [owner, repo] = repository.full_name.split("/");
const installationLinkToOrgExists = await GitAppOrganizationInstallation.findOne({ installationId: installation.id }).lean() if (!commits || !repository || !installation || !pusher) {
return
}
const installationLinkToOrgExists = await GitAppOrganizationInstallation.findOne({ installationId: installation?.id }).lean()
if (!installationLinkToOrgExists) { if (!installationLinkToOrgExists) {
return return
} }
@@ -78,31 +75,30 @@ export = async (app: Probot) => {
}); });
const data: any = fileContentsResponse.data; const data: any = fileContentsResponse.data;
const fileContent = Buffer.from(data.content, 'base64').toString(); const fileContent = Buffer.from(data.content, "base64").toString();
const findings = await scanContentAndGetFindings(`\n${fileContent}`) // to count lines correctly const findings = await scanContentAndGetFindings(`\n${fileContent}`) // extra line to count lines correctly
for (const finding of findings) { for (const finding of findings) {
const fingerPrint = `${commit.id}:${filepath}:${finding.RuleID}:${finding.StartLine}` const fingerPrintWithCommitId = `${commit.id}:${filepath}:${finding.RuleID}:${finding.StartLine}`
finding.Fingerprint = fingerPrint const fingerPrintWithoutCommitId = `${filepath}:${finding.RuleID}:${finding.StartLine}`
finding.Fingerprint = fingerPrintWithCommitId
finding.FingerPrintWithoutCommitId = fingerPrintWithoutCommitId
finding.Commit = commit.id finding.Commit = commit.id
finding.File = filepath finding.File = filepath
finding.Author = commit.author.name finding.Author = commit.author.name
finding.Email = commit.author.email finding.Email = commit?.author?.email ? commit?.author?.email : ""
allFindingsByFingerprint[fingerPrint] = finding
allFindingsByFingerprint[fingerPrintWithCommitId] = finding
} }
} catch (error) { } catch (error) {
console.error(`Error fetching content for ${filepath}`, error); console.error(`Error fetching content for ${filepath}`, error); // eslint-disable-line
} }
} }
} }
// change to update // change to update
const noneFalsePositiveFindings = {}
for (const key in allFindingsByFingerprint) { for (const key in allFindingsByFingerprint) {
const risk = await GitRisks.findOneAndUpdate({ fingerprint: allFindingsByFingerprint[key].Fingerprint }, const risk = await GitRisks.findOneAndUpdate({ fingerprint: allFindingsByFingerprint[key].Fingerprint },
{ {
@@ -114,11 +110,6 @@ export = async (app: Probot) => {
}, { }, {
upsert: true upsert: true
}).lean() }).lean()
if (risk.status != STATUS_RESOLVED_FALSE_POSITIVE) {
noneFalsePositiveFindings[key] = { ...convertKeysToLowercase(allFindingsByFingerprint[key]) }
}
} }
// get emails of admins // get emails of admins
const adminsOfWork = await MembershipOrg.find({ const adminsOfWork = await MembershipOrg.find({
@@ -137,39 +128,30 @@ export = async (app: Probot) => {
const adminOrOwnerEmails = userEmails.map(userObject => userObject.email) 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
}
});
}
// TODO const postHogClient = await TelemetryService.getPostHogClient();
// don't notify if the risk is marked as false positive if (postHogClient) {
postHogClient.capture({
await sendMail({ event: "cloud secret scan",
template: "secretLeakIncident.handlebars", distinctId: pusher.email,
subjectLine: `Incident alert: leaked secrets found in Github repository ${repository.full_name}`, properties: {
recipients: [pusher.email, ...adminOrOwnerEmails], numberOfCommitsScanned: commits.length,
substitutions: { numberOfRisksFound: Object.keys(allFindingsByFingerprint).length,
numberOfSecrets: Object.keys(allFindingsByFingerprint).length, }
pusher_email: pusher.email, });
pusher_name: pusher.name
}
});
});
app.on(['pull_request.opened', 'pull_request.synchronize'], async (context) => {
const { payload } = context;
const { pull_request } = payload
if (false) {
const check = {
owner: pull_request.head.repo.owner.login,
repo: pull_request.head.repo.name,
name: 'Secret Detection',
head_sha: pull_request.head.sha,
status: 'completed',
conclusion: 'failure',
output: {
title: `X Secrets detected`,
summary: 'We detected potential leaked secret(s) in your pull request.',
},
};
return context.octokit.checks.create(check);
} }
}); });
}; };
@@ -254,28 +236,13 @@ function deleteTempFolder(folderPath: string): Promise<void> {
}); });
} }
const initDatabase = async () => {
try {
await mongoose.connect(process.env.MONGO_URL);
// allow empty strings to pass the required validator
mongoose.Schema.Types.String.checkRequired(v => typeof v === "string");
console.log("Database connection established");
} catch (err) {
console.log(`Unable to establish Database connection due to the error.\n${err}`);
}
return mongoose.connection;
}
function convertKeysToLowercase<T>(obj: T): T { function convertKeysToLowercase<T>(obj: T): T {
const convertedObj = {} as T; const convertedObj = {} as T;
for (const key in obj) { for (const key in obj) {
if (Object.prototype.hasOwnProperty.call(obj, key)) { if (Object.prototype.hasOwnProperty.call(obj, key)) {
const lowercaseKey = key.charAt(0).toLowerCase() + key.slice(1); const lowercaseKey = key.charAt(0).toLowerCase() + key.slice(1);
convertedObj[lowercaseKey] = obj[key]; convertedObj[lowercaseKey as keyof T] = obj[key];
} }
} }

View File

@@ -0,0 +1,87 @@
import { Types } from "mongoose";
import Folder from "../models/folder";
import Secret, { ISecret } from "../models/secret";
import SecretImport from "../models/secretImports";
import { getFolderByPath } from "./FolderService";
type TSecretImportFid = { environment: string; folderId: string; secretPath: string };
export const getAllImportedSecrets = async (
workspaceId: string,
environment: string,
folderId = "root"
) => {
const secImports = await SecretImport.findOne({
workspace: workspaceId,
environment,
folderId
});
if (!secImports) return [];
if (secImports.imports.length === 0) return [];
const importedEnv: Record<string, boolean> = {}; // to get folders from all environment
secImports.imports.forEach((el) => (importedEnv[el.environment] = true));
const folders = await Folder.find({
workspace: workspaceId,
environment: { $in: Object.keys(importedEnv) }
});
const importedSecByFid: TSecretImportFid[] = [];
secImports.imports.forEach((el) => {
const folder = folders.find((fl) => fl.environment === el.environment);
if (folder) {
const secPathFolder = getFolderByPath(folder.nodes, el.secretPath);
if (secPathFolder)
importedSecByFid.push({
environment: el.environment,
folderId: secPathFolder.id,
secretPath: el.secretPath
});
} else {
if (el.secretPath === "/") {
// this happens when importing with a fresh env without any folders
importedSecByFid.push({ environment: el.environment, folderId: "root", secretPath: "/" });
}
}
});
if (importedSecByFid.length === 0) return [];
const secsGroupedByRef = await Secret.aggregate([
{
$match: {
workspace: new Types.ObjectId(workspaceId),
type: "shared"
}
},
{
$group: {
_id: {
environment: "$environment",
folderId: "$folder"
},
secrets: { $push: "$$ROOT" }
}
},
{
$match: {
$or: importedSecByFid.map(({ environment, folderId: fid }) => ({
"_id.environment": environment,
"_id.folderId": fid
}))
}
}
]);
// now let stitch together secrets.
const importedSecrets: Array<TSecretImportFid & { secrets: ISecret[] }> = [];
importedSecByFid.forEach(({ environment, folderId, secretPath }) => {
const secretsGrouped = secsGroupedByRef.find(
(el) => el._id.environment === environment && el._id.folderId === folderId
);
if (secretsGrouped) {
importedSecrets.push({ secretPath, folderId, environment, secrets: secretsGrouped.secrets });
}
});
return importedSecrets;
};

View File

@@ -2,17 +2,21 @@ import DatabaseService from "./DatabaseService";
// import { logTelemetryMessage, getPostHogClient } from './TelemetryService'; // import { logTelemetryMessage, getPostHogClient } from './TelemetryService';
import TelemetryService from "./TelemetryService"; import TelemetryService from "./TelemetryService";
import BotService from "./BotService"; import BotService from "./BotService";
import BotOrgService from "./BotOrgService";
import EventService from "./EventService"; import EventService from "./EventService";
import IntegrationService from "./IntegrationService"; import IntegrationService from "./IntegrationService";
import TokenService from "./TokenService"; import TokenService from "./TokenService";
import SecretService from "./SecretService"; import SecretService from "./SecretService";
import GithubSecretScanningService from "./GithubSecretScanningService"
export { export {
TelemetryService, TelemetryService,
DatabaseService, DatabaseService,
BotService, BotService,
EventService, BotOrgService,
IntegrationService, EventService,
TokenService, IntegrationService,
SecretService, TokenService,
} SecretService,
GithubSecretScanningService
}

View File

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

View File

@@ -20,6 +20,7 @@ declare global {
workspace: any; workspace: any;
membership: any; membership: any;
targetMembership: any; targetMembership: any;
isUserCompleted: boolean;
providerAuthToken: any; providerAuthToken: any;
organization: any; organization: any;
membershipOrg: any; membershipOrg: any;

View File

@@ -1,11 +1,14 @@
import express from "express"; import express from "express";
import passport from "passport"; import passport from "passport";
import { Types } from "mongoose";
import { AuthData } from "../interfaces/middleware"; import { AuthData } from "../interfaces/middleware";
import { import {
AuthProvider, AuthProvider,
ServiceAccount, ServiceAccount,
ServiceTokenData, ServiceTokenData,
User, User,
Organization,
MembershipOrg
} from "../models"; } from "../models";
import { createToken } from "../helpers/auth"; import { createToken } from "../helpers/auth";
import { import {
@@ -14,11 +17,15 @@ import {
getJwtProviderAuthLifetime, getJwtProviderAuthLifetime,
getJwtProviderAuthSecret, getJwtProviderAuthSecret,
} from "../config"; } from "../config";
import { getSSOConfigHelper } from "../ee/helpers/organizations";
import { OrganizationNotFoundError, InternalServerError } from "./errors";
import { MEMBER, INVITED } from "../variables";
import { getSiteURL } from "../config";
// eslint-disable-next-line @typescript-eslint/no-var-requires // eslint-disable-next-line @typescript-eslint/no-var-requires
const GoogleStrategy = require("passport-google-oauth20").Strategy; const GoogleStrategy = require("passport-google-oauth20").Strategy;
// eslint-disable-next-line @typescript-eslint/no-var-requires
// TODO: find a more optimal folder structure to store these types of functions const { MultiSamlStrategy } = require("@node-saml/passport-saml");
/** /**
* Returns an object containing the id of the authentication data payload * Returns an object containing the id of the authentication data payload
@@ -39,7 +46,6 @@ const getAuthDataPayloadIdObj = (authData: AuthData) => {
} }
}; };
/** /**
* Returns an object containing the user associated with the authentication data payload * Returns an object containing the user associated with the authentication data payload
* @param {AuthData} authData - authentication data object * @param {AuthData} authData - authentication data object
@@ -56,7 +62,7 @@ const getAuthDataPayloadUserObj = (authData: AuthData) => {
} }
if (authData.authPayload instanceof ServiceTokenData) { if (authData.authPayload instanceof ServiceTokenData) {
return { user: authData.authPayload.user }; return { user: authData.authPayload.user };0
} }
} }
@@ -68,47 +74,143 @@ const initializePassport = async () => {
passReqToCallback: true, passReqToCallback: true,
clientID: googleClientId, clientID: googleClientId,
clientSecret: googleClientSecret, clientSecret: googleClientSecret,
callbackURL: "/api/v1/auth/callback/google", callbackURL: "/api/v1/sso/google",
scope: ["profile", " email"], scope: ["profile", " email"],
}, async ( }, async (
req: express.Request, req: express.Request,
accessToken: string, accessToken: string,
refreshToken: string, refreshToken: string,
profile: any, profile: any,
cb: any done: any
) => { ) => {
try { try {
const email = profile.emails[0].value; const email = profile.emails[0].value;
const firstName = profile.name.givenName;
const lastName = profile.name.familyName;
let user = await User.findOne({ let user = await User.findOne({
authProvider: AuthProvider.GOOGLE, email
authId: profile.id, }).select("+publicKey");
}).select("+publicKey")
if (user && user.authProvider !== AuthProvider.GOOGLE) {
done(InternalServerError());
}
if (!user) { if (!user) {
user = await new User({ user = await new User({
email, email,
authProvider: AuthProvider.GOOGLE, authProvider: AuthProvider.GOOGLE,
authId: profile.id, authId: profile.id,
firstName,
lastName
}).save(); }).save();
} }
const isUserCompleted = !!user.publicKey;
const providerAuthToken = createToken({ const providerAuthToken = createToken({
payload: { payload: {
userId: user._id.toString(), userId: user._id.toString(),
email: user.email, email: user.email,
firstName,
lastName,
authProvider: user.authProvider, authProvider: user.authProvider,
isUserCompleted: !!user.publicKey, isUserCompleted
}, },
expiresIn: await getJwtProviderAuthLifetime(), expiresIn: await getJwtProviderAuthLifetime(),
secret: await getJwtProviderAuthSecret(), secret: await getJwtProviderAuthSecret(),
}); });
req.isUserCompleted = isUserCompleted;
req.providerAuthToken = providerAuthToken; req.providerAuthToken = providerAuthToken;
cb(null, profile); done(null, profile);
} catch (err) { } catch (err) {
cb(null, false); done(null, false);
} }
})); }));
passport.use("saml", new MultiSamlStrategy(
{
passReqToCallback: true,
getSamlOptions: async (req: any, done: any) => {
const { ssoIdentifier } = req.params;
const ssoConfig = await getSSOConfigHelper({
ssoConfigId: new Types.ObjectId(ssoIdentifier)
});
const samlConfig = ({
path: "/api/v1/auth/callback/saml",
callbackURL: `${await getSiteURL()}/api/v1/auth/callback/saml`,
entryPoint: ssoConfig.entryPoint,
issuer: ssoConfig.issuer,
cert: ssoConfig.cert,
audience: ssoConfig.audience
});
req.ssoConfig = ssoConfig;
done(null, samlConfig);
},
},
async (req: any, profile: any, done: any) => {
if (!req.ssoConfig.isActive) return done(InternalServerError());
const organization = await Organization.findById(req.ssoConfig.organization);
if (!organization) return done(OrganizationNotFoundError());
const email = profile.email;
const firstName = profile.firstName;
const lastName = profile.lastName;
let user = await User.findOne({
email
}).select("+publicKey");
if (user && user.authProvider !== AuthProvider.OKTA_SAML) {
done(InternalServerError());
}
if (!user) {
user = await new User({
email,
authProvider: AuthProvider.OKTA_SAML,
authId: profile.id,
firstName,
lastName
}).save();
await new MembershipOrg({
inviteEmail: email,
user: user._id,
organization: organization?._id,
role: MEMBER,
status: INVITED
}).save();
}
const isUserCompleted = !!user.publicKey;
const providerAuthToken = createToken({
payload: {
userId: user._id.toString(),
email: user.email,
firstName,
lastName,
organizationName: organization?.name,
authProvider: user.authProvider,
isUserCompleted
},
expiresIn: await getJwtProviderAuthLifetime(),
secret: await getJwtProviderAuthSecret(),
});
req.isUserCompleted = isUserCompleted;
req.providerAuthToken = providerAuthToken;
done(null, profile);
}
));
} }
export { export {

View File

@@ -46,7 +46,7 @@ export const BadRequestError = (error?: Partial<RequestErrorContext>) => new Req
stack: error?.stack, stack: error?.stack,
}); });
export const ResourceNotFound = (error?: Partial<RequestErrorContext>) => new RequestError({ export const ResourceNotFoundError = (error?: Partial<RequestErrorContext>) => new RequestError({
logLevel: error?.logLevel ?? LogLevel.INFO, logLevel: error?.logLevel ?? LogLevel.INFO,
statusCode: error?.statusCode ?? 404, statusCode: error?.statusCode ?? 404,
type: error?.type ?? "resource_not_found", type: error?.type ?? "resource_not_found",

View File

@@ -7,9 +7,11 @@ import { ISecretVersion, SecretSnapshot, SecretVersion } from "../../ee/models";
import { import {
BackupPrivateKey, BackupPrivateKey,
Bot, Bot,
BotOrg,
ISecret, ISecret,
Integration, Integration,
IntegrationAuth, IntegrationAuth,
Organization,
Secret, Secret,
SecretBlindIndexData, SecretBlindIndexData,
ServiceTokenData, ServiceTokenData,
@@ -137,6 +139,103 @@ export const backfillBots = async () => {
await Bot.insertMany(botsToInsert); await Bot.insertMany(botsToInsert);
}; };
/**
* Backfill organization bots to ensure that every organization has a bot
*/
export const backfillBotOrgs = async () => {
const encryptionKey = await getEncryptionKey();
const rootEncryptionKey = await getRootEncryptionKey();
const organizationIdsWithBot = await BotOrg.distinct("organization");
const organizationIdsToAddBot = await Organization.distinct("_id", {
_id: {
$nin: organizationIdsWithBot
}
});
if (organizationIdsToAddBot.length === 0) return;
const botsToInsert = await Promise.all(
organizationIdsToAddBot.map(async (organizationToAddBot) => {
const { publicKey, privateKey } = generateKeyPair();
const key = client.createSymmetricKey();
if (rootEncryptionKey) {
const {
ciphertext: encryptedPrivateKey,
iv: privateKeyIV,
tag: privateKeyTag
} = client.encryptSymmetric(privateKey, rootEncryptionKey);
const {
ciphertext: encryptedSymmetricKey,
iv: symmetricKeyIV,
tag: symmetricKeyTag
} = client.encryptSymmetric(key, rootEncryptionKey);
return new BotOrg({
name: "Infisical Bot",
organization: organizationToAddBot,
isActive: false,
publicKey,
encryptedSymmetricKey,
symmetricKeyIV,
symmetricKeyTag,
symmetricKeyAlgorithm: ALGORITHM_AES_256_GCM,
symmetricKeyKeyEncoding: ENCODING_SCHEME_BASE64,
encryptedPrivateKey,
privateKeyIV,
privateKeyTag,
privateKeyAlgorithm: ALGORITHM_AES_256_GCM,
privateKeyKeyEncoding: ENCODING_SCHEME_BASE64
});
} else if (encryptionKey) {
const {
ciphertext: encryptedPrivateKey,
iv: privateKeyIV,
tag: privateKeyTag
} = encryptSymmetric128BitHexKeyUTF8({
plaintext: privateKey,
key: encryptionKey
});
const {
ciphertext: encryptedSymmetricKey,
iv: symmetricKeyIV,
tag: symmetricKeyTag
} = encryptSymmetric128BitHexKeyUTF8({
plaintext: key,
key: encryptionKey
});
return new BotOrg({
name: "Infisical Bot",
organization: organizationToAddBot,
isActive: false,
publicKey,
encryptedSymmetricKey,
symmetricKeyIV,
symmetricKeyTag,
symmetricKeyAlgorithm: ALGORITHM_AES_256_GCM,
symmetricKeyKeyEncoding: ENCODING_SCHEME_UTF8,
encryptedPrivateKey,
privateKeyIV,
privateKeyTag,
privateKeyAlgorithm: ALGORITHM_AES_256_GCM,
privateKeyKeyEncoding: ENCODING_SCHEME_UTF8
});
}
throw InternalServerError({
message: "Failed to backfill organization bots due to missing encryption key"
});
})
);
await BotOrg.insertMany(botsToInsert);
};
/** /**
* Backfill secret blind index data to ensure that every workspace * Backfill secret blind index data to ensure that every workspace
* has a secret blind index data * has a secret blind index data

View File

@@ -7,6 +7,7 @@ import { createTestUserForDevelopment } from "../addDevelopmentUser";
// eslint-disable-next-line @typescript-eslint/no-var-requires // eslint-disable-next-line @typescript-eslint/no-var-requires
import { validateEncryptionKeysConfig } from "./validateConfig"; import { validateEncryptionKeysConfig } from "./validateConfig";
import { import {
backfillBotOrgs,
backfillBots, backfillBots,
backfillEncryptionMetadata, backfillEncryptionMetadata,
backfillIntegration, backfillIntegration,
@@ -16,7 +17,11 @@ import {
backfillServiceToken, backfillServiceToken,
backfillServiceTokenMultiScope backfillServiceTokenMultiScope
} from "./backfillData"; } from "./backfillData";
import { reencryptBotPrivateKeys, reencryptSecretBlindIndexDataSalts } from "./reencryptData"; import {
reencryptBotOrgKeys,
reencryptBotPrivateKeys,
reencryptSecretBlindIndexDataSalts
} from "./reencryptData";
import { import {
getClientIdGoogle, getClientIdGoogle,
getClientSecretGoogle, getClientSecretGoogle,
@@ -72,6 +77,7 @@ export const setup = async () => {
// backfilling data to catch up with new collections and updated fields // backfilling data to catch up with new collections and updated fields
await backfillSecretVersions(); await backfillSecretVersions();
await backfillBots(); await backfillBots();
await backfillBotOrgs();
await backfillSecretBlindIndexData(); await backfillSecretBlindIndexData();
await backfillEncryptionMetadata(); await backfillEncryptionMetadata();
await backfillSecretFolders(); await backfillSecretFolders();
@@ -82,6 +88,7 @@ export const setup = async () => {
// re-encrypt any data previously encrypted under server hex 128-bit ENCRYPTION_KEY // re-encrypt any data previously encrypted under server hex 128-bit ENCRYPTION_KEY
// to base64 256-bit ROOT_ENCRYPTION_KEY // to base64 256-bit ROOT_ENCRYPTION_KEY
await reencryptBotPrivateKeys(); await reencryptBotPrivateKeys();
await reencryptBotOrgKeys();
await reencryptSecretBlindIndexDataSalts(); await reencryptSecretBlindIndexDataSalts();
// initializing Sentry // initializing Sentry

View File

@@ -1,6 +1,8 @@
import { import {
Bot, Bot,
BotOrg,
IBot, IBot,
IBotOrg,
ISecretBlindIndexData, ISecretBlindIndexData,
SecretBlindIndexData, SecretBlindIndexData,
} from "../../models"; } from "../../models";
@@ -17,7 +19,7 @@ import {
} from "../../variables"; } from "../../variables";
/** /**
* Re-encrypt bot private keys from hex 128-bit ENCRYPTION_KEY * Re-encrypt bot private keys from under hex 128-bit ENCRYPTION_KEY
* to base64 256-bit ROOT_ENCRYPTION_KEY * to base64 256-bit ROOT_ENCRYPTION_KEY
*/ */
export const reencryptBotPrivateKeys = async () => { export const reencryptBotPrivateKeys = async () => {
@@ -70,6 +72,79 @@ export const reencryptBotPrivateKeys = async () => {
} }
} }
/**
* Re-encrypt organization bot keys (symmetric and private) from under hex 128-bit ENCRYPTION_KEY
* to base64 256-bit ROOT_ENCRYPTION_KEY
*/
export const reencryptBotOrgKeys = async () => {
const encryptionKey = await getEncryptionKey();
const rootEncryptionKey = await getRootEncryptionKey();
if (encryptionKey && rootEncryptionKey) {
// 1: re-encrypt organization bot keys under ROOT_ENCRYPTION_KEY
const botOrgs = await BotOrg.find({
symmetricKeyAlgorithm: ALGORITHM_AES_256_GCM,
symmetricKeyKeyEncoding: ENCODING_SCHEME_UTF8,
privateKeyAlgorithm: ALGORITHM_AES_256_GCM,
privateKeyKeyEncoding: ENCODING_SCHEME_UTF8
}).select("+encryptedPrivateKey iv tag algorithm keyEncoding");
if (botOrgs.length === 0) return;
const operationsBotOrg = await Promise.all(
botOrgs.map(async (botOrg: IBotOrg) => {
const privateKey = decryptSymmetric128BitHexKeyUTF8({
ciphertext: botOrg.encryptedPrivateKey,
iv: botOrg.privateKeyIV,
tag: botOrg.privateKeyTag,
key: encryptionKey
});
const {
ciphertext: encryptedPrivateKey,
iv: privateKeyIV,
tag: privateKeyTag,
} = client.encryptSymmetric(privateKey, rootEncryptionKey);
const symmetricKey = decryptSymmetric128BitHexKeyUTF8({
ciphertext: botOrg.encryptedSymmetricKey,
iv: botOrg.symmetricKeyIV,
tag: botOrg.symmetricKeyTag,
key: encryptionKey
});
const {
ciphertext: encryptedSymmetricKey,
iv: symmetricKeyIV,
tag: symmetricKeyTag,
} = client.encryptSymmetric(symmetricKey, rootEncryptionKey);
return ({
updateOne: {
filter: {
_id: botOrg._id,
},
update: {
encryptedSymmetricKey,
symmetricKeyIV,
symmetricKeyTag,
symmetricKeyAlgorithm: ALGORITHM_AES_256_GCM,
symmetricKeyKeyEncoding: ENCODING_SCHEME_BASE64,
encryptedPrivateKey,
privateKeyIV,
privateKeyTag,
privateKeyAlgorithm: ALGORITHM_AES_256_GCM,
privateKeyKeyEncoding: ENCODING_SCHEME_BASE64,
},
},
})
})
);
await BotOrg.bulkWrite(operationsBotOrg);
}
}
/** /**
* Re-encrypt secret blind index data salts from hex 128-bit ENCRYPTION_KEY * Re-encrypt secret blind index data salts from hex 128-bit ENCRYPTION_KEY
* to base64 256-bit ROOT_ENCRYPTION_KEY * to base64 256-bit ROOT_ENCRYPTION_KEY

View File

@@ -28,6 +28,7 @@ export const INTEGRATION_CHECKLY = "checkly";
export const INTEGRATION_HASHICORP_VAULT = "hashicorp-vault"; export const INTEGRATION_HASHICORP_VAULT = "hashicorp-vault";
export const INTEGRATION_CLOUDFLARE_PAGES = "cloudflare-pages"; export const INTEGRATION_CLOUDFLARE_PAGES = "cloudflare-pages";
export const INTEGRATION_BITBUCKET = "bitbucket"; export const INTEGRATION_BITBUCKET = "bitbucket";
export const INTEGRATION_CODEFRESH = "codefresh";
export const INTEGRATION_SET = new Set([ export const INTEGRATION_SET = new Set([
INTEGRATION_AZURE_KEY_VAULT, INTEGRATION_AZURE_KEY_VAULT,
INTEGRATION_HEROKU, INTEGRATION_HEROKU,
@@ -44,7 +45,8 @@ export const INTEGRATION_SET = new Set([
INTEGRATION_CHECKLY, INTEGRATION_CHECKLY,
INTEGRATION_HASHICORP_VAULT, INTEGRATION_HASHICORP_VAULT,
INTEGRATION_CLOUDFLARE_PAGES, INTEGRATION_CLOUDFLARE_PAGES,
INTEGRATION_BITBUCKET INTEGRATION_BITBUCKET,
INTEGRATION_CODEFRESH
]); ]);
// integration types // integration types
@@ -76,6 +78,7 @@ export const INTEGRATION_LARAVELFORGE_API_URL = "https://forge.laravel.com";
export const INTEGRATION_CHECKLY_API_URL = "https://api.checklyhq.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_CLOUDFLARE_PAGES_API_URL = "https://api.cloudflare.com";
export const INTEGRATION_BITBUCKET_API_URL = "https://api.bitbucket.org"; export const INTEGRATION_BITBUCKET_API_URL = "https://api.bitbucket.org";
export const INTEGRATION_CODEFRESH_API_URL = "https://g.codefresh.io/api";
export const getIntegrationOptions = async () => { export const getIntegrationOptions = async () => {
const INTEGRATION_OPTIONS = [ const INTEGRATION_OPTIONS = [
@@ -259,6 +262,15 @@ export const getIntegrationOptions = async () => {
type: "oauth", type: "oauth",
clientId: await getClientIdBitBucket(), clientId: await getClientIdBitBucket(),
docsLink: "" docsLink: ""
},
{
name: "Codefresh",
slug: "codefresh",
image: "Codefresh.png",
isAvailable: true,
type: "pat",
clientId: "",
docsLink: "",
} }
] ]

View File

@@ -235,6 +235,10 @@ func CallGetSecretsV3(httpClient *resty.Client, request GetEncryptedSecretsV3Req
SetQueryParam("environment", request.Environment). SetQueryParam("environment", request.Environment).
SetQueryParam("workspaceId", request.WorkspaceId) SetQueryParam("workspaceId", request.WorkspaceId)
if request.IncludeImport {
httpRequest.SetQueryParam("include_imports", "true")
}
if request.SecretPath != "" { if request.SecretPath != "" {
httpRequest.SetQueryParam("secretPath", request.SecretPath) httpRequest.SetQueryParam("secretPath", request.SecretPath)
} }

View File

@@ -272,40 +272,51 @@ type GetNewAccessTokenWithRefreshTokenResponse struct {
} }
type GetEncryptedSecretsV3Request struct { type GetEncryptedSecretsV3Request struct {
Environment string `json:"environment"` Environment string `json:"environment"`
WorkspaceId string `json:"workspaceId"` WorkspaceId string `json:"workspaceId"`
SecretPath string `json:"secretPath"` SecretPath string `json:"secretPath"`
IncludeImport bool `json:"include_imports"`
}
type EncryptedSecretV3 struct {
ID string `json:"_id"`
Version int `json:"version"`
Workspace string `json:"workspace"`
Type string `json:"type"`
Tags []struct {
ID string `json:"_id"`
Name string `json:"name"`
Slug string `json:"slug"`
Workspace string `json:"workspace"`
} `json:"tags"`
Environment string `json:"environment"`
SecretKeyCiphertext string `json:"secretKeyCiphertext"`
SecretKeyIV string `json:"secretKeyIV"`
SecretKeyTag string `json:"secretKeyTag"`
SecretValueCiphertext string `json:"secretValueCiphertext"`
SecretValueIV string `json:"secretValueIV"`
SecretValueTag string `json:"secretValueTag"`
SecretCommentCiphertext string `json:"secretCommentCiphertext"`
SecretCommentIV string `json:"secretCommentIV"`
SecretCommentTag string `json:"secretCommentTag"`
Algorithm string `json:"algorithm"`
KeyEncoding string `json:"keyEncoding"`
Folder string `json:"folder"`
V int `json:"__v"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
}
type ImportedSecretV3 struct {
Environment string `json:"environment"`
FolderId string `json:"folderId"`
SecretPath string `json:"secretPath"`
Secrets []EncryptedSecretV3 `json:"secrets"`
} }
type GetEncryptedSecretsV3Response struct { type GetEncryptedSecretsV3Response struct {
Secrets []struct { Secrets []EncryptedSecretV3 `json:"secrets"`
ID string `json:"_id"` ImportedSecrets []ImportedSecretV3 `json:"imports,omitempty"`
Version int `json:"version"`
Workspace string `json:"workspace"`
Type string `json:"type"`
Tags []struct {
ID string `json:"_id"`
Name string `json:"name"`
Slug string `json:"slug"`
Workspace string `json:"workspace"`
} `json:"tags"`
Environment string `json:"environment"`
SecretKeyCiphertext string `json:"secretKeyCiphertext"`
SecretKeyIV string `json:"secretKeyIV"`
SecretKeyTag string `json:"secretKeyTag"`
SecretValueCiphertext string `json:"secretValueCiphertext"`
SecretValueIV string `json:"secretValueIV"`
SecretValueTag string `json:"secretValueTag"`
SecretCommentCiphertext string `json:"secretCommentCiphertext"`
SecretCommentIV string `json:"secretCommentIV"`
SecretCommentTag string `json:"secretCommentTag"`
Algorithm string `json:"algorithm"`
KeyEncoding string `json:"keyEncoding"`
Folder string `json:"folder"`
V int `json:"__v"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
} `json:"secrets"`
} }
type CreateSecretV3Request struct { type CreateSecretV3Request struct {

View File

@@ -87,7 +87,12 @@ var runCmd = &cobra.Command{
util.HandleError(err, "Unable to parse flag") util.HandleError(err, "Unable to parse flag")
} }
secrets, err := util.GetAllEnvironmentVariables(models.GetAllSecretsParameters{Environment: environmentName, InfisicalToken: infisicalToken, TagSlugs: tagSlugs, SecretsPath: secretsPath}) includeImports, err := cmd.Flags().GetBool("include-imports")
if err != nil {
util.HandleError(err, "Unable to parse flag")
}
secrets, err := util.GetAllEnvironmentVariables(models.GetAllSecretsParameters{Environment: environmentName, InfisicalToken: infisicalToken, TagSlugs: tagSlugs, SecretsPath: secretsPath, IncludeImport: includeImports})
if err != nil { if err != nil {
util.HandleError(err, "Could not fetch secrets", "If you are using a service token to fetch secrets, please ensure it is valid") util.HandleError(err, "Could not fetch secrets", "If you are using a service token to fetch secrets, please ensure it is valid")
@@ -186,6 +191,7 @@ func init() {
runCmd.Flags().String("token", "", "Fetch secrets using the Infisical Token") runCmd.Flags().String("token", "", "Fetch secrets using the Infisical Token")
runCmd.Flags().StringP("env", "e", "dev", "Set the environment (dev, prod, etc.) from which your secrets should be pulled from") runCmd.Flags().StringP("env", "e", "dev", "Set the environment (dev, prod, etc.) from which your secrets should be pulled from")
runCmd.Flags().Bool("expand", true, "Parse shell parameter expansions in your secrets") runCmd.Flags().Bool("expand", true, "Parse shell parameter expansions in your secrets")
runCmd.Flags().Bool("include-imports", true, "Import linked secrets ")
runCmd.Flags().Bool("secret-overriding", true, "Prioritizes personal secrets, if any, with the same name over shared secrets") runCmd.Flags().Bool("secret-overriding", true, "Prioritizes personal secrets, if any, with the same name over shared secrets")
runCmd.Flags().StringP("command", "c", "", "chained commands to execute (e.g. \"npm install && npm run dev; echo ...\")") runCmd.Flags().StringP("command", "c", "", "chained commands to execute (e.g. \"npm install && npm run dev; echo ...\")")
runCmd.Flags().StringP("tags", "t", "", "filter secrets by tag slugs ") runCmd.Flags().StringP("tags", "t", "", "filter secrets by tag slugs ")

View File

@@ -54,12 +54,17 @@ var secretsCmd = &cobra.Command{
util.HandleError(err) util.HandleError(err)
} }
includeImports, err := cmd.Flags().GetBool("include-imports")
if err != nil {
util.HandleError(err)
}
tagSlugs, err := cmd.Flags().GetString("tags") tagSlugs, err := cmd.Flags().GetString("tags")
if err != nil { if err != nil {
util.HandleError(err, "Unable to parse flag") util.HandleError(err, "Unable to parse flag")
} }
secrets, err := util.GetAllEnvironmentVariables(models.GetAllSecretsParameters{Environment: environmentName, InfisicalToken: infisicalToken, TagSlugs: tagSlugs, SecretsPath: secretsPath}) secrets, err := util.GetAllEnvironmentVariables(models.GetAllSecretsParameters{Environment: environmentName, InfisicalToken: infisicalToken, TagSlugs: tagSlugs, SecretsPath: secretsPath, IncludeImport: includeImports})
if err != nil { if err != nil {
util.HandleError(err) util.HandleError(err)
} }
@@ -647,6 +652,7 @@ func init() {
secretsCmd.Flags().String("token", "", "Fetch secrets using the Infisical Token") secretsCmd.Flags().String("token", "", "Fetch secrets using the Infisical Token")
secretsCmd.PersistentFlags().String("env", "dev", "Used to select the environment name on which actions should be taken on") secretsCmd.PersistentFlags().String("env", "dev", "Used to select the environment name on which actions should be taken on")
secretsCmd.Flags().Bool("expand", true, "Parse shell parameter expansions in your secrets") secretsCmd.Flags().Bool("expand", true, "Parse shell parameter expansions in your secrets")
secretsCmd.Flags().Bool("include-imports", true, "Imported linked secrets ")
secretsCmd.PersistentFlags().StringP("tags", "t", "", "filter secrets by tag slugs") secretsCmd.PersistentFlags().StringP("tags", "t", "", "filter secrets by tag slugs")
secretsCmd.Flags().String("path", "/", "get secrets within a folder path") secretsCmd.Flags().String("path", "/", "get secrets within a folder path")
rootCmd.AddCommand(secretsCmd) rootCmd.AddCommand(secretsCmd)

View File

@@ -65,4 +65,5 @@ type GetAllSecretsParameters struct {
TagSlugs string TagSlugs string
WorkspaceId string WorkspaceId string
SecretsPath string SecretsPath string
IncludeImport bool
} }

View File

@@ -17,7 +17,7 @@ import (
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
) )
func GetPlainTextSecretsViaServiceToken(fullServiceToken string, environment string, secretPath string) ([]models.SingleEnvironmentVariable, api.GetServiceTokenDetailsResponse, error) { func GetPlainTextSecretsViaServiceToken(fullServiceToken string, environment string, secretPath string, includeImports bool) ([]models.SingleEnvironmentVariable, api.GetServiceTokenDetailsResponse, error) {
serviceTokenParts := strings.SplitN(fullServiceToken, ".", 4) serviceTokenParts := strings.SplitN(fullServiceToken, ".", 4)
if len(serviceTokenParts) < 4 { if len(serviceTokenParts) < 4 {
return nil, api.GetServiceTokenDetailsResponse{}, fmt.Errorf("invalid service token entered. Please double check your service token and try again") return nil, api.GetServiceTokenDetailsResponse{}, fmt.Errorf("invalid service token entered. Please double check your service token and try again")
@@ -45,9 +45,10 @@ func GetPlainTextSecretsViaServiceToken(fullServiceToken string, environment str
} }
encryptedSecrets, err := api.CallGetSecretsV3(httpClient, api.GetEncryptedSecretsV3Request{ encryptedSecrets, err := api.CallGetSecretsV3(httpClient, api.GetEncryptedSecretsV3Request{
WorkspaceId: serviceTokenDetails.Workspace, WorkspaceId: serviceTokenDetails.Workspace,
Environment: environment, Environment: environment,
SecretPath: secretPath, SecretPath: secretPath,
IncludeImport: includeImports,
}) })
if err != nil { if err != nil {
@@ -64,15 +65,22 @@ func GetPlainTextSecretsViaServiceToken(fullServiceToken string, environment str
return nil, api.GetServiceTokenDetailsResponse{}, fmt.Errorf("unable to decrypt the required workspace key") return nil, api.GetServiceTokenDetailsResponse{}, fmt.Errorf("unable to decrypt the required workspace key")
} }
plainTextSecrets, err := GetPlainTextSecrets(plainTextWorkspaceKey, encryptedSecrets) plainTextSecrets, err := GetPlainTextSecrets(plainTextWorkspaceKey, encryptedSecrets.Secrets)
if err != nil { if err != nil {
return nil, api.GetServiceTokenDetailsResponse{}, fmt.Errorf("unable to decrypt your secrets [err=%v]", err) return nil, api.GetServiceTokenDetailsResponse{}, fmt.Errorf("unable to decrypt your secrets [err=%v]", err)
} }
if includeImports {
plainTextSecrets, err = InjectImportedSecret(plainTextWorkspaceKey, plainTextSecrets, encryptedSecrets.ImportedSecrets)
if err != nil {
return nil, api.GetServiceTokenDetailsResponse{}, err
}
}
return plainTextSecrets, serviceTokenDetails, nil return plainTextSecrets, serviceTokenDetails, nil
} }
func GetPlainTextSecretsViaJTW(JTWToken string, receiversPrivateKey string, workspaceId string, environmentName string, tagSlugs string, secretsPath string) ([]models.SingleEnvironmentVariable, error) { func GetPlainTextSecretsViaJTW(JTWToken string, receiversPrivateKey string, workspaceId string, environmentName string, tagSlugs string, secretsPath string, includeImports bool) ([]models.SingleEnvironmentVariable, error) {
httpClient := resty.New() httpClient := resty.New()
httpClient.SetAuthToken(JTWToken). httpClient.SetAuthToken(JTWToken).
SetHeader("Accept", "application/json") SetHeader("Accept", "application/json")
@@ -114,8 +122,9 @@ func GetPlainTextSecretsViaJTW(JTWToken string, receiversPrivateKey string, work
plainTextWorkspaceKey := crypto.DecryptAsymmetric(encryptedWorkspaceKey, encryptedWorkspaceKeyNonce, encryptedWorkspaceKeySenderPublicKey, currentUsersPrivateKey) plainTextWorkspaceKey := crypto.DecryptAsymmetric(encryptedWorkspaceKey, encryptedWorkspaceKeyNonce, encryptedWorkspaceKeySenderPublicKey, currentUsersPrivateKey)
getSecretsRequest := api.GetEncryptedSecretsV3Request{ getSecretsRequest := api.GetEncryptedSecretsV3Request{
WorkspaceId: workspaceId, WorkspaceId: workspaceId,
Environment: environmentName, Environment: environmentName,
IncludeImport: includeImports,
// TagSlugs: tagSlugs, // TagSlugs: tagSlugs,
} }
@@ -124,19 +133,53 @@ func GetPlainTextSecretsViaJTW(JTWToken string, receiversPrivateKey string, work
} }
encryptedSecrets, err := api.CallGetSecretsV3(httpClient, getSecretsRequest) encryptedSecrets, err := api.CallGetSecretsV3(httpClient, getSecretsRequest)
if err != nil { if err != nil {
return nil, err return nil, err
} }
plainTextSecrets, err := GetPlainTextSecrets(plainTextWorkspaceKey, encryptedSecrets) plainTextSecrets, err := GetPlainTextSecrets(plainTextWorkspaceKey, encryptedSecrets.Secrets)
if err != nil { if err != nil {
return nil, fmt.Errorf("unable to decrypt your secrets [err=%v]", err) return nil, fmt.Errorf("unable to decrypt your secrets [err=%v]", err)
} }
if includeImports {
plainTextSecrets, err = InjectImportedSecret(plainTextWorkspaceKey, plainTextSecrets, encryptedSecrets.ImportedSecrets)
if err != nil {
return nil, err
}
}
return plainTextSecrets, nil return plainTextSecrets, nil
} }
func InjectImportedSecret(plainTextWorkspaceKey []byte, secrets []models.SingleEnvironmentVariable, importedSecrets []api.ImportedSecretV3) ([]models.SingleEnvironmentVariable, error) {
if importedSecrets == nil {
return secrets, nil
}
hasOverriden := make(map[string]bool)
for _, sec := range secrets {
hasOverriden[sec.Key] = true
}
for i := len(importedSecrets) - 1; i >= 0; i-- {
importSec := importedSecrets[i]
plainTextImportedSecrets, err := GetPlainTextSecrets(plainTextWorkspaceKey, importSec.Secrets)
if err != nil {
return nil, fmt.Errorf("unable to decrypt your imported secrets [err=%v]", err)
}
for _, sec := range plainTextImportedSecrets {
if _, ok := hasOverriden[sec.Key]; !ok {
secrets = append(secrets, sec)
hasOverriden[sec.Key] = true
}
}
}
return secrets, nil
}
func GetAllEnvironmentVariables(params models.GetAllSecretsParameters) ([]models.SingleEnvironmentVariable, error) { func GetAllEnvironmentVariables(params models.GetAllSecretsParameters) ([]models.SingleEnvironmentVariable, error) {
var infisicalToken string var infisicalToken string
if params.InfisicalToken == "" { if params.InfisicalToken == "" {
@@ -179,7 +222,8 @@ func GetAllEnvironmentVariables(params models.GetAllSecretsParameters) ([]models
return nil, fmt.Errorf("unable to validate environment name because [err=%s]", err) return nil, fmt.Errorf("unable to validate environment name because [err=%s]", err)
} }
secretsToReturn, errorToReturn = GetPlainTextSecretsViaJTW(loggedInUserDetails.UserCredentials.JTWToken, loggedInUserDetails.UserCredentials.PrivateKey, workspaceFile.WorkspaceId, params.Environment, params.TagSlugs, params.SecretsPath) secretsToReturn, errorToReturn = GetPlainTextSecretsViaJTW(loggedInUserDetails.UserCredentials.JTWToken, loggedInUserDetails.UserCredentials.PrivateKey, workspaceFile.WorkspaceId,
params.Environment, params.TagSlugs, params.SecretsPath, params.IncludeImport)
log.Debug().Msgf("GetAllEnvironmentVariables: Trying to fetch secrets JTW token [err=%s]", errorToReturn) log.Debug().Msgf("GetAllEnvironmentVariables: Trying to fetch secrets JTW token [err=%s]", errorToReturn)
backupSecretsEncryptionKey := []byte(loggedInUserDetails.UserCredentials.PrivateKey)[0:32] backupSecretsEncryptionKey := []byte(loggedInUserDetails.UserCredentials.PrivateKey)[0:32]
@@ -199,7 +243,7 @@ func GetAllEnvironmentVariables(params models.GetAllSecretsParameters) ([]models
} else { } else {
log.Debug().Msg("Trying to fetch secrets using service token") log.Debug().Msg("Trying to fetch secrets using service token")
secretsToReturn, _, errorToReturn = GetPlainTextSecretsViaServiceToken(infisicalToken, params.Environment, params.SecretsPath) secretsToReturn, _, errorToReturn = GetPlainTextSecretsViaServiceToken(infisicalToken, params.Environment, params.SecretsPath, params.IncludeImport)
} }
return secretsToReturn, errorToReturn return secretsToReturn, errorToReturn
@@ -427,9 +471,9 @@ func OverrideSecrets(secrets []models.SingleEnvironmentVariable, secretType stri
return secretsToReturn return secretsToReturn
} }
func GetPlainTextSecrets(key []byte, encryptedSecrets api.GetEncryptedSecretsV3Response) ([]models.SingleEnvironmentVariable, error) { func GetPlainTextSecrets(key []byte, encryptedSecrets []api.EncryptedSecretV3) ([]models.SingleEnvironmentVariable, error) {
plainTextSecrets := []models.SingleEnvironmentVariable{} plainTextSecrets := []models.SingleEnvironmentVariable{}
for _, secret := range encryptedSecrets.Secrets { for _, secret := range encryptedSecrets {
// Decrypt key // Decrypt key
key_iv, err := base64.StdEncoding.DecodeString(secret.SecretKeyIV) key_iv, err := base64.StdEncoding.DecodeString(secret.SecretKeyIV)
if err != nil { if err != nil {

View File

@@ -58,31 +58,6 @@ services:
networks: networks:
- infisical-dev - infisical-dev
# git-app:
# container_name: infisical-dev-git-app
# restart: unless-stopped
# depends_on:
# - mongo
# - smtp-server
# - backend
# - frontend
# volumes:
# - ./secret-engine/src:/app/src/ # mounted whole src to avoid missing reload on new files
# ports:
# - "3000:3001"
# build:
# context: ./secret-engine
# dockerfile: Dockerfile.dev
# command: npm run start
# env_file: ./secret-engine/.env
# environment:
# - NODE_ENV=development
# - MONGO_URL=mongodb://root:example@mongo:27017/?authSource=admin
# networks:
# - infisical-dev
# extra_hosts:
# - "host.docker.internal:host-gateway"
mongo: mongo:
image: mongo image: mongo
container_name: infisical-dev-mongo container_name: infisical-dev-mongo

View File

@@ -6,7 +6,7 @@ The changelog below reflects new product developments and updates on a monthly b
## July 2023 ## July 2023
- Released [secret referencing](https://infisical.com/docs/documentation/platform/secret-reference) across folders and environments. - Released [secret referencing and importing](https://infisical.com/docs/documentation/platform/secret-reference) across folders and environments.
- Added the [intergation with Laravel Forge](https://infisical.com/docs/integrations/cloud/laravel-forge). - Added the [intergation with Laravel Forge](https://infisical.com/docs/integrations/cloud/laravel-forge).
- Redesigned the project/organization experience. - Redesigned the project/organization experience.

View File

@@ -3,7 +3,8 @@ title: 'Install'
description: "Infisical's CLI is one of the best way to manage environments and secrets. Install it here" description: "Infisical's CLI is one of the best way to manage environments and secrets. Install it here"
--- ---
The Infisical CLI can be used to access secrets across various environments, whether it's local development, CI/CD, staging, or production. The Infisical CLI is powerful command line tool that can be used to retrieve, modify, export and inject secrets into any process or application as environment variables.
You can use it across various environments, whether it's local development, CI/CD, staging, or production.
## Installation ## Installation
@@ -57,7 +58,10 @@ The Infisical CLI can be used to access secrets across various environments, whe
```bash ```bash
apk update && sudo apk add infisical apk update && sudo apk add infisical
``` ```
###
<Tip>
If you are installing the CLI in production environments, we highly recommend to set the version of the CLI to a specific version. This will help keep your CLI version consistent across reinstalls. [View versions](https://cloudsmith.io/~infisical/repos/infisical-cli/packages/)
</Tip>
</Tab> </Tab>
<Tab title="RedHat/CentOs/Amazon"> <Tab title="RedHat/CentOs/Amazon">
Add Infisical repository Add Infisical repository
@@ -71,7 +75,10 @@ The Infisical CLI can be used to access secrets across various environments, whe
```bash ```bash
sudo yum install infisical sudo yum install infisical
``` ```
###
<Tip>
If you are installing the CLI in production environments, we highly recommend to set the version of the CLI to a specific version. This will help keep your CLI version consistent across reinstalls. [View versions](https://cloudsmith.io/~infisical/repos/infisical-cli/packages/)
</Tip>
</Tab> </Tab>
<Tab title="Debian/Ubuntu"> <Tab title="Debian/Ubuntu">
Add Infisical repository Add Infisical repository
@@ -86,7 +93,10 @@ The Infisical CLI can be used to access secrets across various environments, whe
```bash ```bash
sudo apt-get update && sudo apt-get install -y infisical sudo apt-get update && sudo apt-get install -y infisical
``` ```
###
<Tip>
If you are installing the CLI in production environments, we highly recommend to set the version of the CLI to a specific version. This will help keep your CLI version consistent across reinstalls. [View versions](https://cloudsmith.io/~infisical/repos/infisical-cli/packages/)
</Tip>
</Tab> </Tab>
<Tab title="Arch Linux"> <Tab title="Arch Linux">
Use the `yay` package manager to install from the [Arch User Repository](https://aur.archlinux.org/packages/infisical-bin) Use the `yay` package manager to install from the [Arch User Repository](https://aur.archlinux.org/packages/infisical-bin)
@@ -95,6 +105,9 @@ The Infisical CLI can be used to access secrets across various environments, whe
yay -S infisical-bin yay -S infisical-bin
``` ```
###
<Tip>
If you are installing the CLI in production environments, we highly recommend to set the version of the CLI to a specific version. This will help keep your CLI version consistent across reinstalls. [View versions](https://cloudsmith.io/~infisical/repos/infisical-cli/packages/)
</Tip>
</Tab> </Tab>
</Tabs> </Tabs>

View File

@@ -8,7 +8,7 @@ The distinguishing factor, however, is the authentication method used.
<Tabs> <Tabs>
<Tab title="Local development"> <Tab title="Local development">
To use the Infisical CLI in your development environment, simply run the following command and follow the interactive guide. To use the Infisical CLI in your development environment, simply run the command below and follow the interactive guide.
```bash ```bash
infisical login infisical login

View File

@@ -17,7 +17,7 @@ Start syncing environment variables with [Infisical Cloud](https://app.infisical
Store secrets like API keys, database credentials, environment variables with Infisical Store secrets like API keys, database credentials, environment variables with Infisical
</Card> </Card>
## Integrate with Infisical ## Access secrets
<CardGroup cols={2}> <CardGroup cols={2}>
<Card href="../../cli/overview" title="Command Line Interface (CLI)" icon="square-terminal" color="#3775a9"> <Card href="../../cli/overview" title="Command Line Interface (CLI)" icon="square-terminal" color="#3775a9">
@@ -31,11 +31,11 @@ Start syncing environment variables with [Infisical Cloud](https://app.infisical
> >
Fetch secrets with any programming language on demand Fetch secrets with any programming language on demand
</Card> </Card>
<Card href="/documentation/getting-started/docker" title="Docker" icon="docker" color="#0078d3"> <Card href="../../integrations/platforms/docker-intro" title="Docker" icon="docker" color="#0078d3">
Inject secrets into Docker containers Inject secrets into Docker containers
</Card> </Card>
<Card <Card
href="/documentation/getting-started/kubernetes" href="../../integrations/platforms/kubernetes"
title="Kubernetes" title="Kubernetes"
icon="server" icon="server"
color="#3775a9" color="#3775a9"

View File

@@ -1,5 +1,5 @@
--- ---
title: "Reference Secrets" title: "Reference/Import Secrets"
description: "How to use reference secrets in Infisical" description: "How to use reference secrets in Infisical"
--- ---
@@ -29,9 +29,24 @@ Here are a few more examples to help you understand how to reference secrets in
## Fetching fully constructed values ## Fetching fully constructed values
Secret referencing combines multiple secrets into one unified value, reconstructed only on the client side. To retrieve this value, you need access to read the environment and [folder](./folder) from where the secrets originate. Secret referencing combines multiple secrets into one unified value, reconstructed only on the client side. To retrieve this value, you need access to read the environment and [folder](./folder) from where the secrets originate.
For instance, to access a secret 'A' composed of secrets 'B' and 'C' from different environments, you must have read access to both. For instance, to access a secret 'A' composed of secrets 'B' and 'C' from different environments, you must have read access to both 'A' and 'B'
When using [service tokens](./token) to fetch referenced secrets, ensure the service token has read access to all referenced environments and folders. When using [service tokens](./token) to fetch referenced secrets, ensure the service token has read access to all referenced environments and folders.
Without proper permissions, the final secret value may be incomplete. Without proper permissions, the final secret value may be incomplete.
## Import entire folders
While secret referencing effectively minimizes duplication, there might be instances where you need to import or replicate an entire folder's secrets into another. This can be achieved using the 'Import' feature.
This feature allows you to link secrets from one environment/folder into another environment/folder. It proves beneficial when you have common secrets that need to be available across multiple environments/folders.
To add an import, simply click on the `Add import` button and provide the environment and secret path from where the secrets should be imported.
![secret import change order](../../images/secret-import-add.png)
The hierarchy of importing secrets is governed by a "last-one-wins" rule. This means the sequence in which you import matters - the final folder imported will override secrets from any prior folders.
Moreover, any secrets you define directly in your environment will take precedence over secrets from any imported folders.
You can modify this sequence by dragging and rearranging the folders using the `Change Order` drag handle.
![secret import change order](../../images/secret-import-change-order.png)

View File

@@ -12,7 +12,7 @@ This level of control not only ensures maximum flexibility but also significantl
## Creating a service token ## Creating a service token
To generate the the token, head over to your project settings as shown below. On creating a service token you can scope it to a path to limit the access. To generate the token, head over to your project settings as shown below. On creating a service token you can scope it to a path to limit the access.
![token add](../../images/project-token-add.png) ![token add](../../images/project-token-add.png)

Binary file not shown.

After

Width:  |  Height:  |  Size: 275 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 288 KiB

View File

@@ -11,11 +11,11 @@ Prerequisites:
Follow this [guide](./docker) to configure the Infisical CLI for each service that you wish to inject environment variables into; you'll have to update the Dockerfile of each service. Follow this [guide](./docker) to configure the Infisical CLI for each service that you wish to inject environment variables into; you'll have to update the Dockerfile of each service.
## Generate Infisical Tokens ## Generate service token
Generate a unique [Infisical Token](/documentation/platform/token) for each service. Generate a unique [Infisical Token](/documentation/platform/token) for each service.
## Add Infisical Tokens to your Docker Compose file ## Feed service token to your Docker Compose file
For each service you want to inject secrets into, set an environment variable called `INFISICAL_TOKEN` equal to a unique identifier variable. For each service you want to inject secrets into, set an environment variable called `INFISICAL_TOKEN` equal to a unique identifier variable.

View File

@@ -0,0 +1,25 @@
---
title: "Docker"
description: "Learn how to feed secrets from Infisical into your docker application"
---
There are many methods to inject Infisical secrets to docker-based applications.
Regardless of which method you choose, these methods will inject secrets from Infisical as environment variables into your Docker container.
<Card title="Docker Entrypoint" color="#ea5a0c" href="./docker">
Install and run your app start command with Infisical CLI
</Card>
<CardGroup cols={2}>
<Card title="Docker run" color="#0285c7" href="./docker-pass-envs">
Feed secrets via `--env-file` flag in docker run command
</Card>
<Card title="Docker compose" color="#16a34a" href="./docker-compose">
Inject secrets to multiple services using Docker Compose
</Card>
</CardGroup>
<Info>
The main difference between the "Docker Entrypoint" and "Docker run" approach is where the Infisical CLI is installed.
In most production settings, it's typically inconvenient to have the Infisical CLI installed and executed externally.
As a result, we suggest using the "Docker Entrypoint" method for production purposes.
However, if this limitation doesn't apply to you, select the method that best fits your needs.
</Info>

View File

@@ -0,0 +1,48 @@
---
title: "Docker Run"
description: "Pass secrets to your docker container at run time"
---
This method allows you to feed secrets from Infisical into your container using the `--env-file` flag of `docker run` command.
Rather than giving the flag a file path to your env file, you'll use the Infisical CLI to create a virtual file path.
For this method to function as expected, you must have a bash shell (for processing substitution) and the [Infisical CLI](../../cli/overview) installed in the environment where you will be running the `docker run` command.
## 1. Authentication
If you are already logged in via the CLI you can skip this step. Otherwise, head to your project settings in Infisical Cloud to generate an [Infisical Token](/documentation/platform/token). The service token will allow you to authenticate and fetch secrets from Infisical.
Once you have created a service token with the required permissions, you'll need to feed the token to the CLI.
#### Pass as flag
You may use the --token flag to set the token
```bash
infisical export --token=<>
```
#### Pass via shell environment variable
The CLI is configured to look for an environment variable named `INFISICAL_TOKEN`. If set, it'll attempt to use it for authentication.
```bash
export INFISICAL_TOKEN=<>
```
<Warning>
In production scenarios, please to avoid using the `infisical login` command and instead use a [service token](/documentation/platform/token).
</Warning>
## 2. Run your docker command with Infisical
Next, use the --env-file flag of the `docker run` command with Infisical CLI to point to your secrets.
Under the hood, this command will fetch secrets from Infisical and serve them as a file to the `--env-file` flag.
```bash
# In this example, executing a docker run command will initiate an empty Alpine container and display the environment variables passed to it by Infisical.
docker run --rm --env-file <(infisical export --format=dotenv) alpine printenv
```
To view all options of the `export` command, click [here](../../cli/commands/export)
<Warning>
When using the --env-file option, Docker does not have the capability to support secrets that span multiple lines.
</Warning>

View File

@@ -1,11 +1,10 @@
--- ---
title: "Docker" title: "Docker Entrypoint"
description: "How to use Infisical to inject environment variables into a Docker container." description: "How to use Infisical to inject environment variables into a Docker container."
--- ---
Prerequisites: This approach allows you to inject secrets from Infisical directly into your application.
This is achieved by installing the Infisical CLI into your docker image and modifying your start command to execute with Infisical.
- Set up and add envars to [Infisical Cloud](https://app.infisical.com)
## Add the Infisical CLI to your Dockerfile ## Add the Infisical CLI to your Dockerfile
@@ -33,6 +32,10 @@ Prerequisites:
``` ```
</Tab> </Tab>
</Tabs> </Tabs>
####
<Tip>
We recommend you to set the version of the CLI to a specific version. This will help keep your CLI version consistent across reinstalls. [View versions](https://cloudsmith.io/~infisical/repos/infisical-cli/packages/)
</Tip>
## Modify the start command in your Dockerfile ## Modify the start command in your Dockerfile
@@ -48,18 +51,17 @@ CMD ["infisical", "run", "--", "npm", "run", "start"]
CMD ["infisical", "run", "--command", "npm run start && ..."] CMD ["infisical", "run", "--command", "npm run start && ..."]
``` ```
## Generate an Infisical Token ## Generate an service token
Head to your project settings in Infisical Cloud to generate an [Infisical Token](/documentation/platform/token). Head to your project settings in the Infisical dashboard to generate an [service token](/documentation/platform/token).
This service token will allow you to authenticate and fetch secrets from Infisical.
Once you have created a service token with the required permissions, you’ll need to feed the token to the CLI installed in your docker container.
## Feed Docker your Infisical Token ## Feed service token to docker container
The last step is to give the Infisical CLI installed in your Docker container access to the service token. This will allow the CLI to fetch and inject the secrets into your application.
To feed the service token to the container, use the INFISICAL_TOKEN environment variable as shown below.
```bash ```bash
docker run --env INFISICAL_TOKEN=[token] [DOCKER-IMAGE]... docker run --env INFISICAL_TOKEN=[token] [DOCKER-IMAGE]...
``` ```
<Info>
The Infisical CLI uses the detected `INFISICAL_TOKEN` environment variable to authenticate, retrieve, and inject the environment variables which the token is authorized for.
</Info>

View File

@@ -91,8 +91,8 @@
"documentation/getting-started/introduction", "documentation/getting-started/introduction",
"documentation/getting-started/platform", "documentation/getting-started/platform",
"documentation/getting-started/sdks", "documentation/getting-started/sdks",
"documentation/getting-started/docker", "integrations/platforms/kubernetes",
"documentation/getting-started/kubernetes", "integrations/platforms/docker-intro",
"documentation/getting-started/api" "documentation/getting-started/api"
] ]
}, },
@@ -187,7 +187,9 @@
{ {
"group": "Docker", "group": "Docker",
"pages": [ "pages": [
"integrations/platforms/docker-intro",
"integrations/platforms/docker", "integrations/platforms/docker",
"integrations/platforms/docker-pass-envs",
"integrations/platforms/docker-compose" "integrations/platforms/docker-compose"
] ]
}, },

View File

@@ -8,7 +8,7 @@ Self-hosted Infisical allows you to maintain your sensitive information within y
Choose from a variety of deployment options listed below to get started. Choose from a variety of deployment options listed below to get started.
<Card <Card
title="Kubernetes" title="Kubernetes (recommended)"
color="#ea5a0c" color="#ea5a0c"
href="deployment-options/kubernetes-helm" href="deployment-options/kubernetes-helm"
> >

View File

@@ -5,6 +5,9 @@
"packages": { "packages": {
"": { "": {
"dependencies": { "dependencies": {
"@dnd-kit/core": "^6.0.8",
"@dnd-kit/modifiers": "^6.0.1",
"@dnd-kit/sortable": "^7.0.2",
"@emotion/css": "^11.10.0", "@emotion/css": "^11.10.0",
"@emotion/server": "^11.10.0", "@emotion/server": "^11.10.0",
"@fontsource/inter": "^4.5.15", "@fontsource/inter": "^4.5.15",
@@ -2468,6 +2471,68 @@
"node": ">=10.0.0" "node": ">=10.0.0"
} }
}, },
"node_modules/@dnd-kit/accessibility": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/@dnd-kit/accessibility/-/accessibility-3.0.1.tgz",
"integrity": "sha512-HXRrwS9YUYQO9lFRc/49uO/VICbM+O+ZRpFDe9Pd1rwVv2PCNkRiTZRdxrDgng/UkvdC3Re9r2vwPpXXrWeFzg==",
"dependencies": {
"tslib": "^2.0.0"
},
"peerDependencies": {
"react": ">=16.8.0"
}
},
"node_modules/@dnd-kit/core": {
"version": "6.0.8",
"resolved": "https://registry.npmjs.org/@dnd-kit/core/-/core-6.0.8.tgz",
"integrity": "sha512-lYaoP8yHTQSLlZe6Rr9qogouGUz9oRUj4AHhDQGQzq/hqaJRpFo65X+JKsdHf8oUFBzx5A+SJPUvxAwTF2OabA==",
"dependencies": {
"@dnd-kit/accessibility": "^3.0.0",
"@dnd-kit/utilities": "^3.2.1",
"tslib": "^2.0.0"
},
"peerDependencies": {
"react": ">=16.8.0",
"react-dom": ">=16.8.0"
}
},
"node_modules/@dnd-kit/modifiers": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/@dnd-kit/modifiers/-/modifiers-6.0.1.tgz",
"integrity": "sha512-rbxcsg3HhzlcMHVHWDuh9LCjpOVAgqbV78wLGI8tziXY3+qcMQ61qVXIvNKQFuhj75dSfD+o+PYZQ/NUk2A23A==",
"dependencies": {
"@dnd-kit/utilities": "^3.2.1",
"tslib": "^2.0.0"
},
"peerDependencies": {
"@dnd-kit/core": "^6.0.6",
"react": ">=16.8.0"
}
},
"node_modules/@dnd-kit/sortable": {
"version": "7.0.2",
"resolved": "https://registry.npmjs.org/@dnd-kit/sortable/-/sortable-7.0.2.tgz",
"integrity": "sha512-wDkBHHf9iCi1veM834Gbk1429bd4lHX4RpAwT0y2cHLf246GAvU2sVw/oxWNpPKQNQRQaeGXhAVgrOl1IT+iyA==",
"dependencies": {
"@dnd-kit/utilities": "^3.2.0",
"tslib": "^2.0.0"
},
"peerDependencies": {
"@dnd-kit/core": "^6.0.7",
"react": ">=16.8.0"
}
},
"node_modules/@dnd-kit/utilities": {
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/@dnd-kit/utilities/-/utilities-3.2.1.tgz",
"integrity": "sha512-OOXqISfvBw/1REtkSK2N3Fi2EQiLMlWUlqnOK/UpOISqBZPWpE6TqL+jcPtMOkE8TqYGiURvRdPSI9hltNUjEA==",
"dependencies": {
"tslib": "^2.0.0"
},
"peerDependencies": {
"react": ">=16.8.0"
}
},
"node_modules/@emotion/babel-plugin": { "node_modules/@emotion/babel-plugin": {
"version": "11.11.0", "version": "11.11.0",
"resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.11.0.tgz", "resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.11.0.tgz",
@@ -24491,6 +24556,50 @@
"integrity": "sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw==", "integrity": "sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw==",
"dev": true "dev": true
}, },
"@dnd-kit/accessibility": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/@dnd-kit/accessibility/-/accessibility-3.0.1.tgz",
"integrity": "sha512-HXRrwS9YUYQO9lFRc/49uO/VICbM+O+ZRpFDe9Pd1rwVv2PCNkRiTZRdxrDgng/UkvdC3Re9r2vwPpXXrWeFzg==",
"requires": {
"tslib": "^2.0.0"
}
},
"@dnd-kit/core": {
"version": "6.0.8",
"resolved": "https://registry.npmjs.org/@dnd-kit/core/-/core-6.0.8.tgz",
"integrity": "sha512-lYaoP8yHTQSLlZe6Rr9qogouGUz9oRUj4AHhDQGQzq/hqaJRpFo65X+JKsdHf8oUFBzx5A+SJPUvxAwTF2OabA==",
"requires": {
"@dnd-kit/accessibility": "^3.0.0",
"@dnd-kit/utilities": "^3.2.1",
"tslib": "^2.0.0"
}
},
"@dnd-kit/modifiers": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/@dnd-kit/modifiers/-/modifiers-6.0.1.tgz",
"integrity": "sha512-rbxcsg3HhzlcMHVHWDuh9LCjpOVAgqbV78wLGI8tziXY3+qcMQ61qVXIvNKQFuhj75dSfD+o+PYZQ/NUk2A23A==",
"requires": {
"@dnd-kit/utilities": "^3.2.1",
"tslib": "^2.0.0"
}
},
"@dnd-kit/sortable": {
"version": "7.0.2",
"resolved": "https://registry.npmjs.org/@dnd-kit/sortable/-/sortable-7.0.2.tgz",
"integrity": "sha512-wDkBHHf9iCi1veM834Gbk1429bd4lHX4RpAwT0y2cHLf246GAvU2sVw/oxWNpPKQNQRQaeGXhAVgrOl1IT+iyA==",
"requires": {
"@dnd-kit/utilities": "^3.2.0",
"tslib": "^2.0.0"
}
},
"@dnd-kit/utilities": {
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/@dnd-kit/utilities/-/utilities-3.2.1.tgz",
"integrity": "sha512-OOXqISfvBw/1REtkSK2N3Fi2EQiLMlWUlqnOK/UpOISqBZPWpE6TqL+jcPtMOkE8TqYGiURvRdPSI9hltNUjEA==",
"requires": {
"tslib": "^2.0.0"
}
},
"@emotion/babel-plugin": { "@emotion/babel-plugin": {
"version": "11.11.0", "version": "11.11.0",
"resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.11.0.tgz", "resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.11.0.tgz",

View File

@@ -13,6 +13,9 @@
"build-storybook": "storybook build" "build-storybook": "storybook build"
}, },
"dependencies": { "dependencies": {
"@dnd-kit/core": "^6.0.8",
"@dnd-kit/modifiers": "^6.0.1",
"@dnd-kit/sortable": "^7.0.2",
"@emotion/css": "^11.10.0", "@emotion/css": "^11.10.0",
"@emotion/server": "^11.10.0", "@emotion/server": "^11.10.0",
"@fontsource/inter": "^4.5.15", "@fontsource/inter": "^4.5.15",

View File

@@ -21,6 +21,7 @@ const integrationSlugNameMapping: Mapping = {
checkly: "Checkly", checkly: "Checkly",
"hashicorp-vault": "Vault", "hashicorp-vault": "Vault",
"cloudflare-pages": "Cloudflare Pages", "cloudflare-pages": "Cloudflare Pages",
"codefresh": "Codefresh",
bitbucket: "BitBucket" bitbucket: "BitBucket"
}; };

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

File diff suppressed because one or more lines are too long

View File

@@ -1,188 +0,0 @@
import { FormEvent, useState } from "react";
import { useTranslation } from "react-i18next";
import Link from "next/link";
import { useRouter } from "next/router";
import axios from "axios"
import attemptLogin from "@app/components/utilities/attemptLogin";
import getOrganizations from "@app/pages/api/organization/getOrgs";
import Error from "../basic/Error";
import attemptCliLogin from "../utilities/attemptCliLogin";
// import { faGoogle } from '@fortawesome/free-brands-svg-icons';
// import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { Button, Input } from "../v2";
export default function InitialLoginStep({
setStep,
email,
setEmail,
password,
setPassword,
}: {
setStep: (step: number) => void;
email: string;
setEmail: (email: string) => void;
password: string;
setPassword: (password: string) => void;
}) {
const router = useRouter();
const { t } = useTranslation();
const [isLoading, setIsLoading] = useState(false);
const [loginError, setLoginError] = useState(false);
const handleLogin = async (e: FormEvent<HTMLFormElement>) => {
e.preventDefault()
try {
if (!email || !password) {
return;
}
setIsLoading(true);
const queryParams = new URLSearchParams(window.location.search)
if (queryParams && queryParams.get("callback_port")) {
const callbackPort = queryParams.get("callback_port")
// attemptCliLogin
const isCliLoginSuccessful = await attemptCliLogin({
email,
password,
})
if (isCliLoginSuccessful && isCliLoginSuccessful.success) {
if (isCliLoginSuccessful.mfaEnabled) {
// case: login requires MFA step
setStep(2);
setIsLoading(false);
return;
}
// case: login was successful
const cliUrl = `http://localhost:${callbackPort}`
// send request to server endpoint
const instance = axios.create()
const cliResp = await instance.post(cliUrl, { ...isCliLoginSuccessful.loginResponse })
console.log(cliResp)
// cli page
router.push("/cli-redirect");
// on success, router.push to cli Login Successful page
}
} else {
const isLoginSuccessful = await attemptLogin({
email,
password,
});
if (isLoginSuccessful && isLoginSuccessful.success) {
// case: login was successful
if (isLoginSuccessful.mfaEnabled) {
// case: login requires MFA step
setStep(2);
setIsLoading(false);
return;
}
const userOrgs = await getOrganizations();
const userOrg = userOrgs[0] && userOrgs[0]._id;
// case: login does not require MFA step
router.push(`/org/${userOrg}/overview`);
}
}
} catch (err) {
setLoginError(true);
}
setIsLoading(false);
}
return <form onSubmit={handleLogin} className='flex flex-col mx-auto w-full justify-center items-center'>
<h1 className='text-xl font-medium text-transparent bg-clip-text bg-gradient-to-b from-white to-bunker-200 text-center mb-8' >Login to Infisical</h1>
{/* <div className='lg:w-1/6 w-1/4 min-w-[20rem] rounded-md'>
<Button
colorSchema="primary"
variant="solid"
onClick={() => {
window.open('/api/v1/auth/redirect/google')
}}
leftIcon={<FontAwesomeIcon icon={faGoogle} className="mr-1" />}
className="h-14 w-full mx-0"
>
{t('login.continue-with-google')}
</Button>
</div> */}
<div className="relative md:px-1.5 flex items-center justify-center lg:w-1/6 w-1/4 min-w-[21.3rem] md:min-w-[22rem] mx-auto rounded-lg max-h-24 md:max-h-28">
<div className="flex items-center justify-center w-full md:px-2 md:py-1 rounded-lg max-h-24 md:max-h-28">
<Input
value={email}
onChange={(e) => setEmail(e.target.value)}
type="email"
placeholder="Enter your email..."
isRequired
autoComplete="username"
className="h-12"
/>
</div>
</div>
<div className="relative pt-2 md:pt-0 md:px-1.5 flex items-center justify-center w-1/4 lg:w-1/6 min-w-[21.3rem] md:min-w-[22rem] mx-auto rounded-lg max-h-24 md:max-h-28">
<div className="flex items-center justify-center w-full md:p-2 rounded-lg max-h-24 md:max-h-28">
<Input
value={password}
onChange={(e) => setPassword(e.target.value)}
type="password"
placeholder="Enter your password..."
isRequired
autoComplete="current-password"
id="current-password"
className="h-12 select:-webkit-autofill:focus"
/>
</div>
</div>
{!isLoading && loginError && <Error text={t("login.error-login") ?? ""} />}
<div className='lg:w-1/6 w-1/4 min-w-[21.2rem] md:min-w-[20.1rem] text-center rounded-md mt-4'>
<Button
type="submit"
size="sm"
isFullWidth
className='h-12'
colorSchema="primary"
variant="solid"
isLoading={isLoading}
> Login </Button>
</div>
<div className='lg:w-1/6 w-1/4 min-w-[20rem] flex flex-row items-center mt-4 py-2'>
<div className='w-1/2 border-t border-mineshaft-500' />
<span className='px-4 text-sm text-bunker-400'>or</span>
<div className='w-1/2 border-t border-mineshaft-500' />
</div>
<div className='lg:w-1/6 w-1/4 min-w-[20rem] text-center rounded-md mt-4'>
<Button
colorSchema="primary"
variant="outline_bg"
onClick={() => router.push("/saml-sso")}
isFullWidth
className="h-14 w-full mx-0"
>
Continue with SAML SSO
</Button>
</div>
<div className="mt-6 text-bunker-400 text-sm flex flex-row">
<span className="mr-1">Don&apos;t have an acount yet?</span>
<Link href="/signup">
<span className='hover:underline hover:underline-offset-4 hover:decoration-primary-700 hover:text-bunker-200 duration-200 cursor-pointer'>{t("login.create-account")}</span>
</Link>
</div>
<div className="text-bunker-400 text-sm flex flex-row">
<span className="mr-1">Forgot password?</span>
<Link href="/verify-email">
<span className='hover:underline hover:underline-offset-4 hover:decoration-primary-700 hover:text-bunker-200 duration-200 cursor-pointer'>Recover your account</span>
</Link>
</div>
</form>
}

View File

@@ -1,122 +0,0 @@
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import Link from "next/link";
import { useRouter } from "next/router";
import Error from "@app/components/basic/Error";
import attemptLogin from "@app/components/utilities/attemptLogin";
import getOrganizations from "@app/pages/api/organization/getOrgs";
import SecurityClient from "../utilities/SecurityClient";
import { Button, Input } from "../v2";
export default function PasswordInputStep({
email,
password,
providerAuthToken,
setPassword,
setProviderAuthToken,
setStep
}: {
email: string;
password: string;
providerAuthToken: string;
setPassword: (password: string) => void;
setProviderAuthToken: (value: string) => void;
setStep: (step: number) => void;
}) {
const router = useRouter();
const [isLoading, setIsLoading] = useState(false);
const [loginError, setLoginError] = useState(false);
const { t } = useTranslation();
const handleLogin = async () => {
try {
setIsLoading(true);
const loginAttempt = await attemptLogin({
email,
password,
providerAuthToken,
});
if (loginAttempt && loginAttempt.success) {
// case: login was successful
if (loginAttempt.mfaEnabled) {
// case: login requires MFA step
setStep(2);
setIsLoading(false);
return;
}
// case: login does not require MFA step
const userOrgs = await getOrganizations();
const userOrg = userOrgs[0]._id;
router.push(`/org/${userOrg?._id}/overview`);
}
} catch (err) {
setLoginError(true);
}
setIsLoading(false);
};
return (
<form onSubmit={(e) => e.preventDefault()}>
<div className="h-full mx-auto w-full max-w-md px-6 pt-8">
<p className="mx-auto mb-6 flex w-max justify-center text-xl font-medium text-transparent bg-clip-text bg-gradient-to-b from-white to-bunker-200 text-center mb-8">
What’s your Infisical Password?
</p>
<div className="relative flex items-center justify-center lg:w-1/6 w-1/4 min-w-[22rem] mx-auto w-full rounded-lg max-h-24 md:max-h-28">
<div className="flex items-center justify-center w-full rounded-lg max-h-24 md:max-h-28">
<Input
value={password}
onChange={(e) => setPassword(e.target.value)}
type="password"
placeholder="Enter your password..."
isRequired
autoComplete="current-password"
id="current-password"
className="h-12"
/>
</div>
</div>
{!isLoading && loginError && <Error text={t("login.error-login") ?? ""} />}
<div className='lg:w-1/6 w-1/4 w-full mx-auto flex items-center justify-center min-w-[22rem] text-center rounded-md mt-4'>
<Button
colorSchema="primary"
variant="outline_bg"
onClick={async () => handleLogin()}
isFullWidth
isLoading={isLoading}
className="h-14"
>
{t("login.login")}
</Button>
</div>
<div className="text-bunker-400 text-xs flex flex-col items-center w-max mx-auto mt-4">
<span className='duration-200 max-w-sm text-center px-4'>
Infisical Master Password serves as a decryption mechanism so that even Google is not able to access your secrets.
</span>
<Link href="/verify-email">
<span className='hover:underline mt-2 hover:underline-offset-4 hover:decoration-primary-700 hover:text-bunker-200 duration-200 cursor-pointer'>{t("login.forgot-password")}</span>
</Link>
</div>
<div className="flex flex-row items-center justify-center">
<button
onClick={(e) => {
e.preventDefault();
SecurityClient.setProviderAuthToken("");
setProviderAuthToken("");
}}
type="button"
className="text-bunker-400 text-xs hover:underline mt-2 hover:underline-offset-4 hover:decoration-primary-700 hover:text-bunker-200 duration-200 cursor-pointer"
>
{t("login.other-option")}
</button>
</div>
</div>
</form>
);
}

View File

@@ -1,9 +1,9 @@
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import Link from "next/link"; import Link from "next/link";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import { faGoogle } from "@fortawesome/free-brands-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
// import { faGoogle } from '@fortawesome/free-brands-svg-icons';
// import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { Button } from "../v2"; import { Button } from "../v2";
export default function InitialSignupStep({ export default function InitialSignupStep({
@@ -16,19 +16,6 @@ export default function InitialSignupStep({
return <div className='flex flex-col mx-auto w-full justify-center items-center'> return <div className='flex flex-col mx-auto w-full justify-center items-center'>
<h1 className='text-xl font-medium text-transparent bg-clip-text bg-gradient-to-b from-white to-bunker-200 text-center mb-8' >{t("signup.initial-title")}</h1> <h1 className='text-xl font-medium text-transparent bg-clip-text bg-gradient-to-b from-white to-bunker-200 text-center mb-8' >{t("signup.initial-title")}</h1>
{/* <div className='lg:w-1/6 w-1/4 min-w-[20rem] rounded-md'>
<Button
colorSchema="primary"
variant="solid"
onClick={() => {
window.open('/api/v1/auth/redirect/google')
}}
leftIcon={<FontAwesomeIcon icon={faGoogle} className="mr-1" />}
className="h-14 w-full mx-0"
>
{t('signup.continue-with-google')}
</Button>
</div> */}
<div className='lg:w-1/6 w-1/4 min-w-[20rem] text-center rounded-md'> <div className='lg:w-1/6 w-1/4 min-w-[20rem] text-center rounded-md'>
<Button <Button
colorSchema="primary" colorSchema="primary"
@@ -42,6 +29,25 @@ export default function InitialSignupStep({
Sign Up with email Sign Up with email
</Button> </Button>
</div> </div>
<div className='lg:w-1/6 w-1/4 min-w-[20rem] flex flex-row items-center my-4 py-2'>
<div className='w-1/2 border-t border-mineshaft-500' />
<span className='px-4 text-sm text-bunker-400'>or</span>
<div className='w-1/2 border-t border-mineshaft-500' />
</div>
<div className='lg:w-1/6 w-1/4 min-w-[20rem] rounded-md'>
<Button
colorSchema="primary"
variant="solid"
onClick={() => {
window.open("/api/v1/sso/redirect/google");
window.close();
}}
leftIcon={<FontAwesomeIcon icon={faGoogle} className="mr-1" />}
className="h-14 w-full mx-0"
>
{t("signup.continue-with-google")}
</Button>
</div>
<div className='lg:w-1/6 w-1/4 min-w-[20rem] text-center rounded-md mt-4'> <div className='lg:w-1/6 w-1/4 min-w-[20rem] text-center rounded-md mt-4'>
<Button <Button
colorSchema="primary" colorSchema="primary"

View File

@@ -25,7 +25,7 @@ export const EmptyState = ({
className className
)} )}
> >
<FontAwesomeIcon icon={icon} size={iconSize} className="mr-4" /> <FontAwesomeIcon icon={icon} size={iconSize} />
<div className="flex flex-row items-center py-4"> <div className="flex flex-row items-center py-4">
<div className="text-sm text-bunker-300">{title}</div> <div className="text-sm text-bunker-300">{title}</div>
<div>{children}</div> <div>{children}</div>

View File

@@ -4,6 +4,7 @@ export const publicPaths = [
"/signupinvite", "/signupinvite",
"/pricing", "/pricing",
"/signup", "/signup",
"/signup/sso",
"/login", "/login",
"/blog", "/blog",
"/docs", "/docs",
@@ -18,8 +19,9 @@ export const publicPaths = [
"/verify-email", "/verify-email",
"/password-reset", "/password-reset",
"/saml-sso", "/saml-sso",
"/login/provider/success", "/login/provider/success", // TODO: change
"/login/provider/error" "/login/provider/error", // TODO: change
"/login/sso"
]; ];
export const languageMap = { export const languageMap = {

View File

@@ -6,10 +6,12 @@ export * from "./integrations";
export * from "./keys"; export * from "./keys";
export * from "./organization"; export * from "./organization";
export * from "./secretFolders"; export * from "./secretFolders";
export * from "./secretImports";
export * from "./secrets"; export * from "./secrets";
export * from "./secretSnapshots"; export * from "./secretSnapshots";
export * from "./serviceAccounts"; export * from "./serviceAccounts";
export * from "./serviceTokens"; export * from "./serviceTokens";
export * from "./ssoConfig";
export * from "./subscriptions"; export * from "./subscriptions";
export * from "./tags"; export * from "./tags";
export * from "./users"; export * from "./users";

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