mirror of
https://github.com/h44z/wg-portal.git
synced 2026-08-13 11:16:19 +00:00
Merge commit from fork
This commit is contained in:
@@ -153,7 +153,7 @@ func main() {
|
|||||||
|
|
||||||
// region API v1 (User REST API)
|
// region API v1 (User REST API)
|
||||||
|
|
||||||
apiV1Auth := handlersV1.NewAuthenticationHandler(userManager)
|
apiV1Auth := handlersV1.NewAuthenticationHandler(authenticator, userManager)
|
||||||
apiV1BackendUsers := backendV1.NewUserService(cfg, userManager)
|
apiV1BackendUsers := backendV1.NewUserService(cfg, userManager)
|
||||||
apiV1BackendPeers := backendV1.NewPeerService(cfg, wireGuardManager, userManager)
|
apiV1BackendPeers := backendV1.NewPeerService(cfg, wireGuardManager, userManager)
|
||||||
apiV1BackendInterfaces := backendV1.NewInterfaceService(cfg, wireGuardManager)
|
apiV1BackendInterfaces := backendV1.NewInterfaceService(cfg, wireGuardManager)
|
||||||
|
|||||||
@@ -16,16 +16,22 @@ const (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type UserAuthenticator interface {
|
type UserAuthenticator interface {
|
||||||
|
IsUserValid(ctx context.Context, id domain.UserIdentifier) bool
|
||||||
|
}
|
||||||
|
|
||||||
|
type UserRepository interface {
|
||||||
GetUser(ctx context.Context, id domain.UserIdentifier) (*domain.User, error)
|
GetUser(ctx context.Context, id domain.UserIdentifier) (*domain.User, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
type AuthenticationHandler struct {
|
type AuthenticationHandler struct {
|
||||||
authenticator UserAuthenticator
|
authenticator UserAuthenticator
|
||||||
|
userRepo UserRepository
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewAuthenticationHandler(authenticator UserAuthenticator) AuthenticationHandler {
|
func NewAuthenticationHandler(authenticator UserAuthenticator, userRepo UserRepository) AuthenticationHandler {
|
||||||
return AuthenticationHandler{
|
return AuthenticationHandler{
|
||||||
authenticator: authenticator,
|
authenticator: authenticator,
|
||||||
|
userRepo: userRepo,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -44,7 +50,7 @@ func (h AuthenticationHandler) LoggedIn(scopes ...Scope) func(next http.Handler)
|
|||||||
// check if user exists in DB
|
// check if user exists in DB
|
||||||
|
|
||||||
ctx := domain.SetUserInfo(r.Context(), domain.SystemAdminContextUserInfo())
|
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 {
|
if err != nil {
|
||||||
// Abort the request with the appropriate error code
|
// Abort the request with the appropriate error code
|
||||||
respond.JSON(w, http.StatusUnauthorized,
|
respond.JSON(w, http.StatusUnauthorized,
|
||||||
@@ -60,6 +66,13 @@ func (h AuthenticationHandler) LoggedIn(scopes ...Scope) func(next http.Handler)
|
|||||||
return
|
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...) {
|
if !UserHasScopes(user, scopes...) {
|
||||||
// Abort the request with the appropriate error code
|
// Abort the request with the appropriate error code
|
||||||
respond.JSON(w, http.StatusForbidden,
|
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")
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user