From 9376ec89cd2918245756511d79d9adbef3a673f3 Mon Sep 17 00:00:00 2001 From: Rahul Lanjewar Date: Fri, 1 Dec 2023 18:21:25 +0530 Subject: [PATCH] merge --- package.json | 2 +- src/index.ts | 213 ++++++++++++++++++++++++++++++++------------------- src/types.ts | 21 +++++ 3 files changed, 157 insertions(+), 79 deletions(-) diff --git a/package.json b/package.json index 36cdd58..ab18f27 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "parse-audit-log", - "version": "1.0.9", + "version": "1.0.10", "description": "Audit logger for parse server", "scripts": { "test": "echo \"Error: no test specified\"", diff --git a/src/index.ts b/src/index.ts index b1b1cbc..52c36ea 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,57 +1,52 @@ import { JSONSchema } from "./index.d"; -import { AuditLoggerOptions } from "./types"; - -const defaultOptions: AuditLoggerOptions = { - prefix: "AUDIT_", - parseClient: Parse, -} - -const defaultClassLevelPermissions: JSONSchema['classLevelPermissions'] = { - get: { - '*': true, - }, - find: { - '*': true, - }, - create: { - '*': true, - }, - update: { - '*': true, - }, - delete: { - '*': true, - }, - protectedFields: {}, -}; - -const defaultFields: JSONSchema['fields'] = { - user: { - type: 'Pointer', - targetClass: '_User', - }, - master: { - type: 'Boolean' - }, - action: { - type: 'String', - }, - class: { - type: 'String', +import { AuditLoggerOptions, AuditOptions } from "./types"; + +export default class AuditLogger { + private static defaultOptions: AuditLoggerOptions = { + prefix: "AUDIT_", + parseClient: Parse, + useSingleClass: false, + singleClassName: 'AUDIT', + } + + private static defaultClassLevelPermissions: JSONSchema['classLevelPermissions'] = { + get: { + '*': true, + }, + find: { + '*': true, + }, + create: { + '*': true, + }, + update: { + '*': true, + }, + delete: { + '*': true, + }, + protectedFields: {}, } -} -function getDefaultFields(className: string): JSONSchema['fields'] { - return { - ...defaultFields, - subject: { + private static defaultFields: JSONSchema['fields'] = { + user: { type: 'Pointer', - targetClass: className + targetClass: '_User', + }, + master: { + type: 'Boolean' + }, + action: { + type: 'String', + }, + class: { + type: 'String', + }, + meta: { + type: 'Object', } } -} -export default class AuditLogger { private static initialized: boolean; private static options: AuditLoggerOptions; @@ -61,21 +56,58 @@ export default class AuditLogger { } } + public static validateOptions(options: AuditLoggerOptions) { + if (options.useSingleClass === true && options.singleClassName?.length === 0) { + throw new Error('Empty string not allowed as singleClassName. Set it to a non-empty string'); + } + } + public static initialize(options?: AuditLoggerOptions) { - this.options = Object.assign({}, defaultOptions, options); + this.options = Object.assign({}, this.defaultOptions, options); + this.validateOptions(this.options); this.initialized = true; } - public static schemas(classNames: string[]): JSONSchema[] { + private static getDefaultFields(className: string): JSONSchema['fields'] { + return { + ...this.defaultFields, + subject: this.options.useSingleClass ? { + type: 'Object', + } : { + type: 'Pointer', + targetClass: className + } + } + } + + public static schemas(classNames?: string[]): JSONSchema[] { this.validateState(); - const result: JSONSchema[] = classNames.map(className => { - return { - className: `${this.options.prefix ?? ''}${className}${this.options.postfix ?? ''}`, - fields: getDefaultFields(className), - classLevelPermissions: defaultClassLevelPermissions, - } - }) + const result: JSONSchema[] = []; + + if (classNames?.length === 0 && !this.options.useSingleClass) { + throw new Error('classNames is required.'); + } + + if (classNames?.length && this.options.useSingleClass) { + throw new Error('when useSingleClass is set to true, classNames is not required.'); + } + + if (this.options.useSingleClass) { + result.push({ + className: this.options.singleClassName as string, + fields: this.getDefaultFields(this.options.singleClassName as string), + classLevelPermissions: this.defaultClassLevelPermissions, + }) + } else { + result.push(...classNames!.map(className => { + return { + className: `${this.options.prefix ?? ''}${className}${this.options.postfix ?? ''}`, + fields: this.getDefaultFields(className), + classLevelPermissions: this.defaultClassLevelPermissions, + } + })); + } return result; } @@ -86,25 +118,29 @@ export default class AuditLogger { | Parse.Cloud.BeforeSaveRequest | Parse.Cloud.AfterSaveRequest | Parse.Cloud.BeforeDeleteRequest - | Parse.Cloud.AfterDeleteRequest + | Parse.Cloud.AfterDeleteRequest, + options?: AuditOptions ) { this.validateState(); - const auditOptions: Record = {} + const auditData: Record = {} // Handle find requests if (req.triggerName === 'beforeFind' || req.triggerName === 'afterFind') { // Handle beforeFind requests. if (req.triggerName === 'beforeFind') { const r = (req as Parse.Cloud.BeforeFindRequest); - auditOptions.action = r.isGet ? 'GET' : 'FIND'; - auditOptions.class = r.query.className; + auditData.action = r.isGet ? 'GET' : 'FIND'; + auditData.class = r.query.className; - // If req is get set the subject const queryJSON = r.query.toJSON(); - if (r.isGet) { - auditOptions.subject = new Parse.Object(r.query.className, { + // If req is get and its not a or query, set the subject + if (r.isGet && queryJSON?.where?.objectId) { + const subject = new Parse.Object(r.query.className, { objectId: r.query.toJSON().where.objectId }); + + // if we are not logging everything in a single class, subject will be a nested key. + auditData.subject = this.options.useSingleClass ? { pointer: subject } : subject } } @@ -112,16 +148,15 @@ export default class AuditLogger { if (req.triggerName === 'afterFind') { const r = (req as Parse.Cloud.AfterFindRequest); const objectCLassName = r.objects[0]?.className; - auditOptions.class = objectCLassName; - auditOptions.action = 'FIND'; + auditData.class = objectCLassName; + auditData.action = 'FIND'; } - /** * If onFind classes are provided, * and if current class does not exists in the provided classes, then return. */ - if (this.options.onFind && !this.options.onFind.includes(auditOptions.class)) { + if (this.options.onFind && !this.options.onFind.includes(auditData.class)) { return; } @@ -131,9 +166,23 @@ export default class AuditLogger { if (req.triggerName === 'beforeSave' || req.triggerName === 'afterSave') { const r = (req as Parse.Cloud.AfterSaveRequest); const objectCLassName = r.object.className; - auditOptions.subject = r.object; - auditOptions.class = objectCLassName; - auditOptions.action = r.original ? 'UPDATE' : 'CREATE'; + + auditData.class = objectCLassName; + auditData.action = r.original ? 'UPDATE' : 'CREATE'; + + + const subject = r.object.toPointer(); + // if we are not logging everything in a single class, subject will be a nested key. + auditData.subject = this.options.useSingleClass ? { pointer: subject } : subject + + // If storeChanges is set to true, then save previous and current value + if (options?.storeChanges) { + // Only available in update requests + auditData.meta = { + previous: req.original?.toJSON(), + current: req.object.toJSON() + } + } /** * If onSave classes are provided, @@ -148,10 +197,12 @@ export default class AuditLogger { if (req.triggerName === 'beforeDelete' || req.triggerName === 'afterDelete') { const r = (req as Parse.Cloud.AfterDeleteRequest); const objectCLassName = r.object.className; - auditOptions.subject = r.object.toPointer(); - auditOptions.class = objectCLassName; - auditOptions.action = 'DELETE'; + auditData.class = objectCLassName; + auditData.action = 'DELETE'; + const subject = r.object.toPointer(); + // if we are not logging everything in a single class, subject will be a nested key. + auditData.subject = this.options.useSingleClass ? { pointer: subject } : subject; /** * If onDelete classes are provided, * and if current class does not exists in the provided classes, then return. @@ -161,16 +212,22 @@ export default class AuditLogger { } } - auditOptions.master = req.master; - auditOptions.user = req.user; + auditData.master = req.master; + auditData.user = req.user; //Validate auditOptions - if (!auditOptions.class) { + if (!auditData.class) { return; } - const auditObject = new Parse.Object(`${this.options.prefix ?? ''}${auditOptions.class}${this.options.postfix ?? ''}`); - await auditObject.save(auditOptions, { useMasterKey: this.options.useMasterKey, cascadeSave: false }); + const parsedClassName = `${this.options.prefix ?? ''}${auditData.class}${this.options.postfix ?? ''}`; + + const auditObject = new Parse.Object( + this.options.useSingleClass + ? this.options.singleClassName + : parsedClassName + ); + await auditObject.save(auditData, { useMasterKey: this.options.useMasterKey, cascadeSave: false }); } } \ No newline at end of file diff --git a/src/types.ts b/src/types.ts index bb2441c..a7bd8e7 100644 --- a/src/types.ts +++ b/src/types.ts @@ -50,4 +50,25 @@ export type AuditLoggerOptions = { * If this option is provided, only the classes provided in this array will be audited. */ onDelete?: string[], + /** + * Instead of creating a new class for each audit, use a single class. + * + * When using single class, the subject is a embedded in subject.pointer + * @default false + */ + useSingleClass?: boolean, + /** + * When use single class is enabled, you can provide the name of the class you want to set. + * @default AUDIT + */ + singleClassName?: string, +} + +export type AuditOptions = { + /** + * Store previous and current values in audit objects. + * The value is stored in `meta.previous` and `meta.current` + * @default false + */ + storeChanges?: boolean } \ No newline at end of file