test/integration/sa_oauthclient_test.go
633d6986
 package integration
 
 import (
 	"crypto/tls"
3905a097
 	"fmt"
633d6986
 	"net/http"
590fb8ce
 	"net/http/cookiejar"
633d6986
 	"net/http/httptest"
 	"net/http/httputil"
590fb8ce
 	"reflect"
633d6986
 	"testing"
 	"time"
 
590fb8ce
 	"golang.org/x/net/html"
 
633d6986
 	"github.com/RangelReale/osincli"
590fb8ce
 	"github.com/golang/glog"
633d6986
 
 	kapi "k8s.io/kubernetes/pkg/api"
 	kapierrors "k8s.io/kubernetes/pkg/api/errors"
 	"k8s.io/kubernetes/pkg/client/restclient"
dea25666
 	kclient "k8s.io/kubernetes/pkg/client/unversioned"
633d6986
 	"k8s.io/kubernetes/pkg/serviceaccount"
71d5a830
 	"k8s.io/kubernetes/pkg/util/wait"
633d6986
 
 	"github.com/openshift/origin/pkg/client"
 	"github.com/openshift/origin/pkg/cmd/util/clientcmd"
590fb8ce
 	oauthapi "github.com/openshift/origin/pkg/oauth/api"
633d6986
 	"github.com/openshift/origin/pkg/oauth/scope"
 	saoauth "github.com/openshift/origin/pkg/serviceaccounts/oauthclient"
 	testutil "github.com/openshift/origin/test/util"
590fb8ce
 	htmlutil "github.com/openshift/origin/test/util/html"
633d6986
 	testserver "github.com/openshift/origin/test/util/server"
 )
 
 func TestSAAsOAuthClient(t *testing.T) {
 	testutil.RequireEtcd(t)
9f6552c6
 	defer testutil.DumpEtcdOnFailure(t)
633d6986
 	_, clusterAdminKubeConfig, err := testserver.StartTestMaster()
 	if err != nil {
 		t.Fatalf("unexpected error: %v", err)
 	}
 
 	authorizationCodes := make(chan string, 1)
 	authorizationErrors := make(chan string, 1)
 	oauthServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
 		t.Logf("fake pod server got %v", req.URL)
 
 		if code := req.URL.Query().Get("code"); len(code) > 0 {
 			authorizationCodes <- code
 		}
 		if err := req.URL.Query().Get("error"); len(err) > 0 {
 			authorizationErrors <- err
 		}
 	}))
 	defer oauthServer.Close()
590fb8ce
 	redirectURL := oauthServer.URL + "/oauthcallback"
633d6986
 
 	clusterAdminClient, err := testutil.GetClusterAdminClient(clusterAdminKubeConfig)
 	if err != nil {
 		t.Fatalf("unexpected error: %v", err)
 	}
97e6f1de
 	clusterAdminKubeClientset, err := testutil.GetClusterAdminKubeClient(clusterAdminKubeConfig)
633d6986
 	if err != nil {
 		t.Fatalf("unexpected error: %v", err)
 	}
 	clusterAdminClientConfig, err := testutil.GetClusterAdminClientConfig(clusterAdminKubeConfig)
 	if err != nil {
 		t.Fatalf("unexpected error: %v", err)
 	}
 
 	projectName := "hammer-project"
 	if _, err := testserver.CreateNewProject(clusterAdminClient, *clusterAdminClientConfig, projectName, "harold"); err != nil {
 		t.Fatalf("unexpected error: %v", err)
 	}
97e6f1de
 	if err := testserver.WaitForServiceAccounts(clusterAdminKubeClientset, projectName, []string{"default"}); err != nil {
633d6986
 		t.Fatalf("unexpected error: %v", err)
 	}
 
590fb8ce
 	promptingClient, err := clusterAdminClient.OAuthClients().Create(&oauthapi.OAuthClient{
 		ObjectMeta:            kapi.ObjectMeta{Name: "prompting-client"},
 		Secret:                "prompting-client-secret",
 		RedirectURIs:          []string{redirectURL},
 		GrantMethod:           oauthapi.GrantHandlerPrompt,
 		RespondWithChallenges: true,
 	})
 	if err != nil {
 		t.Fatalf("unexpected error: %v", err)
 	}
 
