Files
BetterDesk/web-nodejs/middleware/auth.js
T
UNITRONIX bb3da6b801 feat: implement RdClient login functionality and enhance authentication flow
- Added a dedicated login page for the Remote Desktop client, allowing operators to sign in when their session expires.
- Implemented safe return URL handling for the login process.
- Updated authentication middleware to support RdClient-specific routes and permissions.
- Enhanced localization support for the new login feature across multiple languages.
- Improved user experience by redirecting to the appropriate pages based on authentication status.
2026-06-14 17:28:59 +02:00

333 lines
11 KiB
JavaScript

/**
* BetterDesk Console - Auth Middleware
* Protects routes that require authentication
*/
// Default role-permission map (mirrors Go auth/permissions.go — Phase 52+)
//
// Role hierarchy (branched, not strictly linear):
// super_admin — full server + all-org access
// ├── server_admin — server config/logs, read-only user visibility
// ├── global_admin — all-org user/device management, no server access
// └── admin — legacy alias for super_admin
// operator, viewer, pro — unchanged (pro = PRO activation only, no devices)
const DEFAULT_ROLE_PERMISSIONS = {
super_admin: null, // null = ALL permissions
admin: null, // legacy admin = super_admin
// Server Admin: infrastructure + read-only users
server_admin: new Set([
'server.config', 'server.keys', 'server.attestation',
'blocklist.edit',
'user.view',
'device.view',
'audit.view', 'metrics.view',
'enrollment.manage',
]),
// Global Admin: all user/org management, NO server config
global_admin: new Set([
'user.view', 'user.create', 'user.edit', 'user.delete',
'org.create', 'org.edit', 'org.delete', 'org.manage_users', 'org.manage_devices',
'device.view', 'device.connect', 'device.edit', 'device.delete',
'device.ban', 'device.change_id',
'audit.view', 'metrics.view',
'cdap.view', 'cdap.command',
'chat.access',
'enrollment.manage', 'enrollment.approve',
'branding.edit',
'billing.view', 'billing.manage', 'billing.reports', 'billing.export',
]),
operator: new Set([
'device.view', 'device.connect', 'device.edit',
'user.view',
'audit.view', 'metrics.view',
'cdap.view', 'cdap.command',
'enrollment.approve',
'chat.access',
'org.manage_devices',
'billing.view', 'billing.reports',
]),
viewer: new Set([
'device.view',
'audit.view', 'metrics.view',
'cdap.view',
'chat.access',
]),
pro: new Set(),
};
function isProRole(role) {
return role === 'pro';
}
function proRoleBlocksPermission(role, permission) {
if (!isProRole(role)) return false;
return permission.startsWith('device.') || permission === 'org.manage_devices';
}
// Roles that have full admin privileges (bypass all permission checks).
const SUPER_ADMIN_ROLES = new Set(['super_admin', 'admin']);
/**
* Check if a role is a super admin (or legacy admin).
*/
function isSuperAdminRole(role) {
return SUPER_ADMIN_ROLES.has(role);
}
/**
* Check if a role has a specific permission by default.
* @param {string} role
* @param {string} permission
* @returns {boolean}
*/
function roleHasPermission(role, permission) {
if (proRoleBlocksPermission(role, permission)) return false;
if (isSuperAdminRole(role)) return true;
const perms = DEFAULT_ROLE_PERMISSIONS[role];
if (!perms) return false;
return perms.has(permission);
}
/**
* Safe relative return URL for RdClient pages only (/remote…).
*/
function isSafeRdClientReturnUrl(u) {
if (typeof u !== 'string' || u.length === 0) return false;
if (/[\r\n\x00]/.test(u)) return false;
if (!u.startsWith('/remote')) return false;
if (u.startsWith('//') || u.startsWith('/\\')) return false;
if (u.startsWith('/remote/login')) return false;
return true;
}
function rdClientLoginRedirect(req) {
const requested = req.originalUrl || req.url || '/remote';
const returnParam = isSafeRdClientReturnUrl(requested)
? requested
: (isSafeRdClientReturnUrl(req.query.return) ? req.query.return : '/remote');
return `/remote/login?return=${encodeURIComponent(returnParam)}`;
}
/**
* RdClient auth — redirects to /remote/login instead of panel /login.
* @param {string} [permission] - optional permission (e.g. device.connect)
*/
function requireRdClientAuth(permission) {
return function (req, res, next) {
if (!req.session || !req.session.userId) {
if (req.path.startsWith('/api/')) {
return res.status(401).json({ success: false, error: 'Unauthorized. Please log in.' });
}
return res.redirect(rdClientLoginRedirect(req));
}
if (req.session.user && req.session.user.role === 'pro') {
req.session.destroy();
if (req.path.startsWith('/api/')) {
return res.status(403).json({
success: false,
error: 'Pro accounts can only access the RustDesk desktop client API'
});
}
return res.redirect(rdClientLoginRedirect(req));
}
if (permission) {
const role = req.session.user && req.session.user.role;
if (!roleHasPermission(role, permission)) {
if (req.path.startsWith('/api/')) {
return res.status(403).json({ success: false, error: `Permission denied: ${permission}` });
}
return res.status(403).render('errors/403', {
title: 'Forbidden',
message: 'You do not have permission to access this resource'
});
}
}
res.locals.user = req.session.user;
res.locals.emergencyMode = !!req.session.emergencyMode;
next();
};
}
/**
* RdClient login page — redirect away if already authenticated with device.connect.
*/
function rdClientGuestOnly(req, res, next) {
const role = req.session && req.session.user && req.session.user.role;
if (req.session && req.session.userId && role !== 'pro' && roleHasPermission(role, 'device.connect')) {
const dest = isSafeRdClientReturnUrl(req.query.return) ? req.query.return : '/remote';
return res.redirect(dest);
}
next();
}
/**
* Require authentication middleware
* Redirects to login page for HTML requests, returns 401 for API requests
*/
function requireAuth(req, res, next) {
if (!req.session || !req.session.userId) {
// API request
if (req.path.startsWith('/api/')) {
return res.status(401).json({
success: false,
error: 'Unauthorized. Please log in.'
});
}
// HTML request - redirect to login
return res.redirect('/login');
}
// Block pro-only users from web panel (API-only role)
if (req.session.user && req.session.user.role === 'pro') {
req.session.destroy();
if (req.path.startsWith('/api/')) {
return res.status(403).json({
success: false,
error: 'Pro accounts can only access the RustDesk desktop client API'
});
}
return res.redirect('/login');
}
// Add user info to locals for templates
res.locals.user = req.session.user;
// Phase A: expose emergency mode flag to EJS templates
res.locals.emergencyMode = !!req.session.emergencyMode;
next();
}
/**
* Require specific role
*/
function requireRole(role) {
return function(req, res, next) {
if (!req.session || !req.session.userId) {
if (req.path.startsWith('/api/')) {
return res.status(401).json({ success: false, error: 'Unauthorized' });
}
return res.redirect('/login');
}
const userRole = req.session.user && req.session.user.role;
// Super admin roles bypass all role checks.
if (isSuperAdminRole(userRole)) {
res.locals.user = req.session.user;
return next();
}
if (userRole !== role) {
if (req.path.startsWith('/api/')) {
return res.status(403).json({ success: false, error: 'Forbidden' });
}
return res.status(403).render('error', {
title: 'Forbidden',
message: 'You do not have permission to access this resource'
});
}
res.locals.user = req.session.user;
next();
};
}
/**
* Optional auth - add user to locals if logged in, but don't require it
*/
function optionalAuth(req, res, next) {
if (req.session && req.session.userId) {
res.locals.user = req.session.user;
} else {
res.locals.user = null;
}
next();
}
/**
* Guest only - redirect to dashboard if already logged in
*/
function guestOnly(req, res, next) {
if (req.session && req.session.userId) {
return res.redirect('/');
}
next();
}
/**
* Require admin role (super_admin, admin, or global_admin for user management)
*/
function requireAdmin(req, res, next) {
if (!req.session || !req.session.userId) {
if (req.path.startsWith('/api/')) {
return res.status(401).json({ success: false, error: 'Unauthorized' });
}
return res.redirect('/login');
}
const userRole = req.session.user && req.session.user.role;
if (!isSuperAdminRole(userRole) && userRole !== 'global_admin') {
if (req.path.startsWith('/api/')) {
return res.status(403).json({ success: false, error: 'Admin access required' });
}
return res.status(403).render('errors/403', {
title: 'Forbidden',
message: 'You do not have permission to access this resource'
});
}
res.locals.user = req.session.user;
next();
}
/**
* Require a specific granular permission (RBAC Phase 52).
* Uses the default role-permission map. Admin role always passes.
* @param {string} permission - e.g. 'device.view', 'user.edit'
*/
function requirePermission(permission) {
return function(req, res, next) {
if (!req.session || !req.session.userId) {
if (req.path.startsWith('/api/')) {
return res.status(401).json({ success: false, error: 'Unauthorized' });
}
return res.redirect('/login');
}
const role = req.session.user && req.session.user.role;
if (!roleHasPermission(role, permission)) {
if (req.path.startsWith('/api/')) {
return res.status(403).json({ success: false, error: `Permission denied: ${permission}` });
}
return res.status(403).render('errors/403', {
title: 'Forbidden',
message: 'You do not have permission to access this resource'
});
}
res.locals.user = req.session.user;
next();
};
}
module.exports = {
requireAuth,
requireRole,
requireAdmin,
requirePermission,
requireRdClientAuth,
rdClientGuestOnly,
isSafeRdClientReturnUrl,
optionalAuth,
guestOnly,
roleHasPermission,
isSuperAdminRole
};