Merge commit from fork
Docker / Build and Push (push) Has been cancelled
github-pages / deploy (push) Has been cancelled
Docker / release (push) Has been cancelled

This commit is contained in:
h44z
2026-07-23 23:01:13 +02:00
committed by GitHub
parent 20c2d6faff
commit 207d9ae1c3
3 changed files with 132 additions and 3 deletions
+1 -1
View File
@@ -153,7 +153,7 @@ func main() {
// region API v1 (User REST API)
apiV1Auth := handlersV1.NewAuthenticationHandler(userManager)
apiV1Auth := handlersV1.NewAuthenticationHandler(authenticator, userManager)
apiV1BackendUsers := backendV1.NewUserService(cfg, userManager)
apiV1BackendPeers := backendV1.NewPeerService(cfg, wireGuardManager, userManager)
apiV1BackendInterfaces := backendV1.NewInterfaceService(cfg, wireGuardManager)
@@ -16,16 +16,22 @@ const (
)
type UserAuthenticator interface {
IsUserValid(ctx context.Context, id domain.UserIdentifier) bool
}
type UserRepository interface {
GetUser(ctx context.Context, id domain.UserIdentifier) (*domain.User, error)
}
type AuthenticationHandler struct {
authenticator UserAuthenticator
userRepo UserRepository
}
func NewAuthenticationHandler(authenticator UserAuthenticator) AuthenticationHandler {
func NewAuthenticationHandler(authenticator UserAuthenticator, userRepo UserRepository) AuthenticationHandler {
return AuthenticationHandler{
authenticator: authenticator,
userRepo: userRepo,
}
}
@@ -44,7 +50,7 @@ func (h AuthenticationHandler) LoggedIn(scopes ...Scope) func(next http.Handler)
// check if user exists in DB
ctx := domain.SetUserInfo(r.Context(), domain.SystemAdminContextUserInfo())
user, err := h.authenticator.GetUser(ctx, domain.UserIdentifier(username))
user, err := h.userRepo.GetUser(ctx, domain.UserIdentifier(username))
if err != nil {
// Abort the request with the appropriate error code
respond.JSON(w, http.StatusUnauthorized,
@@ -60,6 +66,13 @@ func (h AuthenticationHandler) LoggedIn(scopes ...Scope) func(next http.Handler)
return
}
// ensure that user is still valid
if valid := h.authenticator.IsUserValid(r.Context(), domain.UserIdentifier(user.Identifier)); !valid {
respond.JSON(w, http.StatusForbidden,
model.Error{Code: http.StatusForbidden, Message: "account disabled or locked"})
return
}
if !UserHasScopes(user, scopes...) {
// Abort the request with the appropriate error code
respond.JSON(w, http.StatusForbidden,
@@ -0,0 +1,116 @@
package handlers
import (
"context"
"net/http"
"net/http/httptest"
"strings"
"testing"
"time"
"github.com/h44z/wg-portal/internal/domain"
)
type stubUserAuthenticator struct {
valid bool
}
func (s stubUserAuthenticator) IsUserValid(ctx context.Context, id domain.UserIdentifier) bool {
return s.valid
}
type stubUserRepository struct {
user *domain.User
err error
}
func (s stubUserRepository) GetUser(ctx context.Context, id domain.UserIdentifier) (*domain.User, error) {
if s.err != nil {
return nil, s.err
}
return s.user, nil
}
func TestAuthenticationHandler_LoggedInRejectsDisabledOrLockedUser(t *testing.T) {
tests := []struct {
name string
configure func(*domain.User)
}{
{
name: "disabled user",
configure: func(user *domain.User) {
user.Disabled = &[]time.Time{time.Now()}[0]
},
},
{
name: "locked user",
configure: func(user *domain.User) {
user.Locked = &[]time.Time{time.Now()}[0]
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
user := &domain.User{
Identifier: "test-user",
ApiToken: "token",
}
tt.configure(user)
handler := NewAuthenticationHandler(stubUserAuthenticator{valid: false}, stubUserRepository{user: user})
nextCalled := false
next := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
nextCalled = true
w.WriteHeader(http.StatusOK)
})
req := httptest.NewRequest(http.MethodGet, "/", nil)
req.SetBasicAuth(string(user.Identifier), user.ApiToken)
rr := httptest.NewRecorder()
handler.LoggedIn()(next).ServeHTTP(rr, req)
if rr.Code != http.StatusForbidden {
t.Fatalf("expected status %d, got %d", http.StatusForbidden, rr.Code)
}
if nextCalled {
t.Fatal("expected downstream handler not to be called")
}
if !strings.Contains(rr.Body.String(), "account disabled or locked") {
t.Fatalf("expected error message to mention disabled or locked, got %q", rr.Body.String())
}
})
}
}
func TestAuthenticationHandler_LoggedInAllowsValidUser(t *testing.T) {
user := &domain.User{
Identifier: "test-user",
ApiToken: "token",
}
handler := NewAuthenticationHandler(stubUserAuthenticator{valid: true}, stubUserRepository{user: user})
nextCalled := false
next := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
nextCalled = true
w.WriteHeader(http.StatusOK)
})
req := httptest.NewRequest(http.MethodGet, "/", nil)
req.SetBasicAuth(string(user.Identifier), user.ApiToken)
rr := httptest.NewRecorder()
handler.LoggedIn()(next).ServeHTTP(rr, req)
if rr.Code != http.StatusOK {
t.Fatalf("expected status %d, got %d", http.StatusOK, rr.Code)
}
if !nextCalled {
t.Fatal("expected downstream handler to be called")
}
}