feat: implement group-based access control (Admin, Standard_User, Leadership, Read_Only)
- Add user_group migration and created_by column migration - Replace requireRole middleware with requireGroup - Update all backend routes to use group-based authorization - Add Standard_User conditional delete with ownership, state, and compliance checks - Add cascade impact check for CVE deletes - Update AuthContext with group-based permission helpers - Update all frontend components for group-based rendering - Update UserManagement UI with group dropdown, confirmation dialogs, self-demotion prevention
This commit is contained in:
@@ -162,7 +162,7 @@ const API_HOST = process.env.REACT_APP_API_HOST || 'http://localhost:3001';
|
||||
const severityLevels = ['All Severities', 'Critical', 'High', 'Medium', 'Low'];
|
||||
|
||||
export default function App() {
|
||||
const { isAuthenticated, loading: authLoading, canWrite, isAdmin, user } = useAuth();
|
||||
const { isAuthenticated, loading: authLoading, canWrite, canDelete, canExport, isAdmin, user } = useAuth();
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
const [selectedVendor, setSelectedVendor] = useState('All Vendors');
|
||||
const [selectedSeverity, setSelectedSeverity] = useState('All Severities');
|
||||
@@ -1746,7 +1746,7 @@ export default function App() {
|
||||
<span className="text-gray-500 mx-2">•</span>
|
||||
<span className="text-gray-300">{cves.length}</span> vendor entr{cves.length !== 1 ? 'ies' : 'y'}
|
||||
</p>
|
||||
{selectedDocuments.length > 0 && (
|
||||
{selectedDocuments.length > 0 && canExport() && (
|
||||
<button
|
||||
onClick={exportSelectedDocuments}
|
||||
className="intel-button intel-button-primary flex items-center gap-2"
|
||||
@@ -1833,7 +1833,7 @@ export default function App() {
|
||||
<span>Published: {vendorEntries[0].published_date}</span>
|
||||
<span className="text-intel-accent">•</span>
|
||||
<span>{vendorEntries.length} affected vendor{vendorEntries.length > 1 ? 's' : ''}</span>
|
||||
{canWrite() && vendorEntries.length >= 2 && (
|
||||
{isAdmin() && vendorEntries.length >= 2 && (
|
||||
<button
|
||||
onClick={(e) => { e.stopPropagation(); handleDeleteEntireCVE(cveId, vendorEntries.length); }}
|
||||
className="ml-2 px-3 py-1 text-xs intel-button intel-button-danger flex items-center gap-1"
|
||||
@@ -1894,7 +1894,7 @@ export default function App() {
|
||||
<Edit2 className="w-4 h-4" />
|
||||
</button>
|
||||
)}
|
||||
{canWrite() && (
|
||||
{canDelete(cve) && (
|
||||
<button
|
||||
onClick={() => handleDeleteCVEEntry(cve)}
|
||||
className="px-3 py-2 text-intel-danger hover:bg-intel-medium rounded border border-intel-danger/50 transition-all flex items-center gap-1"
|
||||
@@ -2026,9 +2026,11 @@ export default function App() {
|
||||
<button onClick={() => handleEditTicket(ticket)} className="p-1 text-gray-400 hover:text-intel-warning transition-colors">
|
||||
<Edit2 className="w-4 h-4" />
|
||||
</button>
|
||||
{canDelete(ticket) && (
|
||||
<button onClick={() => handleDeleteTicket(ticket)} className="p-1 text-gray-400 hover:text-intel-danger transition-colors">
|
||||
<Trash2 className="w-4 h-4" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -2152,9 +2154,11 @@ export default function App() {
|
||||
<button onClick={() => handleEditTicket(ticket)} className="text-gray-400 hover:text-intel-warning transition-colors">
|
||||
<Edit2 className="w-3 h-3" />
|
||||
</button>
|
||||
{canDelete(ticket) && (
|
||||
<button onClick={() => handleDeleteTicket(ticket)} className="text-gray-400 hover:text-intel-danger transition-colors">
|
||||
<Trash2 className="w-3 h-3" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -2220,14 +2224,16 @@ export default function App() {
|
||||
>
|
||||
<Filter className="w-3 h-3" />
|
||||
</button>
|
||||
{canWrite() && (<>
|
||||
{canWrite() && (
|
||||
<button onClick={() => handleEditArcherTicket(ticket)} className="text-gray-400 hover:text-purple-400 transition-colors">
|
||||
<Edit2 className="w-3 h-3" />
|
||||
</button>
|
||||
)}
|
||||
{canDelete(ticket) && (
|
||||
<button onClick={() => handleDeleteArcherTicket(ticket)} className="text-gray-400 hover:text-intel-danger transition-colors">
|
||||
<Trash2 className="w-3 h-3" />
|
||||
</button>
|
||||
</>)}
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-xs text-white font-mono mb-1">{ticket.cve_id}</div>
|
||||
@@ -2256,6 +2262,7 @@ export default function App() {
|
||||
<Activity className="w-5 h-5" />
|
||||
Ivanti Workflows
|
||||
</h2>
|
||||
{canWrite() && (
|
||||
<button
|
||||
onClick={syncIvantiWorkflows}
|
||||
disabled={ivantiSyncing || ivantiLoading}
|
||||
@@ -2265,6 +2272,7 @@ export default function App() {
|
||||
<RefreshCw className={`w-3 h-3 ${ivantiSyncing ? 'animate-spin' : ''}`} />
|
||||
{ivantiSyncing ? 'Syncing…' : 'Sync'}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Last synced line */}
|
||||
|
||||
Reference in New Issue
Block a user