Make nmon easier to install and run without editing YAML by hand: - Settings: point-and-click forms over config.yaml (GET/PUT /api/v1/settings/form); the YAML editor moves to /settings/yaml. - First install: with no sign-in configured nmon starts in install mode, serves only the setup wizard (sign-in, basics, devices, alerting) and closes it for good once sign-in is saved. A self-signed certificate is generated when no TLS source is given. poller.interval becomes a config setting. - Reverse proxies: opt-in --http-listen plain-HTTP listener beside HTTPS; sign-in stays required. - Webhooks: Generic, Teams Adaptive Card, Slack and Discord presets, template preview and test send. - Help: the operations, coverage and profile docs served at /help. - Polling: per-device "Poll now", a dry-run test poll returning an SNMP trace, and time-limited in-memory SNMP debug recording, in the UI and as `nmon device poll`. - Ansible: deploys into install mode when no sign-in is set, and no longer overwrites the config.yaml the wizard wrote. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017TuKgyKve2XLFtrrXWMi2w
63 lines
2.3 KiB
Go
63 lines
2.3 KiB
Go
package auth_test
|
|
|
|
import (
|
|
"context"
|
|
"net/http"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
// sessionCookieFor signs in through break-glass with the given proxy headers and returns the session cookie.
|
|
func sessionCookieFor(t *testing.T, e *env, hdr map[string]string) *http.Cookie {
|
|
t.Helper()
|
|
req, err := http.NewRequestWithContext(context.Background(), http.MethodPost, e.srv.URL+"/auth/breakglass",
|
|
strings.NewReader(`{"username":"breakglass","password":"`+bgPassword+`"}`))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
req.Header.Set("Content-Type", "application/json")
|
|
for k, v := range hdr {
|
|
req.Header.Set(k, v)
|
|
}
|
|
resp, err := http.DefaultClient.Do(req)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer closeBody(t, resp)
|
|
if resp.StatusCode != http.StatusNoContent {
|
|
t.Fatalf("break-glass: %d", resp.StatusCode)
|
|
}
|
|
for _, c := range resp.Cookies() {
|
|
if c.Name != "" && c.MaxAge > 0 {
|
|
return c
|
|
}
|
|
}
|
|
t.Fatal("no session cookie")
|
|
return nil
|
|
}
|
|
|
|
func TestSessionCookieIsSecureOnlyWhenTheBrowserUsesHTTPS(t *testing.T) {
|
|
// The test server is plain HTTP on the loopback address, which is the "trusted proxy" in the first two cases.
|
|
for name, tc := range map[string]struct {
|
|
proxies []string
|
|
hdr map[string]string
|
|
wantSecure bool
|
|
}{
|
|
"direct http": {nil, nil, false},
|
|
"direct http, spoofed proto": {nil, map[string]string{"X-Forwarded-Proto": "https"}, false},
|
|
"trusted proxy says https": {[]string{"127.0.0.1", "::1"}, map[string]string{"X-Forwarded-Proto": "https"}, true},
|
|
"trusted proxy says http": {[]string{"127.0.0.1", "::1"}, map[string]string{"X-Forwarded-Proto": "http"}, false},
|
|
"trusted proxy, no proto header": {[]string{"127.0.0.1", "::1"}, nil, false},
|
|
"untrusted proxy spoofing https": {[]string{"10.0.0.0/8"}, map[string]string{"X-Forwarded-Proto": "https"}, false},
|
|
"untrusted proxy spoofing everything": {[]string{"10.0.0.0/8"}, map[string]string{"X-Forwarded-Proto": "https", "X-Forwarded-For": "203.0.113.9"}, false},
|
|
} {
|
|
t.Run(name, func(t *testing.T) {
|
|
e := newEnv(t, envOpts{proxies: tc.proxies, secure: true})
|
|
c := sessionCookieFor(t, e, tc.hdr)
|
|
if c.Secure != tc.wantSecure || !c.HttpOnly {
|
|
t.Fatalf("cookie Secure=%v HttpOnly=%v, want Secure=%v", c.Secure, c.HttpOnly, tc.wantSecure)
|
|
}
|
|
})
|
|
}
|
|
}
|