From 52930558e15f9238f6717b4e3aba298ef8e3fdb7 Mon Sep 17 00:00:00 2001 From: Maninder Singh Date: Wed, 3 Mar 2021 17:05:13 +0530 Subject: [PATCH 01/15] feat: log activity for team create and edit --- app/activity/handler.ts | 15 ++++++ app/activity/index.ts | 16 ++++++ app/activity/resource.ts | 24 +++++++++ app/activity/schema.ts | 16 ++++++ config/composer.ts | 3 ++ factory/activity.ts | 19 +++++++ lib/casbin/index.ts | 7 ++- model/activity.ts | 58 ++++++++++++++++++++ model/group.ts | 3 +- model/role.ts | 3 +- model/user.ts | 9 +++- ormconfig.js | 6 ++- package.json | 1 + subscriber/model.ts | 112 +++++++++++++++++++++++++++++++++++++++ utils/constant.ts | 9 ++++ utils/deep-diff.ts | 11 ++++ yarn.lock | 5 ++ 17 files changed, 311 insertions(+), 6 deletions(-) create mode 100644 app/activity/handler.ts create mode 100644 app/activity/index.ts create mode 100644 app/activity/resource.ts create mode 100644 app/activity/schema.ts create mode 100644 factory/activity.ts create mode 100644 model/activity.ts create mode 100644 subscriber/model.ts create mode 100644 utils/constant.ts create mode 100644 utils/deep-diff.ts diff --git a/app/activity/handler.ts b/app/activity/handler.ts new file mode 100644 index 000000000..43a041e8b --- /dev/null +++ b/app/activity/handler.ts @@ -0,0 +1,15 @@ +import Hapi from '@hapi/hapi'; +import * as Resource from './resource'; + +// groups/{groupId}/activities + +// activities?{} + +export const get = { + description: 'get all activities or team activities', + tags: ['api'], + handler: async (request: Hapi.Request) => { + const { team } = request.query; + return Resource.get(team); + } +}; diff --git a/app/activity/index.ts b/app/activity/index.ts new file mode 100644 index 000000000..1f1fd20c5 --- /dev/null +++ b/app/activity/index.ts @@ -0,0 +1,16 @@ +import Hapi from '@hapi/hapi'; +import * as Handler from './handler'; + +export const plugin = { + name: 'activity', + dependencies: 'postgres', + register(server: Hapi.Server) { + server.route([ + { + method: 'GET', + path: '/api/activities', + options: Handler.get + } + ]); + } +}; diff --git a/app/activity/resource.ts b/app/activity/resource.ts new file mode 100644 index 000000000..88da9db0d --- /dev/null +++ b/app/activity/resource.ts @@ -0,0 +1,24 @@ +import { Activity } from '../../model/activity'; + +export const get = async (team = '') => { + let criteria: any = { + order: { + createdAt: 'DESC' + } + }; + + if (team.length !== 0) { + // fetch activities based on team + criteria = Object.assign(criteria, { + where: { + team + } + }); + } + + return Activity.find(criteria); +}; + +export const create = async (payload: any) => { + return await Activity.save({ ...payload }); +}; diff --git a/app/activity/schema.ts b/app/activity/schema.ts new file mode 100644 index 000000000..22317e3da --- /dev/null +++ b/app/activity/schema.ts @@ -0,0 +1,16 @@ +import Joi from 'joi'; +import Config from '../../config/config'; + +const validationOptions = Config.get('/validationOptions'); + +export const ActivityPayload = Joi.object() + .label('ActivityPayload') + .keys({ + id: Joi.string().required(), + title: Joi.string().required(), + team: Joi.string().required(), + details: Joi.array().items(Joi.object().optional()), + createdAt: Joi.date().iso().required(), + createdBy: Joi.string().required() + }) + .options(validationOptions); diff --git a/config/composer.ts b/config/composer.ts index 7f62c1401..1fce902d5 100644 --- a/config/composer.ts +++ b/config/composer.ts @@ -61,6 +61,9 @@ internals.manifest = { { plugin: '../app/role/index' }, + { + plugin: '../app/activity/index' + }, { plugin: '../plugin/proxy' }, diff --git a/factory/activity.ts b/factory/activity.ts new file mode 100644 index 000000000..41b25711d --- /dev/null +++ b/factory/activity.ts @@ -0,0 +1,19 @@ +import Faker from 'faker'; +import { define } from 'typeorm-seeding'; +import { Activity } from '../model/activity'; +// import { User } from '../model/user'; + +define(Activity, (faker: typeof Faker) => { + const activity = new Activity(); + activity.id = faker.random.uuid(); + activity.title = faker.random.words(3).toString(); + activity.model = 'User'; + activity.document = {}; + activity.documentId = faker.random.uuid(); + activity.diffs = [{}]; + // const user = new User(); + // user.id = faker.random.uuid(); + // user.displayname = faker.random.word(); + // activity.createdBy = user; + return activity; +}); diff --git a/lib/casbin/index.ts b/lib/casbin/index.ts index 9c7897ce4..3ac54f276 100644 --- a/lib/casbin/index.ts +++ b/lib/casbin/index.ts @@ -8,6 +8,10 @@ import { JsonFilteredEnforcer } from './JsonFilteredEnforcer'; +const Config = require('../../config/config'); + +const baseDir = Config.get('/typeormDir').dir; + const { newWatcher } = CasbinPgWatcher; class CasbinSingleton { @@ -51,7 +55,8 @@ class CasbinSingleton { if (!this.policyAdapter) { this.policyAdapter = await TypeORMAdapter.newAdapter({ type: 'postgres', - url: dbConnectionUrl + url: dbConnectionUrl, + subscribers: [`${baseDir}/subscriber/*{.ts,.js}`] }); } diff --git a/model/activity.ts b/model/activity.ts new file mode 100644 index 000000000..89a42e33d --- /dev/null +++ b/model/activity.ts @@ -0,0 +1,58 @@ +import { + Entity, + Column, + CreateDateColumn, + BaseEntity, + PrimaryGeneratedColumn +} from 'typeorm'; + +import Constants from '../utils/constant'; + +// eslint-disable-next-line import/no-cycle +// import { User } from './user'; + +@Entity(Constants.MODEL.Activity) +export class Activity extends BaseEntity { + @PrimaryGeneratedColumn('uuid') + id: string; + + @Column({ + type: 'varchar', + nullable: false + }) + title: string; + + @Column({ + type: 'varchar', + nullable: false + }) + model: string; + + @Column({ + type: 'varchar', + nullable: false + }) + documentId: string; + + @Column({ + type: 'jsonb', + nullable: false + }) + document: Record; + + @Column({ + type: 'jsonb', + nullable: true + }) + diffs: Record[]; + + @CreateDateColumn() + createdAt: string; + + // @Column({ + // type: 'varchar', + // nullable: false + // }) + // @ManyToOne(() => User, (user) => user.activities) + // createdBy: User; +} diff --git a/model/group.ts b/model/group.ts index 9515a5171..2860c5e69 100644 --- a/model/group.ts +++ b/model/group.ts @@ -6,8 +6,9 @@ import { BaseEntity, PrimaryGeneratedColumn } from 'typeorm'; +import Constants from '../utils/constant'; -@Entity('groups') +@Entity(Constants.MODEL.Group) export class Group extends BaseEntity { @PrimaryGeneratedColumn('uuid') id: string; diff --git a/model/role.ts b/model/role.ts index 4d288beec..b83043e6a 100644 --- a/model/role.ts +++ b/model/role.ts @@ -6,8 +6,9 @@ import { BaseEntity, PrimaryGeneratedColumn } from 'typeorm'; +import Constants from '../utils/constant'; -@Entity('roles') +@Entity(Constants.MODEL.Role) export class Role extends BaseEntity { @PrimaryGeneratedColumn('uuid') id: string; diff --git a/model/user.ts b/model/user.ts index d0d7b775e..29cb9d19b 100644 --- a/model/user.ts +++ b/model/user.ts @@ -7,7 +7,11 @@ import { PrimaryGeneratedColumn } from 'typeorm'; -@Entity('users') +// eslint-disable-next-line import/no-cycle +// import { Activity } from './activity'; +import Constants from '../utils/constant'; + +@Entity(Constants.MODEL.User) export class User extends BaseEntity { @PrimaryGeneratedColumn('uuid') id: string; @@ -30,6 +34,9 @@ export class User extends BaseEntity { }) metadata: Record; + // @OneToMany(() => Activity, (activity) => activity.createdBy) + // activities: Activity[]; + @CreateDateColumn() createdAt: string; diff --git a/ormconfig.js b/ormconfig.js index dc59650f3..607213213 100644 --- a/ormconfig.js +++ b/ormconfig.js @@ -1,5 +1,6 @@ +const { SnakeNamingStrategy } = require('typeorm-naming-strategies'); const Config = require('./config/config'); -const {SnakeNamingStrategy} = require('typeorm-naming-strategies'); + const baseDir = Config.get('/typeormDir').dir; module.exports = { @@ -10,8 +11,9 @@ module.exports = { entities: [`${baseDir}/model/*{.ts,.js}`], migrations: [`${baseDir}/migration/*{.ts,.js}`], factories: [`${baseDir}/factory/*{.ts,.js}`], + subscribers: [`${baseDir}/subscriber/*{.ts,.js}`], cli: { migrationsDir: 'migration' }, - namingStrategy:new SnakeNamingStrategy() + namingStrategy: new SnakeNamingStrategy() }; diff --git a/package.json b/package.json index 63639ab47..5168f43cf 100644 --- a/package.json +++ b/package.json @@ -29,6 +29,7 @@ "casbin": "^5.2.2", "casbin-pg-watcher": "^0.1.1", "confidence": "^5.0.0", + "deep-diff": "^1.0.2", "faker": "^5.1.0", "hapi-swagger": "^14.0.0", "joi": "^17.3.0", diff --git a/subscriber/model.ts b/subscriber/model.ts new file mode 100644 index 000000000..cba741258 --- /dev/null +++ b/subscriber/model.ts @@ -0,0 +1,112 @@ +import { + EntitySubscriberInterface, + EventSubscriber, + InsertEvent, + UpdateEvent +} from 'typeorm'; +import { delta } from '../utils/deep-diff'; +import Constants from '../utils/constant'; +import { create } from '../app/activity/resource'; + +const excludeFields = ['id', 'createdAt', 'updatedAt']; +const actions = { + CREATE: 'create', + EDIT: 'edit' +}; + +const getTitle = (event: any, type: string) => { + let title = ''; + switch (type) { + case actions.CREATE: + if (event.metadata.tableName === Constants.MODEL.Group) { + title = `Created ${event.entity?.displayname} Team `; + } else if (event.metadata.tableName === Constants.MODEL.CasbinRule) { + title = `Created ${event.entity?.ptype} Casbin Rule `; + } + break; + case actions.EDIT: + if (event.metadata.tableName === Constants.MODEL.Group) { + title = `Edited ${event.entity?.displayname}`; + } else if (event.metadata.tableName === Constants.MODEL.CasbinRule) { + title = `Edited ${event.entity?.ptype} Casbin Rule `; + } + break; + default: + title = ''; + } + + return title; +}; + +const getDiff = (event: any, type: string) => { + switch (type) { + case actions.CREATE: + return delta({}, event.entity, { + exclude: excludeFields + }); + case actions.EDIT: + return delta(event.databaseEntity, event.entity, { + exclude: excludeFields + }); + default: + return []; + } +}; + +const storeActivityPayload = async (event: any, type: string) => { + // console.log( + // 'storeActivityPayload event -> ', + // event.entity, + // event.databaseEntity, + // event.metadata.tableName + // ); + if ( + event.metadata.tableName === Constants.MODEL.Activity || + event.metadata.tableName === Constants.MODEL.Role || + event.metadata.tableName === Constants.MODEL.User + ) { + return; + } + const title = getTitle(event, type); + await create({ + document: event.entity, + title, + documentId: event.entity.id, + model: event.metadata.tableName, + diffs: getDiff(event, type) + }); +}; + +@EventSubscriber() +export class ModelSubscriber implements EntitySubscriberInterface { + afterInsert = async (event: InsertEvent) => { + await storeActivityPayload(event, actions.CREATE); + }; + + afterUpdate = async (event: UpdateEvent) => { + await storeActivityPayload(event, actions.EDIT); + }; + + /** + * Called before entity removal. + */ + // beforeRemove = async (event: RemoveEvent) => { + // console.log( + // `BEFORE ENTITY WITH metadata.tableName ${JSON.stringify( + // event.metadata.tableName + // )} REMOVED: `, + // event.entity + // ); + // }; + + /** + * Called after entity removal. + */ + // afterRemove = async (event: RemoveEvent) => { + // Object.keys(event.queryRunner.data).forEach((key) => { + // console.log(`key => ${key} and value => ${event.queryRunner.data[key]}`); + // }); + // + // console.log(`AFTER ENTITY WITH queryRunner REMOVED: `, event.entity); + // }; +} diff --git a/utils/constant.ts b/utils/constant.ts new file mode 100644 index 000000000..206f7ed58 --- /dev/null +++ b/utils/constant.ts @@ -0,0 +1,9 @@ +export default { + MODEL: { + Activity: 'activities', + Group: 'groups', + Role: 'roles', + User: 'users', + CasbinRule: 'casbin_rule' + } +}; diff --git a/utils/deep-diff.ts b/utils/deep-diff.ts new file mode 100644 index 000000000..918d97e63 --- /dev/null +++ b/utils/deep-diff.ts @@ -0,0 +1,11 @@ +const { diff } = require('deep-diff'); + +export const delta = ( + previous = {}, + current = {}, + options?: { exclude: string[] } +) => { + return diff(previous, current).filter((i: any) => + (options?.exclude || []).every((x: any) => i.path.indexOf(x) === -1) + ); +}; diff --git a/yarn.lock b/yarn.lock index 3940d6ff2..ebd32b699 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1703,6 +1703,11 @@ decompress-response@^3.3.0: dependencies: mimic-response "^1.0.0" +deep-diff@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/deep-diff/-/deep-diff-1.0.2.tgz#afd3d1f749115be965e89c63edc7abb1506b9c26" + integrity sha512-aWS3UIVH+NPGCD1kki+DCU9Dua032iSsO43LqQpcs4R3+dVv7tX0qBGjiVHJHjplsoUM2XRO/KB92glqc68awg== + deep-extend@^0.6.0: version "0.6.0" resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.6.0.tgz#c4fa7c95404a17a9c3e8ca7e1537312b736330ac" From d7fe70fd12c9ae9d2a767c739fb105abd10532e8 Mon Sep 17 00:00:00 2001 From: Maninder Singh Date: Thu, 4 Mar 2021 17:08:17 +0530 Subject: [PATCH 02/15] feat: remove policies log --- app/policy/resource.ts | 9 +++ subscriber/model.ts | 138 ++++++++++++++++++++++++++--------------- 2 files changed, 96 insertions(+), 51 deletions(-) diff --git a/app/policy/resource.ts b/app/policy/resource.ts index 75369ae09..a7fa6d2b4 100644 --- a/app/policy/resource.ts +++ b/app/policy/resource.ts @@ -16,6 +16,7 @@ export interface PolicyOperation { resource: JSObj; action: JSObj; } +let casbinPolicies: any[] = []; export const bulkOperation = async ( policyOperations: PolicyOperation[] = [], @@ -211,3 +212,11 @@ export const getUsersOfGroupWithPolicies = async ( return parsePoliciesWithSubject(rawResult, 'user'); }; + +export const setPolicies = (policies: [] = []) => { + casbinPolicies = policies; +}; + +export const getPolicies = () => { + return casbinPolicies; +}; diff --git a/subscriber/model.ts b/subscriber/model.ts index cba741258..521b6eaea 100644 --- a/subscriber/model.ts +++ b/subscriber/model.ts @@ -2,16 +2,23 @@ import { EntitySubscriberInterface, EventSubscriber, InsertEvent, + RemoveEvent, UpdateEvent } from 'typeorm'; import { delta } from '../utils/deep-diff'; import Constants from '../utils/constant'; import { create } from '../app/activity/resource'; +import { getPolicies, setPolicies } from '../app/policy/resource'; -const excludeFields = ['id', 'createdAt', 'updatedAt']; +interface Policy { + [key: string]: any; +} + +const excludeFields = ['createdAt', 'updatedAt']; const actions = { CREATE: 'create', - EDIT: 'edit' + EDIT: 'edit', + DELETE: 'delete' }; const getTitle = (event: any, type: string) => { @@ -31,6 +38,13 @@ const getTitle = (event: any, type: string) => { title = `Edited ${event.entity?.ptype} Casbin Rule `; } break; + case actions.DELETE: + if (event.metadata.tableName === Constants.MODEL.Group) { + title = `Deleted ${event.entity?.displayname} Team`; + } else if (event.metadata.tableName === Constants.MODEL.CasbinRule) { + title = `Deleted ${event.databaseEntity?.ptype} Casbin Rule `; + } + break; default: title = ''; } @@ -38,28 +52,7 @@ const getTitle = (event: any, type: string) => { return title; }; -const getDiff = (event: any, type: string) => { - switch (type) { - case actions.CREATE: - return delta({}, event.entity, { - exclude: excludeFields - }); - case actions.EDIT: - return delta(event.databaseEntity, event.entity, { - exclude: excludeFields - }); - default: - return []; - } -}; - const storeActivityPayload = async (event: any, type: string) => { - // console.log( - // 'storeActivityPayload event -> ', - // event.entity, - // event.databaseEntity, - // event.metadata.tableName - // ); if ( event.metadata.tableName === Constants.MODEL.Activity || event.metadata.tableName === Constants.MODEL.Role || @@ -67,14 +60,46 @@ const storeActivityPayload = async (event: any, type: string) => { ) { return; } + let promise = null; const title = getTitle(event, type); - await create({ - document: event.entity, - title, - documentId: event.entity.id, - model: event.metadata.tableName, - diffs: getDiff(event, type) - }); + switch (type) { + case actions.CREATE: + promise = create({ + document: {}, + title, + documentId: '0', + model: event.metadata.tableName, + diffs: delta({}, event.entity || {}, { + exclude: excludeFields + }) + }); + break; + case actions.EDIT: + promise = create({ + document: event.databaseEntity, + title, + documentId: event.databaseEntity.id, + model: event.metadata.tableName, + diffs: delta(event.databaseEntity || {}, event.entity || {}, { + exclude: excludeFields + }) + }); + break; + case actions.DELETE: + promise = create({ + document: event.databaseEntity, + title, + documentId: event.databaseEntity.id, + model: event.metadata.tableName, + diffs: delta(event.databaseEntity || {}, event.entity || {}, { + exclude: excludeFields + }) + }); + break; + default: + promise = Promise.resolve(); + } + await promise; }; @EventSubscriber() @@ -87,26 +112,37 @@ export class ModelSubscriber implements EntitySubscriberInterface { await storeActivityPayload(event, actions.EDIT); }; - /** - * Called before entity removal. - */ - // beforeRemove = async (event: RemoveEvent) => { - // console.log( - // `BEFORE ENTITY WITH metadata.tableName ${JSON.stringify( - // event.metadata.tableName - // )} REMOVED: `, - // event.entity - // ); - // }; + beforeRemove = async (event: RemoveEvent) => { + const policies = await event.queryRunner.query('select * from casbin_rule'); + setPolicies(policies); + }; - /** - * Called after entity removal. - */ - // afterRemove = async (event: RemoveEvent) => { - // Object.keys(event.queryRunner.data).forEach((key) => { - // console.log(`key => ${key} and value => ${event.queryRunner.data[key]}`); - // }); - // - // console.log(`AFTER ENTITY WITH queryRunner REMOVED: `, event.entity); - // }; + afterRemove = async (event: RemoveEvent) => { + const previousPolicies = getPolicies(); + const currentPolicies = await event.queryRunner.query( + 'select * from casbin_rule' + ); + const currentPoliciesMap: Policy = {}; + currentPolicies.forEach((policy: any) => { + if ( + !Object.prototype.hasOwnProperty.call(currentPoliciesMap, policy.id) + ) { + currentPoliciesMap[policy.id] = policy; + } + }); + await Promise.all( + previousPolicies + .filter((policy: any) => { + return !Object.prototype.hasOwnProperty.call( + currentPoliciesMap, + policy.id + ); + }) + .map((policy: any) => { + // eslint-disable-next-line no-param-reassign + event.databaseEntity = policy; + return storeActivityPayload(event, actions.DELETE); + }) + ); + }; } From 1931f27e9053757792374ff8357b89a7374a10b7 Mon Sep 17 00:00:00 2001 From: Maninder Singh Date: Thu, 4 Mar 2021 17:24:13 +0530 Subject: [PATCH 03/15] feat : migration script for activity table --- .../1614858387492-CreateActivityTable.ts | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 migration/1614858387492-CreateActivityTable.ts diff --git a/migration/1614858387492-CreateActivityTable.ts b/migration/1614858387492-CreateActivityTable.ts new file mode 100644 index 000000000..cf358b326 --- /dev/null +++ b/migration/1614858387492-CreateActivityTable.ts @@ -0,0 +1,22 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class CreateActivityTable1614858387492 implements MigrationInterface { + name = 'CreateActivityTable1614858387492'; + + public up = async (queryRunner: QueryRunner) => { + await queryRunner.query(` + create table activities ( + id uuid default uuid_generate_v4() not null constraint activities_pk primary key, + title character varying not null, + model character varying not null, + document_id character varying not null, + document jsonb, + diffs jsonb, + created_at timestamp default now() not null ); + `); + }; + + public down = async (queryRunner: QueryRunner) => { + await queryRunner.query(`DROP TABLE "activities"`); + }; +} From ff1f431613e1824749686c1b0155434b6c37ca34 Mon Sep 17 00:00:00 2001 From: Maninder Singh Date: Fri, 5 Mar 2021 18:18:31 +0530 Subject: [PATCH 04/15] feat : pass logged in user into typeorm subscriber --- app/group/resource.ts | 18 ++++++++++++++++-- migration/1614858387492-CreateActivityTable.ts | 1 + model/activity.ts | 15 ++++++--------- model/user.ts | 5 ----- subscriber/model.ts | 9 ++++++--- 5 files changed, 29 insertions(+), 19 deletions(-) diff --git a/app/group/resource.ts b/app/group/resource.ts index d4f36d3b8..9e5550aec 100644 --- a/app/group/resource.ts +++ b/app/group/resource.ts @@ -8,6 +8,7 @@ import { toLikeQuery } from '../policy/util'; import CasbinSingleton from '../../lib/casbin'; import { parseGroupListResult } from './util'; import getUniqName from '../../lib/getUniqName'; +import { User } from '../../model/user'; type JSObj = Record; @@ -182,7 +183,15 @@ export const create = async (payload: any, loggedInUserId: string) => { const basename = groupPayload?.groupname || groupPayload?.displayname; const groupname = await getUniqName(basename, 'groupname', Group); - const groupResult = await Group.save({ ...groupPayload, groupname }); + const user = await User.findOne({ + where: { + id: loggedInUserId + } + }); + const groupResult = await Group.save( + { ...groupPayload, groupname }, + { data: { user } } + ); const groupId = groupResult.id; await upsertGroupAndAttributesMapping(groupId, attributes); @@ -204,6 +213,11 @@ export const update = async ( const { policies = [], attributes = [], ...groupPayload } = payload; const groupWithExtraKeys = await get(groupId); const group = R.omit(['policies', 'attributes'], groupWithExtraKeys); + const user = await User.findOne({ + where: { + id: loggedInUserId + } + }); // ? We need to check this only if the attributes change await checkSubjectHasAccessToEditGroup( @@ -212,7 +226,7 @@ export const update = async ( loggedInUserId ); - await Group.save({ ...group, ...groupPayload }); + await Group.save({ ...group, ...groupPayload }, { data: { user } }); const policyOperationResult = await bulkUpsertPoliciesForGroup( groupId, diff --git a/migration/1614858387492-CreateActivityTable.ts b/migration/1614858387492-CreateActivityTable.ts index cf358b326..436c24ba0 100644 --- a/migration/1614858387492-CreateActivityTable.ts +++ b/migration/1614858387492-CreateActivityTable.ts @@ -12,6 +12,7 @@ export class CreateActivityTable1614858387492 implements MigrationInterface { document_id character varying not null, document jsonb, diffs jsonb, + created_by jsonb, created_at timestamp default now() not null ); `); }; diff --git a/model/activity.ts b/model/activity.ts index 89a42e33d..83ced545f 100644 --- a/model/activity.ts +++ b/model/activity.ts @@ -7,9 +7,7 @@ import { } from 'typeorm'; import Constants from '../utils/constant'; - -// eslint-disable-next-line import/no-cycle -// import { User } from './user'; +import { User } from './user'; @Entity(Constants.MODEL.Activity) export class Activity extends BaseEntity { @@ -49,10 +47,9 @@ export class Activity extends BaseEntity { @CreateDateColumn() createdAt: string; - // @Column({ - // type: 'varchar', - // nullable: false - // }) - // @ManyToOne(() => User, (user) => user.activities) - // createdBy: User; + @Column({ + type: 'jsonb', + nullable: false + }) + createdBy: User; } diff --git a/model/user.ts b/model/user.ts index 29cb9d19b..1f0134649 100644 --- a/model/user.ts +++ b/model/user.ts @@ -7,8 +7,6 @@ import { PrimaryGeneratedColumn } from 'typeorm'; -// eslint-disable-next-line import/no-cycle -// import { Activity } from './activity'; import Constants from '../utils/constant'; @Entity(Constants.MODEL.User) @@ -34,9 +32,6 @@ export class User extends BaseEntity { }) metadata: Record; - // @OneToMany(() => Activity, (activity) => activity.createdBy) - // activities: Activity[]; - @CreateDateColumn() createdAt: string; diff --git a/subscriber/model.ts b/subscriber/model.ts index 521b6eaea..d6e8fbb07 100644 --- a/subscriber/model.ts +++ b/subscriber/model.ts @@ -71,7 +71,8 @@ const storeActivityPayload = async (event: any, type: string) => { model: event.metadata.tableName, diffs: delta({}, event.entity || {}, { exclude: excludeFields - }) + }), + createdBy: event.queryRunner.data.user }); break; case actions.EDIT: @@ -82,7 +83,8 @@ const storeActivityPayload = async (event: any, type: string) => { model: event.metadata.tableName, diffs: delta(event.databaseEntity || {}, event.entity || {}, { exclude: excludeFields - }) + }), + createdBy: event.queryRunner.data.user }); break; case actions.DELETE: @@ -93,7 +95,8 @@ const storeActivityPayload = async (event: any, type: string) => { model: event.metadata.tableName, diffs: delta(event.databaseEntity || {}, event.entity || {}, { exclude: excludeFields - }) + }), + createdBy: event.queryRunner.data.user }); break; default: From 7086024ef08792dc332d3f3cff640dca58441ce9 Mon Sep 17 00:00:00 2001 From: Maninder Singh Date: Tue, 9 Mar 2021 10:51:58 +0530 Subject: [PATCH 05/15] feat: log roles in activity --- app/activity/resource.ts | 86 ++++++++++++++++ app/group/resource.ts | 31 ++++-- app/group/user/resource.ts | 27 ++++- app/policy/resource.ts | 12 ++- lib/casbin/IEnforcer.ts | 31 ++++-- lib/casbin/JsonFilteredEnforcer.ts | 141 ++++++++++++++++++++++++-- plugin/iam/responseHooks.ts | 16 ++- subscriber/model.ts | 157 ++++------------------------- 8 files changed, 327 insertions(+), 174 deletions(-) diff --git a/app/activity/resource.ts b/app/activity/resource.ts index 88da9db0d..3ab52fa5b 100644 --- a/app/activity/resource.ts +++ b/app/activity/resource.ts @@ -1,4 +1,6 @@ import { Activity } from '../../model/activity'; +import Constants from '../../utils/constant'; +import { delta } from '../../utils/deep-diff'; export const get = async (team = '') => { let criteria: any = { @@ -22,3 +24,87 @@ export const get = async (team = '') => { export const create = async (payload: any) => { return await Activity.save({ ...payload }); }; + +const excludeFields = ['createdAt', 'updatedAt']; +export const actions = { + CREATE: 'create', + EDIT: 'edit', + DELETE: 'delete' +}; + +const getTitle = (event: any, type: string) => { + let title = ''; + switch (type) { + case actions.CREATE: + if (event.metadata.tableName === Constants.MODEL.Group) { + title = `Created ${event.entity?.displayname} Team `; + } else if (event.metadata.tableName === Constants.MODEL.CasbinRule) { + title = `Created ${event.entity?.ptype} Casbin Rule `; + } + break; + case actions.EDIT: + if (event.metadata.tableName === Constants.MODEL.Group) { + title = `Edited ${event.entity?.displayname}`; + } else if (event.metadata.tableName === Constants.MODEL.CasbinRule) { + title = `Edited ${event.entity?.ptype} Casbin Rule `; + } + break; + case actions.DELETE: + if (event.metadata.tableName === Constants.MODEL.Group) { + title = `Deleted ${event.entity?.displayname} Team`; + } else if (event.metadata.tableName === Constants.MODEL.CasbinRule) { + title = `Deleted ${event.databaseEntity?.ptype} Casbin Rule `; + } + break; + default: + title = ''; + } + + return title; +}; + +export const log = async (event: any, type: string) => { + let promise = null; + const title = getTitle(event, type); + switch (type) { + case actions.CREATE: + promise = create({ + document: {}, + title, + documentId: '0', + model: event.metadata.tableName, + diffs: delta({}, event.entity || {}, { + exclude: excludeFields + }), + createdBy: event.queryRunner.data.user + }); + break; + case actions.EDIT: + promise = create({ + document: event.databaseEntity, + title, + documentId: event.databaseEntity.id, + model: event.metadata.tableName, + diffs: delta(event.databaseEntity || {}, event.entity || {}, { + exclude: excludeFields + }), + createdBy: event.queryRunner.data.user + }); + break; + case actions.DELETE: + promise = create({ + document: event.databaseEntity, + title, + documentId: event.databaseEntity.id, + model: event.metadata.tableName, + diffs: delta(event.databaseEntity || {}, event.entity || {}, { + exclude: excludeFields + }), + createdBy: event.queryRunner.data.user + }); + break; + default: + promise = Promise.resolve(); + } + await promise; +}; diff --git a/app/group/resource.ts b/app/group/resource.ts index 9e5550aec..79eb67506 100644 --- a/app/group/resource.ts +++ b/app/group/resource.ts @@ -56,20 +56,29 @@ export const checkSubjectHasAccessToCreateAttributesMapping = async ( export const upsertGroupAndAttributesMapping = async ( groupId: string, - attributes: JSObj[] + attributes: JSObj[], + loggedInUser: User | undefined ) => { if (R.isEmpty(attributes)) { return; } - - await CasbinSingleton?.enforcer?.removeAllResourceGroupingJsonPolicy({ - group: groupId - }); + const options = { created_by: loggedInUser }; + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + await CasbinSingleton?.enforcer?.removeAllResourceGroupingJsonPolicy( + { + group: groupId + }, + options + ); await Promise.all( attributes.map(async (attribute: JSObj) => { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore await CasbinSingleton?.enforcer?.addResourceGroupingJsonPolicy( { group: groupId }, - attribute + attribute, + options ); }) ); @@ -93,7 +102,11 @@ export const checkSubjectHasAccessToEditGroup = async ( ) => { const groupId = group.id; const prevAttributes = group.attributes; - + const user = await User.findOne({ + where: { + id: loggedInUserId + } + }); // ? We need to check this only if the attributes change if (!R.equals(attributes, prevAttributes)) { await checkSubjectHasAccessToCreateAttributesMapping( @@ -102,7 +115,7 @@ export const checkSubjectHasAccessToEditGroup = async ( }, [...attributes, ...prevAttributes] ); - await upsertGroupAndAttributesMapping(groupId, attributes); + await upsertGroupAndAttributesMapping(groupId, attributes, user); } // ? the user needs access to the group if they need to edit it @@ -194,7 +207,7 @@ export const create = async (payload: any, loggedInUserId: string) => { ); const groupId = groupResult.id; - await upsertGroupAndAttributesMapping(groupId, attributes); + await upsertGroupAndAttributesMapping(groupId, attributes, user); const policyOperationResult = await bulkUpsertPoliciesForGroup( groupId, diff --git a/app/group/user/resource.ts b/app/group/user/resource.ts index 9adb0e17b..50e197ac7 100644 --- a/app/group/user/resource.ts +++ b/app/group/user/resource.ts @@ -23,8 +23,19 @@ export const create = async ( const group = { group: groupId }; - - await CasbinSingleton.enforcer?.addSubjectGroupingJsonPolicy(subject, group); + const user = await User.findOne({ + where: { + id: loggedInUserId + } + }); + const options = { created_by: user }; + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + await CasbinSingleton.enforcer?.addSubjectGroupingJsonPolicy( + subject, + group, + options + ); return await bulkOperation(policies, { user: loggedInUserId }); }; @@ -49,10 +60,18 @@ export const remove = async ( ) => { const userObj = { user: userId }; const groupObj = { group: groupId }; - + const user = await User.findOne({ + where: { + id: loggedInUserId + } + }); + const options = { created_by: user }; + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore await CasbinSingleton.enforcer?.removeSubjectGroupingJsonPolicy( userObj, - groupObj + groupObj, + options ); const policies = (await getPoliciesBySubject(userObj, groupObj)).map( diff --git a/app/policy/resource.ts b/app/policy/resource.ts index a7fa6d2b4..839ecd6c3 100644 --- a/app/policy/resource.ts +++ b/app/policy/resource.ts @@ -22,6 +22,11 @@ export const bulkOperation = async ( policyOperations: PolicyOperation[] = [], subject: JSObj ) => { + const user = await User.findOne({ + where: { + id: subject.user + } + }); const promiseList = policyOperations.map(async ({ operation, ...policy }) => { // ? the subject who is performing the action should have iam.manage permission const hasAccess = await CasbinSingleton.enforcer?.enforceJson( @@ -31,12 +36,14 @@ export const bulkOperation = async ( ); if (!hasAccess) return false; + const options: JSObj = { created_by: user }; switch (operation) { case 'create': { await CasbinSingleton.enforcer?.addJsonPolicy( policy.subject, policy.resource, - policy.action + policy.action, + options ); break; } @@ -44,7 +51,8 @@ export const bulkOperation = async ( await CasbinSingleton.enforcer?.removeJsonPolicy( policy.subject, policy.resource, - policy.action + policy.action, + { created_by: user } ); break; } diff --git a/lib/casbin/IEnforcer.ts b/lib/casbin/IEnforcer.ts index b416f867b..541d23894 100644 --- a/lib/casbin/IEnforcer.ts +++ b/lib/casbin/IEnforcer.ts @@ -19,45 +19,58 @@ interface IEnforcer { addJsonPolicy( subject: JsonAttributes, resource: JsonAttributes, - action: JsonAttributes + action: JsonAttributes, + options: JsonAttributes ): void; - addStrPolicy(subject: string, resource: string, action: string): void; + addStrPolicy( + subject: string, + resource: string, + action: string, + options: JsonAttributes + ): void; removeJsonPolicy( subject: JsonAttributes, resource: JsonAttributes, - action: JsonAttributes + action: JsonAttributes, + options: JsonAttributes ): void; addSubjectGroupingJsonPolicy( subject: OneKey, - jsonAttributes: JsonAttributes + jsonAttributes: JsonAttributes, + options: JsonAttributes ): void; removeSubjectGroupingJsonPolicy( subject: OneKey, - jsonAttributes: JsonAttributes + jsonAttributes: JsonAttributes, + options: JsonAttributes ): void; addResourceGroupingJsonPolicy( resource: OneKey, - jsonAttributes: JsonAttributes + jsonAttributes: JsonAttributes, + options: JsonAttributes ): void; // ? Note: this will remove all policies by resource keys and then insert the new one upsertResourceGroupingJsonPolicy( resource: OneKey, - jsonAttributes: JsonAttributes + jsonAttributes: JsonAttributes, + options: JsonAttributes ): void; removeAllResourceGroupingJsonPolicy( - resource: OneKey + resource: OneKey, + options: JsonAttributes ): void; addActionGroupingJsonPolicy( action: OneKey, - jsonAttributes: JsonAttributes + jsonAttributes: JsonAttributes, + options: JsonAttributes ): void; } diff --git a/lib/casbin/JsonFilteredEnforcer.ts b/lib/casbin/JsonFilteredEnforcer.ts index 9dbc66735..50feead9b 100644 --- a/lib/casbin/JsonFilteredEnforcer.ts +++ b/lib/casbin/JsonFilteredEnforcer.ts @@ -5,6 +5,10 @@ import { newEnforcerWithClass } from 'casbin'; import { convertJSONToStringInOrder, JsonEnforcer } from './JsonEnforcer'; import { toLikeQuery } from '../../app/policy/util'; import IEnforcer, { JsonAttributes, OneKey, PolicyObj } from './IEnforcer'; +import { + log as ActivityLog, + actions as ActivityActions +} from '../../app/activity/resource'; const groupPolicyParameters = (policies: PolicyObj[]) => { const res = policies.reduce( @@ -23,6 +27,45 @@ const groupPolicyParameters = (policies: PolicyObj[]) => { }; }; +const diff = (previous: JsonAttributes[], current: JsonAttributes[]) => { + return R.differenceWith( + (first, second) => { + return first.id === second?.id; + }, + current, + previous + ); +}; + +const sendLog = async ( + policies: JsonAttributes[], + type: string, + user: unknown +) => { + return Promise.all( + policies.map((policy: any) => { + const log = { + entity: policy, + databaseEntity: {}, + metadata: { + tableName: 'casbin_rule' + }, + queryRunner: { + data: { + user + } + } + }; + + if (type === ActivityActions.DELETE) { + log.databaseEntity = log.entity; + log.entity = {}; + } + return ActivityLog(log, type); + }) + ); +}; + export class JsonFilteredEnforcer implements IEnforcer { public static params: any[]; @@ -100,6 +143,13 @@ export class JsonFilteredEnforcer implements IEnforcer { return R.uniq(actionMappings.map((aM) => aM.v1).concat(actions)); } + private async getAllPolicies() { + return await createQueryBuilder() + .select('*') + .from('casbin_rule', 'casbin_rule') + .getRawMany(); + } + private async getEnforcerWithPolicies(policies: PolicyObj[]) { const enforcer = await this.getEnforcer(); const { subjects, resources, actions } = groupPolicyParameters(policies); @@ -177,27 +227,49 @@ export class JsonFilteredEnforcer implements IEnforcer { public async addJsonPolicy( subject: JsonAttributes, resource: JsonAttributes, - action: JsonAttributes + action: JsonAttributes, + options: JsonAttributes ) { const enforcer = await this.getEnforcer(); + const previousPolicies = await this.getAllPolicies(); await enforcer.addPolicy( convertJSONToStringInOrder(subject), convertJSONToStringInOrder(resource), convertJSONToStringInOrder(action) ); + const currentPolicies = await this.getAllPolicies(); + await sendLog( + diff(previousPolicies, currentPolicies), + ActivityActions.CREATE, + options.created_by + ); } - public async addStrPolicy(subject: string, resource: string, action: string) { + public async addStrPolicy( + subject: string, + resource: string, + action: string, + options: JsonAttributes + ) { const enforcer = await this.getEnforcer(); + const previousPolicies = await this.getAllPolicies(); await enforcer.addPolicy(subject, resource, action); + const currentPolicies = await this.getAllPolicies(); + await sendLog( + diff(previousPolicies, currentPolicies), + ActivityActions.CREATE, + options.created_by + ); } public async removeJsonPolicy( subject: JsonAttributes, resource: JsonAttributes, - action: JsonAttributes + action: JsonAttributes, + options: JsonAttributes ) { const enforcer = await this.getEnforcer(); + const previousPolicies = await this.getAllPolicies(); await enforcer.removeFilteredNamedPolicy( 'p', 0, @@ -205,26 +277,42 @@ export class JsonFilteredEnforcer implements IEnforcer { convertJSONToStringInOrder(resource), convertJSONToStringInOrder(action) ); + const currentPolicies = await this.getAllPolicies(); + await sendLog( + diff(currentPolicies, previousPolicies), + ActivityActions.DELETE, + options.created_by + ); } public async addSubjectGroupingJsonPolicy( subject: OneKey, - jsonAttributes: JsonAttributes + jsonAttributes: JsonAttributes, + options: JsonAttributes ) { const enforcer = await this.getEnforcer(); + const previousPolicies = await this.getAllPolicies(); await enforcer.addNamedGroupingPolicy( 'g', convertJSONToStringInOrder(subject), convertJSONToStringInOrder(jsonAttributes), 'subject' ); + const currentPolicies = await this.getAllPolicies(); + await sendLog( + diff(previousPolicies, currentPolicies), + ActivityActions.CREATE, + options.created_by + ); } public async removeSubjectGroupingJsonPolicy( subject: OneKey, - jsonAttributes: JsonAttributes + jsonAttributes: JsonAttributes, + options: JsonAttributes ) { const enforcer = await this.getEnforcer(); + const previousPolicies = await this.getAllPolicies(); await enforcer.removeFilteredNamedGroupingPolicy( 'g', 0, @@ -232,52 +320,83 @@ export class JsonFilteredEnforcer implements IEnforcer { convertJSONToStringInOrder(jsonAttributes), 'subject' ); + const currentPolicies = await this.getAllPolicies(); + await sendLog( + diff(currentPolicies, previousPolicies), + ActivityActions.DELETE, + options.created_by + ); } public async addResourceGroupingJsonPolicy( resource: OneKey, - jsonAttributes: JsonAttributes + jsonAttributes: JsonAttributes, + options: JsonAttributes ) { const enforcer = await this.getEnforcer(); + const previousPolicies = await this.getAllPolicies(); await enforcer.addNamedGroupingPolicy( 'g2', convertJSONToStringInOrder(resource), convertJSONToStringInOrder(jsonAttributes), 'resource' ); + const currentPolicies = await this.getAllPolicies(); + await sendLog( + diff(previousPolicies, currentPolicies), + ActivityActions.CREATE, + options.created_by + ); } // ? Note: this will remove all policies by resource keys and then insert the new one public async upsertResourceGroupingJsonPolicy( resource: OneKey, - jsonAttributes: JsonAttributes + jsonAttributes: JsonAttributes, + options: JsonAttributes ) { - await this.removeAllResourceGroupingJsonPolicy(resource); - await this.addResourceGroupingJsonPolicy(resource, jsonAttributes); + await this.removeAllResourceGroupingJsonPolicy(resource, options); + await this.addResourceGroupingJsonPolicy(resource, jsonAttributes, options); } public async removeAllResourceGroupingJsonPolicy( - resource: OneKey + resource: OneKey, + options: JsonAttributes ) { const enforcer = await this.getEnforcer(); + const previousPolicies = await this.getAllPolicies(); await enforcer.removeFilteredNamedGroupingPolicy( 'g2', 0, convertJSONToStringInOrder(resource) ); + const currentPolicies = await this.getAllPolicies(); + await sendLog( + diff(currentPolicies, previousPolicies), + ActivityActions.DELETE, + options.created_by + ); } public async addActionGroupingJsonPolicy( action: OneKey, - jsonAttributes: JsonAttributes + jsonAttributes: JsonAttributes, + options: JsonAttributes ) { const enforcer = await this.getEnforcer(); + const previousPolicies = await this.getAllPolicies(); await enforcer.addNamedGroupingPolicy( 'g3', convertJSONToStringInOrder(action), convertJSONToStringInOrder(jsonAttributes), 'action' ); + const currentPolicies = await this.getAllPolicies(); + await sendLog( + diff(previousPolicies, currentPolicies), + ActivityActions.CREATE, + options.created_by + ); } } diff --git a/plugin/iam/responseHooks.ts b/plugin/iam/responseHooks.ts index e78073719..1ecbe7f6f 100644 --- a/plugin/iam/responseHooks.ts +++ b/plugin/iam/responseHooks.ts @@ -7,7 +7,8 @@ import { constructIAMResourceFromConfig } from './utils'; export const upsertResourceAttributesMapping = async ( iamUpsertConfigList: IAMUpsertConfig[], - requestData: Record + requestData: Record, + user: Hapi.UserCredentials | undefined ) => { const { enforcer } = CasbinSingleton; @@ -22,9 +23,12 @@ export const upsertResourceAttributesMapping = async ( requestData ); if (!R.isEmpty(resource) && !R.isEmpty(resourceAttributes)) { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore return enforcer?.upsertResourceGroupingJsonPolicy( resource, - resourceAttributes + resourceAttributes, + { created_by: JSON.parse(JSON.stringify(user)) } ); } @@ -82,7 +86,7 @@ const manageResourceAttributesMapping = async ( h: Hapi.ResponseToolkit ) => { const route = server.match(request.method, request.path); - + const { user } = request.auth.credentials; const shouldUpsertResourceAttributes = checkIfShouldUpsertResourceAttributes( route, request @@ -93,7 +97,11 @@ const manageResourceAttributesMapping = async ( const requestData = await getRequestData(request); - await upsertResourceAttributesMapping(iamUpsertConfigList, requestData); + await upsertResourceAttributesMapping( + iamUpsertConfigList, + requestData, + user + ); if (!R.isEmpty(requestData.response)) { return h.response(requestData.response); } diff --git a/subscriber/model.ts b/subscriber/model.ts index d6e8fbb07..09ce87527 100644 --- a/subscriber/model.ts +++ b/subscriber/model.ts @@ -2,150 +2,37 @@ import { EntitySubscriberInterface, EventSubscriber, InsertEvent, - RemoveEvent, UpdateEvent } from 'typeorm'; -import { delta } from '../utils/deep-diff'; +import { + log as ActivityLog, + actions as ActivityActions +} from '../app/activity/resource'; import Constants from '../utils/constant'; -import { create } from '../app/activity/resource'; -import { getPolicies, setPolicies } from '../app/policy/resource'; - -interface Policy { - [key: string]: any; -} - -const excludeFields = ['createdAt', 'updatedAt']; -const actions = { - CREATE: 'create', - EDIT: 'edit', - DELETE: 'delete' -}; - -const getTitle = (event: any, type: string) => { - let title = ''; - switch (type) { - case actions.CREATE: - if (event.metadata.tableName === Constants.MODEL.Group) { - title = `Created ${event.entity?.displayname} Team `; - } else if (event.metadata.tableName === Constants.MODEL.CasbinRule) { - title = `Created ${event.entity?.ptype} Casbin Rule `; - } - break; - case actions.EDIT: - if (event.metadata.tableName === Constants.MODEL.Group) { - title = `Edited ${event.entity?.displayname}`; - } else if (event.metadata.tableName === Constants.MODEL.CasbinRule) { - title = `Edited ${event.entity?.ptype} Casbin Rule `; - } - break; - case actions.DELETE: - if (event.metadata.tableName === Constants.MODEL.Group) { - title = `Deleted ${event.entity?.displayname} Team`; - } else if (event.metadata.tableName === Constants.MODEL.CasbinRule) { - title = `Deleted ${event.databaseEntity?.ptype} Casbin Rule `; - } - break; - default: - title = ''; - } - - return title; -}; - -const storeActivityPayload = async (event: any, type: string) => { - if ( - event.metadata.tableName === Constants.MODEL.Activity || - event.metadata.tableName === Constants.MODEL.Role || - event.metadata.tableName === Constants.MODEL.User - ) { - return; - } - let promise = null; - const title = getTitle(event, type); - switch (type) { - case actions.CREATE: - promise = create({ - document: {}, - title, - documentId: '0', - model: event.metadata.tableName, - diffs: delta({}, event.entity || {}, { - exclude: excludeFields - }), - createdBy: event.queryRunner.data.user - }); - break; - case actions.EDIT: - promise = create({ - document: event.databaseEntity, - title, - documentId: event.databaseEntity.id, - model: event.metadata.tableName, - diffs: delta(event.databaseEntity || {}, event.entity || {}, { - exclude: excludeFields - }), - createdBy: event.queryRunner.data.user - }); - break; - case actions.DELETE: - promise = create({ - document: event.databaseEntity, - title, - documentId: event.databaseEntity.id, - model: event.metadata.tableName, - diffs: delta(event.databaseEntity || {}, event.entity || {}, { - exclude: excludeFields - }), - createdBy: event.queryRunner.data.user - }); - break; - default: - promise = Promise.resolve(); - } - await promise; -}; @EventSubscriber() export class ModelSubscriber implements EntitySubscriberInterface { afterInsert = async (event: InsertEvent) => { - await storeActivityPayload(event, actions.CREATE); + if ( + event.metadata.tableName === Constants.MODEL.Activity || + event.metadata.tableName === Constants.MODEL.Role || + event.metadata.tableName === Constants.MODEL.User || + event.metadata.tableName === Constants.MODEL.CasbinRule + ) { + return; + } + await ActivityLog(event, ActivityActions.CREATE); }; afterUpdate = async (event: UpdateEvent) => { - await storeActivityPayload(event, actions.EDIT); - }; - - beforeRemove = async (event: RemoveEvent) => { - const policies = await event.queryRunner.query('select * from casbin_rule'); - setPolicies(policies); - }; - - afterRemove = async (event: RemoveEvent) => { - const previousPolicies = getPolicies(); - const currentPolicies = await event.queryRunner.query( - 'select * from casbin_rule' - ); - const currentPoliciesMap: Policy = {}; - currentPolicies.forEach((policy: any) => { - if ( - !Object.prototype.hasOwnProperty.call(currentPoliciesMap, policy.id) - ) { - currentPoliciesMap[policy.id] = policy; - } - }); - await Promise.all( - previousPolicies - .filter((policy: any) => { - return !Object.prototype.hasOwnProperty.call( - currentPoliciesMap, - policy.id - ); - }) - .map((policy: any) => { - // eslint-disable-next-line no-param-reassign - event.databaseEntity = policy; - return storeActivityPayload(event, actions.DELETE); - }) - ); + if ( + event.metadata.tableName === Constants.MODEL.Activity || + event.metadata.tableName === Constants.MODEL.Role || + event.metadata.tableName === Constants.MODEL.User || + event.metadata.tableName === Constants.MODEL.CasbinRule + ) { + return; + } + await ActivityLog(event, ActivityActions.EDIT); }; } From ce0320a968b8fbd0440e98e02c29bb7be4830cac Mon Sep 17 00:00:00 2001 From: Maninder Singh Date: Tue, 9 Mar 2021 11:29:17 +0530 Subject: [PATCH 06/15] feat: delete generic model subscriber, add group subscribe for group related event --- subscriber/group.ts | 26 ++++++++++++++++++++++++++ subscriber/model.ts | 38 -------------------------------------- 2 files changed, 26 insertions(+), 38 deletions(-) create mode 100644 subscriber/group.ts delete mode 100644 subscriber/model.ts diff --git a/subscriber/group.ts b/subscriber/group.ts new file mode 100644 index 000000000..758a07855 --- /dev/null +++ b/subscriber/group.ts @@ -0,0 +1,26 @@ +import { + EntitySubscriberInterface, + EventSubscriber, + InsertEvent, + UpdateEvent +} from 'typeorm'; +import { + log as ActivityLog, + actions as ActivityActions +} from '../app/activity/resource'; +import { Group } from '../model/group'; + +@EventSubscriber() +export class GroupSubscriber implements EntitySubscriberInterface { + listenTo = () => { + return Group; + }; + + afterInsert = async (event: InsertEvent) => { + await ActivityLog(event, ActivityActions.CREATE); + }; + + afterUpdate = async (event: UpdateEvent) => { + await ActivityLog(event, ActivityActions.EDIT); + }; +} diff --git a/subscriber/model.ts b/subscriber/model.ts deleted file mode 100644 index 09ce87527..000000000 --- a/subscriber/model.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { - EntitySubscriberInterface, - EventSubscriber, - InsertEvent, - UpdateEvent -} from 'typeorm'; -import { - log as ActivityLog, - actions as ActivityActions -} from '../app/activity/resource'; -import Constants from '../utils/constant'; - -@EventSubscriber() -export class ModelSubscriber implements EntitySubscriberInterface { - afterInsert = async (event: InsertEvent) => { - if ( - event.metadata.tableName === Constants.MODEL.Activity || - event.metadata.tableName === Constants.MODEL.Role || - event.metadata.tableName === Constants.MODEL.User || - event.metadata.tableName === Constants.MODEL.CasbinRule - ) { - return; - } - await ActivityLog(event, ActivityActions.CREATE); - }; - - afterUpdate = async (event: UpdateEvent) => { - if ( - event.metadata.tableName === Constants.MODEL.Activity || - event.metadata.tableName === Constants.MODEL.Role || - event.metadata.tableName === Constants.MODEL.User || - event.metadata.tableName === Constants.MODEL.CasbinRule - ) { - return; - } - await ActivityLog(event, ActivityActions.EDIT); - }; -} From 45669b3465ca6d4c5438621dba70c602c59460a0 Mon Sep 17 00:00:00 2001 From: Maninder Singh Date: Tue, 9 Mar 2021 11:36:16 +0530 Subject: [PATCH 07/15] feat: remove unused methods --- app/policy/resource.ts | 9 --------- 1 file changed, 9 deletions(-) diff --git a/app/policy/resource.ts b/app/policy/resource.ts index 839ecd6c3..5baded181 100644 --- a/app/policy/resource.ts +++ b/app/policy/resource.ts @@ -16,7 +16,6 @@ export interface PolicyOperation { resource: JSObj; action: JSObj; } -let casbinPolicies: any[] = []; export const bulkOperation = async ( policyOperations: PolicyOperation[] = [], @@ -220,11 +219,3 @@ export const getUsersOfGroupWithPolicies = async ( return parsePoliciesWithSubject(rawResult, 'user'); }; - -export const setPolicies = (policies: [] = []) => { - casbinPolicies = policies; -}; - -export const getPolicies = () => { - return casbinPolicies; -}; From 50b122dedcb45030b232464d1cf0186c6e2dee0b Mon Sep 17 00:00:00 2001 From: Maninder Singh Date: Tue, 9 Mar 2021 14:49:13 +0530 Subject: [PATCH 08/15] feat: pass logged in user into group user api --- app/group/user/handler.ts | 9 +++++++-- app/group/user/resource.ts | 9 +++++++-- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/app/group/user/handler.ts b/app/group/user/handler.ts index ddc0c1087..865e5ac3a 100644 --- a/app/group/user/handler.ts +++ b/app/group/user/handler.ts @@ -67,7 +67,8 @@ export const remove = { app: iamConfig('iam.delete'), handler: async (request: Hapi.Request) => { const { groupId, userId } = request.params; - return Resource.remove(groupId, userId); + const { id: loggedInUserId } = request.auth.credentials; + return Resource.remove(groupId, userId, loggedInUserId); } }; @@ -77,6 +78,10 @@ export const removeSelf = { handler: async (request: Hapi.Request) => { const { groupId } = request.params; const { id: loggedInUserId } = request.auth.credentials; - return Resource.remove(groupId, loggedInUserId); + return Resource.remove( + groupId, + loggedInUserId, + loggedInUserId + ); } }; diff --git a/app/group/user/resource.ts b/app/group/user/resource.ts index 38d60aab1..46ddb1432 100644 --- a/app/group/user/resource.ts +++ b/app/group/user/resource.ts @@ -52,7 +52,11 @@ export const update = async ( return await bulkOperation(policies, subject); }; -export const remove = async (groupId: string, userId: string) => { +export const remove = async ( + groupId: string, + userId: string, + loggedInUserId: string +) => { const userObj = { user: userId }; const groupObj = { group: groupId }; const user = await User.findOne({ @@ -76,7 +80,8 @@ export const remove = async (groupId: string, userId: string) => { await CasbinSingleton.enforcer?.removeJsonPolicy( policy.subject, policy.resource, - policy.action + policy.action, + options ); }); await Promise.all(promiseList); From a740b509db6df19ef3f90718749c9ad733259349 Mon Sep 17 00:00:00 2001 From: Maninder Singh Date: Tue, 9 Mar 2021 19:00:54 +0530 Subject: [PATCH 09/15] fix broken test cases --- lib/casbin/JsonFilteredEnforcer.ts | 2 +- test/app/group/resource.ts | 55 ++++++++++--- test/app/group/user/handler.ts | 8 +- test/app/group/user/resource.ts | 20 +++-- test/app/user/group/resource.ts | 34 +++++--- test/app/user/resource.ts | 66 ++++++++++------ test/lib/casbin/sample.ts | 123 +++++++++++++++++++++-------- test/lib/casbin/scenarios.ts | 11 ++- test/plugin/iam/responseHooks.ts | 10 ++- 9 files changed, 235 insertions(+), 94 deletions(-) diff --git a/lib/casbin/JsonFilteredEnforcer.ts b/lib/casbin/JsonFilteredEnforcer.ts index 3cc00f6ea..736452984 100644 --- a/lib/casbin/JsonFilteredEnforcer.ts +++ b/lib/casbin/JsonFilteredEnforcer.ts @@ -409,7 +409,7 @@ export class JsonFilteredEnforcer implements IEnforcer { await sendLog( diff(previousPolicies, currentPolicies), ActivityActions.CREATE, - options.created_by + options?.created_by ); } } diff --git a/test/app/group/resource.ts b/test/app/group/resource.ts index 08aafe1ef..febe834ea 100644 --- a/test/app/group/resource.ts +++ b/test/app/group/resource.ts @@ -118,9 +118,19 @@ lab.experiment('Group::resource', () => { ...payload }); - const loggedInUserId = Faker.random.uuid(); + const user = await factory(User)().create(); + const loggedInUserId = user.id; + + Sandbox.stub(User, 'findOne').resolves(user); + const response = await Resource.create(payload, loggedInUserId); + Sandbox.assert.calledWithExactly( + checkSubjectHasAccessToCreateAttributesMappingStub, + { user: loggedInUserId }, + payload.attributes + ); + Sandbox.assert.calledWithExactly( checkSubjectHasAccessToCreateAttributesMappingStub, { user: loggedInUserId }, @@ -129,11 +139,13 @@ lab.experiment('Group::resource', () => { Sandbox.assert.calledWithExactly( upsertGroupAndAttributesMappingStub, groupId, - payload.attributes + payload.attributes, + user ); Sandbox.assert.calledWithExactly( groupSaveStub, - R.omit(['attributes', 'policies'], payload) + R.omit(['attributes', 'policies'], payload), + { data: { user } } ); Sandbox.assert.calledWithExactly( bulkUpsertPoliciesForGroupStub, @@ -142,6 +154,7 @@ lab.experiment('Group::resource', () => { loggedInUserId ); Sandbox.assert.calledWithExactly(getStub, groupId, loggedInUserId); + Code.expect(response).to.equal({ id: groupId, ...payload, @@ -191,9 +204,11 @@ lab.experiment('Group::resource', () => { id: groupId, ...payload }; + const user = await factory(User)().create(); const getStub = Sandbox.stub(Resource, 'get').returns(group); + Sandbox.stub(User, 'findOne').returns(user); - const loggedInUserId = Faker.random.uuid(); + const loggedInUserId = user.id; const response = await Resource.update(groupId, payload, loggedInUserId); Sandbox.assert.calledWithExactly( @@ -204,7 +219,8 @@ lab.experiment('Group::resource', () => { ); Sandbox.assert.calledWithExactly( groupSaveStub, - R.omit(['attributes', 'policies'], { ...payload, id: groupId }) + R.omit(['attributes', 'policies'], { ...payload, id: groupId }), + { data: { user } } ); Sandbox.assert.calledWithExactly( bulkUpsertPoliciesForGroupStub, @@ -223,7 +239,7 @@ lab.experiment('Group::resource', () => { }); lab.experiment('get list of groups', () => { - let groups, users, currentUser; + let groups: any, users: any, currentUser: any; const sortByDisplayName = R.sortBy(R.propOr(null, 'displayname')); const removeExtraKeys = R.map(R.omit(['createdAt', 'updatedAt'])); @@ -243,7 +259,7 @@ lab.experiment('Group::resource', () => { sortMemberPolicies ); - const getMemberPolicy = (user, group, role) => { + const getMemberPolicy = (user: any, group: any, role: any) => { return { subject: { user: user.id }, resource: { group: group.id }, @@ -264,18 +280,22 @@ lab.experiment('Group::resource', () => { const getId = R.path(['id']); - const makeUserTeamAdmin = async (user, group) => { + const makeUserTeamAdmin = async (user: any, group: any) => { const groupId = getId(group); const userId = getId(user); + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore await enforcer?.addSubjectGroupingJsonPolicy( { user: userId }, - { group: groupId } + { group: groupId }, + { created_by: user } ); await enforcer?.addJsonPolicy( { user: userId }, { group: groupId }, - { role: 'team.admin' } + { role: 'team.admin' }, + { created_by: user } ); }; @@ -283,14 +303,20 @@ lab.experiment('Group::resource', () => { await Promise.all( R.take(3, groups).map(async (group) => { const groupId = getId(group); + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore await enforcer?.addResourceGroupingJsonPolicy( { group: groupId }, - { entity: 'gojek' } + { entity: 'gojek' }, + { created_by: currentUser } ); + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore await enforcer?.addSubjectGroupingJsonPolicy( { user: getId(currentUser) }, - { group: groupId } + { group: groupId }, + { created_by: currentUser } ); }) ); @@ -298,9 +324,12 @@ lab.experiment('Group::resource', () => { // map 2 groups with gofin await Promise.all( R.takeLast(2, groups).map(async (group) => { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore await enforcer?.addResourceGroupingJsonPolicy( { group: getId(group) }, - { entity: 'gofin' } + { entity: 'gofin' }, + { created_by: currentUser } ); }) ); diff --git a/test/app/group/user/handler.ts b/test/app/group/user/handler.ts index 2bfabe0f1..0be50e5c7 100644 --- a/test/app/group/user/handler.ts +++ b/test/app/group/user/handler.ts @@ -200,7 +200,12 @@ lab.experiment('Group:User::Handler', () => { const response = await server.inject(request); - Sandbox.assert.calledWithExactly(removeStub, GROUP_ID, USER_ID); + Sandbox.assert.calledWithExactly( + removeStub, + GROUP_ID, + USER_ID, + TEST_AUTH.credentials.id + ); Code.expect(response.result).to.equal(expectedResult); Code.expect(response.statusCode).to.equal(200); }); @@ -233,6 +238,7 @@ lab.experiment('Group:User::Handler', () => { Sandbox.assert.calledWithExactly( removeStub, GROUP_ID, + TEST_AUTH.credentials.id, TEST_AUTH.credentials.id ); Code.expect(response.result).to.equal(expectedResult); diff --git a/test/app/group/user/resource.ts b/test/app/group/user/resource.ts index d287ffc14..7245add2a 100644 --- a/test/app/group/user/resource.ts +++ b/test/app/group/user/resource.ts @@ -1,6 +1,7 @@ import Lab from '@hapi/lab'; import Sinon from 'sinon'; import Code from 'code'; +import { factory } from 'typeorm-seeding'; import { lab } from '../../../setup'; import * as Resource from '../../../../app/group/user/resource'; import CasbinSingleton from '../../../../lib/casbin'; @@ -87,14 +88,15 @@ lab.experiment('Group:User:Mapping::resource', () => { addSubjectGroupingJsonPolicy: addSubjectGroupingJsonPolicyStub }); + const user = await factory(User)().create(); const groupId = 'test_group'; - const userId = 'test_user'; - const loggedInUserId = 'test_logged_in_user'; + const userId = user.id; + const loggedInUserId = user.id; const payload = { policies: [ { operation: 'create', - subject: { user: 'test_user' }, + subject: { user: user.id }, resource: { group: 'test_group', entity: 'gojek', @@ -212,6 +214,7 @@ lab.experiment('Group:User:Mapping::resource', () => { } ] }; + const user = await factory(User)().create(); const removeSubjectGroupingJsonPolicyStub = Sandbox.stub(); const removeJsonPolicyStub = Sandbox.stub(); Sandbox.stub(CasbinSingleton, 'enforcer').value({ @@ -224,7 +227,7 @@ lab.experiment('Group:User:Mapping::resource', () => { 'getPoliciesBySubject' ).returns(payload.policies); - const response = await Resource.remove(groupId, userId); + const response = await Resource.remove(groupId, userId, user.id); Sandbox.assert.calledWithExactly( getPoliciesBySubjectStub, @@ -234,7 +237,8 @@ lab.experiment('Group:User:Mapping::resource', () => { Sandbox.assert.calledWithExactly( removeSubjectGroupingJsonPolicyStub, { user: userId }, - { group: groupId } + { group: groupId }, + { created_by: user } ); Sandbox.assert.callCount(removeJsonPolicyStub, payload.policies.length); Code.expect(response).to.equal(true); @@ -249,6 +253,7 @@ lab.experiment('Group:User:Mapping::resource', () => { const payload: any = { policies: [] }; + const user = await factory(User)().create(); const removeSubjectGroupingJsonPolicyStub = Sandbox.stub(); const removeJsonPolicyStub = Sandbox.stub(); Sandbox.stub(CasbinSingleton, 'enforcer').value({ @@ -261,7 +266,7 @@ lab.experiment('Group:User:Mapping::resource', () => { 'getPoliciesBySubject' ).returns(payload.policies); - const response = await Resource.remove(groupId, userId); + const response = await Resource.remove(groupId, userId, user.id); Sandbox.assert.calledWithExactly( getPoliciesBySubjectStub, @@ -271,7 +276,8 @@ lab.experiment('Group:User:Mapping::resource', () => { Sandbox.assert.calledWithExactly( removeSubjectGroupingJsonPolicyStub, { user: userId }, - { group: groupId } + { group: groupId }, + { created_by: user } ); Sandbox.assert.notCalled(removeJsonPolicyStub); Code.expect(response).to.equal(true); diff --git a/test/app/user/group/resource.ts b/test/app/user/group/resource.ts index f7fd181a6..a6a65d178 100644 --- a/test/app/user/group/resource.ts +++ b/test/app/user/group/resource.ts @@ -23,7 +23,7 @@ lab.afterEach(() => { lab.experiment('UserGroup::resource', () => { lab.experiment('get groups of a user', () => { - let users, groups, enforcer; + let users: any, groups: any, enforcer: any; lab.before(async () => { const dbUri = Config.get('/postgres').uri; @@ -33,52 +33,62 @@ lab.experiment('UserGroup::resource', () => { lab.beforeEach(async () => { users = await factory(User)().createMany(2); groups = await factory(Group)().createMany(3); - + const user = users[0]; await enforcer.addResourceGroupingJsonPolicy( { group: groups[0].id }, - { entity: 'gojek' } + { entity: 'gojek' }, + { created_by: user } ); await enforcer.addResourceGroupingJsonPolicy( { group: groups[1].id }, - { entity: 'gojek' } + { entity: 'gojek' }, + { created_by: user } ); await enforcer.addResourceGroupingJsonPolicy( { group: groups[2].id }, - { entity: 'gofin' } + { entity: 'gofin' }, + { created_by: user } ); await enforcer.addSubjectGroupingJsonPolicy( { user: users[0].id }, - { group: groups[0].id } + { group: groups[0].id }, + { created_by: user } ); await enforcer.addSubjectGroupingJsonPolicy( { user: users[0].id }, - { group: groups[1].id } + { group: groups[1].id }, + { created_by: user } ); await enforcer.addSubjectGroupingJsonPolicy( { user: users[1].id }, - { group: groups[0].id } + { group: groups[0].id }, + { created_by: user } ); await enforcer.addActionGroupingJsonPolicy( { action: '*' }, - { role: 'entity.admin' } + { role: 'entity.admin' }, + { created_by: user } ); await enforcer.addJsonPolicy( { user: users[1].id }, { entity: 'gofin' }, - { role: 'entity.admin' } + { role: 'entity.admin' }, + { created_by: user } ); await enforcer.addJsonPolicy( { user: users[0].id }, { group: groups[0].id }, - { role: 'team.admin' } + { role: 'team.admin' }, + { created_by: user } ); await enforcer.addJsonPolicy( { user: users[1].id }, { group: groups[0].id }, - { role: 'team.admin' } + { role: 'team.admin' }, + { created_by: user } ); }); diff --git a/test/app/user/resource.ts b/test/app/user/resource.ts index 21b788dc9..2db96ad5c 100644 --- a/test/app/user/resource.ts +++ b/test/app/user/resource.ts @@ -37,7 +37,7 @@ lab.experiment('User::resource', () => { }); lab.experiment('list users', () => { - let users, groups, userEntityPolicy; + let users: any, groups, userEntityPolicy: any; lab.beforeEach(async () => { // setup data @@ -46,58 +46,80 @@ lab.experiment('User::resource', () => { users = await factory(User)().createMany(5); groups = await factory(Group)().createMany(2); + const user = users[0]; // user group mapping + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore await enforcer.addSubjectGroupingJsonPolicy( { user: users[0].id }, - { group: groups[0].id } + { group: groups[0].id }, + { created_by: user } ); + + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore await enforcer.addSubjectGroupingJsonPolicy( { user: users[1].id }, - { group: groups[0].id } + { group: groups[0].id }, + { created_by: user } ); + + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore await enforcer.addSubjectGroupingJsonPolicy( { user: users[3].id }, - { group: groups[1].id } + { group: groups[1].id }, + { created_by: user } ); - await enforcer.addSubjectGroupingJsonPolicy( + + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + await enforcer?.addSubjectGroupingJsonPolicy( { user: users[2].id }, - { group: groups[1].id } + { group: groups[1].id }, + { created_by: user } ); // create relavant policies - await enforcer.addJsonPolicy( + await enforcer?.addJsonPolicy( { group: groups[0].id }, { entity: 'gojek', privacy: 'public' }, - { action: 'firehose.read' } + { action: 'firehose.read' }, + { created_by: user } ); userEntityPolicy = { subject: { user: users[2].id }, resource: { entity: 'gojek' }, action: { action: 'firehose.read' } }; - await enforcer.addJsonPolicy( + await enforcer?.addJsonPolicy( userEntityPolicy.subject, userEntityPolicy.resource, - userEntityPolicy.action + userEntityPolicy.action, + { created_by: user } ); - await enforcer.addJsonPolicy( + await enforcer?.addJsonPolicy( { user: users[2].id }, { group: groups[1].id }, - { role: 'team.admin' } + { role: 'team.admin' }, + { created_by: user } ); }); - lab.test('should return all users if no filters are specifed', async () => { - const getListWithFiltersStub = Sandbox.stub( - Resource, - 'getListWithFilters' - ).returns([]); - - const result = await Resource.list(); - Code.expect(result).to.equal(users); - Sandbox.assert.notCalled(getListWithFiltersStub); - }); + lab.test( + 'should return all users if no filters are specified', + async () => { + const getListWithFiltersStub = Sandbox.stub( + Resource, + 'getListWithFilters' + ).returns(users); + + const result = await Resource.list(); + Code.expect(result).to.equal(users); + Sandbox.assert.notCalled(getListWithFiltersStub); + } + ); lab.test('should return users that match the filter', async () => { const removeTimestamps = R.omit(['createdAt', 'updatedAt']); diff --git a/test/lib/casbin/sample.ts b/test/lib/casbin/sample.ts index 77380b1c5..612488bd7 100644 --- a/test/lib/casbin/sample.ts +++ b/test/lib/casbin/sample.ts @@ -1,6 +1,15 @@ +import { factory } from 'typeorm-seeding'; import CasbinSingleton from '../../../lib/casbin'; +import { User } from '../../../model/user'; +const users: any[] = []; +const createUser = async () => { + const user = await factory(User)().create(); + users.push(user); + return user; +}; const setupPolicies = async () => { + const user = await createUser(); await CasbinSingleton.enforcer.addJsonPolicy( { user: 'alice' }, { @@ -9,7 +18,8 @@ const setupPolicies = async () => { environment: 'production', team: 'transport' }, - { role: 'resource.manager' } + { role: 'resource.manager' }, + { created_by: user } ); await CasbinSingleton.enforcer.addJsonPolicy( @@ -17,7 +27,8 @@ const setupPolicies = async () => { { entity: 'gojek' }, - { role: 'dwh.manager' } + { role: 'dwh.manager' }, + { created_by: user } ); await CasbinSingleton.enforcer.addJsonPolicy( @@ -28,7 +39,8 @@ const setupPolicies = async () => { environment: 'production', team: 'augur' }, - { role: 'resource.manager' } + { role: 'resource.manager' }, + { created_by: user } ); await CasbinSingleton.enforcer.addJsonPolicy( @@ -36,7 +48,8 @@ const setupPolicies = async () => { { team: 'transport' }, - { role: 'team.admin' } + { role: 'team.admin' }, + { created_by: user } ); await CasbinSingleton.enforcer.addJsonPolicy( @@ -44,7 +57,8 @@ const setupPolicies = async () => { { entity: 'gojek' }, - { role: 'entity.admin' } + { role: 'entity.admin' }, + { created_by: user } ); await CasbinSingleton.enforcer.addJsonPolicy( @@ -52,7 +66,8 @@ const setupPolicies = async () => { { team: 'transport' }, - { role: 'resource.viewer' } + { role: 'resource.viewer' }, + { created_by: user } ); await CasbinSingleton.enforcer.addJsonPolicy( @@ -60,7 +75,8 @@ const setupPolicies = async () => { { team: 'augur' }, - { role: 'resource.viewer' } + { role: 'resource.viewer' }, + { created_by: user } ); await CasbinSingleton.enforcer.addJsonPolicy( @@ -68,7 +84,8 @@ const setupPolicies = async () => { { team: 'gofinance' }, - { role: 'resource.viewer' } + { role: 'resource.viewer' }, + { created_by: user } ); await CasbinSingleton.enforcer.addJsonPolicy( @@ -77,7 +94,8 @@ const setupPolicies = async () => { entity: 'gojek', privacy: 'public' }, - { role: 'resource.viewer' } + { role: 'resource.viewer' }, + { created_by: user } ); await CasbinSingleton.enforcer.addJsonPolicy( @@ -86,7 +104,8 @@ const setupPolicies = async () => { entity: 'gojek', privacy: 'public' }, - { role: 'resource.viewer' } + { role: 'resource.viewer' }, + { created_by: user } ); await CasbinSingleton.enforcer.addJsonPolicy( @@ -95,44 +114,56 @@ const setupPolicies = async () => { entity: 'gofin', privacy: 'public' }, - { role: 'resource.viewer' } + { role: 'resource.viewer' }, + { created_by: user } ); await CasbinSingleton.enforcer.addStrPolicy( JSON.stringify({ team: 'de' }), '*', - JSON.stringify({ role: 'super.admin' }) + JSON.stringify({ role: 'super.admin' }), + { created_by: user } ); }; const setupUserTeamMapping = async () => { + const user = await createUser(); + await CasbinSingleton.enforcer.addSubjectGroupingJsonPolicy( { user: 'alice' }, - { team: 'transport' } + { team: 'transport' }, + { created_by: user } ); await CasbinSingleton.enforcer.addSubjectGroupingJsonPolicy( { user: 'bob' }, - { team: 'transport' } + { team: 'transport' }, + { created_by: user } ); await CasbinSingleton.enforcer.addSubjectGroupingJsonPolicy( { user: 'dave' }, - { team: 'augur' } + { team: 'augur' }, + { created_by: user } ); await CasbinSingleton.enforcer.addSubjectGroupingJsonPolicy( { user: 'frank' }, - { team: 'augur' } + { team: 'augur' }, + { created_by: user } ); await CasbinSingleton.enforcer.addSubjectGroupingJsonPolicy( { user: 'ele' }, - { team: 'gofinance' } + { team: 'gofinance' }, + { created_by: user } ); await CasbinSingleton.enforcer.addSubjectGroupingJsonPolicy( { user: 'gary' }, - { team: 'de' } + { team: 'de' }, + { created_by: user } ); }; const setupResourceProjectMapping = async () => { + const user = await createUser(); + await CasbinSingleton.enforcer.addResourceGroupingJsonPolicy( { resource: 'p-gojek-id-firehose-transport-123' @@ -143,7 +174,8 @@ const setupResourceProjectMapping = async () => { landscape: 'id', team: 'transport', privacy: 'public' - } + }, + { created_by: user } ); await CasbinSingleton.enforcer.addResourceGroupingJsonPolicy( @@ -156,7 +188,8 @@ const setupResourceProjectMapping = async () => { landscape: 'id', team: 'augur', privacy: 'public' - } + }, + { created_by: user } ); await CasbinSingleton.enforcer.addResourceGroupingJsonPolicy( @@ -169,7 +202,8 @@ const setupResourceProjectMapping = async () => { landscape: 'id', team: 'augur', privacy: 'private' - } + }, + { created_by: user } ); await CasbinSingleton.enforcer.addResourceGroupingJsonPolicy( @@ -181,7 +215,8 @@ const setupResourceProjectMapping = async () => { environment: 'production', landscape: 'id', privacy: 'public' - } + }, + { created_by: user } ); await CasbinSingleton.enforcer.addResourceGroupingJsonPolicy( @@ -194,18 +229,21 @@ const setupResourceProjectMapping = async () => { landscape: 'id', privacy: 'public', team: 'gofinance' - } + }, + { created_by: user } ); }; const setupTeamEntityProjectMapping = async () => { + const user = await createUser(); await CasbinSingleton.enforcer.addResourceGroupingJsonPolicy( { team: 'augur' }, { entity: 'gojek' - } + }, + { created_by: user } ); await CasbinSingleton.enforcer.addResourceGroupingJsonPolicy( @@ -214,7 +252,8 @@ const setupTeamEntityProjectMapping = async () => { }, { entity: 'gojek' - } + }, + { created_by: user } ); await CasbinSingleton.enforcer.addResourceGroupingJsonPolicy( @@ -223,27 +262,33 @@ const setupTeamEntityProjectMapping = async () => { }, { entity: 'gofin' - } + }, + { created_by: user } ); }; const setupPermissionRoleMapping = async () => { + const user = await createUser(); await CasbinSingleton.enforcer.addActionGroupingJsonPolicy( { action: '*' }, - { role: 'team.admin' } + { role: 'team.admin' }, + { created_by: user } ); await CasbinSingleton.enforcer.addActionGroupingJsonPolicy( { action: '*' }, - { role: 'entity.admin' } + { role: 'entity.admin' }, + { created_by: user } ); await CasbinSingleton.enforcer.addActionGroupingJsonPolicy( { action: '*' }, - { role: 'super.admin' } + { role: 'super.admin' }, + { created_by: user } ); await CasbinSingleton.enforcer.addActionGroupingJsonPolicy( { action: 'firehose.read' }, - { role: 'resource.viewer' } + { role: 'resource.viewer' }, + { created_by: user } ); await CasbinSingleton.enforcer.addActionGroupingJsonPolicy( { action: 'dagger.read' }, @@ -251,25 +296,30 @@ const setupPermissionRoleMapping = async () => { ); await CasbinSingleton.enforcer.addActionGroupingJsonPolicy( { action: 'beast.read' }, - { role: 'resource.viewer' } + { role: 'resource.viewer' }, + { created_by: user } ); await CasbinSingleton.enforcer.addActionGroupingJsonPolicy( { action: 'firehose.write' }, - { role: 'resource.manager' } + { role: 'resource.manager' }, + { created_by: user } ); await CasbinSingleton.enforcer.addActionGroupingJsonPolicy( { action: 'dagger.write' }, - { role: 'resource.manager' } + { role: 'resource.manager' }, + { created_by: user } ); await CasbinSingleton.enforcer.addActionGroupingJsonPolicy( { role: 'resource.viewer' }, - { role: 'resource.manager' } + { role: 'resource.manager' }, + { created_by: user } ); await CasbinSingleton.enforcer.addActionGroupingJsonPolicy( { action: 'beast.*' }, - { role: 'dwh.manager' } + { role: 'dwh.manager' }, + { created_by: user } ); }; @@ -279,4 +329,7 @@ export default async () => { await setupResourceProjectMapping(); await setupTeamEntityProjectMapping(); await setupPermissionRoleMapping(); + return { + users + }; }; diff --git a/test/lib/casbin/scenarios.ts b/test/lib/casbin/scenarios.ts index 70fb65195..02818ffe6 100644 --- a/test/lib/casbin/scenarios.ts +++ b/test/lib/casbin/scenarios.ts @@ -1,16 +1,20 @@ import Code from 'code'; import Lab from '@hapi/lab'; +import { factory } from 'typeorm-seeding'; import * as Config from '../../../config/config'; import setupSampleData from './sample'; import CasbinSingleton from '../../../lib/casbin'; import { lab } from '../../setup'; import connection from '../../connection'; +import { User } from '../../../model/user'; exports.lab = Lab.script(); const testScenarios = () => { + let users: any[]; lab.beforeEach(async () => { - await setupSampleData(); + const response = await setupSampleData(); + users = response.users; }); lab.test( @@ -34,6 +38,11 @@ const testScenarios = () => { const obj = { resource: 'p-gojek-id-firehose-transport-123' }; + const user = await factory(User)().create(); + user.id = 'alice'; + user.username = 'alice'; + user.displayname = 'alice'; + const options = { created_by: user }; const act = { action: 'firehose.write' }; const res = await context.enforcer.enforceJson(sub, obj, act); Code.expect(res).to.equal(true); diff --git a/test/plugin/iam/responseHooks.ts b/test/plugin/iam/responseHooks.ts index aa956ebd3..6bbf79cf7 100644 --- a/test/plugin/iam/responseHooks.ts +++ b/test/plugin/iam/responseHooks.ts @@ -177,7 +177,7 @@ lab.experiment('ResponseHooks::upsertResourceAttributesMapping', () => { } }; - await upsertResourceAttributesMapping(iamUpsertConfig, requestData); + await upsertResourceAttributesMapping(iamUpsertConfig, requestData, {}); Sandbox.assert.called(constructIAMResourceFromConfigStub); // TODO: Check why this is not stubbing @@ -187,7 +187,13 @@ lab.experiment('ResponseHooks::upsertResourceAttributesMapping', () => { }); lab.experiment('ResponseHooks::mergeResourceListWithAttributes', () => { - let getResourceAttributeMappingsByResourcesStub; + let getResourceAttributeMappingsByResourcesStub: + | Sinon.SinonStub< + [resources?: Record[] | undefined], + Promise<{ resource: any; attributes: any }[]> + > + | Sinon.SinonSpy<[{ name: string }[]], any> + | Sinon.SinonSpyCall<[{ name: string }[]], any>; const hook = { resources: [{ name: { key: 'name', type: 'response' } }], attributes: [ From cdd9538237fc33f1be4e9a8b5db94e597909c97e Mon Sep 17 00:00:00 2001 From: Maninder Singh Date: Wed, 10 Mar 2021 10:21:55 +0530 Subject: [PATCH 10/15] update activity model details in schema defination --- app/activity/handler.ts | 6 +++--- app/activity/resource.ts | 8 ++++---- app/activity/schema.ts | 10 ++++++---- 3 files changed, 13 insertions(+), 11 deletions(-) diff --git a/app/activity/handler.ts b/app/activity/handler.ts index 43a041e8b..a568525d9 100644 --- a/app/activity/handler.ts +++ b/app/activity/handler.ts @@ -6,10 +6,10 @@ import * as Resource from './resource'; // activities?{} export const get = { - description: 'get all activities or team activities', + description: 'get all activities or group activities', tags: ['api'], handler: async (request: Hapi.Request) => { - const { team } = request.query; - return Resource.get(team); + const { group } = request.query; + return Resource.get(group); } }; diff --git a/app/activity/resource.ts b/app/activity/resource.ts index 3ab52fa5b..429c2aab6 100644 --- a/app/activity/resource.ts +++ b/app/activity/resource.ts @@ -2,18 +2,18 @@ import { Activity } from '../../model/activity'; import Constants from '../../utils/constant'; import { delta } from '../../utils/deep-diff'; -export const get = async (team = '') => { +export const get = async (group = '') => { let criteria: any = { order: { createdAt: 'DESC' } }; - if (team.length !== 0) { - // fetch activities based on team + if (group.length !== 0) { + // fetch activities based on group criteria = Object.assign(criteria, { where: { - team + model: group } }); } diff --git a/app/activity/schema.ts b/app/activity/schema.ts index 22317e3da..97383a8e8 100644 --- a/app/activity/schema.ts +++ b/app/activity/schema.ts @@ -8,9 +8,11 @@ export const ActivityPayload = Joi.object() .keys({ id: Joi.string().required(), title: Joi.string().required(), - team: Joi.string().required(), - details: Joi.array().items(Joi.object().optional()), - createdAt: Joi.date().iso().required(), - createdBy: Joi.string().required() + model: Joi.string().required(), + documentId: Joi.string().required(), + document: Joi.object().required(), + diffs: Joi.array().items(Joi.object().optional()), + createdBy: Joi.string().required(), + createdAt: Joi.date().iso().required() }) .options(validationOptions); From 09e811dcc63001d9d52872869a48921a40f05a3a Mon Sep 17 00:00:00 2001 From: Maninder Singh Date: Thu, 11 Mar 2021 20:54:10 +0530 Subject: [PATCH 11/15] feat: get all activity log or based on group name --- app/activity/handler.ts | 12 +- app/activity/resource.ts | 229 +++++++++++++++++++++++++++++++++++---- app/group/handler.ts | 10 ++ app/group/index.ts | 5 + 4 files changed, 226 insertions(+), 30 deletions(-) diff --git a/app/activity/handler.ts b/app/activity/handler.ts index a568525d9..24e5f4306 100644 --- a/app/activity/handler.ts +++ b/app/activity/handler.ts @@ -1,15 +1,9 @@ -import Hapi from '@hapi/hapi'; import * as Resource from './resource'; -// groups/{groupId}/activities - -// activities?{} - export const get = { - description: 'get all activities or group activities', + description: 'get all activities', tags: ['api'], - handler: async (request: Hapi.Request) => { - const { group } = request.query; - return Resource.get(group); + handler: async () => { + return Resource.get(); } }; diff --git a/app/activity/resource.ts b/app/activity/resource.ts index 429c2aab6..119ea1734 100644 --- a/app/activity/resource.ts +++ b/app/activity/resource.ts @@ -1,45 +1,226 @@ +import { getManager } from 'typeorm'; import { Activity } from '../../model/activity'; import Constants from '../../utils/constant'; import { delta } from '../../utils/deep-diff'; +import { User } from '../../model/user'; +import { Role } from '../../model/role'; -export const get = async (group = '') => { - let criteria: any = { - order: { - createdAt: 'DESC' - } +type ActivityType = { + id: string; + reason: string; + createdAt: string; + diff: { + created: string[] | undefined; + edited: string[] | undefined; + removed: string[] | undefined; + }; + user: string; +}; + +const excludeFields = ['createdAt', 'updatedAt']; + +export const actions = { + CREATE: 'create', + EDIT: 'edit', + DELETE: 'delete' +}; + +const titleMap = { + ASSIGNED_ROLE: 'Assigned a role', + ASSIGNED_USER: 'Assigned a user', + ADD_ATTRIBUTE_TO_GROUP: 'Added attribute to a team', + REMOVED_ROLE: 'Removed a role', + REMOVED_USER: 'Removed a user', + REMOVED_ATTRIBUTE_FROM_GROUP: 'Removed attribute from a team' +}; + +const activityResponsePayload = (activity: Activity) => { + const activityResponse: ActivityType = { + createdAt: activity.createdAt, + diff: { created: undefined, edited: undefined, removed: undefined }, + id: activity.id, + reason: activity.title, + user: activity.createdBy.username }; + return activityResponse; +}; + +const calcDiff = (input: Record[], key: string) => { + return input.filter((diff) => { + return diff.path[0] === key; + }); +}; + +const relationType = (diffs: Record[]) => { + const relation = { + isRole: false, + isUser: false + }; + const pType = calcDiff(diffs, 'ptype'); + let value = ''; + if (pType.length > 0) { + if (Object.prototype.hasOwnProperty.call(pType[0], 'rhs')) { + value = pType[0].rhs; + } else { + value = pType[0].lhs; + } + + if (value === 'p') { + relation.isRole = true; + } + + if (value === 'g') { + relation.isUser = true; + } + } + return relation; +}; - if (group.length !== 0) { - // fetch activities based on group - criteria = Object.assign(criteria, { +const parseGroupActivity = async (activity: Activity) => { + const output = activityResponsePayload(activity); + const displayName = calcDiff(activity.diffs, 'displayname'); + if (activity.documentId === '0') { + output.diff.created = [displayName[0].rhs]; + } else if ( + Object.prototype.hasOwnProperty.call(displayName[0], 'rhs') && + Object.prototype.hasOwnProperty.call(displayName[0], 'lhs') + ) { + output.diff.edited = [displayName[0].lhs, displayName[0].rhs]; + } else if ( + !Object.prototype.hasOwnProperty.call(displayName[0], 'rhs') && + Object.prototype.hasOwnProperty.call(displayName[0], 'lhs') + ) { + output.diff.removed = [displayName[0].lhs]; + } + return output; +}; + +const parseCasbinActivity = async (activity: Activity) => { + const output = activityResponsePayload(activity); + const relation = relationType(activity.diffs); + if (activity.documentId === '0') { + if (relation.isRole) { + const roleDiff = calcDiff(activity.diffs, 'v2'); + const role = await Role.findOne({ + select: ['displayname'], + where: { + id: JSON.parse(roleDiff[0].rhs).role + } + }); + output.diff.created = [role?.displayname || '']; + } else if (relation.isUser) { + const userDiff = calcDiff(activity.diffs, 'v0'); + const user = await User.findOne({ + select: ['displayname'], + where: { + id: JSON.parse(userDiff[0].rhs).user + } + }); + output.diff.created = [user?.displayname || '']; + } + } else if (relation.isRole) { + const role = await Role.findOne({ + select: ['displayname'], where: { - model: group + id: JSON.parse(activity.document.v2).role } }); + output.diff.removed = [role?.displayname || '', '']; + } else if (relation.isUser) { + const user = await User.findOne({ + select: ['displayname'], + where: { + id: JSON.parse(activity.document.v0).user + } + }); + output.diff.removed = [user?.displayname || '', '']; + } + return output; +}; + +export const get = async (groupId = '') => { + let whereClause = + '( activity.title != :addAttributeToGroup AND activity.title != :removeAttributeFromGroup )'; + const whereParameter = { + addAttributeToGroup: titleMap.ADD_ATTRIBUTE_TO_GROUP, + removeAttributeFromGroup: titleMap.REMOVED_ATTRIBUTE_FROM_GROUP, + createGroup: '', + userRoleGroupMap: '', + groupId: '' + }; + const ActivityRepository = getManager().getRepository(Activity); + if (groupId) { + whereClause += + ' AND ( activity.diffs @> :createGroup OR activity.diffs ::jsonb @> :userRoleGroupMap ) '; + // whereClause += + // ' AND (activity.diffs ::jsonb @> \'[{"rhs": \\:groupId\\}]\' OR\n' + + // ' activity.diffs ::jsonb @> \'[{"rhs": "{\\"group\\": \\:groupId\\}"}]\'\n' + + // ' )'; + // + // whereParameter.groupId = groupId; + // whereClause += + // ' AND (activity.diffs ::jsonb @> \'[{"rhs": "6ead8ac9-2f91-4ae4-a6a8-8e5d16dce53f"}]\' OR\n' + + // ' activity.diffs ::jsonb @> \'[{"rhs": "{\\"group\\":\\"6ead8ac9-2f91-4ae4-a6a8-8e5d16dce53f\\"}"}]\'\n' + + // ' )'; + + // whereClause += + // ' AND (activity.diffs ::jsonb @> :createGroup OR activity.diffs ::jsonb @> :userRoleGroupMap )'; + + whereParameter.createGroup = JSON.stringify([ + { rhs: groupId, kind: 'N', path: ['id'] } + ]); + whereParameter.userRoleGroupMap = JSON.stringify([ + { + rhs: { group: groupId, kind: 'N', path: ['v1'] } + } + ]); } - return Activity.find(criteria); + const activities = await ActivityRepository.createQueryBuilder('activity') + .where(whereClause, whereParameter) + .orderBy('activity.created_at', 'DESC') + .skip(0) + .take(50) + .getMany(); + + return await Promise.all( + activities.map(async (activity) => { + let output: ActivityType = { + createdAt: '', + diff: { created: undefined, edited: undefined, removed: undefined }, + id: '', + reason: '', + user: '' + }; + + if (activity.model === Constants.MODEL.Group) { + output = await parseGroupActivity(activity); + } else if (activity.model === Constants.MODEL.CasbinRule) { + output = await parseCasbinActivity(activity); + } + return output; + }) + ); }; export const create = async (payload: any) => { return await Activity.save({ ...payload }); }; -const excludeFields = ['createdAt', 'updatedAt']; -export const actions = { - CREATE: 'create', - EDIT: 'edit', - DELETE: 'delete' -}; - const getTitle = (event: any, type: string) => { let title = ''; switch (type) { case actions.CREATE: if (event.metadata.tableName === Constants.MODEL.Group) { - title = `Created ${event.entity?.displayname} Team `; + title = `Created ${event.entity?.displayname} team `; } else if (event.metadata.tableName === Constants.MODEL.CasbinRule) { - title = `Created ${event.entity?.ptype} Casbin Rule `; + if (event.entity?.ptype === 'p') { + title = titleMap.ASSIGNED_ROLE; + } else if (event.entity?.ptype === 'g') { + title = titleMap.ASSIGNED_USER; + } else if (event.entity?.ptype === 'g2') { + title = titleMap.ADD_ATTRIBUTE_TO_GROUP; + } } break; case actions.EDIT: @@ -51,9 +232,15 @@ const getTitle = (event: any, type: string) => { break; case actions.DELETE: if (event.metadata.tableName === Constants.MODEL.Group) { - title = `Deleted ${event.entity?.displayname} Team`; + title = `Deleted ${event.entity?.displayname} team`; } else if (event.metadata.tableName === Constants.MODEL.CasbinRule) { - title = `Deleted ${event.databaseEntity?.ptype} Casbin Rule `; + if (event.databaseEntity?.ptype === 'p') { + title = titleMap.REMOVED_ROLE; + } else if (event.databaseEntity?.ptype === 'g') { + title = titleMap.REMOVED_USER; + } else if (event.databaseEntity?.ptype === 'g2') { + title = titleMap.REMOVED_ATTRIBUTE_FROM_GROUP; + } } break; default: diff --git a/app/group/handler.ts b/app/group/handler.ts index 057e3394a..27724db89 100644 --- a/app/group/handler.ts +++ b/app/group/handler.ts @@ -1,6 +1,7 @@ import Hapi from '@hapi/hapi'; import * as Schema from './schema'; import * as Resource from './resource'; +import { get as ActivitiesByGroup } from '../activity/resource'; export const list = { description: 'get list of groups', @@ -51,3 +52,12 @@ export const update = { ); } }; + +export const getActivitiesByGroup = { + description: 'get activities by group', + tags: ['api'], + handler: async (request: Hapi.Request) => { + const { id: groupId } = request.params; + return ActivitiesByGroup(groupId); + } +}; diff --git a/app/group/index.ts b/app/group/index.ts index b03895430..b57288f68 100644 --- a/app/group/index.ts +++ b/app/group/index.ts @@ -27,6 +27,11 @@ export const plugin = { method: 'PUT', path: '/api/groups/{id}', options: Handler.update + }, + { + method: 'GET', + path: '/api/groups/{id}/activities', + options: Handler.getActivitiesByGroup } ]; From 9a39d6686f3d53b8bb5f0de2beb97d09456d6beb Mon Sep 17 00:00:00 2001 From: Maninder Singh Date: Fri, 12 Mar 2021 20:28:51 +0530 Subject: [PATCH 12/15] feat: get activities based on group , test cases for activity handler and resources --- app/activity/resource.ts | 82 +++++---- factory/activity.ts | 7 +- model/activity.ts | 2 +- test/app/activity/handler.ts | 60 ++++++ test/app/activity/resource.ts | 331 ++++++++++++++++++++++++++++++++++ 5 files changed, 443 insertions(+), 39 deletions(-) create mode 100644 test/app/activity/handler.ts create mode 100644 test/app/activity/resource.ts diff --git a/app/activity/resource.ts b/app/activity/resource.ts index 119ea1734..93e10a42b 100644 --- a/app/activity/resource.ts +++ b/app/activity/resource.ts @@ -10,9 +10,7 @@ type ActivityType = { reason: string; createdAt: string; diff: { - created: string[] | undefined; - edited: string[] | undefined; - removed: string[] | undefined; + [key: string]: string[] | undefined; }; user: string; }; @@ -79,19 +77,54 @@ const relationType = (diffs: Record[]) => { const parseGroupActivity = async (activity: Activity) => { const output = activityResponsePayload(activity); const displayName = calcDiff(activity.diffs, 'displayname'); + const metadata = calcDiff(activity.diffs, 'metadata'); + if (activity.documentId === '0') { + // created output.diff.created = [displayName[0].rhs]; - } else if ( - Object.prototype.hasOwnProperty.call(displayName[0], 'rhs') && - Object.prototype.hasOwnProperty.call(displayName[0], 'lhs') - ) { - output.diff.edited = [displayName[0].lhs, displayName[0].rhs]; - } else if ( - !Object.prototype.hasOwnProperty.call(displayName[0], 'rhs') && - Object.prototype.hasOwnProperty.call(displayName[0], 'lhs') - ) { - output.diff.removed = [displayName[0].lhs]; + if (metadata.length > 0) { + Object.keys(metadata[0].rhs).forEach((key) => { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + output.diff[key] = [metadata[0].rhs[key]]; + }); + } + } else { + if (metadata.length > 0) { + metadata.forEach((meta) => { + output.diff[meta.path[1] || ''] = [meta.lhs, meta.rhs]; + }); + } + + if ( + displayName.length > 0 && + Object.prototype.hasOwnProperty.call(displayName[0], 'lhs') + ) { + if (Object.prototype.hasOwnProperty.call(displayName[0], 'rhs')) { + output.diff.edited = [displayName[0].lhs, displayName[0].rhs]; + } else { + output.diff.removed = [displayName[0].lhs]; + } + } } + + // else if ( + // displayName.length > 0 && + // Object.prototype.hasOwnProperty.call(displayName[0], 'lhs') + // ) { + // if (Object.prototype.hasOwnProperty.call(displayName[0], 'rhs')) { + // console.log('metadata => ', metadata); + // if (metadata.length > 0) { + // metadata.forEach((meta) => { + // output.diff[meta.path[1] || ''] = [meta.lhs, meta.rhs]; + // }); + // } + // output.diff.edited = [displayName[0].lhs, displayName[0].rhs]; + // console.log('output edited => ', output); + // } else { + // output.diff.removed = [displayName[0].lhs]; + // } + // } return output; }; @@ -148,30 +181,15 @@ export const get = async (groupId = '') => { userRoleGroupMap: '', groupId: '' }; + const ActivityRepository = getManager().getRepository(Activity); if (groupId) { whereClause += - ' AND ( activity.diffs @> :createGroup OR activity.diffs ::jsonb @> :userRoleGroupMap ) '; - // whereClause += - // ' AND (activity.diffs ::jsonb @> \'[{"rhs": \\:groupId\\}]\' OR\n' + - // ' activity.diffs ::jsonb @> \'[{"rhs": "{\\"group\\": \\:groupId\\}"}]\'\n' + - // ' )'; - // - // whereParameter.groupId = groupId; - // whereClause += - // ' AND (activity.diffs ::jsonb @> \'[{"rhs": "6ead8ac9-2f91-4ae4-a6a8-8e5d16dce53f"}]\' OR\n' + - // ' activity.diffs ::jsonb @> \'[{"rhs": "{\\"group\\":\\"6ead8ac9-2f91-4ae4-a6a8-8e5d16dce53f\\"}"}]\'\n' + - // ' )'; - - // whereClause += - // ' AND (activity.diffs ::jsonb @> :createGroup OR activity.diffs ::jsonb @> :userRoleGroupMap )'; - - whereParameter.createGroup = JSON.stringify([ - { rhs: groupId, kind: 'N', path: ['id'] } - ]); + ' AND ( activity.diffs @> :createGroup OR activity.diffs @> :userRoleGroupMap ) '; + whereParameter.createGroup = JSON.stringify([{ rhs: groupId }]); whereParameter.userRoleGroupMap = JSON.stringify([ { - rhs: { group: groupId, kind: 'N', path: ['v1'] } + rhs: JSON.stringify({ group: groupId }) } ]); } diff --git a/factory/activity.ts b/factory/activity.ts index 41b25711d..3a5827835 100644 --- a/factory/activity.ts +++ b/factory/activity.ts @@ -1,7 +1,6 @@ import Faker from 'faker'; import { define } from 'typeorm-seeding'; import { Activity } from '../model/activity'; -// import { User } from '../model/user'; define(Activity, (faker: typeof Faker) => { const activity = new Activity(); @@ -10,10 +9,6 @@ define(Activity, (faker: typeof Faker) => { activity.model = 'User'; activity.document = {}; activity.documentId = faker.random.uuid(); - activity.diffs = [{}]; - // const user = new User(); - // user.id = faker.random.uuid(); - // user.displayname = faker.random.word(); - // activity.createdBy = user; + activity.diffs = []; return activity; }); diff --git a/model/activity.ts b/model/activity.ts index 83ced545f..d87cb95bf 100644 --- a/model/activity.ts +++ b/model/activity.ts @@ -42,7 +42,7 @@ export class Activity extends BaseEntity { type: 'jsonb', nullable: true }) - diffs: Record[]; + diffs: Record[]; @CreateDateColumn() createdAt: string; diff --git a/test/app/activity/handler.ts b/test/app/activity/handler.ts new file mode 100644 index 000000000..6fbfb4159 --- /dev/null +++ b/test/app/activity/handler.ts @@ -0,0 +1,60 @@ +import Code from 'code'; +import Lab from '@hapi/lab'; +import Hapi from '@hapi/hapi'; +import Sinon from 'sinon'; +import { factory } from 'typeorm-seeding'; +import { lab } from '../../setup'; +import * as Config from '../../../config/config'; +import * as activityPlugin from '../../../app/activity'; +import * as Resource from '../../../app/activity/resource'; +import { Activity } from '../../../model/activity'; + +exports.lab = Lab.script(); +let server: Hapi.Server; +const Sandbox = Sinon.createSandbox(); + +const TEST_AUTH = { + strategy: 'test', + credentials: { id: 'dev.test' } +}; + +lab.before(async () => { + const plugins = [activityPlugin]; + server = new Hapi.Server({ port: Config.get('/port/web'), debug: false }); + await server.register(plugins); +}); + +lab.after(async () => { + await server.stop(); +}); + +lab.afterEach(() => { + Sandbox.restore(); +}); + +lab.experiment('Activity::Handler', () => { + lab.experiment('get all activities', () => { + let request: any, getStub: any, activities: any; + + lab.beforeEach(async () => { + activities = await factory(Activity)().createMany(5); + getStub = Sandbox.stub(Resource, 'get'); + request = { + method: 'GET', + url: `/api/activities`, + auth: TEST_AUTH + }; + }); + lab.afterEach(() => { + getStub.restore(); + }); + + lab.test('should get user by id', async () => { + getStub.resolves(activities); + const response = await server.inject(request); + Sandbox.assert.calledWithExactly(getStub); + Code.expect(response.result).to.equal(activities); + Code.expect(response.statusCode).to.equal(200); + }); + }); +}); diff --git a/test/app/activity/resource.ts b/test/app/activity/resource.ts new file mode 100644 index 000000000..d96ce3622 --- /dev/null +++ b/test/app/activity/resource.ts @@ -0,0 +1,331 @@ +import Lab from '@hapi/lab'; +import Sinon from 'sinon'; +import Code from 'code'; +import * as Faker from 'faker'; +import { factory } from 'typeorm-seeding'; +import { lab } from '../../setup'; +import { Activity } from '../../../model/activity'; +import * as Resource from '../../../app/activity/resource'; +import { User } from '../../../model/user'; +import { delta } from '../../../utils/deep-diff'; + +exports.lab = Lab.script(); +const Sandbox = Sinon.createSandbox(); + +lab.afterEach(() => { + Sandbox.restore(); +}); + +lab.experiment('Activity::resource', () => { + lab.experiment('create activity', () => { + lab.afterEach(() => { + Sandbox.restore(); + }); + + lab.test('should create activity', async () => { + const activity = { + createdAt: 'Fri Mar 12 2021 13:24:53 GMT+0530 (India Standard Time)', + diffs: [], + document: {}, + documentId: 'ba256f55-bfce-4d17-8174-a0abbf26ccd4', + model: 'User', + title: 'Bypass Chips heuristic' + }; + const activityId = Faker.random.uuid(); + const activitySaveStub = Sandbox.stub(Activity, 'save').returns({ + id: activityId, + ...activity + }); + const response = await Resource.create(activity); + Sandbox.assert.calledWithExactly(activitySaveStub, activity); + Code.expect(response).to.equal({ id: activityId, ...activity }); + }); + }); + + lab.experiment('log activity', () => { + lab.afterEach(() => { + Sandbox.restore(); + }); + + lab.test( + 'should log activity by typeorm subscriber after insert event', + async () => { + const user = await factory(User)().create(); + const activityId = Faker.random.uuid(); + const event = { + metadata: { + tableName: 'groups' + }, + entity: { + id: activityId, + displayname: 'Mock', + first: 'First', + second: 'Second', + metadata: { + key: 'value', + number: 1, + bool: true + }, + createdAt: 'Fri Mar 12 2021 13:24:53 GMT+0530 (India Standard Time)' + }, + queryRunner: { + data: { + user + } + } + }; + + const activity = { + document: {}, + documentId: '0', + diffs: delta({}, event.entity, { + exclude: ['createdAt', 'updatedAt'] + }), + title: 'Created Mock team ', + createdBy: user + }; + + Sandbox.stub(Activity, 'create').returns({ + id: activityId, + ...activity + }); + await Resource.log(event, Resource.actions.CREATE); + } + ); + + lab.test( + 'should log activity by typeorm subscriber after update event', + async () => { + const user = await factory(User)().create(); + const activityId = Faker.random.uuid(); + const event = { + metadata: { + tableName: 'groups' + }, + databaseEntity: { + id: activityId, + displayname: 'Mock', + first: 'First', + second: 'Second', + metadata: { + key: 'value', + number: 1, + bool: true + }, + createdAt: 'Fri Mar 12 2021 13:24:53 GMT+0530 (India Standard Time)' + }, + entity: { + id: activityId, + displayname: 'Mock', + first: 'First Edited', + second: 'Second', + metadata: { + key: 'value edited', + number: 2, + bool: true + }, + createdAt: 'Fri Mar 12 2021 13:24:53 GMT+0530 (India Standard Time)' + }, + queryRunner: { + data: { + user + } + } + }; + + const activity = { + document: { + id: activityId, + displayname: 'Mock', + first: 'First', + second: 'Second', + metadata: { + key: 'value', + number: 1, + bool: true + }, + createdAt: 'Fri Mar 12 2021 13:24:53 GMT+0530 (India Standard Time)' + }, + documentId: activityId, + diffs: delta(event.databaseEntity, event.entity, { + exclude: ['createdAt', 'updatedAt'] + }), + title: 'Edited Mock team ', + createdBy: user + }; + + Sandbox.stub(Activity, 'create').returns({ + id: activityId, + ...activity + }); + await Resource.log(event, Resource.actions.EDIT); + } + ); + + lab.test( + 'should log activity by typeorm subscriber after remove event', + async () => { + const user = await factory(User)().create(); + const activityId = Faker.random.uuid(); + const event = { + metadata: { + tableName: 'groups' + }, + databaseEntity: { + id: activityId, + displayname: 'Mock', + first: 'First', + second: 'Second', + metadata: { + key: 'value', + number: 1, + bool: true + }, + createdAt: 'Fri Mar 12 2021 13:24:53 GMT+0530 (India Standard Time)' + }, + entity: {}, + queryRunner: { + data: { + user + } + } + }; + + const activity = { + document: { + id: activityId, + displayname: 'Mock', + first: 'First', + second: 'Second', + metadata: { + key: 'value', + number: 1, + bool: true + }, + createdAt: 'Fri Mar 12 2021 13:24:53 GMT+0530 (India Standard Time)' + }, + documentId: activityId, + diffs: delta(event.databaseEntity, event.entity, { + exclude: ['createdAt', 'updatedAt'] + }), + title: 'Removed Mock team ', + createdBy: user + }; + + Sandbox.stub(Activity, 'create').returns({ + id: activityId, + ...activity + }); + await Resource.log(event, Resource.actions.DELETE); + } + ); + }); + + // lab.experiment('list users', () => { + // let users: any, groups, userEntityPolicy: any; + // + // lab.beforeEach(async () => { + // // setup data + // const dbUri = Config.get('/postgres').uri; + // const enforcer = await CasbinSingleton.create(dbUri); + // + // users = await factory(User)().createMany(5); + // groups = await factory(Group)().createMany(2); + // const user = users[0]; + // + // // user group mapping + // // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // // @ts-ignore + // await enforcer.addSubjectGroupingJsonPolicy( + // { user: users[0].id }, + // { group: groups[0].id }, + // { created_by: user } + // ); + // + // // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // // @ts-ignore + // await enforcer.addSubjectGroupingJsonPolicy( + // { user: users[1].id }, + // { group: groups[0].id }, + // { created_by: user } + // ); + // + // // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // // @ts-ignore + // await enforcer.addSubjectGroupingJsonPolicy( + // { user: users[3].id }, + // { group: groups[1].id }, + // { created_by: user } + // ); + // + // // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // // @ts-ignore + // await enforcer?.addSubjectGroupingJsonPolicy( + // { user: users[2].id }, + // { group: groups[1].id }, + // { created_by: user } + // ); + // + // // create relavant policies + // await enforcer?.addJsonPolicy( + // { group: groups[0].id }, + // { entity: 'gojek', privacy: 'public' }, + // { action: 'firehose.read' }, + // { created_by: user } + // ); + // userEntityPolicy = { + // subject: { user: users[2].id }, + // resource: { entity: 'gojek' }, + // action: { action: 'firehose.read' } + // }; + // await enforcer?.addJsonPolicy( + // userEntityPolicy.subject, + // userEntityPolicy.resource, + // userEntityPolicy.action, + // { created_by: user } + // ); + // await enforcer?.addJsonPolicy( + // { user: users[2].id }, + // { group: groups[1].id }, + // { role: 'team.admin' }, + // { created_by: user } + // ); + // }); + // + // lab.test( + // 'should return all users if no filters are specified', + // async () => { + // const getListWithFiltersStub = Sandbox.stub( + // Resource, + // 'getListWithFilters' + // ).returns(users); + // + // const result = await Resource.list(); + // Code.expect(result).to.equal(users); + // Sandbox.assert.notCalled(getListWithFiltersStub); + // } + // ); + // + // lab.test('should return users that match the filter', async () => { + // const removeTimestamps = R.omit(['createdAt', 'updatedAt']); + // + // const filter = { + // entity: 'gojek', + // privacy: 'public', + // action: 'firehose.read' + // }; + // const result = (await Resource.list(filter)).map(removeTimestamps); + // + // const expectedResult = [ + // { ...users[0], policies: [] }, + // { ...users[1], policies: [] }, + // { ...users[2], policies: [userEntityPolicy] } + // ].map(removeTimestamps); + // + // // ? We need to sort before checking because [1, 2, 3] != [2, 1, 3] + // Code.expect(R.sortBy(R.propOr(null, 'id'), result)).to.equal( + // R.sortBy(R.propOr(null, 'id'), expectedResult) + // ); + // }); + // }); +}); From 46a73a5e6678f4134edd19bdedde8e4fd354ecc1 Mon Sep 17 00:00:00 2001 From: Maninder Singh Date: Fri, 12 Mar 2021 20:32:39 +0530 Subject: [PATCH 13/15] remove commented code --- app/activity/resource.ts | 18 ------ test/app/activity/resource.ts | 108 ---------------------------------- 2 files changed, 126 deletions(-) diff --git a/app/activity/resource.ts b/app/activity/resource.ts index 93e10a42b..6b3bd9c27 100644 --- a/app/activity/resource.ts +++ b/app/activity/resource.ts @@ -107,24 +107,6 @@ const parseGroupActivity = async (activity: Activity) => { } } } - - // else if ( - // displayName.length > 0 && - // Object.prototype.hasOwnProperty.call(displayName[0], 'lhs') - // ) { - // if (Object.prototype.hasOwnProperty.call(displayName[0], 'rhs')) { - // console.log('metadata => ', metadata); - // if (metadata.length > 0) { - // metadata.forEach((meta) => { - // output.diff[meta.path[1] || ''] = [meta.lhs, meta.rhs]; - // }); - // } - // output.diff.edited = [displayName[0].lhs, displayName[0].rhs]; - // console.log('output edited => ', output); - // } else { - // output.diff.removed = [displayName[0].lhs]; - // } - // } return output; }; diff --git a/test/app/activity/resource.ts b/test/app/activity/resource.ts index d96ce3622..37d1343eb 100644 --- a/test/app/activity/resource.ts +++ b/test/app/activity/resource.ts @@ -220,112 +220,4 @@ lab.experiment('Activity::resource', () => { } ); }); - - // lab.experiment('list users', () => { - // let users: any, groups, userEntityPolicy: any; - // - // lab.beforeEach(async () => { - // // setup data - // const dbUri = Config.get('/postgres').uri; - // const enforcer = await CasbinSingleton.create(dbUri); - // - // users = await factory(User)().createMany(5); - // groups = await factory(Group)().createMany(2); - // const user = users[0]; - // - // // user group mapping - // // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // // @ts-ignore - // await enforcer.addSubjectGroupingJsonPolicy( - // { user: users[0].id }, - // { group: groups[0].id }, - // { created_by: user } - // ); - // - // // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // // @ts-ignore - // await enforcer.addSubjectGroupingJsonPolicy( - // { user: users[1].id }, - // { group: groups[0].id }, - // { created_by: user } - // ); - // - // // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // // @ts-ignore - // await enforcer.addSubjectGroupingJsonPolicy( - // { user: users[3].id }, - // { group: groups[1].id }, - // { created_by: user } - // ); - // - // // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // // @ts-ignore - // await enforcer?.addSubjectGroupingJsonPolicy( - // { user: users[2].id }, - // { group: groups[1].id }, - // { created_by: user } - // ); - // - // // create relavant policies - // await enforcer?.addJsonPolicy( - // { group: groups[0].id }, - // { entity: 'gojek', privacy: 'public' }, - // { action: 'firehose.read' }, - // { created_by: user } - // ); - // userEntityPolicy = { - // subject: { user: users[2].id }, - // resource: { entity: 'gojek' }, - // action: { action: 'firehose.read' } - // }; - // await enforcer?.addJsonPolicy( - // userEntityPolicy.subject, - // userEntityPolicy.resource, - // userEntityPolicy.action, - // { created_by: user } - // ); - // await enforcer?.addJsonPolicy( - // { user: users[2].id }, - // { group: groups[1].id }, - // { role: 'team.admin' }, - // { created_by: user } - // ); - // }); - // - // lab.test( - // 'should return all users if no filters are specified', - // async () => { - // const getListWithFiltersStub = Sandbox.stub( - // Resource, - // 'getListWithFilters' - // ).returns(users); - // - // const result = await Resource.list(); - // Code.expect(result).to.equal(users); - // Sandbox.assert.notCalled(getListWithFiltersStub); - // } - // ); - // - // lab.test('should return users that match the filter', async () => { - // const removeTimestamps = R.omit(['createdAt', 'updatedAt']); - // - // const filter = { - // entity: 'gojek', - // privacy: 'public', - // action: 'firehose.read' - // }; - // const result = (await Resource.list(filter)).map(removeTimestamps); - // - // const expectedResult = [ - // { ...users[0], policies: [] }, - // { ...users[1], policies: [] }, - // { ...users[2], policies: [userEntityPolicy] } - // ].map(removeTimestamps); - // - // // ? We need to sort before checking because [1, 2, 3] != [2, 1, 3] - // Code.expect(R.sortBy(R.propOr(null, 'id'), result)).to.equal( - // R.sortBy(R.propOr(null, 'id'), expectedResult) - // ); - // }); - // }); }); From 468ab58dd637ba7495c747bab5ea748c9feb8610 Mon Sep 17 00:00:00 2001 From: Maninder Singh Date: Mon, 15 Mar 2021 10:52:54 +0530 Subject: [PATCH 14/15] format message for role and group acitivity --- app/activity/resource.ts | 63 ++++++++++++++++++++++++++++++++-------- 1 file changed, 51 insertions(+), 12 deletions(-) diff --git a/app/activity/resource.ts b/app/activity/resource.ts index 6b3bd9c27..a9026ac18 100644 --- a/app/activity/resource.ts +++ b/app/activity/resource.ts @@ -4,6 +4,7 @@ import Constants from '../../utils/constant'; import { delta } from '../../utils/deep-diff'; import { User } from '../../model/user'; import { Role } from '../../model/role'; +import { Group } from '../../model/group'; type ActivityType = { id: string; @@ -32,6 +33,16 @@ const titleMap = { REMOVED_ATTRIBUTE_FROM_GROUP: 'Removed attribute from a team' }; +const mapData = (input: any[] = [], key: string) => { + return input.reduce((output, row) => { + if (!Object.prototype.hasOwnProperty.call(output, row[key])) { + // eslint-disable-next-line no-param-reassign + output[row[key]] = row; + } + return output; + }, {}); +}; + const activityResponsePayload = (activity: Activity) => { const activityResponse: ActivityType = { createdAt: activity.createdAt, @@ -111,36 +122,58 @@ const parseGroupActivity = async (activity: Activity) => { }; const parseCasbinActivity = async (activity: Activity) => { + const [groups, roles] = await Promise.all([ + await Group.find(), + await Role.find() + ]); + const groupMap = mapData(groups, 'id'); + const roleMap = mapData(roles, 'id'); + const output = activityResponsePayload(activity); const relation = relationType(activity.diffs); if (activity.documentId === '0') { if (relation.isRole) { + const userDiff = calcDiff(activity.diffs, 'v0'); + const groupDiff = calcDiff(activity.diffs, 'v1'); const roleDiff = calcDiff(activity.diffs, 'v2'); - const role = await Role.findOne({ + const role = roleMap[JSON.parse(roleDiff[0].rhs).role || '']; + const group = groupMap[JSON.parse(groupDiff[0].rhs).group || '']; + const user = await User.findOne({ select: ['displayname'], where: { - id: JSON.parse(roleDiff[0].rhs).role + id: JSON.parse(userDiff[0].rhs).user } }); - output.diff.created = [role?.displayname || '']; + output.diff.created = [ + `Assigned a role ${role?.displayname || ''} to user ${ + user?.displayname || '' + } for team ${group?.displayname || ''}` + ]; } else if (relation.isUser) { const userDiff = calcDiff(activity.diffs, 'v0'); + const groupDiff = calcDiff(activity.diffs, 'v1'); + const group = groupMap[JSON.parse(groupDiff[0].rhs).group || '']; const user = await User.findOne({ select: ['displayname'], where: { id: JSON.parse(userDiff[0].rhs).user } }); - output.diff.created = [user?.displayname || '']; + output.diff.created = [ + `Assigned a user ${user?.displayname || ''} to team ${ + group?.displayname || '' + }` + ]; } } else if (relation.isRole) { - const role = await Role.findOne({ - select: ['displayname'], - where: { - id: JSON.parse(activity.document.v2).role - } - }); - output.diff.removed = [role?.displayname || '', '']; + const { role } = JSON.parse(activity.document.v2); + const { group } = JSON.parse(activity.document.v0); + output.diff.removed = [ + `Removed a role ${roleMap[role]?.displayname || ''} from team ${ + groupMap[group]?.displayname || '' + }`, + '' + ]; } else if (relation.isUser) { const user = await User.findOne({ select: ['displayname'], @@ -148,7 +181,13 @@ const parseCasbinActivity = async (activity: Activity) => { id: JSON.parse(activity.document.v0).user } }); - output.diff.removed = [user?.displayname || '', '']; + const { group } = JSON.parse(activity.document.v1); + output.diff.removed = [ + `Remove a user ${user?.displayname || ''} from team ${ + groupMap[group]?.displayname || '' + }`, + '' + ]; } return output; }; From 6b326d9df3d6a4213943809a01f2a3eea13aa844 Mon Sep 17 00:00:00 2001 From: Maninder Singh Date: Mon, 15 Mar 2021 19:39:08 +0530 Subject: [PATCH 15/15] feat: log activity without calling db for policies --- app/activity/resource.ts | 47 +++++++------- lib/casbin/JsonFilteredEnforcer.ts | 99 ++++++++++++------------------ model/activity.ts | 2 +- test/app/activity/resource.ts | 34 +++++++++- utils/deep-diff.ts | 8 ++- 5 files changed, 103 insertions(+), 87 deletions(-) diff --git a/app/activity/resource.ts b/app/activity/resource.ts index a9026ac18..e44a65124 100644 --- a/app/activity/resource.ts +++ b/app/activity/resource.ts @@ -128,35 +128,35 @@ const parseCasbinActivity = async (activity: Activity) => { ]); const groupMap = mapData(groups, 'id'); const roleMap = mapData(roles, 'id'); - const output = activityResponsePayload(activity); const relation = relationType(activity.diffs); - if (activity.documentId === '0') { + const isDocumentEmpty = Object.keys(activity.document).length === 0; + if (isDocumentEmpty) { if (relation.isRole) { - const userDiff = calcDiff(activity.diffs, 'v0'); - const groupDiff = calcDiff(activity.diffs, 'v1'); - const roleDiff = calcDiff(activity.diffs, 'v2'); - const role = roleMap[JSON.parse(roleDiff[0].rhs).role || '']; - const group = groupMap[JSON.parse(groupDiff[0].rhs).group || '']; + const userDiff: any = calcDiff(activity.diffs, 'subject'); + const groupDiff: any = calcDiff(activity.diffs, 'resource'); + const roleDiff: any = calcDiff(activity.diffs, 'action'); + const role = roleMap[roleDiff[0]?.rhs?.role || '']; + const group = groupMap[groupDiff[0]?.rhs?.group || '']; const user = await User.findOne({ select: ['displayname'], where: { - id: JSON.parse(userDiff[0].rhs).user + id: userDiff[0]?.rhs?.user } }); output.diff.created = [ - `Assigned a role ${role?.displayname || ''} to user ${ - user?.displayname || '' + `Assigned a role ${role?.displayname || ''} ${ + user?.displayname ? `to user ${user?.displayname}` : '' } for team ${group?.displayname || ''}` ]; } else if (relation.isUser) { - const userDiff = calcDiff(activity.diffs, 'v0'); - const groupDiff = calcDiff(activity.diffs, 'v1'); - const group = groupMap[JSON.parse(groupDiff[0].rhs).group || '']; + const userDiff: any = calcDiff(activity.diffs, 'subject'); + const groupDiff: any = calcDiff(activity.diffs, 'resource'); + const group = groupMap[groupDiff[0]?.rhs?.group || '']; const user = await User.findOne({ select: ['displayname'], where: { - id: JSON.parse(userDiff[0].rhs).user + id: userDiff[0]?.rhs?.user } }); output.diff.created = [ @@ -166,8 +166,8 @@ const parseCasbinActivity = async (activity: Activity) => { ]; } } else if (relation.isRole) { - const { role } = JSON.parse(activity.document.v2); - const { group } = JSON.parse(activity.document.v0); + const { role } = activity.document.action; + const { group } = activity.document.subject; output.diff.removed = [ `Removed a role ${roleMap[role]?.displayname || ''} from team ${ groupMap[group]?.displayname || '' @@ -178,10 +178,10 @@ const parseCasbinActivity = async (activity: Activity) => { const user = await User.findOne({ select: ['displayname'], where: { - id: JSON.parse(activity.document.v0).user + id: activity.document?.subject?.user } }); - const { group } = JSON.parse(activity.document.v1); + const { group } = activity.document?.resource; output.diff.removed = [ `Remove a user ${user?.displayname || ''} from team ${ groupMap[group]?.displayname || '' @@ -210,7 +210,7 @@ export const get = async (groupId = '') => { whereParameter.createGroup = JSON.stringify([{ rhs: groupId }]); whereParameter.userRoleGroupMap = JSON.stringify([ { - rhs: JSON.stringify({ group: groupId }) + rhs: { group: groupId } } ]); } @@ -243,7 +243,10 @@ export const get = async (groupId = '') => { }; export const create = async (payload: any) => { - return await Activity.save({ ...payload }); + if (payload?.diffs && payload.diffs.length > 0) { + return await Activity.save({ ...payload }); + } + return null; }; const getTitle = (event: any, type: string) => { @@ -309,7 +312,7 @@ export const log = async (event: any, type: string) => { promise = create({ document: event.databaseEntity, title, - documentId: event.databaseEntity.id, + documentId: event.databaseEntity?.id || '0', model: event.metadata.tableName, diffs: delta(event.databaseEntity || {}, event.entity || {}, { exclude: excludeFields @@ -321,7 +324,7 @@ export const log = async (event: any, type: string) => { promise = create({ document: event.databaseEntity, title, - documentId: event.databaseEntity.id, + documentId: event.databaseEntity?.id || '0', model: event.metadata.tableName, diffs: delta(event.databaseEntity || {}, event.entity || {}, { exclude: excludeFields diff --git a/lib/casbin/JsonFilteredEnforcer.ts b/lib/casbin/JsonFilteredEnforcer.ts index 736452984..c9ec5f4dd 100644 --- a/lib/casbin/JsonFilteredEnforcer.ts +++ b/lib/casbin/JsonFilteredEnforcer.ts @@ -27,43 +27,25 @@ const groupPolicyParameters = (policies: PolicyObj[]) => { }; }; -const diff = (previous: JsonAttributes[], current: JsonAttributes[]) => { - return R.differenceWith( - (first, second) => { - return first.id === second?.id; +const sendLog = async (policy: any, type: string, user: any) => { + const log = { + entity: policy || {}, + databaseEntity: {}, + metadata: { + tableName: 'casbin_rule' }, - current, - previous - ); -}; - -const sendLog = async ( - policies: JsonAttributes[], - type: string, - user: unknown -) => { - return Promise.all( - policies.map((policy: any) => { - const log = { - entity: policy, - databaseEntity: {}, - metadata: { - tableName: 'casbin_rule' - }, - queryRunner: { - data: { - user - } - } - }; - - if (type === ActivityActions.DELETE) { - log.databaseEntity = log.entity; - log.entity = {}; + queryRunner: { + data: { + user } - return ActivityLog(log, type); - }) - ); + } + }; + + if (type === ActivityActions.DELETE) { + log.databaseEntity = log.entity; + log.entity = {}; + } + return ActivityLog(log, type); }; export class JsonFilteredEnforcer implements IEnforcer { @@ -245,15 +227,13 @@ export class JsonFilteredEnforcer implements IEnforcer { options: JsonAttributes ) { const enforcer = await this.getEnforcer(); - const previousPolicies = await this.getAllPolicies(); await enforcer.addPolicy( convertJSONToStringInOrder(subject), convertJSONToStringInOrder(resource), convertJSONToStringInOrder(action) ); - const currentPolicies = await this.getAllPolicies(); await sendLog( - diff(previousPolicies, currentPolicies), + { ptype: 'p', subject, resource, action }, ActivityActions.CREATE, options.created_by ); @@ -266,11 +246,9 @@ export class JsonFilteredEnforcer implements IEnforcer { options: JsonAttributes ) { const enforcer = await this.getEnforcer(); - const previousPolicies = await this.getAllPolicies(); await enforcer.addPolicy(subject, resource, action); - const currentPolicies = await this.getAllPolicies(); await sendLog( - diff(previousPolicies, currentPolicies), + { subject, resource, action }, ActivityActions.CREATE, options.created_by ); @@ -283,7 +261,6 @@ export class JsonFilteredEnforcer implements IEnforcer { options: JsonAttributes ) { const enforcer = await this.getEnforcer(); - const previousPolicies = await this.getAllPolicies(); await enforcer.removeFilteredNamedPolicy( 'p', 0, @@ -291,9 +268,8 @@ export class JsonFilteredEnforcer implements IEnforcer { convertJSONToStringInOrder(resource), convertJSONToStringInOrder(action) ); - const currentPolicies = await this.getAllPolicies(); await sendLog( - diff(currentPolicies, previousPolicies), + { ptype: 'p', subject, resource, action }, ActivityActions.DELETE, options.created_by ); @@ -305,16 +281,14 @@ export class JsonFilteredEnforcer implements IEnforcer { options: JsonAttributes ) { const enforcer = await this.getEnforcer(); - const previousPolicies = await this.getAllPolicies(); await enforcer.addNamedGroupingPolicy( 'g', convertJSONToStringInOrder(subject), convertJSONToStringInOrder(jsonAttributes), 'subject' ); - const currentPolicies = await this.getAllPolicies(); await sendLog( - diff(previousPolicies, currentPolicies), + { ptype: 'g', subject, resource: jsonAttributes, action: 'subject' }, ActivityActions.CREATE, options.created_by ); @@ -326,7 +300,6 @@ export class JsonFilteredEnforcer implements IEnforcer { options: JsonAttributes ) { const enforcer = await this.getEnforcer(); - const previousPolicies = await this.getAllPolicies(); await enforcer.removeFilteredNamedGroupingPolicy( 'g', 0, @@ -334,9 +307,8 @@ export class JsonFilteredEnforcer implements IEnforcer { convertJSONToStringInOrder(jsonAttributes), 'subject' ); - const currentPolicies = await this.getAllPolicies(); await sendLog( - diff(currentPolicies, previousPolicies), + { ptype: 'g', subject, resource: jsonAttributes, action: 'subject' }, ActivityActions.DELETE, options.created_by ); @@ -348,16 +320,19 @@ export class JsonFilteredEnforcer implements IEnforcer { options: JsonAttributes ) { const enforcer = await this.getEnforcer(); - const previousPolicies = await this.getAllPolicies(); await enforcer.addNamedGroupingPolicy( 'g2', convertJSONToStringInOrder(resource), convertJSONToStringInOrder(jsonAttributes), 'resource' ); - const currentPolicies = await this.getAllPolicies(); await sendLog( - diff(previousPolicies, currentPolicies), + { + ptype: 'g2', + subject: resource, + resource: jsonAttributes, + action: 'resource' + }, ActivityActions.CREATE, options.created_by ); @@ -378,15 +353,18 @@ export class JsonFilteredEnforcer implements IEnforcer { options: JsonAttributes ) { const enforcer = await this.getEnforcer(); - const previousPolicies = await this.getAllPolicies(); await enforcer.removeFilteredNamedGroupingPolicy( 'g2', 0, convertJSONToStringInOrder(resource) ); - const currentPolicies = await this.getAllPolicies(); await sendLog( - diff(currentPolicies, previousPolicies), + { + ptype: 'g2', + subject: resource, + resource: {}, + action: 'resource' + }, ActivityActions.DELETE, options.created_by ); @@ -398,16 +376,19 @@ export class JsonFilteredEnforcer implements IEnforcer { options: JsonAttributes ) { const enforcer = await this.getEnforcer(); - const previousPolicies = await this.getAllPolicies(); await enforcer.addNamedGroupingPolicy( 'g3', convertJSONToStringInOrder(action), convertJSONToStringInOrder(jsonAttributes), 'action' ); - const currentPolicies = await this.getAllPolicies(); await sendLog( - diff(previousPolicies, currentPolicies), + { + ptype: 'g3', + subject: action, + resource: jsonAttributes, + action: 'action' + }, ActivityActions.CREATE, options?.created_by ); diff --git a/model/activity.ts b/model/activity.ts index d87cb95bf..c81bdbc9a 100644 --- a/model/activity.ts +++ b/model/activity.ts @@ -36,7 +36,7 @@ export class Activity extends BaseEntity { type: 'jsonb', nullable: false }) - document: Record; + document: Record; @Column({ type: 'jsonb', diff --git a/test/app/activity/resource.ts b/test/app/activity/resource.ts index 37d1343eb..14f0e1195 100644 --- a/test/app/activity/resource.ts +++ b/test/app/activity/resource.ts @@ -22,10 +22,27 @@ lab.experiment('Activity::resource', () => { Sandbox.restore(); }); - lab.test('should create activity', async () => { + lab.test('should create activity with delta', async () => { const activity = { createdAt: 'Fri Mar 12 2021 13:24:53 GMT+0530 (India Standard Time)', - diffs: [], + diffs: [ + { rhs: 'p', kind: 'N', path: ['ptype'] }, + { + rhs: { group: '231d7c4c-d3c4-4bff-aacc-d7b0cf197edf' }, + kind: 'N', + path: ['subject'] + }, + { + rhs: { group: '231d7c4c-d3c4-4bff-aacc-d7b0cf197edf' }, + kind: 'N', + path: ['resource'] + }, + { + rhs: { role: 'd56e6288-6d67-43bc-8062-69bde9835312' }, + kind: 'N', + path: ['action'] + } + ], document: {}, documentId: 'ba256f55-bfce-4d17-8174-a0abbf26ccd4', model: 'User', @@ -40,6 +57,19 @@ lab.experiment('Activity::resource', () => { Sandbox.assert.calledWithExactly(activitySaveStub, activity); Code.expect(response).to.equal({ id: activityId, ...activity }); }); + + lab.test('should create activity without delta', async () => { + const activity = { + createdAt: 'Fri Mar 12 2021 13:24:53 GMT+0530 (India Standard Time)', + diffs: [], + document: {}, + documentId: 'ba256f55-bfce-4d17-8174-a0abbf26ccd4', + model: 'User', + title: 'Bypass Chips heuristic' + }; + const response = await Resource.create(activity); + Code.expect(response).to.equal(null); + }); }); lab.experiment('log activity', () => { diff --git a/utils/deep-diff.ts b/utils/deep-diff.ts index 918d97e63..97836fd08 100644 --- a/utils/deep-diff.ts +++ b/utils/deep-diff.ts @@ -5,7 +5,9 @@ export const delta = ( current = {}, options?: { exclude: string[] } ) => { - return diff(previous, current).filter((i: any) => - (options?.exclude || []).every((x: any) => i.path.indexOf(x) === -1) - ); + return diff(previous, current).filter((i: any) => { + return (options?.exclude || []).every( + (x: any) => i.path && i.path.indexOf(x) === -1 + ); + }); };