Compare commits

...

68 Commits

Author SHA1 Message Date
41c1828324 roll forward: disable IP white listing 2023-07-26 20:50:53 -04:00
c2c8cf90b7 Merge branch 'main' of https://github.com/Infisical/infisical 2023-07-26 14:03:47 -07:00
00b4d6bd45 changed the icon 2023-07-26 14:03:37 -07:00
f5a6270d2a add workspace auth for multi env/glob request 2023-07-26 16:50:35 -04:00
8331cd4de8 Merge pull request #761 from atimapreandrew/terraform-cloud-integration
Terraform cloud integration
2023-07-26 23:16:51 +07:00
3447074eb5 Fix merge conflicts 2023-07-26 23:13:33 +07:00
5a708ee931 Optimize Terraform Cloud sync function 2023-07-26 23:10:38 +07:00
9913b2fb6c Initialize TrustedIP upon creating a new workspace 2023-07-26 22:20:51 +07:00
2c021f852f Update filter for trusted IPs backfill 2023-07-26 21:15:07 +07:00
8dbc894ce9 Replace insertMany operation with upsert for backfilling trusted ips 2023-07-26 20:45:58 +07:00
511904605f Merge pull request #786 from Infisical/debug-integrations
Fix PATCH IP whitelist behavior and breaking integrations due to incorrect project id in local storage
2023-07-26 17:55:24 +07:00
7ae6d1610f Fix IP whitelist PATCH endpoint, update localStorage project id to reflect navigated to project 2023-07-26 17:46:33 +07:00
7da6d72f13 Remove save call from backfilling trusted ips 2023-07-26 16:18:13 +07:00
ad33356994 Remove required comment for trusted IP schema 2023-07-26 15:29:43 +07:00
cfa2461479 Merge pull request #785 from Infisical/network-access
Add support for IP allowlisting / trusted IPs
2023-07-26 15:11:04 +07:00
bf08bfacb5 Fix lint errors 2023-07-26 15:06:18 +07:00
cf77820059 Merge remote-tracking branch 'origin' into network-access 2023-07-26 14:55:12 +07:00
1ca90f56b8 Add docs for IP allowlisting 2023-07-26 14:51:25 +07:00
5899d7aee9 Complete trusted IPs feature 2023-07-26 13:34:56 +07:00
b565194c43 create versions for brew releases 2023-07-25 15:39:29 -04:00
86e04577c9 print exec error messages as is 2023-07-25 14:17:31 -04:00
f4b3cafc5b Added Terraform Cloud integration docs 2023-07-25 16:51:53 +01:00
18aad7d520 Terraform Cloud integration 2023-07-25 15:25:11 +01:00
54c79012db fix the org-members link 2023-07-25 07:01:27 -07:00
4b720bf940 Update kubernetes.mdx 2023-07-24 18:13:35 -04:00
993866bb8b Update secret-reference.mdx 2023-07-24 17:18:07 -04:00
8c39fa2438 add conditional imports to raw api 2023-07-24 15:01:31 -04:00
7bccfaefac Merge pull request #784 from akhilmhdh/fix/import-delete
fix: resolved secret import delete and include_import response control
2023-07-24 11:47:02 -04:00
e2b666345b fix: resolved secret import delete and include_import response control 2023-07-24 20:53:59 +05:30
90910819a3 Merge pull request #778 from afrieirham/docs/running-docs-locally
docs: add running infisical docs locally guide
2023-07-24 09:00:46 -04:00
8b070484dd docs: add running infisical docs locally 2023-07-24 20:36:42 +08:00
a764087c83 Merge pull request #782 from Infisical/improve-security-docs
Add section on service token best practices
2023-07-24 18:14:26 +07:00
27d5fa5aa0 Add section on service token best practices 2023-07-24 18:10:37 +07:00
2e7705999c Updated changelog and contributors in README 2023-07-24 12:56:02 +07:00
428bf8e252 Merge branch 'main' of https://github.com/Infisical/infisical 2023-07-23 13:59:42 -07:00
264740d84d style updates 2023-07-23 13:59:25 -07:00
723bcd4d83 Update react.mdx 2023-07-23 15:44:54 -04:00
9ed516ccb6 Uncomment Google SSO for signup 2023-07-24 02:35:51 +07:00
067ade94c8 Merge branch 'main' of https://github.com/Infisical/infisical 2023-07-24 01:54:09 +07:00
446edb6ed9 Add CLI support for SAML SSO 2023-07-24 01:53:56 +07:00
896529b7c6 auto scope raw secrets GET with service token 2023-07-23 12:31:03 -04:00
5c836d1c10 Merge pull request #779 from afrieirham/integration/digital-ocean-app-platform
Digital Ocean App Platform Integration
2023-07-23 23:01:35 +07:00
409d46aa10 Fix merge conflicts 2023-07-23 22:55:18 +07:00
682c63bc2a Fix DigitalOcean getApps case where there are no apps 2023-07-23 22:42:41 +07:00
1419371588 Merge pull request #776 from afrieirham/integration/cloud66
Cloud 66 integration
2023-07-23 22:19:23 +07:00
77fdb6307c Optimize Cloud66 integration sync function 2023-07-23 22:16:27 +07:00
c61bba2b6b docs: add digital ocean app platform integration guide 2023-07-23 22:39:08 +08:00
2dc0563042 view: fix integration name only show 3 words 2023-07-23 22:02:59 +08:00
b5fb2ef354 feat: DO app platform integration 2023-07-23 22:01:48 +08:00
dc01758946 Update SAML Okta docs screenshots 2023-07-23 16:59:08 +07:00
1f8683f59e Merge pull request #777 from Infisical/saml-docs
Add docs for Okta SAML 2.0 SSO
2023-07-23 16:49:47 +07:00
a5273cb86f Add docs for Okta SAML 2.0 SSO 2023-07-23 16:47:43 +07:00
d48b5157d4 docs: add cloud 66 integration guide 2023-07-23 17:39:29 +08:00
94a23bfa23 feat: add cloud 66 integration 2023-07-23 16:36:26 +08:00
fcdfa424bc Restrict changing user auth methods if SAML SSO is enforced 2023-07-23 15:19:17 +07:00
3fba1b3ff7 Merge pull request #774 from Infisical/saml-sso-edge-cases
Block inviting members to organization if SAML SSO is configured
2023-07-23 13:27:30 +07:00
953eed70b2 Add back attribution source for non-SAML SSO case 2023-07-23 13:24:12 +07:00
39ba795604 Block inviting members to organization if SAML SSO is configured 2023-07-23 13:05:37 +07:00
5b36227321 Merge pull request #773 from Infisical/debug-google-sso
Initialize organization bot upon creating organization
2023-07-23 12:07:45 +07:00
70d04be978 Initialize organization bot upon creating organization 2023-07-23 12:03:39 +07:00
565f234921 Merge pull request #772 from Infisical/switch-to-google-sso
Add user support for changing authentication methods
2023-07-22 12:38:22 +07:00
ab43e32982 Add user support for changing auth methods 2023-07-22 12:33:57 +07:00
be677fd6c2 disable token error 2023-07-21 18:41:32 -04:00
edef22d28e Terraform Cloud integration 2023-07-18 23:14:41 +01:00
76f43ab6b4 Terraform Cloud integration 2023-07-18 21:08:30 +01:00
ceeebc24fa Terraform Cloud integration 2023-07-16 21:12:35 +01:00
a3836b970a Terraform Cloud integration 2023-07-10 23:44:55 +01:00
3e975dc4f0 Terraform Cloud integration 2023-07-08 00:07:38 +01:00
152 changed files with 3515 additions and 341 deletions

