Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Role merge #1185

Merged
merged 20 commits into from
Mar 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 0 additions & 26 deletions lib/layer/decorate.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -179,32 +179,6 @@ export default async function decorate(layer) {
layer.style.label = typeof layer.style.label === 'object' ? layer.style.label : layer.style.labels[layer.style.label || Object.keys(layer.style.labels)[0]];
}

// Handle role-based configurations.
if (Array.isArray(mapp.user?.roles)) {
for (const role in layer.roles || {}) {
if (layer.roles[role] !== null && typeof layer.roles[role] === 'object') {
const negatedRole = role.match(/(?<=^!)(.*)/g)?.[0];
if (mapp.user.roles.includes(role)) {
mapp.utils.merge(layer, layer.roles[role]);
} else if (negatedRole && !mapp.user.roles.includes(negatedRole)) {
mapp.utils.merge(layer, layer.roles[role]);
}
}
}
layer.infoj?.filter(entry => typeof entry.roles === 'object').forEach(entry => {
for (const role in entry.roles) {
if (typeof entry.roles[role] === 'object') {
const roleName = role.match(/(?<=^!)(.*)/g)?.[0];
if (mapp.user.roles.includes(role)) {
mapp.utils.merge(entry, entry.roles[role]);
} else if (roleName && !mapp.user.roles.includes(roleName)) {
mapp.utils.merge(entry, entry.roles[role]);
}
}
}
});
}

