Merge pull request #785 from Infisical/network-access

Add support for IP allowlisting / trusted IPs
This commit is contained in:
BlackMagiq
2023-07-26 15:11:04 +07:00
committed by GitHub
46 changed files with 1191 additions and 71 deletions
+2
View File
@@ -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,
@@ -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
});
}
@@ -8,11 +8,14 @@ import {
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 +591,132 @@ 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 trustedIp = await TrustedIP.findOneAndUpdate(
{
_id: new Types.ObjectId(trustedIpId),
workspace: new Types.ObjectId(workspaceId),
},
{
ipAddress,
type,
prefix,
comment
},
{
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
});
}
+1 -3
View File
@@ -66,6 +66,4 @@ const actionSchema = new Schema<IAction>(
} }
); );
const Action = model<IAction>("Action", actionSchema); export const Action = model<IAction>("Action", actionSchema);
export default Action;
+1 -3
View File
@@ -52,9 +52,7 @@ const folderRootVersionSchema = new Schema<TFolderRootVersionSchema>(
} }
); );
const FolderVersion = model<TFolderRootVersionSchema>( export const FolderVersion = model<TFolderRootVersionSchema>(
"FolderVersion", "FolderVersion",
folderRootVersionSchema folderRootVersionSchema
); );
export default FolderVersion;
+7 -21
View File
@@ -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
};
+1 -3
View File
@@ -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;
+1 -3
View File
@@ -46,9 +46,7 @@ const secretSnapshotSchema = new Schema<ISecretSnapshot>(
} }
); );
const SecretSnapshot = model<ISecretSnapshot>( export const SecretSnapshot = model<ISecretSnapshot>(
"SecretSnapshot", "SecretSnapshot",
secretSnapshotSchema secretSnapshotSchema
); );
export default SecretSnapshot;
+1 -3
View File
@@ -124,9 +124,7 @@ const secretVersionSchema = new Schema<ISecretVersion>(
} }
); );
const SecretVersion = model<ISecretVersion>( export const SecretVersion = model<ISecretVersion>(
"SecretVersion", "SecretVersion",
secretVersionSchema secretVersionSchema
); );
export default SecretVersion;
+1 -3
View File
@@ -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;
+55
View File
@@ -0,0 +1,55 @@
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,
required: true
}
},
{
timestamps: true
}
);
export const TrustedIP = model<ITrustedIP>("TrustedIP", trustedIpSchema);
+2
View File
@@ -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,
+17
View File
@@ -0,0 +1,17 @@
import express from "express";
const router = express.Router();
import {
requireAuth
} from "../../../middleware";
import { AUTH_MODE_API_KEY, AUTH_MODE_JWT } from "../../../variables";
import { usersController } from "../../controllers/v1";
router.get(
"/me/ip",
requireAuth({
acceptedAuthModes: [AUTH_MODE_JWT, AUTH_MODE_API_KEY],
}),
usersController.getMyIp
);
export default router;
+72 -5
View File
@@ -6,13 +6,18 @@ import {
validateRequest, validateRequest,
} from "../../../middleware"; } from "../../../middleware";
import { body, param, query } from "express-validator"; import { body, param, query } from "express-validator";
import { ADMIN, MEMBER } from "../../../variables"; import {
ADMIN,
AUTH_MODE_API_KEY,
AUTH_MODE_JWT,
MEMBER
} from "../../../variables";
import { workspaceController } from "../../controllers/v1"; import { workspaceController } from "../../controllers/v1";
router.get( router.get(
"/:workspaceId/secret-snapshots", "/:workspaceId/secret-snapshots",
requireAuth({ requireAuth({
acceptedAuthModes: ["jwt", "apiKey"], acceptedAuthModes: [AUTH_MODE_JWT, AUTH_MODE_API_KEY],
}), }),
requireWorkspaceAuth({ requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER], acceptedRoles: [ADMIN, MEMBER],
@@ -30,7 +35,7 @@ router.get(
router.get( router.get(
"/:workspaceId/secret-snapshots/count", "/:workspaceId/secret-snapshots/count",
requireAuth({ requireAuth({
acceptedAuthModes: ["jwt"], acceptedAuthModes: [AUTH_MODE_JWT],
}), }),
requireWorkspaceAuth({ requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER], acceptedRoles: [ADMIN, MEMBER],
@@ -46,7 +51,7 @@ router.get(
router.post( router.post(
"/:workspaceId/secret-snapshots/rollback", "/:workspaceId/secret-snapshots/rollback",
requireAuth({ requireAuth({
acceptedAuthModes: ["jwt", "apiKey"], acceptedAuthModes: [AUTH_MODE_JWT, AUTH_MODE_API_KEY],
}), }),
requireWorkspaceAuth({ requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER], acceptedRoles: [ADMIN, MEMBER],
@@ -63,7 +68,7 @@ router.post(
router.get( router.get(
"/:workspaceId/logs", "/:workspaceId/logs",
requireAuth({ requireAuth({
acceptedAuthModes: ["jwt", "apiKey"], acceptedAuthModes: [AUTH_MODE_JWT, AUTH_MODE_API_KEY],
}), }),
requireWorkspaceAuth({ requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER], acceptedRoles: [ADMIN, MEMBER],
@@ -79,4 +84,66 @@ router.get(
workspaceController.getWorkspaceLogs workspaceController.getWorkspaceLogs
); );
router.get(
"/:workspaceId/trusted-ips",
param("workspaceId").exists().isString().trim(),
requireAuth({
acceptedAuthModes: [AUTH_MODE_JWT],
}),
requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER],
locationWorkspaceId: "params",
}),
workspaceController.getWorkspaceTrustedIps
);
router.post(
"/:workspaceId/trusted-ips",
param("workspaceId").exists().isString().trim(),
body("ipAddress").exists().isString().trim(),
body("comment").default("").isString().trim(),
body("isActive").exists().isBoolean(),
validateRequest,
requireAuth({
acceptedAuthModes: [AUTH_MODE_JWT],
}),
requireWorkspaceAuth({
acceptedRoles: [ADMIN],
locationWorkspaceId: "params",
}),
workspaceController.addWorkspaceTrustedIp
);
router.patch(
"/:workspaceId/trusted-ips/:trustedIpId",
param("workspaceId").exists().isString().trim(),
param("trustedIpId").exists().isString().trim(),
body("ipAddress").isString().trim().default(""),
body("comment").default("").isString().trim(),
validateRequest,
requireAuth({
acceptedAuthModes: [AUTH_MODE_JWT],
}),
requireWorkspaceAuth({
acceptedRoles: [ADMIN],
locationWorkspaceId: "params",
}),
workspaceController.updateWorkspaceTrustedIp
);
router.delete(
"/:workspaceId/trusted-ips/:trustedIpId",
param("workspaceId").exists().isString().trim(),
param("trustedIpId").exists().isString().trim(),
validateRequest,
requireAuth({
acceptedAuthModes: [AUTH_MODE_JWT],
}),
requireWorkspaceAuth({
acceptedRoles: [ADMIN],
locationWorkspaceId: "params",
}),
workspaceController.deleteWorkspaceTrustedIp
);
export default router; export default router;
@@ -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 -1
View File
@@ -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);
@@ -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) {
+2 -2
View File
@@ -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 -9
View File
@@ -56,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
); );
@@ -84,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
); );
@@ -112,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
); );
@@ -139,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
); );
@@ -164,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
); );
@@ -199,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
); );
@@ -225,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
); );
@@ -255,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
); );
@@ -282,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
); );
+1
View File
@@ -0,0 +1 @@
export * from "./ip";
+101
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;
}
+35 -1
View File
@@ -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,
@@ -549,3 +555,31 @@ 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) return;
const trustedIpsToInsert = workspaceIdsToAddTrustedIp.map((workspaceId) => {
return new TrustedIP({
workspace: new Types.ObjectId(workspaceId),
ipAddress: "0.0.0.0",
type: IPType.IPV4,
prefix: 0,
isActive: true,
comment: "",
}).save();
});
await TrustedIP.insertMany(trustedIpsToInsert);
}
+3 -1
View File
@@ -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
+43 -7
View File
@@ -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,
@@ -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)
Binary file not shown.

