Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
62 commits
Select commit Hold shift + click to select a range
600eed6
feat(auth): support parent-based areaRestrictions in config
unseenmagik Mar 19, 2026
37cd8ad
fix(areas): include parent keys in grouped child selection
unseenmagik Mar 20, 2026
8a136a0
fix(areas): normalize parent restrictions to area keys
Mygod Mar 26, 2026
f3794da
fix(areas): keep parent rows aligned with visible permissions
Mygod Mar 26, 2026
02f41c3
fix(areas): handle header-only parents and domain scoping
Mygod Mar 26, 2026
de39a54
Merge branch 'develop' into main
Mygod Mar 26, 2026
1ffbdfd
fix(areas): tighten parent restriction expansion
Mygod Mar 26, 2026
f7562fa
fix(areas): preserve parent group access
Mygod Mar 26, 2026
889b852
fix(areas): tighten parent filter keys
Mygod Mar 26, 2026
c557395
fix(auth): close parent area auth gaps
Mygod Mar 27, 2026
6038634
fix(areas): harden mem filters and parent rows
Mygod Mar 27, 2026
1185cbc
fix(areas): restore legacy parent behavior
Mygod Mar 27, 2026
17b0beb
fix(areas): preserve parent rollout access
Mygod Mar 27, 2026
400ab95
fix(areas): restore unrestricted scan area access
Mygod Mar 27, 2026
1bbcaf5
fix(auth): distinguish unrestricted area grants
Mygod Mar 27, 2026
1afbb31
fix(weather): honor no-access area restrictions
Mygod Mar 27, 2026
b5fb48e
fix(areas): preserve persisted child filters
Mygod Mar 27, 2026
28fcd46
fix(auth): normalize persisted area restrictions
Mygod Mar 27, 2026
9dab6da
fix(areas): scope filtered parent scan toggles
Mygod Mar 27, 2026
e65f4df
fix(auth): preserve unrestricted area merges
Mygod Mar 30, 2026
2803e05
fix(auth): preserve no-access normalization
Mygod Mar 30, 2026
f081a88
fix(auth): preserve scoped area restrictions
Mygod Mar 30, 2026
155040b
fix(auth): preserve legacy parent area grants
Mygod Mar 31, 2026
d210742
fix(auth): scope legacy parent fallback
Mygod Mar 31, 2026
ac88d2d
fix(auth): prefer concrete area names
Mygod Mar 31, 2026
f136ea6
fix(auth): resolve legacy area targets safely
Mygod Mar 31, 2026
591f718
fix(auth): avoid ambiguous parent expansion
Mygod Mar 31, 2026
5b84973
fix(drawer): include grouped parent area key
Mygod Mar 31, 2026
0df5be0
fix(drawer): keep grouped area toggles scoped
Mygod Mar 31, 2026
a980290
fix(auth): expand reused parent keys
Mygod Mar 31, 2026
5325f12
fix(drawer): preserve grouped area exclusions
Mygod Mar 31, 2026
7e3b1d5
fix(auth): scope parent grants per request
Mygod Mar 31, 2026
90d64e4
fix(drawer): preserve grouped search state
Mygod Mar 31, 2026
8af5dfc
fix(drawer): keep grouped toggles consistent
Mygod Mar 31, 2026
ce0ad44
fix(drawer): avoid parent area overreach
Mygod Mar 31, 2026
9e51d12
fix(drawer): clear legacy parent filters
Mygod Mar 31, 2026
8f4a99f
fix(auth): scope serialized parent grants
Mygod Mar 31, 2026
79e3a2d
fix(auth): preserve area restriction semantics
Mygod Mar 31, 2026
e9dc694
fix(auth): keep parent grants portable
Mygod Mar 31, 2026
8bc11e1
fix(auth): avoid parent key overreach
Mygod Mar 31, 2026
ae79c3e
fix(auth): scope named area rules
Mygod Mar 31, 2026
3e23d99
fix(auth): scope serialized parent grants
Mygod Mar 31, 2026
168edf5
fix(auth): preserve unrestricted area grants
Mygod Mar 31, 2026
aea4ec7
fix(drawer): exclude grouped parent filter keys
Mygod Mar 31, 2026
f2776a7
fix(auth): keep legacy area grants global
Mygod Mar 31, 2026
b6c863a
fix(drawer): allow manual-only parent filters
Mygod Mar 31, 2026
9052b70
fix(auth): scope named area grants
Mygod Mar 31, 2026
c8f4dcb
fix(auth): scope anonymous parent grants
Mygod Mar 31, 2026
85fd773
fix(drawer): keep map parent filters editable
Mygod Mar 31, 2026
1052c23
fix(drawer): keep child selection literal
Mygod Mar 31, 2026
d5e84b4
fix(drawer): normalize grouped map toggles
Mygod Mar 31, 2026
2723720
fix(scanArea): migrate legacy grouped filters
Mygod Mar 31, 2026
d966285
fix(scanArea): migrate filters outside the layer
Mygod Mar 31, 2026
68ed024
fix(areas): align legacy grouped selection state
Mygod Mar 31, 2026
2e0e698
fix(scanArea): preserve legacy child map toggles
Mygod Mar 31, 2026
70d0332
fix(scanArea): migrate legacy keys on hydration
Mygod Mar 31, 2026
f7c97c1
fix(scanArea): gate legacy filter migration
Mygod Mar 31, 2026
bfb73c6
fix(scanArea): hydrate legacy filters from config
Mygod Mar 31, 2026
b4ea431
fix(scanArea): preserve partial parent toggles
Mygod Mar 31, 2026
12de2fa
fix(scanArea): migrate legacy filters from settings
Mygod Mar 31, 2026
8496744
fix(scanArea): make partial parent taps additive
Mygod Mar 31, 2026
737058c
fix(auth): preserve no-access scan area settings
Mygod Mar 31, 2026
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
6 changes: 4 additions & 2 deletions config/local.example.json
Original file line number Diff line number Diff line change
Expand Up @@ -90,11 +90,13 @@
"areaRestrictions": [
{
"roles": [],
"areas": []
"areas": [],
"parent": []
},
{
"roles": [],
"areas": []
"areas": [],
"parent": []
}
],
"aliases": [
Expand Down
3 changes: 2 additions & 1 deletion packages/config/lib/mutations.js
Original file line number Diff line number Diff line change
Expand Up @@ -307,9 +307,10 @@ const applyMutations = (config) => {
})

