chore: resolve merge conflicts
@@ -108,6 +108,22 @@ brews:
|
|||||||
zsh_completion.install "completions/infisical.zsh" => "_infisical"
|
zsh_completion.install "completions/infisical.zsh" => "_infisical"
|
||||||
fish_completion.install "completions/infisical.fish"
|
fish_completion.install "completions/infisical.fish"
|
||||||
man1.install "manpages/infisical.1.gz"
|
man1.install "manpages/infisical.1.gz"
|
||||||
|
- name: 'infisical@{{.Version}}'
|
||||||
|
tap:
|
||||||
|
owner: Infisical
|
||||||
|
name: homebrew-get-cli
|
||||||
|
commit_author:
|
||||||
|
name: "Infisical"
|
||||||
|
email: [email protected]
|
||||||
|
folder: Formula
|
||||||
|
homepage: "https://infisical.com"
|
||||||
|
description: "The official Infisical CLI"
|
||||||
|
install: |-
|
||||||
|
bin.install "infisical"
|
||||||
|
bash_completion.install "completions/infisical.bash" => "infisical"
|
||||||
|
zsh_completion.install "completions/infisical.zsh" => "_infisical"
|
||||||
|
fish_completion.install "completions/infisical.fish"
|
||||||
|
man1.install "manpages/infisical.1.gz"
|
||||||
|
|
||||||
nfpms:
|
nfpms:
|
||||||
- id: infisical
|
- id: infisical
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { Types } from "mongoose";
|
import { Types } from "mongoose";
|
||||||
import { Request, Response } from "express";
|
import { Request, Response } from "express";
|
||||||
import { MembershipOrg, Organization, User } from "../../models";
|
import { MembershipOrg, Organization, User } from "../../models";
|
||||||
|
import { SSOConfig } from "../../ee/models";
|
||||||
import { deleteMembershipOrg as deleteMemberFromOrg } from "../../helpers/membershipOrg";
|
import { deleteMembershipOrg as deleteMemberFromOrg } from "../../helpers/membershipOrg";
|
||||||
import { createToken } from "../../helpers/auth";
|
import { createToken } from "../../helpers/auth";
|
||||||
import { updateSubscriptionOrgQuantity } from "../../helpers/organization";
|
import { updateSubscriptionOrgQuantity } from "../../helpers/organization";
|
||||||
@@ -111,6 +112,18 @@ export const inviteUserToOrganization = async (req: Request, res: Response) => {
|
|||||||
|
|
||||||
const plan = await EELicenseService.getPlan(organizationId);
|
const plan = await EELicenseService.getPlan(organizationId);
|
||||||
|
|
||||||
|
const ssoConfig = await SSOConfig.findOne({
|
||||||
|
organization: new Types.ObjectId(organizationId)
|
||||||
|
});
|
||||||
|
|
||||||
|
if (ssoConfig && ssoConfig.isActive) {
|
||||||
|
// case: SAML SSO is enabled for the organization
|
||||||
|
return res.status(400).send({
|
||||||
|
message:
|
||||||
|
"Failed to invite member due to SAML SSO configured for organization"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (plan.memberLimit !== null) {
|
if (plan.memberLimit !== null) {
|
||||||
// case: limit imposed on number of members allowed
|
// case: limit imposed on number of members allowed
|
||||||
|
|
||||||
|
|||||||
@@ -30,7 +30,6 @@ export const createSecretImport = async (req: Request, res: Response) => {
|
|||||||
if (doesImportExist) {
|
if (doesImportExist) {
|
||||||
throw BadRequestError({ message: "Secret import already exist" });
|
throw BadRequestError({ message: "Secret import already exist" });
|
||||||
}
|
}
|
||||||
|
|
||||||
importSecDoc.imports.push({
|
importSecDoc.imports.push({
|
||||||
environment: secretImport.environment,
|
environment: secretImport.environment,
|
||||||
secretPath: secretImport.secretPath
|
secretPath: secretImport.secretPath
|
||||||
|
|||||||
@@ -830,7 +830,7 @@ export const getSecrets = async (req: Request, res: Response) => {
|
|||||||
|
|
||||||
// TODO(akhilmhdh) - secret-imp change this to org type
|
// TODO(akhilmhdh) - secret-imp change this to org type
|
||||||
let importedSecrets: any[] = [];
|
let importedSecrets: any[] = [];
|
||||||
if (include_imports) {
|
if (include_imports === "true") {
|
||||||
importedSecrets = await getAllImportedSecrets(workspaceId, environment, folderId as string);
|
importedSecrets = await getAllImportedSecrets(workspaceId, environment, folderId as string);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import crypto from "crypto";
|
|||||||
import bcrypt from "bcrypt";
|
import bcrypt from "bcrypt";
|
||||||
import {
|
import {
|
||||||
APIKeyData,
|
APIKeyData,
|
||||||
|
AuthProvider,
|
||||||
MembershipOrg,
|
MembershipOrg,
|
||||||
TokenVersion,
|
TokenVersion,
|
||||||
User
|
User
|
||||||
@@ -122,6 +123,10 @@ export const updateAuthProvider = async (req: Request, res: Response) => {
|
|||||||
authProvider
|
authProvider
|
||||||
} = req.body;
|
} = req.body;
|
||||||
|
|
||||||
|
if (req.user?.authProvider === AuthProvider.OKTA_SAML) return res.status(400).send({
|
||||||
|
message: "Failed to update user authentication method because SAML SSO is enforced"
|
||||||
|
});
|
||||||
|
|
||||||
const user = await User.findByIdAndUpdate(
|
const user = await User.findByIdAndUpdate(
|
||||||
req.user._id.toString(),
|
req.user._id.toString(),
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -3,12 +3,15 @@ import { Types } from "mongoose";
|
|||||||
import { EventService, SecretService } from "../../services";
|
import { EventService, SecretService } from "../../services";
|
||||||
import { eventPushSecrets } from "../../events";
|
import { eventPushSecrets } from "../../events";
|
||||||
import { BotService } from "../../services";
|
import { BotService } from "../../services";
|
||||||
import { repackageSecretToRaw } from "../../helpers/secrets";
|
import { containsGlobPatterns, repackageSecretToRaw } from "../../helpers/secrets";
|
||||||
import { encryptSymmetric128BitHexKeyUTF8 } from "../../utils/crypto";
|
import { encryptSymmetric128BitHexKeyUTF8 } from "../../utils/crypto";
|
||||||
import { getAllImportedSecrets } from "../../services/SecretImportService";
|
import { getAllImportedSecrets } from "../../services/SecretImportService";
|
||||||
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";
|
||||||
|
import { IServiceTokenData } from "../../models";
|
||||||
|
import { requireWorkspaceAuth } from "../../middleware";
|
||||||
|
import { ADMIN, MEMBER, PERMISSION_READ_SECRETS } from "../../variables";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return secrets for workspace with id [workspaceId] and environment
|
* Return secrets for workspace with id [workspaceId] and environment
|
||||||
@@ -17,11 +20,34 @@ import { BadRequestError } from "../../utils/errors";
|
|||||||
* @param res
|
* @param res
|
||||||
*/
|
*/
|
||||||
export const getSecretsRaw = async (req: Request, res: Response) => {
|
export const getSecretsRaw = async (req: Request, res: Response) => {
|
||||||
const workspaceId = req.query.workspaceId as string;
|
let workspaceId = req.query.workspaceId as string;
|
||||||
const environment = req.query.environment as string;
|
let environment = req.query.environment as string;
|
||||||
const secretPath = req.query.secretPath as string;
|
let secretPath = req.query.secretPath as string;
|
||||||
const includeImports = req.query.include_imports as string;
|
const includeImports = req.query.include_imports as string;
|
||||||
|
|
||||||
|
// if the service token has single scope, it will get all secrets for that scope by default
|
||||||
|
const serviceTokenDetails: IServiceTokenData = req?.serviceTokenData;
|
||||||
|
if (serviceTokenDetails) {
|
||||||
|
if (
|
||||||
|
serviceTokenDetails.scopes.length == 1 &&
|
||||||
|
!containsGlobPatterns(serviceTokenDetails.scopes[0].secretPath)
|
||||||
|
) {
|
||||||
|
const scope = serviceTokenDetails.scopes[0];
|
||||||
|
secretPath = scope.secretPath;
|
||||||
|
environment = scope.environment;
|
||||||
|
workspaceId = serviceTokenDetails.workspace.toString();
|
||||||
|
} else {
|
||||||
|
requireWorkspaceAuth({
|
||||||
|
acceptedRoles: [ADMIN, MEMBER],
|
||||||
|
locationWorkspaceId: "query",
|
||||||
|
locationEnvironment: "query",
|
||||||
|
requiredPermissions: [PERMISSION_READ_SECRETS],
|
||||||
|
requireBlindIndicesEnabled: true,
|
||||||
|
requireE2EEOff: true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const secrets = await SecretService.getSecrets({
|
const secrets = await SecretService.getSecrets({
|
||||||
workspaceId: new Types.ObjectId(workspaceId),
|
workspaceId: new Types.ObjectId(workspaceId),
|
||||||
environment,
|
environment,
|
||||||
@@ -33,7 +59,7 @@ export const getSecretsRaw = async (req: Request, res: Response) => {
|
|||||||
workspaceId: new Types.ObjectId(workspaceId)
|
workspaceId: new Types.ObjectId(workspaceId)
|
||||||
});
|
});
|
||||||
|
|
||||||
if (includeImports) {
|
if (includeImports === "true") {
|
||||||
const folders = await Folder.findOne({ workspace: workspaceId, environment });
|
const folders = await Folder.findOne({ workspace: workspaceId, environment });
|
||||||
let folderId = "root";
|
let folderId = "root";
|
||||||
// if folder exist get it and replace folderid with new one
|
// if folder exist get it and replace folderid with new one
|
||||||
@@ -271,7 +297,7 @@ export const getSecrets = async (req: Request, res: Response) => {
|
|||||||
authData: req.authData
|
authData: req.authData
|
||||||
});
|
});
|
||||||
|
|
||||||
if (includeImports) {
|
if (includeImports === "true") {
|
||||||
const folders = await Folder.findOne({ workspace: workspaceId, environment });
|
const folders = await Folder.findOne({ workspace: workspaceId, environment });
|
||||||
let folderId = "root";
|
let folderId = "root";
|
||||||
// if folder exist get it and replace folderid with new one
|
// if folder exist get it and replace folderid with new one
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ 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 ssoController from "./ssoController";
|
||||||
|
import * as usersController from "./usersController";
|
||||||
import * as workspaceController from "./workspaceController";
|
import * as workspaceController from "./workspaceController";
|
||||||
import * as actionController from "./actionController";
|
import * as actionController from "./actionController";
|
||||||
import * as membershipController from "./membershipController";
|
import * as membershipController from "./membershipController";
|
||||||
@@ -12,6 +13,7 @@ export {
|
|||||||
secretSnapshotController,
|
secretSnapshotController,
|
||||||
organizationsController,
|
organizationsController,
|
||||||
ssoController,
|
ssoController,
|
||||||
|
usersController,
|
||||||
workspaceController,
|
workspaceController,
|
||||||
actionController,
|
actionController,
|
||||||
membershipController,
|
membershipController,
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import { getSSOConfigHelper } from "../../helpers/organizations";
|
|||||||
import { client } from "../../../config";
|
import { client } from "../../../config";
|
||||||
import { ResourceNotFoundError } from "../../../utils/errors";
|
import { ResourceNotFoundError } from "../../../utils/errors";
|
||||||
import { getSiteURL } from "../../../config";
|
import { getSiteURL } from "../../../config";
|
||||||
|
import { EELicenseService } from "../../services";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Redirect user to appropriate SSO endpoint after successful authentication
|
* Redirect user to appropriate SSO endpoint after successful authentication
|
||||||
@@ -59,6 +60,12 @@ export const updateSSOConfig = async (req: Request, res: Response) => {
|
|||||||
audience
|
audience
|
||||||
} = req.body;
|
} = req.body;
|
||||||
|
|
||||||
|
const plan = await EELicenseService.getPlan(organizationId);
|
||||||
|
|
||||||
|
if (!plan.samlSSO) return res.status(400).send({
|
||||||
|
message: "Failed to update SAML SSO configuration due to plan restriction. Upgrade plan to update SSO configuration."
|
||||||
|
});
|
||||||
|
|
||||||
interface PatchUpdate {
|
interface PatchUpdate {
|
||||||
authProvider?: string;
|
authProvider?: string;
|
||||||
isActive?: boolean;
|
isActive?: boolean;
|
||||||
@@ -204,6 +211,12 @@ export const createSSOConfig = async (req: Request, res: Response) => {
|
|||||||
audience
|
audience
|
||||||
} = req.body;
|
} = req.body;
|
||||||
|
|
||||||
|
const plan = await EELicenseService.getPlan(organizationId);
|
||||||
|
|
||||||
|
if (!plan.samlSSO) return res.status(400).send({
|
||||||
|
message: "Failed to create SAML SSO configuration due to plan restriction. Upgrade plan to add SSO configuration."
|
||||||
|
});
|
||||||
|
|
||||||
const key = await BotOrgService.getSymmetricKey(
|
const key = await BotOrgService.getSymmetricKey(
|
||||||
new Types.ObjectId(organizationId)
|
new Types.ObjectId(organizationId)
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
import { Request, Response } from "express";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the ip address of the current user
|
||||||
|
* @param req
|
||||||
|
* @param res
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export const getMyIp = (req: Request, res: Response) => {
|
||||||
|
return res.status(200).send({
|
||||||
|
ip: req.authData.authIP
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -3,16 +3,20 @@ import { PipelineStage, Types } from "mongoose";
|
|||||||
import { Secret } from "../../../models";
|
import { Secret } from "../../../models";
|
||||||
import {
|
import {
|
||||||
FolderVersion,
|
FolderVersion,
|
||||||
|
IPType,
|
||||||
ISecretVersion,
|
ISecretVersion,
|
||||||
Log,
|
Log,
|
||||||
SecretSnapshot,
|
SecretSnapshot,
|
||||||
SecretVersion,
|
SecretVersion,
|
||||||
TFolderRootVersionSchema,
|
TFolderRootVersionSchema,
|
||||||
|
TrustedIP
|
||||||
} from "../../models";
|
} from "../../models";
|
||||||
import { EESecretService } from "../../services";
|
import { EESecretService } from "../../services";
|
||||||
import { getLatestSecretVersionIds } from "../../helpers/secretVersion";
|
import { getLatestSecretVersionIds } from "../../helpers/secretVersion";
|
||||||
import Folder, { TFolderSchema } from "../../../models/folder";
|
import Folder, { TFolderSchema } from "../../../models/folder";
|
||||||
import { searchByFolderId } from "../../../services/FolderService";
|
import { searchByFolderId } from "../../../services/FolderService";
|
||||||
|
import { EELicenseService } from "../../services";
|
||||||
|
import { extractIPDetails, isValidIpOrCidr } from "../../../utils/ip";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return secret snapshots for workspace with id [workspaceId]
|
* Return secret snapshots for workspace with id [workspaceId]
|
||||||
@@ -588,3 +592,147 @@ export const getWorkspaceLogs = async (req: Request, res: Response) => {
|
|||||||
logs,
|
logs,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return trusted ips for workspace with id [workspaceId]
|
||||||
|
* @param req
|
||||||
|
* @param res
|
||||||
|
*/
|
||||||
|
export const getWorkspaceTrustedIps = async (req: Request, res: Response) => {
|
||||||
|
const { workspaceId } = req.params;
|
||||||
|
|
||||||
|
const trustedIps = await TrustedIP.find({
|
||||||
|
workspace: new Types.ObjectId(workspaceId)
|
||||||
|
});
|
||||||
|
|
||||||
|
return res.status(200).send({
|
||||||
|
trustedIps
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a trusted ip to workspace with id [workspaceId]
|
||||||
|
* @param req
|
||||||
|
* @param res
|
||||||
|
*/
|
||||||
|
export const addWorkspaceTrustedIp = async (req: Request, res: Response) => {
|
||||||
|
const { workspaceId } = req.params;
|
||||||
|
const {
|
||||||
|
ipAddress: ip,
|
||||||
|
comment,
|
||||||
|
isActive
|
||||||
|
} = req.body;
|
||||||
|
|
||||||
|
const plan = await EELicenseService.getPlan(req.workspace.organization.toString());
|
||||||
|
|
||||||
|
if (!plan.ipAllowlisting) return res.status(400).send({
|
||||||
|
message: "Failed to add IP access range due to plan restriction. Upgrade plan to add IP access range."
|
||||||
|
});
|
||||||
|
|
||||||
|
const isValidIPOrCidr = isValidIpOrCidr(ip);
|
||||||
|
|
||||||
|
if (!isValidIPOrCidr) return res.status(400).send({
|
||||||
|
message: "The IP is not a valid IPv4, IPv6, or CIDR block"
|
||||||
|
});
|
||||||
|
|
||||||
|
const { ipAddress, type, prefix } = extractIPDetails(ip);
|
||||||
|
|
||||||
|
const trustedIp = await new TrustedIP({
|
||||||
|
workspace: new Types.ObjectId(workspaceId),
|
||||||
|
ipAddress,
|
||||||
|
type,
|
||||||
|
prefix,
|
||||||
|
isActive,
|
||||||
|
comment,
|
||||||
|
}).save();
|
||||||
|
|
||||||
|
return res.status(200).send({
|
||||||
|
trustedIp
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update trusted ip with id [trustedIpId] workspace with id [workspaceId]
|
||||||
|
* @param req
|
||||||
|
* @param res
|
||||||
|
*/
|
||||||
|
export const updateWorkspaceTrustedIp = async (req: Request, res: Response) => {
|
||||||
|
const { workspaceId, trustedIpId } = req.params;
|
||||||
|
const {
|
||||||
|
ipAddress: ip,
|
||||||
|
comment
|
||||||
|
} = req.body;
|
||||||
|
|
||||||
|
const plan = await EELicenseService.getPlan(req.workspace.organization.toString());
|
||||||
|
|
||||||
|
if (!plan.ipAllowlisting) return res.status(400).send({
|
||||||
|
message: "Failed to update IP access range due to plan restriction. Upgrade plan to update IP access range."
|
||||||
|
});
|
||||||
|
|
||||||
|
const isValidIPOrCidr = isValidIpOrCidr(ip);
|
||||||
|
|
||||||
|
if (!isValidIPOrCidr) return res.status(400).send({
|
||||||
|
message: "The IP is not a valid IPv4, IPv6, or CIDR block"
|
||||||
|
});
|
||||||
|
|
||||||
|
const { ipAddress, type, prefix } = extractIPDetails(ip);
|
||||||
|
|
||||||
|
const updateObject: {
|
||||||
|
ipAddress: string;
|
||||||
|
type: IPType;
|
||||||
|
comment: string;
|
||||||
|
prefix?: number;
|
||||||
|
$unset?: {
|
||||||
|
prefix: number;
|
||||||
|
}
|
||||||
|
} = {
|
||||||
|
ipAddress,
|
||||||
|
type,
|
||||||
|
comment
|
||||||
|
};
|
||||||
|
|
||||||
|
if (prefix !== undefined) {
|
||||||
|
updateObject.prefix = prefix;
|
||||||
|
} else {
|
||||||
|
updateObject.$unset = { prefix: 1 };
|
||||||
|
}
|
||||||
|
|
||||||
|
const trustedIp = await TrustedIP.findOneAndUpdate(
|
||||||
|
{
|
||||||
|
_id: new Types.ObjectId(trustedIpId),
|
||||||
|
workspace: new Types.ObjectId(workspaceId),
|
||||||
|
},
|
||||||
|
updateObject,
|
||||||
|
{
|
||||||
|
new: true
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return res.status(200).send({
|
||||||
|
trustedIp
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete IP access range from workspace with id [workspaceId]
|
||||||
|
* @param req
|
||||||
|
* @param res
|
||||||
|
*/
|
||||||
|
export const deleteWorkspaceTrustedIp = async (req: Request, res: Response) => {
|
||||||
|
const { workspaceId, trustedIpId } = req.params;
|
||||||
|
|
||||||
|
const plan = await EELicenseService.getPlan(req.workspace.organization.toString());
|
||||||
|
|
||||||
|
if (!plan.ipAllowlisting) return res.status(400).send({
|
||||||
|
message: "Failed to delete IP access range due to plan restriction. Upgrade plan to delete IP access range."
|
||||||
|
});
|
||||||
|
|
||||||
|
const trustedIp = await TrustedIP.findOneAndDelete({
|
||||||
|
_id: new Types.ObjectId(trustedIpId),
|
||||||
|
workspace: new Types.ObjectId(workspaceId)
|
||||||
|
});
|
||||||
|
|
||||||
|
return res.status(200).send({
|
||||||
|
trustedIp
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -66,6 +66,4 @@ const actionSchema = new Schema<IAction>(
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
const Action = model<IAction>("Action", actionSchema);
|
export const Action = model<IAction>("Action", actionSchema);
|
||||||
|
|
||||||
export default Action;
|
|
||||||
@@ -52,9 +52,7 @@ const folderRootVersionSchema = new Schema<TFolderRootVersionSchema>(
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
const FolderVersion = model<TFolderRootVersionSchema>(
|
export const FolderVersion = model<TFolderRootVersionSchema>(
|
||||||
"FolderVersion",
|
"FolderVersion",
|
||||||
folderRootVersionSchema
|
folderRootVersionSchema
|
||||||
);
|
);
|
||||||
|
|
||||||
export default FolderVersion;
|
|
||||||
|
|||||||
@@ -1,21 +1,7 @@
|
|||||||
import SecretSnapshot, { ISecretSnapshot } from "./secretSnapshot";
|
export * from "./secretSnapshot";
|
||||||
import SecretVersion, { ISecretVersion } from "./secretVersion";
|
export * from "./secretVersion";
|
||||||
import FolderVersion, { TFolderRootVersionSchema } from "./folderVersion";
|
export * from "./folderVersion";
|
||||||
import Log, { ILog } from "./log";
|
export * from "./log";
|
||||||
import Action, { IAction } from "./action";
|
export * from "./action";
|
||||||
import SSOConfig, { ISSOConfig } from "./ssoConfig";
|
export * from "./ssoConfig";
|
||||||
|
export * from "./trustedIp";
|
||||||
export {
|
|
||||||
SecretSnapshot,
|
|
||||||
ISecretSnapshot,
|
|
||||||
SecretVersion,
|
|
||||||
ISecretVersion,
|
|
||||||
FolderVersion,
|
|
||||||
TFolderRootVersionSchema,
|
|
||||||
Log,
|
|
||||||
ILog,
|
|
||||||
Action,
|
|
||||||
IAction,
|
|
||||||
SSOConfig,
|
|
||||||
ISSOConfig
|
|
||||||
};
|
|
||||||
@@ -69,6 +69,4 @@ const logSchema = new Schema<ILog>(
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
const Log = model<ILog>("Log", logSchema);
|
export const Log = model<ILog>("Log", logSchema);
|
||||||
|
|
||||||
export default Log;
|
|
||||||
@@ -46,9 +46,7 @@ const secretSnapshotSchema = new Schema<ISecretSnapshot>(
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
const SecretSnapshot = model<ISecretSnapshot>(
|
export const SecretSnapshot = model<ISecretSnapshot>(
|
||||||
"SecretSnapshot",
|
"SecretSnapshot",
|
||||||
secretSnapshotSchema
|
secretSnapshotSchema
|
||||||
);
|
);
|
||||||
|
|
||||||
export default SecretSnapshot;
|
|
||||||
|
|||||||
@@ -124,9 +124,7 @@ const secretVersionSchema = new Schema<ISecretVersion>(
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
const SecretVersion = model<ISecretVersion>(
|
export const SecretVersion = model<ISecretVersion>(
|
||||||
"SecretVersion",
|
"SecretVersion",
|
||||||
secretVersionSchema
|
secretVersionSchema
|
||||||
);
|
);
|
||||||
|
|
||||||
export default SecretVersion;
|
|
||||||
|
|||||||
@@ -77,6 +77,4 @@ const ssoConfigSchema = new Schema<ISSOConfig>(
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
const SSOConfig = model<ISSOConfig>("SSOConfig", ssoConfigSchema);
|
export const SSOConfig = model<ISSOConfig>("SSOConfig", ssoConfigSchema);
|
||||||
|
|
||||||
export default SSOConfig;
|
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
import { Schema, Types, model } from "mongoose";
|
||||||
|
|
||||||
|
export enum IPType {
|
||||||
|
IPV4 = "ipv4",
|
||||||
|
IPV6 = "ipv6"
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ITrustedIP {
|
||||||
|
_id: Types.ObjectId;
|
||||||
|
workspace: Types.ObjectId;
|
||||||
|
ipAddress: string;
|
||||||
|
type: "ipv4" | "ipv6", // either IPv4/IPv6 address or network IPv4/IPv6 address
|
||||||
|
isActive: boolean;
|
||||||
|
comment: string;
|
||||||
|
prefix?: number; // CIDR
|
||||||
|
}
|
||||||
|
|
||||||
|
const trustedIpSchema = new Schema<ITrustedIP>(
|
||||||
|
{
|
||||||
|
workspace: {
|
||||||
|
type: Schema.Types.ObjectId,
|
||||||
|
ref: "Workspace",
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
ipAddress: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
type: {
|
||||||
|
type: String,
|
||||||
|
enum: [
|
||||||
|
IPType.IPV4,
|
||||||
|
IPType.IPV6
|
||||||
|
],
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
prefix: {
|
||||||
|
type: Number,
|
||||||
|
required: false
|
||||||
|
},
|
||||||
|
isActive: {
|
||||||
|
type: Boolean,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
comment: {
|
||||||
|
type: String
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
timestamps: true
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
export const TrustedIP = model<ITrustedIP>("TrustedIP", trustedIpSchema);
|
||||||
@@ -2,6 +2,7 @@ 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 sso from "./sso";
|
||||||
|
import users from "./users";
|
||||||
import workspace from "./workspace";
|
import workspace from "./workspace";
|
||||||
import action from "./action";
|
import action from "./action";
|
||||||
import cloudProducts from "./cloudProducts";
|
import cloudProducts from "./cloudProducts";
|
||||||
@@ -11,6 +12,7 @@ export {
|
|||||||
secretSnapshot,
|
secretSnapshot,
|
||||||
organizations,
|
organizations,
|
||||||
sso,
|
sso,
|
||||||
|
users,
|
||||||
workspace,
|
workspace,
|
||||||
action,
|
action,
|
||||||
cloudProducts,
|
cloudProducts,
|
||||||
|
|||||||
@@ -18,10 +18,15 @@ import {
|
|||||||
router.get(
|
router.get(
|
||||||
"/redirect/google",
|
"/redirect/google",
|
||||||
authLimiter,
|
authLimiter,
|
||||||
|
(req, res, next) => {
|
||||||
passport.authenticate("google", {
|
passport.authenticate("google", {
|
||||||
scope: ["profile", "email"],
|
scope: ["profile", "email"],
|
||||||
session: false,
|
session: false,
|
||||||
})
|
...(req.query.callback_port ? {
|
||||||
|
state: req.query.callback_port as string
|
||||||
|
} : {})
|
||||||
|
})(req, res, next);
|
||||||
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
router.get(
|
router.get(
|
||||||
@@ -36,9 +41,15 @@ router.get(
|
|||||||
router.get(
|
router.get(
|
||||||
"/redirect/saml2/:ssoIdentifier",
|
"/redirect/saml2/:ssoIdentifier",
|
||||||
authLimiter,
|
authLimiter,
|
||||||
passport.authenticate("saml", {
|
(req, res, next) => {
|
||||||
failureRedirect: "/login/fail"
|
const options = {
|
||||||
})
|
failureRedirect: "/",
|
||||||
|
additionalParams: {
|
||||||
|
RelayState: req.query.callback_port ?? ""
|
||||||
|
},
|
||||||
|
};
|
||||||
|
passport.authenticate("saml", options)(req, res, next);
|
||||||
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
router.post("/saml2/:ssoIdentifier",
|
router.post("/saml2/:ssoIdentifier",
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
import express from "express";
|
||||||
|
const router = express.Router();
|
||||||
|
import {
|
||||||
|
requireAuth
|
||||||
|
} from "../../../middleware";
|
||||||
|
import { AUTH_MODE_API_KEY, AUTH_MODE_JWT } from "../../../variables";
|
||||||
|
import { usersController } from "../../controllers/v1";
|
||||||
|
|
||||||
|
router.get(
|
||||||
|
"/me/ip",
|
||||||
|
requireAuth({
|
||||||
|
acceptedAuthModes: [AUTH_MODE_JWT, AUTH_MODE_API_KEY],
|
||||||
|
}),
|
||||||
|
usersController.getMyIp
|
||||||
|
);
|
||||||
|
|
||||||
|
export default router;
|
||||||
@@ -6,13 +6,18 @@ import {
|
|||||||
validateRequest,
|
validateRequest,
|
||||||
} from "../../../middleware";
|
} from "../../../middleware";
|
||||||
import { body, param, query } from "express-validator";
|
import { body, param, query } from "express-validator";
|
||||||
import { ADMIN, MEMBER } from "../../../variables";
|
import {
|
||||||
|
ADMIN,
|
||||||
|
AUTH_MODE_API_KEY,
|
||||||
|
AUTH_MODE_JWT,
|
||||||
|
MEMBER
|
||||||
|
} from "../../../variables";
|
||||||
import { workspaceController } from "../../controllers/v1";
|
import { workspaceController } from "../../controllers/v1";
|
||||||
|
|
||||||
router.get(
|
router.get(
|
||||||
"/:workspaceId/secret-snapshots",
|
"/:workspaceId/secret-snapshots",
|
||||||
requireAuth({
|
requireAuth({
|
||||||
acceptedAuthModes: ["jwt", "apiKey"],
|
acceptedAuthModes: [AUTH_MODE_JWT, AUTH_MODE_API_KEY],
|
||||||
}),
|
}),
|
||||||
requireWorkspaceAuth({
|
requireWorkspaceAuth({
|
||||||
acceptedRoles: [ADMIN, MEMBER],
|
acceptedRoles: [ADMIN, MEMBER],
|
||||||
@@ -30,7 +35,7 @@ router.get(
|
|||||||
router.get(
|
router.get(
|
||||||
"/:workspaceId/secret-snapshots/count",
|
"/:workspaceId/secret-snapshots/count",
|
||||||
requireAuth({
|
requireAuth({
|
||||||
acceptedAuthModes: ["jwt"],
|
acceptedAuthModes: [AUTH_MODE_JWT],
|
||||||
}),
|
}),
|
||||||
requireWorkspaceAuth({
|
requireWorkspaceAuth({
|
||||||
acceptedRoles: [ADMIN, MEMBER],
|
acceptedRoles: [ADMIN, MEMBER],
|
||||||
@@ -46,7 +51,7 @@ router.get(
|
|||||||
router.post(
|
router.post(
|
||||||
"/:workspaceId/secret-snapshots/rollback",
|
"/:workspaceId/secret-snapshots/rollback",
|
||||||
requireAuth({
|
requireAuth({
|
||||||
acceptedAuthModes: ["jwt", "apiKey"],
|
acceptedAuthModes: [AUTH_MODE_JWT, AUTH_MODE_API_KEY],
|
||||||
}),
|
}),
|
||||||
requireWorkspaceAuth({
|
requireWorkspaceAuth({
|
||||||
acceptedRoles: [ADMIN, MEMBER],
|
acceptedRoles: [ADMIN, MEMBER],
|
||||||
@@ -63,7 +68,7 @@ router.post(
|
|||||||
router.get(
|
router.get(
|
||||||
"/:workspaceId/logs",
|
"/:workspaceId/logs",
|
||||||
requireAuth({
|
requireAuth({
|
||||||
acceptedAuthModes: ["jwt", "apiKey"],
|
acceptedAuthModes: [AUTH_MODE_JWT, AUTH_MODE_API_KEY],
|
||||||
}),
|
}),
|
||||||
requireWorkspaceAuth({
|
requireWorkspaceAuth({
|
||||||
acceptedRoles: [ADMIN, MEMBER],
|
acceptedRoles: [ADMIN, MEMBER],
|
||||||
@@ -79,4 +84,66 @@ router.get(
|
|||||||
workspaceController.getWorkspaceLogs
|
workspaceController.getWorkspaceLogs
|
||||||
);
|
);
|
||||||
|
|
||||||
|
router.get(
|
||||||
|
"/:workspaceId/trusted-ips",
|
||||||
|
param("workspaceId").exists().isString().trim(),
|
||||||
|
requireAuth({
|
||||||
|
acceptedAuthModes: [AUTH_MODE_JWT],
|
||||||
|
}),
|
||||||
|
requireWorkspaceAuth({
|
||||||
|
acceptedRoles: [ADMIN, MEMBER],
|
||||||
|
locationWorkspaceId: "params",
|
||||||
|
}),
|
||||||
|
workspaceController.getWorkspaceTrustedIps
|
||||||
|
);
|
||||||
|
|
||||||
|
router.post(
|
||||||
|
"/:workspaceId/trusted-ips",
|
||||||
|
param("workspaceId").exists().isString().trim(),
|
||||||
|
body("ipAddress").exists().isString().trim(),
|
||||||
|
body("comment").default("").isString().trim(),
|
||||||
|
body("isActive").exists().isBoolean(),
|
||||||
|
validateRequest,
|
||||||
|
requireAuth({
|
||||||
|
acceptedAuthModes: [AUTH_MODE_JWT],
|
||||||
|
}),
|
||||||
|
requireWorkspaceAuth({
|
||||||
|
acceptedRoles: [ADMIN],
|
||||||
|
locationWorkspaceId: "params",
|
||||||
|
}),
|
||||||
|
workspaceController.addWorkspaceTrustedIp
|
||||||
|
);
|
||||||
|
|
||||||
|
router.patch(
|
||||||
|
"/:workspaceId/trusted-ips/:trustedIpId",
|
||||||
|
param("workspaceId").exists().isString().trim(),
|
||||||
|
param("trustedIpId").exists().isString().trim(),
|
||||||
|
body("ipAddress").isString().trim().default(""),
|
||||||
|
body("comment").default("").isString().trim(),
|
||||||
|
validateRequest,
|
||||||
|
requireAuth({
|
||||||
|
acceptedAuthModes: [AUTH_MODE_JWT],
|
||||||
|
}),
|
||||||
|
requireWorkspaceAuth({
|
||||||
|
acceptedRoles: [ADMIN],
|
||||||
|
locationWorkspaceId: "params",
|
||||||
|
}),
|
||||||
|
workspaceController.updateWorkspaceTrustedIp
|
||||||
|
);
|
||||||
|
|
||||||
|
router.delete(
|
||||||
|
"/:workspaceId/trusted-ips/:trustedIpId",
|
||||||
|
param("workspaceId").exists().isString().trim(),
|
||||||
|
param("trustedIpId").exists().isString().trim(),
|
||||||
|
validateRequest,
|
||||||
|
requireAuth({
|
||||||
|
acceptedAuthModes: [AUTH_MODE_JWT],
|
||||||
|
}),
|
||||||
|
requireWorkspaceAuth({
|
||||||
|
acceptedRoles: [ADMIN],
|
||||||
|
locationWorkspaceId: "params",
|
||||||
|
}),
|
||||||
|
workspaceController.deleteWorkspaceTrustedIp
|
||||||
|
);
|
||||||
|
|
||||||
export default router;
|
export default router;
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ interface FeatureSet {
|
|||||||
environmentsUsed: number;
|
environmentsUsed: number;
|
||||||
secretVersioning: boolean;
|
secretVersioning: boolean;
|
||||||
pitRecovery: boolean;
|
pitRecovery: boolean;
|
||||||
|
ipAllowlisting: boolean;
|
||||||
rbac: boolean;
|
rbac: boolean;
|
||||||
customRateLimits: boolean;
|
customRateLimits: boolean;
|
||||||
customAlerts: boolean;
|
customAlerts: boolean;
|
||||||
@@ -60,6 +61,7 @@ class EELicenseService {
|
|||||||
environmentsUsed: 0,
|
environmentsUsed: 0,
|
||||||
secretVersioning: true,
|
secretVersioning: true,
|
||||||
pitRecovery: false,
|
pitRecovery: false,
|
||||||
|
ipAllowlisting: false,
|
||||||
rbac: true,
|
rbac: true,
|
||||||
customRateLimits: true,
|
customRateLimits: true,
|
||||||
customAlerts: true,
|
customAlerts: true,
|
||||||
|
|||||||
@@ -3,12 +3,100 @@ import { client, getEncryptionKey, getRootEncryptionKey } from "../config";
|
|||||||
import { BotOrg } from "../models";
|
import { BotOrg } from "../models";
|
||||||
import { decryptSymmetric128BitHexKeyUTF8 } from "../utils/crypto";
|
import { decryptSymmetric128BitHexKeyUTF8 } from "../utils/crypto";
|
||||||
import {
|
import {
|
||||||
|
ALGORITHM_AES_256_GCM,
|
||||||
ENCODING_SCHEME_BASE64,
|
ENCODING_SCHEME_BASE64,
|
||||||
ENCODING_SCHEME_UTF8
|
ENCODING_SCHEME_UTF8
|
||||||
} from "../variables";
|
} from "../variables";
|
||||||
import { InternalServerError } from "../utils/errors";
|
import { InternalServerError } from "../utils/errors";
|
||||||
|
import { encryptSymmetric128BitHexKeyUTF8, generateKeyPair } from "../utils/crypto";
|
||||||
|
|
||||||
// TODO: DOCstrings
|
/**
|
||||||
|
* Create a bot with name [name] for organization with id [organizationId]
|
||||||
|
* @param {Object} obj
|
||||||
|
* @param {String} obj.name - name of bot
|
||||||
|
* @param {String} obj.organizationId - id of organization that bot belongs to
|
||||||
|
*/
|
||||||
|
export const createBotOrg = async ({
|
||||||
|
name,
|
||||||
|
organizationId,
|
||||||
|
}: {
|
||||||
|
name: string;
|
||||||
|
organizationId: Types.ObjectId;
|
||||||
|
}) => {
|
||||||
|
const encryptionKey = await getEncryptionKey();
|
||||||
|
const rootEncryptionKey = await getRootEncryptionKey();
|
||||||
|
|
||||||
|
const { publicKey, privateKey } = generateKeyPair();
|
||||||
|
const key = client.createSymmetricKey();
|
||||||
|
|
||||||
|
if (rootEncryptionKey) {
|
||||||
|
const {
|
||||||
|
ciphertext: encryptedPrivateKey,
|
||||||
|
iv: privateKeyIV,
|
||||||
|
tag: privateKeyTag
|
||||||
|
} = client.encryptSymmetric(privateKey, rootEncryptionKey);
|
||||||
|
|
||||||
|
const {
|
||||||
|
ciphertext: encryptedSymmetricKey,
|
||||||
|
iv: symmetricKeyIV,
|
||||||
|
tag: symmetricKeyTag
|
||||||
|
} = client.encryptSymmetric(key, rootEncryptionKey);
|
||||||
|
|
||||||
|
return await new BotOrg({
|
||||||
|
name,
|
||||||
|
organization: organizationId,
|
||||||
|
publicKey,
|
||||||
|
encryptedSymmetricKey,
|
||||||
|
symmetricKeyIV,
|
||||||
|
symmetricKeyTag,
|
||||||
|
symmetricKeyAlgorithm: ALGORITHM_AES_256_GCM,
|
||||||
|
symmetricKeyKeyEncoding: ENCODING_SCHEME_BASE64,
|
||||||
|
encryptedPrivateKey,
|
||||||
|
privateKeyIV,
|
||||||
|
privateKeyTag,
|
||||||
|
privateKeyAlgorithm: ALGORITHM_AES_256_GCM,
|
||||||
|
privateKeyKeyEncoding: ENCODING_SCHEME_BASE64
|
||||||
|
}).save();
|
||||||
|
} else if (encryptionKey) {
|
||||||
|
const {
|
||||||
|
ciphertext: encryptedPrivateKey,
|
||||||
|
iv: privateKeyIV,
|
||||||
|
tag: privateKeyTag
|
||||||
|
} = encryptSymmetric128BitHexKeyUTF8({
|
||||||
|
plaintext: privateKey,
|
||||||
|
key: encryptionKey
|
||||||
|
});
|
||||||
|
|
||||||
|
const {
|
||||||
|
ciphertext: encryptedSymmetricKey,
|
||||||
|
iv: symmetricKeyIV,
|
||||||
|
tag: symmetricKeyTag
|
||||||
|
} = encryptSymmetric128BitHexKeyUTF8({
|
||||||
|
plaintext: key,
|
||||||
|
key: encryptionKey
|
||||||
|
});
|
||||||
|
|
||||||
|
return await new BotOrg({
|
||||||
|
name,
|
||||||
|
organization: organizationId,
|
||||||
|
publicKey,
|
||||||
|
encryptedSymmetricKey,
|
||||||
|
symmetricKeyIV,
|
||||||
|
symmetricKeyTag,
|
||||||
|
symmetricKeyAlgorithm: ALGORITHM_AES_256_GCM,
|
||||||
|
symmetricKeyKeyEncoding: ENCODING_SCHEME_UTF8,
|
||||||
|
encryptedPrivateKey,
|
||||||
|
privateKeyIV,
|
||||||
|
privateKeyTag,
|
||||||
|
privateKeyAlgorithm: ALGORITHM_AES_256_GCM,
|
||||||
|
privateKeyKeyEncoding: ENCODING_SCHEME_UTF8
|
||||||
|
}).save();
|
||||||
|
}
|
||||||
|
|
||||||
|
throw InternalServerError({
|
||||||
|
message: "Failed to create new organization bot due to missing encryption key",
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
export const getSymmetricKeyHelper = async (organizationId: Types.ObjectId) => {
|
export const getSymmetricKeyHelper = async (organizationId: Types.ObjectId) => {
|
||||||
const rootEncryptionKey = await getRootEncryptionKey();
|
const rootEncryptionKey = await getRootEncryptionKey();
|
||||||
|
|||||||
@@ -14,6 +14,9 @@ import {
|
|||||||
licenseKeyRequest,
|
licenseKeyRequest,
|
||||||
licenseServerKeyRequest,
|
licenseServerKeyRequest,
|
||||||
} from "../config/request";
|
} from "../config/request";
|
||||||
|
import {
|
||||||
|
createBotOrg
|
||||||
|
} from "./botOrg";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create an organization with name [name]
|
* Create an organization with name [name]
|
||||||
@@ -29,6 +32,7 @@ export const createOrganization = async ({
|
|||||||
name: string;
|
name: string;
|
||||||
email: string;
|
email: string;
|
||||||
}) => {
|
}) => {
|
||||||
|
|
||||||
const licenseServerKey = await getLicenseServerKey();
|
const licenseServerKey = await getLicenseServerKey();
|
||||||
let organization;
|
let organization;
|
||||||
|
|
||||||
@@ -52,6 +56,12 @@ export const createOrganization = async ({
|
|||||||
}).save();
|
}).save();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// initialize bot for organization
|
||||||
|
await createBotOrg({
|
||||||
|
name,
|
||||||
|
organizationId: organization._id
|
||||||
|
});
|
||||||
|
|
||||||
return organization;
|
return organization;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ import { EELogService, EESecretService } from "../ee/services";
|
|||||||
import { getAuthDataPayloadIdObj, getAuthDataPayloadUserObj } from "../utils/auth";
|
import { getAuthDataPayloadIdObj, getAuthDataPayloadUserObj } from "../utils/auth";
|
||||||
import { getFolderIdFromServiceToken } from "../services/FolderService";
|
import { getFolderIdFromServiceToken } from "../services/FolderService";
|
||||||
import picomatch from "picomatch";
|
import picomatch from "picomatch";
|
||||||
|
import path from "path";
|
||||||
|
|
||||||
export const isValidScope = (
|
export const isValidScope = (
|
||||||
authPayload: IServiceTokenData,
|
authPayload: IServiceTokenData,
|
||||||
@@ -60,6 +61,13 @@ export const isValidScope = (
|
|||||||
return Boolean(validScope);
|
return Boolean(validScope);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export function containsGlobPatterns(secretPath: string) {
|
||||||
|
const globChars = ["*", "?", "[", "]", "{", "}", "**"];
|
||||||
|
const normalizedPath = path.normalize(secretPath);
|
||||||
|
return globChars.some(char => normalizedPath.includes(char));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns an object containing secret [secret] but with its value, key, comment decrypted.
|
* Returns an object containing secret [secret] but with its value, key, comment decrypted.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -5,6 +5,10 @@ import {
|
|||||||
Secret,
|
Secret,
|
||||||
Workspace,
|
Workspace,
|
||||||
} from "../models";
|
} from "../models";
|
||||||
|
import {
|
||||||
|
IPType,
|
||||||
|
TrustedIP
|
||||||
|
} from "../ee/models";
|
||||||
import { createBot } from "../helpers/bot";
|
import { createBot } from "../helpers/bot";
|
||||||
import { EELicenseService } from "../ee/services";
|
import { EELicenseService } from "../ee/services";
|
||||||
import { SecretService } from "../services";
|
import { SecretService } from "../services";
|
||||||
@@ -41,6 +45,16 @@ export const createWorkspace = async ({
|
|||||||
workspaceId: workspace._id,
|
workspaceId: workspace._id,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// initialize default trusted ip of 0.0.0.0/0
|
||||||
|
await new TrustedIP({
|
||||||
|
workspace: workspace._id,
|
||||||
|
ipAddress: "0.0.0.0",
|
||||||
|
type: IPType.IPV4,
|
||||||
|
prefix: 0,
|
||||||
|
isActive: true,
|
||||||
|
comment: ""
|
||||||
|
}).save()
|
||||||
|
|
||||||
await EELicenseService.refreshPlan(organizationId);
|
await EELicenseService.refreshPlan(organizationId);
|
||||||
|
|
||||||
return workspace;
|
return workspace;
|
||||||
|
|||||||
@@ -22,7 +22,8 @@ import {
|
|||||||
sso as eeSSORouter,
|
sso as eeSSORouter,
|
||||||
secret as eeSecretRouter,
|
secret as eeSecretRouter,
|
||||||
secretSnapshot as eeSecretSnapshotRouter,
|
secretSnapshot as eeSecretSnapshotRouter,
|
||||||
workspace as eeWorkspaceRouter
|
users as eeUsersRouter,
|
||||||
|
workspace as eeWorkspaceRouter,
|
||||||
} from "./ee/routes/v1";
|
} from "./ee/routes/v1";
|
||||||
import {
|
import {
|
||||||
auth as v1AuthRouter,
|
auth as v1AuthRouter,
|
||||||
@@ -129,6 +130,7 @@ const main = async () => {
|
|||||||
// (EE) routes
|
// (EE) routes
|
||||||
app.use("/api/v1/secret", eeSecretRouter);
|
app.use("/api/v1/secret", eeSecretRouter);
|
||||||
app.use("/api/v1/secret-snapshot", eeSecretSnapshotRouter);
|
app.use("/api/v1/secret-snapshot", eeSecretSnapshotRouter);
|
||||||
|
app.use("/api/v1/users", eeUsersRouter);
|
||||||
app.use("/api/v1/workspace", eeWorkspaceRouter);
|
app.use("/api/v1/workspace", eeWorkspaceRouter);
|
||||||
app.use("/api/v1/action", eeActionRouter);
|
app.use("/api/v1/action", eeActionRouter);
|
||||||
app.use("/api/v1/organizations", eeOrganizationsRouter);
|
app.use("/api/v1/organizations", eeOrganizationsRouter);
|
||||||
|
|||||||
@@ -1,6 +1,3 @@
|
|||||||
import { Octokit } from "@octokit/rest";
|
|
||||||
import { IIntegrationAuth } from "../models";
|
|
||||||
import { standardRequest } from "../config/request";
|
|
||||||
import {
|
import {
|
||||||
INTEGRATION_AWS_PARAMETER_STORE,
|
INTEGRATION_AWS_PARAMETER_STORE,
|
||||||
INTEGRATION_AWS_SECRET_MANAGER,
|
INTEGRATION_AWS_SECRET_MANAGER,
|
||||||
@@ -13,8 +10,12 @@ import {
|
|||||||
INTEGRATION_CIRCLECI_API_URL,
|
INTEGRATION_CIRCLECI_API_URL,
|
||||||
INTEGRATION_CLOUDFLARE_PAGES,
|
INTEGRATION_CLOUDFLARE_PAGES,
|
||||||
INTEGRATION_CLOUDFLARE_PAGES_API_URL,
|
INTEGRATION_CLOUDFLARE_PAGES_API_URL,
|
||||||
|
INTEGRATION_CLOUD_66,
|
||||||
|
INTEGRATION_CLOUD_66_API_URL,
|
||||||
INTEGRATION_CODEFRESH,
|
INTEGRATION_CODEFRESH,
|
||||||
INTEGRATION_CODEFRESH_API_URL,
|
INTEGRATION_CODEFRESH_API_URL,
|
||||||
|
INTEGRATION_DIGITAL_OCEAN_API_URL,
|
||||||
|
INTEGRATION_DIGITAL_OCEAN_APP_PLATFORM,
|
||||||
INTEGRATION_FLYIO,
|
INTEGRATION_FLYIO,
|
||||||
INTEGRATION_FLYIO_API_URL,
|
INTEGRATION_FLYIO_API_URL,
|
||||||
INTEGRATION_GITHUB,
|
INTEGRATION_GITHUB,
|
||||||
@@ -34,11 +35,16 @@ import {
|
|||||||
INTEGRATION_RENDER_API_URL,
|
INTEGRATION_RENDER_API_URL,
|
||||||
INTEGRATION_SUPABASE,
|
INTEGRATION_SUPABASE,
|
||||||
INTEGRATION_SUPABASE_API_URL,
|
INTEGRATION_SUPABASE_API_URL,
|
||||||
|
INTEGRATION_TERRAFORM_CLOUD,
|
||||||
|
INTEGRATION_TERRAFORM_CLOUD_API_URL,
|
||||||
INTEGRATION_TRAVISCI,
|
INTEGRATION_TRAVISCI,
|
||||||
INTEGRATION_TRAVISCI_API_URL,
|
INTEGRATION_TRAVISCI_API_URL,
|
||||||
INTEGRATION_VERCEL,
|
INTEGRATION_VERCEL,
|
||||||
INTEGRATION_VERCEL_API_URL
|
INTEGRATION_VERCEL_API_URL
|
||||||
} from "../variables";
|
} from "../variables";
|
||||||
|
import { IIntegrationAuth } from "../models";
|
||||||
|
import { Octokit } from "@octokit/rest";
|
||||||
|
import { standardRequest } from "../config/request";
|
||||||
|
|
||||||
interface App {
|
interface App {
|
||||||
name: string;
|
name: string;
|
||||||
@@ -132,6 +138,12 @@ const getApps = async ({
|
|||||||
serverId: accessId
|
serverId: accessId
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
|
case INTEGRATION_TERRAFORM_CLOUD:
|
||||||
|
apps = await getAppsTerraformCloud({
|
||||||
|
accessToken,
|
||||||
|
workspacesId: accessId,
|
||||||
|
});
|
||||||
|
break;
|
||||||
case INTEGRATION_TRAVISCI:
|
case INTEGRATION_TRAVISCI:
|
||||||
apps = await getAppsTravisCI({
|
apps = await getAppsTravisCI({
|
||||||
accessToken,
|
accessToken,
|
||||||
@@ -169,6 +181,16 @@ const getApps = async ({
|
|||||||
accessToken,
|
accessToken,
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
|
case INTEGRATION_DIGITAL_OCEAN_APP_PLATFORM:
|
||||||
|
apps = await getAppsDigitalOceanAppPlatform({
|
||||||
|
accessToken
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case INTEGRATION_CLOUD_66:
|
||||||
|
apps = await getAppsCloud66({
|
||||||
|
accessToken,
|
||||||
|
});
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
return apps;
|
return apps;
|
||||||
@@ -556,6 +578,43 @@ const getAppsTravisCI = async ({ accessToken }: { accessToken: string }) => {
|
|||||||
return apps;
|
return apps;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return list of projects for Terraform Cloud integration
|
||||||
|
* @param {Object} obj
|
||||||
|
* @param {String} obj.accessToken - access token for Terraform Cloud API
|
||||||
|
* @param {String} obj.workspacesId - workspace id of Terraform Cloud projects
|
||||||
|
* @returns {Object[]} apps - names and ids of Terraform Cloud projects
|
||||||
|
* @returns {String} apps.name - name of Terraform Cloud projects
|
||||||
|
*/
|
||||||
|
const getAppsTerraformCloud = async ({
|
||||||
|
accessToken,
|
||||||
|
workspacesId
|
||||||
|
}: {
|
||||||
|
accessToken: string;
|
||||||
|
workspacesId?: string;
|
||||||
|
}) => {
|
||||||
|
const res = (
|
||||||
|
await standardRequest.get(`${INTEGRATION_TERRAFORM_CLOUD_API_URL}/api/v2/workspaces/${workspacesId}`, {
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${accessToken}`,
|
||||||
|
Accept: "application/json",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
).data.data;
|
||||||
|
|
||||||
|
const apps = []
|
||||||
|
|
||||||
|
const appsObj = {
|
||||||
|
name: res?.attributes.name,
|
||||||
|
appId: res?.id,
|
||||||
|
};
|
||||||
|
|
||||||
|
apps.push(appsObj)
|
||||||
|
|
||||||
|
return apps;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return list of repositories for GitLab integration
|
* Return list of repositories for GitLab integration
|
||||||
* @param {Object} obj
|
* @param {Object} obj
|
||||||
@@ -884,7 +943,6 @@ const getAppsNorthflank = async ({ accessToken }: { accessToken: string }) => {
|
|||||||
* @returns {Object[]} apps - names of Supabase apps
|
* @returns {Object[]} apps - names of Supabase apps
|
||||||
* @returns {String} apps.name - name of Supabase app
|
* @returns {String} apps.name - name of Supabase app
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const getAppsCodefresh = async ({
|
const getAppsCodefresh = async ({
|
||||||
accessToken,
|
accessToken,
|
||||||
}: {
|
}: {
|
||||||
@@ -907,4 +965,106 @@ const getAppsCodefresh = async ({
|
|||||||
return apps;
|
return apps;
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return list of applications for DigitalOcean App Platform integration
|
||||||
|
* @param {Object} obj
|
||||||
|
* @param {String} obj.accessToken - personal access token for DigitalOcean
|
||||||
|
* @returns {Object[]} apps - names of DigitalOcean apps
|
||||||
|
* @returns {String} apps.name - name of DigitalOcean app
|
||||||
|
* @returns {String} apps.appId - id of DigitalOcean app
|
||||||
|
*/
|
||||||
|
const getAppsDigitalOceanAppPlatform = async ({ accessToken }: { accessToken: string }) => {
|
||||||
|
interface DigitalOceanApp {
|
||||||
|
id: string;
|
||||||
|
owner_uuid: string;
|
||||||
|
spec: Spec;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Spec {
|
||||||
|
name: string;
|
||||||
|
region: string;
|
||||||
|
envs: Env[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Env {
|
||||||
|
key: string;
|
||||||
|
value: string;
|
||||||
|
scope: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = (
|
||||||
|
await standardRequest.get(`${INTEGRATION_DIGITAL_OCEAN_API_URL}/v2/apps`, {
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${accessToken}`,
|
||||||
|
"Accept-Encoding": "application/json"
|
||||||
|
}
|
||||||
|
})
|
||||||
|
).data;
|
||||||
|
|
||||||
|
return (res.apps ?? []).map((a: DigitalOceanApp) => ({
|
||||||
|
name: a.spec.name,
|
||||||
|
appId: a.id
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return list of applications for Cloud66 integration
|
||||||
|
* @param {Object} obj
|
||||||
|
* @param {String} obj.accessToken - personal access token for Cloud66 API
|
||||||
|
* @returns {Object[]} apps - Cloud66 apps
|
||||||
|
* @returns {String} apps.name - name of Cloud66 app
|
||||||
|
* @returns {String} apps.appId - uid of Cloud66 app
|
||||||
|
*/
|
||||||
|
const getAppsCloud66 = async ({ accessToken }: { accessToken: string }) => {
|
||||||
|
interface Cloud66Apps {
|
||||||
|
uid: string;
|
||||||
|
name: string;
|
||||||
|
account_id: number;
|
||||||
|
git: string;
|
||||||
|
git_branch: string;
|
||||||
|
environment: string;
|
||||||
|
cloud: string;
|
||||||
|
fqdn: string;
|
||||||
|
language: string;
|
||||||
|
framework: string;
|
||||||
|
status: number;
|
||||||
|
health: number;
|
||||||
|
last_activity: string;
|
||||||
|
last_activity_iso: string;
|
||||||
|
maintenance_mode: boolean;
|
||||||
|
has_loadbalancer: boolean;
|
||||||
|
created_at: string;
|
||||||
|
updated_at: string;
|
||||||
|
deploy_directory: string;
|
||||||
|
cloud_status: string;
|
||||||
|
backend: string;
|
||||||
|
version: string;
|
||||||
|
revision: string;
|
||||||
|
is_busy: boolean;
|
||||||
|
account_name: string;
|
||||||
|
is_cluster: boolean;
|
||||||
|
is_inside_cluster: boolean;
|
||||||
|
cluster_name: any;
|
||||||
|
application_address: string;
|
||||||
|
configstore_namespace: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const stacks = (
|
||||||
|
await standardRequest.get(`${INTEGRATION_CLOUD_66_API_URL}/3/stacks`, {
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${accessToken}`,
|
||||||
|
"Accept-Encoding": "application/json"
|
||||||
|
}
|
||||||
|
})
|
||||||
|
).data.response as Cloud66Apps[]
|
||||||
|
|
||||||
|
const apps = stacks.map((app) => ({
|
||||||
|
name: app.name,
|
||||||
|
appId: app.uid
|
||||||
|
}));
|
||||||
|
|
||||||
|
return apps;
|
||||||
|
};
|
||||||
|
|
||||||
export { getApps };
|
export { getApps };
|
||||||
|
|||||||
@@ -1,14 +1,10 @@
|
|||||||
import _ from "lodash";
|
|
||||||
import AWS from "aws-sdk";
|
|
||||||
import {
|
import {
|
||||||
CreateSecretCommand,
|
CreateSecretCommand,
|
||||||
GetSecretValueCommand,
|
GetSecretValueCommand,
|
||||||
ResourceNotFoundException,
|
ResourceNotFoundException,
|
||||||
SecretsManagerClient,
|
SecretsManagerClient,
|
||||||
UpdateSecretCommand,
|
UpdateSecretCommand
|
||||||
} from "@aws-sdk/client-secrets-manager";
|
} from "@aws-sdk/client-secrets-manager";
|
||||||
import { Octokit } from "@octokit/rest";
|
|
||||||
import sodium from "libsodium-wrappers";
|
|
||||||
import { IIntegration, IIntegrationAuth } from "../models";
|
import { IIntegration, IIntegrationAuth } from "../models";
|
||||||
import {
|
import {
|
||||||
INTEGRATION_AWS_PARAMETER_STORE,
|
INTEGRATION_AWS_PARAMETER_STORE,
|
||||||
@@ -22,8 +18,12 @@ import {
|
|||||||
INTEGRATION_CIRCLECI_API_URL,
|
INTEGRATION_CIRCLECI_API_URL,
|
||||||
INTEGRATION_CLOUDFLARE_PAGES,
|
INTEGRATION_CLOUDFLARE_PAGES,
|
||||||
INTEGRATION_CLOUDFLARE_PAGES_API_URL,
|
INTEGRATION_CLOUDFLARE_PAGES_API_URL,
|
||||||
|
INTEGRATION_CLOUD_66,
|
||||||
|
INTEGRATION_CLOUD_66_API_URL,
|
||||||
INTEGRATION_CODEFRESH,
|
INTEGRATION_CODEFRESH,
|
||||||
INTEGRATION_CODEFRESH_API_URL,
|
INTEGRATION_CODEFRESH_API_URL,
|
||||||
|
INTEGRATION_DIGITAL_OCEAN_API_URL,
|
||||||
|
INTEGRATION_DIGITAL_OCEAN_APP_PLATFORM,
|
||||||
INTEGRATION_FLYIO,
|
INTEGRATION_FLYIO,
|
||||||
INTEGRATION_FLYIO_API_URL,
|
INTEGRATION_FLYIO_API_URL,
|
||||||
INTEGRATION_GITHUB,
|
INTEGRATION_GITHUB,
|
||||||
@@ -44,11 +44,17 @@ import {
|
|||||||
INTEGRATION_RENDER_API_URL,
|
INTEGRATION_RENDER_API_URL,
|
||||||
INTEGRATION_SUPABASE,
|
INTEGRATION_SUPABASE,
|
||||||
INTEGRATION_SUPABASE_API_URL,
|
INTEGRATION_SUPABASE_API_URL,
|
||||||
|
INTEGRATION_TERRAFORM_CLOUD,
|
||||||
|
INTEGRATION_TERRAFORM_CLOUD_API_URL,
|
||||||
INTEGRATION_TRAVISCI,
|
INTEGRATION_TRAVISCI,
|
||||||
INTEGRATION_TRAVISCI_API_URL,
|
INTEGRATION_TRAVISCI_API_URL,
|
||||||
INTEGRATION_VERCEL,
|
INTEGRATION_VERCEL,
|
||||||
INTEGRATION_VERCEL_API_URL
|
INTEGRATION_VERCEL_API_URL
|
||||||
} from "../variables";
|
} from "../variables";
|
||||||
|
import AWS from "aws-sdk";
|
||||||
|
import { Octokit } from "@octokit/rest";
|
||||||
|
import _ from "lodash";
|
||||||
|
import sodium from "libsodium-wrappers";
|
||||||
import { standardRequest } from "../config/request";
|
import { standardRequest } from "../config/request";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -191,6 +197,13 @@ const syncSecrets = async ({
|
|||||||
accessToken,
|
accessToken,
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
|
case INTEGRATION_TERRAFORM_CLOUD:
|
||||||
|
await syncSecretsTerraformCloud({
|
||||||
|
integration,
|
||||||
|
secrets,
|
||||||
|
accessToken,
|
||||||
|
});
|
||||||
|
break;
|
||||||
case INTEGRATION_HASHICORP_VAULT:
|
case INTEGRATION_HASHICORP_VAULT:
|
||||||
await syncSecretsHashiCorpVault({
|
await syncSecretsHashiCorpVault({
|
||||||
integration,
|
integration,
|
||||||
@@ -222,6 +235,20 @@ const syncSecrets = async ({
|
|||||||
accessToken,
|
accessToken,
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
|
case INTEGRATION_DIGITAL_OCEAN_APP_PLATFORM:
|
||||||
|
await syncSecretsDigitalOceanAppPlatform({
|
||||||
|
integration,
|
||||||
|
secrets,
|
||||||
|
accessToken,
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case INTEGRATION_CLOUD_66:
|
||||||
|
await syncSecretsCloud66({
|
||||||
|
integration,
|
||||||
|
secrets,
|
||||||
|
accessToken
|
||||||
|
});
|
||||||
|
break;
|
||||||
case INTEGRATION_NORTHFLANK:
|
case INTEGRATION_NORTHFLANK:
|
||||||
await syncSecretsNorthflank({
|
await syncSecretsNorthflank({
|
||||||
integration,
|
integration,
|
||||||
@@ -1831,6 +1858,106 @@ const syncSecretsCheckly = async ({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sync/push [secrets] to Terraform Cloud project with id [integration.appId]
|
||||||
|
* @param {Object} obj
|
||||||
|
* @param {IIntegration} obj.integration - integration details
|
||||||
|
* @param {Object} obj.secrets - secrets to push to integration (object where keys are secret keys and values are secret values)
|
||||||
|
* @param {String} obj.accessToken - access token for Terraform Cloud API
|
||||||
|
*/
|
||||||
|
const syncSecretsTerraformCloud = async ({
|
||||||
|
integration,
|
||||||
|
secrets,
|
||||||
|
accessToken,
|
||||||
|
}: {
|
||||||
|
integration: IIntegration;
|
||||||
|
secrets: any;
|
||||||
|
accessToken: string;
|
||||||
|
}) => {
|
||||||
|
// get secrets from Terraform Cloud
|
||||||
|
const getSecretsRes = (
|
||||||
|
await standardRequest.get(`${INTEGRATION_TERRAFORM_CLOUD_API_URL}/api/v2/workspaces/${integration.appId}/vars`,
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${accessToken}`,
|
||||||
|
Accept: "application/json",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
))
|
||||||
|
.data
|
||||||
|
.data
|
||||||
|
.reduce((obj: any, secret: any) => ({
|
||||||
|
...obj,
|
||||||
|
[secret.attributes.key]: secret
|
||||||
|
}), {});
|
||||||
|
|
||||||
|
// create or update secrets on Terraform Cloud
|
||||||
|
for await (const key of Object.keys(secrets)) {
|
||||||
|
if (!(key in getSecretsRes)) {
|
||||||
|
// case: secret does not exist in Terraform Cloud
|
||||||
|
// -> add secret
|
||||||
|
await standardRequest.post(
|
||||||
|
`${INTEGRATION_TERRAFORM_CLOUD_API_URL}/api/v2/workspaces/${integration.appId}/vars`,
|
||||||
|
{
|
||||||
|
data: {
|
||||||
|
type: "vars",
|
||||||
|
attributes: {
|
||||||
|
key,
|
||||||
|
value: secrets[key],
|
||||||
|
category: integration.targetService,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${accessToken}`,
|
||||||
|
"Content-Type": "application/vnd.api+json",
|
||||||
|
Accept: "application/vnd.api+json",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// case: secret exists in Terraform Cloud
|
||||||
|
if (secrets[key] !== getSecretsRes[key].attributes.value) {
|
||||||
|
// -> update secret
|
||||||
|
await standardRequest.patch(
|
||||||
|
`${INTEGRATION_TERRAFORM_CLOUD_API_URL}/api/v2/workspaces/${integration.appId}/vars/${getSecretsRes[key].id}`,
|
||||||
|
{
|
||||||
|
data: {
|
||||||
|
type: "vars",
|
||||||
|
id: getSecretsRes[key].id,
|
||||||
|
attributes: {
|
||||||
|
...getSecretsRes[key],
|
||||||
|
value: secrets[key]
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${accessToken}`,
|
||||||
|
"Content-Type": "application/vnd.api+json",
|
||||||
|
Accept: "application/vnd.api+json",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for await (const key of Object.keys(getSecretsRes)) {
|
||||||
|
if (!(key in secrets)) {
|
||||||
|
// case: delete secret
|
||||||
|
await standardRequest.delete(`${INTEGRATION_TERRAFORM_CLOUD_API_URL}/api/v2/workspaces/${integration.appId}/vars/${getSecretsRes[key].id}`, {
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${accessToken}`,
|
||||||
|
"Content-Type": "application/vnd.api+json",
|
||||||
|
Accept: "application/vnd.api+json",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sync/push [secrets] to HashiCorp Vault path
|
* Sync/push [secrets] to HashiCorp Vault path
|
||||||
* @param {Object} obj
|
* @param {Object} obj
|
||||||
@@ -2077,10 +2204,11 @@ const syncSecretsBitBucket = async ({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/**
|
||||||
* Sync/push [secrets] to Codefresh with name [integration.app]
|
* Sync/push [secrets] to Codefresh project with name [integration.app]
|
||||||
* @param {Object} obj
|
* @param {Object} obj
|
||||||
* @param {IIntegration} obj.integration - integration details
|
* @param {IIntegration} obj.integration - integration details
|
||||||
|
* @param {IIntegrationAuth} obj.integrationAuth - integration auth details
|
||||||
* @param {Object} obj.secrets - secrets to push to integration (object where keys are secret keys and values are secret values)
|
* @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
|
* @param {String} obj.accessToken - access token for Codefresh integration
|
||||||
*/
|
*/
|
||||||
@@ -2110,6 +2238,143 @@ const syncSecretsCodefresh = async ({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sync/push [secrets] to DigitalOcean App Platform application with name [integration.app]
|
||||||
|
* @param {Object} obj
|
||||||
|
* @param {IIntegration} obj.integration - integration details
|
||||||
|
* @param {IIntegrationAuth} obj.integrationAuth - integration auth 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 - personal access token for DigitalOcean
|
||||||
|
*/
|
||||||
|
const syncSecretsDigitalOceanAppPlatform = async ({
|
||||||
|
integration,
|
||||||
|
secrets,
|
||||||
|
accessToken
|
||||||
|
}: {
|
||||||
|
integration: IIntegration;
|
||||||
|
secrets: any;
|
||||||
|
accessToken: string;
|
||||||
|
}) => {
|
||||||
|
await standardRequest.put(
|
||||||
|
`${INTEGRATION_DIGITAL_OCEAN_API_URL}/v2/apps/${integration.appId}`,
|
||||||
|
{
|
||||||
|
spec: {
|
||||||
|
name: integration.app,
|
||||||
|
envs: Object.entries(secrets).map(([key, value]) => ({ key, value }))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${accessToken}`,
|
||||||
|
Accept: "application/json"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sync/push [secrets] to Cloud66 application with name [integration.app]
|
||||||
|
* @param {Object} obj
|
||||||
|
* @param {IIntegration} obj.integration - integration details
|
||||||
|
* @param {IIntegrationAuth} obj.integrationAuth - integration auth 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 Cloud66 integration
|
||||||
|
*/
|
||||||
|
const syncSecretsCloud66 = async ({
|
||||||
|
integration,
|
||||||
|
secrets,
|
||||||
|
accessToken
|
||||||
|
}: {
|
||||||
|
integration: IIntegration;
|
||||||
|
secrets: any;
|
||||||
|
accessToken: string;
|
||||||
|
}) => {
|
||||||
|
|
||||||
|
interface Cloud66Secret {
|
||||||
|
id: number;
|
||||||
|
key: string;
|
||||||
|
value: string;
|
||||||
|
readonly: boolean;
|
||||||
|
created_at: string;
|
||||||
|
updated_at: string;
|
||||||
|
is_password: boolean;
|
||||||
|
is_generated: boolean;
|
||||||
|
history: any[];
|
||||||
|
}
|
||||||
|
|
||||||
|
// get all current secrets
|
||||||
|
const res = (
|
||||||
|
await standardRequest.get(
|
||||||
|
`${INTEGRATION_CLOUD_66_API_URL}/3/stacks/${integration.appId}/environments`,
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${accessToken}`,
|
||||||
|
Accept: "application/json"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.data
|
||||||
|
.response
|
||||||
|
.filter((secret: Cloud66Secret) => !secret.readonly || !secret.is_generated)
|
||||||
|
.reduce(
|
||||||
|
(obj: any, secret: any) => ({
|
||||||
|
...obj,
|
||||||
|
[secret.key]: secret
|
||||||
|
}),
|
||||||
|
{}
|
||||||
|
);
|
||||||
|
|
||||||
|
for await (const key of Object.keys(secrets)) {
|
||||||
|
if (key in res) {
|
||||||
|
// update existing secret
|
||||||
|
await standardRequest.put(
|
||||||
|
`${INTEGRATION_CLOUD_66_API_URL}/3/stacks/${integration.appId}/environments/${key}`,
|
||||||
|
{
|
||||||
|
key,
|
||||||
|
value: secrets[key]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${accessToken}`,
|
||||||
|
Accept: "application/json"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// create new secret
|
||||||
|
await standardRequest.post(
|
||||||
|
`${INTEGRATION_CLOUD_66_API_URL}/3/stacks/${integration.appId}/environments`,
|
||||||
|
{
|
||||||
|
key,
|
||||||
|
value: secrets[key]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${accessToken}`,
|
||||||
|
Accept: "application/json"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for await (const key of Object.keys(res)) {
|
||||||
|
if (!(key in secrets)) {
|
||||||
|
// delete secret
|
||||||
|
await standardRequest.delete(
|
||||||
|
`${INTEGRATION_CLOUD_66_API_URL}/3/stacks/${integration.appId}/environments/${key}`,
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${accessToken}`,
|
||||||
|
Accept: "application/json"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
/* Sync/push [secrets] to Northflank
|
/* Sync/push [secrets] to Northflank
|
||||||
* @param {Object} obj
|
* @param {Object} obj
|
||||||
* @param {IIntegration} obj.integration - integration details
|
* @param {IIntegration} obj.integration - integration details
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ const requireWorkspaceAuth = ({
|
|||||||
requiredPermissions = [],
|
requiredPermissions = [],
|
||||||
requireBlindIndicesEnabled = false,
|
requireBlindIndicesEnabled = false,
|
||||||
requireE2EEOff = false,
|
requireE2EEOff = false,
|
||||||
|
checkIPAllowlist = false
|
||||||
}: {
|
}: {
|
||||||
acceptedRoles: Array<"admin" | "member">;
|
acceptedRoles: Array<"admin" | "member">;
|
||||||
locationWorkspaceId: req;
|
locationWorkspaceId: req;
|
||||||
@@ -25,6 +26,7 @@ const requireWorkspaceAuth = ({
|
|||||||
requiredPermissions?: string[];
|
requiredPermissions?: string[];
|
||||||
requireBlindIndicesEnabled?: boolean;
|
requireBlindIndicesEnabled?: boolean;
|
||||||
requireE2EEOff?: boolean;
|
requireE2EEOff?: boolean;
|
||||||
|
checkIPAllowlist?: boolean;
|
||||||
}) => {
|
}) => {
|
||||||
return async (req: Request, res: Response, next: NextFunction) => {
|
return async (req: Request, res: Response, next: NextFunction) => {
|
||||||
const workspaceId = req[locationWorkspaceId]?.workspaceId;
|
const workspaceId = req[locationWorkspaceId]?.workspaceId;
|
||||||
@@ -39,6 +41,7 @@ const requireWorkspaceAuth = ({
|
|||||||
requiredPermissions,
|
requiredPermissions,
|
||||||
requireBlindIndicesEnabled,
|
requireBlindIndicesEnabled,
|
||||||
requireE2EEOff,
|
requireE2EEOff,
|
||||||
|
checkIPAllowlist
|
||||||
});
|
});
|
||||||
|
|
||||||
if (membership) {
|
if (membership) {
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { Schema, Types, model } from "mongoose";
|
|
||||||
import {
|
import {
|
||||||
INTEGRATION_AWS_PARAMETER_STORE,
|
INTEGRATION_AWS_PARAMETER_STORE,
|
||||||
INTEGRATION_AWS_SECRET_MANAGER,
|
INTEGRATION_AWS_SECRET_MANAGER,
|
||||||
@@ -7,7 +6,9 @@ import {
|
|||||||
INTEGRATION_CHECKLY,
|
INTEGRATION_CHECKLY,
|
||||||
INTEGRATION_CIRCLECI,
|
INTEGRATION_CIRCLECI,
|
||||||
INTEGRATION_CLOUDFLARE_PAGES,
|
INTEGRATION_CLOUDFLARE_PAGES,
|
||||||
|
INTEGRATION_CLOUD_66,
|
||||||
INTEGRATION_CODEFRESH,
|
INTEGRATION_CODEFRESH,
|
||||||
|
INTEGRATION_DIGITAL_OCEAN_APP_PLATFORM,
|
||||||
INTEGRATION_FLYIO,
|
INTEGRATION_FLYIO,
|
||||||
INTEGRATION_GITHUB,
|
INTEGRATION_GITHUB,
|
||||||
INTEGRATION_GITLAB,
|
INTEGRATION_GITLAB,
|
||||||
@@ -19,9 +20,11 @@ import {
|
|||||||
INTEGRATION_RAILWAY,
|
INTEGRATION_RAILWAY,
|
||||||
INTEGRATION_RENDER,
|
INTEGRATION_RENDER,
|
||||||
INTEGRATION_SUPABASE,
|
INTEGRATION_SUPABASE,
|
||||||
|
INTEGRATION_TERRAFORM_CLOUD,
|
||||||
INTEGRATION_TRAVISCI,
|
INTEGRATION_TRAVISCI,
|
||||||
INTEGRATION_VERCEL
|
INTEGRATION_VERCEL
|
||||||
} from "../variables";
|
} from "../variables";
|
||||||
|
import { Schema, Types, model } from "mongoose";
|
||||||
|
|
||||||
export interface IIntegration {
|
export interface IIntegration {
|
||||||
_id: Types.ObjectId;
|
_id: Types.ObjectId;
|
||||||
@@ -57,10 +60,13 @@ export interface IIntegration {
|
|||||||
| "travisci"
|
| "travisci"
|
||||||
| "supabase"
|
| "supabase"
|
||||||
| "checkly"
|
| "checkly"
|
||||||
|
| "terraform-cloud"
|
||||||
| "hashicorp-vault"
|
| "hashicorp-vault"
|
||||||
| "cloudflare-pages"
|
| "cloudflare-pages"
|
||||||
| "bitbucket"
|
| "bitbucket"
|
||||||
| "codefresh"
|
| "codefresh"
|
||||||
|
| "digital-ocean-app-platform"
|
||||||
|
| "cloud-66"
|
||||||
| "northflank"
|
| "northflank"
|
||||||
integrationAuth: Types.ObjectId;
|
integrationAuth: Types.ObjectId;
|
||||||
}
|
}
|
||||||
@@ -149,10 +155,13 @@ const integrationSchema = new Schema<IIntegration>(
|
|||||||
INTEGRATION_TRAVISCI,
|
INTEGRATION_TRAVISCI,
|
||||||
INTEGRATION_SUPABASE,
|
INTEGRATION_SUPABASE,
|
||||||
INTEGRATION_CHECKLY,
|
INTEGRATION_CHECKLY,
|
||||||
|
INTEGRATION_TERRAFORM_CLOUD,
|
||||||
INTEGRATION_HASHICORP_VAULT,
|
INTEGRATION_HASHICORP_VAULT,
|
||||||
INTEGRATION_CLOUDFLARE_PAGES,
|
INTEGRATION_CLOUDFLARE_PAGES,
|
||||||
INTEGRATION_BITBUCKET,
|
INTEGRATION_BITBUCKET,
|
||||||
|
INTEGRATION_DIGITAL_OCEAN_APP_PLATFORM,
|
||||||
INTEGRATION_CODEFRESH,
|
INTEGRATION_CODEFRESH,
|
||||||
|
INTEGRATION_CLOUD_66,
|
||||||
INTEGRATION_NORTHFLANK
|
INTEGRATION_NORTHFLANK
|
||||||
],
|
],
|
||||||
required: true,
|
required: true,
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { Document, Schema, Types, model } from "mongoose";
|
|
||||||
import {
|
import {
|
||||||
ALGORITHM_AES_256_GCM,
|
ALGORITHM_AES_256_GCM,
|
||||||
ENCODING_SCHEME_BASE64,
|
ENCODING_SCHEME_BASE64,
|
||||||
@@ -9,7 +8,9 @@ import {
|
|||||||
INTEGRATION_BITBUCKET,
|
INTEGRATION_BITBUCKET,
|
||||||
INTEGRATION_CIRCLECI,
|
INTEGRATION_CIRCLECI,
|
||||||
INTEGRATION_CLOUDFLARE_PAGES,
|
INTEGRATION_CLOUDFLARE_PAGES,
|
||||||
|
INTEGRATION_CLOUD_66,
|
||||||
INTEGRATION_CODEFRESH,
|
INTEGRATION_CODEFRESH,
|
||||||
|
INTEGRATION_DIGITAL_OCEAN_APP_PLATFORM,
|
||||||
INTEGRATION_FLYIO,
|
INTEGRATION_FLYIO,
|
||||||
INTEGRATION_GITHUB,
|
INTEGRATION_GITHUB,
|
||||||
INTEGRATION_GITLAB,
|
INTEGRATION_GITLAB,
|
||||||
@@ -21,9 +22,11 @@ import {
|
|||||||
INTEGRATION_RAILWAY,
|
INTEGRATION_RAILWAY,
|
||||||
INTEGRATION_RENDER,
|
INTEGRATION_RENDER,
|
||||||
INTEGRATION_SUPABASE,
|
INTEGRATION_SUPABASE,
|
||||||
|
INTEGRATION_TERRAFORM_CLOUD,
|
||||||
INTEGRATION_TRAVISCI,
|
INTEGRATION_TRAVISCI,
|
||||||
INTEGRATION_VERCEL
|
INTEGRATION_VERCEL
|
||||||
} from "../variables";
|
} from "../variables";
|
||||||
|
import { Document, Schema, Types, model } from "mongoose";
|
||||||
|
|
||||||
export interface IIntegrationAuth extends Document {
|
export interface IIntegrationAuth extends Document {
|
||||||
_id: Types.ObjectId;
|
_id: Types.ObjectId;
|
||||||
@@ -47,7 +50,10 @@ export interface IIntegrationAuth extends Document {
|
|||||||
| "checkly"
|
| "checkly"
|
||||||
| "cloudflare-pages"
|
| "cloudflare-pages"
|
||||||
| "codefresh"
|
| "codefresh"
|
||||||
|
| "digital-ocean-app-platform"
|
||||||
| "bitbucket"
|
| "bitbucket"
|
||||||
|
| "cloud-66"
|
||||||
|
| "terraform-cloud"
|
||||||
| "northflank";
|
| "northflank";
|
||||||
teamId: string;
|
teamId: string;
|
||||||
accountId: string;
|
accountId: string;
|
||||||
@@ -92,10 +98,13 @@ const integrationAuthSchema = new Schema<IIntegrationAuth>(
|
|||||||
INTEGRATION_LARAVELFORGE,
|
INTEGRATION_LARAVELFORGE,
|
||||||
INTEGRATION_TRAVISCI,
|
INTEGRATION_TRAVISCI,
|
||||||
INTEGRATION_SUPABASE,
|
INTEGRATION_SUPABASE,
|
||||||
|
INTEGRATION_TERRAFORM_CLOUD,
|
||||||
INTEGRATION_HASHICORP_VAULT,
|
INTEGRATION_HASHICORP_VAULT,
|
||||||
INTEGRATION_CLOUDFLARE_PAGES,
|
INTEGRATION_CLOUDFLARE_PAGES,
|
||||||
INTEGRATION_BITBUCKET,
|
INTEGRATION_BITBUCKET,
|
||||||
|
INTEGRATION_DIGITAL_OCEAN_APP_PLATFORM,
|
||||||
INTEGRATION_CODEFRESH,
|
INTEGRATION_CODEFRESH,
|
||||||
|
INTEGRATION_CLOUD_66,
|
||||||
INTEGRATION_NORTHFLANK
|
INTEGRATION_NORTHFLANK
|
||||||
],
|
],
|
||||||
required: true,
|
required: true,
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ router.delete(
|
|||||||
body("secretImportPath").isString().exists().trim(),
|
body("secretImportPath").isString().exists().trim(),
|
||||||
body("secretImportEnv").isString().exists().trim(),
|
body("secretImportEnv").isString().exists().trim(),
|
||||||
validateRequest,
|
validateRequest,
|
||||||
secretImportController.updateSecretImport
|
secretImportController.deleteSecretImport
|
||||||
);
|
);
|
||||||
|
|
||||||
router.get(
|
router.get(
|
||||||
|
|||||||
@@ -93,7 +93,7 @@ router.delete(
|
|||||||
usersController.deleteAPIKey
|
usersController.deleteAPIKey
|
||||||
);
|
);
|
||||||
|
|
||||||
router.get( // new
|
router.get(
|
||||||
"/me/sessions",
|
"/me/sessions",
|
||||||
requireAuth({
|
requireAuth({
|
||||||
acceptedAuthModes: [AUTH_MODE_JWT],
|
acceptedAuthModes: [AUTH_MODE_JWT],
|
||||||
@@ -101,7 +101,7 @@ router.get( // new
|
|||||||
usersController.getMySessions
|
usersController.getMySessions
|
||||||
);
|
);
|
||||||
|
|
||||||
router.delete( // new
|
router.delete(
|
||||||
"/me/sessions",
|
"/me/sessions",
|
||||||
requireAuth({
|
requireAuth({
|
||||||
acceptedAuthModes: [AUTH_MODE_JWT],
|
acceptedAuthModes: [AUTH_MODE_JWT],
|
||||||
|
|||||||
@@ -18,8 +18,8 @@ import {
|
|||||||
|
|
||||||
router.get(
|
router.get(
|
||||||
"/raw",
|
"/raw",
|
||||||
query("workspaceId").exists().isString().trim(),
|
query("workspaceId").optional().isString().trim(),
|
||||||
query("environment").exists().isString().trim(),
|
query("environment").optional().isString().trim(),
|
||||||
query("secretPath").default("/").isString().trim(),
|
query("secretPath").default("/").isString().trim(),
|
||||||
query("include_imports").optional().isBoolean().default(false),
|
query("include_imports").optional().isBoolean().default(false),
|
||||||
validateRequest,
|
validateRequest,
|
||||||
@@ -31,14 +31,6 @@ router.get(
|
|||||||
AUTH_MODE_SERVICE_ACCOUNT
|
AUTH_MODE_SERVICE_ACCOUNT
|
||||||
]
|
]
|
||||||
}),
|
}),
|
||||||
requireWorkspaceAuth({
|
|
||||||
acceptedRoles: [ADMIN, MEMBER],
|
|
||||||
locationWorkspaceId: "query",
|
|
||||||
locationEnvironment: "query",
|
|
||||||
requiredPermissions: [PERMISSION_READ_SECRETS],
|
|
||||||
requireBlindIndicesEnabled: true,
|
|
||||||
requireE2EEOff: true
|
|
||||||
}),
|
|
||||||
secretsController.getSecretsRaw
|
secretsController.getSecretsRaw
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -64,7 +56,8 @@ router.get(
|
|||||||
locationEnvironment: "query",
|
locationEnvironment: "query",
|
||||||
requiredPermissions: [PERMISSION_READ_SECRETS],
|
requiredPermissions: [PERMISSION_READ_SECRETS],
|
||||||
requireBlindIndicesEnabled: true,
|
requireBlindIndicesEnabled: true,
|
||||||
requireE2EEOff: true
|
requireE2EEOff: true,
|
||||||
|
checkIPAllowlist: true
|
||||||
}),
|
}),
|
||||||
secretsController.getSecretByNameRaw
|
secretsController.getSecretByNameRaw
|
||||||
);
|
);
|
||||||
@@ -92,7 +85,8 @@ router.post(
|
|||||||
locationEnvironment: "body",
|
locationEnvironment: "body",
|
||||||
requiredPermissions: [PERMISSION_WRITE_SECRETS],
|
requiredPermissions: [PERMISSION_WRITE_SECRETS],
|
||||||
requireBlindIndicesEnabled: true,
|
requireBlindIndicesEnabled: true,
|
||||||
requireE2EEOff: true
|
requireE2EEOff: true,
|
||||||
|
checkIPAllowlist: true
|
||||||
}),
|
}),
|
||||||
secretsController.createSecretRaw
|
secretsController.createSecretRaw
|
||||||
);
|
);
|
||||||
@@ -120,7 +114,8 @@ router.patch(
|
|||||||
locationEnvironment: "body",
|
locationEnvironment: "body",
|
||||||
requiredPermissions: [PERMISSION_WRITE_SECRETS],
|
requiredPermissions: [PERMISSION_WRITE_SECRETS],
|
||||||
requireBlindIndicesEnabled: true,
|
requireBlindIndicesEnabled: true,
|
||||||
requireE2EEOff: true
|
requireE2EEOff: true,
|
||||||
|
checkIPAllowlist: true
|
||||||
}),
|
}),
|
||||||
secretsController.updateSecretByNameRaw
|
secretsController.updateSecretByNameRaw
|
||||||
);
|
);
|
||||||
@@ -147,7 +142,8 @@ router.delete(
|
|||||||
locationEnvironment: "body",
|
locationEnvironment: "body",
|
||||||
requiredPermissions: [PERMISSION_WRITE_SECRETS],
|
requiredPermissions: [PERMISSION_WRITE_SECRETS],
|
||||||
requireBlindIndicesEnabled: true,
|
requireBlindIndicesEnabled: true,
|
||||||
requireE2EEOff: true
|
requireE2EEOff: true,
|
||||||
|
checkIPAllowlist: true
|
||||||
}),
|
}),
|
||||||
secretsController.deleteSecretByNameRaw
|
secretsController.deleteSecretByNameRaw
|
||||||
);
|
);
|
||||||
@@ -172,7 +168,8 @@ router.get(
|
|||||||
locationEnvironment: "query",
|
locationEnvironment: "query",
|
||||||
requiredPermissions: [PERMISSION_READ_SECRETS],
|
requiredPermissions: [PERMISSION_READ_SECRETS],
|
||||||
requireBlindIndicesEnabled: true,
|
requireBlindIndicesEnabled: true,
|
||||||
requireE2EEOff: false
|
requireE2EEOff: false,
|
||||||
|
checkIPAllowlist: true
|
||||||
}),
|
}),
|
||||||
secretsController.getSecrets
|
secretsController.getSecrets
|
||||||
);
|
);
|
||||||
@@ -207,7 +204,8 @@ router.post(
|
|||||||
locationEnvironment: "body",
|
locationEnvironment: "body",
|
||||||
requiredPermissions: [PERMISSION_WRITE_SECRETS],
|
requiredPermissions: [PERMISSION_WRITE_SECRETS],
|
||||||
requireBlindIndicesEnabled: true,
|
requireBlindIndicesEnabled: true,
|
||||||
requireE2EEOff: false
|
requireE2EEOff: false,
|
||||||
|
checkIPAllowlist: true
|
||||||
}),
|
}),
|
||||||
secretsController.createSecret
|
secretsController.createSecret
|
||||||
);
|
);
|
||||||
@@ -233,7 +231,8 @@ router.get(
|
|||||||
locationWorkspaceId: "query",
|
locationWorkspaceId: "query",
|
||||||
locationEnvironment: "query",
|
locationEnvironment: "query",
|
||||||
requiredPermissions: [PERMISSION_READ_SECRETS],
|
requiredPermissions: [PERMISSION_READ_SECRETS],
|
||||||
requireBlindIndicesEnabled: true
|
requireBlindIndicesEnabled: true,
|
||||||
|
checkIPAllowlist: true
|
||||||
}),
|
}),
|
||||||
secretsController.getSecretByName
|
secretsController.getSecretByName
|
||||||
);
|
);
|
||||||
@@ -263,7 +262,8 @@ router.patch(
|
|||||||
locationEnvironment: "body",
|
locationEnvironment: "body",
|
||||||
requiredPermissions: [PERMISSION_WRITE_SECRETS],
|
requiredPermissions: [PERMISSION_WRITE_SECRETS],
|
||||||
requireBlindIndicesEnabled: true,
|
requireBlindIndicesEnabled: true,
|
||||||
requireE2EEOff: false
|
requireE2EEOff: false,
|
||||||
|
checkIPAllowlist: true
|
||||||
}),
|
}),
|
||||||
secretsController.updateSecretByName
|
secretsController.updateSecretByName
|
||||||
);
|
);
|
||||||
@@ -290,7 +290,8 @@ router.delete(
|
|||||||
locationEnvironment: "body",
|
locationEnvironment: "body",
|
||||||
requiredPermissions: [PERMISSION_WRITE_SECRETS],
|
requiredPermissions: [PERMISSION_WRITE_SECRETS],
|
||||||
requireBlindIndicesEnabled: true,
|
requireBlindIndicesEnabled: true,
|
||||||
requireE2EEOff: false
|
requireE2EEOff: false,
|
||||||
|
checkIPAllowlist: true
|
||||||
}),
|
}),
|
||||||
secretsController.deleteSecretByName
|
secretsController.deleteSecretByName
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -114,7 +114,10 @@ const initializePassport = async () => {
|
|||||||
firstName,
|
firstName,
|
||||||
lastName,
|
lastName,
|
||||||
authProvider: user.authProvider,
|
authProvider: user.authProvider,
|
||||||
isUserCompleted
|
isUserCompleted,
|
||||||
|
...(req.query.state ? {
|
||||||
|
callbackPort: req.query.state as string
|
||||||
|
} : {})
|
||||||
},
|
},
|
||||||
expiresIn: await getJwtProviderAuthLifetime(),
|
expiresIn: await getJwtProviderAuthLifetime(),
|
||||||
secret: await getJwtProviderAuthSecret(),
|
secret: await getJwtProviderAuthSecret(),
|
||||||
@@ -153,7 +156,6 @@ const initializePassport = async () => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
async (req: any, profile: any, done: any) => {
|
async (req: any, profile: any, done: any) => {
|
||||||
|
|
||||||
if (!req.ssoConfig.isActive) return done(InternalServerError());
|
if (!req.ssoConfig.isActive) return done(InternalServerError());
|
||||||
|
|
||||||
const organization = await Organization.findById(req.ssoConfig.organization);
|
const organization = await Organization.findById(req.ssoConfig.organization);
|
||||||
@@ -199,7 +201,10 @@ const initializePassport = async () => {
|
|||||||
lastName,
|
lastName,
|
||||||
organizationName: organization?.name,
|
organizationName: organization?.name,
|
||||||
authProvider: user.authProvider,
|
authProvider: user.authProvider,
|
||||||
isUserCompleted
|
isUserCompleted,
|
||||||
|
...(req.body.RelayState ? {
|
||||||
|
callbackPort: req.body.RelayState as string
|
||||||
|
} : {})
|
||||||
},
|
},
|
||||||
expiresIn: await getJwtProviderAuthLifetime(),
|
expiresIn: await getJwtProviderAuthLifetime(),
|
||||||
secret: await getJwtProviderAuthSecret(),
|
secret: await getJwtProviderAuthSecret(),
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
export * from "./ip";
|
||||||
@@ -0,0 +1,101 @@
|
|||||||
|
import net from "net";
|
||||||
|
import { IPType } from "../../ee/models";
|
||||||
|
import { InternalServerError } from "../errors";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return details of IP [ip]:
|
||||||
|
* - If [ip] is a specific IP address then return the IPv4/IPv6 address
|
||||||
|
* - If [ip] is a subnet then return the network IPv4/IPv6 address and prefix
|
||||||
|
* @param {String} ip - ip whose details to return
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export const extractIPDetails = (ip: string) => {
|
||||||
|
if (net.isIPv4(ip)) return ({
|
||||||
|
ipAddress: ip,
|
||||||
|
type: IPType.IPV4
|
||||||
|
});
|
||||||
|
|
||||||
|
if (net.isIPv6(ip)) return ({
|
||||||
|
ipAddress: ip,
|
||||||
|
type: IPType.IPV6
|
||||||
|
});
|
||||||
|
|
||||||
|
const [ipNet, prefix] = ip.split("/");
|
||||||
|
|
||||||
|
let type;
|
||||||
|
switch (net.isIP(ipNet)) {
|
||||||
|
case 4:
|
||||||
|
type = IPType.IPV4;
|
||||||
|
break;
|
||||||
|
case 6:
|
||||||
|
type = IPType.IPV6;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw InternalServerError({
|
||||||
|
message: "Failed to extract IP details"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return ({
|
||||||
|
ipAddress: ipNet,
|
||||||
|
type,
|
||||||
|
prefix: parseInt(prefix, 10)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if a given string is a valid CIDR block.
|
||||||
|
*
|
||||||
|
* The function checks if the input string is a valid IPv4 or IPv6 address in CIDR notation.
|
||||||
|
*
|
||||||
|
* CIDR notation includes a network address followed by a slash ('/') and a prefix length.
|
||||||
|
* For IPv4, the prefix length must be between 0 and 32. For IPv6, it must be between 0 and 128.
|
||||||
|
* If the input string is not a valid CIDR block, the function returns `false`.
|
||||||
|
*
|
||||||
|
* @param {string} cidr - string in CIDR notation
|
||||||
|
* @returns {boolean} Returns `true` if the string is a valid CIDR block, `false` otherwise.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
export const isValidCidr = (cidr: string): boolean => {
|
||||||
|
const [ip, prefix] = cidr.split("/");
|
||||||
|
|
||||||
|
const prefixNum = parseInt(prefix, 10);
|
||||||
|
|
||||||
|
// ensure prefix exists and is a number within the appropriate range for each IP version
|
||||||
|
if (!prefix || isNaN(prefixNum) ||
|
||||||
|
(net.isIPv4(ip) && (prefixNum < 0 || prefixNum > 32)) ||
|
||||||
|
(net.isIPv6(ip) && (prefixNum < 0 || prefixNum > 128))) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ensure the IP portion of the CIDR block is a valid IPv4 or IPv6 address
|
||||||
|
if (!net.isIPv4(ip) && !net.isIPv6(ip)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if a given string is a valid IPv4/IPv6 address or a valid CIDR block.
|
||||||
|
*
|
||||||
|
* If the string contains a slash ('/'), it treats the input as a CIDR block and checks its validity.
|
||||||
|
* Otherwise, it treats the string as a standalone IP address (either IPv4 or IPv6) and checks its validity.
|
||||||
|
*
|
||||||
|
* @param {string} input - The string to be checked. It could be an IP address or a CIDR block.
|
||||||
|
* @returns {boolean} Returns `true` if the string is a valid IP address (either IPv4 or IPv6) or a valid CIDR block, `false` otherwise.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
export const isValidIpOrCidr = (ip: string): boolean => {
|
||||||
|
// if the string contains a slash, treat it as a CIDR block
|
||||||
|
if (ip.includes("/")) {
|
||||||
|
return isValidCidr(ip);
|
||||||
|
}
|
||||||
|
|
||||||
|
// otherwise, treat it as a standalone IP address
|
||||||
|
if (net.isIPv4(ip) || net.isIPv6(ip)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
@@ -3,7 +3,13 @@ import crypto from "crypto";
|
|||||||
import { Types } from "mongoose";
|
import { Types } from "mongoose";
|
||||||
import { encryptSymmetric128BitHexKeyUTF8 } from "../crypto";
|
import { encryptSymmetric128BitHexKeyUTF8 } from "../crypto";
|
||||||
import { EESecretService } from "../../ee/services";
|
import { EESecretService } from "../../ee/services";
|
||||||
import { ISecretVersion, SecretSnapshot, SecretVersion } from "../../ee/models";
|
import {
|
||||||
|
IPType,
|
||||||
|
ISecretVersion,
|
||||||
|
SecretSnapshot,
|
||||||
|
SecretVersion,
|
||||||
|
TrustedIP
|
||||||
|
} from "../../ee/models";
|
||||||
import {
|
import {
|
||||||
BackupPrivateKey,
|
BackupPrivateKey,
|
||||||
Bot,
|
Bot,
|
||||||
@@ -177,7 +183,6 @@ export const backfillBotOrgs = async () => {
|
|||||||
return new BotOrg({
|
return new BotOrg({
|
||||||
name: "Infisical Bot",
|
name: "Infisical Bot",
|
||||||
organization: organizationToAddBot,
|
organization: organizationToAddBot,
|
||||||
isActive: false,
|
|
||||||
publicKey,
|
publicKey,
|
||||||
encryptedSymmetricKey,
|
encryptedSymmetricKey,
|
||||||
symmetricKeyIV,
|
symmetricKeyIV,
|
||||||
@@ -212,7 +217,6 @@ export const backfillBotOrgs = async () => {
|
|||||||
return new BotOrg({
|
return new BotOrg({
|
||||||
name: "Infisical Bot",
|
name: "Infisical Bot",
|
||||||
organization: organizationToAddBot,
|
organization: organizationToAddBot,
|
||||||
isActive: false,
|
|
||||||
publicKey,
|
publicKey,
|
||||||
encryptedSymmetricKey,
|
encryptedSymmetricKey,
|
||||||
symmetricKeyIV,
|
symmetricKeyIV,
|
||||||
@@ -551,3 +555,41 @@ export const backfillServiceTokenMultiScope = async () => {
|
|||||||
|
|
||||||
console.log("Migration: Service token migration v2 complete");
|
console.log("Migration: Service token migration v2 complete");
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Backfill each workspace without any registered trusted IPs to
|
||||||
|
* have default trusted ip of 0.0.0.0/0
|
||||||
|
*/
|
||||||
|
export const backfillTrustedIps = async () => {
|
||||||
|
const workspaceIdsWithTrustedIps = await TrustedIP.distinct("workspace");
|
||||||
|
const workspaceIdsToAddTrustedIp = await Workspace.distinct("_id", {
|
||||||
|
_id: {
|
||||||
|
$nin: workspaceIdsWithTrustedIps
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (workspaceIdsToAddTrustedIp.length > 0) {
|
||||||
|
const operations = workspaceIdsToAddTrustedIp.map((workspaceId) => {
|
||||||
|
return {
|
||||||
|
updateOne: {
|
||||||
|
filter: {
|
||||||
|
workspace: workspaceId,
|
||||||
|
ipAddress: "0.0.0.0"
|
||||||
|
},
|
||||||
|
update: {
|
||||||
|
workspace: workspaceId,
|
||||||
|
ipAddress: "0.0.0.0",
|
||||||
|
type: IPType.IPV4.toString(),
|
||||||
|
prefix: 0,
|
||||||
|
isActive: true,
|
||||||
|
comment: ""
|
||||||
|
},
|
||||||
|
upsert: true,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
await TrustedIP.bulkWrite(operations);
|
||||||
|
console.log("Backfill: Trusted IPs complete");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -15,7 +15,8 @@ import {
|
|||||||
backfillSecretFolders,
|
backfillSecretFolders,
|
||||||
backfillSecretVersions,
|
backfillSecretVersions,
|
||||||
backfillServiceToken,
|
backfillServiceToken,
|
||||||
backfillServiceTokenMultiScope
|
backfillServiceTokenMultiScope,
|
||||||
|
backfillTrustedIps
|
||||||
} from "./backfillData";
|
} from "./backfillData";
|
||||||
import {
|
import {
|
||||||
reencryptBotOrgKeys,
|
reencryptBotOrgKeys,
|
||||||
@@ -84,6 +85,7 @@ export const setup = async () => {
|
|||||||
await backfillServiceToken();
|
await backfillServiceToken();
|
||||||
await backfillIntegration();
|
await backfillIntegration();
|
||||||
await backfillServiceTokenMultiScope();
|
await backfillServiceTokenMultiScope();
|
||||||
|
await backfillTrustedIps();
|
||||||
|
|
||||||
// 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
|
||||||
|
|||||||
@@ -1,14 +1,15 @@
|
|||||||
|
import net from "net";
|
||||||
import { Types } from "mongoose";
|
import { Types } from "mongoose";
|
||||||
import {
|
import {
|
||||||
IServiceAccount,
|
|
||||||
IServiceTokenData,
|
|
||||||
IUser,
|
|
||||||
SecretBlindIndexData,
|
SecretBlindIndexData,
|
||||||
ServiceAccount,
|
ServiceAccount,
|
||||||
ServiceTokenData,
|
ServiceTokenData,
|
||||||
User,
|
User,
|
||||||
Workspace,
|
Workspace,
|
||||||
} from "../models";
|
} from "../models";
|
||||||
|
import {
|
||||||
|
TrustedIP
|
||||||
|
} from "../ee/models";
|
||||||
import { validateServiceAccountClientForWorkspace } from "./serviceAccount";
|
import { validateServiceAccountClientForWorkspace } from "./serviceAccount";
|
||||||
import { validateUserClientForWorkspace } from "./user";
|
import { validateUserClientForWorkspace } from "./user";
|
||||||
import { validateServiceTokenDataClientForWorkspace } from "./serviceTokenData";
|
import { validateServiceTokenDataClientForWorkspace } from "./serviceTokenData";
|
||||||
@@ -24,6 +25,7 @@ import {
|
|||||||
AUTH_MODE_SERVICE_TOKEN,
|
AUTH_MODE_SERVICE_TOKEN,
|
||||||
} from "../variables";
|
} from "../variables";
|
||||||
import { BotService } from "../services";
|
import { BotService } from "../services";
|
||||||
|
import { AuthData } from "../interfaces/middleware";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Validate authenticated clients for workspace with id [workspaceId] based
|
* Validate authenticated clients for workspace with id [workspaceId] based
|
||||||
@@ -43,17 +45,16 @@ export const validateClientForWorkspace = async ({
|
|||||||
requiredPermissions,
|
requiredPermissions,
|
||||||
requireBlindIndicesEnabled,
|
requireBlindIndicesEnabled,
|
||||||
requireE2EEOff,
|
requireE2EEOff,
|
||||||
|
checkIPAllowlist
|
||||||
}: {
|
}: {
|
||||||
authData: {
|
authData: AuthData;
|
||||||
authMode: string;
|
|
||||||
authPayload: IUser | IServiceAccount | IServiceTokenData;
|
|
||||||
};
|
|
||||||
workspaceId: Types.ObjectId;
|
workspaceId: Types.ObjectId;
|
||||||
environment?: string;
|
environment?: string;
|
||||||
acceptedRoles: Array<"admin" | "member">;
|
acceptedRoles: Array<"admin" | "member">;
|
||||||
requiredPermissions?: string[];
|
requiredPermissions?: string[];
|
||||||
requireBlindIndicesEnabled: boolean;
|
requireBlindIndicesEnabled: boolean;
|
||||||
requireE2EEOff: boolean;
|
requireE2EEOff: boolean;
|
||||||
|
checkIPAllowlist: boolean;
|
||||||
}) => {
|
}) => {
|
||||||
const workspace = await Workspace.findById(workspaceId);
|
const workspace = await Workspace.findById(workspaceId);
|
||||||
|
|
||||||
@@ -83,6 +84,8 @@ export const validateClientForWorkspace = async ({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
if (authData.authMode === AUTH_MODE_JWT && authData.authPayload instanceof User) {
|
if (authData.authMode === AUTH_MODE_JWT && authData.authPayload instanceof User) {
|
||||||
const membership = await validateUserClientForWorkspace({
|
const membership = await validateUserClientForWorkspace({
|
||||||
user: authData.authPayload,
|
user: authData.authPayload,
|
||||||
@@ -107,6 +110,39 @@ export const validateClientForWorkspace = async ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (authData.authMode === AUTH_MODE_SERVICE_TOKEN && authData.authPayload instanceof ServiceTokenData) {
|
if (authData.authMode === AUTH_MODE_SERVICE_TOKEN && authData.authPayload instanceof ServiceTokenData) {
|
||||||
|
if (checkIPAllowlist) {
|
||||||
|
const trustedIps = await TrustedIP.find({
|
||||||
|
workspace: workspaceId
|
||||||
|
});
|
||||||
|
|
||||||
|
if (trustedIps.length > 0) {
|
||||||
|
// case: check the IP address of the inbound request against trusted IPs
|
||||||
|
|
||||||
|
const blockList = new net.BlockList();
|
||||||
|
|
||||||
|
for (const trustedIp of trustedIps) {
|
||||||
|
if (trustedIp.prefix !== undefined) {
|
||||||
|
blockList.addSubnet(
|
||||||
|
trustedIp.ipAddress,
|
||||||
|
trustedIp.prefix,
|
||||||
|
trustedIp.type
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
blockList.addAddress(
|
||||||
|
trustedIp.ipAddress,
|
||||||
|
trustedIp.type
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const check = blockList.check(authData.authIP);
|
||||||
|
|
||||||
|
if (!check) throw UnauthorizedRequestError({
|
||||||
|
message: "Failed workspace authorization"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
await validateServiceTokenDataClientForWorkspace({
|
await validateServiceTokenDataClientForWorkspace({
|
||||||
serviceTokenData: authData.authPayload,
|
serviceTokenData: authData.authPayload,
|
||||||
workspaceId,
|
workspaceId,
|
||||||
|
|||||||
@@ -25,10 +25,13 @@ export const INTEGRATION_CIRCLECI = "circleci";
|
|||||||
export const INTEGRATION_TRAVISCI = "travisci";
|
export const INTEGRATION_TRAVISCI = "travisci";
|
||||||
export const INTEGRATION_SUPABASE = "supabase";
|
export const INTEGRATION_SUPABASE = "supabase";
|
||||||
export const INTEGRATION_CHECKLY = "checkly";
|
export const INTEGRATION_CHECKLY = "checkly";
|
||||||
|
export const INTEGRATION_TERRAFORM_CLOUD = "terraform-cloud";
|
||||||
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_CODEFRESH = "codefresh";
|
||||||
|
export const INTEGRATION_DIGITAL_OCEAN_APP_PLATFORM = "digital-ocean-app-platform";
|
||||||
|
export const INTEGRATION_CLOUD_66 = "cloud-66";
|
||||||
export const INTEGRATION_NORTHFLANK = "northflank";
|
export const INTEGRATION_NORTHFLANK = "northflank";
|
||||||
export const INTEGRATION_SET = new Set([
|
export const INTEGRATION_SET = new Set([
|
||||||
INTEGRATION_AZURE_KEY_VAULT,
|
INTEGRATION_AZURE_KEY_VAULT,
|
||||||
@@ -44,10 +47,13 @@ export const INTEGRATION_SET = new Set([
|
|||||||
INTEGRATION_TRAVISCI,
|
INTEGRATION_TRAVISCI,
|
||||||
INTEGRATION_SUPABASE,
|
INTEGRATION_SUPABASE,
|
||||||
INTEGRATION_CHECKLY,
|
INTEGRATION_CHECKLY,
|
||||||
|
INTEGRATION_TERRAFORM_CLOUD,
|
||||||
INTEGRATION_HASHICORP_VAULT,
|
INTEGRATION_HASHICORP_VAULT,
|
||||||
INTEGRATION_CLOUDFLARE_PAGES,
|
INTEGRATION_CLOUDFLARE_PAGES,
|
||||||
INTEGRATION_BITBUCKET,
|
INTEGRATION_BITBUCKET,
|
||||||
|
INTEGRATION_DIGITAL_OCEAN_APP_PLATFORM,
|
||||||
INTEGRATION_CODEFRESH,
|
INTEGRATION_CODEFRESH,
|
||||||
|
INTEGRATION_CLOUD_66,
|
||||||
INTEGRATION_NORTHFLANK
|
INTEGRATION_NORTHFLANK
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@@ -78,9 +84,12 @@ export const INTEGRATION_TRAVISCI_API_URL = "https://api.travis-ci.com";
|
|||||||
export const INTEGRATION_SUPABASE_API_URL = "https://api.supabase.com";
|
export const INTEGRATION_SUPABASE_API_URL = "https://api.supabase.com";
|
||||||
export const INTEGRATION_LARAVELFORGE_API_URL = "https://forge.laravel.com";
|
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_TERRAFORM_CLOUD_API_URL = "https://app.terraform.io";
|
||||||
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 INTEGRATION_CODEFRESH_API_URL = "https://g.codefresh.io/api";
|
||||||
|
export const INTEGRATION_DIGITAL_OCEAN_API_URL = "https://api.digitalocean.com";
|
||||||
|
export const INTEGRATION_CLOUD_66_API_URL = "https://app.cloud66.com/api";
|
||||||
export const INTEGRATION_NORTHFLANK_API_URL = "https://api.northflank.com";
|
export const INTEGRATION_NORTHFLANK_API_URL = "https://api.northflank.com";
|
||||||
|
|
||||||
export const getIntegrationOptions = async () => {
|
export const getIntegrationOptions = async () => {
|
||||||
@@ -203,6 +212,15 @@ export const getIntegrationOptions = async () => {
|
|||||||
clientId: await getClientIdGitLab(),
|
clientId: await getClientIdGitLab(),
|
||||||
docsLink: "",
|
docsLink: "",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "Terraform Cloud",
|
||||||
|
slug: "terraform-cloud",
|
||||||
|
image: "Terraform Cloud.png",
|
||||||
|
isAvailable: true,
|
||||||
|
type: "pat",
|
||||||
|
cliendId: "",
|
||||||
|
docsLink: "",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: "Travis CI",
|
name: "Travis CI",
|
||||||
slug: "travisci",
|
slug: "travisci",
|
||||||
@@ -275,6 +293,24 @@ export const getIntegrationOptions = async () => {
|
|||||||
clientId: "",
|
clientId: "",
|
||||||
docsLink: "",
|
docsLink: "",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "Digital Ocean App Platform",
|
||||||
|
slug: "digital-ocean-app-platform",
|
||||||
|
image: "Digital Ocean.png",
|
||||||
|
isAvailable: true,
|
||||||
|
type: "pat",
|
||||||
|
clientId: "",
|
||||||
|
docsLink: "",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Cloud 66",
|
||||||
|
slug: "cloud-66",
|
||||||
|
image: "Cloud 66.png",
|
||||||
|
isAvailable: true,
|
||||||
|
type: "pat",
|
||||||
|
clientId: "",
|
||||||
|
docsLink: "",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: "Northflank",
|
name: "Northflank",
|
||||||
slug: "northflank",
|
slug: "northflank",
|
||||||
@@ -283,7 +319,7 @@ export const getIntegrationOptions = async () => {
|
|||||||
type: "pat",
|
type: "pat",
|
||||||
clientId: "",
|
clientId: "",
|
||||||
docsLink: ""
|
docsLink: ""
|
||||||
}
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
return INTEGRATION_OPTIONS;
|
return INTEGRATION_OPTIONS;
|
||||||
|
|||||||
@@ -143,13 +143,13 @@ var runCmd = &cobra.Command{
|
|||||||
|
|
||||||
err = executeMultipleCommandWithEnvs(command, len(secretsByKey), env)
|
err = executeMultipleCommandWithEnvs(command, len(secretsByKey), env)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
util.HandleError(err, "Unable to execute your chained command")
|
fmt.Println(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
err = executeSingleCommandWithEnvs(args, len(secretsByKey), env)
|
err = executeSingleCommandWithEnvs(args, len(secretsByKey), env)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
util.HandleError(err, "Unable to execute your single command")
|
fmt.Println(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ in plaintext. Effectively, this means each such secret operation only requires 1
|
|||||||
<AccordionGroup>
|
<AccordionGroup>
|
||||||
<Accordion title="Retrieve secrets">
|
<Accordion title="Retrieve secrets">
|
||||||
Retrieve all secrets for an Infisical project and environment.
|
Retrieve all secrets for an Infisical project and environment.
|
||||||
|
|
||||||
<Tabs>
|
<Tabs>
|
||||||
<Tab title="cURL">
|
<Tab title="cURL">
|
||||||
```bash
|
```bash
|
||||||
@@ -18,7 +17,12 @@ in plaintext. Effectively, this means each such secret operation only requires 1
|
|||||||
```
|
```
|
||||||
</Tab>
|
</Tab>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
|
####
|
||||||
|
<Info>
|
||||||
|
When using a [service token](../../../documentation/platform/token) with access to a single environment and path, you don't need to provide request parameters because the server will automatically scope the request to the defined environment/secrets path of the service token used.
|
||||||
|
For all other cases, request parameters are required.
|
||||||
|
</Info>
|
||||||
|
####
|
||||||
<ParamField query="workspaceId" type="string" required>
|
<ParamField query="workspaceId" type="string" required>
|
||||||
The ID of the workspace
|
The ID of the workspace
|
||||||
</ParamField>
|
</ParamField>
|
||||||
|
|||||||
@@ -7,18 +7,25 @@ The changelog below reflects new product developments and updates on a monthly b
|
|||||||
## July 2023
|
## July 2023
|
||||||
|
|
||||||
- Released [secret referencing and importing](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).
|
|
||||||
- Redesigned the project/organization experience.
|
- Redesigned the project/organization experience.
|
||||||
|
- Added native [Laravel Forge integration](https://infisical.com/docs/integrations/cloud/laravel-forge).
|
||||||
|
- Added native [Codefresh integration](https://infisical.com/docs/integrations/cicd/codefresh)
|
||||||
|
- Added native [Bitbucket integration](https://infisical.com/docs/integrations/cicd/bitbucket)
|
||||||
|
- Added native [DigitalOcean App Platform integration](https://infisical.com/docs/integrations/cloud/digital-ocean-app-platform)
|
||||||
|
- Added native [Cloud66 integration](https://infisical.com/docs/integrations/cloud/cloud-66)
|
||||||
|
- Added support for Google SSO.
|
||||||
|
- Added support for [Okta SAML 2.0 authentication](https://infisical.com/docs/documentation/platform/saml)
|
||||||
|
- Released [folders / path-based secret storage](https://infisical.com/docs/documentation/platform/folder)
|
||||||
|
- Released [webhooks](https://infisical.com/docs/documentation/platform/webhooks)
|
||||||
|
|
||||||
## June 2023
|
## June 2023
|
||||||
|
|
||||||
- Released the [Terraform Provider](https://infisical.com/docs/integrations/frameworks/terraform#5-run-terraform).
|
- Released the [Terraform Provider](https://infisical.com/docs/integrations/frameworks/terraform#5-run-terraform).
|
||||||
- Updated the usage and billing page. Added the free trial for the professional tier.
|
- Updated the usage and billing page. Added the free trial for the professional tier.
|
||||||
- Added the intergation with [Checkly](https://infisical.com/docs/integrations/cloud/checkly), [Hashicorp Vault](https://infisical.com/docs/integrations/cloud/hashicorp-vault), and [Cloudflare Pages](https://infisical.com/docs/integrations/cloud/cloudflare-pages).
|
- Added the intergation with [Checkly](https://infisical.com/docs/integrations/cloud/checkly), [Hashicorp Vault](https://infisical.com/docs/integrations/cloud/hashicorp-vault), and [Cloudflare Pages](https://infisical.com/docs/integrations/cloud/cloudflare-pages).
|
||||||
- Comleted a penetration test with a `very good` result.
|
- Completed a penetration test with a `very good` result.
|
||||||
- Added support for multi-line secrets.
|
- Added support for multi-line secrets.
|
||||||
|
|
||||||
|
|
||||||
## May 2023
|
## May 2023
|
||||||
|
|
||||||
- Released secret scanning capability for the CLI.
|
- Released secret scanning capability for the CLI.
|
||||||
|
|||||||
@@ -83,3 +83,27 @@ Password: `testInfisical1`
|
|||||||
# To stop environment use Control+C (on Mac) CTRL+C (on Win) or
|
# To stop environment use Control+C (on Mac) CTRL+C (on Win) or
|
||||||
docker-compose -f docker-compose.dev.yml down
|
docker-compose -f docker-compose.dev.yml down
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Starting Infisical docs locally
|
||||||
|
|
||||||
|
We use [Mintlify](https://mintlify.com/) for our docs.
|
||||||
|
|
||||||
|
#### Install Mintlify CLI.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm i -g mintlify
|
||||||
|
```
|
||||||
|
|
||||||
|
or
|
||||||
|
|
||||||
|
```bash
|
||||||
|
yarn global add mintlify
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Running the docs
|
||||||
|
Go to `docs` directory and run `mintlify dev`. This will start up the docs on `localhost:3000`
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# From the root directory
|
||||||
|
cd docs; mintlify dev;
|
||||||
|
```
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
---
|
||||||
|
title: "IP Allowlisting"
|
||||||
|
description: "Restrict access to your secrets in Infisical using trusted IPs"
|
||||||
|
---
|
||||||
|
|
||||||
|
Projects in Infisical can be configured to restrict client access to specific IP addresses or CIDR ranges. This applies to any client using service tokens and
|
||||||
|
can be useful, for example, for limiting access to traffic coming from corporate networks.
|
||||||
|
|
||||||
|
By default, each project is initialized with the `0.0.0.0/0` entry, representing all possible IPv4 addresses.
|
||||||
|
For enhanced security, we strongly recommend replacing the default entry with your client IPs to tighten access to your secrets.
|
||||||
|
|
||||||
|
<Note>
|
||||||
|
You must be a project `admin` to manage your project's IP whitelist.
|
||||||
|
</Note>
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## Creating a trusted IP entry
|
||||||
|
|
||||||
|
To create a trusted IP entry, head over to the **IP Whitelist** tab in your project. When creating an entry,
|
||||||
|
you can specify either a specific IP address like `192.0.2.1` or a CIDR range like `2001:db8::/32`; both IPv4 and IPv6
|
||||||
|
formats are accepted.
|
||||||
|
|
||||||
|

|
||||||
@@ -0,0 +1,100 @@
|
|||||||
|
---
|
||||||
|
title: "SSO"
|
||||||
|
description: "Log in to Infisical via SSO protocols"
|
||||||
|
---
|
||||||
|
|
||||||
|
<Warning>
|
||||||
|
Infisical currently only supports SAML SSO authentication with [Okta as the
|
||||||
|
identity provider (IDP)](https://www.okta.com/). We're expanding support for
|
||||||
|
other IDPs in the coming months, so stay tuned with this issue
|
||||||
|
[here](https://github.com/Infisical/infisical/issues/442).
|
||||||
|
</Warning>
|
||||||
|
|
||||||
|
You can configure your organization in Infisical to have members authenticate with the platform via protocols like [SAML 2.0](https://en.wikipedia.org/wiki/SAML_2.0).
|
||||||
|
|
||||||
|
To note, configuring SSO retains the end-to-end encrypted architecture of Infisical because we decouple the **authentication** and **decryption** steps. In all login with SSO implementations,
|
||||||
|
your IDP cannot and will not have access to the decryption key needed to decrypt your secrets.
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
Head over to your organization Settings > Authentication > SAML SSO Configuration.
|
||||||
|
|
||||||
|
Next, press "Set up SAML SSO" in the SAML SSO and follow the instructions
|
||||||
|
below to configure SSO for your identity provider:
|
||||||
|
|
||||||
|
<Note>
|
||||||
|
Note that only members with the `owner` or `admin` roles in an organization
|
||||||
|
can configure SSO for it.
|
||||||
|
</Note>
|
||||||
|
|
||||||
|
<AccordionGroup>
|
||||||
|
<Accordion title="Okta SAML 2.0">
|
||||||
|
1. In the Okta Admin Portal, select Applications > Applications from the
|
||||||
|
navigation. On the Applications screen, select the Create App Integration
|
||||||
|
button.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
2. In the Create a New Application Integration dialog, select the SAML 2.0 radio button:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
3. On the General Settings screen, give the application a unique, Infisical-specific name and select Next.
|
||||||
|
|
||||||
|
4. On the Configure SAML screen, configure the following fields:
|
||||||
|
|
||||||
|
- Single sign on URL: `https://app.infisical.com/api/v1/sso/saml2/:identifier`; we'll update the `:identifier` part later in step 6.
|
||||||
|
- Audience URI (SP Entity ID): `https://app.infisical.com`
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
<Note>
|
||||||
|
If you're self-hosting Infisical, then you will want to replace `https://app.infisical.com` with your own domain.
|
||||||
|
</Note>
|
||||||
|
|
||||||
|
4. Also on the Configure SAML screen, configure the Attribute Statements to map:
|
||||||
|
|
||||||
|
- `id -> user.id`,
|
||||||
|
- `email -> user.email`,
|
||||||
|
- `firstName -> user.firstName`
|
||||||
|
- `lastName -> user.lastName`
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
Once configured, select the Next button to proceed to the Feedback screen and select Finish.
|
||||||
|
|
||||||
|
5. Get IDP values
|
||||||
|
|
||||||
|
Once your application is created, select the Sign On tab for the app and select the View Setup Instructions button located on the right side of the screen:
|
||||||
|
|
||||||
|
Copy the Identity Provider Single Sign-On URL, the Identity Provider Issuer, and the X.509 Certificate to be pasted into your Infisical SAML SSO configuration details with the following map:
|
||||||
|
|
||||||
|
- `Audience -> Okta Audience URI (SP Entity ID)`
|
||||||
|
- `Entrypoint -> Okta Identity Provider Single Sign-On URL`
|
||||||
|
- `Issuer -> Identity Provider Issuer`
|
||||||
|
- `Certificate -> X.509 Certificate`.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
6. Create the SSO configuration and copy your SSO identifier in Infisical; update `:identifier` from step 4 earlier to be this value.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
7. Assignments
|
||||||
|
|
||||||
|
Finally, Navigate to the Assignments tab and select the Assign button:
|
||||||
|
|
||||||
|
You can assign access to the application on a user-by-user basis using the Assign to People option, or in-bulk using the Assign to Groups option.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
At this point, you have configured everything you need within the context of the Okta Admin Portal.
|
||||||
|
|
||||||
|
8. Return to Infisical and enable SAML SSO.
|
||||||
|
|
||||||
|
Enabling SAML SSO enforces all members in your organization to only be able to log into Infisical via Okta.
|
||||||
|
|
||||||
|
</Accordion>
|
||||||
|
</AccordionGroup>
|
||||||
@@ -45,8 +45,8 @@ To add an import, simply click on the `Add import` button and provide the enviro
|
|||||||

|

|
||||||
|
|
||||||
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.
|
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.
|
Additionally, any secrets you define directly in your environment will override any secrets that are imported with the same name.
|
||||||
|
|
||||||
You can modify this sequence by dragging and rearranging the folders using the `Change Order` drag handle.
|
You can modify the order of folders to control overrides using the `Change Order` drag handle.
|
||||||
|
|
||||||

|

|
||||||
|
|||||||
|
After Width: | Height: | Size: 890 KiB |
|
After Width: | Height: | Size: 814 KiB |
|
After Width: | Height: | Size: 1.1 MiB |
|
After Width: | Height: | Size: 742 KiB |
|
After Width: | Height: | Size: 1.3 MiB |
|
After Width: | Height: | Size: 1.4 MiB |
|
After Width: | Height: | Size: 1.1 MiB |
|
After Width: | Height: | Size: 969 KiB |
|
After Width: | Height: | Size: 812 KiB |
|
After Width: | Height: | Size: 1.1 MiB |
|
After Width: | Height: | Size: 1.2 MiB |
|
After Width: | Height: | Size: 1.2 MiB |
|
After Width: | Height: | Size: 1.4 MiB |
|
After Width: | Height: | Size: 1.0 MiB |
|
After Width: | Height: | Size: 218 KiB |
|
After Width: | Height: | Size: 228 KiB |
|
After Width: | Height: | Size: 241 KiB |
|
After Width: | Height: | Size: 202 KiB |
|
After Width: | Height: | Size: 301 KiB |
|
After Width: | Height: | Size: 220 KiB |
|
After Width: | Height: | Size: 345 KiB |
|
Before Width: | Height: | Size: 650 KiB After Width: | Height: | Size: 1.4 MiB |
|
After Width: | Height: | Size: 313 KiB |
|
After Width: | Height: | Size: 439 KiB |
|
After Width: | Height: | Size: 264 KiB |
|
After Width: | Height: | Size: 381 KiB |
|
After Width: | Height: | Size: 423 KiB |
|
After Width: | Height: | Size: 316 KiB |
|
After Width: | Height: | Size: 598 KiB |
|
After Width: | Height: | Size: 443 KiB |
|
After Width: | Height: | Size: 563 KiB |
|
After Width: | Height: | Size: 386 KiB |
@@ -0,0 +1,55 @@
|
|||||||
|
---
|
||||||
|
title: "Cloud 66"
|
||||||
|
description: "How to sync secrets from Infisical to Cloud 66"
|
||||||
|
---
|
||||||
|
|
||||||
|
Prerequisites:
|
||||||
|
|
||||||
|
- Set up and add envars to [Infisical Cloud](https://app.infisical.com)
|
||||||
|
|
||||||
|
## Navigate to your project's integrations tab
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## Enter your Cloud 66 Access Token
|
||||||
|
|
||||||
|
In Cloud 66 Dashboard, click on the top right icon > Account Settings > Access Token
|
||||||
|

|
||||||
|

|
||||||
|
|
||||||
|
Create new Personal Access Token.
|
||||||
|

|
||||||
|
|
||||||
|
Name it **infisical** and check **Public** and **Admin**. Then click "Create Token"
|
||||||
|

|
||||||
|
|
||||||
|
Copy and save your token.
|
||||||
|

|
||||||
|
|
||||||
|
### Go to Infisical Integration Page
|
||||||
|
|
||||||
|
Click on the Cloud 66 tile and enter your API token to grant Infisical access to your Cloud 66 account.
|
||||||
|

|
||||||
|
|
||||||
|
<Info>
|
||||||
|
If this is your project's first cloud integration, then you'll have to grant
|
||||||
|
Infisical access to your project's environment variables. Although this step
|
||||||
|
breaks E2EE, it's necessary for Infisical to sync the environment variables to
|
||||||
|
the cloud platform.
|
||||||
|
</Info>
|
||||||
|
|
||||||
|
Enter your Cloud 66 Personal Access Token here. Then click "Connect to Cloud 66".
|
||||||
|

|
||||||
|
|
||||||
|
|
||||||
|
## Start integration
|
||||||
|
|
||||||
|
Select which Infisical environment secrets you want to sync to which Cloud 66 stacks and press create integration to start syncing secrets to Cloud 66.
|
||||||
|

|
||||||
|
|
||||||
|
<Warning>
|
||||||
|
Any existing environment variables in Cloud 66 will be deleted when you start syncing. Make sure to add all the secrets into the Infisical dashboard first before doing any integrations.
|
||||||
|
</Warning>
|
||||||
|
|
||||||
|
Done!
|
||||||
|

|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
---
|
||||||
|
title: "Digital Ocean App Platform"
|
||||||
|
description: "How to sync secrets from Infisical to Digital Ocean App Platform"
|
||||||
|
---
|
||||||
|
|
||||||
|
Prerequisites:
|
||||||
|
|
||||||
|
- Set up and add envars to [Infisical Cloud](https://app.infisical.com)
|
||||||
|
|
||||||
|
## Get your Digital Ocean Personal Access Tokens
|
||||||
|
|
||||||
|
On Digital Ocean dashboard, navigate to **API > Tokens** and click on "Generate New Token"
|
||||||
|

|
||||||
|
|
||||||
|
Name it **infisical**, choose **No expiry**, and make sure to check **Write (optional)**. Then click on "Generate Token" and copy your API token.
|
||||||
|

|
||||||
|
|
||||||
|
## Navigate to your project's integrations tab
|
||||||
|
|
||||||
|
Click on the **Digital Ocean App Platform** tile and enter your API token to grant Infisical access to your Digital Ocean account.
|
||||||
|

|
||||||
|
|
||||||
|
<Info>
|
||||||
|
If this is your project's first cloud integration, then you'll have to grant
|
||||||
|
Infisical access to your project's environment variables. Although this step
|
||||||
|
breaks E2EE, it's necessary for Infisical to sync the environment variables to
|
||||||
|
the cloud platform.
|
||||||
|
</Info>
|
||||||
|
|
||||||
|
Then enter your Digital Ocean Personal Access Token here. Then click "Connect to Digital Ocean App Platform".
|
||||||
|

|
||||||
|
|
||||||
|
## Start integration
|
||||||
|
|
||||||
|
Select which Infisical environment secrets you want to sync to which Digital Ocean App and click "Create Integration".
|
||||||
|

|
||||||
|
|
||||||
|
Done!
|
||||||
|

|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
---
|
||||||
|
title: "Terraform Cloud"
|
||||||
|
description: "How to sync secrets from Infisical to Terraform Cloud"
|
||||||
|
---
|
||||||
|
|
||||||
|
Prerequisites:
|
||||||
|
|
||||||
|
- Set up and add envars to [Infisical Cloud](https://app.infisical.com)
|
||||||
|
|
||||||
|
## Navigate to your project's integrations tab
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## Enter your Terraform Cloud API Token and Workspace Id
|
||||||
|
|
||||||
|
Obtain a Terraform Cloud API Token in User Settings > Tokens
|
||||||
|
|
||||||
|

|
||||||
|

|
||||||
|
|
||||||
|
Obtain your Terraform Cloud Workspace Id in Projects & Workspaces > Workspace > ID
|
||||||
|
|
||||||
|

|
||||||
|

|
||||||
|
|
||||||
|
Press on the Terraform Cloud tile and input your Terraform Cloud API Token and Workspace Id to grant Infisical access to your Terraform Cloud account.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
<Info>
|
||||||
|
If this is your project's first cloud integration, then you'll have to grant
|
||||||
|
Infisical access to your project's environment variables. Although this step
|
||||||
|
breaks E2EE, it's necessary for Infisical to sync the environment variables to
|
||||||
|
the cloud platform.
|
||||||
|
</Info>
|
||||||
|
|
||||||
|
## Start integration
|
||||||
|
|
||||||
|
Select which Infisical environment secrets and Terraform Cloud variable type you want to sync to which Terraform Cloud workspace/project and press create integration to start syncing secrets to Terraform Cloud.
|
||||||
|
|
||||||
|

|
||||||
|

|
||||||
@@ -26,3 +26,7 @@ infisical run -- <your application start command>
|
|||||||
# Example
|
# Example
|
||||||
infisical run -- npm run dev
|
infisical run -- npm run dev
|
||||||
```
|
```
|
||||||
|
|
||||||
|
<Info>
|
||||||
|
React environment variables must be prefixed with `REACT_APP_` to show up within the application
|
||||||
|
</Info>
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ Missing an integration? [Throw in a request](https://github.com/Infisical/infisi
|
|||||||
| [Render](/integrations/cloud/render) | Cloud | Available |
|
| [Render](/integrations/cloud/render) | Cloud | Available |
|
||||||
| [Laravel Forge](/integrations/cloud/laravel-forge) | Cloud | Available |
|
| [Laravel Forge](/integrations/cloud/laravel-forge) | Cloud | Available |
|
||||||
| [Railway](/integrations/cloud/railway) | Cloud | Available |
|
| [Railway](/integrations/cloud/railway) | Cloud | Available |
|
||||||
|
| [Terraform Cloud](/integrations/cloud/terraform-cloud) | Cloud | Available |
|
||||||
| [Fly.io](/integrations/cloud/flyio) | Cloud | Available |
|
| [Fly.io](/integrations/cloud/flyio) | Cloud | Available |
|
||||||
| [Supabase](/integrations/cloud/supabase) | Cloud | Available |
|
| [Supabase](/integrations/cloud/supabase) | Cloud | Available |
|
||||||
| [Cloudflare Pages](/integrations/cloud/cloudflare-pages) | Cloud | Available |
|
| [Cloudflare Pages](/integrations/cloud/cloudflare-pages) | Cloud | Available |
|
||||||
|
|||||||
@@ -66,12 +66,12 @@ metadata:
|
|||||||
spec:
|
spec:
|
||||||
# The host that should be used to pull secrets from. If left empty, the value specified in Global configuration will be used
|
# The host that should be used to pull secrets from. If left empty, the value specified in Global configuration will be used
|
||||||
hostAPI: https://app.infisical.com/api
|
hostAPI: https://app.infisical.com/api
|
||||||
resyncInterval:
|
resyncInterval: 60
|
||||||
authentication:
|
authentication:
|
||||||
serviceToken:
|
serviceToken:
|
||||||
serviceTokenSecretReference:
|
serviceTokenSecretReference:
|
||||||
secretName: service-token
|
secretName: service-token
|
||||||
secretNamespace: option
|
secretNamespace: default
|
||||||
secretsScope:
|
secretsScope:
|
||||||
envSlug: dev
|
envSlug: dev
|
||||||
secretsPath: "/"
|
secretsPath: "/"
|
||||||
|
|||||||
@@ -118,8 +118,10 @@
|
|||||||
"documentation/platform/pit-recovery",
|
"documentation/platform/pit-recovery",
|
||||||
"documentation/platform/secret-versioning",
|
"documentation/platform/secret-versioning",
|
||||||
"documentation/platform/audit-logs",
|
"documentation/platform/audit-logs",
|
||||||
|
"documentation/platform/token",
|
||||||
|
"documentation/platform/ip-allowlisting",
|
||||||
"documentation/platform/mfa",
|
"documentation/platform/mfa",
|
||||||
"documentation/platform/token"
|
"documentation/platform/saml"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -135,7 +137,6 @@
|
|||||||
"self-hosting/deployment-options/standalone-infisical",
|
"self-hosting/deployment-options/standalone-infisical",
|
||||||
"self-hosting/deployment-options/fly.io",
|
"self-hosting/deployment-options/fly.io",
|
||||||
"self-hosting/deployment-options/render",
|
"self-hosting/deployment-options/render",
|
||||||
"self-hosting/deployment-options/laravel-forge",
|
|
||||||
"self-hosting/deployment-options/digital-ocean-marketplace"
|
"self-hosting/deployment-options/digital-ocean-marketplace"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@@ -207,6 +208,12 @@
|
|||||||
"integrations/cloud/aws-secret-manager"
|
"integrations/cloud/aws-secret-manager"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"group": "Digital Ocean",
|
||||||
|
"pages": [
|
||||||
|
"integrations/cloud/digital-ocean-app-platform"
|
||||||
|
]
|
||||||
|
},
|
||||||
"integrations/cloud/heroku",
|
"integrations/cloud/heroku",
|
||||||
"integrations/cloud/vercel",
|
"integrations/cloud/vercel",
|
||||||
"integrations/cloud/netlify",
|
"integrations/cloud/netlify",
|
||||||
@@ -215,10 +222,12 @@
|
|||||||
"integrations/cloud/flyio",
|
"integrations/cloud/flyio",
|
||||||
"integrations/cloud/laravel-forge",
|
"integrations/cloud/laravel-forge",
|
||||||
"integrations/cloud/supabase",
|
"integrations/cloud/supabase",
|
||||||
|
"integrations/cloud/terraform-cloud",
|
||||||
"integrations/cloud/cloudflare-pages",
|
"integrations/cloud/cloudflare-pages",
|
||||||
"integrations/cloud/checkly",
|
"integrations/cloud/checkly",
|
||||||
"integrations/cloud/hashicorp-vault",
|
"integrations/cloud/hashicorp-vault",
|
||||||
"integrations/cloud/azure-key-vault",
|
"integrations/cloud/azure-key-vault",
|
||||||
|
"integrations/cloud/cloud-66",
|
||||||
"integrations/cicd/githubactions",
|
"integrations/cicd/githubactions",
|
||||||
"integrations/cicd/gitlab",
|
"integrations/cicd/gitlab",
|
||||||
"integrations/cicd/circleci",
|
"integrations/cicd/circleci",
|
||||||
@@ -324,7 +333,8 @@
|
|||||||
"pages": [
|
"pages": [
|
||||||
"security/overview",
|
"security/overview",
|
||||||
"security/data-model",
|
"security/data-model",
|
||||||
"security/mechanics"
|
"security/mechanics",
|
||||||
|
"security/service-tokens"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
---
|
||||||
|
title: "Service Tokens"
|
||||||
|
description: "Understanding service tokens and their best practices"
|
||||||
|
---
|
||||||
|
|
||||||
|
## Background
|
||||||
|
|
||||||
|
Many clients use service tokens to authenticate and read/write secrets from/to Infisical.
|
||||||
|
|
||||||
|
Each service token consist of two parts used for authentication and decryption, separated by `.`. Consider the token `st.abc.def.ghi`. Here, `st.abc.def` can be used to authenticate with the API, by including it in the `Authorization` header under `Bearer st.abc.def`, and retrieve (encrypted) secrets as well as a project key back. Meanwhile, `ghi`, a hex-string, can be used to decrypt the project key used to decrypt the secrets.
|
||||||
|
|
||||||
|
Note that when using service tokens via select client methods like SDK or CLI, cryptographic operations are abstracted for you that is the token is parsed and encryption/decryption operations are handled. If using service tokens with the REST API and end-to-end encryption enabled, then you will have to handle the encryption/decryption operations yourself.
|
||||||
|
|
||||||
|
## Recommendations
|
||||||
|
|
||||||
|
1. Issuance: When creating a new service token, it’s important to consider the [principle of least privilege(PoLP)](https://en.wikipedia.org/wiki/Principle_of_least_privilege) when setting its scope and expiration date. For example, if the client using the token only requires access to a staging environment, then you should scope the token to that environment only; you can further scope tokens to path(s) within environment(s) if you happen to use [path-based secret storage](/documentation/platform/folder). Likewise, if the client does not intend to access secrets indefinitely, then you may consider setting a finite lifetime for the token such as 6 months or 1 year from now. Finally, you should consider carefully whether or not your client requires the ability to read and/or write secrets from/to Infisical.
|
||||||
|
|
||||||
|
2. Network access: We recommend configuring the IP whitelist settings of each project to allow either single IP addresses or CIDR-notated range of addresses to read/write secrets to Infisical. With this feature, you can specify the IP range of your client servers to restrict access to your project in Infisical.
|
||||||
|
|
||||||
|
3. Storage: Since service tokens grant access to your secrets, we recommend storing service tokens securely across your development cycle whether it be in a `.env` file in local development or as an environment variable of your deployment platform.
|
||||||
|
|
||||||
|
4. Rotation: We recommend periodically rotating the service token, even in the absence of compromise. Since service tokens are capable of decrypting project keys used to decrypt secrets, all of which use AES-256-GCM encryption, they should be rotated before approximately 2^32 encryptions have been performed; this follows the guidance set forth by NIST publication 800-38D. Note that Infisical keeps track of the number of times that service tokens are used and will alert you when you have reached 90% of the recommended capacity.
|
||||||
@@ -10,7 +10,7 @@ However, the following functionality will be disabled.
|
|||||||
- Sending invite links via email for projects to teammates
|
- Sending invite links via email for projects to teammates
|
||||||
- Sending alerts such as suspicious login attempts
|
- Sending alerts such as suspicious login attempts
|
||||||
|
|
||||||
## General configuration
|
## Configuration
|
||||||
|
|
||||||
If you choose to setup email service, you need to configure the following SMTP [environment variables](https://infisical.com/docs/self-hosting/configuration/envars):
|
If you choose to setup email service, you need to configure the following SMTP [environment variables](https://infisical.com/docs/self-hosting/configuration/envars):
|
||||||
|
|
||||||
|
|||||||
@@ -19,10 +19,13 @@ const integrationSlugNameMapping: Mapping = {
|
|||||||
travisci: "TravisCI",
|
travisci: "TravisCI",
|
||||||
supabase: "Supabase",
|
supabase: "Supabase",
|
||||||
checkly: "Checkly",
|
checkly: "Checkly",
|
||||||
|
'terraform-cloud': 'Terraform Cloud',
|
||||||
"hashicorp-vault": "Vault",
|
"hashicorp-vault": "Vault",
|
||||||
"cloudflare-pages": "Cloudflare Pages",
|
"cloudflare-pages": "Cloudflare Pages",
|
||||||
"codefresh": "Codefresh",
|
"codefresh": "Codefresh",
|
||||||
|
"digital-ocean-app-platform": "Digital Ocean App Platform",
|
||||||
bitbucket: "BitBucket",
|
bitbucket: "BitBucket",
|
||||||
|
"cloud-66": "Cloud 66",
|
||||||
northflank: "Northflank"
|
northflank: "Northflank"
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 6.6 KiB |
@@ -127,7 +127,7 @@ const AddProjectMemberDialog = ({
|
|||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<Button
|
<Button
|
||||||
onButtonPressed={() => router.push(`/settings/org/${router.query.id}`)}
|
onButtonPressed={() => router.push(`/org/${localStorage.getItem("orgData.id")}/members`)}
|
||||||
color="mineshaft"
|
color="mineshaft"
|
||||||
text={t("section.members.add-dialog.add-user-to-org") as string}
|
text={t("section.members.add-dialog.add-user-to-org") as string}
|
||||||
size="md"
|
size="md"
|
||||||
|
|||||||
@@ -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,25 +16,7 @@ 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] text-center rounded-md'>
|
<div className='lg:w-1/6 w-1/4 min-w-[20rem] rounded-md'>
|
||||||
<Button
|
|
||||||
colorSchema="primary"
|
|
||||||
variant="solid"
|
|
||||||
onClick={() => {
|
|
||||||
setIsSignupWithEmail(true);
|
|
||||||
}}
|
|
||||||
isFullWidth
|
|
||||||
className="h-14 w-full mx-0"
|
|
||||||
>
|
|
||||||
Sign Up with email
|
|
||||||
</Button>
|
|
||||||
</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
|
<Button
|
||||||
colorSchema="primary"
|
colorSchema="primary"
|
||||||
variant="solid"
|
variant="solid"
|
||||||
@@ -47,7 +29,20 @@ export default function InitialSignupStep({
|
|||||||
>
|
>
|
||||||
{t("signup.continue-with-google")}
|
{t("signup.continue-with-google")}
|
||||||
</Button>
|
</Button>
|
||||||
</div> */}
|
</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={() => {
|
||||||
|
setIsSignupWithEmail(true);
|
||||||
|
}}
|
||||||
|
isFullWidth
|
||||||
|
className="h-14 w-full mx-0"
|
||||||
|
>
|
||||||
|
Sign Up with email
|
||||||
|
</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"
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ export * from "./serviceTokens";
|
|||||||
export * from "./ssoConfig";
|
export * from "./ssoConfig";
|
||||||
export * from "./subscriptions";
|
export * from "./subscriptions";
|
||||||
export * from "./tags";
|
export * from "./tags";
|
||||||
|
export * from "./trustedIps";
|
||||||
export * from "./users";
|
export * from "./users";
|
||||||
export * from "./webhooks";
|
export * from "./webhooks";
|
||||||
export * from "./workspace";
|
export * from "./workspace";
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ export type SubscriptionPlan = {
|
|||||||
customAlerts: boolean;
|
customAlerts: boolean;
|
||||||
customRateLimits: boolean;
|
customRateLimits: boolean;
|
||||||
pitRecovery: boolean;
|
pitRecovery: boolean;
|
||||||
|
ipAllowlisting: boolean;
|
||||||
rbac: boolean;
|
rbac: boolean;
|
||||||
secretVersioning: boolean;
|
secretVersioning: boolean;
|
||||||
slug: string;
|
slug: string;
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
export {
|
||||||
|
useAddTrustedIp,
|
||||||
|
useDeleteTrustedIp,
|
||||||
|
useGetTrustedIps,
|
||||||
|
useUpdateTrustedIp} from "./queries";
|
||||||