Skip to content

Commit

Permalink
merge
Browse files Browse the repository at this point in the history
  • Loading branch information
RahulLanjewar93 committed Dec 1, 2023
1 parent fd2aadd commit 9376ec8
Show file tree
Hide file tree
Showing 3 changed files with 157 additions and 79 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -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\"",
Expand Down
213 changes: 135 additions & 78 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -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;

Expand All @@ -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;
}
Expand All @@ -86,42 +118,45 @@ 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<any, any> = {}
const auditData: Record<any, any> = {}

// 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
}
}

// Handle afterFind requests
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;
}

Expand All @@ -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,
Expand All @@ -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.
Expand All @@ -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 });
}
}
21 changes: 21 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

0 comments on commit 9376ec8

Please sign in to comment.