Implement hidden fields omission in audit logs and notifications for database operations
All checks were successful
farmcontrol/farmcontrol-api/pipeline/head This commit looks good
All checks were successful
farmcontrol/farmcontrol-api/pipeline/head This commit looks good
This commit introduces a utility function, `omitHiddenFields`, to exclude specified schema hidden fields from audit logs and notifications during create, edit, and delete operations in the database module. The `printerSchema` is updated to define hidden fields, ensuring sensitive information is not logged. Corresponding tests are added to verify the correct omission of these fields in audit logs and notifications, enhancing data privacy and compliance.
This commit is contained in:
parent
fd219700f8
commit
1d8fcdd831
@ -75,7 +75,8 @@ jest.unstable_mockModule('../schemas/management/file.schema.js', () => ({
|
|||||||
// Now import the database utilities
|
// Now import the database utilities
|
||||||
const { listObjects, getObject, newObject, editObject, deleteObject } =
|
const { listObjects, getObject, newObject, editObject, deleteObject } =
|
||||||
await import('../database.js');
|
await import('../database.js');
|
||||||
const { editAuditLog, editNotification } = await import('../../utils.js');
|
const { deleteAuditLog, deleteNotification, editAuditLog, editNotification, newAuditLog } =
|
||||||
|
await import('../../utils.js');
|
||||||
|
|
||||||
describe('Database Utilities (CRUD)', () => {
|
describe('Database Utilities (CRUD)', () => {
|
||||||
let mockModel;
|
let mockModel;
|
||||||
@ -151,6 +152,26 @@ describe('Database Utilities (CRUD)', () => {
|
|||||||
expect(mockModel.create).toHaveBeenCalledWith(newData);
|
expect(mockModel.create).toHaveBeenCalledWith(newData);
|
||||||
expect(result).toEqual(createdData);
|
expect(result).toEqual(createdData);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('omits schema hidden fields from the create audit log', async () => {
|
||||||
|
mockModel.hiddenFields = ['alerts', 'pendingSlicerUploads'];
|
||||||
|
const newData = {
|
||||||
|
name: 'Printer',
|
||||||
|
alerts: [{ type: 'error', message: 'hotend' }],
|
||||||
|
pendingSlicerUploads: [{ file: 'file-1' }],
|
||||||
|
};
|
||||||
|
mockModel.create.mockResolvedValue({
|
||||||
|
toObject: () => ({ _id: '456', ...newData }),
|
||||||
|
_id: '456',
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await newObject({ model: mockModel, newData });
|
||||||
|
|
||||||
|
expect(mockModel.create).toHaveBeenCalledWith(newData);
|
||||||
|
expect(newAuditLog).toHaveBeenCalledWith({ name: 'Printer' }, '456', 'TestModel', null);
|
||||||
|
expect(result.alerts).toEqual(newData.alerts);
|
||||||
|
expect(result.pendingSlicerUploads).toEqual(newData.pendingSlicerUploads);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('editObject', () => {
|
describe('editObject', () => {
|
||||||
@ -199,6 +220,46 @@ describe('Database Utilities (CRUD)', () => {
|
|||||||
);
|
);
|
||||||
expect(editNotification).not.toHaveBeenCalled();
|
expect(editNotification).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('omits schema hidden fields from edit audit logs and notifications', async () => {
|
||||||
|
mockModel.hiddenFields = ['alerts', 'pendingSlicerUploads'];
|
||||||
|
mockModel.lean.mockResolvedValue({
|
||||||
|
_id: '123',
|
||||||
|
name: 'Old',
|
||||||
|
alerts: [{ type: 'info', message: 'before' }],
|
||||||
|
pendingSlicerUploads: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await editObject({
|
||||||
|
model: mockModel,
|
||||||
|
id: '123',
|
||||||
|
updateData: {
|
||||||
|
name: 'Updated',
|
||||||
|
alerts: [{ type: 'error', message: 'after' }],
|
||||||
|
pendingSlicerUploads: [{ file: 'file-1' }],
|
||||||
|
},
|
||||||
|
user: { _id: 'user-1' },
|
||||||
|
});
|
||||||
|
|
||||||
|
const visiblePrevious = { _id: '123', name: 'Old' };
|
||||||
|
const visibleUpdated = { _id: '123', name: 'Updated' };
|
||||||
|
expect(editAuditLog).toHaveBeenCalledWith(
|
||||||
|
visiblePrevious,
|
||||||
|
visibleUpdated,
|
||||||
|
'123',
|
||||||
|
'TestModel',
|
||||||
|
{ _id: 'user-1' }
|
||||||
|
);
|
||||||
|
expect(editNotification).toHaveBeenCalledWith(
|
||||||
|
visiblePrevious,
|
||||||
|
visibleUpdated,
|
||||||
|
'123',
|
||||||
|
'TestModel',
|
||||||
|
{ _id: 'user-1' }
|
||||||
|
);
|
||||||
|
expect(result.alerts).toEqual([{ type: 'error', message: 'after' }]);
|
||||||
|
expect(result.pendingSlicerUploads).toEqual([{ file: 'file-1' }]);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('deleteObject', () => {
|
describe('deleteObject', () => {
|
||||||
@ -214,5 +275,31 @@ describe('Database Utilities (CRUD)', () => {
|
|||||||
expect(mockModel.findByIdAndDelete).toHaveBeenCalledWith(id);
|
expect(mockModel.findByIdAndDelete).toHaveBeenCalledWith(id);
|
||||||
expect(result).toEqual({ deleted: true, object: mockData });
|
expect(result).toEqual({ deleted: true, object: mockData });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('omits schema hidden fields from delete audit logs and notifications', async () => {
|
||||||
|
mockModel.hiddenFields = ['alerts', 'pendingSlicerUploads'];
|
||||||
|
const mockData = {
|
||||||
|
_id: '123',
|
||||||
|
name: 'To be deleted',
|
||||||
|
alerts: [{ type: 'error', message: 'hotend' }],
|
||||||
|
pendingSlicerUploads: [{ file: 'file-1' }],
|
||||||
|
};
|
||||||
|
mockModel.findByIdAndDelete.mockResolvedValue({
|
||||||
|
toObject: () => mockData,
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await deleteObject({
|
||||||
|
model: mockModel,
|
||||||
|
id: '123',
|
||||||
|
user: { _id: 'user-1' },
|
||||||
|
});
|
||||||
|
|
||||||
|
const visible = { _id: '123', name: 'To be deleted' };
|
||||||
|
expect(deleteAuditLog).toHaveBeenCalledWith(visible, '123', 'TestModel', { _id: 'user-1' });
|
||||||
|
expect(deleteNotification).toHaveBeenCalledWith(visible, '123', 'TestModel', {
|
||||||
|
_id: 'user-1',
|
||||||
|
});
|
||||||
|
expect(result.object).toEqual(mockData);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -34,6 +34,24 @@ import { convertObjectIdStringsInFilter } from './utils.js';
|
|||||||
|
|
||||||
const BASIC_OBJECT_FIELDS = '_id _reference name color state';
|
const BASIC_OBJECT_FIELDS = '_id _reference name color state';
|
||||||
|
|
||||||
|
// Schema statics such as printerSchema.statics.hiddenFields are copied onto the model.
|
||||||
|
// Drop them from snapshots that become audit logs or notifications; the stored
|
||||||
|
// document and live updates still include them.
|
||||||
|
const omitHiddenFields = (model, value) => {
|
||||||
|
const hiddenFields = model?.hiddenFields;
|
||||||
|
if (
|
||||||
|
!Array.isArray(hiddenFields) ||
|
||||||
|
hiddenFields.length === 0 ||
|
||||||
|
value == null ||
|
||||||
|
typeof value !== 'object' ||
|
||||||
|
Array.isArray(value)
|
||||||
|
) {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
return _.omit(value, hiddenFields);
|
||||||
|
};
|
||||||
|
|
||||||
const logger = log4js.getLogger('Database');
|
const logger = log4js.getLogger('Database');
|
||||||
logger.level = config.server.logLevel;
|
logger.level = config.server.logLevel;
|
||||||
|
|
||||||
@ -1221,8 +1239,11 @@ export const editObject = async ({ model, id, updateData, user, populate, recalc
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const previousForAudit = omitHiddenFields(model, previousExpandedObject);
|
||||||
|
const updatedForAudit = omitHiddenFields(model, updatedObject);
|
||||||
|
|
||||||
// Audit log before update
|
// Audit log before update
|
||||||
await editAuditLog(previousExpandedObject, updatedObject, id, parentType, user);
|
await editAuditLog(previousForAudit, updatedForAudit, id, parentType, user);
|
||||||
|
|
||||||
if (
|
if (
|
||||||
user !== 'system' &&
|
user !== 'system' &&
|
||||||
@ -1231,7 +1252,7 @@ export const editObject = async ({ model, id, updateData, user, populate, recalc
|
|||||||
parentType !== 'userNotifier' &&
|
parentType !== 'userNotifier' &&
|
||||||
parentType !== 'objectView'
|
parentType !== 'objectView'
|
||||||
) {
|
) {
|
||||||
await editNotification(previousExpandedObject, updatedObject, id, parentType, user);
|
await editNotification(previousForAudit, updatedForAudit, id, parentType, user);
|
||||||
}
|
}
|
||||||
// Distribute update
|
// Distribute update
|
||||||
await distributeUpdate(updateData, id, parentType);
|
await distributeUpdate(updateData, id, parentType);
|
||||||
@ -1310,7 +1331,7 @@ export const newObject = async (
|
|||||||
typeof result.toObject === 'function' ? result.toObject() : result.$toObject();
|
typeof result.toObject === 'function' ? result.toObject() : result.$toObject();
|
||||||
const created = expandObjectIds(serialized);
|
const created = expandObjectIds(serialized);
|
||||||
|
|
||||||
await newAuditLog(newData, created._id, parentType, user);
|
await newAuditLog(omitHiddenFields(model, newData), created._id, parentType, user);
|
||||||
|
|
||||||
if (distributeChanges == true) {
|
if (distributeChanges == true) {
|
||||||
await distributeNew(created, parentType);
|
await distributeNew(created, parentType);
|
||||||
@ -1587,8 +1608,10 @@ export const deleteObject = async (
|
|||||||
typeof result.toObject === 'function' ? result.toObject() : result.$toObject();
|
typeof result.toObject === 'function' ? result.toObject() : result.$toObject();
|
||||||
const deleted = expandObjectIds(serialized);
|
const deleted = expandObjectIds(serialized);
|
||||||
|
|
||||||
|
const deletedForAudit = omitHiddenFields(model, deleted);
|
||||||
|
|
||||||
// Audit log the deletion
|
// Audit log the deletion
|
||||||
await deleteAuditLog(deleted, id.toString(), parentType, user);
|
await deleteAuditLog(deletedForAudit, id.toString(), parentType, user);
|
||||||
|
|
||||||
if (
|
if (
|
||||||
user !== 'system' &&
|
user !== 'system' &&
|
||||||
@ -1597,7 +1620,7 @@ export const deleteObject = async (
|
|||||||
parentType !== 'userNotifier' &&
|
parentType !== 'userNotifier' &&
|
||||||
parentType !== 'objectView'
|
parentType !== 'objectView'
|
||||||
) {
|
) {
|
||||||
await deleteNotification(deleted, id.toString(), parentType, user);
|
await deleteNotification(deletedForAudit, id.toString(), parentType, user);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (distributeChanges == true) {
|
if (distributeChanges == true) {
|
||||||
|
|||||||
@ -101,6 +101,8 @@ const rollupConfigs = [
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
printerSchema.statics.hiddenFields = ['alerts', 'pendingSlicerUploads'];
|
||||||
|
|
||||||
printerSchema.statics.stats = async function () {
|
printerSchema.statics.stats = async function () {
|
||||||
const results = await aggregateRollups({
|
const results = await aggregateRollups({
|
||||||
model: this,
|
model: this,
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user