Add trusted IP rules to ST V3

This commit is contained in:
Tuan Dang
2023-09-30 20:52:04 +01:00
parent cdf4440848
commit eb2f433f43
11 changed files with 404 additions and 205 deletions
@@ -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
}
},
+8 -4
View File
@@ -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>;
}
}
+1 -4
View File
@@ -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,
+38 -2
View File
@@ -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
}
},
{
+37 -2
View File
@@ -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"
});
}
+24 -1
View File
@@ -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()
}),
});