import { Plugin } from '@nocobase/server'; import { ACL } from '@nocobase/acl'; import path from 'path'; import { availableActionResource } from './actions/available-actions'; import { roleCollectionsResource } from './actions/role-collections'; import { createACL } from './acl'; import { RoleResourceActionModel } from './model/RoleResourceActionModel'; import { RoleResourceModel } from './model/RoleResourceModel'; export interface AssociationFieldAction { associationActions: string[]; targetActions?: string[]; } interface AssociationFieldActions { [availableActionName: string]: AssociationFieldAction; } export interface AssociationFieldsActions { [associationType: string]: AssociationFieldActions; } export class GrantHelper { resourceTargetActionMap = new Map(); targetActionResourceMap = new Map(); constructor() {} } export default class PluginACL extends Plugin { acl: ACL; associationFieldsActions: AssociationFieldsActions = {}; grantHelper = new GrantHelper(); registerAssociationFieldAction(associationType: string, value: AssociationFieldActions) { this.associationFieldsActions[associationType] = value; } getACL() { return this.acl; } registerAssociationFieldsActions() { this.registerAssociationFieldAction('linkTo', { view: { associationActions: ['list', 'get'], }, create: { associationActions: ['add'], targetActions: ['view'], }, update: { associationActions: ['add', 'remove', 'toggle'], targetActions: ['view'], }, }); this.registerAssociationFieldAction('attachments', { view: { associationActions: ['list', 'get'], }, add: { associationActions: ['upload', 'add'], }, update: { associationActions: ['update', 'add', 'remove', 'toggle'], }, }); this.registerAssociationFieldAction('subTable', { view: { associationActions: ['list', 'get'], }, create: { associationActions: ['create'], }, update: { associationActions: ['update', 'destroy'], }, }); } async beforeLoad() { const acl = createACL(); this.acl = acl; this.app.db.registerModels({ RoleResourceActionModel, RoleResourceModel, }); this.registerAssociationFieldsActions(); this.app.resourcer.define(availableActionResource); this.app.resourcer.define(roleCollectionsResource); this.app.db.on('roles.afterSave', async (model, options) => { const { transaction } = options; const roleName = model.get('name'); let role = acl.getRole(roleName); if (!role) { role = acl.define({ role: model.get('name'), }); } role.setStrategy({ ...(model.get('strategy') || {}), allowConfigure: model.get('allowConfigure'), }); // model is default if (model.get('default')) { await this.app.db.getRepository('roles').update({ values: { default: false, }, filter: { 'name.$ne': model.get('name'), }, hooks: false, transaction, }); } }); this.app.db.on('roles.afterDestroy', (model) => { const roleName = model.get('name'); acl.removeRole(roleName); }); this.app.db.on('rolesResources.afterSaveWithAssociations', async (model: RoleResourceModel, options) => { await model.writeToACL({ acl: this.acl, associationFieldsActions: this.associationFieldsActions, transaction: options.transaction, grantHelper: this.grantHelper, }); }); this.app.db.on('rolesResourcesActions.afterUpdateWithAssociations', async (model, options) => { const { transaction } = options; const resource = await model.getResource({ transaction, }); await resource.writeToACL({ acl: this.acl, associationFieldsActions: this.associationFieldsActions, transaction: options.transaction, grantHelper: this.grantHelper, }); }); this.app.db.on('fields.afterDestroy', async (model, options) => { const collectionName = model.get('collectionName'); const fieldName = model.get('name'); const resourceActions = await this.app.db.getRepository('rolesResourcesActions').find({ filter: { 'resource.name': collectionName, 'fields.$anyOf': [fieldName], }, transaction: options.transaction, }); for (const resourceAction of resourceActions) { const fields = resourceAction.get('fields') as string[]; const newFields = fields.filter((field) => field != fieldName); await this.app.db.getRepository('rolesResourcesActions').update({ filterByTk: resourceAction.get('id') as number, values: { fields: newFields, }, transaction: options.transaction, }); } }); } async load() { await this.app.db.import({ directory: path.resolve(__dirname, 'collections'), }); this.app.resourcer.use(this.acl.middleware()); } }