mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-10-27 05:57:27 +00:00
2ce3605454
Two routes registered without a mux method pattern changed state on any
method. The demo-mode read-only guard and the CSRF check both admit GET and
HEAD as reads, and the SameSite=Lax session cookie rides cross-site
top-level GET navigations, so each was writable by a safe method:
- /api/discovery/settings had no method check, so a GET or HEAD carrying a
JSON body changed the discovery staleness threshold, including on a
DEMO_MODE instance.
- SP-initiated SAML logout at /api/saml/{id}/logout cleared the session and
redirected to the IdP on GET. Nothing in the UI calls it, the /api/logout
fallback it delegates to is already POST-only, and the IdP's
LogoutResponse returns to /slo, not here.
Add requireRequestMethod (405 with Allow) as the shared handler-level
guard. Discovery settings keeps PUT, its documented method, and POST, which
it accepted before; SAML logout takes POST only. An audit of every route in
internal/api found no other caller-requested write reachable by GET or HEAD
beyond the Assistant session routes guarded in #2670. GET flows the
protocol requires (OIDC login and callback, SAML login initiation and SLO,
magic-link and cloud handoff, checkout bridges) and reads that run lazy
initialization or idempotent reconciliation are left as they are. One
known exception is deferred: single-resource report generation still runs
on GET and can call the configured AI narrator, which records provider
usage in the cost ledger; moving it behind POST changes the report API the
UI downloads from, so it is tracked separately.
70 lines
2.2 KiB
Go
70 lines
2.2 KiB
Go
package api
|
|
|
|
import (
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/rcourtman/pulse-go-rewrite/internal/config"
|
|
)
|
|
|
|
// TestSAMLRuntimeEndpointsUseCommunitySSOFeature verifies that the SAML
|
|
// login-flow endpoints (/api/saml/{id}/login, /acs, /metadata, /logout, /slo)
|
|
// do not return the old paid-feature 402 when no paid license is active. SAML
|
|
// is part of the Community SSO capability.
|
|
func TestSAMLRuntimeEndpointsUseCommunitySSOFeature(t *testing.T) {
|
|
tempDir := t.TempDir()
|
|
cfg := &config.Config{DataPath: tempDir, ConfigPath: tempDir}
|
|
router := NewRouter(cfg, nil, nil, nil, nil, "1.0.0")
|
|
handler := router.Handler()
|
|
|
|
cases := []struct {
|
|
method string
|
|
path string
|
|
}{
|
|
{http.MethodGet, "/api/saml/test-provider/login"},
|
|
{http.MethodPost, "/api/saml/test-provider/acs"},
|
|
{http.MethodGet, "/api/saml/test-provider/metadata"},
|
|
{http.MethodPost, "/api/saml/test-provider/logout"},
|
|
{http.MethodPost, "/api/saml/test-provider/slo"},
|
|
}
|
|
|
|
for _, tc := range cases {
|
|
var req *http.Request
|
|
if tc.method == http.MethodPost {
|
|
req = httptest.NewRequest(tc.method, tc.path, strings.NewReader(`<samlp:Response/>`))
|
|
} else {
|
|
req = httptest.NewRequest(tc.method, tc.path, nil)
|
|
}
|
|
rec := httptest.NewRecorder()
|
|
handler.ServeHTTP(rec, req)
|
|
|
|
if rec.Code == http.StatusPaymentRequired {
|
|
t.Errorf("%s %s: should not get 402 for Community SSO, got body: %s", tc.method, tc.path, rec.Body.String())
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestSAMLRuntimeNoUpgradeResponse verifies SAML runtime endpoints do not emit
|
|
// upgrade metadata when the provider is missing.
|
|
func TestSAMLRuntimeNoUpgradeResponse(t *testing.T) {
|
|
tempDir := t.TempDir()
|
|
cfg := &config.Config{DataPath: tempDir, ConfigPath: tempDir}
|
|
router := NewRouter(cfg, nil, nil, nil, nil, "1.0.0")
|
|
handler := router.Handler()
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/api/saml/test-provider/login", nil)
|
|
rec := httptest.NewRecorder()
|
|
handler.ServeHTTP(rec, req)
|
|
|
|
if rec.Code == http.StatusPaymentRequired {
|
|
t.Fatalf("expected non-402 for Community SSO, got %d: %s", rec.Code, rec.Body.String())
|
|
}
|
|
|
|
body := rec.Body.String()
|
|
if strings.Contains(body, "upgrade_url") || strings.Contains(body, "advanced_sso") {
|
|
t.Errorf("SAML runtime returned old upgrade response body: %s", body)
|
|
}
|
|
}
|