diff --git a/server/src/graphql/resolvers.js b/server/src/graphql/resolvers.js index f9333a3c3..76ccca259 100644 --- a/server/src/graphql/resolvers.js +++ b/server/src/graphql/resolvers.js @@ -19,6 +19,9 @@ const { getPlacementCells } = require('../utils/getPlacementCells') const { getTypeCells } = require('../utils/getTypeCells') const { getValidCoords } = require('../utils/getValidCoords') +const { loadCachedAreas } = require('../services/areas') +const { buildAreaPermissionChecker } = require('../services/permissions/area') + /** @type {import("@apollo/server").ApolloServerOptions['resolvers']} */ const resolvers = { JSON: GraphQLJSON, @@ -342,38 +345,47 @@ const resolvers = { } return [{ features: [] }] }, +@@ scanAreasMenu: (_, _args, { req, perms }) => { - if (perms?.scanAreas) { - const scanAreas = config.getAreas(req, 'scanAreasMenu') - if (perms.areaRestrictions.length) { - const filtered = scanAreas - .map((parent) => ({ - ...parent, - children: perms.areaRestrictions.includes(parent.name) - ? parent.children - : parent.children.filter((child) => - perms.areaRestrictions.includes(child.properties.name), - ), - })) - .filter((parent) => parent.children.length) + if (!perms?.scanAreas) return [] - // // Adds new blanks to account for area restrictions trimming some - // filtered.forEach(({ children }) => { - // if (children.length % 2 === 1) { - // children.push({ - // type: 'Feature', - // properties: { - // name: '', - // manual: !!config.getSafe('manualAreas.length'), - // }, - // }) - // } - // }) - return filtered - } - return scanAreas.filter((parent) => parent.children.length) - } - return [] + // Existing domain-scoped menu from config + const menu = config.getAreas(req, 'scanAreasMenu') + + // Roles source (match how the rest of your resolvers derive roles) + const roles = + (perms && (perms.roles || perms.all)) || + (req && req.user && req.user.roles) || + [] + + // Build runtime checker that understands roles + areas + parent(s) + const ctx = loadCachedAreas() // has scanAreasObj with .properties.key/name/parent + const isAllowed = buildAreaPermissionChecker({ scanAreasObj: ctx.scanAreasObj }) + + // Pre-compute allowed area keys for this user + const allowed = new Set( + Object.keys(ctx.scanAreasObj).filter((k) => isAllowed(roles, k)), + ) + + // Final filter: include only children whose key is allowed. + // (If you still want to honor perms.areaRestrictions as an extra allow-list, + // uncomment the OR below.) + const filtered = menu + .map((parent) => ({ + ...parent, + children: parent.children.filter((child) => { + const key = child.properties.key + const name = child.properties.name + const allowByRule = allowed.has(key) + // Optional legacy allow by explicit perms.areaRestrictions (name-based): + // const allowByPerms = perms.areaRestrictions?.includes(parent.name) || perms.areaRestrictions?.includes(name) + // return allowByRule || allowByPerms + return allowByRule + }), + })) + .filter((p) => p.children.length) + + return filtered }, scannerConfig: (_, { mode }, { perms }) => { const scanner = config.getSafe('scanner') diff --git a/server/src/services/permissions/area.js b/server/src/services/permissions/area.js new file mode 100644 index 000000000..2492ad79e --- /dev/null +++ b/server/src/services/permissions/area.js @@ -0,0 +1,37 @@ +// Tiny helper to evaluate areaRestrictions with role + area + parent filters. +// Accepts either "parent" or "parents" in rules. AND semantics by default. +const config = require('@rm/config') + +/** + * @param {{ scanAreasObj: Record }} deps + * @returns {(userRoles: string[]|undefined|null, areaKey: string) => boolean} + */ +function buildAreaPermissionChecker({ scanAreasObj }) { + const rules = config.getSafe('authentication.areaRestrictions') || [] + if (!rules.length) return () => true + + return (userRoles, areaKey) => { + const f = scanAreasObj[areaKey] + const name = f?.properties?.name + const parent = f?.properties?.parent || null + const rolesSet = new Set(userRoles || []) + + return rules.some(rule => { + const roles = rule.roles || [] + const areas = rule.areas || [] + const parents = rule.parent || rule.parents || [] + + // roles: empty => applies to all; otherwise user must have at least one + const roleOK = roles.length === 0 || roles.some(r => rolesSet.has(r)) + // areas: empty => no area-name filter; allow matching by key or by name + const areaOK = areas.length === 0 || areas.includes(areaKey) || (name && areas.includes(name)) + // parents: empty => no parent filter + const parentOK = parents.length === 0 || (parent && parents.includes(parent)) + + // AND semantics; for OR use: roleOK && (areaOK || parentOK) + return roleOK && areaOK && parentOK + }) + } +} + ++module.exports = { buildAreaPermissionChecker }