Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 1c16354725 | |||
| 89f78a38dd |
@@ -15,7 +15,14 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// knownRoles lists the built-in roles shown as checkboxes in the roles editor.
|
// knownRoles lists the built-in roles shown as checkboxes in the roles editor.
|
||||||
var knownRoles = []string{"admin", "user", "service"}
|
var knownRoles = []string{
|
||||||
|
model.RoleAdmin,
|
||||||
|
model.RoleUser,
|
||||||
|
model.RoleGuest,
|
||||||
|
model.RoleViewer,
|
||||||
|
model.RoleEditor,
|
||||||
|
model.RoleCommenter,
|
||||||
|
}
|
||||||
|
|
||||||
// handleAccountsList renders the accounts list page.
|
// handleAccountsList renders the accounts list page.
|
||||||
func (u *UIServer) handleAccountsList(w http.ResponseWriter, r *http.Request) {
|
func (u *UIServer) handleAccountsList(w http.ResponseWriter, r *http.Request) {
|
||||||
@@ -907,26 +914,8 @@ func (u *UIServer) handleCreatePGCreds(w http.ResponseWriter, r *http.Request) {
|
|||||||
// storage. The plaintext is never logged or included in any response.
|
// storage. The plaintext is never logged or included in any response.
|
||||||
// Audit event EventPasswordChanged is recorded on success.
|
// Audit event EventPasswordChanged is recorded on success.
|
||||||
func (u *UIServer) handleAdminResetPassword(w http.ResponseWriter, r *http.Request) {
|
func (u *UIServer) handleAdminResetPassword(w http.ResponseWriter, r *http.Request) {
|
||||||
// Security: enforce admin role; requireCookieAuth only validates the token,
|
// Security: admin role is enforced by the requireAdminRole middleware in
|
||||||
// it does not check roles. A non-admin with a valid session must not be
|
// the route registration (ui.go); no inline check needed here.
|
||||||
// able to reset arbitrary accounts' passwords.
|
|
||||||
callerClaims := claimsFromContext(r.Context())
|
|
||||||
if callerClaims == nil {
|
|
||||||
u.renderError(w, r, http.StatusUnauthorized, "unauthorized")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
isAdmin := false
|
|
||||||
for _, role := range callerClaims.Roles {
|
|
||||||
if role == "admin" {
|
|
||||||
isAdmin = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if !isAdmin {
|
|
||||||
u.renderError(w, r, http.StatusForbidden, "admin role required")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
r.Body = http.MaxBytesReader(w, r.Body, maxFormBytes)
|
r.Body = http.MaxBytesReader(w, r.Body, maxFormBytes)
|
||||||
if err := r.ParseForm(); err != nil {
|
if err := r.ParseForm(); err != nil {
|
||||||
u.renderError(w, r, http.StatusBadRequest, "invalid form")
|
u.renderError(w, r, http.StatusBadRequest, "invalid form")
|
||||||
|
|||||||
@@ -301,12 +301,17 @@ func (u *UIServer) Register(mux *http.ServeMux) {
|
|||||||
uiMux.HandleFunc("POST /logout", u.handleLogout)
|
uiMux.HandleFunc("POST /logout", u.handleLogout)
|
||||||
|
|
||||||
// Protected routes.
|
// Protected routes.
|
||||||
auth := u.requireCookieAuth
|
//
|
||||||
|
// Security: three distinct access levels:
|
||||||
|
// - authed: any valid session cookie (authenticated user)
|
||||||
|
// - admin: authed + admin role in JWT claims (mutating admin ops)
|
||||||
|
// - adminGet: authed + admin role (read-only admin pages, no CSRF)
|
||||||
|
authed := u.requireCookieAuth
|
||||||
admin := func(h http.HandlerFunc) http.Handler {
|
admin := func(h http.HandlerFunc) http.Handler {
|
||||||
return auth(u.requireCSRF(http.HandlerFunc(h)))
|
return authed(u.requireAdminRole(u.requireCSRF(http.HandlerFunc(h))))
|
||||||
}
|
}
|
||||||
adminGet := func(h http.HandlerFunc) http.Handler {
|
adminGet := func(h http.HandlerFunc) http.Handler {
|
||||||
return auth(http.HandlerFunc(h))
|
return authed(u.requireAdminRole(http.HandlerFunc(h)))
|
||||||
}
|
}
|
||||||
|
|
||||||
uiMux.Handle("GET /dashboard", adminGet(u.handleDashboard))
|
uiMux.Handle("GET /dashboard", adminGet(u.handleDashboard))
|
||||||
@@ -335,8 +340,8 @@ func (u *UIServer) Register(mux *http.ServeMux) {
|
|||||||
uiMux.Handle("PUT /accounts/{id}/password", admin(u.handleAdminResetPassword))
|
uiMux.Handle("PUT /accounts/{id}/password", admin(u.handleAdminResetPassword))
|
||||||
|
|
||||||
// Profile routes — accessible to any authenticated user (not admin-only).
|
// Profile routes — accessible to any authenticated user (not admin-only).
|
||||||
uiMux.Handle("GET /profile", adminGet(u.handleProfilePage))
|
uiMux.Handle("GET /profile", authed(http.HandlerFunc(u.handleProfilePage)))
|
||||||
uiMux.Handle("PUT /profile/password", auth(u.requireCSRF(http.HandlerFunc(u.handleSelfChangePassword))))
|
uiMux.Handle("PUT /profile/password", authed(u.requireCSRF(http.HandlerFunc(u.handleSelfChangePassword))))
|
||||||
|
|
||||||
// Mount the wrapped UI mux on the parent mux. The "/" pattern acts as a
|
// Mount the wrapped UI mux on the parent mux. The "/" pattern acts as a
|
||||||
// catch-all for all UI paths; the more-specific /v1/ API patterns registered
|
// catch-all for all UI paths; the more-specific /v1/ API patterns registered
|
||||||
@@ -405,6 +410,25 @@ func (u *UIServer) requireCSRF(next http.Handler) http.Handler {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// requireAdminRole checks that the authenticated user holds the "admin" role.
|
||||||
|
// Must be placed after requireCookieAuth in the middleware chain so that
|
||||||
|
// claims are available in the context.
|
||||||
|
//
|
||||||
|
// Security: This is the authoritative server-side check that prevents
|
||||||
|
// non-admin users from accessing admin-only UI endpoints. The JWT claims
|
||||||
|
// are populated from the database at login/renewal and signed with the
|
||||||
|
// server's Ed25519 private key, so they cannot be forged client-side.
|
||||||
|
func (u *UIServer) requireAdminRole(next http.Handler) http.Handler {
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
claims := claimsFromContext(r.Context())
|
||||||
|
if claims == nil || !claims.HasRole("admin") {
|
||||||
|
u.renderError(w, r, http.StatusForbidden, "admin role required")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
next.ServeHTTP(w, r)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// ---- Helpers ----
|
// ---- Helpers ----
|
||||||
|
|
||||||
// isHTMX reports whether the request was initiated by HTMX.
|
// isHTMX reports whether the request was initiated by HTMX.
|
||||||
|
|||||||
Reference in New Issue
Block a user