Added ldap and pam authentication
This commit is contained in:
@@ -0,0 +1,129 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"io"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
const testUserHeader = "X-Uopi-User"
|
||||
|
||||
func quietLogger() *slog.Logger {
|
||||
return slog.New(slog.NewTextHandler(io.Discard, nil))
|
||||
}
|
||||
|
||||
// echoUser is a terminal handler that reports the resolved identity header so a
|
||||
// test can assert what basicAuth stamped.
|
||||
func echoUser() http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
_, _ = io.WriteString(w, r.Header.Get(testUserHeader))
|
||||
})
|
||||
}
|
||||
|
||||
func basicReq(t *testing.T, user, pass string, setHeader string) *http.Request {
|
||||
t.Helper()
|
||||
r := httptest.NewRequest(http.MethodGet, "/api/v1/me", nil)
|
||||
if user != "" || pass != "" {
|
||||
r.SetBasicAuth(user, pass)
|
||||
}
|
||||
if setHeader != "" {
|
||||
r.Header.Set(testUserHeader, setHeader) // a spoof attempt
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
// authFn that accepts only alice/secret.
|
||||
func aliceAuth(calls *int32) func(string, string) error {
|
||||
return func(user, pass string) error {
|
||||
atomic.AddInt32(calls, 1)
|
||||
if user == "alice" && pass == "secret" {
|
||||
return nil
|
||||
}
|
||||
return pamErr{}
|
||||
}
|
||||
}
|
||||
|
||||
type pamErr struct{}
|
||||
|
||||
func (pamErr) Error() string { return "bad credentials" }
|
||||
|
||||
func TestBasicAuthChallengesWithoutCredentials(t *testing.T) {
|
||||
cache := newCredCache(time.Minute)
|
||||
h := basicAuth(aliceAuth(new(int32)), testUserHeader, "uopi", true, cache, quietLogger(), echoUser())
|
||||
|
||||
rec := httptest.NewRecorder()
|
||||
h.ServeHTTP(rec, basicReq(t, "", "", ""))
|
||||
|
||||
if rec.Code != http.StatusUnauthorized {
|
||||
t.Fatalf("want 401, got %d", rec.Code)
|
||||
}
|
||||
if got := rec.Header().Get("WWW-Authenticate"); got == "" {
|
||||
t.Fatalf("missing WWW-Authenticate challenge header")
|
||||
}
|
||||
}
|
||||
|
||||
func TestBasicAuthValidCredentialsStampUser(t *testing.T) {
|
||||
cache := newCredCache(time.Minute)
|
||||
h := basicAuth(aliceAuth(new(int32)), testUserHeader, "uopi", true, cache, quietLogger(), echoUser())
|
||||
|
||||
rec := httptest.NewRecorder()
|
||||
// Client also tries to spoof the identity header; it must be ignored.
|
||||
h.ServeHTTP(rec, basicReq(t, "alice", "secret", "root"))
|
||||
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf("want 200, got %d", rec.Code)
|
||||
}
|
||||
if got := rec.Body.String(); got != "alice" {
|
||||
t.Fatalf("want resolved user alice, got %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBasicAuthInvalidCredentialsRejected(t *testing.T) {
|
||||
cache := newCredCache(time.Minute)
|
||||
h := basicAuth(aliceAuth(new(int32)), testUserHeader, "uopi", true, cache, quietLogger(), echoUser())
|
||||
|
||||
rec := httptest.NewRecorder()
|
||||
h.ServeHTTP(rec, basicReq(t, "alice", "wrong", ""))
|
||||
|
||||
if rec.Code != http.StatusUnauthorized {
|
||||
t.Fatalf("want 401, got %d", rec.Code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBasicAuthBestEffortFallsThrough(t *testing.T) {
|
||||
cache := newCredCache(time.Minute)
|
||||
// challenge=false (WebSocket path): no creds → pass through unauthenticated.
|
||||
h := basicAuth(aliceAuth(new(int32)), testUserHeader, "uopi", false, cache, quietLogger(), echoUser())
|
||||
|
||||
rec := httptest.NewRecorder()
|
||||
h.ServeHTTP(rec, basicReq(t, "", "", ""))
|
||||
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf("want 200 pass-through, got %d", rec.Code)
|
||||
}
|
||||
if got := rec.Body.String(); got != "" {
|
||||
t.Fatalf("want empty identity, got %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBasicAuthCachesSuccess(t *testing.T) {
|
||||
var calls int32
|
||||
cache := newCredCache(time.Minute)
|
||||
h := basicAuth(aliceAuth(&calls), testUserHeader, "uopi", true, cache, quietLogger(), echoUser())
|
||||
|
||||
for i := 0; i < 3; i++ {
|
||||
rec := httptest.NewRecorder()
|
||||
h.ServeHTTP(rec, basicReq(t, "alice", "secret", ""))
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf("req %d: want 200, got %d", i, rec.Code)
|
||||
}
|
||||
}
|
||||
if got := atomic.LoadInt32(&calls); got != 1 {
|
||||
t.Fatalf("want authFn called once (cached), got %d", got)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user