package integration
import (
"bytes"
"crypto/tls"
"encoding/json"
"fmt"
"html/template"
"io/ioutil"
"net/http"
"net/url"
"path"
"regexp"
"strings"
"testing"
knet "k8s.io/kubernetes/pkg/util/net"
configapi "github.com/openshift/origin/pkg/cmd/server/api"
testutil "github.com/openshift/origin/test/util"
testserver "github.com/openshift/origin/test/util/server"
)
// simulate embedding the given string in a template href
func templateEscapeHref(test *testing.T, s string) string {
prefix := `<a href="`
suffix := `">`
b := new(bytes.Buffer)
t := template.Must(template.New("foo").Parse(fmt.Sprintf(`%s{{.}}%s`, prefix, suffix)))
if err := t.Execute(b, s); err != nil {
test.Fatalf("unexpected error escaping %s: %v", s, err)
return ""
}
escaped := b.String()
return escaped[len(prefix) : len(escaped)-len(suffix)]
}
func tryAccessURL(t *testing.T, url string, expectedStatus int, expectedRedirectLocation string, expectedLinks []string) *http.Response {
transport := knet.SetTransportDefaults(&http.Transport{
TLSClientConfig: &tls.Config{
InsecureSkipVerify: true,
},
})
req, err := http.NewRequest("GET", url, nil)
req.Header.Set("Accept", "text/html")
resp, err := transport.RoundTrip(req)
if err != nil {
t.Errorf("Unexpected error while accessing %q: %v", url, err)
return nil
}
if resp.StatusCode != expectedStatus {
t.Errorf("Expected status %d for %q, got %d", expectedStatus, url, resp.StatusCode)
}
// ignore query parameters
location := resp.Header.Get("Location")
location = strings.SplitN(location, "?", 2)[0]
if location != expectedRedirectLocation {
t.Errorf("Expected redirection to %q for %q, got %q instead", expectedRedirectLocation, url, location)
}
if expectedLinks != nil {
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
t.Errorf("failed to read reposponse's body: %v", err)
} else {
for _, linkRegexp := range expectedLinks {
matched, err := regexp.Match(linkRegexp, body)
if err != nil {
t.Errorf("unexpected error: %v", err)
} else if !matched {
t.Errorf("Expected response body to match %s", linkRegexp)
t.Logf("Response body was %s", body)
}
}
}
}
return resp
}
func TestAccessOriginWebConsole(t *testing.T) {
testutil.RequireEtcd(t)
defer testutil.DumpEtcdOnFailure(t)
masterOptions, err := testserver.DefaultMasterOptions()
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if _, err = testserver.StartConfiguredMaster(masterOptions); err != nil {
t.Fatalf("unexpected error: %v", err)
}
for endpoint, exp := range map[string]struct {
statusCode int
location string
}{
"": {http.StatusFound, masterOptions.AssetConfig.PublicURL},
"healthz": {http.StatusOK, ""},
"login?then=%2F": {http.StatusOK, ""},
"oauth/token/request": {http.StatusFound, masterOptions.AssetConfig.MasterPublicURL + "/oauth/authorize"},
"console": {http.StatusMovedPermanently, "/console/"},
"console/": {http.StatusOK, ""},
"console/java": {http.StatusOK, ""},
} {
url := masterOptions.AssetConfig.MasterPublicURL + "/" + endpoint
tryAccessURL(t, url, exp.statusCode, exp.location, nil)
}
}
func TestAccessDisabledWebConsole(t *testing.T) {
testutil.RequireEtcd(t)
defer testutil.DumpEtcdOnFailure(t)
masterOptions, err := testserver.DefaultMasterOptions()
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
masterOptions.DisabledFeatures.Add(configapi.FeatureWebConsole)
if _, err := testserver.StartConfiguredMaster(masterOptions); err != nil {
t.Fatalf("unexpected error: %v", err)
}
resp := tryAccessURL(t, masterOptions.AssetConfig.MasterPublicURL+"/", http.StatusOK, "", nil)
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
t.Errorf("failed to read reposponse's body: %v", err)
} else {
var value interface{}
if err = json.Unmarshal(body, &value); err != nil {
t.Errorf("expected json body which couldn't be parsed: %v, got: %s", err, body)
}
}
for endpoint, exp := range map[string]struct {
statusCode int
location string
}{
"healthz": {http.StatusOK, ""},
"login?then=%2F": {http.StatusOK, ""},
"oauth/token/request": {http.StatusFound, masterOptions.AssetConfig.MasterPublicURL + "/oauth/authorize"},
"console": {http.StatusForbidden, ""},
"console/": {http.StatusForbidden, ""},
"console/java": {http.StatusForbidden, ""},
} {
url := masterOptions.AssetConfig.MasterPublicURL + "/" + endpoint
tryAccessURL(t, url, exp.statusCode, exp.location, nil)
}
}
func TestAccessOriginWebConsoleMultipleIdentityProviders(t *testing.T) {
testutil.RequireEtcd(t)
defer testutil.DumpEtcdOnFailure(t)
masterOptions, err := testserver.DefaultMasterOptions()
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
// Replace the default IdentityProvider with an AllowAll provider
masterOptions.OAuthConfig.IdentityProviders[0] = configapi.IdentityProvider{
Name: "foo",
UseAsChallenger: true,
UseAsLogin: true,
MappingMethod: "claim",
Provider: &configapi.AllowAllPasswordIdentityProvider{},
}
// Set up a second AllowAll provider
masterOptions.OAuthConfig.IdentityProviders = append(masterOptions.OAuthConfig.IdentityProviders, configapi.IdentityProvider{
Name: "bar",
UseAsChallenger: true,
UseAsLogin: true,
MappingMethod: "claim",
Provider: &configapi.AllowAllPasswordIdentityProvider{},
})
// Set up a third AllowAll provider with a space in the name and some unicode characters
masterOptions.OAuthConfig.IdentityProviders = append(masterOptions.OAuthConfig.IdentityProviders, configapi.IdentityProvider{
Name: "Iñtërnâtiônà lizætiøn, !@#$^&*()",
UseAsChallenger: true,
UseAsLogin: true,
MappingMethod: "claim",
Provider: &configapi.AllowAllPasswordIdentityProvider{},
})
// Launch the configured server
if _, err = testserver.StartConfiguredMaster(masterOptions); err != nil {
t.Fatalf("unexpected error: %v", err)
}
// Create a map of URLs to test
type urlResults struct {
statusCode int
location string
}
urlMap := make(map[string]urlResults)
linkRegexps := make([]string, 0)
// Verify that the plain /login URI is unavailable when multiple IDPs are in use.
urlMap["/login"] = urlResults{http.StatusForbidden, ""}
// Create the common base URLs
escapedPublicURL := url.QueryEscape(masterOptions.OAuthConfig.AssetPublicURL)
loginSelectorBase := "/oauth/authorize?client_id=openshift-web-console&response_type=token&state=%2F&redirect_uri=" + escapedPublicURL
// Iterate through each of the providers and verify that they redirect to
// the appropriate login page and that the login page exists.
// This is done in a loop so that we can add an arbitrary additional set
// of providers to test.
for _, value := range masterOptions.OAuthConfig.IdentityProviders {
// Query-encode the idp=<provider name> parameter name and value
idpQueryParam := url.Values{"idp": []string{value.Name}}.Encode()
// Construct a URL that will select that IDP
providerSelectionURL := loginSelectorBase + "&" + idpQueryParam
// URL-path-encode the idp name to construct the login page URL
loginURL := (&url.URL{Path: path.Join("/login", value.Name)}).String()
// Expect the providerSelectionURL to redirect to the loginURL
urlMap[providerSelectionURL] = urlResults{http.StatusFound, loginURL}
// Expect the loginURL to be valid (requires a 'then' param)
urlMap[loginURL+"?then=%2F"] = urlResults{http.StatusOK, ""}
// escape the query param the way the template will
templateIDPParam := templateEscapeHref(t, idpQueryParam)
// quote for the regex
regexIDPParam := regexp.QuoteMeta(templateIDPParam)
// Expect to see a link to the provider selection page URL with the idp param
linkRegexps = append(linkRegexps, fmt.Sprintf(`/oauth/authorize\?(.*&)?%s(&|")`, regexIDPParam))
}
// Test the loginSelectorBase for links to all of the IDPs
url := masterOptions.AssetConfig.MasterPublicURL + loginSelectorBase
tryAccessURL(t, url, http.StatusOK, "", linkRegexps)
// Test all of these URLs
for endpoint, exp := range urlMap {
url := masterOptions.AssetConfig.MasterPublicURL + endpoint
tryAccessURL(t, url, exp.statusCode, exp.location, nil)
}
}