feat: Betrieb hinter einem Anmelde-Proxy, Schlüssel für die App-Liste
Marc stellt Pangolin davor und will keinen zweiten Anmeldeschirm. - HONEYOS_PROXY_AUTH=true: HoneyOS prüft nicht mehr selbst, der Start sagt deutlich, dass der Container dann nur über den Proxy erreichbar sein darf. Mit HONEYOS_USER_HEADER bekommt jeder Name seine eigenen Einstellungen. - HONEYOS_APPS_KEY schützt /api/apps. Nötig, weil HoneyOS auf einer anderen Site liegt als die Apps (duckdns.org steht auf der Public-Suffix-Liste) – der Browser schickt die Sitzung dort nicht mit, und der Pfad muss im Proxy von der Anmeldung befreit sein. Beim Durchspielen mit zwei echten Hostnamen fiel ein Loch auf: Ohne eigene Anmeldung gilt jede Anfrage als angemeldet, also gab der Rückfall auf diese Prüfung die Liste an jeden heraus – auch ohne oder mit falschem Schlüssel. Der Rückfall greift jetzt nur noch, wo es wirklich eine Sitzung geben kann. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
905b136516
commit
1c3218939b
8 files changed
+382
-45
No files matched your search
+154
@@ -386,3 +386,157 @@ func signIn(t *testing.T, h http.Handler) (sess, csrf *http.Cookie) {
|
||||
}
|
||||
return sess, csrf
|
||||
}
|
||||
|
||||
// ── Betrieb hinter einem Anmelde-Proxy ─────────────────────────────────────
|
||||
// Kein eigener Anmeldeschirm: Pangolin o. ä. hat die Tür schon zugemacht.
|
||||
|
||||
func proxyServer(t *testing.T, cfg Config) *Server {
|
||||
t.Helper()
|
||||
store, err := OpenStore(dataPath(t.TempDir()))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
cfg.SessionSecret = strings.Repeat("k", 40)
|
||||
return &Server{cfg: cfg, auth: NewAuth(cfg), store: store}
|
||||
}
|
||||
|
||||
func TestProxyBetriebLässtDurch(t *testing.T) {
|
||||
h := proxyServer(t, Config{ProxyAuth: true}).routes()
|
||||
|
||||
rec := httptest.NewRecorder()
|
||||
h.ServeHTTP(rec, httptest.NewRequest("GET", "/api/me", nil))
|
||||
if rec.Code != 200 {
|
||||
t.Fatalf("/api/me hinter dem Proxy: %d", rec.Code)
|
||||
}
|
||||
// Kein Anmeldeschirm: /api/public darf nicht behaupten, es gäbe OIDC.
|
||||
rec = httptest.NewRecorder()
|
||||
h.ServeHTTP(rec, httptest.NewRequest("GET", "/api/public", nil))
|
||||
var pub struct{ OIDC, SignedIn bool }
|
||||
_ = json.Unmarshal(rec.Body.Bytes(), &pub)
|
||||
if pub.OIDC || !pub.SignedIn {
|
||||
t.Errorf("die Oberfläche würde einen Anmeldeschirm zeigen: %+v", pub)
|
||||
}
|
||||
}
|
||||
|
||||
// Nennt der Proxy einen Namen, bekommt jeder Name seine eigenen Einstellungen.
|
||||
func TestProxyKopfzeileTrenntEinstellungen(t *testing.T) {
|
||||
s := proxyServer(t, Config{ProxyAuth: true, UserHeader: "Remote-User"})
|
||||
h := s.routes()
|
||||
|
||||
put := func(user, body string) {
|
||||
req := httptest.NewRequest("PUT", "/api/settings", strings.NewReader(body))
|
||||
if user != "" {
|
||||
req.Header.Set("Remote-User", user)
|
||||
}
|
||||
rec := httptest.NewRecorder()
|
||||
h.ServeHTTP(rec, req)
|
||||
if rec.Code != http.StatusNoContent {
|
||||
t.Fatalf("PUT für %q: %d", user, rec.Code)
|
||||
}
|
||||
}
|
||||
put("marc", `{"phosphor":"green"}`)
|
||||
put("gast", `{"phosphor":"ice"}`)
|
||||
|
||||
if got := s.store.User("proxy:marc").Settings["phosphor"]; got != "green" {
|
||||
t.Errorf("marc: %q", got)
|
||||
}
|
||||
if got := s.store.User("proxy:gast").Settings["phosphor"]; got != "ice" {
|
||||
t.Errorf("gast: %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
// ── Schlüssel für die App-Liste ────────────────────────────────────────────
|
||||
|
||||
func TestAppListeBrauchtDenSchlüssel(t *testing.T) {
|
||||
idp := newFakeIdP(t, "honeyos")
|
||||
s := newTestServer(t, idp.URL)
|
||||
s.cfg.AppsKey = "geheim123"
|
||||
s.auth = NewAuth(s.cfg)
|
||||
h := s.routes()
|
||||
|
||||
get := func(mod func(*http.Request)) int {
|
||||
req := httptest.NewRequest("GET", "/api/apps", nil)
|
||||
if mod != nil {
|
||||
mod(req)
|
||||
}
|
||||
rec := httptest.NewRecorder()
|
||||
h.ServeHTTP(rec, req)
|
||||
return rec.Code
|
||||
}
|
||||
|
||||
if code := get(nil); code != http.StatusUnauthorized {
|
||||
t.Errorf("ohne Schlüssel: %d, erwartet 401", code)
|
||||
}
|
||||
if code := get(func(r *http.Request) { r.Header.Set("X-HoneyOS-Key", "falsch") }); code != http.StatusUnauthorized {
|
||||
t.Errorf("falscher Schlüssel: %d, erwartet 401", code)
|
||||
}
|
||||
if code := get(func(r *http.Request) { r.Header.Set("X-HoneyOS-Key", "geheim123") }); code != 200 {
|
||||
t.Errorf("richtiger Schlüssel: %d, erwartet 200", code)
|
||||
}
|
||||
if code := get(func(r *http.Request) { r.URL.RawQuery = "key=geheim123" }); code != 200 {
|
||||
t.Errorf("Schlüssel als Parameter: %d, erwartet 200", code)
|
||||
}
|
||||
}
|
||||
|
||||
// Der Schlüssel öffnet nur das Lesen der Liste, nicht das Schreiben.
|
||||
func TestSchlüsselDarfNichtSchreiben(t *testing.T) {
|
||||
idp := newFakeIdP(t, "honeyos")
|
||||
s := newTestServer(t, idp.URL)
|
||||
s.cfg.AppsKey = "geheim123"
|
||||
s.auth = NewAuth(s.cfg)
|
||||
h := s.routes()
|
||||
|
||||
req := httptest.NewRequest("PUT", "/api/apps", strings.NewReader(`[{"id":"x","url":"https://a.test"}]`))
|
||||
req.Header.Set("X-HoneyOS-Key", "geheim123")
|
||||
rec := httptest.NewRecorder()
|
||||
h.ServeHTTP(rec, req)
|
||||
if rec.Code != http.StatusUnauthorized {
|
||||
t.Fatalf("Schreiben mit Schlüssel: %d, erwartet 401", rec.Code)
|
||||
}
|
||||
}
|
||||
|
||||
// Die Oberfläche muss den Schlüssel mitbekommen, sonst fragt sie sich selbst aus.
|
||||
func TestStartseiteTrägtDenSchlüssel(t *testing.T) {
|
||||
s := proxyServer(t, Config{ProxyAuth: true, AppsKey: "geheim123"})
|
||||
rec := httptest.NewRecorder()
|
||||
s.routes().ServeHTTP(rec, httptest.NewRequest("GET", "/", nil))
|
||||
|
||||
if !strings.Contains(rec.Body.String(), `name="honeyos-key" content="geheim123"`) {
|
||||
t.Fatal("Schlüssel steht nicht in der Startseite")
|
||||
}
|
||||
|
||||
// Ohne Schlüssel darf auch nichts drinstehen.
|
||||
s2 := proxyServer(t, Config{ProxyAuth: true})
|
||||
rec = httptest.NewRecorder()
|
||||
s2.routes().ServeHTTP(rec, httptest.NewRequest("GET", "/", nil))
|
||||
if strings.Contains(rec.Body.String(), "honeyos-key") {
|
||||
t.Error("ohne Schlüssel steht trotzdem eine Meta-Angabe in der Seite")
|
||||
}
|
||||
}
|
||||
|
||||
// Der Fall, der beim Durchspielen aufgefallen ist: Ohne eigene Anmeldung gilt
|
||||
// jede Anfrage als angemeldet. Der Schlüssel darf dann NICHT auf diese Prüfung
|
||||
// zurückfallen, sonst gibt der freigegebene Pfad die Liste an jeden heraus.
|
||||
func TestSchlüsselGiltAuchHinterDemProxy(t *testing.T) {
|
||||
h := proxyServer(t, Config{ProxyAuth: true, AppsKey: "geheim123"}).routes()
|
||||
|
||||
get := func(key string) int {
|
||||
req := httptest.NewRequest("GET", "/api/apps", nil)
|
||||
if key != "" {
|
||||
req.Header.Set("X-HoneyOS-Key", key)
|
||||
}
|
||||
rec := httptest.NewRecorder()
|
||||
h.ServeHTTP(rec, req)
|
||||
return rec.Code
|
||||
}
|
||||
|
||||
if code := get(""); code != http.StatusUnauthorized {
|
||||
t.Errorf("ohne Schlüssel hinter dem Proxy: %d, erwartet 401", code)
|
||||
}
|
||||
if code := get("falsch"); code != http.StatusUnauthorized {
|
||||
t.Errorf("falscher Schlüssel hinter dem Proxy: %d, erwartet 401", code)
|
||||
}
|
||||
if code := get("geheim123"); code != 200 {
|
||||
t.Errorf("richtiger Schlüssel: %d, erwartet 200", code)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user