http: session/login reject disabled users; mid-session disable kicks immediately

This commit is contained in:
2026-05-05 09:22:07 +01:00
parent c1e974aad9
commit cbdd94ca12
4 changed files with 60 additions and 0 deletions
+3
View File
@@ -59,6 +59,9 @@ func (s *Server) authenticateAndSession(w stdhttp.ResponseWriter, r *stdhttp.Req
if err := auth.VerifyPassword(u.PasswordHash, password); err != nil { if err := auth.VerifyPassword(u.PasswordHash, password); err != nil {
return nil, errInvalidCredentials return nil, errInvalidCredentials
} }
if u.DisabledAt != nil {
return nil, errInvalidCredentials
}
token, err := auth.NewToken() token, err := auth.NewToken()
if err != nil { if err != nil {
+6
View File
@@ -152,6 +152,12 @@ func (s *Server) requireUser(r *stdhttp.Request) (*store.User, bool) {
if err != nil { if err != nil {
return nil, false return nil, false
} }
if u.DisabledAt != nil {
// Disabled mid-session — kill the session and reject the
// request as if it were unauthenticated.
_ = s.deps.Store.DeleteSession(r.Context(), auth.HashToken(c.Value))
return nil, false
}
return u, true return u, true
} }
+47
View File
@@ -1,10 +1,13 @@
package http package http
import ( import (
"bytes"
"encoding/json"
stdhttp "net/http" stdhttp "net/http"
"net/http/httptest" "net/http/httptest"
"strings" "strings"
"testing" "testing"
"time"
"gitea.dcglab.co.uk/steve/restic-manager/internal/store" "gitea.dcglab.co.uk/steve/restic-manager/internal/store"
) )
@@ -95,6 +98,50 @@ func TestRequireRoleUnauthenticated401OnAPI(t *testing.T) {
} }
} }
func TestRequireRoleRejectsDisabledMidSession(t *testing.T) {
t.Parallel()
srv, urlBase := newTestServer(t, false)
uid := makeUser(t, srv, "victim", store.RoleOperator)
cookie := loginAs(t, srv, uid)
// Disable the user *while their session is still valid*.
if err := srv.deps.Store.DisableUser(t.Context(), uid, time.Now().UTC()); err != nil {
t.Fatalf("disable: %v", err)
}
req, _ := stdhttp.NewRequest("GET", urlBase+"/api/hosts", nil)
req.AddCookie(cookie)
res, err := stdhttp.DefaultClient.Do(req)
if err != nil {
t.Fatalf("GET: %v", err)
}
defer res.Body.Close()
if res.StatusCode != stdhttp.StatusUnauthorized {
t.Errorf("status: got %d want 401", res.StatusCode)
}
}
func TestLoginRejectsDisabledUser(t *testing.T) {
t.Parallel()
srv, urlBase := newTestServer(t, false)
uid := makeUser(t, srv, "disabled1", store.RoleOperator)
if err := srv.deps.Store.DisableUser(t.Context(), uid, time.Now().UTC()); err != nil {
t.Fatalf("disable: %v", err)
}
body, _ := json.Marshal(map[string]string{
"username": "disabled1", "password": "test-password",
})
res, err := stdhttp.Post(urlBase+"/api/auth/login", "application/json", bytes.NewReader(body))
if err != nil {
t.Fatalf("POST: %v", err)
}
defer res.Body.Close()
if res.StatusCode != stdhttp.StatusUnauthorized {
t.Errorf("status: got %d want 401", res.StatusCode)
}
}
func TestAdminBandRejectsOperator(t *testing.T) { func TestAdminBandRejectsOperator(t *testing.T) {
t.Parallel() t.Parallel()
// This test will start asserting 403 once Task B4 mounts /api/users // This test will start asserting 403 once Task B4 mounts /api/users
+4
View File
@@ -66,6 +66,10 @@ func (s *Server) sessionUser(r *stdhttp.Request) (*ui.User, error) {
} }
return nil, err return nil, err
} }
if u.DisabledAt != nil {
_ = s.deps.Store.DeleteSession(r.Context(), auth.HashToken(c.Value))
return nil, nil
}
return &ui.User{ID: u.ID, Username: u.Username, Role: string(u.Role)}, nil return &ui.User{ID: u.ID, Username: u.Username, Role: string(u.Role)}, nil
} }