// Call layer and/or plugin methods.
Object.keys(layer).forEach((key) => {
typeof mapp.layer[key] === 'function' && mapp.layer[key]?.(layer);
Expand Down
13 changes: 4 additions & 9 deletions mod/query.js
Original file line number Diff line number Diff line change
Expand Up @@ -302,16 +302,11 @@ async function layerQuery(req, res) {
// Layer queries must have a geom param.
req.params.geom ??= req.params.layer.geom

// Get array of role filter from layer configuration.
const roles = Roles.filter(req.params.layer, req.params.user?.roles)

// Create params filter string from roleFilter filter params.
req.params.filter = `
${req.params.layer.filter?.default && `AND ${sqlFilter(req.params.layer.filter.default, req.params.SQL)}` || ''}
${req.params.filter && `AND ${sqlFilter(JSON.parse(req.params.filter), req.params.SQL)}` || ''}
${roles && Object.values(roles).some(r => !!r)
? `AND ${sqlFilter(Object.values(roles).filter(r => !!r), req.params.SQL)}`
: ''}`
req.params.filter = [
req.params.layer.filter?.default && `AND ${sqlFilter(req.params.layer.filter.default, req.params.SQL)}` || '',
req.params.filter && `AND ${sqlFilter(JSON.parse(req.params.filter), req.params.SQL)}` || '']
.join(' ')

if (req.params.viewport) {

Expand Down
92 changes: 59 additions & 33 deletions mod/utils/roles.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@
@module /utils/roles
*/

const merge = require('./merge')

module.exports = {
check,
filter,
get
get,
objMerge
}

function check(obj, user_roles) {
Expand All @@ -14,10 +16,9 @@ function check(obj, user_roles) {
if (!obj.roles) return obj;

// Always return object with '*' asterisk role.
if (Object.hasOwn(obj.roles,'*')) return obj;
if (Object.hasOwn(obj.roles, '*')) return obj;

// There are no user roles or user_roles are not an array.
if (!user_roles || !Array.isArray(user_roles)) return false;
if (user_roles === undefined) return false

// Some negated role is included in user_roles[]
const someNegatedRole = Object.keys(obj.roles).some(
Expand All @@ -44,34 +45,6 @@ function check(obj, user_roles) {
return false;
}

// Return an object with filter matching the layer.roles with user_roles.
function filter(layer, user_roles) {

// The layer must have roles.
if (!layer.roles) return;

// user_roles must be an array.
if (!Array.isArray(user_roles)) return;

const roleFilter = Object.keys(layer.roles)

// filter roles with a filter object.
.filter(key => layer.roles[key] && typeof layer.roles[key].filter === 'object')

// filter roles included in the user_roles array.
.filter(key => user_roles.includes(key)

// or negated roles (!) NOT included in the array.
|| !user_roles.includes(key.match(/(?<=^!)(.*)/g)?.[0]))

.reduce((o, key) => {
o[key] = layer.roles[key].filter
return o
}, {})

return roleFilter
}

function get(obj) {

const roles = new Set();
Expand All @@ -98,4 +71,57 @@ function get(obj) {
roles.delete('*')

return Array.from(roles)
}

function objMerge(obj, user_roles) {

if (typeof obj !== 'object') return obj;

if (user_roles === undefined) return obj

if (Array.isArray(obj)) {

return obj.map(arrEntry => objMerge(arrEntry, user_roles))
}

Object.keys(obj)
.filter(key => typeof obj[key] === 'object')
.forEach(key => {

// Cannot convert undefined or null to object.
if (!obj[key]) return;

obj[key] = objMerge(obj[key], user_roles)
})

if (!obj.roles) return obj;

if (typeof obj.roles !== 'object') return obj;

if (Array.isArray(obj.roles)) return obj;

if (typeof obj.roles === 'function') return obj;

const clone = structuredClone(obj)

function notIncludesNegatedRole(role, user_roles) {

return role.match(/(?<=^!)(.*)/g)?.[0]?
!user_roles.includes(role.match(/(?<=^!)(.*)/g)?.[0]):
false
}

Object.keys(clone.roles)
.filter(role => clone.roles[role] !== true)
.filter(role => clone.roles[role] !== null)
.filter(role => typeof clone.roles[role] === 'object')
.filter(role => !Array.isArray(clone.roles[role]))
.filter(role => user_roles.includes(role) || notIncludesNegatedRole(role, user_roles))
.forEach(role => {
merge(clone, clone.roles[role])
})

delete clone.roles

return clone
}
38 changes: 23 additions & 15 deletions mod/workspace/_workspace.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@ async function layer(req, res) {
const locale = workspace.locales[req.params.locale]

const roles = req.params.user?.roles || []

roles.push('*')

if (!Roles.check(locale, roles)) {
return res.status(403).send('Role access denied for locale.')
Expand All @@ -54,21 +56,21 @@ async function layer(req, res) {
return res.status(400).send(`Unable to validate layer param.`)
}

const layer = await getLayer(req.params)
let layer = await getLayer(req.params)

if (!Roles.check(layer, roles)) {
return res.status(403).send('Role access denied for layer.')
}

layer = Roles.objMerge(layer, roles)

res.json(layer)
}

function locales(req, res) {

const roles = req.params.user?.roles || []

const locales = Object.values(workspace.locales)
.filter(locale => !!Roles.check(locale, roles))
.filter(locale => !!Roles.check(locale, req.params.user?.roles))
.map(locale => ({
key: locale.key,
name: locale.name
Expand Down Expand Up @@ -104,28 +106,34 @@ async function locale(req, res) {
matched => process.env[`SRC_${matched.replace(/(^\${)|(}$)/g, '')}`])
)

const roles = req.params.user?.roles || []

roles.push('*')

// Return layer object instead of array of layer keys
if (req.params.layers) {

const layers = []

for (const key of Object.keys(locale.layers)) {

const layer = await getLayer({
const layers = Object.keys(locale.layers)
.map(async key => await getLayer({
...req.params,
layer: key
})
}))

if (layer instanceof Error) continue;
if(!Roles.check(layer, req.params.user?.roles)) continue;
await Promise.all(layers).then(layers=>{

layers.push(layer)
}
locale.layers = layers
.filter(layer => !!layer)
.filter(layer => !(layer instanceof Error))
.filter(layer => Roles.check(layer, roles))
})

locale.layers = layers
// Also merges roles in layer objects.
locale = Roles.objMerge(locale, roles)

return res.json(locale)
}

locale = Roles.objMerge(locale, roles)

// Check layer access.
locale.layers = locale.layers && Object.entries(locale.layers)
Expand Down
3 changes: 3 additions & 0 deletions mod/workspace/getLayer.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,9 @@ module.exports = async (params) => {

let layer = locale.layers[params.layer]

// layer maybe null or undefined.
if (!layer) return;

// Return already merged layer.
if (layer.merged) return layer

Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
},
"devDependencies": {
"clean-jsdoc-theme": "^4.2.17",
"codi-test-framework": "^0.0.8",
"codi-test-framework": "^0.0.10",
"cookie-parser": "^1.4.5",
"dotenv": "^16.4.5",
"esbuild": "^0.19.11",
Expand Down
12 changes: 6 additions & 6 deletions tests/mod/utils/merge.test.mjs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { describe, it, assertDeepEqual } from 'codi-test-framework';
import { describe, it, assertEqual } from 'codi-test-framework';
import mergeDeep from '../../../mod/utils/merge.js'

describe('mergeDeep', () => {
describe('mergeDeep Module', () => {

it('should merge objects deeply', () => {
const target = {
Expand Down Expand Up @@ -36,7 +36,7 @@ describe('mergeDeep', () => {

const mergedObj = mergeDeep(target, source);

assertDeepEqual(mergedObj, expected)
assertEqual(mergedObj, expected)
});

it('should merge arrays deeply', () => {
Expand All @@ -54,7 +54,7 @@ describe('mergeDeep', () => {

const mergedObj = mergeDeep(target, source);

assertDeepEqual(mergedObj, expected)
assertEqual(mergedObj, expected)
});

it('should handle merging with null or undefined values', () => {
Expand All @@ -74,8 +74,8 @@ describe('mergeDeep', () => {
const mergedObj1 = mergeDeep(target, source1);
const mergedObj2 = mergeDeep(target, source2);

assertDeepEqual(mergedObj1, expected);
assertDeepEqual(mergedObj2, expected);
assertEqual(mergedObj1, expected);
assertEqual(mergedObj2, expected);
});

});
47 changes: 18 additions & 29 deletions tests/mod/utils/roles.test.js
Original file line number Diff line number Diff line change
@@ -1,36 +1,25 @@
const {
check,
filter,
get
check,
get
} = require('../../../mod/utils/roles');

describe('Testing module functions', () => {

describe('Testing check function', () => {
it('should return the object when all roles are negated', () => {
const obj = {roles: {'!admin': true}};
const roles = ['user'];
expect(check(obj, roles)).toEqual(obj);
});

describe('Testing check function', () => {
it('should return the object when all roles are negated', () => {
const obj = { roles: { '!admin': true } };
const roles = ['user'];
expect(check(obj, roles)).toEqual(obj);
});

describe('Testing filter function', () => {
it('should return filter objects for user_roles matched with layer.roles', () => {
const layer = {roles: {'user': { filter: 'test' }}};
const user_roles = ['user'];
expect(filter(layer, user_roles)).toEqual({});
});


});

describe('Testing get function', () => {
it('should return array of roles from the object', () => {
const obj = {roles: {'user': true}, test: {roles: {'admin': true}}};
expect(get(obj)).toEqual(['user', 'admin']);
});


});

describe('Testing get function', () => {
it('should return array of roles from the object', () => {
const obj = { roles: { 'user': true }, test: { roles: { 'admin': true } } };
expect(get(obj)).toEqual(['user', 'admin']);
});

});

});

});
Loading
Loading