mirror of https://github.com/nocodb/nocodb
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
697 lines
21 KiB
697 lines
21 KiB
/* tslint:disable:prefer-const */ |
|
import colors from 'colors'; |
|
import fs from 'fs'; |
|
import glob from 'glob'; |
|
import jsonfile from 'jsonfile'; |
|
import path from 'path'; |
|
import Util from '../util/Util'; |
|
|
|
import Table from 'cli-table3'; |
|
|
|
class PermissionsMgr { |
|
|
|
public static async set(args) { |
|
|
|
if (Util.isProjectGraphql()) { |
|
try { |
|
|
|
if (args._.length < 4) { |
|
console.warn('Invalid arguments for : xc permissions.set') |
|
return; |
|
} |
|
|
|
|
|
// @ts-ignore |
|
let [_, models, users, ...resolvers] = args._; |
|
models = models.split('.'); |
|
users = users.split('.'); |
|
|
|
/* get all policies */ |
|
const policiesPath = path.join(process.cwd(), 'server', PermissionsMgr.getPolicyPath(), '**', '*.policy.js'); |
|
|
|
glob.sync(policiesPath).forEach((file) => { |
|
|
|
const modelName = path.basename(file).split('.')[0]; |
|
|
|
if (models.includes(modelName) || models[0] === '$') { |
|
const filePermissions = require(file); |
|
|
|
const roles = this.extractUniqueGqlPolicyRoles(filePermissions); |
|
|
|
if (users[0] === '$') { |
|
for (const [route, rolesObj] of Object.entries(filePermissions)) { |
|
|
|
if (resolvers[0] === '$=1') { |
|
const permObj = roles.reduce((obj, role) => { |
|
obj[role] = true; |
|
return obj; |
|
}, {}); |
|
|
|
Object.assign(rolesObj, permObj) |
|
console.log(`Setting Permissions for model:${modelName} roles:${roles.join(', ')} resolver: ${route}`); |
|
} else if (resolvers[0] === '$=0') { |
|
const permObj = roles.reduce((obj, role) => { |
|
obj[role] = false; |
|
return obj; |
|
}, {}); |
|
|
|
Object.assign(rolesObj, permObj) |
|
console.log(`Setting Permissions for model:${modelName} roles:${roles.join(', ')} resolver: ${route}`); |
|
} else { |
|
resolvers.forEach(permission => { |
|
const permTuple = permission.split('=') |
|
if (route === permTuple[0]) { |
|
const permObj = roles.reduce((obj, role) => { |
|
const val = !!(permTuple.length === 1 ? 1 : +permTuple[1] || 0); |
|
obj[role] = val; |
|
return obj; |
|
}, {}); |
|
Object.assign(rolesObj, permObj) |
|
console.log(`Setting Permissions for model:${modelName} roles:${roles.join(', ')} , resolver: ${route}`); |
|
} |
|
}) |
|
} |
|
|
|
} |
|
|
|
} else { |
|
|
|
|
|
for (const [route, rolesObj] of Object.entries(filePermissions)) { |
|
resolvers.forEach(permission => { |
|
const permTuple = permission.split('=') |
|
if (route === permTuple[0] || permTuple[0] === '$') { |
|
const permObj = users.reduce((obj, role) => { |
|
const val = !!(permTuple.length === 1 ? 1 : +permTuple[1] || 0); |
|
obj[role] = val; |
|
return obj; |
|
}, {}); |
|
Object.assign(rolesObj, permObj) |
|
console.log(`Setting Permissions for model:${modelName} roles:${users} , resolver: ${route}`); |
|
} |
|
}) |
|
} |
|
} |
|
|
|
const policyFileContent = `module.exports = ${JSON.stringify(filePermissions, null, 2)};\n`; |
|
fs.writeFileSync(file, policyFileContent) |
|
|
|
} |
|
|
|
}); |
|
|
|
} catch (e) { |
|
console.error(`Error in xc permissions.set`, e); |
|
} |
|
} else { |
|
try { |
|
if (args._.length < 4) { |
|
console.warn('Invalid arguments for : xc permissions.set') |
|
return; |
|
} |
|
|
|
// @ts-ignore |
|
let [_, models, users, ...permissions] = args._; |
|
|
|
models = models.split('.'); |
|
users = users.split('.'); |
|
|
|
/* get all policies */ |
|
const policiesPath = path.join(process.cwd(), 'server', PermissionsMgr.getPolicyPath(), '**', '*.routes.js'); |
|
|
|
glob.sync(policiesPath).forEach((file) => { |
|
|
|
const modelName = path.basename(file).split('.')[0]; |
|
if (models.includes(modelName) || models[0] === '$') { |
|
|
|
const routesList: Route[] = require(file); |
|
const roles = this.extractUniqueRoles(routesList); |
|
|
|
if (users[0] === '$') { |
|
for (const route of routesList) { |
|
|
|
if (permissions[0] === '$=1') { |
|
const permObj = roles.reduce((obj, role) => { |
|
obj[role] = true; |
|
return obj; |
|
}, {}); |
|
Object.assign(route.acl, permObj) |
|
console.log(`Setting Permissions for model:${modelName} roles:${roles.join(', ')} method: ${route.type}=true, route: ${route.path}`); |
|
} else if (permissions[0] === '$=0') { |
|
const permObj = roles.reduce((obj, role) => { |
|
obj[role] = false; |
|
return obj; |
|
}, {}); |
|
Object.assign(route.acl, permObj) |
|
console.log(`Setting Permissions for model:${modelName} roles:${roles.join(', ')} method: ${route.type}=false, route: ${route.path}`); |
|
} else { |
|
permissions.forEach(permission => { |
|
const permTuple = permission.split('=') |
|
const val = !!(permTuple.length === 1 ? 1 : +permTuple[1] || 0); |
|
const permObj = roles.reduce((obj, role) => { |
|
obj[role] = val; |
|
return obj; |
|
}, {}); |
|
if (route.type === permTuple[0]) { |
|
Object.assign(route.acl, permObj) |
|
console.log(`Setting Permissions for model:${modelName} roles:${roles.join(', ')} method: ${permTuple[0]}=${val}, route: ${route.path}`); |
|
} |
|
}) |
|
} |
|
} |
|
} else { |
|
for (const route of routesList) { |
|
permissions.forEach(permission => { |
|
const permTuple = permission.split('=') |
|
const val = !!(permTuple.length === 1 ? 1 : +permTuple[1] || 0); |
|
const permObj = users.reduce((obj, role) => { |
|
obj[role] = val; |
|
return obj; |
|
}, {}); |
|
|
|
if (route.type === permTuple[0]) { |
|
Object.assign(route.acl, permObj) |
|
console.log(`Setting Permissions for model:${modelName} roles:${users.join(', ')} method: ${permTuple[0]}=${val}, route: ${route.path}`); |
|
} else if (permTuple[0] === '*') { |
|
Object.assign(route.acl, permObj) |
|
console.log(`Setting Permissions for model:${modelName} roles:${users.join(', ')} method: ${route.type}=${val}, route: ${route.path}`); |
|
} |
|
}) |
|
} |
|
} |
|
|
|
const policyFileContent = `module.exports = ${JSON.stringify(routesList, null, 2)};\n`; |
|
fs.writeFileSync(file, policyFileContent) |
|
|
|
} |
|
}); |
|
} catch (e) { |
|
console.error(`Error in xc permissions.set`, e); |
|
} |
|
} |
|
} |
|
|
|
public static async get(args) { |
|
|
|
if (Util.isProjectGraphql()) { |
|
try { |
|
if (args._.length < 2) { |
|
console.warn('Invalid arguments for : xc permissions.get') |
|
return; |
|
} |
|
|
|
let {1: models} = args._; |
|
models = models.split('.'); |
|
|
|
/* get all policies */ |
|
const policiesPath = path.join(process.cwd(), 'server', PermissionsMgr.getPolicyPath(), '**', '*.policy.js'); |
|
|
|
|
|
// instantiate |
|
let rows: any[] = []; |
|
let roles: any[] = []; |
|
|
|
|
|
glob.sync(policiesPath).forEach((file) => { |
|
// let filePermissions = require(file); |
|
|
|
const modelName = path.basename(file).split('.')[0]; |
|
|
|
if (models.includes(modelName)) { |
|
const filePermissions = require(file); |
|
|
|
|
|
roles = this.extractUniqueGqlPolicyRoles(filePermissions); |
|
|
|
rows.push([{ |
|
colSpan: roles.length + 1, |
|
content: colors.green(file), |
|
hAlign: 'center' |
|
}]) |
|
|
|
for (const [route, methods] of Object.entries(filePermissions)) { |
|
const row: any[] = [{content: route, vAlign: 'center'}]; |
|
for (const role of roles) { |
|
row.push(methods[role] ? colors.green('✔') : colors.red('x')); |
|
} |
|
rows.push(row) |
|
} |
|
} |
|
}) |
|
|
|
|
|
const table = new Table({ |
|
head: ['Route', ...roles] |
|
}); |
|
|
|
table.push(...rows); |
|
console.log(table.toString()); |
|
|
|
} catch (e) { |
|
console.error(`Error in xc permissions.get`, e); |
|
} |
|
} else { |
|
|
|
|
|
try { |
|
|
|
if (args._.length < 2) { |
|
console.warn('Invalid arguments for : xc permissions.get') |
|
return; |
|
} |
|
|
|
let {1: models} = args._; |
|
models = models.split('.'); |
|
|
|
/* get all policies */ |
|
const policiesPath = path.join(process.cwd(), 'server', PermissionsMgr.getPolicyPath(), '**', '*.routes.js'); |
|
|
|
|
|
// instantiate |
|
const table = new Table({ |
|
head: ['Route', 'Role', 'GET', 'POST', 'PUT', 'DELETE', 'PATCH'] |
|
}); |
|
|
|
|
|
glob.sync(policiesPath) |
|
.sort(this.sortFiles) |
|
.forEach((file) => { |
|
const modelName = path.basename(file).split('.')[0]; |
|
|
|
if (models.includes(modelName)) { |
|
const routesList: Route[] = require(file); |
|
const groupedRoutes = this.groupRoutes(routesList); |
|
|
|
|
|
// extract unique roles |
|
const roles = this.extractUniqueRoles(routesList); |
|
|
|
table.push([{ |
|
colSpan: 7, |
|
content: colors.green(file), |
|
hAlign: 'center' |
|
}]) |
|
|
|
|
|
for (const [routePath, methods] of Object.entries(groupedRoutes)) { |
|
let i = 0; |
|
for (const role of roles) { |
|
{ |
|
table.push([...(i++ ? [] : [{ |
|
content: routePath, |
|
rowSpan: roles.length, |
|
vAlign: 'center' |
|
}]), role, |
|
methods?.get?.acl?.[role] ? colors.green('✔') : colors.red('x'), |
|
methods?.post?.acl?.[role] ? colors.green('✔') : colors.red('x'), |
|
methods?.put?.acl?.[role] ? colors.green('✔') : colors.red('x'), |
|
methods?.delete?.acl?.[role] ? colors.green('✔') : colors.red('x'), |
|
methods?.patch?.acl?.[role] ? colors.green('✔') : colors.red('x'), |
|
] as any) |
|
} |
|
} |
|
} |
|
|
|
} |
|
|
|
}) |
|
|
|
console.log(table.toString()); |
|
|
|
|
|
} catch (e) { |
|
console.error(`Error in xc permissions.get`, e); |
|
} |
|
} |
|
} |
|
|
|
|
|
public static async userAdd(args) { |
|
|
|
if (Util.isProjectGraphql()) { |
|
|
|
try { |
|
if (args._.length < 2) { |
|
console.warn('Invalid arguments for : xc permissions.userAdd') |
|
return; |
|
} |
|
|
|
const {1: user} = args._; |
|
|
|
/* get all policies */ |
|
const policiesPath = path.join(process.cwd(), 'server', PermissionsMgr.getPolicyPath(), '**', '*.policy.js'); |
|
|
|
glob.sync(policiesPath).forEach((file) => { |
|
|
|
const modelName = path.basename(file).split('.')[0]; |
|
|
|
|
|
const filePermissions = require(file); |
|
|
|
const roles = this.extractUniqueGqlPolicyRoles(filePermissions); |
|
|
|
|
|
if (roles.includes(user)) { |
|
console.warn(`${user} already exist in ${modelName} policy`); |
|
return; |
|
} |
|
|
|
for (const roles1 of Object.values(filePermissions)) { |
|
roles1[user] = true; |
|
} |
|
|
|
|
|
console.log(`Adding new role permission for model:${modelName} roles:${user}`); |
|
|
|
|
|
const policyFileContent = `module.exports = ${JSON.stringify(filePermissions, null, 2)};\n`; |
|
fs.writeFileSync(file, policyFileContent) |
|
|
|
}); |
|
|
|
} catch (e) { |
|
console.error(`Error in xc permissions.user.add`, e); |
|
} |
|
|
|
} else { |
|
|
|
try { |
|
if (args._.length < 2) { |
|
console.warn('Invalid arguments for : xc permissions.userAdd') |
|
return; |
|
} |
|
|
|
const {1: user} = args._; |
|
|
|
/* get all policies */ |
|
const policiesPath = path.join(process.cwd(), 'server', PermissionsMgr.getPolicyPath(), '**', '*.routes.js'); |
|
|
|
glob.sync(policiesPath).forEach((file) => { |
|
|
|
const modelName = path.basename(file).split('.')[0]; |
|
|
|
|
|
const routes: Route[] = require(file); |
|
|
|
const roles = this.extractUniqueRoles(routes); |
|
|
|
|
|
if (roles.includes(user)) { |
|
console.warn(`${user} already exist in ${modelName} policy`); |
|
return; |
|
} |
|
|
|
for (const route of routes) { |
|
route.acl[user] = true; |
|
} |
|
|
|
|
|
console.log(`Adding new role permission for model:${modelName} roles:${user}`); |
|
|
|
|
|
const policyFileContent = `module.exports = ${JSON.stringify(routes, null, 2)};\n`; |
|
fs.writeFileSync(file, policyFileContent) |
|
|
|
}); |
|
|
|
} catch (e) { |
|
console.error(`Error in xc permissions.user.add`, e); |
|
} |
|
} |
|
|
|
|
|
} |
|
|
|
|
|
public static async userDelete(args) { |
|
|
|
if (Util.isProjectGraphql()) { |
|
try { |
|
if (args._.length < 2) { |
|
console.warn('Invalid arguments for : xc permissions.userAdd') |
|
return; |
|
} |
|
|
|
const {1: user} = args._; |
|
|
|
/* get all policies */ |
|
const policiesPath = path.join(process.cwd(), 'server', PermissionsMgr.getPolicyPath(), '**', '*.policy.js'); |
|
|
|
glob.sync(policiesPath).forEach((file) => { |
|
|
|
const modelName = path.basename(file).split('.')[0]; |
|
|
|
|
|
const filePermissions = require(file); |
|
|
|
|
|
const roles = this.extractUniqueGqlPolicyRoles(filePermissions); |
|
|
|
|
|
if (!roles.includes(user)) { |
|
console.warn(`${user} not exist in ${modelName} policy`); |
|
return; |
|
} |
|
|
|
for (const roles1 of Object.values(filePermissions)) { |
|
delete roles1[user]; |
|
} |
|
|
|
|
|
console.log(`Deleting user permission for model:${modelName} roles:${user}`); |
|
|
|
|
|
const policyFileContent = `module.exports = ${JSON.stringify(filePermissions, null, 2)};\n`; |
|
fs.writeFileSync(file, policyFileContent) |
|
|
|
}); |
|
|
|
} catch (e) { |
|
console.error(`Error in xc permissions.user.delete`, e); |
|
} |
|
} else { |
|
try { |
|
if (args._.length < 2) { |
|
console.warn('Invalid arguments for : xc permissions.userAdd') |
|
return; |
|
} |
|
|
|
const {1: user} = args._; |
|
|
|
/* get all policies */ |
|
const policiesPath = path.join(process.cwd(), 'server', PermissionsMgr.getPolicyPath(), '**', '*.routes.js'); |
|
|
|
glob.sync(policiesPath).forEach((file) => { |
|
|
|
const modelName = path.basename(file).split('.')[0]; |
|
|
|
|
|
const routes: Route[] = require(file); |
|
|
|
|
|
const roles = this.extractUniqueRoles(routes) |
|
|
|
|
|
if (!roles.includes(user)) { |
|
console.warn(`${user} not exist in ${modelName} policy`); |
|
return; |
|
} |
|
|
|
for (const route of routes) { |
|
delete route.acl[user]; |
|
} |
|
|
|
|
|
console.log(`Deleting user permission for model:${modelName} roles:${user}`); |
|
|
|
|
|
const policyFileContent = `module.exports = ${JSON.stringify(routes, null, 2)};\n`; |
|
fs.writeFileSync(file, policyFileContent) |
|
|
|
}); |
|
|
|
} catch (e) { |
|
console.error(`Error in xc permissions.user.delete`, e); |
|
} |
|
} |
|
} |
|
|
|
public static async userRename(args) { |
|
|
|
if (Util.isProjectGraphql()) { |
|
try { |
|
if (args._.length < 3) { |
|
console.warn('Invalid arguments for : xc permissions.userAdd') |
|
return; |
|
} |
|
|
|
const {1: oldUser, 2: newUser} = args._; |
|
|
|
/* get all policies */ |
|
const policiesPath = path.join(process.cwd(), 'server', PermissionsMgr.getPolicyPath(), '**', '*.policy.js'); |
|
|
|
glob.sync(policiesPath).forEach((file) => { |
|
|
|
const modelName = path.basename(file).split('.')[0]; |
|
|
|
|
|
const filePermissions = require(file); |
|
|
|
const roles = this.extractUniqueGqlPolicyRoles(filePermissions); |
|
|
|
|
|
if (!roles.includes(oldUser)) { |
|
console.warn(`${oldUser} not exist in ${modelName} policy`); |
|
return; |
|
} |
|
if (roles.includes(newUser)) { |
|
console.warn(`${newUser} is already exist in ${modelName} policy`); |
|
return; |
|
} |
|
|
|
for (const roles1 of Object.values(filePermissions)) { |
|
roles1[newUser] = roles1[oldUser]; |
|
delete roles1[oldUser]; |
|
} |
|
|
|
|
|
console.log(`Renaming user permission ${oldUser} to ${newUser} for model:${modelName}`); |
|
const policyFileContent = `module.exports = ${JSON.stringify(filePermissions, null, 2)};\n`; |
|
fs.writeFileSync(file, policyFileContent) |
|
|
|
}); |
|
|
|
} catch (e) { |
|
console.error(`Error in xc permissions.user.delete`, e); |
|
} |
|
} else { |
|
try { |
|
if (args._.length < 3) { |
|
console.warn('Invalid arguments for : xc permissions.userAdd') |
|
return; |
|
} |
|
|
|
const {1: oldUser, 2: newUser} = args._; |
|
|
|
/* get all policies */ |
|
const policiesPath = path.join(process.cwd(), 'server', PermissionsMgr.getPolicyPath(), '**', '*.routes.js'); |
|
|
|
glob.sync(policiesPath).forEach((file) => { |
|
|
|
const modelName = path.basename(file).split('.')[0]; |
|
|
|
|
|
const routes: Route[] = require(file); |
|
|
|
const roles = this.extractUniqueRoles(routes); |
|
|
|
if (!roles.includes(oldUser)) { |
|
console.warn(`${oldUser} not exist in ${modelName} policy`); |
|
return; |
|
} |
|
if (roles.includes(newUser)) { |
|
console.warn(`${newUser} is already exist in ${modelName} policy`); |
|
return; |
|
} |
|
|
|
for (const route of routes) { |
|
route.acl[newUser] = route.acl[oldUser]; |
|
delete route.acl[oldUser]; |
|
} |
|
|
|
|
|
console.log(`Renaming user permission ${oldUser} to ${newUser} for model:${modelName}`); |
|
const policyFileContent = `module.exports = ${JSON.stringify(routes, null, 2)};\n`; |
|
fs.writeFileSync(file, policyFileContent) |
|
|
|
}); |
|
|
|
} catch (e) { |
|
console.error(`Error in xc permissions.user.delete`, e); |
|
} |
|
} |
|
} |
|
|
|
public static getPolicyPath() { |
|
|
|
const projectConfig = jsonfile.readFileSync(path.join(process.cwd(), 'config.xc.json')); |
|
return projectConfig.meta.projectType === 'rest' ? 'routers' : 'resolvers'; |
|
} |
|
|
|
private static extractUniqueGqlPolicyRoles(filePermissions) { |
|
return Object.values(filePermissions) |
|
.flatMap(roles1 => Object.keys(roles1)) |
|
.filter((v, i, arr) => arr.indexOf(v) === i); |
|
} |
|
|
|
private static extractUniqueRoles(routesList: Route[]) { |
|
const roles = routesList |
|
.flatMap( |
|
route => Object.keys(route.acl) |
|
) |
|
.filter((v, i, arr) => arr.indexOf(v) === i); |
|
return roles; |
|
} |
|
|
|
|
|
private static groupRoutes(routes: Route[]): GroupedRoutes { |
|
const groupedRoutes: GroupedRoutes = {}; |
|
for (const route of routes) { |
|
groupedRoutes[route.path] = groupedRoutes[route.path] || {}; |
|
groupedRoutes[route.path][route.type] = route; |
|
} |
|
return groupedRoutes; |
|
} |
|
|
|
private static sortFiles(file1: string, file2: string): number { |
|
return ((file1.indexOf('.bt.') > -1 ? 1 : 0) || (file1.indexOf('.hm.') > -1 ? 2 : 0)) |
|
- ((file2.indexOf('.bt.') > -1 ? 1 : 0) || (file2.indexOf('.hm.') > -1 ? 2 : 0)); |
|
} |
|
} |
|
|
|
|
|
export default PermissionsMgr; |
|
|
|
export interface Route { |
|
type: string; |
|
path: string; |
|
acl: { |
|
[key: string]: boolean |
|
}; |
|
} |
|
|
|
type httpMethods = 'get' | 'post' | 'patch' | 'put' | 'delete'; |
|
|
|
export interface GroupedRoutes { |
|
[key: string]: { |
|
[key in httpMethods]?: { |
|
type: string; |
|
path: string; |
|
acl: { |
|
[key: string]: boolean |
|
}; |
|
} |
|
} |
|
} |
|
/** |
|
* @copyright Copyright (c) 2021, Xgene Cloud Ltd |
|
* |
|
* @author Naveen MR <oof1lab@gmail.com> |
|
* @author Pranav C Balan <pranavxc@gmail.com> |
|
* |
|
* @license GNU AGPL version 3 or any later version |
|
* |
|
* This program is free software: you can redistribute it and/or modify |
|
* it under the terms of the GNU Affero General Public License as |
|
* published by the Free Software Foundation, either version 3 of the |
|
* License, or (at your option) any later version. |
|
* |
|
* This program is distributed in the hope that it will be useful, |
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
* GNU Affero General Public License for more details. |
|
* |
|
* You should have received a copy of the GNU Affero General Public License |
|
* along with this program. If not, see <http://www.gnu.org/licenses/>. |
|
* |
|
*/
|
|
|