mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-14 07:30:20 +00:00
Add trusted IP rules to ST V3
This commit is contained in:
@@ -106,6 +106,7 @@ export const getSecretsRaw = async (req: Request, res: Response) => {
|
||||
}
|
||||
case ActorType.SERVICE_V3: {
|
||||
await validateServiceTokenDataV3ClientForWorkspace({
|
||||
authData: req.authData,
|
||||
serviceTokenData: req.authData.authPayload as IServiceTokenDataV3,
|
||||
workspaceId: new Types.ObjectId(workspaceId),
|
||||
environment,
|
||||
@@ -209,6 +210,7 @@ export const getSecretByNameRaw = async (req: Request, res: Response) => {
|
||||
}
|
||||
case ActorType.SERVICE_V3: {
|
||||
await validateServiceTokenDataV3ClientForWorkspace({
|
||||
authData: req.authData,
|
||||
serviceTokenData: req.authData.authPayload as IServiceTokenDataV3,
|
||||
workspaceId: new Types.ObjectId(workspaceId),
|
||||
environment,
|
||||
@@ -281,6 +283,7 @@ export const createSecretRaw = async (req: Request, res: Response) => {
|
||||
}
|
||||
case ActorType.SERVICE_V3: {
|
||||
await validateServiceTokenDataV3ClientForWorkspace({
|
||||
authData: req.authData,
|
||||
serviceTokenData: req.authData.authPayload as IServiceTokenDataV3,
|
||||
workspaceId: new Types.ObjectId(workspaceId),
|
||||
environment,
|
||||
@@ -380,6 +383,7 @@ export const updateSecretByNameRaw = async (req: Request, res: Response) => {
|
||||
}
|
||||
case ActorType.SERVICE_V3: {
|
||||
await validateServiceTokenDataV3ClientForWorkspace({
|
||||
authData: req.authData,
|
||||
serviceTokenData: req.authData.authPayload as IServiceTokenDataV3,
|
||||
workspaceId: new Types.ObjectId(workspaceId),
|
||||
environment,
|
||||
@@ -460,6 +464,7 @@ export const deleteSecretByNameRaw = async (req: Request, res: Response) => {
|
||||
}
|
||||
case ActorType.SERVICE_V3: {
|
||||
await validateServiceTokenDataV3ClientForWorkspace({
|
||||
authData: req.authData,
|
||||
serviceTokenData: req.authData.authPayload as IServiceTokenDataV3,
|
||||
workspaceId: new Types.ObjectId(workspaceId),
|
||||
environment,
|
||||
@@ -555,6 +560,7 @@ export const getSecrets = async (req: Request, res: Response) => {
|
||||
}
|
||||
case ActorType.SERVICE_V3: {
|
||||
await validateServiceTokenDataV3ClientForWorkspace({
|
||||
authData: req.authData,
|
||||
serviceTokenData: req.authData.authPayload as IServiceTokenDataV3,
|
||||
workspaceId: new Types.ObjectId(workspaceId),
|
||||
environment,
|
||||
@@ -640,6 +646,7 @@ export const getSecretByName = async (req: Request, res: Response) => {
|
||||
}
|
||||
case ActorType.SERVICE_V3: {
|
||||
await validateServiceTokenDataV3ClientForWorkspace({
|
||||
authData: req.authData,
|
||||
serviceTokenData: req.authData.authPayload as IServiceTokenDataV3,
|
||||
workspaceId: new Types.ObjectId(workspaceId),
|
||||
environment,
|
||||
@@ -713,6 +720,7 @@ export const createSecret = async (req: Request, res: Response) => {
|
||||
}
|
||||
case ActorType.SERVICE_V3: {
|
||||
await validateServiceTokenDataV3ClientForWorkspace({
|
||||
authData: req.authData,
|
||||
serviceTokenData: req.authData.authPayload as IServiceTokenDataV3,
|
||||
workspaceId: new Types.ObjectId(workspaceId),
|
||||
environment,
|
||||
@@ -812,6 +820,7 @@ export const updateSecretByName = async (req: Request, res: Response) => {
|
||||
}
|
||||
case ActorType.SERVICE_V3: {
|
||||
await validateServiceTokenDataV3ClientForWorkspace({
|
||||
authData: req.authData,
|
||||
serviceTokenData: req.authData.authPayload as IServiceTokenDataV3,
|
||||
workspaceId: new Types.ObjectId(workspaceId),
|
||||
environment,
|
||||
@@ -888,6 +897,7 @@ export const deleteSecretByName = async (req: Request, res: Response) => {
|
||||
}
|
||||
case ActorType.SERVICE_V3: {
|
||||
await validateServiceTokenDataV3ClientForWorkspace({
|
||||
authData: req.authData,
|
||||
serviceTokenData: req.authData.authPayload as IServiceTokenDataV3,
|
||||
workspaceId: new Types.ObjectId(workspaceId),
|
||||
environment,
|
||||
@@ -946,6 +956,7 @@ export const createSecretByNameBatch = async (req: Request, res: Response) => {
|
||||
}
|
||||
case ActorType.SERVICE_V3: {
|
||||
await validateServiceTokenDataV3ClientForWorkspace({
|
||||
authData: req.authData,
|
||||
serviceTokenData: req.authData.authPayload as IServiceTokenDataV3,
|
||||
workspaceId: new Types.ObjectId(workspaceId),
|
||||
environment,
|
||||
@@ -995,6 +1006,7 @@ export const updateSecretByNameBatch = async (req: Request, res: Response) => {
|
||||
}
|
||||
case ActorType.SERVICE_V3: {
|
||||
await validateServiceTokenDataV3ClientForWorkspace({
|
||||
authData: req.authData,
|
||||
serviceTokenData: req.authData.authPayload as IServiceTokenDataV3,
|
||||
workspaceId: new Types.ObjectId(workspaceId),
|
||||
environment,
|
||||
@@ -1044,6 +1056,7 @@ export const deleteSecretByNameBatch = async (req: Request, res: Response) => {
|
||||
}
|
||||
case ActorType.SERVICE_V3: {
|
||||
await validateServiceTokenDataV3ClientForWorkspace({
|
||||
authData: req.authData,
|
||||
serviceTokenData: req.authData.authPayload as IServiceTokenDataV3,
|
||||
workspaceId: new Types.ObjectId(workspaceId),
|
||||
environment,
|
||||
|
||||
@@ -7,7 +7,8 @@ import {
|
||||
ServiceTokenDataV3Key
|
||||
} from "../../models";
|
||||
import {
|
||||
Scope
|
||||
IServiceTokenV3Scope,
|
||||
IServiceTokenV3TrustedIp
|
||||
} from "../../models/serviceTokenDataV3";
|
||||
import {
|
||||
ActorType,
|
||||
@@ -23,6 +24,7 @@ import {
|
||||
} from "../../ee/services/ProjectRoleService";
|
||||
import { ForbiddenError } from "@casl/ability";
|
||||
import { BadRequestError, ResourceNotFoundError } from "../../utils/errors";
|
||||
import { extractIPDetails, isValidIpOrCidr } from "../../utils/ip";
|
||||
import { EEAuditLogService } from "../../ee/services";
|
||||
import { getJwtServiceTokenSecret } from "../../config";
|
||||
|
||||
@@ -66,6 +68,7 @@ export const createServiceTokenData = async (req: Request, res: Response) => {
|
||||
workspaceId,
|
||||
publicKey,
|
||||
scopes,
|
||||
trustedIps,
|
||||
expiresIn,
|
||||
encryptedKey, // for ServiceTokenDataV3Key
|
||||
nonce // for ServiceTokenDataV3Key
|
||||
@@ -77,6 +80,17 @@ export const createServiceTokenData = async (req: Request, res: Response) => {
|
||||
ProjectPermissionSub.ServiceTokens
|
||||
);
|
||||
|
||||
// validate trusted ips
|
||||
const reformattedTrustedIps = trustedIps.map((trustedIp) => {
|
||||
const isValidIPOrCidr = isValidIpOrCidr(trustedIp.ipAddress);
|
||||
|
||||
if (!isValidIPOrCidr) return res.status(400).send({
|
||||
message: "The IP is not a valid IPv4, IPv6, or CIDR block"
|
||||
});
|
||||
|
||||
return extractIPDetails(trustedIp.ipAddress);
|
||||
});
|
||||
|
||||
let expiresAt;
|
||||
if (expiresIn) {
|
||||
expiresAt = new Date();
|
||||
@@ -95,6 +109,7 @@ export const createServiceTokenData = async (req: Request, res: Response) => {
|
||||
workspace: new Types.ObjectId(workspaceId),
|
||||
publicKey,
|
||||
usageCount: 0,
|
||||
trustedIps: reformattedTrustedIps,
|
||||
scopes,
|
||||
isActive,
|
||||
expiresAt
|
||||
@@ -123,7 +138,8 @@ export const createServiceTokenData = async (req: Request, res: Response) => {
|
||||
metadata: {
|
||||
name,
|
||||
isActive,
|
||||
scopes: scopes as Array<Scope>,
|
||||
scopes: scopes as Array<IServiceTokenV3Scope>,
|
||||
trustedIps: reformattedTrustedIps as Array<IServiceTokenV3TrustedIp>,
|
||||
expiresAt
|
||||
}
|
||||
},
|
||||
@@ -151,6 +167,7 @@ export const updateServiceTokenData = async (req: Request, res: Response) => {
|
||||
name,
|
||||
isActive,
|
||||
scopes,
|
||||
trustedIps,
|
||||
expiresIn
|
||||
}
|
||||
} = await validateRequest(reqValidator.UpdateServiceTokenV3, req);
|
||||
@@ -171,6 +188,20 @@ export const updateServiceTokenData = async (req: Request, res: Response) => {
|
||||
ProjectPermissionSub.ServiceTokens
|
||||
);
|
||||
|
||||
// validate trusted ips
|
||||
let reformattedTrustedIps;
|
||||
if (trustedIps) {
|
||||
reformattedTrustedIps = trustedIps.map((trustedIp) => {
|
||||
const isValidIPOrCidr = isValidIpOrCidr(trustedIp.ipAddress);
|
||||
|
||||
if (!isValidIPOrCidr) return res.status(400).send({
|
||||
message: "The IP is not a valid IPv4, IPv6, or CIDR block"
|
||||
});
|
||||
|
||||
return extractIPDetails(trustedIp.ipAddress);
|
||||
});
|
||||
}
|
||||
|
||||
let expiresAt;
|
||||
if (expiresIn) {
|
||||
expiresAt = new Date();
|
||||
@@ -183,6 +214,7 @@ export const updateServiceTokenData = async (req: Request, res: Response) => {
|
||||
name,
|
||||
isActive,
|
||||
scopes,
|
||||
trustedIps: reformattedTrustedIps,
|
||||
expiresAt
|
||||
},
|
||||
{
|
||||
@@ -201,7 +233,8 @@ export const updateServiceTokenData = async (req: Request, res: Response) => {
|
||||
metadata: {
|
||||
name: serviceTokenData.name,
|
||||
isActive,
|
||||
scopes: scopes as Array<Scope>,
|
||||
scopes: scopes as Array<IServiceTokenV3Scope>,
|
||||
trustedIps: reformattedTrustedIps as Array<IServiceTokenV3TrustedIp>,
|
||||
expiresAt
|
||||
}
|
||||
},
|
||||
@@ -254,7 +287,8 @@ export const deleteServiceTokenData = async (req: Request, res: Response) => {
|
||||
metadata: {
|
||||
name: serviceTokenData.name,
|
||||
isActive: serviceTokenData.isActive,
|
||||
scopes: serviceTokenData.scopes as Array<Scope>,
|
||||
scopes: serviceTokenData.scopes as Array<IServiceTokenV3Scope>,
|
||||
trustedIps: serviceTokenData.trustedIps as Array<IServiceTokenV3TrustedIp>,
|
||||
expiresAt: serviceTokenData.expiresAt
|
||||
}
|
||||
},
|
||||
|
||||
@@ -3,7 +3,8 @@ import {
|
||||
EventType
|
||||
} from "./enums";
|
||||
import {
|
||||
Scope
|
||||
IServiceTokenV3Scope,
|
||||
IServiceTokenV3TrustedIp
|
||||
} from "../../../models/serviceTokenDataV3";
|
||||
|
||||
interface UserActorMetadata {
|
||||
@@ -229,7 +230,8 @@ interface CreateServiceTokenV3Event {
|
||||
metadata: {
|
||||
name: string;
|
||||
isActive: boolean;
|
||||
scopes: Array<Scope>;
|
||||
scopes: Array<IServiceTokenV3Scope>;
|
||||
trustedIps: Array<IServiceTokenV3TrustedIp>;
|
||||
expiresAt?: Date;
|
||||
}
|
||||
}
|
||||
@@ -239,7 +241,8 @@ interface UpdateServiceTokenV3Event {
|
||||
metadata: {
|
||||
name?: string;
|
||||
isActive?: boolean;
|
||||
scopes?: Array<Scope>;
|
||||
scopes?: Array<IServiceTokenV3Scope>;
|
||||
trustedIps?: Array<IServiceTokenV3TrustedIp>;
|
||||
expiresAt?: Date;
|
||||
}
|
||||
}
|
||||
@@ -249,8 +252,9 @@ interface DeleteServiceTokenV3Event {
|
||||
metadata: {
|
||||
name: string;
|
||||
isActive: boolean;
|
||||
scopes: Array<Scope>;
|
||||
scopes: Array<IServiceTokenV3Scope>;
|
||||
expiresAt?: Date;
|
||||
trustedIps: Array<IServiceTokenV3TrustedIp>;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -19,10 +19,7 @@ import {
|
||||
ServiceTokenData,
|
||||
TFolderRootSchema
|
||||
} from "../models";
|
||||
import {
|
||||
Scope,
|
||||
Permission
|
||||
} from "../models/serviceTokenDataV3";
|
||||
import { Permission } from "../models/serviceTokenDataV3";
|
||||
import { EventType, SecretVersion } from "../ee/models";
|
||||
import {
|
||||
BadRequestError,
|
||||
|
||||
@@ -1,16 +1,23 @@
|
||||
import { Document, Schema, Types, model } from "mongoose";
|
||||
import { IPType } from "../ee/models";
|
||||
|
||||
export enum Permission {
|
||||
READ = "read",
|
||||
WRITE = "write"
|
||||
}
|
||||
|
||||
export interface Scope {
|
||||
export interface IServiceTokenV3Scope {
|
||||
environment: string;
|
||||
secretPath: string;
|
||||
permissions: Permission[];
|
||||
}
|
||||
|
||||
export interface IServiceTokenV3TrustedIp {
|
||||
ipAddress: string;
|
||||
type: IPType;
|
||||
prefix: number;
|
||||
}
|
||||
|
||||
export interface IServiceTokenDataV3 extends Document {
|
||||
_id: Types.ObjectId;
|
||||
name: string;
|
||||
@@ -21,7 +28,8 @@ export interface IServiceTokenDataV3 extends Document {
|
||||
lastUsed?: Date;
|
||||
usageCount: number;
|
||||
expiresAt?: Date;
|
||||
scopes: Array<Scope>;
|
||||
scopes: Array<IServiceTokenV3Scope>;
|
||||
trustedIps: Array<IServiceTokenV3TrustedIp>;
|
||||
}
|
||||
|
||||
const serviceTokenDataV3Schema = new Schema(
|
||||
@@ -83,6 +91,34 @@ const serviceTokenDataV3Schema = new Schema(
|
||||
}
|
||||
],
|
||||
required: true
|
||||
},
|
||||
trustedIps: {
|
||||
type: [
|
||||
{
|
||||
ipAddress: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
type: {
|
||||
type: String,
|
||||
enum: [
|
||||
IPType.IPV4,
|
||||
IPType.IPV6
|
||||
],
|
||||
required: true
|
||||
},
|
||||
prefix: {
|
||||
type: Number,
|
||||
required: false
|
||||
}
|
||||
}
|
||||
],
|
||||
default: [{
|
||||
ipAddress: "0.0.0.0",
|
||||
type: IPType.IPV4.toString(),
|
||||
prefix: 0
|
||||
}],
|
||||
required: true
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import net from "net";
|
||||
import { IPType } from "../../ee/models";
|
||||
import { InternalServerError } from "../errors";
|
||||
import { InternalServerError, UnauthorizedRequestError } from "../errors";
|
||||
|
||||
/**
|
||||
* Return details of IP [ip]:
|
||||
@@ -98,4 +98,39 @@ export const isValidIpOrCidr = (ip: string): boolean => {
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates the IP address [ipAddress] against the trusted IPs [trustedIps].
|
||||
* @param {Object} obj
|
||||
* @param {String} obj.ipAddress - IP address to check
|
||||
* @param {Object[]} obj.trustedIps - IPs to trust in blocklist
|
||||
*/
|
||||
export const checkIPAgainstBlocklist = ({
|
||||
ipAddress,
|
||||
trustedIps
|
||||
}: {
|
||||
ipAddress: string;
|
||||
trustedIps: {
|
||||
ipAddress: string;
|
||||
type: IPType;
|
||||
prefix: number;
|
||||
}[]
|
||||
}) => {
|
||||
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 { type } = extractIPDetails(ipAddress);
|
||||
const check = blockList.check(ipAddress, type);
|
||||
|
||||
if (!check) throw UnauthorizedRequestError({
|
||||
message: "Failed to authenticate"
|
||||
});
|
||||
}
|
||||
|
||||
@@ -4,6 +4,8 @@ import { Permission } from "../models/serviceTokenDataV3";
|
||||
import { z } from "zod";
|
||||
import { UnauthorizedRequestError } from "../utils/errors";
|
||||
import { isValidScopeV3 } from "../helpers";
|
||||
import { AuthData } from "../interfaces/middleware";
|
||||
import { checkIPAgainstBlocklist } from "../utils/ip";
|
||||
|
||||
/**
|
||||
* Validate that service token (client) can access workspace
|
||||
@@ -16,19 +18,27 @@ import { isValidScopeV3 } from "../helpers";
|
||||
* @param {String[]} acceptedPermissions - accepted permissions as part of the endpoint
|
||||
*/
|
||||
export const validateServiceTokenDataV3ClientForWorkspace = async ({
|
||||
authData,
|
||||
serviceTokenData,
|
||||
workspaceId,
|
||||
environment,
|
||||
secretPath = "/",
|
||||
requiredPermissions
|
||||
}: {
|
||||
authData: AuthData;
|
||||
serviceTokenData: IServiceTokenDataV3;
|
||||
workspaceId: Types.ObjectId;
|
||||
environment?: string;
|
||||
secretPath?: string;
|
||||
requiredPermissions: Permission[];
|
||||
}) => {
|
||||
|
||||
|
||||
// validate ST V3 IP address
|
||||
checkIPAgainstBlocklist({
|
||||
ipAddress: authData.ipAddress,
|
||||
trustedIps: serviceTokenData.trustedIps
|
||||
});
|
||||
|
||||
if (!serviceTokenData.workspace.equals(workspaceId)) {
|
||||
// case: invalid workspaceId passed
|
||||
throw UnauthorizedRequestError({
|
||||
@@ -63,6 +73,12 @@ export const CreateServiceTokenV3 = z.object({
|
||||
})
|
||||
.array()
|
||||
.min(1),
|
||||
trustedIps: z
|
||||
.object({
|
||||
ipAddress: z.string().trim(),
|
||||
})
|
||||
.array()
|
||||
.min(1),
|
||||
expiresIn: z.number().optional(),
|
||||
encryptedKey: z.string().trim(),
|
||||
nonce: z.string().trim()
|
||||
@@ -85,6 +101,13 @@ export const UpdateServiceTokenV3 = z.object({
|
||||
.array()
|
||||
.min(1)
|
||||
.optional(),
|
||||
trustedIps: z
|
||||
.object({
|
||||
ipAddress: z.string().trim()
|
||||
})
|
||||
.array()
|
||||
.min(1)
|
||||
.optional(),
|
||||
expiresIn: z.number().optional()
|
||||
}),
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user