Add page visibility by group with centralized matrix
Introduce a Page Visibility Matrix that controls which pages each user
group can access, enforced in both frontend and backend:
Frontend:
- Create frontend/src/config/pageVisibility.js with PAGE_VISIBILITY
matrix and canAccessPage() / getAccessiblePages() helpers
- NavDrawer: replace inline requiredGroups with canAccessPage() filter
- App.js: replace per-page isInGroup()/isAdmin() checks with generic
route guard in setCurrentPage; remove VALID_PAGES constant
- localStorage validation: verify persisted page is accessible on load
Backend (page-level access enforcement):
- jiraTickets.js: add router-level requireGroup('Admin','Standard_User')
- archerTemplates.js: add router-level requireGroup('Admin','Standard_User')
- VCL multi-vertical already had requireGroup('Admin','Leadership')
Visibility matrix:
- Home, Knowledge Base: all groups
- Triage, Compliance, Exports: Admin, Standard_User, Leadership
- CCP Metrics: Admin, Leadership
- Jira, Archer Templates: Admin, Standard_User
- Admin Panel: Admin only
- Read_Only sees only Home and Knowledge Base
This commit is contained in:
@@ -20,6 +20,10 @@ const SECTION_MAX_LENGTH = 10000;
|
|||||||
function createArcherTemplatesRouter() {
|
function createArcherTemplatesRouter() {
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
|
|
||||||
|
// All Archer template routes require authentication and Admin or Standard_User group (page-level access)
|
||||||
|
router.use(requireAuth());
|
||||||
|
router.use(requireGroup('Admin', 'Standard_User'));
|
||||||
|
|
||||||
// --- Hierarchy endpoints (MUST be defined before /:id to avoid route conflicts) ---
|
// --- Hierarchy endpoints (MUST be defined before /:id to avoid route conflicts) ---
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -30,6 +30,10 @@ function isValidVendor(vendor) {
|
|||||||
function createJiraTicketsRouter() {
|
function createJiraTicketsRouter() {
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
|
|
||||||
|
// All Jira routes require authentication and Admin or Standard_User group (page-level access)
|
||||||
|
router.use(requireAuth());
|
||||||
|
router.use(requireGroup('Admin', 'Standard_User'));
|
||||||
|
|
||||||
// -----------------------------------------------------------------------
|
// -----------------------------------------------------------------------
|
||||||
// Jira API integration endpoints
|
// Jira API integration endpoints
|
||||||
// -----------------------------------------------------------------------
|
// -----------------------------------------------------------------------
|
||||||
|
|||||||
@@ -19,20 +19,20 @@ import ArcherTemplatePage from './components/pages/ArcherTemplatePage';
|
|||||||
import HomePage from './components/pages/HomePage';
|
import HomePage from './components/pages/HomePage';
|
||||||
import FeedbackModal from './components/FeedbackModal';
|
import FeedbackModal from './components/FeedbackModal';
|
||||||
import NotificationBell from './components/NotificationBell';
|
import NotificationBell from './components/NotificationBell';
|
||||||
|
import { canAccessPage } from './config/pageVisibility';
|
||||||
import './App.css';
|
import './App.css';
|
||||||
|
|
||||||
const VALID_PAGES = new Set(['home', 'triage', 'compliance', 'knowledge-base', 'exports', 'jira', 'admin', 'archer-templates']);
|
|
||||||
|
|
||||||
export default function App() {
|
export default function App() {
|
||||||
const { isAuthenticated, loading: authLoading, canWrite, isAdmin, isInGroup } = useAuth();
|
const { isAuthenticated, loading: authLoading, canWrite, user } = useAuth();
|
||||||
|
|
||||||
const [currentPage, setCurrentPageRaw] = useState(() => {
|
const [currentPage, setCurrentPageRaw] = useState(() => {
|
||||||
try {
|
try {
|
||||||
const saved = localStorage.getItem('cve-dashboard-page');
|
const saved = localStorage.getItem('cve-dashboard-page');
|
||||||
return saved && VALID_PAGES.has(saved) ? saved : 'home';
|
return saved && canAccessPage(saved, user?.group) ? saved : 'home';
|
||||||
} catch { return 'home'; }
|
} catch { return 'home'; }
|
||||||
});
|
});
|
||||||
const setCurrentPage = (page) => {
|
const setCurrentPage = (page) => {
|
||||||
|
if (!canAccessPage(page, user?.group)) { setCurrentPageRaw('home'); return; }
|
||||||
setCurrentPageRaw(page);
|
setCurrentPageRaw(page);
|
||||||
try { localStorage.setItem('cve-dashboard-page', page); } catch {}
|
try { localStorage.setItem('cve-dashboard-page', page); } catch {}
|
||||||
};
|
};
|
||||||
@@ -160,18 +160,16 @@ export default function App() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Page content */}
|
{/* Page content — generic route guard via canAccessPage */}
|
||||||
{currentPage === 'home' && <HomePage onNavigate={handleNavigate} showAddCVE={showAddCVE} setShowAddCVE={setShowAddCVE} />}
|
{currentPage === 'home' && <HomePage onNavigate={handleNavigate} showAddCVE={showAddCVE} setShowAddCVE={setShowAddCVE} />}
|
||||||
{currentPage === 'triage' && <VulnerabilityTriagePage filterDate={calendarFilter} filterEXC={reportingExcFilter} />}
|
{currentPage === 'triage' && <VulnerabilityTriagePage filterDate={calendarFilter} filterEXC={reportingExcFilter} />}
|
||||||
{currentPage === 'compliance' && <CompliancePage onNavigate={setCurrentPage} />}
|
{currentPage === 'compliance' && <CompliancePage onNavigate={setCurrentPage} />}
|
||||||
{currentPage === 'ccp-metrics' && isInGroup('Admin', 'Leadership') && <CCPMetricsPage />}
|
{currentPage === 'ccp-metrics' && <CCPMetricsPage />}
|
||||||
{currentPage === 'ccp-metrics' && !isInGroup('Admin', 'Leadership') && (() => { setCurrentPage('home'); return null; })()}
|
|
||||||
{currentPage === 'knowledge-base' && <KnowledgeBasePage />}
|
{currentPage === 'knowledge-base' && <KnowledgeBasePage />}
|
||||||
{currentPage === 'exports' && <ExportsPage />}
|
{currentPage === 'exports' && <ExportsPage />}
|
||||||
{currentPage === 'jira' && <JiraPage />}
|
{currentPage === 'jira' && <JiraPage />}
|
||||||
{currentPage === 'archer-templates' && <ArcherTemplatePage />}
|
{currentPage === 'archer-templates' && <ArcherTemplatePage />}
|
||||||
{currentPage === 'admin' && isAdmin() && <AdminPage />}
|
{currentPage === 'admin' && <AdminPage />}
|
||||||
{currentPage === 'admin' && !isAdmin() && (() => { setCurrentPage('home'); return null; })()}
|
|
||||||
|
|
||||||
{/* Global Modals */}
|
{/* Global Modals */}
|
||||||
{showUserManagement && <UserManagement onClose={() => setShowUserManagement(false)} />}
|
{showUserManagement && <UserManagement onClose={() => setShowUserManagement(false)} />}
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { X, Home, BarChart2, BookOpen, Download, ShieldCheck, Settings, Ticket, Building2, Layers } from 'lucide-react';
|
import { X, Home, BarChart2, BookOpen, Download, ShieldCheck, Settings, Ticket, Building2, Layers } from 'lucide-react';
|
||||||
import { useAuth } from '../contexts/AuthContext';
|
import { useAuth } from '../contexts/AuthContext';
|
||||||
|
import { canAccessPage } from '../config/pageVisibility';
|
||||||
|
|
||||||
const NAV_ITEMS = [
|
const NAV_ITEMS = [
|
||||||
{ id: 'home', label: 'Home', icon: Home, color: '#0EA5E9', description: 'Main dashboard' },
|
{ id: 'home', label: 'Home', icon: Home, color: '#0EA5E9', description: 'Main dashboard' },
|
||||||
{ id: 'triage', label: 'Vuln Triage', icon: BarChart2, color: '#F59E0B', description: 'Active findings & CVE triage' },
|
{ id: 'triage', label: 'Vuln Triage', icon: BarChart2, color: '#F59E0B', description: 'Active findings & CVE triage' },
|
||||||
{ id: 'compliance', label: 'Compliance', icon: ShieldCheck, color: '#14B8A6', description: 'AEO posture & metrics' },
|
{ id: 'compliance', label: 'Compliance', icon: ShieldCheck, color: '#14B8A6', description: 'AEO posture & metrics' },
|
||||||
{ id: 'ccp-metrics', label: 'CCP Metrics', icon: Building2, color: '#A78BFA', description: 'Cross-vertical VCL reporting', requiredGroups: ['Admin', 'Leadership'] },
|
{ id: 'ccp-metrics', label: 'CCP Metrics', icon: Building2, color: '#A78BFA', description: 'Cross-vertical VCL reporting' },
|
||||||
{ id: 'knowledge-base', label: 'Knowledge Base', icon: BookOpen, color: '#10B981', description: 'Articles & documentation' },
|
{ id: 'knowledge-base', label: 'Knowledge Base', icon: BookOpen, color: '#10B981', description: 'Articles & documentation' },
|
||||||
{ id: 'exports', label: 'Exports', icon: Download, color: '#8B5CF6', description: 'Export data & reports' },
|
{ id: 'exports', label: 'Exports', icon: Download, color: '#8B5CF6', description: 'Export data & reports' },
|
||||||
{ id: 'jira', label: 'Jira Tickets', icon: Ticket, color: '#6366F1', description: 'Jira issue tracking & sync' },
|
{ id: 'jira', label: 'Jira Tickets', icon: Ticket, color: '#6366F1', description: 'Jira issue tracking & sync' },
|
||||||
@@ -16,7 +17,7 @@ const NAV_ITEMS = [
|
|||||||
const ADMIN_ITEM = { id: 'admin', label: 'Admin Panel', icon: Settings, color: '#EF4444', description: 'User management & audit' };
|
const ADMIN_ITEM = { id: 'admin', label: 'Admin Panel', icon: Settings, color: '#EF4444', description: 'User management & audit' };
|
||||||
|
|
||||||
export default function NavDrawer({ isOpen, onClose, currentPage, onNavigate }) {
|
export default function NavDrawer({ isOpen, onClose, currentPage, onNavigate }) {
|
||||||
const { isAdmin, isInGroup } = useAuth();
|
const { user } = useAuth();
|
||||||
|
|
||||||
if (!isOpen) return null;
|
if (!isOpen) return null;
|
||||||
|
|
||||||
@@ -70,7 +71,7 @@ export default function NavDrawer({ isOpen, onClose, currentPage, onNavigate })
|
|||||||
|
|
||||||
{/* Nav items */}
|
{/* Nav items */}
|
||||||
<nav style={{ display: 'flex', flexDirection: 'column', gap: '0.375rem' }}>
|
<nav style={{ display: 'flex', flexDirection: 'column', gap: '0.375rem' }}>
|
||||||
{NAV_ITEMS.filter(({ requiredGroups }) => !requiredGroups || isInGroup(...requiredGroups)).map(({ id, label, icon: Icon, color, description }) => {
|
{NAV_ITEMS.filter(item => canAccessPage(item.id, user?.group)).map(({ id, label, icon: Icon, color, description }) => {
|
||||||
const active = currentPage === id;
|
const active = currentPage === id;
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
@@ -124,8 +125,8 @@ export default function NavDrawer({ isOpen, onClose, currentPage, onNavigate })
|
|||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|
||||||
{/* Admin panel link — visible only to Admin group */}
|
{/* Admin panel link — visible based on page visibility matrix */}
|
||||||
{isAdmin() && (() => {
|
{canAccessPage('admin', user?.group) && (() => {
|
||||||
const { id, label, icon: Icon, color, description } = ADMIN_ITEM;
|
const { id, label, icon: Icon, color, description } = ADMIN_ITEM;
|
||||||
const active = currentPage === id;
|
const active = currentPage === id;
|
||||||
return (
|
return (
|
||||||
|
|||||||
37
frontend/src/config/pageVisibility.js
Normal file
37
frontend/src/config/pageVisibility.js
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
// Centralized Page Visibility Matrix
|
||||||
|
// Controls which pages each user group can access.
|
||||||
|
// Empty array = visible to all authenticated users.
|
||||||
|
// Non-empty array = visible only to listed groups.
|
||||||
|
|
||||||
|
export const PAGE_VISIBILITY = {
|
||||||
|
home: [],
|
||||||
|
triage: ['Admin', 'Standard_User', 'Leadership'],
|
||||||
|
compliance: ['Admin', 'Standard_User', 'Leadership'],
|
||||||
|
'ccp-metrics': ['Admin', 'Leadership'],
|
||||||
|
'knowledge-base': [],
|
||||||
|
exports: ['Admin', 'Standard_User', 'Leadership'],
|
||||||
|
jira: ['Admin', 'Standard_User'],
|
||||||
|
'archer-templates': ['Admin', 'Standard_User'],
|
||||||
|
admin: ['Admin'],
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a user group can access a page.
|
||||||
|
* @param {string} pageId - Page identifier
|
||||||
|
* @param {string} userGroup - User's group
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
export function canAccessPage(pageId, userGroup) {
|
||||||
|
const allowed = PAGE_VISIBILITY[pageId];
|
||||||
|
if (!allowed || allowed.length === 0) return true;
|
||||||
|
return allowed.includes(userGroup);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all page IDs accessible to a given group.
|
||||||
|
* @param {string} userGroup - User's group
|
||||||
|
* @returns {string[]}
|
||||||
|
*/
|
||||||
|
export function getAccessiblePages(userGroup) {
|
||||||
|
return Object.keys(PAGE_VISIBILITY).filter(pageId => canAccessPage(pageId, userGroup));
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user