config.authentication.areaRestrictions =
config.authentication.areaRestrictions.map(({ roles, areas }) => ({
config.authentication.areaRestrictions.map(({ roles, areas, parent }) => ({
roles: roles.flatMap(replaceAliases),
areas,
parent,
}))

config.authentication.strategies = config.authentication.strategies.map(
Expand Down
2 changes: 1 addition & 1 deletion packages/types/lib/config.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ export type Config<Client extends boolean = false> = DeepMerge<
}
areas: ConfigAreas
authentication: {
areaRestrictions: { roles: string[]; areas: string[] }[]
areaRestrictions: { roles: string[]; areas: string[]; parent?: string[] }[]
// Unfortunately these types are not convenient for looping the `perms` object...
// excludeFromTutorial: (keyof BaseConfig['authentication']['perms'])[]
// alwaysEnabledPerms: (keyof BaseConfig['authentication']['perms'])[]
Expand Down
71 changes: 34 additions & 37 deletions server/src/graphql/resolvers.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ const { getPolyVector } = require('../utils/getPolyVector')
const { getPlacementCells } = require('../utils/getPlacementCells')
const { getTypeCells } = require('../utils/getTypeCells')
const { getValidCoords } = require('../utils/getValidCoords')
const { hasUnrestrictedAreaGrant } = require('../utils/areaPerms')
const {
getAccessibleScanAreasMenu,
} = require('../utils/getAccessibleScanAreasMenu')

/** @type {import("@apollo/server").ApolloServerOptions<import("@rm/types").GqlContext>['resolvers']} */
const resolvers = {
Expand Down Expand Up @@ -355,54 +359,47 @@ const resolvers = {
scanAreas: (_, _args, { req, perms }) => {
if (perms?.scanAreas) {
const scanAreas = config.getAreas(req, 'scanAreas')
const unrestrictedAreaGrant = hasUnrestrictedAreaGrant(
perms.areaRestrictions,
)
const parentKeyByName = Object.fromEntries(
scanAreas.features
.filter(
(feature) =>
!feature.properties.parent &&
feature.properties.name &&
feature.properties.key,
)
.map((feature) => [
feature.properties.name,
feature.properties.key,
]),
)
const canAccessArea = (properties) =>
unrestrictedAreaGrant ||
!perms.areaRestrictions.length ||
perms.areaRestrictions.includes(properties.key) ||
perms.areaRestrictions.includes(properties.name) ||
(!!properties.parent &&
(perms.areaRestrictions.includes(
parentKeyByName[properties.parent],
) ||
perms.areaRestrictions.includes(properties.parent)))

return [
{
...scanAreas,
features: scanAreas.features.filter(
(feature) =>
!feature.properties.hidden &&
(!perms.areaRestrictions.length ||
perms.areaRestrictions.includes(feature.properties.name) ||
perms.areaRestrictions.includes(feature.properties.parent)),
!feature.properties.hidden && canAccessArea(feature.properties),
),
},
]
}
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)

// // 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 []
},
scanAreasMenu: (_, _args, { req, perms }) =>
getAccessibleScanAreasMenu(req, perms),
scannerConfig: (_, { mode }, { perms }) => {
const scanner = config.getSafe('scanner')
const modeConfig = scanner[mode]
Expand Down
12 changes: 11 additions & 1 deletion server/src/middleware/apollo.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ const { parse } = require('graphql')
const { state } = require('../services/state')
const { version } = require('../../../package.json')
const { DataLimitCheck } = require('../services/DataLimitCheck')
const { normalizeAreaRestrictions } = require('../utils/areaPerms')

/**
*
Expand All @@ -16,7 +17,16 @@ const { DataLimitCheck } = require('../services/DataLimitCheck')
function apolloMiddleware(server) {
return expressMiddleware(server, {
context: async ({ req, res }) => {
const perms = req.user ? req.user.perms : req.session.perms
const rawPerms = req.user ? req.user.perms : req.session.perms
const perms = rawPerms
? {
...rawPerms,
areaRestrictions: normalizeAreaRestrictions(
rawPerms.areaRestrictions || [],
req,
),
}
: rawPerms
const username = req?.user?.username || ''
const id = req?.user?.id || 0

Expand Down
7 changes: 7 additions & 0 deletions server/src/middleware/passport.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// @ts-check

const passport = require('passport')
const { normalizeAreaRestrictions } = require('../utils/areaPerms')

/**
*
Expand All @@ -16,6 +17,12 @@ passport.serializeUser(async (user, done) => {
})

passport.deserializeUser(async (user, done) => {
if (Array.isArray(user?.perms?.areaRestrictions)) {
user.perms.areaRestrictions = normalizeAreaRestrictions(
user.perms.areaRestrictions,
)
}

if (user.perms.map) {
done(null, user)
} else {
Expand Down
28 changes: 20 additions & 8 deletions server/src/models/Weather.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ const config = require('@rm/config')

const { getPolyVector } = require('../utils/getPolyVector')
const { getPolygonBbox } = require('../utils/getBbox')
const { consolidateAreas } = require('../utils/consolidateAreas')
const { hasUnrestrictedAreaGrant } = require('../utils/areaPerms')

class Weather extends Model {
static get tableName() {
Expand Down Expand Up @@ -42,14 +44,24 @@ class Weather extends Model {
const results = await query

const areas = config.getSafe('areas')
const cleanUserAreas = (args.filters.onlyAreas || []).filter((area) =>
areas.names.has(area),
const unrestrictedAreaGrant = hasUnrestrictedAreaGrant(
perms.areaRestrictions,
)
const merged = perms.areaRestrictions.length
? perms.areaRestrictions.filter(
(area) => !cleanUserAreas.length || cleanUserAreas.includes(area),
)
: cleanUserAreas
const hasAreaFilter =
(!unrestrictedAreaGrant && perms.areaRestrictions.length) ||
(args.filters.onlyAreas || []).length
const merged = hasAreaFilter
? [
...consolidateAreas(
unrestrictedAreaGrant ? [] : perms.areaRestrictions,
args.filters.onlyAreas,
),
]
: []

if (hasAreaFilter && !merged.length) {
return []
}

const boundPolygon = getPolygonBbox(args)
return results
Expand All @@ -61,7 +73,7 @@ class Weather extends Model {
(pointInPolygon(center, boundPolygon) ||
booleanOverlap(geojson, boundPolygon) ||
booleanContains(geojson, boundPolygon)) &&
(!merged.length ||
(!hasAreaFilter ||
merged.some(
(area) =>
areas.scanAreasObj[area] &&
Expand Down
2 changes: 1 addition & 1 deletion server/src/routes/rootRouter.js
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,7 @@ rootRouter.get('/api/settings', async (req, res, next) => {
...Object.fromEntries(
Object.keys(authentication.perms).map((p) => [p, false]),
),
areaRestrictions: areaPerms(['none']),
areaRestrictions: areaPerms(['none'], req, true),
webhooks: [],
scanner: Object.keys(scanner).filter(
(key) =>
Expand Down
10 changes: 6 additions & 4 deletions server/src/services/DiscordClient.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ const passport = require('passport')
const config = require('@rm/config')

const { logUserAuth } = require('./logUserAuth')
const { areaPerms } = require('../utils/areaPerms')
const { resolveAreaPerms } = require('../utils/areaPerms')
const { webhookPerms } = require('../utils/webhookPerms')
const { scannerPerms, scannerCooldownBypass } = require('../utils/scannerPerms')
const { mergePerms } = require('../utils/mergePerms')
Expand Down Expand Up @@ -125,9 +125,10 @@ class DiscordClient extends AuthClient {
/**
*
* @param {import('passport-discord').Profile} user
* @param {import('express').Request} req
* @returns {Promise<import("@rm/types").Permissions>}
*/
async getPerms(user) {
async getPerms(user, req) {
const trialActive = this.trialManager.active()
/** @type {import("@rm/types").Permissions} */
// @ts-ignore
Expand Down Expand Up @@ -205,7 +206,8 @@ class DiscordClient extends AuthClient {
}
}
})
areaPerms(userRoles).forEach((x) =>
const guildAreaPerms = resolveAreaPerms(userRoles, req, true)
guildAreaPerms.areaRestrictions.forEach((x) =>
permSets.areaRestrictions.add(x),
)
webhookPerms(userRoles, 'discordRoles', trialActive).forEach(
Expand Down Expand Up @@ -278,7 +280,7 @@ class DiscordClient extends AuthClient {
username: profile.username,
avatar: profile.avatar || '',
locale: profile.locale,
perms: await this.getPerms(profile),
perms: await this.getPerms(profile, req),
rmStrategy: this.rmStrategy,
valid: false,
}
Expand Down
4 changes: 2 additions & 2 deletions server/src/services/LocalClient.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ class LocalClient extends AuthClient {
}

/** @type {import('passport-local').VerifyFunctionWithRequest} */
async authHandler(_req, username, password, done) {
async authHandler(req, username, password, done) {
const forceTutorial = config.getSafe('map.misc.forceTutorial')
const trialActive = this.trialManager.active()
const localPerms = Object.keys(this.perms).filter((key) =>
Expand All @@ -44,7 +44,7 @@ class LocalClient extends AuthClient {
const user = {
perms: /** @type {import('@rm/types').Permissions} */ ({
...Object.fromEntries(Object.keys(this.perms).map((x) => [x, false])),
areaRestrictions: areaPerms(localPerms),
areaRestrictions: areaPerms(localPerms, req, true),
webhooks: [],
scanner: [],
scannerCooldownBypass: [],
Expand Down
7 changes: 4 additions & 3 deletions server/src/services/TelegramClient.js
Original file line number Diff line number Diff line change
Expand Up @@ -62,9 +62,10 @@ class TelegramClient extends AuthClient {
*
* @param {TGUser} user
* @param {string[]} groups
* @param {import('express').Request} req
* @returns {TGUser & { perms: import("@rm/types").Permissions }}
*/
getUserPerms(user, groups) {
getUserPerms(user, groups, req) {
const trialActive = this.trialManager.active()
let gainedAccessViaTrial = false

Expand Down Expand Up @@ -99,7 +100,7 @@ class TelegramClient extends AuthClient {
...perms,
trial: gainedAccessViaTrial,
admin: false,
areaRestrictions: areaPerms(groups),
areaRestrictions: areaPerms(groups, req, true),
webhooks: webhookPerms(groups, 'telegramGroups', trialActive),
scanner: scannerPerms(groups, 'telegramGroups', trialActive),
scannerCooldownBypass: scannerCooldownBypass(groups, 'telegramGroups'),
Expand All @@ -124,7 +125,7 @@ class TelegramClient extends AuthClient {
async authHandler(req, profile, done) {
const baseUser = { ...profile, rmStrategy: this.rmStrategy }
const groups = await this.getUserGroups(baseUser)
const user = this.getUserPerms(baseUser, groups)
const user = this.getUserPerms(baseUser, groups, req)

if (!user.perms.map) {
this.log.warn(user.username, 'was not given map perms')
Expand Down
17 changes: 12 additions & 5 deletions server/src/services/logUserAuth.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
// @ts-check
const { default: fetch } = require('node-fetch')
const { log, TAGS } = require('@rm/logger')
const {
getPublicAreaRestrictions,
normalizeAreaRestrictions,
} = require('../utils/areaPerms')

// PII fields inside getAuthInfo embed
const PII_FIELDS = [
Expand Down Expand Up @@ -155,16 +159,19 @@ async function logUserAuth(req, user, strategy = 'custom', hidePii = false) {
],
timestamp: new Date().toISOString(),
}
if (user.perms.areaRestrictions.length) {
const trimmed = user.perms.areaRestrictions
const publicAreaRestrictions = getPublicAreaRestrictions(
normalizeAreaRestrictions(user.perms.areaRestrictions || [], req),
)
if (publicAreaRestrictions.length) {
const trimmed = publicAreaRestrictions
.filter((_f, i) => i < 15)
.map((f) => capCamel(f))
.join('\n')
embed.fields.push({
name: `(${user.perms.areaRestrictions.length}) Area Restrictions`,
name: `(${publicAreaRestrictions.length}) Area Restrictions`,
value:
user.perms.areaRestrictions.length > 15
? `${trimmed}\n...${user.perms.areaRestrictions.length - 15} more`
publicAreaRestrictions.length > 15
? `${trimmed}\n...${publicAreaRestrictions.length - 15} more`
: trimmed,
inline: true,
})
Expand Down
Loading
Loading