requested changes

This commit is contained in:
Daniel Hougaard
2025-12-18 23:32:18 +04:00
parent 310f1ee8f9
commit 24ecf916c0
5 changed files with 142 additions and 33 deletions

View File

@@ -182,38 +182,36 @@ export const registerDynamicSecretRouter = async (server: FastifyZodProvider) =>
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const dynamicSecretCfg = await server.services.dynamicSecret.updateByName({
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
name: req.params.name,
path: req.body.path,
projectSlug: req.body.projectSlug,
environmentSlug: req.body.environmentSlug,
...req.body.data
});
const { dynamicSecret, updatedFields, projectId, environment, secretPath } =
await server.services.dynamicSecret.updateByName({
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
name: req.params.name,
path: req.body.path,
projectSlug: req.body.projectSlug,
environmentSlug: req.body.environmentSlug,
...req.body.data
});
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
projectId: dynamicSecretCfg.projectId,
projectId,
event: {
type: EventType.UPDATE_DYNAMIC_SECRET,
metadata: {
dynamicSecretName: dynamicSecretCfg.name,
newDynamicSecretName: req.body.data.newName,
dynamicSecretType: dynamicSecretCfg.type,
dynamicSecretId: dynamicSecretCfg.id,
newDefaultTTL: req.body.data.defaultTTL,
newMaxTTL: req.body.data.maxTTL,
newUsernameTemplate: req.body.data.usernameTemplate,
environment: dynamicSecretCfg.environment,
secretPath: dynamicSecretCfg.secretPath,
projectId: dynamicSecretCfg.projectId
dynamicSecretName: dynamicSecret.name,
dynamicSecretType: dynamicSecret.type,
dynamicSecretId: dynamicSecret.id,
environment,
secretPath,
projectId,
updatedFields
}
}
});
return { dynamicSecret: dynamicSecretCfg };
return { dynamicSecret };
}
});
@@ -428,7 +426,6 @@ export const registerDynamicSecretRouter = async (server: FastifyZodProvider) =>
environment,
secretPath,
projectId,
leaseCount: leases.length
}
}

View File

