From 386972366b1a08bf922769d20e4c5ac7d4c5b6ce Mon Sep 17 00:00:00 2001 From: Debian Date: Sat, 23 May 2026 16:57:17 +0200 Subject: [PATCH] =?UTF-8?q?feat(rbac):=20Viewer-Rolle=20durchsetzen=20?= =?UTF-8?q?=E2=80=94=20alle=20Mutations=20nur=20f=C3=BCr=20Admins?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - middleware.go: RequireAdminForMutations() blockiert POST/PUT/DELETE für Nicht-Admins (GET/HEAD passieren immer durch) - main.go: Middleware in den authed-Gruppe eingehängt — wirkt für alle ~30 Resource-Handler gleichzeitig - api/client.ts: 403 → AntD-Notification "Nur Lesezugriff" statt stiller Fehler - Header: "Nur lesen"-Badge + Tooltip wenn role=viewer - i18n de+en: viewerBadge + viewerHint Co-Authored-By: Claude Sonnet 4.6 --- VERSION | 2 +- cmd/edgeguard-api/main.go | 2 +- internal/handlers/middleware.go | 23 +++++++++ management-ui/src/api/client.ts | 7 +++ .../src/components/Layout/Header.tsx | 47 +++++++++++-------- management-ui/src/i18n/locales/de/common.json | 4 +- management-ui/src/i18n/locales/en/common.json | 4 +- 7 files changed, 66 insertions(+), 23 deletions(-) diff --git a/VERSION b/VERSION index b30a005..2733a3e 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.1.57 +1.1.58 diff --git a/cmd/edgeguard-api/main.go b/cmd/edgeguard-api/main.go index 87173d9..2ca316e 100644 --- a/cmd/edgeguard-api/main.go +++ b/cmd/edgeguard-api/main.go @@ -276,7 +276,7 @@ func main() { } authed := v1.Group("") - authed.Use(requireAuth) + authed.Use(requireAuth, handlers.RequireAdminForMutations()) setupHdl.RegisterAuthed(authed) handlers.NewUsersHandler(usersRepo, auditRepo, nodeID).Register(authed) handlers.NewDomainsHandler(domainsRepo, routingRepo, domainHeadersRepo, auditRepo, nodeID, haproxyReloader).Register(authed) diff --git a/internal/handlers/middleware.go b/internal/handlers/middleware.go index f88c81d..fe70f84 100644 --- a/internal/handlers/middleware.go +++ b/internal/handlers/middleware.go @@ -117,4 +117,27 @@ func tokenFromRequest(c *gin.Context) string { return "" } +// RequireAdminForMutations blocks non-GET/HEAD requests from non-admin +// users. GET and HEAD are always allowed for authenticated users so +// read-only ("viewer") accounts can browse all data. Every state- +// changing request (POST, PUT, PATCH, DELETE) requires role="admin". +// +// Must be mounted AFTER RequireAuth so the token is already in context. +func RequireAdminForMutations() gin.HandlerFunc { + return func(c *gin.Context) { + m := c.Request.Method + if m == http.MethodGet || m == http.MethodHead || m == http.MethodOptions { + c.Next() + return + } + tok := CurrentToken(c) + if tok == nil || tok.Role != "admin" { + response.Err(c, http.StatusForbidden, errors.New("admin_required")) + c.Abort() + return + } + c.Next() + } +} + func ptr[T any](v T) *T { return &v } diff --git a/management-ui/src/api/client.ts b/management-ui/src/api/client.ts index 52bc9e3..cd68d6b 100644 --- a/management-ui/src/api/client.ts +++ b/management-ui/src/api/client.ts @@ -1,4 +1,5 @@ import axios, { type AxiosError } from 'axios' +import { message } from 'antd' import { useAuthStore } from '../stores/auth' @@ -38,6 +39,12 @@ apiClient.interceptors.response.use( window.location.replace('/login') } + // 403 admin_required → viewer account tried to mutate; show a + // one-time notification and let the calling mutation handle the rest. + if (error.response?.status === 403) { + message.error('Keine Berechtigung — dieser Account hat nur Lesezugriff.', 4) + } + // 503 setup_required → kick to /setup so the wizard takes over. if ( error.response?.status === 503 && diff --git a/management-ui/src/components/Layout/Header.tsx b/management-ui/src/components/Layout/Header.tsx index b6f2318..fa72c7e 100644 --- a/management-ui/src/components/Layout/Header.tsx +++ b/management-ui/src/components/Layout/Header.tsx @@ -1,5 +1,5 @@ -import { Button, Dropdown, Select, Space } from 'antd' -import { GlobalOutlined, LogoutOutlined, MenuOutlined, UserOutlined } from '@ant-design/icons' +import { Button, Dropdown, Select, Space, Tag, Tooltip } from 'antd' +import { EyeOutlined, GlobalOutlined, LogoutOutlined, MenuOutlined, UserOutlined } from '@ant-design/icons' import { useNavigate } from 'react-router-dom' import { useTranslation } from 'react-i18next' @@ -55,23 +55,32 @@ export default function Header({ pageTitle, onMenuToggle }: HeaderProps) { popupMatchSelectWidth={false} /> {user && ( - , - label: t('auth.logout'), - onClick: onLogout, - }, - ], - }} - placement="bottomRight" - > - - + <> + {user.role === 'viewer' && ( + + } color="default" style={{ marginRight: 4 }}> + {t('auth.viewerBadge')} + + + )} + , + label: t('auth.logout'), + onClick: onLogout, + }, + ], + }} + placement="bottomRight" + > + + + )} diff --git a/management-ui/src/i18n/locales/de/common.json b/management-ui/src/i18n/locales/de/common.json index b05a931..6dc85b3 100644 --- a/management-ui/src/i18n/locales/de/common.json +++ b/management-ui/src/i18n/locales/de/common.json @@ -198,7 +198,9 @@ "logout": "Abmelden", "loginFailed": "Anmeldung fehlgeschlagen", "loggedInAs": "Angemeldet als", - "forgotPassword": "Passwort vergessen?" + "forgotPassword": "Passwort vergessen?", + "viewerBadge": "Nur lesen", + "viewerHint": "Dieser Account hat die Rolle Betrachter — Änderungen sind gesperrt. Ein Admin kann die Rolle anpassen." }, "reset": { "title": "Admin-Passwort zurücksetzen", diff --git a/management-ui/src/i18n/locales/en/common.json b/management-ui/src/i18n/locales/en/common.json index 1391083..f68cf48 100644 --- a/management-ui/src/i18n/locales/en/common.json +++ b/management-ui/src/i18n/locales/en/common.json @@ -198,7 +198,9 @@ "logout": "Sign out", "loginFailed": "Sign-in failed", "loggedInAs": "Signed in as", - "forgotPassword": "Forgot your password?" + "forgotPassword": "Forgot your password?", + "viewerBadge": "Read-only", + "viewerHint": "Your account has viewer role — all changes are blocked. Contact an admin to change your role." }, "reset": { "title": "Reset admin password",