Files
zrok/endpoints/dynamicProxy/http_test.go

209 lines
5.8 KiB
Go

package dynamicProxy
import (
"encoding/json"
"net/http"
"net/http/httptest"
"strings"
"testing"
"github.com/openziti/edge-api/rest_model"
"github.com/openziti/sdk-golang/ziti"
"github.com/openziti/zrok/v2/controller/dynamicProxyController"
"github.com/openziti/zrok/v2/sdk/golang/sdk"
)
func TestShareHandlerOptionsBypassesFrontendAuth(t *testing.T) {
testCases := []struct {
name string
serviceCfg map[string]interface{}
cfg *config
wantFilter bool
}{
{
name: "oauth",
serviceCfg: map[string]interface{}{
"auth_scheme": string(sdk.Oauth),
"oauth": map[string]interface{}{
"provider": "github",
"authorization_check_interval": "3h",
},
},
cfg: &config{
Oauth: &oauthConfig{
CookieName: "zrok_session",
},
},
wantFilter: true,
},
{
name: "basic",
serviceCfg: map[string]interface{}{
"auth_scheme": string(sdk.Basic),
"basic_auth": map[string]interface{}{
"users": []interface{}{
map[string]interface{}{
"username": "demo",
"password": "secret",
},
},
},
},
cfg: &config{},
wantFilter: false,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
restore := stubDynamicProxyService(t, tc.serviceCfg)
defer restore()
mappings := newMappings()
mappings.nameMap["demo.example.com"] = &dynamicProxyController.FrontendMapping{
Name: "demo.example.com",
ShareToken: "share-token",
}
upstreamCalled := false
handler := shareHandler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
upstreamCalled = true
if got := r.Header.Get("Cookie"); tc.wantFilter && strings.Contains(got, "zrok_session=") {
t.Fatalf("expected session cookie to be filtered from proxied request, got %q", got)
}
w.WriteHeader(http.StatusNoContent)
}), tc.cfg, nil, nil, mappings)
req := httptest.NewRequest(http.MethodOptions, "http://demo.example.com/api", nil)
req.Host = "demo.example.com"
req.AddCookie(&http.Cookie{Name: "zrok_session", Value: "secret"})
rec := httptest.NewRecorder()
handler.ServeHTTP(rec, req)
if !upstreamCalled {
t.Fatal("expected OPTIONS request to reach upstream handler")
}
if rec.Code != http.StatusNoContent {
t.Fatalf("expected status %d, got %d", http.StatusNoContent, rec.Code)
}
if location := rec.Header().Get("Location"); location != "" {
t.Fatalf("expected no redirect location, got %q", location)
}
})
}
}
func TestShareHandlerGetStillRedirectsForOAuth(t *testing.T) {
restore := stubDynamicProxyService(t, map[string]interface{}{
"auth_scheme": string(sdk.Oauth),
"oauth": map[string]interface{}{
"provider": "github",
"authorization_check_interval": "3h",
},
})
defer restore()
mappings := newMappings()
mappings.nameMap["demo.example.com"] = &dynamicProxyController.FrontendMapping{
Name: "demo.example.com",
ShareToken: "share-token",
}
getProxied := false
handler := shareHandler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method == http.MethodGet {
getProxied = true
}
w.WriteHeader(http.StatusNoContent)
}), &config{
Oauth: &oauthConfig{
EndpointUrl: "https://oauth.example.com",
CookieName: "zrok_session",
},
}, nil, nil, mappings)
req := httptest.NewRequest(http.MethodGet, "http://demo.example.com/api", nil)
req.Host = "demo.example.com"
rec := httptest.NewRecorder()
handler.ServeHTTP(rec, req)
if getProxied {
t.Fatal("expected GET request without session to be blocked by frontend auth")
}
if rec.Code != http.StatusUnauthorized {
t.Fatalf("expected status %d, got %d", http.StatusUnauthorized, rec.Code)
}
if auth := rec.Header().Get("WWW-Authenticate"); auth != "oauth" {
t.Fatalf("expected WWW-Authenticate: oauth, got %q", auth)
}
if ct := rec.Header().Get("Content-Type"); !strings.Contains(ct, "application/json") {
t.Fatalf("expected application/json content type, got %q", ct)
}
var body map[string]string
if err := json.NewDecoder(rec.Body).Decode(&body); err != nil {
t.Fatalf("expected json body: %v", err)
}
if loginUrl, ok := body["login_url"]; !ok || !strings.Contains(loginUrl, "/github/login") {
t.Fatalf("expected login_url containing /github/login, got %q", body["login_url"])
}
}
func TestShareHandlerGetStillChallengesBasicAuth(t *testing.T) {
restore := stubDynamicProxyService(t, map[string]interface{}{
"auth_scheme": string(sdk.Basic),
"basic_auth": map[string]interface{}{
"users": []interface{}{
map[string]interface{}{
"username": "demo",
"password": "secret",
},
},
},
})
defer restore()
mappings := newMappings()
mappings.nameMap["demo.example.com"] = &dynamicProxyController.FrontendMapping{
Name: "demo.example.com",
ShareToken: "share-token",
}
upstreamCalled := false
handler := shareHandler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
upstreamCalled = true
w.WriteHeader(http.StatusNoContent)
}), &config{}, nil, nil, mappings)
req := httptest.NewRequest(http.MethodGet, "http://demo.example.com/api", nil)
req.Host = "demo.example.com"
rec := httptest.NewRecorder()
handler.ServeHTTP(rec, req)
if upstreamCalled {
t.Fatal("expected GET request without basic auth credentials to be blocked")
}
if rec.Code != http.StatusUnauthorized {
t.Fatalf("expected status %d, got %d", http.StatusUnauthorized, rec.Code)
}
}
func stubDynamicProxyService(t *testing.T, proxyConfig map[string]interface{}) func() {
t.Helper()
previous := getRefreshedService
getRefreshedService = func(_ string, _ ziti.Context) (*rest_model.ServiceDetail, bool) {
return &rest_model.ServiceDetail{
Config: map[string]map[string]interface{}{
sdk.ZrokProxyConfig: proxyConfig,
},
}, true
}
return func() {
getRefreshedService = previous
}
}