Files
BetterDesk/web-nodejs/services/deviceGroupService.js
T
UNITRONIX 87e9251d31 feat: Implement user group management and access control
- Added user group membership functionality, allowing users to be assigned to groups.
- Introduced validation for group GUIDs and enhanced error handling in user routes.
- Updated device group routes to support allowed user groups, enabling better access control for devices.
- Enhanced database schema to include user group memberships and device group user group access.
- Updated services and database adapters to handle user group data and relationships.
- Modified front-end views to display and manage user groups effectively.
- Added tests to ensure proper functionality of user group assignments and device access control.
2026-05-18 02:23:57 +02:00

201 lines
7.4 KiB
JavaScript

'use strict';
const { isSuperAdminRole } = require('../middleware/auth');
function normalizeTags(value) {
if (!value) return [];
if (Array.isArray(value)) return value.map(String).map(t => t.trim()).filter(Boolean);
if (typeof value === 'string') {
try {
const parsed = JSON.parse(value);
if (Array.isArray(parsed)) return parsed.map(String).map(t => t.trim()).filter(Boolean);
} catch (_) {}
return value.split(',').map(t => t.trim()).filter(Boolean);
}
return [];
}
function normalizeUsernames(value) {
const raw = Array.isArray(value) ? value : String(value || '').split(',');
return Array.from(new Set(raw.map(v => String(v || '').trim()).filter(Boolean))).slice(0, 100);
}
function normalizeGroupGuids(value) {
const raw = Array.isArray(value) ? value : String(value || '').split(',');
return Array.from(new Set(raw.map(item => {
if (item && typeof item === 'object') return String(item.guid || '').trim();
return String(item || '').trim();
}).filter(Boolean))).slice(0, 100);
}
function normalizeGroupPayload(body = {}) {
const sourceType = body.source_type === 'tag' || body.dynamic === true ? 'tag' : 'manual';
const tagFilter = sourceType === 'tag' ? String(body.tag_filter || body.tag || '').trim().slice(0, 50) : '';
return {
guid: body.guid ? String(body.guid).trim().slice(0, 64) : '',
name: String(body.name || '').trim().slice(0, 80),
note: String(body.note || '').trim().slice(0, 512),
team_id: String(body.team_id || '').trim().slice(0, 64),
source_type: sourceType,
tag_filter: tagFilter,
allowed_users: normalizeUsernames(body.allowed_users),
allowed_groups: normalizeGroupGuids(body.allowed_groups || body.allowed_user_groups || body.user_group_guids)
};
}
function hasTag(device, tag) {
const expected = String(tag || '').trim().toLowerCase();
if (!expected) return false;
return normalizeTags(device && device.tags).some(t => t.toLowerCase() === expected);
}
function folderIdFromGroupGuid(value) {
const match = String(value || '').trim().match(/^folder_(\d+)$/i);
if (!match) return null;
const id = Number.parseInt(match[1], 10);
return Number.isFinite(id) ? id : null;
}
function getGroupFolderId(group) {
const explicit = group && group.folder_id;
if (explicit !== undefined && explicit !== null && explicit !== '') {
const id = Number.parseInt(explicit, 10);
if (Number.isFinite(id)) return id;
}
return folderIdFromGroupGuid(group && group.guid);
}
function groupAllowedForUser(group, user) {
if (!user || isSuperAdminRole(user.role) || user.role === 'global_admin' || user.role === 'server_admin') {
return true;
}
const allowedUsers = normalizeUsernames(group && group.allowed_users);
const allowedGroups = normalizeGroupGuids(group && (group.allowed_groups || group.allowed_user_groups));
if (allowedUsers.length === 0 && allowedGroups.length === 0) return true;
if (allowedUsers.includes(user.username)) return true;
const userGroups = new Set(normalizeGroupGuids(user.user_groups || user.group_guids || user.allowed_groups));
return allowedGroups.some(guid => userGroups.has(guid));
}
async function getUserAccessContext(db, user) {
if (!user || !user.id || isSuperAdminRole(user.role) || user.role === 'global_admin' || user.role === 'server_admin') {
return user;
}
if (Array.isArray(user.user_groups) || typeof db.getUserGroupsForUser !== 'function') return user;
try {
const groups = await db.getUserGroupsForUser(user.id);
return {
...user,
user_groups: (groups || []).map(group => group.guid).filter(Boolean)
};
} catch (_) {
return user;
}
}
async function getGroupPeerIds(db, group, devices = []) {
const ids = new Set();
const folderId = getGroupFolderId(group);
if (folderId !== null) {
for (const device of devices || []) {
const deviceFolderId = Number.parseInt(device && device.folder_id, 10);
if (Number.isFinite(deviceFolderId) && deviceFolderId === folderId) {
ids.add(String(device.id));
}
}
return ids;
}
if (group && group.guid) {
try {
const staticIds = await db.getDeviceGroupMembers(group.guid);
for (const id of staticIds || []) ids.add(String(id));
} catch (_) {}
}
if ((group.source_type || 'manual') === 'tag' && group.tag_filter) {
for (const device of devices || []) {
if (hasTag(device, group.tag_filter)) ids.add(String(device.id));
}
}
return ids;
}
async function enrichGroups(db, groups, devices = []) {
const enriched = [];
for (const group of groups || []) {
const memberIds = await getGroupPeerIds(db, group, devices);
enriched.push({
...group,
source_type: group.source_type || 'manual',
tag_filter: group.tag_filter || '',
allowed_users: Array.isArray(group.allowed_users) ? group.allowed_users : normalizeUsernames(group.allowed_users),
allowed_groups: Array.isArray(group.allowed_groups) ? group.allowed_groups : normalizeGroupGuids(group.allowed_groups),
allowed_user_groups: Array.isArray(group.allowed_user_groups) ? group.allowed_user_groups : [],
member_count: memberIds.size
});
}
return enriched;
}
async function getDeviceScopeForUser(db, user, devices = []) {
if (!user || !user.id || isSuperAdminRole(user.role) || user.role === 'global_admin' || user.role === 'server_admin') {
return null;
}
if (typeof db.getAllDeviceGroups !== 'function') return null;
const accessUser = await getUserAccessContext(db, user);
const groups = await db.getAllDeviceGroups();
const restrictedGroups = (groups || []).filter(group =>
normalizeUsernames(group.allowed_users).length > 0 ||
normalizeGroupGuids(group.allowed_groups || group.allowed_user_groups).length > 0
);
if (restrictedGroups.length === 0) return null;
const allowedIds = new Set();
const restrictedIds = new Set();
for (const group of restrictedGroups) {
const ids = await getGroupPeerIds(db, group, devices);
const target = groupAllowedForUser(group, accessUser) ? allowedIds : restrictedIds;
for (const id of ids) target.add(id);
}
const visible = new Set();
for (const device of devices || []) {
const id = String(device && device.id || '');
if (!id) continue;
if (!restrictedIds.has(id) || allowedIds.has(id)) visible.add(id);
}
return visible;
}
function filterDevicesByScope(devices, allowedIds) {
if (!allowedIds) return devices;
return (devices || []).filter(device => allowedIds.has(String(device.id)));
}
async function userCanAccessDevice(db, user, device, allDevices) {
const scope = await getDeviceScopeForUser(db, user, allDevices || (device ? [device] : []));
if (!scope) return true;
return device && scope.has(String(device.id));
}
module.exports = {
normalizeTags,
normalizeUsernames,
normalizeGroupGuids,
normalizeGroupPayload,
folderIdFromGroupGuid,
getGroupFolderId,
groupAllowedForUser,
getUserAccessContext,
getGroupPeerIds,
enrichGroups,
getDeviceScopeForUser,
filterDevicesByScope,
userCanAccessDevice
};