633d6986
 	// get the SA ready with redirect URIs and secret annotations
dea25666
 	var defaultSA *kapi.ServiceAccount
633d6986
 
71d5a830
 	// retry this a couple times.  We seem to be flaking on update conflicts and missing secrets all together
dea25666
 	err = kclient.RetryOnConflict(kclient.DefaultRetry, func() error {
97e6f1de
 		defaultSA, err = clusterAdminKubeClientset.Core().ServiceAccounts(projectName).Get("default")
dea25666
 		if err != nil {
 			return err
 		}
71d5a830
 		if defaultSA.Annotations == nil {
 			defaultSA.Annotations = map[string]string{}
 		}
eed8c754
 		defaultSA.Annotations[saoauth.OAuthRedirectModelAnnotationURIPrefix+"one"] = redirectURL
71d5a830
 		defaultSA.Annotations[saoauth.OAuthWantChallengesAnnotationPrefix] = "true"
97e6f1de
 		defaultSA, err = clusterAdminKubeClientset.Core().ServiceAccounts(projectName).Update(defaultSA)
dea25666
 		return err
71d5a830
 	})
633d6986
 	if err != nil {
 		t.Fatalf("unexpected error: %v", err)
 	}
71d5a830
 
633d6986
 	var oauthSecret *kapi.Secret
71d5a830
 	// retry this a couple times.  We seem to be flaking on update conflicts and missing secrets all together
 	err = wait.PollImmediate(30*time.Millisecond, 10*time.Second, func() (done bool, err error) {
97e6f1de
 		allSecrets, err := clusterAdminKubeClientset.Core().Secrets(projectName).List(kapi.ListOptions{})
71d5a830
 		if err != nil {
 			return false, err
 		}
 		for i := range allSecrets.Items {
 			secret := allSecrets.Items[i]
 			if serviceaccount.IsServiceAccountToken(&secret, defaultSA) {
 				oauthSecret = &secret
ee47bff0
 				return true, nil
633d6986
 			}
 		}
71d5a830
 
 		return false, nil
 	})
 	if err != nil {
 		t.Fatalf("unexpected error: %v", err)
633d6986
 	}
 
