ChengLei Shao a614bc7de8
feat: acl optimization (#1136)
* fix: sort field with table dose not have primary key

* feat: fixed params merger

* chore(plugins/acl): fixed params

* chore(plugins/acl): allowConfigure of collections

* chore(plugins/china-region): disable actions other than list

* chore(plugins/collection-manager): allowConfigure permission

* chore(plugins/file-manager): acl fixed params

* chore: acl fixed params

* chore: rolesResourcesScopes onDelete cascade

* fix: install error

* chore: test

* fix: root user fixed params

* fix: role resource scope onDelete

* chore: test

* chore: test

* fix: acl

* chore: disable index.html cache

* chore: disable index.html cache

* test: destory user role

* test: destory throught table

* fix: test

* fix: test

* chore: add rolesUsers to fixed params

* feat: permission logging

* feat: permission logging

* fix: test

* fix: test

* chore: disable grant target action

* fix: appends with fields

* fix: get action params

* fix: associationActions

* chore: change AssociationField using relation type

* chore: typo

* refactor: allow to skip

* fix: prettier

* chore: attachments association action

* fix: allowConfigure condition

* fix: deprecated allow

* fix: please use skip instead

* feat: table column aclcheck

* chore: test

* feat: throw error when detory no permission record

* chore: test

* chore: acl test

* feat: field acl

* chore: after action middleware

* fix: destory permission check

* chore: middleware use

* fix: test

* feat: filter match

* feat: subform/subtable field acl check

* feat: action permision by scope

* feat: action permision by scope

* feat: list action with allowedActions

* chore: all allowed action

* fix: pk error

* fix: merge error

* fix: create query sql

* fix: skip permission

* fix: scope with association field

* feat: action acl fix

* feat: action acl fix

* fix: update submodule

* Feat: setting center permission (#1214)

* feat: add setting center permissions

* feat: setting center permissions backlist

* feat: setting center permissions BLACKLIST

* feat: setting center permissions blacklist

* feat: setting center permissions blacklist

* feat: setting center permission

* feat: configure plugin tab expand

Co-authored-by: chenos <chenlinxh@gmail.com>

* Feat :field acl (#1211)

Co-authored-by: chenos <chenlinxh@gmail.com>

* fix: build error

* test: acl snippet

* feat: set field

* fix: test

* fix: build error

* fix: utils Dependency cycles

* feat: general permissions

* feat: delete pluginTabBlacklist

* fix: test

* feat: snippetManager allow method

* feat: acl role snippetAllowed method

* feat: array field repository

* feat:  ArrayFieldRepository

* fix: test

* fix: ci

* fix: ci error

* fix: add set parse

* test: array field repository

* chore: addSnippetPatten

* fix: start

* feat: sync role snippets

* feat: snippets check

* feat: snippets check

* chore: acl role snippet api

* fix: test

* fix: test

* refactor: acl role snippets

* chore: registerACLSettingSnippet

* chore: default snippets

* feat: snippets match

* feat: snippets check

* feat: snippets check

* feat: pm permision check

* feat: pm permision check

* feat: snippet pattern match

* feat: pluginManagerToolbar check

* feat: pluginManagerToolbar check

* chore: snippets default value

* feat: set role snippets migration

* chore: snippets

* feat: acl local

* feat: acl local

* feat: bookmask fix

* feat: plugin-manger & ui-editor snippet

* feat: set allowConfigure to false when upgrade to snippets

* feat: destory action acl fix

* feat: destory action acl fix

* fix: association resource params merge

* fix: ui editor snippet

* feat:  action acl fix

* chore: move list meta middleware into plugins/acl

* fix: test

* feat:  action acl fix

* feat: action acl check fix

* feat: plugins toolbar fix

* feat: gitmodules

* fix: subproject

* chore: add avaiableActions to snippet

* chore: change plugin-manager snippet

* feat: configure action acl fix

* feat: plugin tab acl check fix

* chore: roles snippets

* fix: add actions to snippet

* feat: allowconfigure fix

* fix: count with filterBy

* fix: build error

* feat: get action with allowedActions

* feat: acl route check fix

* feat:  aclActionProvider fix

* feat: actionscpe fix

* feat: actionname alias

* feat: setting center fix

* feat: acl provider fix

* fix: role collection

* feat: associate resource  acl

* feat: associate resource  acl

* feat: redirect to 403

* feat: route redirct

* feat:  acl scope check by record

* fix: fields  appends fix

* fix: fields  appends fix

* fix: fields  appends fix

* fix: allowedActions  fix

* fix:  menu items

* fix: rename

* fix: improve code

* fix: improve code

* fix: improve code

* fix: ctx?.data?.data

* fix: styling

* fix: allowAll after ignore scope

* chore: allowConfigure condition

* fix: collections.fields:*

* fix: acl test

* fix: update submodule

* fix: acl test

* fix: acl snippet

* fix: updates

* fix: only load history for logged-in users

* fix: this.app.acl.registerSnippet

* fix: downloadXlsxTemplate

* fix: 404

* feat: allowedAction in association list response

* fix: listData get

* fix: test

* fix: x-collection-field

* fix: update record error

* fix: calendar template

* test: allow manager

* fix: fetch action step

* fix: update submodule

* fix: refresh

* fix: refresh

* fix: rolesResourcesScopes

* test: snippets

* fix: snippets

* fix: test

* fix: omit filter.createdById

* fix: improve code

* fix: collections path

* fix: test error

* fix: upgrade error

* fix: errors

* fix: read allowed actions error

* fix: kanban error

* fix: error

Co-authored-by: chenos <chenlinxh@gmail.com>
Co-authored-by: katherinehhh <katherine_15995@163.com>
2023-01-09 07:35:48 +08:00

388 lines
10 KiB
TypeScript

import { CleanOptions, Collection, SyncOptions } from '@nocobase/database';
import { requireModule } from '@nocobase/utils';
import execa from 'execa';
import fs from 'fs';
import net from 'net';
import { resolve } from 'path';
import xpipe from 'xpipe';
import Application from '../application';
import { Plugin } from '../plugin';
import collectionOptions from './options/collection';
import resourceOptions from './options/resource';
import { PluginManagerRepository } from './PluginManagerRepository';
export interface PluginManagerOptions {
app: Application;
plugins?: any[];
}
export interface InstallOptions {
cliArgs?: any[];
clean?: CleanOptions | boolean;
sync?: SyncOptions;
}
export class PluginManager {
app: Application;
collection: Collection;
repository: PluginManagerRepository;
plugins = new Map<string, Plugin>();
server: net.Server;
pmSock: string;
_tmpPluginArgs = [];
constructor(options: PluginManagerOptions) {
this.app = options.app;
const f = resolve(process.cwd(), 'storage', 'pm.sock');
this.pmSock = xpipe.eq(this.app.options.pmSock || f);
this.app.db.registerRepositories({
PluginManagerRepository,
});
this.collection = this.app.db.collection(collectionOptions);
this.repository = this.collection.repository as PluginManagerRepository;
this.repository.setPluginManager(this);
this.app.resourcer.define(resourceOptions);
this.app.acl.registerSnippet({
name: 'pm',
actions: ['pm:*', 'applicationPlugins:list'],
});
this.server = net.createServer((socket) => {
socket.on('data', async (data) => {
const { method, plugins } = JSON.parse(data.toString());
try {
console.log(method, plugins);
await this[method](plugins);
} catch (error) {
console.error(error.message);
}
});
socket.pipe(socket);
});
this.app.on('beforeLoad', async (app, options) => {
if (options?.method && ['install', 'upgrade'].includes(options.method)) {
await this.collection.sync();
}
const exists = await this.app.db.collectionExistsInDb('applicationPlugins');
if (!exists) {
return;
}
if (options?.method !== 'install' || options.reload) {
await this.repository.load();
}
});
this.app.on('beforeUpgrade', async () => {
await this.collection.sync();
});
this.addStaticMultiple(options.plugins);
}
addStaticMultiple(plugins: any) {
for (let plugin of plugins || []) {
if (typeof plugin == 'string') {
this.addStatic(plugin);
} else {
this.addStatic(...plugin);
}
}
}
getPlugins() {
return this.plugins;
}
get(name: string) {
return this.plugins.get(name);
}
has(name: string) {
return this.plugins.has(name);
}
clientWrite(data: any) {
const { method, plugins } = data;
if (method === 'create') {
try {
console.log(method, plugins);
this[method](plugins);
} catch (error) {
console.error(error.message);
}
return;
}
const client = new net.Socket();
client.connect(this.pmSock, () => {
client.write(JSON.stringify(data));
client.end();
});
client.on('error', async () => {
try {
console.log(method, plugins);
await this[method](plugins);
} catch (error) {
console.error(error.message);
}
});
}
async listen(): Promise<net.Server> {
if (fs.existsSync(this.pmSock)) {
await fs.promises.unlink(this.pmSock);
}
return new Promise((resolve) => {
this.server.listen(this.pmSock, () => {
resolve(this.server);
});
});
}
async create(name: string | string[]) {
console.log('creating...');
const pluginNames = Array.isArray(name) ? name : [name];
const { run } = require('@nocobase/cli/src/util');
const createPlugin = async (name) => {
const { PluginGenerator } = require('@nocobase/cli/src/plugin-generator');
const generator = new PluginGenerator({
cwd: resolve(process.cwd(), name),
args: {},
context: {
name,
},
});
await generator.run();
};
await Promise.all(pluginNames.map((pluginName) => createPlugin(pluginName)));
await run('yarn', ['install']);
}
clone() {
const pm = new PluginManager({
app: this.app,
});
for (const arg of this._tmpPluginArgs) {
pm.addStatic(...arg);
}
return pm;
}
addStatic(plugin?: any, options?: any) {
if (!options?.async) {
this._tmpPluginArgs.push([plugin, options]);
}
let name: string;
if (typeof plugin === 'string') {
name = plugin;
plugin = PluginManager.resolvePlugin(plugin);
} else {
name = plugin.name;
if (!name) {
throw new Error(`plugin name invalid`);
}
}
const instance = new plugin(this.app, {
name,
enabled: true,
...options,
});
const pluginName = instance.getName();
if (this.plugins.has(pluginName)) {
throw new Error(`plugin name [${pluginName}] already exists`);
}
this.plugins.set(pluginName, instance);
return instance;
}
async generateClientFile(plugin: string, packageName: string) {
const file = resolve(
process.cwd(),
'packages',
process.env.APP_PACKAGE_ROOT || 'app',
'client/src/plugins',
`${plugin}.ts`,
);
if (!fs.existsSync(file)) {
try {
require.resolve(`${packageName}/client`);
await fs.promises.writeFile(file, `export { default } from '${packageName}/client';`);
const { run } = require('@nocobase/cli/src/util');
await run('yarn', ['nocobase', 'postinstall']);
} catch (error) {}
}
}
async add(plugin: any, options: any = {}, transaction?: any) {
if (Array.isArray(plugin)) {
const t = transaction || (await this.app.db.sequelize.transaction());
try {
const items = await Promise.all(plugin.map((p) => this.add(p, options, t)));
await t.commit();
return items;
} catch (error) {
await t.rollback();
throw error;
}
}
const packageName = await PluginManager.findPackage(plugin);
const packageJson = require(`${packageName}/package.json`);
await this.generateClientFile(plugin, packageName);
const instance = this.addStatic(plugin, {
...options,
async: true,
});
let model = await this.repository.findOne({
transaction,
filter: { name: plugin },
});
if (!model) {
const { enabled, builtIn, installed, ...others } = options;
model = await this.repository.create({
transaction,
values: {
name: plugin,
version: packageJson.version,
enabled: !!enabled,
builtIn: !!builtIn,
installed: !!installed,
options: {
...others,
},
},
});
}
return instance;
}
async load(options: any = {}) {
for (const [name, plugin] of this.plugins) {
if (!plugin.enabled) {
continue;
}
await plugin.beforeLoad();
}
for (const [name, plugin] of this.plugins) {
if (!plugin.enabled) {
continue;
}
await this.app.emitAsync('beforeLoadPlugin', plugin, options);
await plugin.load();
await this.app.emitAsync('afterLoadPlugin', plugin, options);
}
}
async install(options: InstallOptions = {}) {
for (const [name, plugin] of this.plugins) {
if (!plugin.enabled) {
continue;
}
await this.app.emitAsync('beforeInstallPlugin', plugin, options);
await plugin.install(options);
await this.app.emitAsync('afterInstallPlugin', plugin, options);
}
}
async enable(name: string | string[]) {
try {
const pluginNames = await this.repository.enable(name);
await this.app.reload();
await this.app.db.sync();
for (const pluginName of pluginNames) {
const plugin = this.app.getPlugin(pluginName);
if (!plugin) {
throw new Error(`${name} plugin does not exist`);
}
await plugin.install();
await plugin.afterEnable();
}
} catch (error) {
throw error;
}
}
async disable(name: string | string[]) {
try {
const pluginNames = await this.repository.disable(name);
await this.app.reload();
for (const pluginName of pluginNames) {
const plugin = this.app.getPlugin(pluginName);
if (!plugin) {
throw new Error(`${name} plugin does not exist`);
}
await plugin.afterDisable();
}
} catch (error) {
throw error;
}
}
async remove(name: string | string[]) {
const pluginNames = typeof name === 'string' ? [name] : name;
for (const pluginName of pluginNames) {
const plugin = this.app.getPlugin(pluginName);
if (!plugin) {
throw new Error(`${name} plugin does not exist`);
}
await plugin.remove();
}
await this.repository.remove(name);
this.app.reload();
}
static getPackageName(name: string) {
const prefixes = this.getPluginPkgPrefix();
for (const prefix of prefixes) {
try {
require.resolve(`${prefix}${name}`);
return `${prefix}${name}`;
} catch (error) {
continue;
}
}
throw new Error(`${name} plugin does not exist`);
}
static getPluginPkgPrefix() {
return (process.env.PLUGIN_PACKAGE_PREFIX || '@nocobase/plugin-,@nocobase/preset-,@nocobase/plugin-pro-').split(
',',
);
}
static async findPackage(name: string) {
try {
const packageName = this.getPackageName(name);
return packageName;
} catch (error) {
console.log(`\`${name}\` plugin not found locally`);
const prefixes = this.getPluginPkgPrefix();
for (const prefix of prefixes) {
try {
const packageName = `${prefix}${name}`;
console.log(`Try to find ${packageName}`);
await execa('npm', ['v', packageName, 'versions']);
console.log(`${packageName} downloading`);
await execa('yarn', ['add', packageName, '-W']);
console.log(`${packageName} downloaded`);
return packageName;
} catch (error) {
continue;
}
}
}
throw new Error(`No available packages found, ${name} plugin does not exist`);
}
static resolvePlugin(pluginName: string) {
const packageName = this.getPackageName(pluginName);
return requireModule(packageName);
}
}
export default PluginManager;