Skip to content
Closed
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
72 changes: 42 additions & 30 deletions server/src/graphql/resolvers.js
Original file line number Diff line number Diff line change
Expand Up @@ -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<import("@rm/types").GqlContext>['resolvers']} */
const resolvers = {
JSON: GraphQLJSON,
Expand Down Expand Up @@ -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')
Expand Down
37 changes: 37 additions & 0 deletions server/src/services/permissions/area.js
Original file line number Diff line number Diff line change
@@ -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<string, { properties?: { key?: string, name?: string, parent?: string } }> }} 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 }
Loading