590fb8ce
 	// Test with a normal OAuth client
 	{
 		oauthClientConfig := &osincli.ClientConfig{
 			ClientId:                 promptingClient.Name,
 			ClientSecret:             promptingClient.Secret,
 			AuthorizeUrl:             clusterAdminClientConfig.Host + "/oauth/authorize",
 			TokenUrl:                 clusterAdminClientConfig.Host + "/oauth/token",
 			RedirectUrl:              redirectURL,
 			SendClientSecretInParams: true,
 		}
 		t.Log("Testing unrestricted scope")
 		oauthClientConfig.Scope = ""
 		// approval steps are needed for unscoped access
3905a097
 		runOAuthFlow(t, clusterAdminClientConfig, projectName, oauthClientConfig, nil, authorizationCodes, authorizationErrors, true, true, []string{
 			"GET /oauth/authorize",
 			"received challenge",
 			"GET /oauth/authorize",
 			"redirect to /oauth/approve",
 			"form",
 			"POST /oauth/approve",
 			"redirect to /oauth/authorize",
 			"redirect to /oauthcallback",
 			"code",
 			"scope:user:full",
 		})
590fb8ce
 		// verify the persisted client authorization looks like we expect
 		if clientAuth, err := clusterAdminClient.OAuthClientAuthorizations().Get("harold:" + oauthClientConfig.ClientId); err != nil {
 			t.Fatalf("Unexpected error: %v", err)
 		} else if !reflect.DeepEqual(clientAuth.Scopes, []string{"user:full"}) {
 			t.Fatalf("Unexpected scopes: %v", clientAuth.Scopes)
 		} else {
 			// update the authorization to not contain any approved scopes
 			clientAuth.Scopes = nil
 			if _, err := clusterAdminClient.OAuthClientAuthorizations().Update(clientAuth); err != nil {
 				t.Fatalf("Unexpected error: %v", err)
 			}
 		}
 		// approval steps are needed again for unscoped access
3905a097
 		runOAuthFlow(t, clusterAdminClientConfig, projectName, oauthClientConfig, nil, authorizationCodes, authorizationErrors, true, true, []string{
 			"GET /oauth/authorize",
 			"received challenge",
 			"GET /oauth/authorize",
 			"redirect to /oauth/approve",
 			"form",
 			"POST /oauth/approve",
 			"redirect to /oauth/authorize",
 			"redirect to /oauthcallback",
 			"code",
 			"scope:user:full",
 		})
590fb8ce
 		// with the authorization stored, approval steps are skipped
3905a097
 		runOAuthFlow(t, clusterAdminClientConfig, projectName, oauthClientConfig, nil, authorizationCodes, authorizationErrors, true, true, []string{
 			"GET /oauth/authorize",
 			"received challenge",
 			"GET /oauth/authorize",
 			"redirect to /oauthcallback",
 			"code",
 			"scope:user:full",
 		})
026ba1f0
 
3905a097
 		// Approval step is needed again
590fb8ce
 		t.Log("Testing restricted scope")
3905a097
 		oauthClientConfig.Scope = "user:info user:check-access"
 		// filter to disapprove of granting the user:check-access scope
 		deniedScope := false
 		inputFilter := func(inputType, name, value string) bool {
 			if inputType == "checkbox" && name == "scope" && value == "user:check-access" {
 				deniedScope = true
 				return false
 			}
 			return true
 		}
 		// our token only gets the approved one
 		runOAuthFlow(t, clusterAdminClientConfig, projectName, oauthClientConfig, inputFilter, authorizationCodes, authorizationErrors, true, false, []string{
 			"GET /oauth/authorize",
 			"received challenge",
 			"GET /oauth/authorize",
 			"redirect to /oauth/approve",
 			"form",
 			"POST /oauth/approve",
 			"redirect to /oauth/authorize",
 			"redirect to /oauthcallback",
 			"code",
 			"scope:user:info",
 		})
 		if !deniedScope {
 			t.Errorf("Expected form filter to deny user:info scope")
 		}
 		// second time, we approve all, and our token gets all requested scopes
 		runOAuthFlow(t, clusterAdminClientConfig, projectName, oauthClientConfig, nil, authorizationCodes, authorizationErrors, true, false, []string{
 			"GET /oauth/authorize",
 			"received challenge",
 			"GET /oauth/authorize",
 			"redirect to /oauth/approve",
 			"form",
 			"POST /oauth/approve",
 			"redirect to /oauth/authorize",
 			"redirect to /oauthcallback",
 			"code",
 			"scope:" + oauthClientConfig.Scope,
 		})
 		// third time, the approval steps is not needed, and the token gets all requested scopes
 		runOAuthFlow(t, clusterAdminClientConfig, projectName, oauthClientConfig, nil, authorizationCodes, authorizationErrors, true, false, []string{
 			"GET /oauth/authorize",
 			"received challenge",
 			"GET /oauth/authorize",
 			"redirect to /oauthcallback",
 			"code",
 			"scope:" + oauthClientConfig.Scope,
 		})
633d6986
 
590fb8ce
 		// Now request an unscoped token again, and no approval should be needed
 		t.Log("Testing unrestricted scope")
 		oauthClientConfig.Scope = ""
3905a097
 		runOAuthFlow(t, clusterAdminClientConfig, projectName, oauthClientConfig, nil, authorizationCodes, authorizationErrors, true, true, []string{
 			"GET /oauth/authorize",
 			"received challenge",
 			"GET /oauth/authorize",
 			"redirect to /oauthcallback",
 			"code",
 			"scope:user:full",
 		})
633d6986
 
590fb8ce
 		clusterAdminClient.OAuthClientAuthorizations().Delete("harold:" + oauthClientConfig.ClientId)
633d6986
 	}
 
