diff --git a/client/src/components/Layout.jsx b/client/src/components/Layout.jsx index fcf73c8..b6ce03b 100644 --- a/client/src/components/Layout.jsx +++ b/client/src/components/Layout.jsx @@ -3,6 +3,7 @@ import { Outlet, Link, useLocation } from 'react-router-dom'; import { useAuth } from '../contexts/AuthContext'; import { useSocket } from '../contexts/SocketContext'; import DebugToggle from './DebugToggle'; +import { canAccessSettings, hasPermission } from '../utils/rbac'; import { HomeIcon, MapIcon, @@ -27,25 +28,31 @@ const baseNavigation = [ { name: 'Alerts', href: '/alerts', icon: BellIcon }, ]; -const adminNavigation = [ - { name: 'Settings', href: '/settings', icon: CogIcon }, - { name: 'Debug', href: '/debug', icon: BugAntIcon }, -]; - const Layout = () => { const [sidebarOpen, setSidebarOpen] = useState(false); const { user, logout } = useAuth(); const { connected, recentDetections } = useSocket(); const location = useLocation(); - // Build navigation based on user role - ensure it's always an array + // Build navigation based on user permissions const navigation = React.useMemo(() => { if (!user) { return baseNavigation; // Return base navigation if user not loaded yet } - return user.role === 'admin' - ? [...baseNavigation, ...adminNavigation] - : baseNavigation; + + const nav = [...baseNavigation]; + + // Add Settings if user has any settings permissions + if (canAccessSettings(user.role)) { + nav.push({ name: 'Settings', href: '/settings', icon: CogIcon }); + } + + // Add Debug if user has debug permissions + if (hasPermission(user.role, 'debug.access')) { + nav.push({ name: 'Debug', href: '/debug', icon: BugAntIcon }); + } + + return nav; }, [user?.role]); return ( diff --git a/client/src/pages/Settings.jsx b/client/src/pages/Settings.jsx index fa29fca..6fa9676 100644 --- a/client/src/pages/Settings.jsx +++ b/client/src/pages/Settings.jsx @@ -12,6 +12,7 @@ import { EyeIcon, EyeSlashIcon } from '@heroicons/react/24/outline'; +import { hasPermission, canAccessSettings } from '../utils/rbac'; const Settings = () => { const { user } = useAuth(); @@ -20,8 +21,8 @@ const Settings = () => { const [loading, setLoading] = useState(true); const [saving, setSaving] = useState(false); - // Check if user has admin role - const isAdmin = user?.role === 'admin'; + // Check if user can access settings based on RBAC permissions + const canAccess = canAccessSettings(user?.role); useEffect(() => { fetchTenantConfig(); @@ -48,27 +49,60 @@ const Settings = () => { ); } - if (!isAdmin) { + if (!canAccess) { return (

Access Denied

- You need admin privileges to access tenant settings. + You don't have permission to access tenant settings.

); } - const tabs = [ - { id: 'general', name: 'General', icon: CogIcon }, - { id: 'branding', name: 'Branding', icon: PaintBrushIcon }, - { id: 'security', name: 'Security', icon: ShieldCheckIcon }, - { id: 'authentication', name: 'Authentication', icon: KeyIcon }, - { id: 'users', name: 'Users', icon: UserGroupIcon }, - ]; + // Filter tabs based on user permissions + const availableTabs = [ + { + id: 'general', + name: 'General', + icon: CogIcon, + permission: 'tenant.view' + }, + { + id: 'branding', + name: 'Branding', + icon: PaintBrushIcon, + permission: 'branding.view' + }, + { + id: 'security', + name: 'Security', + icon: ShieldCheckIcon, + permission: 'security.view' + }, + { + id: 'authentication', + name: 'Authentication', + icon: KeyIcon, + permission: 'auth.view' + }, + { + id: 'users', + name: 'Users', + icon: UserGroupIcon, + permission: 'users.view' + }, + ].filter(tab => hasPermission(user?.role, tab.permission)); + + // Set initial tab to first available tab + useEffect(() => { + if (availableTabs.length > 0 && !availableTabs.find(tab => tab.id === activeTab)) { + setActiveTab(availableTabs[0].id); + } + }, [availableTabs, activeTab]); return (
@@ -81,7 +115,7 @@ const Settings = () => {
- {activeTab === 'general' && } - {activeTab === 'branding' && } - {activeTab === 'security' && } - {activeTab === 'authentication' && } - {activeTab === 'users' && } + {activeTab === 'general' && hasPermission(user?.role, 'tenant.view') && ( + + )} + {activeTab === 'branding' && hasPermission(user?.role, 'branding.view') && ( + + )} + {activeTab === 'security' && hasPermission(user?.role, 'security.view') && ( + + )} + {activeTab === 'authentication' && hasPermission(user?.role, 'auth.view') && ( + + )} + {activeTab === 'users' && hasPermission(user?.role, 'users.view') && ( + + )}
@@ -141,6 +185,7 @@ const GeneralSettings = ({ tenantConfig }) => ( // Branding Settings Component const BrandingSettings = ({ tenantConfig, onRefresh }) => { + const { user } = useAuth(); const [branding, setBranding] = useState({ logo_url: '', primary_color: '#3B82F6', @@ -151,6 +196,8 @@ const BrandingSettings = ({ tenantConfig, onRefresh }) => { const [uploading, setUploading] = useState(false); const [logoPreview, setLogoPreview] = useState(null); + const canEdit = hasPermission(user?.role, 'branding.edit'); + useEffect(() => { if (tenantConfig?.branding) { setBranding(tenantConfig.branding); @@ -231,7 +278,8 @@ const BrandingSettings = ({ tenantConfig, onRefresh }) => { type="text" value={branding.company_name} onChange={(e) => setBranding(prev => ({ ...prev, company_name: e.target.value }))} - className="mt-1 block w-full border-gray-300 rounded-md shadow-sm focus:ring-primary-500 focus:border-primary-500" + disabled={!canEdit} + className="mt-1 block w-full border-gray-300 rounded-md shadow-sm focus:ring-primary-500 focus:border-primary-500 disabled:bg-gray-100 disabled:cursor-not-allowed" /> @@ -259,12 +307,12 @@ const BrandingSettings = ({ tenantConfig, onRefresh }) => { { handleFilePreview(e); handleLogoUpload(e); }} - className="block w-full text-sm text-gray-500 file:mr-4 file:py-2 file:px-4 file:rounded-md file:border-0 file:text-sm file:font-medium file:bg-primary-50 file:text-primary-700 hover:file:bg-primary-100" - disabled={uploading} + className="block w-full text-sm text-gray-500 file:mr-4 file:py-2 file:px-4 file:rounded-md file:border-0 file:text-sm file:font-medium file:bg-primary-50 file:text-primary-700 hover:file:bg-primary-100 disabled:opacity-50" />

PNG, JPG up to 5MB

@@ -296,7 +344,8 @@ const BrandingSettings = ({ tenantConfig, onRefresh }) => { type="url" value={branding.logo_url} onChange={(e) => setBranding(prev => ({ ...prev, logo_url: e.target.value }))} - className="mt-1 block w-full border-gray-300 rounded-md shadow-sm focus:ring-primary-500 focus:border-primary-500" + disabled={!canEdit} + className="mt-1 block w-full border-gray-300 rounded-md shadow-sm focus:ring-primary-500 focus:border-primary-500 disabled:bg-gray-100 disabled:cursor-not-allowed" placeholder="https://example.com/logo.png" /> @@ -310,13 +359,15 @@ const BrandingSettings = ({ tenantConfig, onRefresh }) => { type="color" value={branding.primary_color} onChange={(e) => setBranding(prev => ({ ...prev, primary_color: e.target.value }))} - className="h-10 w-20 border border-gray-300 rounded-md" + disabled={!canEdit} + className="h-10 w-20 border border-gray-300 rounded-md disabled:opacity-50" /> setBranding(prev => ({ ...prev, primary_color: e.target.value }))} - className="ml-2 block w-full border-gray-300 rounded-md shadow-sm focus:ring-primary-500 focus:border-primary-500" + disabled={!canEdit} + className="ml-2 block w-full border-gray-300 rounded-md shadow-sm focus:ring-primary-500 focus:border-primary-500 disabled:bg-gray-100 disabled:cursor-not-allowed" /> @@ -328,26 +379,34 @@ const BrandingSettings = ({ tenantConfig, onRefresh }) => { type="color" value={branding.secondary_color} onChange={(e) => setBranding(prev => ({ ...prev, secondary_color: e.target.value }))} - className="h-10 w-20 border border-gray-300 rounded-md" + disabled={!canEdit} + className="h-10 w-20 border border-gray-300 rounded-md disabled:opacity-50" /> setBranding(prev => ({ ...prev, secondary_color: e.target.value }))} - className="ml-2 block w-full border-gray-300 rounded-md shadow-sm focus:ring-primary-500 focus:border-primary-500" + disabled={!canEdit} + className="ml-2 block w-full border-gray-300 rounded-md shadow-sm focus:ring-primary-500 focus:border-primary-500 disabled:bg-gray-100 disabled:cursor-not-allowed" />
- + {canEdit ? ( + + ) : ( +
+ You don't have permission to edit branding settings +
+ )}
@@ -357,6 +416,7 @@ const BrandingSettings = ({ tenantConfig, onRefresh }) => { // Placeholder components for other tabs const SecuritySettings = ({ tenantConfig, onRefresh }) => { + const { user } = useAuth(); const [securitySettings, setSecuritySettings] = useState({ ip_restriction_enabled: false, ip_whitelist: [], @@ -365,6 +425,8 @@ const SecuritySettings = ({ tenantConfig, onRefresh }) => { const [newIP, setNewIP] = useState(''); const [saving, setSaving] = useState(false); + const canEdit = hasPermission(user?.role, 'security.edit'); + useEffect(() => { if (tenantConfig) { setSecuritySettings({ @@ -432,7 +494,7 @@ const SecuritySettings = ({ tenantConfig, onRefresh }) => {

Security Settings

- + {canEditUsers && ( + + )} + {canDeleteUsers && ( + + )} )} @@ -1205,7 +1287,7 @@ const UsersSettings = ({ tenantConfig, onRefresh }) => { )} {/* Non-Local Auth Guidance */} - {!canManageUsers && ( + {!isLocalAuth && (

User Management for {authProvider?.toUpperCase()} @@ -1238,7 +1320,7 @@ const UsersSettings = ({ tenantConfig, onRefresh }) => {

{/* Create User Modal for Local Auth */} - {showCreateUser && canManageUsers && ( + {showCreateUser && canCreateUsers && ( setShowCreateUser(false)} @@ -1250,7 +1332,7 @@ const UsersSettings = ({ tenantConfig, onRefresh }) => { )} {/* Edit User Modal for Local Auth */} - {showEditUser && canManageUsers && editingUser && ( + {showEditUser && canEditUsers && editingUser && ( { diff --git a/client/src/utils/rbac.js b/client/src/utils/rbac.js new file mode 100644 index 0000000..9046fd4 --- /dev/null +++ b/client/src/utils/rbac.js @@ -0,0 +1,170 @@ +/** + * Frontend RBAC Utility + * Client-side permission checking to match server-side RBAC system + */ + +// Define the same permissions as the server +export const PERMISSIONS = { + // General tenant management + 'tenant.view': 'View tenant information', + 'tenant.edit': 'Edit basic tenant settings', + + // Branding permissions + 'branding.view': 'View branding settings', + 'branding.edit': 'Edit branding and appearance', + + // Security permissions + 'security.view': 'View security settings', + 'security.edit': 'Edit security settings and IP restrictions', + + // User management permissions + 'users.view': 'View user list', + 'users.create': 'Create new users', + 'users.edit': 'Edit user details', + 'users.delete': 'Delete or deactivate users', + 'users.manage_roles': 'Change user roles', + + // Authentication permissions + 'auth.view': 'View authentication settings', + 'auth.edit': 'Edit authentication provider settings', + + // Operational permissions + 'dashboard.view': 'View dashboard', + 'devices.view': 'View devices', + 'devices.manage': 'Add, edit, delete devices', + 'detections.view': 'View detections', + 'alerts.view': 'View alerts', + 'alerts.manage': 'Manage alert configurations', + 'debug.access': 'Access debug information' +}; + +// Define roles and their permissions (must match server-side) +export const ROLES = { + // Full tenant administrator + 'admin': [ + 'tenant.view', 'tenant.edit', + 'branding.view', 'branding.edit', + 'security.view', 'security.edit', + 'users.view', 'users.create', 'users.edit', 'users.delete', 'users.manage_roles', + 'auth.view', 'auth.edit', + 'dashboard.view', + 'devices.view', 'devices.manage', + 'detections.view', + 'alerts.view', 'alerts.manage', + 'debug.access' + ], + + // User management specialist + 'user_admin': [ + 'tenant.view', + 'users.view', 'users.create', 'users.edit', 'users.delete', 'users.manage_roles', + 'dashboard.view', + 'devices.view', + 'detections.view', + 'alerts.view' + ], + + // Security specialist + 'security_admin': [ + 'tenant.view', + 'security.view', 'security.edit', + 'auth.view', 'auth.edit', + 'users.view', + 'dashboard.view', + 'devices.view', + 'detections.view', + 'alerts.view' + ], + + // Branding/marketing specialist + 'branding_admin': [ + 'tenant.view', + 'branding.view', 'branding.edit', + 'dashboard.view', + 'devices.view', + 'detections.view', + 'alerts.view' + ], + + // Operations manager + 'operator': [ + 'tenant.view', + 'dashboard.view', + 'devices.view', 'devices.manage', + 'detections.view', + 'alerts.view', 'alerts.manage' + ], + + // Read-only user + 'viewer': [ + 'dashboard.view', + 'devices.view', + 'detections.view', + 'alerts.view' + ] +}; + +/** + * Check if a user has a specific permission + * @param {string} userRole - The user's role + * @param {string} permission - The permission to check + * @returns {boolean} True if the user has the permission + */ +export function hasPermission(userRole, permission) { + if (!userRole || !permission) return false; + + const rolePermissions = ROLES[userRole]; + if (!rolePermissions) return false; + + return rolePermissions.includes(permission); +} + +/** + * Check if a user has any of the provided permissions + * @param {string} userRole - The user's role + * @param {string[]} permissions - Array of permissions to check + * @returns {boolean} True if the user has at least one permission + */ +export function hasAnyPermission(userRole, permissions) { + if (!userRole || !permissions || !Array.isArray(permissions)) return false; + + return permissions.some(permission => hasPermission(userRole, permission)); +} + +/** + * Check if a user has all of the provided permissions + * @param {string} userRole - The user's role + * @param {string[]} permissions - Array of permissions to check + * @returns {boolean} True if the user has all permissions + */ +export function hasAllPermissions(userRole, permissions) { + if (!userRole || !permissions || !Array.isArray(permissions)) return false; + + return permissions.every(permission => hasPermission(userRole, permission)); +} + +/** + * Get all permissions for a role + * @param {string} userRole - The user's role + * @returns {string[]} Array of permissions for the role + */ +export function getRolePermissions(userRole) { + if (!userRole) return []; + + return ROLES[userRole] || []; +} + +/** + * Check if user can access settings at all + * @param {string} userRole - The user's role + * @returns {boolean} True if user can access any settings + */ +export function canAccessSettings(userRole) { + return hasAnyPermission(userRole, [ + 'tenant.view', + 'branding.view', + 'security.view', + 'auth.view', + 'users.view' + ]); +}