server: a non-admin account can manage its own uploads
Signing in and being allowed to administer the server were the same question: the OIDC callback refused a session outright to anyone outside the admin group. A legitimate user could authenticate, be told what they could not do, and be left with no way to see or delete the data their own devices had uploaded. They are separate questions now. Everyone who authenticates gets a session; the admin flag rides inside the MAC'd payload, so promoting yourself means forging a signature rather than editing a cookie, and a role that does not parse fails closed to "user". Pages scope themselves through visibleDevices/mayTouchRun rather than filtering individually — per-page scoping is what the next page added will be missing, and that failure is silent, since a listing that leaks other people's uploads looks exactly like one that does not. Someone else's run answers 404, not 403: a distinguishable refusal would confirm the run exists. Revoking devices and minting enrolment tokens affect the whole server and stay behind adminOnly at the route table, where someone looking for who-may-do-what will actually find it. Ownership is re-read per request instead of captured at sign-in, so unlinking an account takes effect immediately rather than at session expiry. Tests cover that, plus the degenerate case of an empty subject, which must own nothing rather than everything with an empty account id. Also: attribute the ICMPv6 finding per network. It compared "is IPv6 configured anywhere on this device" against "did any network answer", which on a phone reports IPv6-is-broken about a network where IPv6 was never configured. network_ref is null on every test, so the probe now records per-network outcomes structurally rather than as prose a finding would have to parse. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
7eaf0c4190
commit
7a5004f293
@@ -83,13 +83,17 @@ func (s *Server) Handler() http.Handler {
|
||||
mux.HandleFunc("GET /admin/callback", s.oidcCallback)
|
||||
mux.HandleFunc("POST /logout", s.logout)
|
||||
|
||||
// Any signed-in account. These handlers scope what they show to the session themselves —
|
||||
// an admin sees everything, a user sees their own devices and runs.
|
||||
mux.HandleFunc("GET /", s.guard(s.dashboard))
|
||||
mux.HandleFunc("GET /devices", s.guard(s.devices))
|
||||
mux.HandleFunc("POST /devices/{id}/revoke", s.guard(s.revokeDevice))
|
||||
mux.HandleFunc("POST /enroll-tokens", s.guard(s.mintToken))
|
||||
mux.HandleFunc("GET /runs", s.guard(s.runsList))
|
||||
mux.HandleFunc("GET /runs/{device}/{id}", s.guard(s.runView))
|
||||
mux.HandleFunc("POST /runs/{device}/{id}/delete", s.guard(s.runDelete))
|
||||
// Deleting your own upload is yours to do; revoking a device or minting an enrolment token
|
||||
// affects the whole server, so those stay with the admin.
|
||||
mux.HandleFunc("POST /devices/{id}/revoke", s.guard(s.adminOnly(s.revokeDevice)))
|
||||
mux.HandleFunc("POST /enroll-tokens", s.guard(s.adminOnly(s.mintToken)))
|
||||
|
||||
return mux
|
||||
}
|
||||
@@ -140,10 +144,28 @@ func (s *Server) csrfOK(r *http.Request, sess *adminauth.Session) bool {
|
||||
return r.PostFormValue(csrfField) == s.csrfToken(sess)
|
||||
}
|
||||
|
||||
func (s *Server) setSession(w http.ResponseWriter, subject, display string) {
|
||||
// adminOnly refuses a handler to a signed-in account that is not an administrator.
|
||||
//
|
||||
// A separate wrapper rather than a check inside each handler: an authorisation rule that has to be
|
||||
// remembered in every handler is one that will eventually be forgotten in a new one, and the route
|
||||
// table is where someone looks to find out who may do what.
|
||||
func (s *Server) adminOnly(
|
||||
h func(http.ResponseWriter, *http.Request, *adminauth.Session),
|
||||
) func(http.ResponseWriter, *http.Request, *adminauth.Session) {
|
||||
return func(w http.ResponseWriter, r *http.Request, sess *adminauth.Session) {
|
||||
if !sess.Admin {
|
||||
slog.Info("admin action refused", "account", sess.Subject, "path", r.URL.Path)
|
||||
http.Error(w, "that action needs an administrator account", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
h(w, r, sess)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) setSession(w http.ResponseWriter, subject, display string, admin bool) {
|
||||
http.SetCookie(w, &http.Cookie{
|
||||
Name: sessionCookie,
|
||||
Value: s.Sessions.Issue(subject, display),
|
||||
Value: s.Sessions.Issue(subject, display, admin),
|
||||
Path: "/",
|
||||
HttpOnly: true, // the cookie is a bearer credential; script has no business reading it
|
||||
Secure: s.Secure,
|
||||
@@ -186,7 +208,7 @@ func (s *Server) loginSubmit(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
s.Throttle.Succeeded()
|
||||
slog.Info("admin login", "user", user, "method", "local", "from", clientIP(r))
|
||||
s.setSession(w, "local:"+cred.Username, cred.Username)
|
||||
s.setSession(w, "local:"+cred.Username, cred.Username, true)
|
||||
http.Redirect(w, r, "/", http.StatusSeeOther)
|
||||
}
|
||||
|
||||
@@ -271,18 +293,14 @@ func (s *Server) oidcCallback(w http.ResponseWriter, r *http.Request) {
|
||||
http.Error(w, "the identity token was not accepted", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
if !s.OIDC.IsAdmin(claims) {
|
||||
// Named explicitly: "you signed in but you are not an admin" is a different problem from
|
||||
// "your password is wrong", and the group is the thing to go and check.
|
||||
slog.Info("admin access denied: not in group", "account", claims.AccountID(),
|
||||
"want_group", s.OIDC.Config().AdminGroup, "have", claims.Groups)
|
||||
http.Error(w, fmt.Sprintf(
|
||||
"Signed in as %s, but that account is not in the %q group, so it cannot administer "+
|
||||
"this server.", claims.Display(), s.OIDC.Config().AdminGroup), http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
slog.Info("admin login", "account", claims.AccountID(), "method", "oidc", "from", clientIP(r))
|
||||
s.setSession(w, claims.AccountID(), claims.Display())
|
||||
// Authentication and authorisation are answered separately here. Someone who is not in the
|
||||
// admin group has still proved who they are, and their own uploads are their business to
|
||||
// manage — refusing them a session outright, as this used to, left a legitimate account with
|
||||
// no way to see or delete the data it had sent.
|
||||
admin := s.OIDC.IsAdmin(claims)
|
||||
slog.Info("login", "account", claims.AccountID(), "method", "oidc", "admin", admin,
|
||||
"from", clientIP(r))
|
||||
s.setSession(w, claims.AccountID(), claims.Display(), admin)
|
||||
http.Redirect(w, r, "/", http.StatusSeeOther)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user