package tokencmd
import (
"bytes"
"errors"
"fmt"
"net/http"
"net/http/httptest"
"testing"
"k8s.io/kubernetes/pkg/client/restclient"
)
type unloadableNegotiator struct {
releaseCalls int
}
func (n *unloadableNegotiator) Load() error {
return errors.New("Load failed")
}
func (n *unloadableNegotiator) InitSecContext(requestURL string, challengeToken []byte) (tokenToSend []byte, err error) {
return nil, errors.New("InitSecContext failed")
}
func (n *unloadableNegotiator) IsComplete() bool {
return false
}
func (n *unloadableNegotiator) Release() error {
n.releaseCalls++
return errors.New("Release failed")
}
type failingNegotiator struct {
releaseCalls int
}
func (n *failingNegotiator) Load() error {
return nil
}
func (n *failingNegotiator) InitSecContext(requestURL string, challengeToken []byte) (tokenToSend []byte, err error) {
return nil, errors.New("InitSecContext failed")
}
func (n *failingNegotiator) IsComplete() bool {
return false
}
func (n *failingNegotiator) Release() error {
n.releaseCalls++
return errors.New("Release failed")
}
type successfulNegotiator struct {
rounds int
initSecContextCalls int
loadCalls int
releaseCalls int
}
func (n *successfulNegotiator) Load() error {
n.loadCalls++
return nil
}
func (n *successfulNegotiator) InitSecContext(requestURL string, challengeToken []byte) (tokenToSend []byte, err error) {
n.initSecContextCalls++
if n.initSecContextCalls > n.rounds {
return nil, fmt.Errorf("InitSecContext: expected %d calls, saw %d", n.rounds, n.initSecContextCalls)
}
if n.initSecContextCalls == 1 {
if len(challengeToken) > 0 {
return nil, errors.New("expected empty token for first challenge")
}
} else {
expectedChallengeToken := fmt.Sprintf("challenge%d", n.initSecContextCalls)
if string(challengeToken) != expectedChallengeToken {
return nil, fmt.Errorf("expected challenge token '%s', got '%s'", expectedChallengeToken, string(challengeToken))
}
}
return []byte(fmt.Sprintf("response%d", n.initSecContextCalls)), nil
}
func (n *successfulNegotiator) IsComplete() bool {
return n.initSecContextCalls == n.rounds
}
func (n *successfulNegotiator) Release() error {
n.releaseCalls++
return nil
}
func TestRequestToken(t *testing.T) {
type req struct {
authorization string
}
type resp struct {
status int
location string
wwwAuthenticate []string
}
type requestResponse struct {
expectedRequest req
serverResponse resp
}
var verifyReleased func(test string, handler ChallengeHandler)
verifyReleased = func(test string, handler ChallengeHandler) {
switch handler := handler.(type) {
case *MultiHandler:
for _, subhandler := range handler.allHandlers {
verifyReleased(test, subhandler)
}
case *BasicChallengeHandler:
// we don't care
case *NegotiateChallengeHandler:
switch negotiator := handler.negotiater.(type) {
case *successfulNegotiator:
if negotiator.releaseCalls != 1 {
t.Errorf("%s: expected one call to Release(), saw %d", test, negotiator.releaseCalls)
}
case *failingNegotiator:
if negotiator.releaseCalls != 1 {
t.Errorf("%s: expected one call to Release(), saw %d", test, negotiator.releaseCalls)
}
case *unloadableNegotiator:
if negotiator.releaseCalls != 1 {
t.Errorf("%s: expected one call to Release(), saw %d", test, negotiator.releaseCalls)
}
default:
t.Errorf("%s: unrecognized negotiator: %#v", test, handler)
}
default:
t.Errorf("%s: unrecognized handler: %#v", test, handler)
}
}
initialRequest := req{}
basicChallenge1 := resp{401, "", []string{"Basic realm=foo"}}
basicRequest1 := req{"Basic bXl1c2VyOm15cGFzc3dvcmQ="} // base64("myuser:mypassword")
basicChallenge2 := resp{401, "", []string{"Basic realm=seriously...foo"}}
negotiateChallenge1 := resp{401, "", []string{"Negotiate"}}
negotiateRequest1 := req{"Negotiate cmVzcG9uc2Ux"} // base64("response1")
negotiateChallenge2 := resp{401, "", []string{"Negotiate Y2hhbGxlbmdlMg=="}} // base64("challenge2")
negotiateRequest2 := req{"Negotiate cmVzcG9uc2Uy"} // base64("response2")
doubleChallenge := resp{401, "", []string{"Negotiate", "Basic realm=foo"}}
successfulToken := "12345"
successfulLocation := fmt.Sprintf("/#access_token=%s", successfulToken)
success := resp{302, successfulLocation, nil}
successWithNegotiate := resp{302, successfulLocation, []string{"Negotiate Y2hhbGxlbmdlMg=="}}
testcases := map[string]struct {
Handler ChallengeHandler
Requests []requestResponse
ExpectedToken string
ExpectedError string
}{
// Defaulting basic handler
"defaulted basic handler, no challenge, success": {
Handler: &BasicChallengeHandler{Username: "myuser", Password: "mypassword"},
Requests: []requestResponse{
{initialRequest, success},
},
ExpectedToken: successfulToken,
},
"defaulted basic handler, basic challenge, success": {
Handler: &BasicChallengeHandler{Username: "myuser", Password: "mypassword"},
Requests: []requestResponse{
{initialRequest, basicChallenge1},
{basicRequest1, success},
},
ExpectedToken: successfulToken,
},
"defaulted basic handler, basic+negotiate challenge, success": {
Handler: &BasicChallengeHandler{Username: "myuser", Password: "mypassword"},
Requests: []requestResponse{
{initialRequest, doubleChallenge},
{basicRequest1, success},
},
ExpectedToken: successfulToken,
},
"defaulted basic handler, basic challenge, failure": {
Handler: &BasicChallengeHandler{Username: "myuser", Password: "mypassword"},
Requests: []requestResponse{
{initialRequest, basicChallenge1},
{basicRequest1, basicChallenge2},
},
ExpectedError: "challenger chose not to retry the request",
},
"defaulted basic handler, negotiate challenge, failure": {
Handler: &BasicChallengeHandler{Username: "myuser", Password: "mypassword"},
Requests: []requestResponse{
{initialRequest, negotiateChallenge1},
},
ExpectedError: "unhandled challenge",
},
"failing basic handler, basic challenge, failure": {
Handler: &BasicChallengeHandler{},
Requests: []requestResponse{
{initialRequest, basicChallenge1},
},
ExpectedError: "challenger chose not to retry the request",
},
// Prompting basic handler
"prompting basic handler, no challenge, success": {
Handler: &BasicChallengeHandler{Reader: bytes.NewBufferString("myuser\nmypassword\n")},
Requests: []requestResponse{
{initialRequest, success},
},
ExpectedToken: successfulToken,
},
"prompting basic handler, basic challenge, success": {
Handler: &BasicChallengeHandler{Reader: bytes.NewBufferString("myuser\nmypassword\n")},
Requests: []requestResponse{
{initialRequest, basicChallenge1},
{basicRequest1, success},
},
ExpectedToken: successfulToken,
},
"prompting basic handler, basic+negotiate challenge, success": {
Handler: &BasicChallengeHandler{Reader: bytes.NewBufferString("myuser\nmypassword\n")},
Requests: []requestResponse{
{initialRequest, doubleChallenge},
{basicRequest1, success},
},
ExpectedToken: successfulToken,
},
"prompting basic handler, basic challenge, failure": {
Handler: &BasicChallengeHandler{Reader: bytes.NewBufferString("myuser\nmypassword\n")},
Requests: []requestResponse{
{initialRequest, basicChallenge1},
{basicRequest1, basicChallenge2},
},
ExpectedError: "challenger chose not to retry the request",
},
"prompting basic handler, negotiate challenge, failure": {
Handler: &BasicChallengeHandler{Reader: bytes.NewBufferString("myuser\nmypassword\n")},
Requests: []requestResponse{
{initialRequest, negotiateChallenge1},
},
ExpectedError: "unhandled challenge",
},
// negotiate handler
"negotiate handler, no challenge, success": {
Handler: &NegotiateChallengeHandler{negotiater: &successfulNegotiator{rounds: 1}},
Requests: []requestResponse{
{initialRequest, success},
},
ExpectedToken: successfulToken,
},
"negotiate handler, negotiate challenge, success": {
Handler: &NegotiateChallengeHandler{negotiater: &successfulNegotiator{rounds: 1}},
Requests: []requestResponse{
{initialRequest, negotiateChallenge1},
{negotiateRequest1, success},
},
ExpectedToken: successfulToken,
},
"negotiate handler, negotiate challenge, 2 rounds, success": {
Handler: &NegotiateChallengeHandler{negotiater: &successfulNegotiator{rounds: 2}},
Requests: []requestResponse{
{initialRequest, negotiateChallenge1},
{negotiateRequest1, negotiateChallenge2},
{negotiateRequest2, success},
},
ExpectedToken: successfulToken,
},
"negotiate handler, negotiate challenge, 2 rounds, success with mutual auth": {
Handler: &NegotiateChallengeHandler{negotiater: &successfulNegotiator{rounds: 2}},
Requests: []requestResponse{
{initialRequest, negotiateChallenge1},
{negotiateRequest1, successWithNegotiate},
},
ExpectedToken: successfulToken,
},
"negotiate handler, negotiate challenge, 2 rounds expected, server success without client completion": {
Handler: &NegotiateChallengeHandler{negotiater: &successfulNegotiator{rounds: 2}},
Requests: []requestResponse{
{initialRequest, negotiateChallenge1},
{negotiateRequest1, success},
},
ExpectedError: "client requires final negotiate token, none provided",
},
// Unloadable negotiate handler
"unloadable negotiate handler, no challenge, success": {
Handler: &NegotiateChallengeHandler{negotiater: &unloadableNegotiator{}},
Requests: []requestResponse{
{initialRequest, success},
},
ExpectedToken: successfulToken,
},
"unloadable negotiate handler, negotiate challenge, failure": {
Handler: &NegotiateChallengeHandler{negotiater: &unloadableNegotiator{}},
Requests: []requestResponse{
{initialRequest, negotiateChallenge1},
},
ExpectedError: "unhandled challenge",
},
"unloadable negotiate handler, basic challenge, failure": {
Handler: &NegotiateChallengeHandler{negotiater: &unloadableNegotiator{}},
Requests: []requestResponse{
{initialRequest, basicChallenge1},
},
ExpectedError: "unhandled challenge",
},
// Failing negotiate handler
"failing negotiate handler, no challenge, success": {
Handler: &NegotiateChallengeHandler{negotiater: &failingNegotiator{}},
Requests: []requestResponse{
{initialRequest, success},
},
ExpectedToken: successfulToken,
},
"failing negotiate handler, negotiate challenge, failure": {
Handler: &NegotiateChallengeHandler{negotiater: &failingNegotiator{}},
Requests: []requestResponse{
{initialRequest, negotiateChallenge1},
},
ExpectedError: "InitSecContext failed",
},
"failing negotiate handler, basic challenge, failure": {
Handler: &NegotiateChallengeHandler{negotiater: &failingNegotiator{}},
Requests: []requestResponse{
{initialRequest, basicChallenge1},
},
ExpectedError: "unhandled challenge",
},
// Negotiate+Basic fallback cases
"failing negotiate+prompting basic handler, no challenge, success": {
Handler: NewMultiHandler(
&NegotiateChallengeHandler{negotiater: &failingNegotiator{}},
&BasicChallengeHandler{Reader: bytes.NewBufferString("myuser\nmypassword\n")},
),
Requests: []requestResponse{
{initialRequest, success},
},
ExpectedToken: successfulToken,
},
"failing negotiate+prompting basic handler, negotiate+basic challenge, success": {
Handler: NewMultiHandler(
&NegotiateChallengeHandler{negotiater: &failingNegotiator{}},
&BasicChallengeHandler{Reader: bytes.NewBufferString("myuser\nmypassword\n")},
),
Requests: []requestResponse{
{initialRequest, doubleChallenge},
{basicRequest1, success},
},
ExpectedToken: successfulToken,
},
"negotiate+failing basic handler, negotiate+basic challenge, success": {
Handler: NewMultiHandler(
&NegotiateChallengeHandler{negotiater: &successfulNegotiator{rounds: 2}},
&BasicChallengeHandler{},
),
Requests: []requestResponse{
{initialRequest, doubleChallenge},
{negotiateRequest1, negotiateChallenge2},
{negotiateRequest2, success},
},
ExpectedToken: successfulToken,
},
"negotiate+basic handler, negotiate+basic challenge, prefers negotiation, success": {
Handler: NewMultiHandler(
&NegotiateChallengeHandler{negotiater: &successfulNegotiator{rounds: 2}},
&BasicChallengeHandler{Reader: bytes.NewBufferString("myuser\nmypassword\n")},
),
Requests: []requestResponse{
{initialRequest, doubleChallenge},
{negotiateRequest1, negotiateChallenge2},
{negotiateRequest2, success},
},
ExpectedToken: successfulToken,
},
"negotiate+basic handler, negotiate+basic challenge, prefers negotiation, sticks with selected handler on failure": {
Handler: NewMultiHandler(
&NegotiateChallengeHandler{negotiater: &successfulNegotiator{rounds: 2}},
&BasicChallengeHandler{Reader: bytes.NewBufferString("myuser\nmypassword\n")},
),
Requests: []requestResponse{
{initialRequest, doubleChallenge},
{negotiateRequest1, negotiateChallenge2},
{negotiateRequest2, doubleChallenge},
},
ExpectedError: "InitSecContext: expected 2 calls, saw 3",
},
}
for k, tc := range testcases {
i := 0
s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
if i > len(tc.Requests) {
t.Errorf("%s: %d: more requests received than expected: %#v", k, i, req)
return
}
rr := tc.Requests[i]
i++
if req.Method != "GET" {
t.Errorf("%s: %d: Expected GET, got %s", k, i, req.Method)
return
}
if req.URL.Path != "/oauth/authorize" {
t.Errorf("%s: %d: Expected /oauth/authorize, got %s", k, i, req.URL.Path)
return
}
if e, a := rr.expectedRequest.authorization, req.Header.Get("Authorization"); e != a {
t.Errorf("%s: %d: expected 'Authorization: %s', got 'Authorization: %s'", k, i, e, a)
return
}
if len(rr.serverResponse.location) > 0 {
w.Header().Add("Location", rr.serverResponse.location)
}
for _, v := range rr.serverResponse.wwwAuthenticate {
w.Header().Add("WWW-Authenticate", v)
}
w.WriteHeader(rr.serverResponse.status)
}))
defer s.Close()
opts := &RequestTokenOptions{
ClientConfig: &restclient.Config{Host: s.URL},
Handler: tc.Handler,
}
token, err := opts.RequestToken()
if token != tc.ExpectedToken {
t.Errorf("%s: expected token '%s', got '%s'", k, tc.ExpectedToken, token)
}
errStr := ""
if err != nil {
errStr = err.Error()
}
if errStr != tc.ExpectedError {
t.Errorf("%s: expected error '%s', got '%s'", k, tc.ExpectedError, errStr)
}
if i != len(tc.Requests) {
t.Errorf("%s: expected %d requests, saw %d", k, len(tc.Requests), i)
}
verifyReleased(k, tc.Handler)
}
}