View File

@ -108,6 +108,22 @@ brews:
zsh_completion.install "completions/infisical.zsh" => "_infisical"
fish_completion.install "completions/infisical.fish"
man1.install "manpages/infisical.1.gz"
- name: 'infisical@{{.Version}}'
tap:
owner: Infisical
name: homebrew-get-cli
commit_author:
name: "Infisical"
email: ai@infisical.com
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:
- id: infisical

File diff suppressed because one or more lines are too long

View File

@ -10,6 +10,8 @@
"plugin:@typescript-eslint/recommended"
],
"rules": {
"no-empty-function": "off",
"@typescript-eslint/no-empty-function": "off",
"no-console": 2,
"quotes": [
"error",

View File

@ -1,6 +1,7 @@
import { Types } from "mongoose";
import { Request, Response } from "express";
import { MembershipOrg, Organization, User } from "../../models";
import { SSOConfig } from "../../ee/models";
import { deleteMembershipOrg as deleteMemberFromOrg } from "../../helpers/membershipOrg";
import { createToken } from "../../helpers/auth";
import { updateSubscriptionOrgQuantity } from "../../helpers/organization";
@ -110,6 +111,18 @@ export const inviteUserToOrganization = async (req: Request, res: Response) => {
}
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) {
// case: limit imposed on number of members allowed

View File

@ -30,7 +30,6 @@ export const createSecretImport = async (req: Request, res: Response) => {
if (doesImportExist) {
throw BadRequestError({ message: "Secret import already exist" });
}
importSecDoc.imports.push({
environment: secretImport.environment,
secretPath: secretImport.secretPath

View File

@ -830,7 +830,7 @@ export const getSecrets = async (req: Request, res: Response) => {
// TODO(akhilmhdh) - secret-imp change this to org type
let importedSecrets: any[] = [];
if (include_imports) {
if (include_imports === "true") {
importedSecrets = await getAllImportedSecrets(workspaceId, environment, folderId as string);
}

View File

@ -4,6 +4,7 @@ import crypto from "crypto";
import bcrypt from "bcrypt";
import {
APIKeyData,
AuthProvider,
MembershipOrg,
TokenVersion,
User
@ -81,25 +82,66 @@ export const updateMyMfaEnabled = async (req: Request, res: Response) => {
}
/**
* Update the current user's name [firstName, lastName].
* Update name of the current user to [firstName, lastName].
* @param req
* @param res
* @returns
*/
export const updateName = async (req: Request, res: Response) => {
const { firstName, lastName }: { firstName: string; lastName: string; } = req.body;
req.user.firstName = firstName;
req.user.lastName = lastName || "";
const {
firstName,
lastName
}: {
firstName: string;
lastName: string;
} = req.body;
await req.user.save();
const user = req.user;
const user = await User.findByIdAndUpdate(
req.user._id.toString(),
{
firstName,
lastName: lastName ?? ""
},
{
new: true
}
);
return res.status(200).send({
user,
});
}
/**
* Update auth provider of the current user to [authProvider]
* @param req
* @param res
* @returns
*/
export const updateAuthProvider = async (req: Request, res: Response) => {
const {
authProvider
} = req.body;
if (req.user?.authProvider === AuthProvider.OKTA_SAML) return res.status(400).send({
message: "Failed to update user authentication method because SAML SSO is enforced"
});
const user = await User.findByIdAndUpdate(
req.user._id.toString(),
{
authProvider
},
{
new: true
}
);
return res.status(200).send({
user
});
}
/**
* Return organizations that the current user is part of.
* @param req

View File

@ -56,7 +56,7 @@ export const login1 = async (req: Request, res: Response) => {
if (!user) throw new Error("Failed to find user");
if (user.authProvider) {
if (user.authProvider && user.authProvider !== AuthProvider.EMAIL) {
await validateProviderAuthToken({
email,
user,
@ -117,7 +117,7 @@ export const login2 = async (req: Request, res: Response) => {
if (!user) throw new Error("Failed to find user");
if (user.authProvider) {
if (user.authProvider && user.authProvider !== AuthProvider.EMAIL) {
await validateProviderAuthToken({
email,
user,

View File

@ -3,12 +3,15 @@ import { Types } from "mongoose";
import { EventService, SecretService } from "../../services";
import { eventPushSecrets } from "../../events";
import { BotService } from "../../services";
import { repackageSecretToRaw } from "../../helpers/secrets";
import { containsGlobPatterns, repackageSecretToRaw } from "../../helpers/secrets";
import { encryptSymmetric128BitHexKeyUTF8 } from "../../utils/crypto";
import { getAllImportedSecrets } from "../../services/SecretImportService";
import Folder from "../../models/folder";
import { getFolderByPath } from "../../services/FolderService";
import { BadRequestError } from "../../utils/errors";
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
@ -17,11 +20,30 @@ import { BadRequestError } from "../../utils/errors";
* @param res
*/
export const getSecretsRaw = async (req: Request, res: Response) => {
const workspaceId = req.query.workspaceId as string;
const environment = req.query.environment as string;
const secretPath = req.query.secretPath as string;
let workspaceId = req.query.workspaceId as string;
let environment = req.query.environment as string;
let secretPath = req.query.secretPath as string;
const includeImports = req.query.include_imports as string;
// if the service token has single scope, it will get all secrets for that scope by default
const serviceTokenDetails: IServiceTokenData = req?.serviceTokenData;
if (serviceTokenDetails && 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({
workspaceId: new Types.ObjectId(workspaceId),
environment,
@ -33,7 +55,7 @@ export const getSecretsRaw = async (req: Request, res: Response) => {
workspaceId: new Types.ObjectId(workspaceId)
});
if (includeImports) {
if (includeImports === "true") {
const folders = await Folder.findOne({ workspace: workspaceId, environment });
let folderId = "root";
// if folder exist get it and replace folderid with new one
@ -271,7 +293,7 @@ export const getSecrets = async (req: Request, res: Response) => {
authData: req.authData
});
if (includeImports) {
if (includeImports === "true") {
const folders = await Folder.findOne({ workspace: workspaceId, environment });
let folderId = "root";
// if folder exist get it and replace folderid with new one

View File

@ -2,6 +2,7 @@ import * as secretController from "./secretController";
import * as secretSnapshotController from "./secretSnapshotController";
import * as organizationsController from "./organizationsController";
import * as ssoController from "./ssoController";
import * as usersController from "./usersController";
import * as workspaceController from "./workspaceController";
import * as actionController from "./actionController";
import * as membershipController from "./membershipController";
@ -12,6 +13,7 @@ export {
secretSnapshotController,
organizationsController,
ssoController,
usersController,
workspaceController,
actionController,
membershipController,

View File

@ -10,6 +10,7 @@ import { getSSOConfigHelper } from "../../helpers/organizations";
import { client } from "../../../config";
import { ResourceNotFoundError } from "../../../utils/errors";
import { getSiteURL } from "../../../config";
import { EELicenseService } from "../../services";
/**
* Redirect user to appropriate SSO endpoint after successful authentication
@ -58,6 +59,12 @@ export const updateSSOConfig = async (req: Request, res: Response) => {
cert,
audience
} = req.body;
const plan = await EELicenseService.getPlan(organizationId);
if (!plan.samlSSO) return res.status(400).send({
message: "Failed to update SAML SSO configuration due to plan restriction. Upgrade plan to update SSO configuration."
});
interface PatchUpdate {
authProvider?: string;
@ -203,6 +210,12 @@ export const createSSOConfig = async (req: Request, res: Response) => {
cert,
audience
} = req.body;
const plan = await EELicenseService.getPlan(organizationId);
if (!plan.samlSSO) return res.status(400).send({
message: "Failed to create SAML SSO configuration due to plan restriction. Upgrade plan to add SSO configuration."
});
const key = await BotOrgService.getSymmetricKey(
new Types.ObjectId(organizationId)

View File

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

View File

@ -3,16 +3,20 @@ import { PipelineStage, Types } from "mongoose";
import { Secret } from "../../../models";
import {
FolderVersion,
IPType,
ISecretVersion,
Log,
SecretSnapshot,
SecretVersion,
TFolderRootVersionSchema,
TrustedIP
} from "../../models";
import { EESecretService } from "../../services";
import { getLatestSecretVersionIds } from "../../helpers/secretVersion";
import Folder, { TFolderSchema } from "../../../models/folder";
import { searchByFolderId } from "../../../services/FolderService";
import { EELicenseService } from "../../services";
import { extractIPDetails, isValidIpOrCidr } from "../../../utils/ip";
/**
* Return secret snapshots for workspace with id [workspaceId]
@ -588,3 +592,147 @@ export const getWorkspaceLogs = async (req: Request, res: Response) => {
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
});
}

View File

@ -66,6 +66,4 @@ const actionSchema = new Schema<IAction>(
}
);
const Action = model<IAction>("Action", actionSchema);
export default Action;
export const Action = model<IAction>("Action", actionSchema);

View File

@ -52,9 +52,7 @@ const folderRootVersionSchema = new Schema<TFolderRootVersionSchema>(
}
);
const FolderVersion = model<TFolderRootVersionSchema>(
export const FolderVersion = model<TFolderRootVersionSchema>(
"FolderVersion",
folderRootVersionSchema
);
export default FolderVersion;
);

View File

@ -1,21 +1,7 @@
import SecretSnapshot, { ISecretSnapshot } from "./secretSnapshot";
import SecretVersion, { ISecretVersion } from "./secretVersion";
import FolderVersion, { TFolderRootVersionSchema } from "./folderVersion";
import Log, { ILog } from "./log";
import Action, { IAction } from "./action";
import SSOConfig, { ISSOConfig } from "./ssoConfig";
export {
SecretSnapshot,
ISecretSnapshot,
SecretVersion,
ISecretVersion,
FolderVersion,
TFolderRootVersionSchema,
Log,
ILog,
Action,
IAction,
SSOConfig,
ISSOConfig
};
export * from "./secretSnapshot";
export * from "./secretVersion";
export * from "./folderVersion";
export * from "./log";
export * from "./action";
export * from "./ssoConfig";
export * from "./trustedIp";

View File

@ -69,6 +69,4 @@ const logSchema = new Schema<ILog>(
}
);
const Log = model<ILog>("Log", logSchema);
export default Log;
export const Log = model<ILog>("Log", logSchema);

View File

@ -46,9 +46,7 @@ const secretSnapshotSchema = new Schema<ISecretSnapshot>(
}
);
const SecretSnapshot = model<ISecretSnapshot>(
export const SecretSnapshot = model<ISecretSnapshot>(
"SecretSnapshot",
secretSnapshotSchema
);
export default SecretSnapshot;
);

View File

@ -124,9 +124,7 @@ const secretVersionSchema = new Schema<ISecretVersion>(
}
);
const SecretVersion = model<ISecretVersion>(
export const SecretVersion = model<ISecretVersion>(
"SecretVersion",
secretVersionSchema
);
export default SecretVersion;
);

View File

@ -77,6 +77,4 @@ const ssoConfigSchema = new Schema<ISSOConfig>(
}
);
const SSOConfig = model<ISSOConfig>("SSOConfig", ssoConfigSchema);
export default SSOConfig;
export const SSOConfig = model<ISSOConfig>("SSOConfig", ssoConfigSchema);

View File

@ -0,0 +1,54 @@
import { Schema, Types, model } from "mongoose";
export enum IPType {
IPV4 = "ipv4",
IPV6 = "ipv6"
}
export interface ITrustedIP {
_id: Types.ObjectId;
workspace: Types.ObjectId;
ipAddress: string;
type: "ipv4" | "ipv6", // either IPv4/IPv6 address or network IPv4/IPv6 address
isActive: boolean;
comment: string;
prefix?: number; // CIDR
}
const trustedIpSchema = new Schema<ITrustedIP>(
{
workspace: {
type: Schema.Types.ObjectId,
ref: "Workspace",
required: true
},
ipAddress: {
type: String,
required: true
},
type: {
type: String,
enum: [
IPType.IPV4,
IPType.IPV6
],
required: true
},
prefix: {
type: Number,
required: false
},
isActive: {
type: Boolean,
required: true
},
comment: {
type: String
}
},
{
timestamps: true
}
);
export const TrustedIP = model<ITrustedIP>("TrustedIP", trustedIpSchema);

View File

@ -2,6 +2,7 @@ import secret from "./secret";
import secretSnapshot from "./secretSnapshot";
import organizations from "./organizations";
import sso from "./sso";
import users from "./users";
import workspace from "./workspace";
import action from "./action";
import cloudProducts from "./cloudProducts";
@ -11,6 +12,7 @@ export {
secretSnapshot,
organizations,
sso,
users,
workspace,
action,
cloudProducts,

View File

@ -18,10 +18,15 @@ import {
router.get(
"/redirect/google",
authLimiter,
passport.authenticate("google", {
scope: ["profile", "email"],
session: false,
})
(req, res, next) => {
passport.authenticate("google", {
scope: ["profile", "email"],
session: false,
...(req.query.callback_port ? {
state: req.query.callback_port as string
} : {})
})(req, res, next);
}
);
router.get(
@ -36,16 +41,22 @@ router.get(
router.get(
"/redirect/saml2/:ssoIdentifier",
authLimiter,
passport.authenticate("saml", {
failureRedirect: "/login/fail"
})
(req, res, next) => {
const options = {
failureRedirect: "/",
additionalParams: {
RelayState: req.query.callback_port ?? ""
},
};
passport.authenticate("saml", options)(req, res, next);
}
);
router.post("/saml2/:ssoIdentifier",
passport.authenticate("saml", {
failureRedirect: "/login/provider/error",
failureFlash: true,
session: false
session: false
}),
ssoController.redirectSSO
);

View File

@ -0,0 +1,17 @@
import express from "express";
const router = express.Router();
import {
requireAuth
} from "../../../middleware";
import { AUTH_MODE_API_KEY, AUTH_MODE_JWT } from "../../../variables";
import { usersController } from "../../controllers/v1";
router.get(
"/me/ip",
requireAuth({
acceptedAuthModes: [AUTH_MODE_JWT, AUTH_MODE_API_KEY],
}),
usersController.getMyIp
);
export default router;

View File

@ -6,13 +6,18 @@ import {
validateRequest,
} from "../../../middleware";
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";
router.get(
"/:workspaceId/secret-snapshots",
requireAuth({
acceptedAuthModes: ["jwt", "apiKey"],
acceptedAuthModes: [AUTH_MODE_JWT, AUTH_MODE_API_KEY],
}),
requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER],
@ -30,7 +35,7 @@ router.get(
router.get(
"/:workspaceId/secret-snapshots/count",
requireAuth({
acceptedAuthModes: ["jwt"],
acceptedAuthModes: [AUTH_MODE_JWT],
}),
requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER],
@ -46,7 +51,7 @@ router.get(
router.post(
"/:workspaceId/secret-snapshots/rollback",
requireAuth({
acceptedAuthModes: ["jwt", "apiKey"],
acceptedAuthModes: [AUTH_MODE_JWT, AUTH_MODE_API_KEY],
}),
requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER],
@ -63,7 +68,7 @@ router.post(
router.get(
"/:workspaceId/logs",
requireAuth({
acceptedAuthModes: ["jwt", "apiKey"],
acceptedAuthModes: [AUTH_MODE_JWT, AUTH_MODE_API_KEY],
}),
requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER],
@ -79,4 +84,66 @@ router.get(
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;

View File

@ -26,6 +26,7 @@ interface FeatureSet {
environmentsUsed: number;
secretVersioning: boolean;
pitRecovery: boolean;
ipAllowlisting: boolean;
rbac: boolean;
customRateLimits: boolean;
customAlerts: boolean;
@ -60,6 +61,7 @@ class EELicenseService {
environmentsUsed: 0,
secretVersioning: true,
pitRecovery: false,
ipAllowlisting: false,
rbac: true,
customRateLimits: true,
customAlerts: true,

View File

@ -3,12 +3,100 @@ import { client, getEncryptionKey, getRootEncryptionKey } from "../config";
import { BotOrg } from "../models";
import { decryptSymmetric128BitHexKeyUTF8 } from "../utils/crypto";
import {
ALGORITHM_AES_256_GCM,
ENCODING_SCHEME_BASE64,
ENCODING_SCHEME_UTF8
} from "../variables";
import { InternalServerError } from "../utils/errors";
import { encryptSymmetric128BitHexKeyUTF8, generateKeyPair } from "../utils/crypto";
// 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) => {
const rootEncryptionKey = await getRootEncryptionKey();

View File

@ -14,6 +14,9 @@ import {
licenseKeyRequest,
licenseServerKeyRequest,
} from "../config/request";
import {
createBotOrg
} from "./botOrg";
/**
* Create an organization with name [name]
@ -29,6 +32,7 @@ export const createOrganization = async ({
name: string;
email: string;
}) => {
const licenseServerKey = await getLicenseServerKey();
let organization;
@ -52,6 +56,12 @@ export const createOrganization = async ({
}).save();
}
// initialize bot for organization
await createBotOrg({
name,
organizationId: organization._id
});
return organization;
};

View File

@ -44,6 +44,7 @@ import { EELogService, EESecretService } from "../ee/services";
import { getAuthDataPayloadIdObj, getAuthDataPayloadUserObj } from "../utils/auth";
import { getFolderIdFromServiceToken } from "../services/FolderService";
import picomatch from "picomatch";
import path from "path";
export const isValidScope = (
authPayload: IServiceTokenData,
@ -60,6 +61,13 @@ export const isValidScope = (
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.
*

View File

@ -5,6 +5,10 @@ import {
Secret,
Workspace,
} from "../models";
import {
IPType,
TrustedIP
} from "../ee/models";
import { createBot } from "../helpers/bot";
import { EELicenseService } from "../ee/services";
import { SecretService } from "../services";
@ -40,6 +44,16 @@ export const createWorkspace = async ({
await SecretService.createSecretBlindIndexData({
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);

View File

@ -22,7 +22,8 @@ import {
sso as eeSSORouter,
secret as eeSecretRouter,
secretSnapshot as eeSecretSnapshotRouter,
workspace as eeWorkspaceRouter
users as eeUsersRouter,
workspace as eeWorkspaceRouter,
} from "./ee/routes/v1";
import {
auth as v1AuthRouter,
@ -129,6 +130,7 @@ const main = async () => {
// (EE) routes
app.use("/api/v1/secret", eeSecretRouter);
app.use("/api/v1/secret-snapshot", eeSecretSnapshotRouter);
app.use("/api/v1/users", eeUsersRouter);
app.use("/api/v1/workspace", eeWorkspaceRouter);
app.use("/api/v1/action", eeActionRouter);
app.use("/api/v1/organizations", eeOrganizationsRouter);

View File

@ -1,6 +1,3 @@
import { Octokit } from "@octokit/rest";
import { IIntegrationAuth } from "../models";
import { standardRequest } from "../config/request";
import {
INTEGRATION_AWS_PARAMETER_STORE,
INTEGRATION_AWS_SECRET_MANAGER,
@ -13,8 +10,12 @@ import {
INTEGRATION_CIRCLECI_API_URL,
INTEGRATION_CLOUDFLARE_PAGES,
INTEGRATION_CLOUDFLARE_PAGES_API_URL,
INTEGRATION_CLOUD_66,
INTEGRATION_CLOUD_66_API_URL,
INTEGRATION_CODEFRESH,
INTEGRATION_CODEFRESH_API_URL,
INTEGRATION_DIGITAL_OCEAN_API_URL,
INTEGRATION_DIGITAL_OCEAN_APP_PLATFORM,
INTEGRATION_FLYIO,
INTEGRATION_FLYIO_API_URL,
INTEGRATION_GITHUB,
@ -32,11 +33,16 @@ import {
INTEGRATION_RENDER_API_URL,
INTEGRATION_SUPABASE,
INTEGRATION_SUPABASE_API_URL,
INTEGRATION_TERRAFORM_CLOUD,
INTEGRATION_TERRAFORM_CLOUD_API_URL,
INTEGRATION_TRAVISCI,
INTEGRATION_TRAVISCI_API_URL,
INTEGRATION_VERCEL,
INTEGRATION_VERCEL_API_URL
} from "../variables";
import { IIntegrationAuth } from "../models";
import { Octokit } from "@octokit/rest";
import { standardRequest } from "../config/request";
interface App {
name: string;
@ -130,6 +136,12 @@ const getApps = async ({
serverId: accessId
});
break;
case INTEGRATION_TERRAFORM_CLOUD:
apps = await getAppsTerraformCloud({
accessToken,
workspacesId: accessId,
});
break;
case INTEGRATION_TRAVISCI:
apps = await getAppsTravisCI({
accessToken,
@ -162,6 +174,16 @@ const getApps = async ({
accessToken,
});
break;
case INTEGRATION_DIGITAL_OCEAN_APP_PLATFORM:
apps = await getAppsDigitalOceanAppPlatform({
accessToken
});
break;
case INTEGRATION_CLOUD_66:
apps = await getAppsCloud66({
accessToken,
});
break;
}
return apps;
@ -549,6 +571,43 @@ const getAppsTravisCI = async ({ accessToken }: { accessToken: string }) => {
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
* @param {Object} obj
@ -819,7 +878,6 @@ const getAppsBitBucket = async ({
* @returns {Object[]} apps - names of Supabase apps
* @returns {String} apps.name - name of Supabase app
*/
const getAppsCodefresh = async ({
accessToken,
}: {
@ -842,4 +900,106 @@ const getAppsCodefresh = async ({
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 };

View File

@ -1,14 +1,10 @@
import _ from "lodash";
import AWS from "aws-sdk";
import {
CreateSecretCommand,
GetSecretValueCommand,
ResourceNotFoundException,
SecretsManagerClient,
UpdateSecretCommand,
UpdateSecretCommand
} from "@aws-sdk/client-secrets-manager";
import { Octokit } from "@octokit/rest";
import sodium from "libsodium-wrappers";
import { IIntegration, IIntegrationAuth } from "../models";
import {
INTEGRATION_AWS_PARAMETER_STORE,
@ -22,8 +18,12 @@ import {
INTEGRATION_CIRCLECI_API_URL,
INTEGRATION_CLOUDFLARE_PAGES,
INTEGRATION_CLOUDFLARE_PAGES_API_URL,
INTEGRATION_CLOUD_66,
INTEGRATION_CLOUD_66_API_URL,
INTEGRATION_CODEFRESH,
INTEGRATION_CODEFRESH_API_URL,
INTEGRATION_DIGITAL_OCEAN_API_URL,
INTEGRATION_DIGITAL_OCEAN_APP_PLATFORM,
INTEGRATION_FLYIO,
INTEGRATION_FLYIO_API_URL,
INTEGRATION_GITHUB,
@ -42,11 +42,17 @@ import {
INTEGRATION_RENDER_API_URL,
INTEGRATION_SUPABASE,
INTEGRATION_SUPABASE_API_URL,
INTEGRATION_TERRAFORM_CLOUD,
INTEGRATION_TERRAFORM_CLOUD_API_URL,
INTEGRATION_TRAVISCI,
INTEGRATION_TRAVISCI_API_URL,
INTEGRATION_VERCEL,
INTEGRATION_VERCEL_API_URL
} 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";
/**
@ -189,6 +195,13 @@ const syncSecrets = async ({
accessToken,
});
break;
case INTEGRATION_TERRAFORM_CLOUD:
await syncSecretsTerraformCloud({
integration,
secrets,
accessToken,
});
break;
case INTEGRATION_HASHICORP_VAULT:
await syncSecretsHashiCorpVault({
integration,
@ -220,6 +233,20 @@ const syncSecrets = async ({
accessToken,
});
break;
case INTEGRATION_DIGITAL_OCEAN_APP_PLATFORM:
await syncSecretsDigitalOceanAppPlatform({
integration,
secrets,
accessToken,
});
break;
case INTEGRATION_CLOUD_66:
await syncSecretsCloud66({
integration,
secrets,
accessToken
});
break;
}
};
@ -1822,6 +1849,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
* @param {Object} obj
@ -2068,10 +2195,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 {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 Codefresh integration
*/
@ -2101,4 +2229,141 @@ 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"
}
}
);
}
}
};
export { syncSecrets };

View File

@ -18,6 +18,7 @@ const requireWorkspaceAuth = ({
requiredPermissions = [],
requireBlindIndicesEnabled = false,
requireE2EEOff = false,
checkIPAllowlist = false
}: {
acceptedRoles: Array<"admin" | "member">;
locationWorkspaceId: req;
@ -25,6 +26,7 @@ const requireWorkspaceAuth = ({
requiredPermissions?: string[];
requireBlindIndicesEnabled?: boolean;
requireE2EEOff?: boolean;
checkIPAllowlist?: boolean;
}) => {
return async (req: Request, res: Response, next: NextFunction) => {
const workspaceId = req[locationWorkspaceId]?.workspaceId;
@ -39,6 +41,7 @@ const requireWorkspaceAuth = ({
requiredPermissions,
requireBlindIndicesEnabled,
requireE2EEOff,
checkIPAllowlist
});
if (membership) {

View File

@ -1,4 +1,3 @@
import { Schema, Types, model } from "mongoose";
import {
INTEGRATION_AWS_PARAMETER_STORE,
INTEGRATION_AWS_SECRET_MANAGER,
@ -7,7 +6,9 @@ import {
INTEGRATION_CHECKLY,
INTEGRATION_CIRCLECI,
INTEGRATION_CLOUDFLARE_PAGES,
INTEGRATION_CLOUD_66,
INTEGRATION_CODEFRESH,
INTEGRATION_DIGITAL_OCEAN_APP_PLATFORM,
INTEGRATION_FLYIO,
INTEGRATION_GITHUB,
INTEGRATION_GITLAB,
@ -18,9 +19,11 @@ import {
INTEGRATION_RAILWAY,
INTEGRATION_RENDER,
INTEGRATION_SUPABASE,
INTEGRATION_TERRAFORM_CLOUD,
INTEGRATION_TRAVISCI,
INTEGRATION_VERCEL
} from "../variables";
import { Schema, Types, model } from "mongoose";
export interface IIntegration {
_id: Types.ObjectId;
@ -55,10 +58,13 @@ export interface IIntegration {
| "travisci"
| "supabase"
| "checkly"
| "terraform-cloud"
| "hashicorp-vault"
| "cloudflare-pages"
| "bitbucket"
| "codefresh"
| "digital-ocean-app-platform"
| "cloud-66"
integrationAuth: Types.ObjectId;
}
@ -146,10 +152,13 @@ const integrationSchema = new Schema<IIntegration>(
INTEGRATION_TRAVISCI,
INTEGRATION_SUPABASE,
INTEGRATION_CHECKLY,
INTEGRATION_TERRAFORM_CLOUD,
INTEGRATION_HASHICORP_VAULT,
INTEGRATION_CLOUDFLARE_PAGES,
INTEGRATION_BITBUCKET,
INTEGRATION_CODEFRESH
INTEGRATION_DIGITAL_OCEAN_APP_PLATFORM,
INTEGRATION_CODEFRESH,
INTEGRATION_CLOUD_66,
],
required: true,
},

View File

@ -1,4 +1,3 @@
import { Document, Schema, Types, model } from "mongoose";
import {
ALGORITHM_AES_256_GCM,
ENCODING_SCHEME_BASE64,
@ -9,7 +8,9 @@ import {
INTEGRATION_BITBUCKET,
INTEGRATION_CIRCLECI,
INTEGRATION_CLOUDFLARE_PAGES,
INTEGRATION_CLOUD_66,
INTEGRATION_CODEFRESH,
INTEGRATION_DIGITAL_OCEAN_APP_PLATFORM,
INTEGRATION_FLYIO,
INTEGRATION_GITHUB,
INTEGRATION_GITLAB,
@ -20,9 +21,11 @@ import {
INTEGRATION_RAILWAY,
INTEGRATION_RENDER,
INTEGRATION_SUPABASE,
INTEGRATION_TERRAFORM_CLOUD,
INTEGRATION_TRAVISCI,
INTEGRATION_VERCEL
} from "../variables";
import { Document, Schema, Types, model } from "mongoose";
export interface IIntegrationAuth extends Document {
_id: Types.ObjectId;
@ -46,7 +49,10 @@ export interface IIntegrationAuth extends Document {
| "checkly"
| "cloudflare-pages"
| "codefresh"
| "bitbucket";
| "digital-ocean-app-platform"
| "bitbucket"
| "cloud-66"
| "terraform-cloud";
teamId: string;
accountId: string;
url: string;
@ -90,10 +96,13 @@ const integrationAuthSchema = new Schema<IIntegrationAuth>(
INTEGRATION_LARAVELFORGE,
INTEGRATION_TRAVISCI,
INTEGRATION_SUPABASE,
INTEGRATION_TERRAFORM_CLOUD,
INTEGRATION_HASHICORP_VAULT,
INTEGRATION_CLOUDFLARE_PAGES,
INTEGRATION_BITBUCKET,
INTEGRATION_CODEFRESH
INTEGRATION_DIGITAL_OCEAN_APP_PLATFORM,
INTEGRATION_CODEFRESH,
INTEGRATION_CLOUD_66,
],
required: true,
},

View File

@ -1,6 +1,7 @@
import { Document, Schema, Types, model } from "mongoose";
export enum AuthProvider {
EMAIL = "email",
GOOGLE = "google",
OKTA_SAML = "okta-saml"
}

View File

@ -46,7 +46,7 @@ router.delete(
body("secretImportPath").isString().exists().trim(),
body("secretImportEnv").isString().exists().trim(),
validateRequest,
secretImportController.updateSecretImport
secretImportController.deleteSecretImport
);
router.get(

View File

@ -10,6 +10,9 @@ import {
AUTH_MODE_API_KEY,
AUTH_MODE_JWT,
} from "../../variables";
import {
AuthProvider
} from "../../models";
router.get(
"/me",
@ -34,11 +37,25 @@ router.patch(
requireAuth({
acceptedAuthModes: [AUTH_MODE_JWT, AUTH_MODE_API_KEY],
}),
body("firstName").exists(),
body("firstName").exists().isString(),
body("lastName").isString(),
validateRequest,
usersController.updateName
);
router.patch(
"/me/auth-provider",
requireAuth({
acceptedAuthModes: [AUTH_MODE_JWT, AUTH_MODE_API_KEY],
}),
body("authProvider").exists().isString().isIn([
AuthProvider.EMAIL,
AuthProvider.GOOGLE
]),
validateRequest,
usersController.updateAuthProvider
);
router.get(
"/me/organizations",
requireAuth({
@ -76,7 +93,7 @@ router.delete(
usersController.deleteAPIKey
);
router.get( // new
router.get(
"/me/sessions",
requireAuth({
acceptedAuthModes: [AUTH_MODE_JWT],
@ -84,7 +101,7 @@ router.get( // new
usersController.getMySessions
);
router.delete( // new
router.delete(
"/me/sessions",
requireAuth({
acceptedAuthModes: [AUTH_MODE_JWT],

View File

@ -18,8 +18,8 @@ import {
router.get(
"/raw",
query("workspaceId").exists().isString().trim(),
query("environment").exists().isString().trim(),
query("workspaceId").optional().isString().trim(),
query("environment").optional().isString().trim(),
query("secretPath").default("/").isString().trim(),
query("include_imports").optional().isBoolean().default(false),
validateRequest,
@ -31,14 +31,6 @@ router.get(
AUTH_MODE_SERVICE_ACCOUNT
]
}),
requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER],
locationWorkspaceId: "query",
locationEnvironment: "query",
requiredPermissions: [PERMISSION_READ_SECRETS],
requireBlindIndicesEnabled: true,
requireE2EEOff: true
}),
secretsController.getSecretsRaw
);
@ -64,7 +56,8 @@ router.get(
locationEnvironment: "query",
requiredPermissions: [PERMISSION_READ_SECRETS],
requireBlindIndicesEnabled: true,
requireE2EEOff: true
requireE2EEOff: true,
checkIPAllowlist: false
}),
secretsController.getSecretByNameRaw
);
@ -92,7 +85,8 @@ router.post(
locationEnvironment: "body",
requiredPermissions: [PERMISSION_WRITE_SECRETS],
requireBlindIndicesEnabled: true,
requireE2EEOff: true
requireE2EEOff: true,
checkIPAllowlist: false
}),
secretsController.createSecretRaw
);
@ -120,7 +114,8 @@ router.patch(
locationEnvironment: "body",
requiredPermissions: [PERMISSION_WRITE_SECRETS],
requireBlindIndicesEnabled: true,
requireE2EEOff: true
requireE2EEOff: true,
checkIPAllowlist: false
}),
secretsController.updateSecretByNameRaw
);
@ -147,7 +142,8 @@ router.delete(
locationEnvironment: "body",
requiredPermissions: [PERMISSION_WRITE_SECRETS],
requireBlindIndicesEnabled: true,
requireE2EEOff: true
requireE2EEOff: true,
checkIPAllowlist: false
}),
secretsController.deleteSecretByNameRaw
);
@ -172,7 +168,8 @@ router.get(
locationEnvironment: "query",
requiredPermissions: [PERMISSION_READ_SECRETS],
requireBlindIndicesEnabled: true,
requireE2EEOff: false
requireE2EEOff: false,
checkIPAllowlist: false
}),
secretsController.getSecrets
);
@ -207,7 +204,8 @@ router.post(
locationEnvironment: "body",
requiredPermissions: [PERMISSION_WRITE_SECRETS],
requireBlindIndicesEnabled: true,
requireE2EEOff: false
requireE2EEOff: false,
checkIPAllowlist: false
}),
secretsController.createSecret
);
@ -233,7 +231,8 @@ router.get(
locationWorkspaceId: "query",
locationEnvironment: "query",
requiredPermissions: [PERMISSION_READ_SECRETS],
requireBlindIndicesEnabled: true
requireBlindIndicesEnabled: true,
checkIPAllowlist: false
}),
secretsController.getSecretByName
);
@ -263,7 +262,8 @@ router.patch(
locationEnvironment: "body",
requiredPermissions: [PERMISSION_WRITE_SECRETS],
requireBlindIndicesEnabled: true,
requireE2EEOff: false
requireE2EEOff: false,
checkIPAllowlist: false
}),
secretsController.updateSecretByName
);
@ -290,7 +290,8 @@ router.delete(
locationEnvironment: "body",
requiredPermissions: [PERMISSION_WRITE_SECRETS],
requireBlindIndicesEnabled: true,
requireE2EEOff: false
requireE2EEOff: false,
checkIPAllowlist: false
}),
secretsController.deleteSecretByName
);

View File

@ -114,7 +114,10 @@ const initializePassport = async () => {
firstName,
lastName,
authProvider: user.authProvider,
isUserCompleted
isUserCompleted,
...(req.query.state ? {
callbackPort: req.query.state as string
} : {})
},
expiresIn: await getJwtProviderAuthLifetime(),
secret: await getJwtProviderAuthSecret(),
@ -153,7 +156,6 @@ const initializePassport = async () => {
},
},
async (req: any, profile: any, done: any) => {
if (!req.ssoConfig.isActive) return done(InternalServerError());
const organization = await Organization.findById(req.ssoConfig.organization);
@ -199,7 +201,10 @@ const initializePassport = async () => {
lastName,
organizationName: organization?.name,
authProvider: user.authProvider,
isUserCompleted
isUserCompleted,
...(req.body.RelayState ? {
callbackPort: req.body.RelayState as string
} : {})
},
expiresIn: await getJwtProviderAuthLifetime(),
secret: await getJwtProviderAuthSecret(),

View File

@ -0,0 +1 @@
export * from "./ip";

101
backend/src/utils/ip/ip.ts Normal file
View File

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

View File

@ -3,7 +3,13 @@ import crypto from "crypto";
import { Types } from "mongoose";
import { encryptSymmetric128BitHexKeyUTF8 } from "../crypto";
import { EESecretService } from "../../ee/services";
import { ISecretVersion, SecretSnapshot, SecretVersion } from "../../ee/models";
import {
IPType,
ISecretVersion,
SecretSnapshot,
SecretVersion,
TrustedIP
} from "../../ee/models";
import {
BackupPrivateKey,
Bot,
@ -177,7 +183,6 @@ export const backfillBotOrgs = async () => {
return new BotOrg({
name: "Infisical Bot",
organization: organizationToAddBot,
isActive: false,
publicKey,
encryptedSymmetricKey,
symmetricKeyIV,
@ -212,7 +217,6 @@ export const backfillBotOrgs = async () => {
return new BotOrg({
name: "Infisical Bot",
organization: organizationToAddBot,
isActive: false,
publicKey,
encryptedSymmetricKey,
symmetricKeyIV,
@ -551,3 +555,41 @@ export const backfillServiceTokenMultiScope = async () => {
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");
}
}

View File

@ -15,7 +15,8 @@ import {
backfillSecretFolders,
backfillSecretVersions,
backfillServiceToken,
backfillServiceTokenMultiScope
backfillServiceTokenMultiScope,
backfillTrustedIps
} from "./backfillData";
import {
reencryptBotOrgKeys,
@ -84,6 +85,7 @@ export const setup = async () => {
await backfillServiceToken();
await backfillIntegration();
await backfillServiceTokenMultiScope();
await backfillTrustedIps();
// re-encrypt any data previously encrypted under server hex 128-bit ENCRYPTION_KEY
// to base64 256-bit ROOT_ENCRYPTION_KEY

View File

@ -1,14 +1,15 @@
import net from "net";
import { Types } from "mongoose";
import {
IServiceAccount,
IServiceTokenData,
IUser,
SecretBlindIndexData,
ServiceAccount,
ServiceTokenData,
User,
Workspace,
} from "../models";
import {
TrustedIP
} from "../ee/models";
import { validateServiceAccountClientForWorkspace } from "./serviceAccount";
import { validateUserClientForWorkspace } from "./user";
import { validateServiceTokenDataClientForWorkspace } from "./serviceTokenData";
@ -24,6 +25,7 @@ import {
AUTH_MODE_SERVICE_TOKEN,
} from "../variables";
import { BotService } from "../services";
import { AuthData } from "../interfaces/middleware";
/**
* Validate authenticated clients for workspace with id [workspaceId] based
@ -43,17 +45,16 @@ export const validateClientForWorkspace = async ({
requiredPermissions,
requireBlindIndicesEnabled,
requireE2EEOff,
checkIPAllowlist
}: {
authData: {
authMode: string;
authPayload: IUser | IServiceAccount | IServiceTokenData;
};
authData: AuthData;
workspaceId: Types.ObjectId;
environment?: string;
acceptedRoles: Array<"admin" | "member">;
requiredPermissions?: string[];
requireBlindIndicesEnabled: boolean;
requireE2EEOff: boolean;
checkIPAllowlist: boolean;
}) => {
const workspace = await Workspace.findById(workspaceId);
@ -82,6 +83,8 @@ export const validateClientForWorkspace = async ({
message: "Failed workspace authorization due to end-to-end encryption not being disabled",
});
}
if (authData.authMode === AUTH_MODE_JWT && authData.authPayload instanceof User) {
const membership = await validateUserClientForWorkspace({
@ -107,6 +110,39 @@ export const validateClientForWorkspace = async ({
}
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({
serviceTokenData: authData.authPayload,
workspaceId,

View File

@ -25,10 +25,13 @@ export const INTEGRATION_CIRCLECI = "circleci";
export const INTEGRATION_TRAVISCI = "travisci";
export const INTEGRATION_SUPABASE = "supabase";
export const INTEGRATION_CHECKLY = "checkly";
export const INTEGRATION_TERRAFORM_CLOUD = "terraform-cloud";
export const INTEGRATION_HASHICORP_VAULT = "hashicorp-vault";
export const INTEGRATION_CLOUDFLARE_PAGES = "cloudflare-pages";
export const INTEGRATION_BITBUCKET = "bitbucket";
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_SET = new Set([
INTEGRATION_AZURE_KEY_VAULT,
INTEGRATION_HEROKU,
@ -43,10 +46,13 @@ export const INTEGRATION_SET = new Set([
INTEGRATION_TRAVISCI,
INTEGRATION_SUPABASE,
INTEGRATION_CHECKLY,
INTEGRATION_TERRAFORM_CLOUD,
INTEGRATION_HASHICORP_VAULT,
INTEGRATION_CLOUDFLARE_PAGES,
INTEGRATION_BITBUCKET,
INTEGRATION_CODEFRESH
INTEGRATION_DIGITAL_OCEAN_APP_PLATFORM,
INTEGRATION_CODEFRESH,
INTEGRATION_CLOUD_66
]);
// integration types
@ -76,9 +82,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_LARAVELFORGE_API_URL = "https://forge.laravel.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_BITBUCKET_API_URL = "https://api.bitbucket.org";
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 getIntegrationOptions = async () => {
const INTEGRATION_OPTIONS = [
@ -200,6 +209,15 @@ export const getIntegrationOptions = async () => {
clientId: await getClientIdGitLab(),
docsLink: "",
},
{
name: "Terraform Cloud",
slug: "terraform-cloud",
image: "Terraform Cloud.png",
isAvailable: true,
type: "pat",
cliendId: "",
docsLink: "",
},
{
name: "Travis CI",
slug: "travisci",
@ -271,7 +289,25 @@ export const getIntegrationOptions = async () => {
type: "pat",
clientId: "",
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: "",
},
]
return INTEGRATION_OPTIONS;

View File

@ -143,13 +143,13 @@ var runCmd = &cobra.Command{
err = executeMultipleCommandWithEnvs(command, len(secretsByKey), env)
if err != nil {
util.HandleError(err, "Unable to execute your chained command")
fmt.Println(err)
}
} else {
err = executeSingleCommandWithEnvs(args, len(secretsByKey), env)
if err != nil {
util.HandleError(err, "Unable to execute your single command")
fmt.Println(err)
}
}
},

View File

@ -7,8 +7,7 @@ in plaintext. Effectively, this means each such secret operation only requires 1
<AccordionGroup>
<Accordion title="Retrieve secrets">
Retrieve all secrets for an Infisical project and environment.
Retrieve all secrets for an Infisical project and environment.
<Tabs>
<Tab title="cURL">
```bash
@ -18,7 +17,12 @@ in plaintext. Effectively, this means each such secret operation only requires 1
```
</Tab>
</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>
The ID of the workspace
</ParamField>

View File

@ -6,19 +6,26 @@ The changelog below reflects new product developments and updates on a monthly b
## July 2023
- 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).
- Released [secret referencing and importing](https://infisical.com/docs/documentation/platform/secret-reference) across folders and environments.
- 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
- 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.
- 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.
## May 2023
- Released secret scanning capability for the CLI.
@ -26,7 +33,7 @@ The changelog below reflects new product developments and updates on a monthly b
- Completed penetration test.
- Released new landing page.
- Started SOC 2 (Type II) compliance certification preparation.
- Released new deployment options for Fly.io, Digital Ocean and Render.
- Released new deployment options for Fly.io, Digital Ocean and Render.
## April 2023

View File

@ -82,4 +82,28 @@ Password: `testInfisical1`
```bash
# To stop environment use Control+C (on Mac) CTRL+C (on Win) or
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;
```

View File

@ -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>
![IP whitelist](../../images/project-ip-whitelist.png)
## 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.
![IP whitelist add](../../images/project-ip-whitelist-add.png)

View File

@ -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.
![SAML Okta create app integration](../../images/saml-okta-1.png)
2. In the Create a New Application Integration dialog, select the SAML 2.0 radio button:
![SAML Okta create SAML 2.0 integration](../../images/saml-okta-2.png)
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`
![SAML Okta configure IDP fields](../../images/saml-okta-3.png)
<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`
![SAML Okta attribute statements](../../images/saml-okta-4.png)
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`.
![SAML Okta IDP values](../../images/saml-okta-5.png)
![SAML Okta paste values into Infisical](../../images/saml-okta-6.png)
6. Create the SSO configuration and copy your SSO identifier in Infisical; update `:identifier` from step 4 earlier to be this value.
![SAML Okta assignments](../../images/saml-okta-7.png)
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.
![SAML Okta assignment](../../images/saml-okta-8.png)
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>

View File

@ -45,8 +45,8 @@ To add an import, simply click on the `Add import` button and provide the enviro
![secret import change order](../../images/secret-import-add.png)
The hierarchy of importing secrets is governed by a "last-one-wins" rule. This means the sequence in which you import matters - the final folder imported will override secrets from any prior folders.
Moreover, any secrets you define directly in your environment will take precedence over secrets from any imported folders.
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.
![secret import change order](../../images/secret-import-change-order.png)

Binary file not shown.

After

Width:  |  Height:  |  Size: 890 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 814 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 742 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 969 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 812 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 218 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 228 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 241 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 202 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 301 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 220 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 345 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 650 KiB

After

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 313 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 439 KiB

BIN
docs/images/saml-okta-1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 264 KiB

BIN
docs/images/saml-okta-2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 381 KiB

BIN
docs/images/saml-okta-3.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 423 KiB

BIN
docs/images/saml-okta-4.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 316 KiB

BIN
docs/images/saml-okta-5.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 598 KiB

BIN
docs/images/saml-okta-6.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 443 KiB

BIN
docs/images/saml-okta-7.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 563 KiB

BIN
docs/images/saml-okta-8.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 386 KiB

View File

@ -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
![integrations](../../images/integrations.png)
## Enter your Cloud 66 Access Token
In Cloud 66 Dashboard, click on the top right icon > Account Settings > Access Token
![integrations cloud 66 dashboard](../../images/integrations-cloud-66-dashboard.png)
![integrations cloud 66 access token](../../images/integrations-cloud-66-access-token.png)
Create new Personal Access Token.
![integrations cloud 66 personal access token](../../images/integrations-cloud-66-pat.png)
Name it **infisical** and check **Public** and **Admin**. Then click "Create Token"
![integrations cloud 66 personal access token setup](../../images/integrations-cloud-66-pat-setup.png)
Copy and save your token.
![integrations cloud 66 copy API token](../../images/integrations-cloud-66-copy-pat.png)
### 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.
![integrations cloud 66 tile in infisical dashboard](../../images/integrations-cloud-66-infisical-dashboard.png)
<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".
![integrations cloud 66 tile in infisical dashboard](../../images/integrations-cloud-66-paste-pat.png)
## 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.
![integrations laravel forge](../../images/integrations-cloud-66-create.png)
<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!
![integrations laravel forge](../../images/integrations-cloud-66-done.png)

View File

@ -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"
![integrations digital ocean dashboard](../../images/integrations-do-dashboard.png)
Name it **infisical**, choose **No expiry**, and make sure to check **Write (optional)**. Then click on "Generate Token" and copy your API token.
![integrations digital ocean token modal](../../images/integrations-do-token-modal.png)
## 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.
![integrations](../../images/integrations.png)
<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".
![integrations infisical dashboard digital ocean integration](../../images/integrations-do-enter-token.png)
## Start integration
Select which Infisical environment secrets you want to sync to which Digital Ocean App and click "Create Integration".
![integrations digital ocean select projects](../../images/integrations-do-select-projects.png)
Done!
![integrations digital ocean integration success](../../images/integrations-do-success.png)

View File

@ -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
![integrations](../../images/integrations.png)
## Enter your Terraform Cloud API Token and Workspace Id
Obtain a Terraform Cloud API Token in User Settings > Tokens
![integrations terraform cloud dashboard](../../images/integrations-terraformcloud-dashboard.png)
![integrations terraform cloud tokens](../../images/integrations-terraformcloud-tokens.png)
Obtain your Terraform Cloud Workspace Id in Projects & Workspaces > Workspace > ID
![integrations terraform cloud projects & workspaces](../../images/integrations-terraformcloud-workspaces.png)
![integrations terraform cloud workspace id](../../images/integrations-terraformcloud-workspaceid.png)
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.
![integrations terraform cloud authorization](../../images/integrations-terraformcloud-auth.png)
<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.
![integrations terraform cloud](../../images/integrations-terraformcloud-create.png)
![integrations terraform cloud](../../images/integrations-terraformcloud.png)

View File

@ -26,3 +26,7 @@ infisical run -- <your application start command>
# Example
infisical run -- npm run dev
```
<Info>
React environment variables must be prefixed with `REACT_APP_` to show up within the application
</Info>

View File

@ -20,6 +20,7 @@ Missing an integration? [Throw in a request](https://github.com/Infisical/infisi
| [Render](/integrations/cloud/render) | Cloud | Available |
| [Laravel Forge](/integrations/cloud/laravel-forge) | Cloud | Available |
| [Railway](/integrations/cloud/railway) | Cloud | Available |
| [Terraform Cloud](/integrations/cloud/terraform-cloud) | Cloud | Available |
| [Fly.io](/integrations/cloud/flyio) | Cloud | Available |
| [Supabase](/integrations/cloud/supabase) | Cloud | Available |
| [Cloudflare Pages](/integrations/cloud/cloudflare-pages) | Cloud | Available |

View File

@ -66,12 +66,12 @@ metadata:
spec:
# 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
resyncInterval:
resyncInterval: 60
authentication:
serviceToken:
serviceTokenSecretReference:
secretName: service-token
secretNamespace: option
secretNamespace: default
secretsScope:
envSlug: dev
secretsPath: "/"

View File

@ -118,8 +118,10 @@
"documentation/platform/pit-recovery",
"documentation/platform/secret-versioning",
"documentation/platform/audit-logs",
"documentation/platform/token",
"documentation/platform/ip-allowlisting",
"documentation/platform/mfa",
"documentation/platform/token"
"documentation/platform/saml"
]
},
{
@ -135,7 +137,6 @@
"self-hosting/deployment-options/standalone-infisical",
"self-hosting/deployment-options/fly.io",
"self-hosting/deployment-options/render",
"self-hosting/deployment-options/laravel-forge",
"self-hosting/deployment-options/digital-ocean-marketplace"
]
},
@ -207,6 +208,12 @@
"integrations/cloud/aws-secret-manager"
]
},
{
"group": "Digital Ocean",
"pages": [
"integrations/cloud/digital-ocean-app-platform"
]
},
"integrations/cloud/heroku",
"integrations/cloud/vercel",
"integrations/cloud/netlify",
@ -215,10 +222,12 @@
"integrations/cloud/flyio",
"integrations/cloud/laravel-forge",
"integrations/cloud/supabase",
"integrations/cloud/terraform-cloud",
"integrations/cloud/cloudflare-pages",
"integrations/cloud/checkly",
"integrations/cloud/hashicorp-vault",
"integrations/cloud/azure-key-vault",
"integrations/cloud/cloud-66",
"integrations/cicd/githubactions",
"integrations/cicd/gitlab",
"integrations/cicd/circleci",
@ -324,7 +333,8 @@
"pages": [
"security/overview",
"security/data-model",
"security/mechanics"
"security/mechanics",
"security/service-tokens"
]
},
{

View File

@ -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, its 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.

View File

@ -10,7 +10,7 @@ However, the following functionality will be disabled.
- Sending invite links via email for projects to teammates
- 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):

View File

@ -19,10 +19,13 @@ const integrationSlugNameMapping: Mapping = {
travisci: "TravisCI",
supabase: "Supabase",
checkly: "Checkly",
'terraform-cloud': 'Terraform Cloud',
"hashicorp-vault": "Vault",
"cloudflare-pages": "Cloudflare Pages",
"codefresh": "Codefresh",
bitbucket: "BitBucket"
"digital-ocean-app-platform": "Digital Ocean App Platform",
bitbucket: "BitBucket",
"cloud-66": "Cloud 66"
};
const envMapping: Mapping = {

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 KiB

View File

@ -227,7 +227,7 @@
},
"password": {
"password": "Password",
"change": "Change password",
"change": "Change Password",
"current": "Current password",
"current-wrong": "The current password may be wrong",
"new": "New password",

File diff suppressed because one or more lines are too long

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