Skip to content

Commit 2ea3ddf

Browse files
committed
fix: viewer role UI restrictions and route ordering
- Fix /api/upload/ecs-fields endpoint routing (move before parameterized routes) - Block viewers from creating/updating/deleting patterns (require user or admin) - Hide API Keys button for viewers in frontend - Hide delete index icon in History for viewers
1 parent e5689ea commit 2ea3ddf

4 files changed

Lines changed: 34 additions & 28 deletions

File tree

backend/app/routers/patterns.py

Lines changed: 15 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
from fastapi import APIRouter, Depends, HTTPException
88
from pydantic import BaseModel, Field
99

10-
from app.routers.auth import require_auth
10+
from app.routers.auth import require_auth, require_user_or_admin
1111
from app.services import database
1212
from app.services.grok_patterns import (
1313
list_builtin_patterns,
@@ -136,9 +136,9 @@ async def list_patterns(
136136
@router.post("", status_code=201)
137137
async def create_pattern(
138138
data: PatternCreate,
139-
user: dict = Depends(require_auth),
139+
user: dict = Depends(require_user_or_admin),
140140
) -> PatternResponse:
141-
"""Create a new custom pattern."""
141+
"""Create a new custom pattern. Requires user or admin role."""
142142
# Validate the pattern
143143
if data.type == "grok":
144144
is_valid, error = validate_grok_pattern(data.pattern)
@@ -246,9 +246,9 @@ async def list_grok_patterns(
246246
@router.post("/grok", status_code=201)
247247
async def create_grok_pattern(
248248
data: GrokPatternCreate,
249-
user: dict = Depends(require_auth),
249+
user: dict = Depends(require_user_or_admin),
250250
) -> GrokPatternResponse:
251-
"""Create a new custom grok pattern component."""
251+
"""Create a new custom grok pattern component. Requires user or admin role."""
252252
# Check if name conflicts with built-in
253253
builtins = {p["name"] for p in list_builtin_patterns()}
254254
if data.name in builtins:
@@ -286,9 +286,9 @@ async def create_grok_pattern(
286286
@router.post("/grok/import", response_model=GrokImportResult)
287287
async def import_grok_patterns(
288288
request: GrokImportRequest,
289-
user: dict = Depends(require_auth),
289+
user: dict = Depends(require_user_or_admin),
290290
) -> GrokImportResult:
291-
"""Import multiple grok patterns from file content.
291+
"""Import multiple grok patterns from file content. Requires user or admin role.
292292
293293
Parses grok pattern file format (PATTERN_NAME<whitespace>pattern_expression).
294294
Lines starting with # are comments, blank lines are skipped.
@@ -368,9 +368,9 @@ async def get_grok_pattern(
368368
async def update_grok_pattern(
369369
pattern_id: str,
370370
data: GrokPatternUpdate,
371-
user: dict = Depends(require_auth),
371+
user: dict = Depends(require_user_or_admin),
372372
) -> GrokPatternResponse:
373-
"""Update a custom grok pattern component."""
373+
"""Update a custom grok pattern component. Requires user or admin role."""
374374
existing = database.get_grok_pattern(pattern_id)
375375
if not existing:
376376
raise HTTPException(status_code=404, detail="Grok pattern not found")
@@ -396,9 +396,9 @@ async def update_grok_pattern(
396396
@router.delete("/grok/{pattern_id}", status_code=204)
397397
async def delete_grok_pattern(
398398
pattern_id: str,
399-
user: dict = Depends(require_auth),
399+
user: dict = Depends(require_user_or_admin),
400400
) -> None:
401-
"""Delete a custom grok pattern component."""
401+
"""Delete a custom grok pattern component. Requires user or admin role."""
402402
existing = database.get_grok_pattern(pattern_id)
403403
if not existing:
404404
raise HTTPException(status_code=404, detail="Grok pattern not found")
@@ -428,9 +428,9 @@ async def get_pattern(
428428
async def update_pattern(
429429
pattern_id: str,
430430
data: PatternUpdate,
431-
user: dict = Depends(require_auth),
431+
user: dict = Depends(require_user_or_admin),
432432
) -> PatternResponse:
433-
"""Update a custom pattern."""
433+
"""Update a custom pattern. Requires user or admin role."""
434434
existing = database.get_pattern(pattern_id)
435435
if not existing:
436436
raise HTTPException(status_code=404, detail="Pattern not found")
@@ -466,9 +466,9 @@ async def update_pattern(
466466
@router.delete("/{pattern_id}", status_code=204)
467467
async def delete_pattern(
468468
pattern_id: str,
469-
user: dict = Depends(require_auth),
469+
user: dict = Depends(require_user_or_admin),
470470
) -> None:
471-
"""Delete a custom pattern."""
471+
"""Delete a custom pattern. Requires user or admin role."""
472472
existing = database.get_pattern(pattern_id)
473473
if not existing:
474474
raise HTTPException(status_code=404, detail="Pattern not found")

backend/app/routers/upload.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -248,6 +248,12 @@ async def upload_files(
248248
)
249249

250250

251+
@router.get("/upload/ecs-fields")
252+
async def list_ecs_fields(user: dict = Depends(require_auth)):
253+
"""List all available ECS fields for manual selection."""
254+
return {"fields": get_all_ecs_fields()}
255+
256+
251257
@router.get("/upload/{upload_id}", response_model=UploadResponse)
252258
async def get_upload(
253259
upload_id: str,
@@ -418,12 +424,6 @@ async def suggest_ecs_fields(upload_id: str):
418424
}
419425

420426

421-
@router.get("/upload/ecs-fields")
422-
async def list_ecs_fields(user: dict = Depends(require_auth)):
423-
"""List all available ECS fields for manual selection."""
424-
return {"fields": get_all_ecs_fields()}
425-
426-
427427
@router.post("/upload/{upload_id}/reparse")
428428
async def reparse_upload(
429429
upload_id: str,

frontend/src/App.tsx

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -286,12 +286,14 @@ function App() {
286286
>
287287
Patterns
288288
</button>
289-
<button
290-
onClick={() => setShowApiKeys(true)}
291-
className="px-4 py-2 text-sm font-medium text-gray-700 dark:text-gray-200 bg-gray-100 dark:bg-gray-700 rounded-md hover:bg-gray-200 dark:hover:bg-gray-600"
292-
>
293-
API Keys
294-
</button>
289+
{user?.role !== 'viewer' && (
290+
<button
291+
onClick={() => setShowApiKeys(true)}
292+
className="px-4 py-2 text-sm font-medium text-gray-700 dark:text-gray-200 bg-gray-100 dark:bg-gray-700 rounded-md hover:bg-gray-200 dark:hover:bg-gray-600"
293+
>
294+
API Keys
295+
</button>
296+
)}
295297
<button
296298
onClick={() => setShowHistory(true)}
297299
className="px-4 py-2 text-sm font-medium text-gray-700 dark:text-gray-200 bg-gray-100 dark:bg-gray-700 rounded-md hover:bg-gray-200 dark:hover:bg-gray-600"

frontend/src/components/History.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import React, { useEffect, useState } from 'react';
22
import { deleteIndex, downloadFailures, getFailuresDownloadUrl, getAppSettings, getHistory, UploadRecord } from '../api/client';
3+
import { useAuth } from '../contexts/AuthContext';
34

45
interface HistoryProps {
56
onClose: () => void;
@@ -41,6 +42,7 @@ function StatusBadge({ status }: { status: UploadRecord['status'] }) {
4142
}
4243

4344
export function History({ onClose }: HistoryProps) {
45+
const { user } = useAuth();
4446
const [uploads, setUploads] = useState<UploadRecord[]>([]);
4547
const [loading, setLoading] = useState(true);
4648
const [error, setError] = useState<string | null>(null);
@@ -52,6 +54,8 @@ export function History({ onClose }: HistoryProps) {
5254
const [toast, setToast] = useState<{ message: string; type: 'success' | 'error' } | null>(null);
5355
const [retentionDays, setRetentionDays] = useState<number>(0);
5456

57+
const canDelete = user?.role !== 'viewer';
58+
5559
const toggleExpand = (id: string) => {
5660
setExpandedId(expandedId === id ? null : id);
5761
};
@@ -297,7 +301,7 @@ export function History({ onClose }: HistoryProps) {
297301
</svg>
298302
</button>
299303
)}
300-
{upload.status === 'completed' && upload.index_name && !upload.index_deleted && upload.index_exists !== false && (
304+
{canDelete && upload.status === 'completed' && upload.index_name && !upload.index_deleted && upload.index_exists !== false && (
301305
<button
302306
onClick={(e) => {
303307
e.stopPropagation();

0 commit comments

Comments
 (0)