590fb8ce
 	{
 		oauthClientConfig := &osincli.ClientConfig{
 			ClientId:     serviceaccount.MakeUsername(defaultSA.Namespace, defaultSA.Name),
 			ClientSecret: string(oauthSecret.Data[kapi.ServiceAccountTokenKey]),
 			AuthorizeUrl: clusterAdminClientConfig.Host + "/oauth/authorize",
 			TokenUrl:     clusterAdminClientConfig.Host + "/oauth/token",
 			RedirectUrl:  redirectURL,
 			Scope:        scope.Join([]string{"user:info", "role:edit:" + projectName}),
 			SendClientSecretInParams: true,
 		}
 		t.Log("Testing allowed scopes")
 		// First time, the approval steps are needed
 		// Second time, the approval steps are skipped
3905a097
 		runOAuthFlow(t, clusterAdminClientConfig, projectName, oauthClientConfig, nil, authorizationCodes, authorizationErrors, true, true, []string{
 			"GET /oauth/authorize",
 			"received challenge",
 			"GET /oauth/authorize",
 			"redirect to /oauth/approve",
 			"form",
 			"POST /oauth/approve",
 			"redirect to /oauth/authorize",
 			"redirect to /oauthcallback",
 			"code",
 			"scope:" + oauthClientConfig.Scope,
 		})
 		runOAuthFlow(t, clusterAdminClientConfig, projectName, oauthClientConfig, nil, authorizationCodes, authorizationErrors, true, true, []string{
 			"GET /oauth/authorize",
 			"received challenge",
 			"GET /oauth/authorize",
 			"redirect to /oauthcallback",
 			"code",
 			"scope:" + oauthClientConfig.Scope,
 		})
590fb8ce
 		clusterAdminClient.OAuthClientAuthorizations().Delete("harold:" + oauthClientConfig.ClientId)
026ba1f0
 	}
 
590fb8ce
 	{
 		oauthClientConfig := &osincli.ClientConfig{
 			ClientId:     serviceaccount.MakeUsername(defaultSA.Namespace, defaultSA.Name),
 			ClientSecret: string(oauthSecret.Data[kapi.ServiceAccountTokenKey]),
 			AuthorizeUrl: clusterAdminClientConfig.Host + "/oauth/authorize",
 			TokenUrl:     clusterAdminClientConfig.Host + "/oauth/token",
 			RedirectUrl:  redirectURL,
 			Scope:        scope.Join([]string{"user:info", "role:edit:other-ns"}),
 			SendClientSecretInParams: true,
 		}
 		t.Log("Testing disallowed scopes")
3905a097
 		runOAuthFlow(t, clusterAdminClientConfig, projectName, oauthClientConfig, nil, authorizationCodes, authorizationErrors, false, false, []string{
590fb8ce
 			"GET /oauth/authorize",
 			"received challenge",
 			"GET /oauth/authorize",
 			"redirect to /oauthcallback",
 			"error:access_denied",
 		})
 		clusterAdminClient.OAuthClientAuthorizations().Delete("harold:" + oauthClientConfig.ClientId)
026ba1f0
 	}
590fb8ce
 
 	{
 		t.Log("Testing invalid scopes")
 		oauthClientConfig := &osincli.ClientConfig{
 			ClientId:     serviceaccount.MakeUsername(defaultSA.Namespace, defaultSA.Name),
 			ClientSecret: string(oauthSecret.Data[kapi.ServiceAccountTokenKey]),
 			AuthorizeUrl: clusterAdminClientConfig.Host + "/oauth/authorize",
 			TokenUrl:     clusterAdminClientConfig.Host + "/oauth/token",
 			RedirectUrl:  redirectURL,
 			Scope:        scope.Join([]string{"unknown-scope"}),
 			SendClientSecretInParams: true,
026ba1f0
 		}
3905a097
 		runOAuthFlow(t, clusterAdminClientConfig, projectName, oauthClientConfig, nil, authorizationCodes, authorizationErrors, false, false, []string{
590fb8ce
 			"GET /oauth/authorize",
 			"received challenge",
 			"GET /oauth/authorize",
 			"redirect to /oauthcallback",
 			"error:invalid_scope",
 		})
 		clusterAdminClient.OAuthClientAuthorizations().Delete("harold:" + oauthClientConfig.ClientId)
026ba1f0
 	}