@@ -4594,10 +4594,7 @@ interface UpdateDynamicSecretEvent {
dynamicSecretName: string;
dynamicSecretId: string;
dynamicSecretType: string;
newDynamicSecretName?: string;
newDefaultTTL?: string;
newMaxTTL?: string | null;
newUsernameTemplate?: string | null;
updatedFields: string[];
environment: string;
secretPath: string;

View File

@@ -9,6 +9,7 @@ import {
} from "@app/ee/services/permission/project-permission";
import { crypto } from "@app/lib/crypto";
import { BadRequestError, NotFoundError } from "@app/lib/errors";
import { extractObjectFieldPaths } from "@app/lib/fn";
import { OrderByDirection } from "@app/lib/types";
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
import { KmsDataKey } from "@app/services/kms/kms-types";
@@ -44,6 +45,34 @@ type TDynamicSecretServiceFactoryDep = {
resourceMetadataDAL: Pick<TResourceMetadataDALFactory, "insertMany" | "delete">;
};
const getUpdatedFieldPaths = (
oldData: Record<string, unknown> | null | undefined,
newData: Record<string, unknown> | null | undefined
): string[] => {
const updatedPaths = new Set<string>();
if (!newData || typeof newData !== "object") {
return [];
}
if (!oldData || typeof oldData !== "object") {
return [];
}
Object.keys(newData).forEach((key) => {
const oldValue = oldData?.[key];
const newValue = newData[key];
if (JSON.stringify(oldValue) !== JSON.stringify(newValue)) {
// Extract paths from the new value
const paths = extractObjectFieldPaths(newValue, key);
paths.forEach((path) => updatedPaths.add(path));
}
});
return Array.from(updatedPaths).sort();
};
export const dynamicSecretServiceFactory = ({
dynamicSecretDAL,
dynamicSecretLeaseDAL,
@@ -284,8 +313,26 @@ export const dynamicSecretServiceFactory = ({
secretManagerDecryptor({ cipherTextBlob: dynamicSecretCfg.encryptedInput }).toString()
) as object;
const newInput = { ...decryptedStoredInput, ...(inputs || {}) };
const oldInput = await selectedProvider.validateProviderInputs(decryptedStoredInput, { projectId });
const updatedInput = await selectedProvider.validateProviderInputs(newInput, { projectId });
const updatedFields = getUpdatedFieldPaths(
{
...(oldInput as object),
maxTTL: dynamicSecretCfg.maxTTL,
defaultTTL: dynamicSecretCfg.defaultTTL,
name: dynamicSecretCfg.name,
usernameTemplate
},
{
...(updatedInput as object),
maxTTL,
defaultTTL,
name: newName ?? name,
usernameTemplate
}
);
let selectedGatewayId: string | null = null;
let isGatewayV1 = true;
if (updatedInput && typeof updatedInput === "object" && "gatewayId" in updatedInput && updatedInput?.gatewayId) {
@@ -364,8 +411,8 @@ export const dynamicSecretServiceFactory = ({
});
return {
...updatedDynamicCfg,
inputs: updatedInput,
dynamicSecret: updatedDynamicCfg,
updatedFields,
projectId: project.id,
environment: environmentSlug,
secretPath: path

View File

@@ -89,9 +89,13 @@ export type TDynamicSecretServiceFactory = {
create: (
arg: TCreateDynamicSecretDTO
) => Promise<TDynamicSecrets & { projectId: string; environment: string; secretPath: string }>;
updateByName: (
arg: TUpdateDynamicSecretDTO
) => Promise<TDynamicSecrets & { projectId: string; environment: string; secretPath: string }>;
updateByName: (arg: TUpdateDynamicSecretDTO) => Promise<{
dynamicSecret: TDynamicSecrets;
updatedFields: string[];
projectId: string;
environment: string;
secretPath: string;
}>;
deleteByName: (
arg: TDeleteDynamicSecretDTO
) => Promise<TDynamicSecrets & { projectId: string; environment: string; secretPath: string }>;

View File

@@ -134,3 +134,67 @@ export const deterministicStringify = (value: unknown): string => {
return JSON.stringify(value);
};
/**
* Recursively extracts all field paths from a nested object structure.
* Returns an array of dot-notation paths (e.g., ["password", "username", "field.nestedField"])
*/
export const extractObjectFieldPaths = (obj: unknown, prefix = ""): string[] => {
const paths: string[] = [];
if (obj === null || obj === undefined) {
return paths;
}
if (typeof obj !== "object") {
// return the path if it exists
if (prefix) {
paths.push(prefix);
}
return paths;
}
if (Array.isArray(obj)) {
// for arrays, we log the array itself and optionally nested paths
if (prefix) {
paths.push(prefix);
}
// we just want to know the array field changed
obj.forEach((item, index) => {
if (typeof item === "object" && item !== null) {
const nestedPaths = extractObjectFieldPaths(item, `${prefix}[${index}]`);
paths.push(...nestedPaths);
}
});
return paths;
}
// for objects, extract all keys and recurse
const keys = Object.keys(obj);
if (keys.length === 0 && prefix) {
// empty object with prefix
paths.push(prefix);
}
keys.forEach((key) => {
const currentPath = prefix ? `${prefix}.${key}` : key;
const value = (obj as Record<string, unknown>)[key];
if (value === null || value === undefined) {
paths.push(currentPath);
} else if (typeof value === "object") {
// recurse into nested objects/arrays
const nestedPaths = extractObjectFieldPaths(value, currentPath);
if (nestedPaths.length === 0) {
// if nested object is empty, add the path itself
paths.push(currentPath);
} else {
paths.push(...nestedPaths);
}
} else {
paths.push(currentPath);
}
});
return paths;
};