After

Width:  |  Height:  |  Size: 313 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 439 KiB

+1
View File
@@ -119,6 +119,7 @@
"documentation/platform/secret-versioning", "documentation/platform/secret-versioning",
"documentation/platform/audit-logs", "documentation/platform/audit-logs",
"documentation/platform/token", "documentation/platform/token",
"documentation/platform/ip-allowlisting",
"documentation/platform/mfa", "documentation/platform/mfa",
"documentation/platform/saml" "documentation/platform/saml"
] ]
+1
View File
@@ -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";
@@ -0,0 +1,108 @@
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { apiRequest } from "@app/config/request";
import {
TrustedIp
} from "./types";
const trustedIps = {
getTrustedIps: (workspaceId: string) => [{ workspaceId }, "trusted-ips"] as const
}
export const useGetTrustedIps = (workspaceId: string) => {
return useQuery({
queryKey: trustedIps.getTrustedIps(workspaceId),
queryFn: async () => {
const { data } = await apiRequest.get<{ trustedIps: TrustedIp[] }>(`/api/v1/workspace/${workspaceId}/trusted-ips`);
return data.trustedIps;
}
});
}
export const useAddTrustedIp = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async ({
workspaceId,
ipAddress,
comment,
isActive
}: {
workspaceId: string;
ipAddress: string;
comment?: string;
isActive: boolean;
}) => {
const { data } = await apiRequest.post(
`/api/v1/workspace/${workspaceId}/trusted-ips`,
{
ipAddress,
...(comment ? { comment } : {}),
isActive
}
);
return data;
},
onSuccess(_, dto) {
queryClient.invalidateQueries(trustedIps.getTrustedIps(dto.workspaceId));
}
});
};
export const useUpdateTrustedIp = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async ({
workspaceId,
trustedIpId,
ipAddress,
comment,
isActive
}: {
workspaceId: string;
trustedIpId: string;
ipAddress: string;
comment?: string;
isActive: boolean;
}) => {
const { data } = await apiRequest.patch(
`/api/v1/workspace/${workspaceId}/trusted-ips/${trustedIpId}`,
{
ipAddress,
...(comment ? { comment } : {}),
isActive
}
);
return data;
},
onSuccess(_, dto) {
queryClient.invalidateQueries(trustedIps.getTrustedIps(dto.workspaceId));
}
});
};
export const useDeleteTrustedIp = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async ({
workspaceId,
trustedIpId,
}: {
workspaceId: string;
trustedIpId: string;
}) => {
const { data } = await apiRequest.delete(
`/api/v1/workspace/${workspaceId}/trusted-ips/${trustedIpId}`
);
return data;
},
onSuccess(_, dto) {
queryClient.invalidateQueries(trustedIps.getTrustedIps(dto.workspaceId));
}
});
};
@@ -0,0 +1,9 @@
export type TrustedIp = {
_id: string;
workspace: string;
ipAddress: string;
type: "ipv4" | "ipv6";
isActive: boolean;
comment: string;
prefix?: number;
};
+1
View File
@@ -6,6 +6,7 @@ export {
useDeleteAPIKey, useDeleteAPIKey,
useDeleteOrgMembership, useDeleteOrgMembership,
useGetMyAPIKeys, useGetMyAPIKeys,
useGetMyIp,
useGetMySessions, useGetMySessions,
useGetOrgUsers, useGetOrgUsers,
useGetUser, useGetUser,
+14
View File
@@ -25,6 +25,7 @@ const userKeys = {
getUser: ["user"] as const, getUser: ["user"] as const,
userAction: ["user-action"] as const, userAction: ["user-action"] as const,
getOrgUsers: (orgId: string) => [{ orgId }, "user"], getOrgUsers: (orgId: string) => [{ orgId }, "user"],
myIp: ["ip"] as const,
myAPIKeys: ["api-keys"] as const, myAPIKeys: ["api-keys"] as const,
mySessions: ["sessions"] as const mySessions: ["sessions"] as const
}; };
@@ -206,6 +207,19 @@ export const useLogoutUser = () =>
} }
}); });
export const useGetMyIp = () => {
return useQuery({
queryKey: userKeys.myIp,
queryFn: async () => {
const { data } = await apiRequest.get<{ ip: string; }>(
"/api/v1/users/me/ip"
);
return data.ip;
},
enabled: true
});
}
export const useGetMyAPIKeys = () => { export const useGetMyAPIKeys = () => {
return useQuery({ return useQuery({
queryKey: userKeys.myAPIKeys, queryKey: userKeys.myAPIKeys,
@@ -404,6 +404,18 @@ export const AppLayout = ({ children }: LayoutProps) => {
</MenuItem> </MenuItem>
</a> </a>
</Link> </Link>
<Link href={`/project/${currentWorkspace?._id}/allowlist`} passHref>
<a>
<MenuItem
isSelected={
router.asPath === `/project/${currentWorkspace?._id}/allowlist`
}
icon="system-outline-109-slider-toggle-settings"
>
IP Allowlist
</MenuItem>
</a>
</Link>
<Link href={`/project/${currentWorkspace?._id}/audit-logs`} passHref> <Link href={`/project/${currentWorkspace?._id}/audit-logs`} passHref>
<a> <a>
<MenuItem <MenuItem
@@ -0,0 +1,21 @@
import { useTranslation } from "react-i18next";
import Head from "next/head";
import { IPAllowlistPage } from "@app/views/Project/IPAllowListPage";
const ProjectAllowlist = () => {
const { t } = useTranslation();
return (
<>
<Head>
<title>{t("common.head-title", { title: t("settings.project.title") })}</title>
<link rel="icon" href="/infisical.ico" />
</Head>
<IPAllowlistPage />
</>
);
}
export default ProjectAllowlist;
ProjectAllowlist.requireAuth = true;
@@ -0,0 +1,15 @@
import { IPAllowlistSection } from "./components";
export const IPAllowlistPage = () => {
return (
<div className="flex justify-center bg-bunker-800 text-white w-full h-full">
<div className="max-w-7xl px-6 w-full">
<div className="my-6">
<p className="text-3xl font-semibold text-gray-200">IP Allowlist</p>
<div />
</div>
<IPAllowlistSection />
</div>
</div>
);
}
@@ -0,0 +1,187 @@
import { useEffect } from "react";
import { Controller, useForm } from "react-hook-form";
import { yupResolver } from "@hookform/resolvers/yup";
import * as yup from "yup";
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
import {
Button,
FormControl,
Input,
Modal,
ModalContent
} from "@app/components/v2";
import { useWorkspace } from "@app/context";
import {
useAddTrustedIp,
useGetMyIp,
useUpdateTrustedIp
} from "@app/hooks/api";
import { UsePopUpState } from "@app/hooks/usePopUp";
const schema = yup.object({
ipAddress: yup.string().required("IP address is required"),
comment: yup.string()
}).required();
export type FormData = yup.InferType<typeof schema>;
type Props = {
popUp: UsePopUpState<["trustedIp"]>;
handlePopUpClose: (popUpName: keyof UsePopUpState<["trustedIp"]>) => void;
handlePopUpToggle: (popUpName: keyof UsePopUpState<["trustedIp"]>, state?: boolean) => void;
};
export const IPAllowlistModal = ({
popUp,
handlePopUpClose,
handlePopUpToggle
}: Props) => {
const { createNotification } = useNotificationContext();
const { data, isLoading } = useGetMyIp();
const { currentWorkspace } = useWorkspace();
const addTrustedIp = useAddTrustedIp();
const updateTrustedIp = useUpdateTrustedIp();
const {
control,
setValue,
handleSubmit,
reset,
formState: { isSubmitting }
} = useForm<FormData>({
resolver: yupResolver(schema)
});
useEffect(() => {
if (popUp?.trustedIp?.data) {
reset(popUp?.trustedIp?.data as {
ipAddress: string;
comment: string;
});
} else {
reset({
ipAddress: "",
comment: ""
});
}
}, [popUp?.trustedIp?.data]);
const onIPAllowlistModalSubmit = async ({
ipAddress,
comment
}: FormData) => {
try {
if (!currentWorkspace?._id) return;
if (popUp?.trustedIp?.data) {
await updateTrustedIp.mutateAsync({
workspaceId: currentWorkspace._id,
trustedIpId: (popUp?.trustedIp?.data as { trustedIpId: string })?.trustedIpId,
ipAddress,
comment,
isActive: true
});
} else {
await addTrustedIp.mutateAsync({
workspaceId: currentWorkspace._id,
ipAddress,
comment,
isActive: true
});
}
createNotification({
text: `Successfully ${popUp?.trustedIp?.data ? "updated" : "added"} trusted IP`,
type: "success"
});
reset();
handlePopUpClose("trustedIp");
} catch (err) {
createNotification({
text: `Failed to ${popUp?.trustedIp?.data ? "update" : "add"} trusted IP`,
type: "error"
});
}
}
return (
<Modal
isOpen={popUp?.trustedIp?.isOpen}
onOpenChange={(isOpen) => {
handlePopUpToggle("trustedIp", isOpen);
reset();
}}
>
<ModalContent title={popUp?.trustedIp?.data ? "Update IP" : "Add IP"}>
<form onSubmit={handleSubmit(onIPAllowlistModalSubmit)}>
<Controller
control={control}
defaultValue=""
name="ipAddress"
render={({ field, fieldState: { error } }) => (
<FormControl
label="IPv4/IPv6 Address / CIDR Notation"
isError={Boolean(error)}
errorText={error?.message}
>
<Input
{...field}
placeholder="123.456.789.0"
/>
</FormControl>
)}
/>
{!isLoading && data && (
<Button
colorSchema="secondary"
type="button"
onClick={() => setValue("ipAddress", data)}
className="mb-8"
>
Add current IP address
</Button>
)}
<Controller
control={control}
defaultValue=""
name="comment"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Comment"
isError={Boolean(error)}
errorText={error?.message}
>
<Input
{...field}
placeholder="My IP address"
/>
</FormControl>
)}
/>
<div className="mt-8 flex items-center">
<Button
className="mr-4"
size="sm"
type="submit"
isLoading={isSubmitting}
isDisabled={isSubmitting}
>
{popUp?.trustedIp?.data ? "Update" : "Add"}
</Button>
<Button
colorSchema="secondary"
variant="plain"
onClick={() => handlePopUpClose("trustedIp")}
>
Cancel
</Button>
</div>
</form>
</ModalContent>
</Modal>
);
}
@@ -0,0 +1,105 @@
import { faPlus } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
import {
Button,
DeleteActionModal,
UpgradePlanModal
} from "@app/components/v2";
import { useSubscription,useWorkspace } from "@app/context";
import {
useDeleteTrustedIp
} from "@app/hooks/api";
import { usePopUp } from "@app/hooks/usePopUp";
import { IPAllowlistModal } from "./IPAllowlistModal";
import { IPAllowlistTable } from "./IPAllowlistTable";
export const IPAllowlistSection = () => {
const { createNotification } = useNotificationContext();
const { mutateAsync } = useDeleteTrustedIp();
const { subscription } = useSubscription();
const { currentWorkspace } = useWorkspace();
const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([
"trustedIp",
"deleteTrustedIp",
"upgradePlan"
] as const);
const onDeleteTrustedIpSubmit = async (trustedIpId: string) => {
try {
if (!currentWorkspace?._id) return;
await mutateAsync({
workspaceId: currentWorkspace._id,
trustedIpId
});
createNotification({
text: "Successfully deleted IP access range",
type: "success"
});
handlePopUpClose("deleteTrustedIp");
} catch (err) {
console.log(err);
createNotification({
text: "Failed to delete IP access range",
type: "error"
});
}
}
return (
<div className="p-4 bg-mineshaft-900 mb-6 rounded-lg border border-mineshaft-600">
<div className="flex items-center mb-8">
<h2 className="text-xl font-semibold flex-1 text-white">
IP Allowlist
</h2>
<Button
onClick={() => {
if (subscription?.ipAllowlisting) {
handlePopUpOpen("trustedIp")
} else {
handlePopUpOpen("upgradePlan");
}
}}
colorSchema="secondary"
isLoading={false}
leftIcon={<FontAwesomeIcon icon={faPlus} />}
>
Add IP
</Button>
</div>
<IPAllowlistTable
popUp={popUp}
handlePopUpOpen={handlePopUpOpen}
handlePopUpToggle={handlePopUpToggle}
/>
<IPAllowlistModal
popUp={popUp}
handlePopUpClose={handlePopUpClose}
handlePopUpToggle={handlePopUpToggle}
/>
<DeleteActionModal
isOpen={popUp.deleteTrustedIp.isOpen}
title={`Are you sure want to delete ${
(popUp?.deleteTrustedIp?.data as { name: string })?.name || " "
}?`}
onChange={(isOpen) => handlePopUpToggle("deleteTrustedIp", isOpen)}
deleteKey="confirm"
onDeleteApproved={() =>
onDeleteTrustedIpSubmit((popUp?.deleteTrustedIp?.data as { trustedIpId: string })?.trustedIpId)
}
/>
<UpgradePlanModal
isOpen={popUp.upgradePlan.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)}
text="You can use IP allowlisting if you switch to Infisical's Pro plan."
/>
</div>
);
}
@@ -0,0 +1,160 @@
import { faGlobe, faPencil, faXmark } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import {
EmptyState,
IconButton,
Table,
TableContainer,
TableSkeleton,
TBody,
Td,
Th,
THead,
Tr,
UpgradePlanModal
} from "@app/components/v2";
import { useSubscription, useWorkspace } from "@app/context";
import {
useGetTrustedIps
} from "@app/hooks/api";
import { UsePopUpState } from "@app/hooks/usePopUp";
type Props = {
popUp: UsePopUpState<["upgradePlan"]>;
handlePopUpOpen: (
popUpName: keyof UsePopUpState<["trustedIp", "deleteTrustedIp", "upgradePlan"]>,
data?: {
trustedIpId: string;
ipAddress?: string;
comment?: string;
isActive?: boolean;
},
) => void;
handlePopUpToggle: (popUpName: keyof UsePopUpState<["upgradePlan"]>, state?: boolean) => void;
};
export const IPAllowlistTable = ({
popUp,
handlePopUpOpen,
handlePopUpToggle
}: Props) => {
const { subscription } = useSubscription();
const { currentWorkspace } = useWorkspace();
const { data, isLoading } = useGetTrustedIps(currentWorkspace?._id ?? "");
const formatType = (type: string, prefix?: number) => {
return `${type.slice(0, 2).toUpperCase() + type.slice(2)} ${(prefix !== undefined) ? "CIDR" : ""}`;
}
return (
<div>
<TableContainer className="mt-4">
<Table>
<THead>
<Tr>
<Th className="flex-1">IP Address / Range</Th>
<Th className="flex-1">Format</Th>
<Th className="flex-1">Comment</Th>
{/* <Th className="flex-1">Status</Th> */}
<Th className="w-5" />
</Tr>
</THead>
<TBody>
{!isLoading && data && data?.length > 0 && data
.sort((a, b) => a.ipAddress.localeCompare(b.ipAddress))
.map(({
_id,
ipAddress,
comment,
type,
prefix,
isActive
}) => {
return (
<Tr
key={`ip-access-range-${_id}`}
className="h-10"
>
<Td>
{`${ipAddress}${(prefix !== undefined) ? `/${prefix}` : ""}`}
</Td>
<Td>
{formatType(type, prefix)}
</Td>
<Td>
{comment}
</Td>
{/* <Td>
<div className="flex items-center">
<FontAwesomeIcon
icon={faCircle}
color="#2ecc71"
/>
<p className="ml-4">Active</p>
</div>
</Td> */}
<Td className="flex items-center">
<IconButton
className="mr-3 py-2"
onClick={() => {
if (subscription?.ipAllowlisting) {
handlePopUpOpen("trustedIp", {
trustedIpId: _id,
ipAddress,
comment,
isActive
});
} else {
handlePopUpOpen("upgradePlan");
}
}}
colorSchema="primary"
variant="plain"
ariaLabel="update"
>
<FontAwesomeIcon icon={faPencil} />
</IconButton>
<IconButton
onClick={() => {
if (subscription?.ipAllowlisting) {
handlePopUpOpen("deleteTrustedIp", {
trustedIpId: _id
});
} else {
handlePopUpOpen("upgradePlan");
}
}}
size="lg"
colorSchema="danger"
variant="plain"
ariaLabel="update"
>
<FontAwesomeIcon icon={faXmark} />
</IconButton>
</Td>
</Tr>
);
})}
{isLoading && <TableSkeleton columns={4} key="ip-access-ranges" />}
{!isLoading && data && data?.length === 0 && (
<Tr>
<Td colSpan={5}>
<EmptyState
title="No IP addresses added"
icon={faGlobe}
/>
</Td>
</Tr>
)}
</TBody>
</Table>
</TableContainer>
<UpgradePlanModal
isOpen={popUp.upgradePlan.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)}
text="You can use IP allowlisting if you switch to Infisical's Pro plan."
/>
</div>
);
}
@@ -0,0 +1 @@
export { IPAllowlistSection } from "./IPAllowlistSection";
@@ -0,0 +1 @@
export { IPAllowlistPage } from "./IPAllowlistPage";
@@ -11,7 +11,8 @@ import {
Td, Td,
Th, Th,
THead, THead,
Tr} from "@app/components/v2"; Tr
} from "@app/components/v2";
import { useOrganization } from "@app/context"; import { useOrganization } from "@app/context";
import { import {
useDeleteOrgPmtMethod, useDeleteOrgPmtMethod,
@@ -11,7 +11,8 @@ import {
Td, Td,
Th, Th,
THead, THead,
Tr} from "@app/components/v2"; Tr
} from "@app/components/v2";
import { useOrganization } from "@app/context"; import { useOrganization } from "@app/context";
import { import {
useGetOrgInvoices useGetOrgInvoices
@@ -18,7 +18,7 @@ import { UsePopUpState } from "@app/hooks/usePopUp";
type Props = { type Props = {
handlePopUpOpen: ( handlePopUpOpen: (
popUpName: keyof UsePopUpState<["updateEnv", "deleteEnv", "deleteEnv", "upgradePlan"]>, popUpName: keyof UsePopUpState<["updateEnv", "deleteEnv", "upgradePlan"]>,
{ {
name, name,
slug slug