Skip to content

Commit

Permalink
Support scoped permissions
Browse files Browse the repository at this point in the history
  • Loading branch information
xinjie-zhang committed Feb 8, 2022
1 parent 91f4dcf commit 61c7dd9
Show file tree
Hide file tree
Showing 6 changed files with 129 additions and 50 deletions.
10 changes: 8 additions & 2 deletions packages/portlet-plugin-extension/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ const _ = require('lodash')
const { getSortedExtensions } = require('./extensions')

function extensionsByZone(name, options) {
let permissions = options.data.root.$permissions || {}
let extensionsInZone = options.data.root._extensionsByZone && options.data.root._extensionsByZone[name]
let extensions = getSortedExtensions(name, extensionsInZone, options.data.root.$allow)
let variable = options.hash.assignTo
Expand All @@ -17,7 +16,14 @@ module.exports = (portlet) => {
portlet.ready.extensions = false
portlet.allExtensionsByZone = {}
portlet.on('decorateResponse', (req, res) => {
res.locals._extensionsByZone = portlet.allExtensionsByZone
res.locals._extensionsByZone = portlet.allExtensionsByZone
res.buildExtensions = (extensionsInZone) => {
return getSortedExtensions('extension', extensionsInZone,
(perm, cond) => res.allow(perm, cond, res.locals._extensionsPermissionScope))
}
res.setExtensionsPermissionScope = (scope) => {
res.locals._extensionsPermissionScope = _.merge(res.locals._extensionsPermissionScope, scope)
}
})

portlet.on('beforeSetupRoutes', () => {
Expand Down
6 changes: 5 additions & 1 deletion packages/portlet-plugin-menu/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,14 @@ module.exports = (portlet) => {
res.locals._menusByZone = portlet.allMenusByZone
res.buildMenus = (menusInZone) => {
let lang = res.locals.$language
let menus = getSortedMenus(lang, 'menu', menusInZone, allow)
let menus = getSortedMenus(lang, 'menu', menusInZone,
(perm, cond) => res.allow(perm, cond, res.locals._menusPermissionScope))
let am = getActiveMenu(menus, res.locals.$url)
return { activeMenu: am && am.index, menus }
}
res.setMenusPermissionScope = (scope) => {
res.locals._menusPermissionScope = _.merge(res.locals._menusPermissionScope, scope)
}
})

portlet.on('beforeSetupRoutes', () => {
Expand Down
3 changes: 2 additions & 1 deletion packages/portlet-plugin-menu/redirects.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@ function setupRedirects(portletServer) {
$logger.error(`Could not find menu zone "${menu.zone}" when redirecting "${path}"`)
return next()
}
let menus = getSortedMenus('', menu.zone, menusInZone, res.allow)
let menus = getSortedMenus('', menu.zone, menusInZone,
(perm, cond) => res.allow(perm, cond, res.locals._menusPermissionScope))
let menuItem = null
if (menu.index == menu.zone) {
menuItem = getFirstMenu(menus)
Expand Down
38 changes: 12 additions & 26 deletions packages/portlet-plugin-permission/index.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
const _ = require('lodash')
const { evaluatePermissionFormular } = require('./utils')
const { evaluatePermissionFormular, empower } = require('./utils')

function allow(perm, options) {
if (!options) {
Expand All @@ -17,36 +17,22 @@ module.exports = (portlet) => {
portlet.on('decorateResponse', (req, res) => {
res.locals.$permissions = {}

res.locals.$allow = res.allow = (perm, cond) => {
let allowed = evaluatePermissionFormular(perm, res.locals.$permissions)
return allowed && (cond === undefined || cond)
res.locals.$allow = res.allow = (perm, cond, scope) => {
if (undefined === scope && _.isObject(cond)) {
scope = cond
cond = true
}
let allowed = evaluatePermissionFormular(perm, res.locals.$permissions, scope)
return allowed && (undefined === cond || cond)
}
res.ensure = (perm, cond) => {
if (!res.allow(perm, cond)) {
res.ensure = (perm, cond, scope) => {
if (!res.allow(perm, cond, scope)) {
$logger.error(`Access Forbidden (${JSON.stringify(req.user)}) @ ${req.path} `)
throw Error('Access Forbidden!')
}
}
res.empower = (perm, result) => {
let permMap = {}
if (undefined === result)
result = true
else
result = !!result

if (_.isString(perm))
permMap[perm] = result
else if (_.isArray(perm)) {
_.each(perm, k => permMap[k] = result)
} else if (_.isPlainObject(perm)) {
permMap = _.mapValues(perm, v => !!v)
} else {
$logger.warn(`Unable to empower ${perm}`)
}
_.each(permMap, (v, k) => {
if (_.indexOf(k, '.') == -1) $logger.warn('Permission id must follow format "{resource}.{action}"')
})
res.locals.$permissions = _.merge(res.locals.$permissions, permMap)
res.empower = (perm, result, scope) => {
_.merge(res.locals.$permissions, empower(perm, result, scope))
}
})

Expand Down
44 changes: 32 additions & 12 deletions packages/portlet-plugin-permission/tests/perm.test.js
Original file line number Diff line number Diff line change
@@ -1,14 +1,12 @@
const _ = require('lodash')
const { setupLogger } = require('@vimesh/logger')
const { evaluatePermissionFormular } = require('../utils')
const { evaluatePermissionFormular, empower } = require('../utils')

setupLogger()

it('normal cases', () => {
let ownedPerms = {
'user.view': true,
'user.edit': true
}
let ownedPerms = empower(['user.view', 'user.edit'])
expect(ownedPerms).toStrictEqual({ 'user.view': true, 'user.edit': true })
let result = evaluatePermissionFormular('user.view && user.edit', ownedPerms)
expect(result).toBeTruthy()
result = evaluatePermissionFormular('user.edit || user.delete', ownedPerms)
Expand All @@ -21,19 +19,41 @@ it('normal cases', () => {
expect(result).toBeTruthy()
})

it('normal cases @ scope', () => {
let ownedPerms = empower(['user@{account}.edit', 'user@{account}.view'], { account: 1 })
expect(ownedPerms).toStrictEqual({ '[email protected]': true, '[email protected]': true })
let result = evaluatePermissionFormular('[email protected] && [email protected]', ownedPerms)
expect(result).toBeTruthy()
result = evaluatePermissionFormular('[email protected] && [email protected]', ownedPerms)
expect(result).toBeFalsy()
result = evaluatePermissionFormular('[email protected] || [email protected]', ownedPerms)
expect(result).toBeTruthy()
result = evaluatePermissionFormular('[email protected] && [email protected]', ownedPerms)
expect(result).toBeFalsy()
})

it('all allowed', () => {
let ownedPerms = {
'*.*': true
}
let ownedPerms = empower('*.*')
expect(ownedPerms).toStrictEqual({ '*.*': true })
let result = evaluatePermissionFormular('user.edit && user.delete', ownedPerms)
expect(result).toBeTruthy()
})

it('evaluate permission @ scope', () => {
let ownedPerms = empower(['user@{account}.edit', 'user@{account}.delete'])
expect(ownedPerms).toStrictEqual({ 'user@{account}.edit': true, 'user@{account}.delete': true })
let result = evaluatePermissionFormular('user@{account}.edit && user@{account}.delete', ownedPerms, { account: 'account3' })
expect(result).toBeTruthy()

ownedPerms = empower(['user@{account}.edit', 'user@{account}.delete'], { account: 'account3' })
expect(ownedPerms).toStrictEqual({ '[email protected]': true, '[email protected]': true })
result = evaluatePermissionFormular('user@{account}.edit && user@{account}.delete', ownedPerms, { account: 'account3' })
expect(result).toBeTruthy()
})

it('all actions in a resource is allowed ', () => {
let ownedPerms = {
'user.*': true,
'order.view': true
}
let ownedPerms = empower(['user.*', 'order.view'])
expect(ownedPerms).toStrictEqual({ 'user.*': true, 'order.view': true })
let result = evaluatePermissionFormular('user.edit && order.view', ownedPerms)
expect(result).toBeTruthy()
result = evaluatePermissionFormular('user.edit && order.edit', ownedPerms)
Expand Down
78 changes: 70 additions & 8 deletions packages/portlet-plugin-permission/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,30 +2,92 @@ const _ = require('lodash')
const evaluate = require('simple-evaluate').default

function getValue(context, perm) {
if (context['*.*']) return true
let rsc, action
let pos = _.indexOf(perm, '.')
let rsc, action, scope = null
let pos = perm.indexOf('.')
if (pos == -1) {
rsc = perm
action = '*'
} else {
rsc = perm.substring(0, pos)
action = perm.substring(pos + 1)
}
if (context[`${rsc}.*`]) return true
return !!context[`${rsc}.${action}`]
pos = rsc.indexOf('@')
if (pos != -1) {
scope = rsc.substring(pos + 1)
rsc = rsc.substring(0, pos)
}
if (scope) {
if (context[`*@${scope}.*`]) return true
if (context[`${rsc}@${scope}.*`]) return true
return !!context[`${rsc}@${scope}.${action}`]
} else {
if (context['*.*']) return true
if (context[`${rsc}.*`]) return true
return !!context[`${rsc}.${action}`]
}
}

function evaluatePermissionFormular(formular, ownedPermissions) {
function applyScopeToPermissions(permMap, scope) {
if (_.isObject(scope)) {
permMap = _.mapKeys(permMap, (v, k) => {
let l = k.indexOf('@{')
let r = k.indexOf('}')
if (l != -1 && l < r && _.has(scope, k.substring(l + 2, r))) {
return k.substring(0, l + 1) + scope[k.substring(l + 2, r)] + k.substring(r + 1)
}
return k
})
}
return permMap
}
function applyScopeToPermissionFormular(formular, scope) {
_.each(_.keys(scope), k => {
formular = formular.replace(new RegExp(`{${k}}`, 'g'), `${scope[k]}`)
})
return formular
}
function evaluatePermissionFormular(formular, ownedPermissions, scope) {
if (!formular) return true
try {
return evaluate(ownedPermissions || {}, formular, { getValue })
if (_.isObject(scope)) {
ownedPermissions = applyScopeToPermissions(ownedPermissions, scope)
formular = applyScopeToPermissionFormular(formular, scope)
}
return evaluate(ownedPermissions, formular, { getValue })
} catch (ex) {
$logger.error(`Fails to evaluate permission formular(${formular}). `, ex)
return false
}
}

function empower(perm, result, scope) {
let permMap = {}
if (undefined === scope) {
if (undefined === result)
result = true
else if (_.isObject(result)) {
scope = result
result = true
} else {
result = !!result
}
}

if (_.isString(perm))
permMap[perm] = result
else if (_.isArray(perm)) {
_.each(perm, k => permMap[k] = result)
} else if (_.isPlainObject(perm)) {
permMap = _.mapValues(perm, v => !!v)
} else {
$logger.warn(`Unable to empower ${perm}`)
}
_.each(permMap, (v, k) => {
if (k.indexOf('.') == -1) $logger.warn('Permission id must follow format "{resource}(@{scope}).{action}"')
})
return applyScopeToPermissions(permMap, scope)
}

module.exports = {
empower,
evaluatePermissionFormular
}

0 comments on commit 61c7dd9

Please sign in to comment.