590fb8ce
 
 	{
 		t.Log("Testing allowed scopes with failed API call")
 		oauthClientConfig := &osincli.ClientConfig{
 			ClientId:     serviceaccount.MakeUsername(defaultSA.Namespace, defaultSA.Name),
 			ClientSecret: string(oauthSecret.Data[kapi.ServiceAccountTokenKey]),
 			AuthorizeUrl: clusterAdminClientConfig.Host + "/oauth/authorize",
 			TokenUrl:     clusterAdminClientConfig.Host + "/oauth/token",
 			RedirectUrl:  redirectURL,
 			Scope:        scope.Join([]string{"user:info"}),
 			SendClientSecretInParams: true,
 		}
 		// First time, the approval is needed
 		// Second time, the approval is skipped
3905a097
 		runOAuthFlow(t, clusterAdminClientConfig, projectName, oauthClientConfig, nil, authorizationCodes, authorizationErrors, true, false, []string{
 			"GET /oauth/authorize",
 			"received challenge",
 			"GET /oauth/authorize",
 			"redirect to /oauth/approve",
 			"form",
 			"POST /oauth/approve",
 			"redirect to /oauth/authorize",
 			"redirect to /oauthcallback",
 			"code",
 			"scope:" + oauthClientConfig.Scope,
 		})
 		runOAuthFlow(t, clusterAdminClientConfig, projectName, oauthClientConfig, nil, authorizationCodes, authorizationErrors, true, false, []string{
 			"GET /oauth/authorize",
 			"received challenge",
 			"GET /oauth/authorize",
 			"redirect to /oauthcallback",
 			"code",
 			"scope:" + oauthClientConfig.Scope,
 		})
590fb8ce
 		clusterAdminClient.OAuthClientAuthorizations().Delete("harold:" + oauthClientConfig.ClientId)
026ba1f0
 	}
