mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-15 01:52:22 +00:00
Replace scopes with role for ST V3
This commit is contained in:
@@ -3,11 +3,10 @@ import { Types } from "mongoose";
|
||||
import { EventService, SecretService } from "../../services";
|
||||
import { eventPushSecrets } from "../../events";
|
||||
import { BotService } from "../../services";
|
||||
import { containsGlobPatterns, isValidScopeV3, repackageSecretToRaw } from "../../helpers/secrets";
|
||||
import { containsGlobPatterns, repackageSecretToRaw } from "../../helpers/secrets";
|
||||
import { encryptSymmetric128BitHexKeyUTF8 } from "../../utils/crypto";
|
||||
import { getAllImportedSecrets } from "../../services/SecretImportService";
|
||||
import { Folder, IMembership, IServiceTokenData, IServiceTokenDataV3 } from "../../models";
|
||||
import { Permission } from "../../models/serviceTokenDataV3";
|
||||
import { getFolderByPath } from "../../services/FolderService";
|
||||
import { BadRequestError } from "../../utils/errors";
|
||||
import { validateRequest } from "../../helpers/validation";
|
||||
@@ -50,24 +49,19 @@ const checkSecretsPermission = async ({
|
||||
membership?: Omit<IMembership, "customRole"> & { customRole: IRole };
|
||||
}> => {
|
||||
let STV2RequiredPermissions = [];
|
||||
let STV3RequiredPermissions: Permission[] = [];
|
||||
|
||||
switch (secretAction) {
|
||||
case ProjectPermissionActions.Create:
|
||||
STV2RequiredPermissions = [PERMISSION_WRITE_SECRETS];
|
||||
STV3RequiredPermissions = [Permission.WRITE];
|
||||
break;
|
||||
case ProjectPermissionActions.Read:
|
||||
STV2RequiredPermissions = [PERMISSION_READ_SECRETS];
|
||||
STV3RequiredPermissions = [Permission.READ];
|
||||
break;
|
||||
case ProjectPermissionActions.Edit:
|
||||
STV2RequiredPermissions = [PERMISSION_WRITE_SECRETS];
|
||||
STV3RequiredPermissions = [Permission.WRITE];
|
||||
break;
|
||||
case ProjectPermissionActions.Delete:
|
||||
STV2RequiredPermissions = [PERMISSION_WRITE_SECRETS];
|
||||
STV3RequiredPermissions = [Permission.WRITE];
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -104,23 +98,27 @@ const checkSecretsPermission = async ({
|
||||
return { authVerifier: () => true };
|
||||
}
|
||||
case ActorType.SERVICE_V3: {
|
||||
|
||||
// TODO: redo this part
|
||||
await validateServiceTokenDataV3ClientForWorkspace({
|
||||
authData,
|
||||
serviceTokenData: authData.authPayload as IServiceTokenDataV3,
|
||||
workspaceId: new Types.ObjectId(workspaceId),
|
||||
environment,
|
||||
secretPath,
|
||||
requiredPermissions: STV3RequiredPermissions
|
||||
});
|
||||
return {
|
||||
authVerifier: (env: string, secPath: string) =>
|
||||
isValidScopeV3({
|
||||
authPayload: authData.authPayload as IServiceTokenDataV3,
|
||||
environment: env,
|
||||
secretPath: secPath,
|
||||
requiredPermissions: STV3RequiredPermissions
|
||||
})
|
||||
};
|
||||
|
||||
// TODO: return an authVerifier
|
||||
|
||||
// return {
|
||||
// authVerifier: (env: string, secPath: string) =>
|
||||
// isValidScopeV3({
|
||||
// authPayload: authData.authPayload as IServiceTokenDataV3,
|
||||
// environment: env,
|
||||
// secretPath: secPath,
|
||||
// requiredPermissions: STV3RequiredPermissions
|
||||
// })
|
||||
// };
|
||||
return { authVerifier: () => true }
|
||||
}
|
||||
default: {
|
||||
throw UnauthorizedRequestError();
|
||||
|
||||
@@ -8,13 +8,11 @@ import {
|
||||
ServiceTokenDataV3Key,
|
||||
Workspace
|
||||
} from "../../../models";
|
||||
import {
|
||||
IServiceTokenV3Scope,
|
||||
IServiceTokenV3TrustedIp
|
||||
} from "../../../models/serviceTokenDataV3";
|
||||
import { IServiceTokenV3TrustedIp } from "../../../models/serviceTokenDataV3";
|
||||
import {
|
||||
ActorType,
|
||||
EventType
|
||||
EventType,
|
||||
Role
|
||||
} from "../../models";
|
||||
import { validateRequest } from "../../../helpers/validation";
|
||||
import * as reqValidator from "../../../validation/serviceTokenDataV3";
|
||||
@@ -29,7 +27,7 @@ import { BadRequestError, ResourceNotFoundError, UnauthorizedRequestError } from
|
||||
import { extractIPDetails, isValidIpOrCidr } from "../../../utils/ip";
|
||||
import { EEAuditLogService, EELicenseService } from "../../services";
|
||||
import { getAuthSecret } from "../../../config";
|
||||
import { AuthTokenType } from "../../../variables";
|
||||
import { ADMIN, AuthTokenType, CUSTOM, MEMBER, VIEWER } from "../../../variables";
|
||||
|
||||
/**
|
||||
* Return project key for service token V3
|
||||
@@ -163,7 +161,7 @@ export const createServiceTokenData = async (req: Request, res: Response) => {
|
||||
name,
|
||||
workspaceId,
|
||||
publicKey,
|
||||
scopes,
|
||||
role,
|
||||
trustedIps,
|
||||
expiresIn,
|
||||
accessTokenTTL,
|
||||
@@ -181,6 +179,19 @@ export const createServiceTokenData = async (req: Request, res: Response) => {
|
||||
const workspace = await Workspace.findById(workspaceId);
|
||||
if (!workspace) throw BadRequestError({ message: "Workspace not found" });
|
||||
|
||||
const isCustomRole = ![ADMIN, MEMBER, VIEWER].includes(role);
|
||||
|
||||
let customRole;
|
||||
if (isCustomRole) {
|
||||
customRole = await Role.findOne({
|
||||
slug: role,
|
||||
isOrgRole: false,
|
||||
workspace: workspace._id
|
||||
});
|
||||
|
||||
if (!customRole) throw BadRequestError({ message: "Role not found" });
|
||||
}
|
||||
|
||||
const plan = await EELicenseService.getPlan(workspace.organization);
|
||||
|
||||
// validate trusted ips
|
||||
@@ -219,7 +230,8 @@ export const createServiceTokenData = async (req: Request, res: Response) => {
|
||||
accessTokenUsageCount: 0,
|
||||
tokenVersion: 1,
|
||||
trustedIps: reformattedTrustedIps,
|
||||
scopes,
|
||||
role: isCustomRole ? CUSTOM : role,
|
||||
customRole,
|
||||
isActive,
|
||||
expiresAt,
|
||||
accessTokenTTL,
|
||||
@@ -250,7 +262,7 @@ export const createServiceTokenData = async (req: Request, res: Response) => {
|
||||
metadata: {
|
||||
name,
|
||||
isActive,
|
||||
scopes: scopes as Array<IServiceTokenV3Scope>,
|
||||
role,
|
||||
trustedIps: reformattedTrustedIps as Array<IServiceTokenV3TrustedIp>,
|
||||
expiresAt
|
||||
}
|
||||
@@ -278,7 +290,7 @@ export const updateServiceTokenData = async (req: Request, res: Response) => {
|
||||
body: {
|
||||
name,
|
||||
isActive,
|
||||
scopes,
|
||||
role,
|
||||
trustedIps,
|
||||
expiresIn,
|
||||
accessTokenTTL,
|
||||
@@ -304,6 +316,20 @@ export const updateServiceTokenData = async (req: Request, res: Response) => {
|
||||
const workspace = await Workspace.findById(serviceTokenData.workspace);
|
||||
if (!workspace) throw BadRequestError({ message: "Workspace not found" });
|
||||
|
||||
let customRole;
|
||||
if (role) {
|
||||
const isCustomRole = ![ADMIN, MEMBER, VIEWER].includes(role);
|
||||
if (isCustomRole) {
|
||||
customRole = await Role.findOne({
|
||||
slug: role,
|
||||
isOrgRole: false,
|
||||
workspace: workspace._id
|
||||
});
|
||||
|
||||
if (!customRole) throw BadRequestError({ message: "Role not found" });
|
||||
}
|
||||
}
|
||||
|
||||
const plan = await EELicenseService.getPlan(workspace.organization);
|
||||
|
||||
// validate trusted ips
|
||||
@@ -335,7 +361,15 @@ export const updateServiceTokenData = async (req: Request, res: Response) => {
|
||||
{
|
||||
name,
|
||||
isActive,
|
||||
scopes,
|
||||
role: customRole ? CUSTOM : role,
|
||||
...(customRole ? {
|
||||
customRole
|
||||
} : {}),
|
||||
...(role && !customRole ? { // non-custom role
|
||||
$unset: {
|
||||
customRole: 1
|
||||
}
|
||||
} : {}),
|
||||
trustedIps: reformattedTrustedIps,
|
||||
expiresAt,
|
||||
accessTokenTTL,
|
||||
@@ -357,7 +391,7 @@ export const updateServiceTokenData = async (req: Request, res: Response) => {
|
||||
metadata: {
|
||||
name: serviceTokenData.name,
|
||||
isActive,
|
||||
scopes: scopes as Array<IServiceTokenV3Scope>,
|
||||
role,
|
||||
trustedIps: reformattedTrustedIps as Array<IServiceTokenV3TrustedIp>,
|
||||
expiresAt
|
||||
}
|
||||
@@ -415,7 +449,7 @@ export const deleteServiceTokenData = async (req: Request, res: Response) => {
|
||||
metadata: {
|
||||
name: serviceTokenData.name,
|
||||
isActive: serviceTokenData.isActive,
|
||||
scopes: serviceTokenData.scopes as Array<IServiceTokenV3Scope>,
|
||||
role: serviceTokenData.role,
|
||||
trustedIps: serviceTokenData.trustedIps as Array<IServiceTokenV3TrustedIp>,
|
||||
expiresAt: serviceTokenData.expiresAt
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { ActorType, EventType } from "./enums";
|
||||
import { IServiceTokenV3Scope, IServiceTokenV3TrustedIp } from "../../../models/serviceTokenDataV3";
|
||||
import { IServiceTokenV3TrustedIp } from "../../../models/serviceTokenDataV3";
|
||||
|
||||
interface UserActorMetadata {
|
||||
userId: string;
|
||||
@@ -225,7 +225,7 @@ interface CreateServiceTokenV3Event {
|
||||
metadata: {
|
||||
name: string;
|
||||
isActive: boolean;
|
||||
scopes: Array<IServiceTokenV3Scope>;
|
||||
role: string;
|
||||
trustedIps: Array<IServiceTokenV3TrustedIp>;
|
||||
expiresAt?: Date;
|
||||
};
|
||||
@@ -236,7 +236,7 @@ interface UpdateServiceTokenV3Event {
|
||||
metadata: {
|
||||
name?: string;
|
||||
isActive?: boolean;
|
||||
scopes?: Array<IServiceTokenV3Scope>;
|
||||
role?: string;
|
||||
trustedIps?: Array<IServiceTokenV3TrustedIp>;
|
||||
expiresAt?: Date;
|
||||
};
|
||||
@@ -247,7 +247,7 @@ interface DeleteServiceTokenV3Event {
|
||||
metadata: {
|
||||
name: string;
|
||||
isActive: boolean;
|
||||
scopes: Array<IServiceTokenV3Scope>;
|
||||
role: string;
|
||||
expiresAt?: Date;
|
||||
trustedIps: Array<IServiceTokenV3TrustedIp>;
|
||||
};
|
||||
|
||||
@@ -13,13 +13,11 @@ import {
|
||||
Folder,
|
||||
ISecret,
|
||||
IServiceTokenData,
|
||||
IServiceTokenDataV3,
|
||||
Secret,
|
||||
SecretBlindIndexData,
|
||||
ServiceTokenData,
|
||||
TFolderRootSchema
|
||||
} from "../models";
|
||||
import { Permission } from "../models/serviceTokenDataV3";
|
||||
import { EventType, SecretVersion } from "../ee/models";
|
||||
import {
|
||||
BadRequestError,
|
||||
@@ -51,42 +49,6 @@ import picomatch from "picomatch";
|
||||
import path from "path";
|
||||
import { getAnImportedSecret } from "../services/SecretImportService";
|
||||
|
||||
/**
|
||||
* Validate scope for service token v3
|
||||
* @param authPayload
|
||||
* @param environment
|
||||
* @param secretPath
|
||||
* @returns
|
||||
*/
|
||||
export const isValidScopeV3 = ({
|
||||
authPayload,
|
||||
environment,
|
||||
secretPath,
|
||||
requiredPermissions
|
||||
}: {
|
||||
authPayload: IServiceTokenDataV3;
|
||||
environment: string;
|
||||
secretPath: string;
|
||||
requiredPermissions: Permission[];
|
||||
}) => {
|
||||
const { scopes } = authPayload;
|
||||
|
||||
const validScope = scopes.find(
|
||||
(scope) =>
|
||||
picomatch.isMatch(secretPath, scope.secretPath, { strictSlashes: false }) &&
|
||||
scope.environment === environment
|
||||
);
|
||||
|
||||
if (
|
||||
validScope &&
|
||||
!requiredPermissions.every((permission) => validScope.permissions.includes(permission))
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return Boolean(validScope);
|
||||
};
|
||||
|
||||
/**
|
||||
* Validate scope for service token v2
|
||||
* @param authPayload
|
||||
|
||||
@@ -1,16 +1,6 @@
|
||||
import { Document, Schema, Types, model } from "mongoose";
|
||||
import { IPType } from "../ee/models";
|
||||
|
||||
export enum Permission {
|
||||
READ = "read",
|
||||
WRITE = "write"
|
||||
}
|
||||
|
||||
export interface IServiceTokenV3Scope {
|
||||
environment: string;
|
||||
secretPath: string;
|
||||
permissions: Permission[];
|
||||
}
|
||||
import { ADMIN, CUSTOM, MEMBER, VIEWER } from "../variables";
|
||||
|
||||
export interface IServiceTokenV3TrustedIp {
|
||||
ipAddress: string;
|
||||
@@ -33,7 +23,8 @@ export interface IServiceTokenDataV3 extends Document {
|
||||
isRefreshTokenRotationEnabled: boolean;
|
||||
expiresAt?: Date;
|
||||
accessTokenTTL: number;
|
||||
scopes: Array<IServiceTokenV3Scope>;
|
||||
role: "admin" | "member" | "viewer" | "custom";
|
||||
customRole: Types.ObjectId;
|
||||
trustedIps: Array<IServiceTokenV3TrustedIp>;
|
||||
}
|
||||
|
||||
@@ -100,28 +91,15 @@ const serviceTokenDataV3Schema = new Schema(
|
||||
default: 7200,
|
||||
required: true
|
||||
},
|
||||
scopes: { // TODO: consider switching this out for roles instead
|
||||
type: [
|
||||
{
|
||||
environment: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
secretPath: {
|
||||
type: String,
|
||||
default: "/",
|
||||
required: true
|
||||
},
|
||||
permissions: {
|
||||
type: [String],
|
||||
enum: [Permission.READ, Permission.WRITE],
|
||||
default: [Permission.READ],
|
||||
required: true
|
||||
}
|
||||
}
|
||||
],
|
||||
role: {
|
||||
type: String,
|
||||
enum: [ADMIN, MEMBER, VIEWER, CUSTOM],
|
||||
required: true
|
||||
},
|
||||
customRole: {
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: "Role"
|
||||
},
|
||||
trustedIps: {
|
||||
type: [
|
||||
{
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import { Types } from "mongoose";
|
||||
import { IServiceTokenDataV3 } from "../models";
|
||||
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";
|
||||
import { MEMBER } from "../variables";
|
||||
|
||||
/**
|
||||
* Validate that service token (client) can access workspace
|
||||
@@ -22,15 +21,13 @@ import { checkIPAgainstBlocklist } from "../utils/ip";
|
||||
serviceTokenData,
|
||||
workspaceId,
|
||||
environment,
|
||||
secretPath = "/",
|
||||
requiredPermissions
|
||||
// secretPath = "/",
|
||||
}: {
|
||||
authData: AuthData;
|
||||
serviceTokenData: IServiceTokenDataV3;
|
||||
workspaceId: Types.ObjectId;
|
||||
environment?: string;
|
||||
secretPath?: string;
|
||||
requiredPermissions: Permission[];
|
||||
// secretPath?: string;
|
||||
}) => {
|
||||
|
||||
// validate ST V3 IP address
|
||||
@@ -47,16 +44,19 @@ import { checkIPAgainstBlocklist } from "../utils/ip";
|
||||
}
|
||||
|
||||
if (environment) {
|
||||
const isValid = isValidScopeV3({
|
||||
authPayload: serviceTokenData,
|
||||
environment,
|
||||
secretPath,
|
||||
requiredPermissions
|
||||
});
|
||||
|
||||
if (!isValid) throw UnauthorizedRequestError({
|
||||
message: "Failed service token authorization for the given workspace"
|
||||
});
|
||||
// TODO: validation fun for ST V3
|
||||
|
||||
// const isValid = isValidScopeV3({
|
||||
// authPayload: serviceTokenData,
|
||||
// environment,
|
||||
// secretPath,
|
||||
// requiredPermissions
|
||||
// });
|
||||
|
||||
// if (!isValid) throw UnauthorizedRequestError({
|
||||
// message: "Failed service token authorization for the given workspace"
|
||||
// });
|
||||
}
|
||||
};
|
||||
|
||||
@@ -71,20 +71,14 @@ export const CreateServiceTokenV3 = z.object({
|
||||
name: z.string().trim(),
|
||||
workspaceId: z.string().trim(),
|
||||
publicKey: z.string().trim(),
|
||||
scopes: z
|
||||
.object({
|
||||
permissions: z.enum(["read", "write"]).array(),
|
||||
environment: z.string().trim(),
|
||||
secretPath: z.string().trim()
|
||||
})
|
||||
.array()
|
||||
.min(1),
|
||||
trustedIps: z
|
||||
role: z.string().trim().min(1).default(MEMBER),
|
||||
trustedIps: z // TODO: provide default
|
||||
.object({
|
||||
ipAddress: z.string().trim(),
|
||||
})
|
||||
.array()
|
||||
.min(1),
|
||||
.min(1)
|
||||
.default([{ ipAddress: "0.0.0.0/0" }]),
|
||||
expiresIn: z.number().optional(),
|
||||
accessTokenTTL: z.number().int().min(1),
|
||||
encryptedKey: z.string().trim(),
|
||||
@@ -100,15 +94,7 @@ export const UpdateServiceTokenV3 = z.object({
|
||||
body: z.object({
|
||||
name: z.string().trim().optional(),
|
||||
isActive: z.boolean().optional(),
|
||||
scopes: z
|
||||
.object({
|
||||
permissions: z.enum(["read", "write"]).array(),
|
||||
environment: z.string().trim(),
|
||||
secretPath: z.string().trim()
|
||||
})
|
||||
.array()
|
||||
.min(1)
|
||||
.optional(),
|
||||
role: z.string().trim().min(1).optional(),
|
||||
trustedIps: z
|
||||
.object({
|
||||
ipAddress: z.string().trim()
|
||||
|
||||
Reference in New Issue
Block a user