fix(rbac): hide vuln write-actions from read-only + scope notification log
- Vulnerabilities list: the per-row Change-Status / Suppress / Mark-False- Positive buttons and the assign dropdown were shown to read-only users and threw 'Access denied. Required role: editor'. Gate them (editor+); assign select disabled for read-only. - CVE detail page: Dismiss/Reactivate now shown only to editor+ (fetches /auth/me for the role). - Notification log (/notifications): GET /log returned EVERY sent mail — recipient emails + which CVE went to whom — to any authenticated user (privacy leak). Now admins see all; everyone else sees only notifications addressed to them (user_id == own).
This commit is contained in:
@@ -54,9 +54,18 @@ async def list_notification_logs(
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user)
|
||||
):
|
||||
"""List notification history"""
|
||||
"""List notification history.
|
||||
|
||||
Privacy: the log holds recipient emails + which CVE/asset went to whom.
|
||||
Admins see everything (operational overview); everyone else sees only the
|
||||
notifications addressed to them (user_id == their own id).
|
||||
"""
|
||||
from app.models.user import UserRole
|
||||
query = db.query(NotificationLog).order_by(desc(NotificationLog.sent_at))
|
||||
|
||||
if current_user.role != UserRole.ADMIN:
|
||||
query = query.filter(NotificationLog.user_id == current_user.id)
|
||||
|
||||
if notification_type:
|
||||
try:
|
||||
nt = NotificationType(notification_type)
|
||||
|
||||
@@ -313,6 +313,11 @@ export default function VulnerabilityDetailPage() {
|
||||
const [vuln, setVuln] = useState<VulnDetail | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [userRole, setUserRole] = useState('');
|
||||
useEffect(() => {
|
||||
api.get('/auth/me').then(r => setUserRole(r.data?.role || '')).catch(() => { });
|
||||
}, []);
|
||||
const canEdit = userRole === 'admin' || userRole === 'editor';
|
||||
// Audit / change history for this vuln — populated separately so
|
||||
// a slow audit query doesn't block the vuln detail render.
|
||||
type AuditEntry = {
|
||||
@@ -470,7 +475,7 @@ export default function VulnerabilityDetailPage() {
|
||||
<span className={`px-4 py-2 rounded-lg text-sm font-medium ${statusColors[vuln.status] || 'bg-gray-100'}`}>
|
||||
{vuln.status.replace('_', ' ')}
|
||||
</span>
|
||||
{vuln.status === 'false_positive' ? (
|
||||
{canEdit && (vuln.status === 'false_positive' ? (
|
||||
<button
|
||||
onClick={handleReactivate}
|
||||
disabled={dismissBusy}
|
||||
@@ -488,7 +493,7 @@ export default function VulnerabilityDetailPage() {
|
||||
>
|
||||
{dismissBusy ? '…' : 'Dismiss'}
|
||||
</button>
|
||||
)}
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1794,7 +1794,8 @@ function VulnerabilitiesContent() {
|
||||
<select
|
||||
value={vuln.assigned_group_id ? `group_${vuln.assigned_group_id}` : (vuln.assigned_user_id ? String(vuln.assigned_user_id) : "")}
|
||||
onChange={(e) => handleAssign(vuln.id, e.target.value)}
|
||||
className="block w-full rounded-md border-0 py-1.5 pl-2 pr-7 text-gray-900 ring-1 ring-inset ring-gray-300 focus:ring-2 focus:ring-inset focus:ring-truevuln-blue sm:text-xs sm:leading-6 bg-transparent"
|
||||
disabled={currentUser?.role === 'readonly'}
|
||||
className="block w-full rounded-md border-0 py-1.5 pl-2 pr-7 text-gray-900 ring-1 ring-inset ring-gray-300 focus:ring-2 focus:ring-inset focus:ring-truevuln-blue sm:text-xs sm:leading-6 bg-transparent disabled:cursor-not-allowed"
|
||||
style={{ maxWidth: '120px' }}
|
||||
>
|
||||
<option value="">Unassigned</option>
|
||||
@@ -1815,6 +1816,7 @@ function VulnerabilitiesContent() {
|
||||
className={`sticky right-0 z-10 px-4 py-3 whitespace-nowrap text-right shadow-[-4px_0_8px_-2px_rgba(0,0,0,0.1)] border-l border-gray-100 ${selectedIds.includes(vuln.id) ? 'bg-blue-50' : 'bg-white group-hover:bg-blue-50/30'}`}
|
||||
>
|
||||
<div className="flex justify-end gap-1">
|
||||
{currentUser?.role !== 'readonly' ? (<>
|
||||
<button
|
||||
onClick={() => handleOpenStatusModal(vuln)}
|
||||
className="p-1 hover:bg-gray-100 rounded text-gray-400 hover:text-truevuln-blue"
|
||||
@@ -1836,6 +1838,9 @@ function VulnerabilitiesContent() {
|
||||
>
|
||||
<NoSymbolIcon className="h-4 w-4" />
|
||||
</button>
|
||||
</>) : (
|
||||
<span className="text-gray-300 text-xs">—</span>
|
||||
)}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
Reference in New Issue
Block a user