590fb8ce
 }
 
 func drain(ch chan string) {
 	for {
 		select {
 		case <-ch:
 		default:
 			return
 		}
026ba1f0
 	}
590fb8ce
 }
 
 type basicAuthTransport struct {
 	rt       http.RoundTripper
 	username string
 	password string
 }
 
 func (b *basicAuthTransport) RoundTrip(req *http.Request) (*http.Response, error) {
 	if len(b.username) > 0 || len(b.password) > 0 {
 		req.SetBasicAuth(b.username, b.password)
026ba1f0
 	}
590fb8ce
 	return b.rt.RoundTrip(req)
 }
 
 func runOAuthFlow(
 	t *testing.T,
 	clusterAdminClientConfig *restclient.Config,
 	projectName string,
 	oauthClientConfig *osincli.ClientConfig,
3905a097
 	inputFilter htmlutil.InputFilterFunc,
590fb8ce
 	authorizationCodes chan string,
 	authorizationErrors chan string,
 	expectGrantSuccess bool,
 	expectBuildSuccess bool,
 	expectOperations []string,
 ) {
 	drain(authorizationCodes)
 	drain(authorizationErrors)
 
 	oauthRuntimeClient, err := osincli.NewClient(oauthClientConfig)
026ba1f0
 	if err != nil {
 		t.Fatalf("unexpected error: %v", err)
 	}
590fb8ce
 	testTransport := &basicAuthTransport{rt: &http.Transport{TLSClientConfig: &tls.Config{InsecureSkipVerify: true}}}
 	oauthRuntimeClient.Transport = testTransport
026ba1f0
 
590fb8ce
 	authorizeRequest := oauthRuntimeClient.NewAuthorizeRequest(osincli.CODE)
 	req, err := http.NewRequest("GET", authorizeRequest.GetAuthorizeUrlWithParams("opaque-state").String(), nil)
026ba1f0
 	if err != nil {
 		t.Fatalf("unexpected error: %v", err)
 	}
590fb8ce
 
 	operations := []string{}
 	jar, _ := cookiejar.New(nil)
 	directHTTPClient := &http.Client{
 		Transport: testTransport,
 		CheckRedirect: func(redirectReq *http.Request, via []*http.Request) error {
 			glog.Infof("302 Location: %s", redirectReq.URL.String())
 			req = redirectReq
 			operations = append(operations, "redirect to "+redirectReq.URL.Path)
 			return nil
 		},
 		Jar: jar,
026ba1f0
 	}
 
590fb8ce
 	for {
 		glog.Infof("%s %s", req.Method, req.URL.String())
 		operations = append(operations, req.Method+" "+req.URL.Path)
 
 		// Always set the csrf header
 		req.Header.Set("X-CSRF-Token", "1")
 		resp, err := directHTTPClient.Do(req)
 		if err != nil {
 			glog.Infof("%#v", operations)
 			glog.Infof("%#v", jar)
 			glog.Errorf("Error %v\n%#v\n%#v", err, err, resp)
 			t.Errorf("Error %v\n%#v\n%#v", err, err, resp)
 			return
 		}
 		defer resp.Body.Close()
 
 		// Save the current URL for reference
 		currentURL := req.URL
 
 		if resp.StatusCode == 401 {
 			// Set up a username and password once we're challenged
 			testTransport.username = "harold"
 			testTransport.password = "any-pass"
 			operations = append(operations, "received challenge")
 			continue
 		}
 
 		if resp.StatusCode != 200 {
 			responseDump, _ := httputil.DumpResponse(resp, true)
 			t.Errorf("Unexpected response %s", string(responseDump))
 			return
 		}
 
 		doc, err := html.Parse(resp.Body)
 		if err != nil {
 			t.Error(err)
 			return
 		}
 		forms := htmlutil.GetElementsByTagName(doc, "form")
 		// if there's a single form, submit it
 		if len(forms) > 1 {
 			t.Errorf("More than one form encountered: %d", len(forms))
 			return
 		}
 		if len(forms) == 0 {
 			break
 		}
3905a097
 		req, err = htmlutil.NewRequestFromForm(forms[0], currentURL, inputFilter)
590fb8ce
 		if err != nil {
 			t.Error(err)
 			return
 		}
 		operations = append(operations, "form")
633d6986
 	}
 
 	authorizationCode := ""
 	select {
 	case authorizationCode = <-authorizationCodes:
590fb8ce
 		operations = append(operations, "code")
 	case authorizationError := <-authorizationErrors:
 		operations = append(operations, "error:"+authorizationError)
 	case <-time.After(5 * time.Second):
 		t.Error("didn't get a code or an error")
633d6986
 	}
 
590fb8ce
 	if len(authorizationCode) > 0 {
 		accessRequest := oauthRuntimeClient.NewAccessRequest(osincli.AUTHORIZATION_CODE, &osincli.AuthorizeData{Code: authorizationCode})
 		accessData, err := accessRequest.GetToken()
 		if err != nil {
 			t.Errorf("unexpected error: %v", err)
 			return
 		}
3905a097
 		operations = append(operations, fmt.Sprintf("scope:%v", accessData.ResponseData["scope"]))
633d6986
 
590fb8ce
 		whoamiConfig := clientcmd.AnonymousClientConfig(clusterAdminClientConfig)
 		whoamiConfig.BearerToken = accessData.AccessToken
 		whoamiClient, err := client.New(&whoamiConfig)
 		if err != nil {
 			t.Errorf("unexpected error: %v", err)
 			return
 		}
633d6986
 
590fb8ce
 		_, err = whoamiClient.Builds(projectName).List(kapi.ListOptions{})
 		if expectBuildSuccess && err != nil {
 			t.Errorf("unexpected error: %v", err)
 			return
 		}
 		if !expectBuildSuccess && !kapierrors.IsForbidden(err) {
 			t.Errorf("expected forbidden error, got %v", err)
 			return
 		}
 
 		user, err := whoamiClient.Users().Get("~")
 		if err != nil {
 			t.Errorf("unexpected error: %v", err)
 			return
 		}
 		if user.Name != "harold" {
 			t.Errorf("expected %v, got %v", "harold", user.Name)
 			return
 		}
633d6986
 	}
3905a097
 
 	if !reflect.DeepEqual(operations, expectOperations) {
 		t.Errorf("Expected:\n%#v\nGot\n%#v", expectOperations, operations)
 	}
 
633d6986
 }