Browse code

Add gssapi negotiate support

Jordan Liggitt authored on 2016/07/07 00:21:40
Showing 17 changed files
... ...
@@ -127,7 +127,7 @@ func NewCommandAdmin(name, fullName string, out io.Writer, errout io.Writer) *co
127 127
 	)
128 128
 
129 129
 	if name == fullName {
130
-		cmds.AddCommand(version.NewVersionCommand(fullName, false))
130
+		cmds.AddCommand(version.NewVersionCommand(fullName, version.Options{}))
131 131
 	}
132 132
 
133 133
 	return cmds
... ...
@@ -197,7 +197,7 @@ func NewCommandCLI(name, fullName string, in io.Reader, out, errout io.Writer) *
197 197
 	cmds.AddCommand(experimental)
198 198
 
199 199
 	if name == fullName {
200
-		cmds.AddCommand(version.NewVersionCommand(fullName, false))
200
+		cmds.AddCommand(version.NewVersionCommand(fullName, version.Options{PrintClientFeatures: true}))
201 201
 	}
202 202
 	cmds.AddCommand(cmd.NewCmdOptions(out))
203 203
 
... ...
@@ -35,7 +35,7 @@ func NewCommandSTIBuilder(name string) *cobra.Command {
35 35
 		},
36 36
 	}
37 37
 
38
-	cmd.AddCommand(version.NewVersionCommand(name, false))
38
+	cmd.AddCommand(version.NewVersionCommand(name, version.Options{}))
39 39
 	return cmd
40 40
 }
41 41
 
... ...
@@ -50,6 +50,6 @@ func NewCommandDockerBuilder(name string) *cobra.Command {
50 50
 			kcmdutil.CheckErr(err)
51 51
 		},
52 52
 	}
53
-	cmd.AddCommand(version.NewVersionCommand(name, false))
53
+	cmd.AddCommand(version.NewVersionCommand(name, version.Options{}))
54 54
 	return cmd
55 55
 }
... ...
@@ -79,7 +79,7 @@ func NewCommandDeployer(name string) *cobra.Command {
79 79
 		},
80 80
 	}
81 81
 
82
-	cmd.AddCommand(version.NewVersionCommand(name, false))
82
+	cmd.AddCommand(version.NewVersionCommand(name, version.Options{}))
83 83
 
84 84
 	flag := cmd.Flags()
85 85
 	flag.StringVar(&cfg.rcName, "deployment", util.Env("OPENSHIFT_DEPLOYMENT_NAME", ""), "The deployment name to start")
... ...
@@ -129,7 +129,7 @@ func NewCommandF5Router(name string) *cobra.Command {
129 129
 		},
130 130
 	}
131 131
 
132
-	cmd.AddCommand(version.NewVersionCommand(name, false))
132
+	cmd.AddCommand(version.NewVersionCommand(name, version.Options{}))
133 133
 
134 134
 	flag := cmd.Flags()
135 135
 	options.Config.Bind(flag)
... ...
@@ -115,7 +115,7 @@ func NewCommandTemplateRouter(name string) *cobra.Command {
115 115
 		},
116 116
 	}
117 117
 
118
-	cmd.AddCommand(version.NewVersionCommand(name, false))
118
+	cmd.AddCommand(version.NewVersionCommand(name, version.Options{}))
119 119
 
120 120
 	flag := cmd.Flags()
121 121
 	options.Config.Bind(flag)
... ...
@@ -116,7 +116,7 @@ func NewCommandOpenShift(name string) *cobra.Command {
116 116
 	root.AddCommand(cli.NewCmdKubectl("kube", out))
117 117
 	root.AddCommand(newExperimentalCommand("ex", name+" ex"))
118 118
 	root.AddCommand(newCompletionCommand("completion", name+" completion"))
119
-	root.AddCommand(version.NewVersionCommand(name, true))
119
+	root.AddCommand(version.NewVersionCommand(name, version.Options{PrintEtcdVersion: true}))
120 120
 
121 121
 	// infra commands are those that are bundled with the binary but not displayed to end users
122 122
 	// directly
... ...
@@ -38,7 +38,7 @@ func NewCommand(name, fullName string, out io.Writer) *cobra.Command {
38 38
 	cmds.AddCommand(NewProxyCommand("proxy", fullName+" proxy", out))
39 39
 	cmds.AddCommand(NewSchedulerCommand("scheduler", fullName+" scheduler", out))
40 40
 	if "hyperkube" == fullName {
41
-		cmds.AddCommand(version.NewVersionCommand(fullName, false))
41
+		cmds.AddCommand(version.NewVersionCommand(fullName, version.Options{}))
42 42
 	}
43 43
 
44 44
 	return cmds
... ...
@@ -14,6 +14,10 @@ import (
14 14
 	"github.com/openshift/origin/pkg/cmd/util"
15 15
 )
16 16
 
17
+func BasicEnabled() bool {
18
+	return true
19
+}
20
+
17 21
 type BasicChallengeHandler struct {
18 22
 	// Host is the server being authenticated to. Used only for displaying messages when prompting for username/password
19 23
 	Host string
... ...
@@ -38,7 +42,7 @@ func (c *BasicChallengeHandler) CanHandle(headers http.Header) bool {
38 38
 	isBasic, _ := basicRealm(headers)
39 39
 	return isBasic
40 40
 }
41
-func (c *BasicChallengeHandler) HandleChallenge(headers http.Header) (http.Header, bool, error) {
41
+func (c *BasicChallengeHandler) HandleChallenge(requestURL string, headers http.Header) (http.Header, bool, error) {
42 42
 	if c.prompted {
43 43
 		glog.V(2).Info("already prompted for challenge, won't prompt again")
44 44
 		return nil, false, nil
... ...
@@ -93,6 +97,13 @@ func (c *BasicChallengeHandler) HandleChallenge(headers http.Header) (http.Heade
93 93
 	glog.V(2).Info("no username or password available")
94 94
 	return nil, false, nil
95 95
 }
96
+func (c *BasicChallengeHandler) CompleteChallenge(requestURL string, headers http.Header) error {
97
+	return nil
98
+}
99
+
100
+func (c *BasicChallengeHandler) Release() error {
101
+	return nil
102
+}
96 103
 
97 104
 // if any of these match a WWW-Authenticate header, it is a basic challenge
98 105
 // capturing group 1 (if present) should contain the realm
... ...
@@ -204,7 +204,7 @@ Password: `,
204 204
 			}
205 205
 
206 206
 			if canHandle {
207
-				headers, handled, err := tc.Handler.HandleChallenge(challenge.Headers)
207
+				headers, handled, err := tc.Handler.HandleChallenge("", challenge.Headers)
208 208
 				if !reflect.DeepEqual(headers, challenge.ExpectedHeaders) {
209 209
 					t.Errorf("%s: %d: Expected headers\n\t%#v\ngot\n\t%#v", k, i, challenge.ExpectedHeaders, headers)
210 210
 				}
211 211
new file mode 100644
... ...
@@ -0,0 +1,105 @@
0
+package tokencmd
1
+
2
+import (
3
+	"net/http"
4
+
5
+	"github.com/golang/glog"
6
+
7
+	apierrs "k8s.io/kubernetes/pkg/api/errors"
8
+	utilerrors "k8s.io/kubernetes/pkg/util/errors"
9
+)
10
+
11
+var _ = ChallengeHandler(&MultiHandler{})
12
+
13
+// MultiHandler manages a series of authentication challenges
14
+// it is single-use only, and not thread-safe
15
+type MultiHandler struct {
16
+	// handler holds the selected handler.
17
+	// automatically populated with the first handler to successfully respond to HandleChallenge(),
18
+	// and used exclusively by CanHandle() and HandleChallenge() from that point forward.
19
+	handler ChallengeHandler
20
+
21
+	// possibleHandlers holds handlers that could handle subsequent challenges.
22
+	// filtered down during HandleChallenge() by calling CanHandle() on each item.
23
+	possibleHandlers []ChallengeHandler
24
+
25
+	// allHandlers holds all handlers, for purposes of delegating Release() calls
26
+	allHandlers []ChallengeHandler
27
+}
28
+
29
+func NewMultiHandler(handlers ...ChallengeHandler) ChallengeHandler {
30
+	return &MultiHandler{
31
+		possibleHandlers: handlers,
32
+		allHandlers:      handlers,
33
+	}
34
+}
35
+
36
+func (h *MultiHandler) CanHandle(headers http.Header) bool {
37
+	// If we've already selected a handler, it alone can decide whether we can handle the current request
38
+	if h.handler != nil {
39
+		return h.handler.CanHandle(headers)
40
+	}
41
+
42
+	// Otherwise, return true if any of our handlers can handle this request
43
+	for _, handler := range h.possibleHandlers {
44
+		if handler.CanHandle(headers) {
45
+			return true
46
+		}
47
+	}
48
+
49
+	return false
50
+}
51
+
52
+func (h *MultiHandler) HandleChallenge(requestURL string, headers http.Header) (http.Header, bool, error) {
53
+	// If we've already selected a handler, it alone can handle all subsequent challenges (don't change horses in mid-stream)
54
+	if h.handler != nil {
55
+		return h.handler.HandleChallenge(requestURL, headers)
56
+	}
57
+
58
+	// Otherwise, filter our list of handlers to the ones that can handle this request
59
+	applicable := []ChallengeHandler{}
60
+	for _, handler := range h.possibleHandlers {
61
+		if handler.CanHandle(headers) {
62
+			applicable = append(applicable, handler)
63
+		}
64
+	}
65
+	h.possibleHandlers = applicable
66
+
67
+	// Then select the first available handler that successfully handles the request
68
+	var (
69
+		retryHeaders http.Header
70
+		retry        bool
71
+		err          error
72
+	)
73
+	for i, handler := range h.possibleHandlers {
74
+		retryHeaders, retry, err = handler.HandleChallenge(requestURL, headers)
75
+
76
+		if err != nil {
77
+			glog.V(5).Infof("handler[%d] error: %v", i, err)
78
+		}
79
+		// If the handler successfully handled the challenge, or we have no other options, select it as our handler
80
+		if err == nil || i == len(h.possibleHandlers)-1 {
81
+			h.handler = handler
82
+			return retryHeaders, retry, err
83
+		}
84
+	}
85
+
86
+	return nil, false, apierrs.NewUnauthorized("unhandled challenge")
87
+}
88
+
89
+func (h *MultiHandler) CompleteChallenge(requestURL string, headers http.Header) error {
90
+	if h.handler != nil {
91
+		return h.handler.CompleteChallenge(requestURL, headers)
92
+	}
93
+	return nil
94
+}
95
+
96
+func (h *MultiHandler) Release() error {
97
+	var errs []error
98
+	for _, handler := range h.allHandlers {
99
+		if err := handler.Release(); err != nil {
100
+			errs = append(errs, err)
101
+		}
102
+	}
103
+	return utilerrors.NewAggregate(errs)
104
+}
0 105
new file mode 100644
... ...
@@ -0,0 +1,112 @@
0
+package tokencmd
1
+
2
+import (
3
+	"encoding/base64"
4
+	"errors"
5
+	"net/http"
6
+	"strings"
7
+
8
+	"github.com/golang/glog"
9
+)
10
+
11
+// Negotiater defines the minimal interface needed to interact with GSSAPI to perform a negotiate challenge/response
12
+type Negotiater interface {
13
+	// InitSecContext returns the response token for a Negotiate challenge token from a given URL,
14
+	// or an error if no response token could be obtained or the incoming token is invalid.
15
+	InitSecContext(requestURL string, challengeToken []byte) (tokenToSend []byte, err error)
16
+	// IsComplete returns true if the negotiator is satisfied with the negotiation.
17
+	// This typically means gssapi returned GSS_S_COMPLETE to an initSecContext call.
18
+	IsComplete() bool
19
+	// Release gives the negotiator a chance to release any resources held during a challenge/response sequence.
20
+	// It is always invoked, even in cases where no challenges were received or handled.
21
+	Release() error
22
+}
23
+
24
+// NegotiateChallengeHandler manages a challenge negotiation session
25
+// it is single-host, single-use only, and not thread-safe
26
+type NegotiateChallengeHandler struct {
27
+	negotiater Negotiater
28
+}
29
+
30
+func NewNegotiateChallengeHandler(negotiater Negotiater) ChallengeHandler {
31
+	return &NegotiateChallengeHandler{negotiater: negotiater}
32
+}
33
+
34
+func (c *NegotiateChallengeHandler) CanHandle(headers http.Header) bool {
35
+	// Make sure this is a negotiate request
36
+	isNegotiate, _, err := getNegotiateToken(headers)
37
+	return err == nil && isNegotiate
38
+}
39
+
40
+func (c *NegotiateChallengeHandler) HandleChallenge(requestURL string, headers http.Header) (http.Header, bool, error) {
41
+	// Get incoming token
42
+	_, incomingToken, err := getNegotiateToken(headers)
43
+	if err != nil {
44
+		return nil, false, err
45
+	}
46
+
47
+	// Process the token
48
+	outgoingToken, err := c.negotiater.InitSecContext(requestURL, incomingToken)
49
+	if err != nil {
50
+		glog.V(5).Infof("InitSecContext returned error: %v", err)
51
+		return nil, false, err
52
+	}
53
+
54
+	// Build the response headers
55
+	responseHeaders := http.Header{}
56
+	responseHeaders.Set("Authorization", "Negotiate "+base64.StdEncoding.EncodeToString(outgoingToken))
57
+	return responseHeaders, true, nil
58
+}
59
+
60
+func (c *NegotiateChallengeHandler) CompleteChallenge(requestURL string, headers http.Header) error {
61
+	if c.negotiater.IsComplete() {
62
+		return nil
63
+	}
64
+	glog.V(5).Infof("continue needed")
65
+
66
+	// Get incoming token
67
+	isNegotiate, incomingToken, err := getNegotiateToken(headers)
68
+	if err != nil {
69
+		return err
70
+	}
71
+	if !isNegotiate {
72
+		return errors.New("client requires final negotiate token, none provided")
73
+	}
74
+
75
+	// Process the token
76
+	_, err = c.negotiater.InitSecContext(requestURL, incomingToken)
77
+	if err != nil {
78
+		glog.V(5).Infof("InitSecContext returned error during final negotiation: %v", err)
79
+		return err
80
+	}
81
+	if !c.negotiater.IsComplete() {
82
+		return errors.New("InitSecContext did not indicate final negotiation completed")
83
+	}
84
+	return nil
85
+}
86
+
87
+func (c *NegotiateChallengeHandler) Release() error {
88
+	return c.negotiater.Release()
89
+}
90
+
91
+const negotiateScheme = "negotiate"
92
+
93
+func getNegotiateToken(headers http.Header) (bool, []byte, error) {
94
+	for _, challengeHeader := range headers[http.CanonicalHeaderKey("WWW-Authenticate")] {
95
+		// TODO: handle WWW-Authenticate headers containing more than one scheme
96
+		caseInsensitiveHeader := strings.ToLower(challengeHeader)
97
+		if caseInsensitiveHeader == negotiateScheme {
98
+			return true, nil, nil
99
+		}
100
+		if strings.HasPrefix(caseInsensitiveHeader, negotiateScheme+" ") {
101
+			payload := challengeHeader[len(negotiateScheme):]
102
+			payload = strings.Replace(payload, " ", "", -1)
103
+			data, err := base64.StdEncoding.DecodeString(payload)
104
+			if err != nil {
105
+				return false, nil, err
106
+			}
107
+			return true, data, nil
108
+		}
109
+	}
110
+	return false, nil, nil
111
+}
0 112
new file mode 100644
... ...
@@ -0,0 +1,131 @@
0
+// +build gssapi
1
+
2
+package tokencmd
3
+
4
+import (
5
+	"net"
6
+	"net/url"
7
+	"sync"
8
+	"time"
9
+
10
+	"github.com/apcera/gssapi"
11
+	"github.com/golang/glog"
12
+
13
+	utilerrors "k8s.io/kubernetes/pkg/util/errors"
14
+)
15
+
16
+func GSSAPIEnabled() bool {
17
+	return true
18
+}
19
+
20
+type gssapiNegotiator struct {
21
+	// handle to a loaded gssapi lib
22
+	lib *gssapi.Lib
23
+	// error seen when loading the lib
24
+	loadError error
25
+	// lock to make sure we only load it once
26
+	loadOnce sync.Once
27
+
28
+	// service name of the server we are negotiating against
29
+	name *gssapi.Name
30
+	// security context holding the state of the negotiation
31
+	ctx *gssapi.CtxId
32
+	// flags indicating what options we want for the negotiation
33
+	// TODO: surface option for mutual authentication, e.g. gssapi.GSS_C_MUTUAL_FLAG
34
+	flags uint32
35
+
36
+	// track whether the last response from InitSecContext was GSS_S_COMPLETE
37
+	complete bool
38
+}
39
+
40
+func NewGSSAPINegotiator() Negotiater {
41
+	return &gssapiNegotiator{}
42
+}
43
+
44
+func (g *gssapiNegotiator) InitSecContext(requestURL string, challengeToken []byte) (tokenToSend []byte, err error) {
45
+	lib, err := g.loadLib()
46
+	if err != nil {
47
+		return nil, err
48
+	}
49
+
50
+	// Initialize our context if we haven't already
51
+	if g.ctx == nil {
52
+		u, err := url.Parse(requestURL)
53
+		if err != nil {
54
+			return nil, err
55
+		}
56
+
57
+		hostname := u.Host
58
+		if h, _, err := net.SplitHostPort(u.Host); err == nil {
59
+			hostname = h
60
+		}
61
+
62
+		serviceName := "HTTP@" + hostname
63
+		glog.V(5).Infof("importing service name %s", serviceName)
64
+		nameBuf, err := lib.MakeBufferString(serviceName)
65
+		if err != nil {
66
+			return nil, err
67
+		}
68
+		defer nameBuf.Release()
69
+
70
+		name, err := nameBuf.Name(lib.GSS_C_NT_HOSTBASED_SERVICE)
71
+		if err != nil {
72
+			return nil, err
73
+		}
74
+		g.name = name
75
+		g.ctx = lib.GSS_C_NO_CONTEXT
76
+	}
77
+
78
+	incomingTokenBuffer, err := lib.MakeBufferBytes(challengeToken)
79
+	if err != nil {
80
+		return nil, err
81
+	}
82
+	defer incomingTokenBuffer.Release()
83
+
84
+	var outgoingToken *gssapi.Buffer
85
+	g.ctx, _, outgoingToken, _, _, err = lib.InitSecContext(lib.GSS_C_NO_CREDENTIAL, g.ctx, g.name, lib.GSS_C_NO_OID, g.flags, time.Duration(0), lib.GSS_C_NO_CHANNEL_BINDINGS, incomingTokenBuffer)
86
+	defer outgoingToken.Release()
87
+
88
+	switch err {
89
+	case nil:
90
+		glog.V(5).Infof("InitSecContext returned GSS_S_COMPLETE")
91
+		g.complete = true
92
+		return outgoingToken.Bytes(), nil
93
+	case gssapi.ErrContinueNeeded:
94
+		glog.V(5).Infof("InitSecContext returned GSS_S_CONTINUE_NEEDED")
95
+		g.complete = false
96
+		return outgoingToken.Bytes(), nil
97
+	default:
98
+		glog.V(5).Infof("InitSecContext returned error: %v", err)
99
+		return nil, err
100
+	}
101
+}
102
+
103
+func (g *gssapiNegotiator) IsComplete() bool {
104
+	return g.complete
105
+}
106
+
107
+func (g *gssapiNegotiator) Release() error {
108
+	var errs []error
109
+	if err := g.name.Release(); err != nil {
110
+		errs = append(errs, err)
111
+	}
112
+	if err := g.ctx.Release(); err != nil {
113
+		errs = append(errs, err)
114
+	}
115
+	if err := g.lib.Unload(); err != nil {
116
+		errs = append(errs, err)
117
+	}
118
+	return utilerrors.NewAggregate(errs)
119
+}
120
+
121
+func (g *gssapiNegotiator) loadLib() (*gssapi.Lib, error) {
122
+	g.loadOnce.Do(func() {
123
+		glog.V(5).Infof("loading gssapi")
124
+		g.lib, g.loadError = gssapi.Load(nil)
125
+		if g.loadError != nil {
126
+			glog.V(5).Infof("could not load gssapi: %v", g.loadError)
127
+		}
128
+	})
129
+	return g.lib, g.loadError
130
+}
0 131
new file mode 100644
... ...
@@ -0,0 +1,25 @@
0
+// +build !gssapi
1
+
2
+package tokencmd
3
+
4
+import "errors"
5
+
6
+func GSSAPIEnabled() bool {
7
+	return false
8
+}
9
+
10
+type gssapiUnsupported struct{}
11
+
12
+func NewGSSAPINegotiator() Negotiater {
13
+	return &gssapiUnsupported{}
14
+}
15
+
16
+func (g *gssapiUnsupported) InitSecContext(requestURL string, challengeToken []byte) (tokenToSend []byte, err error) {
17
+	return nil, errors.New("GSSAPI support is not enabled")
18
+}
19
+func (g *gssapiUnsupported) IsComplete() bool {
20
+	return false
21
+}
22
+func (g *gssapiUnsupported) Release() error {
23
+	return errors.New("GSSAPI support is not enabled")
24
+}
... ...
@@ -9,6 +9,8 @@ import (
9 9
 	"net/url"
10 10
 	"strings"
11 11
 
12
+	"github.com/golang/glog"
13
+
12 14
 	apierrs "k8s.io/kubernetes/pkg/api/errors"
13 15
 	"k8s.io/kubernetes/pkg/api/unversioned"
14 16
 	"k8s.io/kubernetes/pkg/client/restclient"
... ...
@@ -19,28 +21,81 @@ import (
19 19
 // Corresponds to the header expected by basic-auth challenging authenticators
20 20
 const CSRFTokenHeader = "X-CSRF-Token"
21 21
 
22
+// ChallengeHandler handles responses to WWW-Authenticate challenges.
23
+type ChallengeHandler interface {
24
+	// CanHandle returns true if the handler recognizes a challenge it thinks it can handle.
25
+	CanHandle(headers http.Header) bool
26
+	// HandleChallenge lets the handler attempt to handle a challenge.
27
+	// It is only invoked if CanHandle() returned true for the given headers.
28
+	// Returns response headers and true if the challenge is successfully handled.
29
+	// Returns false if the challenge was not handled, and an optional error in error cases.
30
+	HandleChallenge(requestURL string, headers http.Header) (http.Header, bool, error)
31
+	// CompleteChallenge is invoked with the headers from a successful server response
32
+	// received after having handled one or more challenges.
33
+	// Returns an error if the handler does not consider the challenge/response interaction complete.
34
+	CompleteChallenge(requestURL string, headers http.Header) error
35
+	// Release gives the handler a chance to release any resources held during a challenge/response sequence.
36
+	// It is always invoked, even in cases where no challenges were received or handled.
37
+	Release() error
38
+}
39
+
40
+type RequestTokenOptions struct {
41
+	ClientConfig *restclient.Config
42
+	Handler      ChallengeHandler
43
+}
44
+
22 45
 // RequestToken uses the cmd arguments to locate an openshift oauth server and attempts to authenticate
23 46
 // it returns the access token if it gets one.  An error if it does not
24 47
 func RequestToken(clientCfg *restclient.Config, reader io.Reader, defaultUsername string, defaultPassword string) (string, error) {
25
-	challengeHandler := &BasicChallengeHandler{
26
-		Host:     clientCfg.Host,
27
-		Reader:   reader,
28
-		Username: defaultUsername,
29
-		Password: defaultPassword,
48
+	handlers := []ChallengeHandler{}
49
+	if GSSAPIEnabled() {
50
+		handlers = append(handlers, NewNegotiateChallengeHandler(NewGSSAPINegotiator()))
51
+	}
52
+	if BasicEnabled() {
53
+		handlers = append(handlers, &BasicChallengeHandler{Host: clientCfg.Host, Reader: reader, Username: defaultUsername, Password: defaultPassword})
54
+	}
55
+
56
+	var handler ChallengeHandler
57
+	if len(handlers) == 1 {
58
+		handler = handlers[0]
59
+	} else {
60
+		handler = NewMultiHandler(handlers...)
61
+	}
62
+
63
+	opts := &RequestTokenOptions{
64
+		ClientConfig: clientCfg,
65
+		Handler:      handler,
30 66
 	}
31 67
 
32
-	rt, err := restclient.TransportFor(clientCfg)
68
+	return opts.RequestToken()
69
+}
70
+
71
+// RequestToken locates an openshift oauth server and attempts to authenticate.
72
+// It returns the access token if it gets one, or an error if it does not.
73
+// It should only be invoked once on a given RequestTokenOptions instance.
74
+// The Handler held by the options is released as part of this call.
75
+func (o *RequestTokenOptions) RequestToken() (string, error) {
76
+	defer func() {
77
+		// Always release the handler
78
+		if err := o.Handler.Release(); err != nil {
79
+			// Release errors shouldn't fail the token request, just log
80
+			glog.V(4).Infof("error releasing handler: %v", err)
81
+		}
82
+	}()
83
+
84
+	rt, err := restclient.TransportFor(o.ClientConfig)
33 85
 	if err != nil {
34 86
 		return "", err
35 87
 	}
36 88
 
37 89
 	// requestURL holds the current URL to make requests to. This can change if the server responds with a redirect
38
-	requestURL := clientCfg.Host + "/oauth/authorize?response_type=token&client_id=openshift-challenging-client"
39
-	// requestHeaders holds additional headers to add to the request. This can be changed by challengeHandlers
90
+	requestURL := o.ClientConfig.Host + "/oauth/authorize?response_type=token&client_id=openshift-challenging-client"
91
+	// requestHeaders holds additional headers to add to the request. This can be changed by o.Handlers
40 92
 	requestHeaders := http.Header{}
41 93
 	// requestedURLSet/requestedURLList hold the URLs we have requested, to prevent redirect loops. Gets reset when a challenge is handled.
42 94
 	requestedURLSet := sets.NewString()
43 95
 	requestedURLList := []string{}
96
+	handledChallenge := false
44 97
 
45 98
 	for {
46 99
 		// Make the request
... ...
@@ -52,17 +107,19 @@ func RequestToken(clientCfg *restclient.Config, reader io.Reader, defaultUsernam
52 52
 
53 53
 		if resp.StatusCode == http.StatusUnauthorized {
54 54
 			if resp.Header.Get("WWW-Authenticate") != "" {
55
-				if !challengeHandler.CanHandle(resp.Header) {
55
+				if !o.Handler.CanHandle(resp.Header) {
56 56
 					return "", apierrs.NewUnauthorized("unhandled challenge")
57 57
 				}
58
-				// Handle a challenge
59
-				newRequestHeaders, shouldRetry, err := challengeHandler.HandleChallenge(resp.Header)
58
+				// Handle the challenge
59
+				newRequestHeaders, shouldRetry, err := o.Handler.HandleChallenge(requestURL, resp.Header)
60 60
 				if err != nil {
61 61
 					return "", err
62 62
 				}
63 63
 				if !shouldRetry {
64 64
 					return "", apierrs.NewUnauthorized("challenger chose not to retry the request")
65 65
 				}
66
+				// Remember if we've ever handled a challenge
67
+				handledChallenge = true
66 68
 
67 69
 				// Reset request set/list. Since we're setting different headers, it is legitimate to request the same urls
68 70
 				requestedURLSet = sets.NewString()
... ...
@@ -86,6 +143,14 @@ func RequestToken(clientCfg *restclient.Config, reader io.Reader, defaultUsernam
86 86
 			return "", unauthorizedError
87 87
 		}
88 88
 
89
+		// if we've ever handled a challenge, see if the handler also considers the interaction complete.
90
+		// this is required for negotiate flows with mutual authentication.
91
+		if handledChallenge {
92
+			if err := o.Handler.CompleteChallenge(requestURL, resp.Header); err != nil {
93
+				return "", err
94
+			}
95
+		}
96
+
89 97
 		if resp.StatusCode == http.StatusFound {
90 98
 			redirectURL := resp.Header.Get("Location")
91 99
 
92 100
new file mode 100644
... ...
@@ -0,0 +1,392 @@
0
+package tokencmd
1
+
2
+import (
3
+	"bytes"
4
+	"errors"
5
+	"fmt"
6
+	"net/http"
7
+	"net/http/httptest"
8
+	"testing"
9
+
10
+	"k8s.io/kubernetes/pkg/client/restclient"
11
+)
12
+
13
+type failingNegotiator struct {
14
+	releaseCalls int
15
+}
16
+
17
+func (n *failingNegotiator) InitSecContext(requestURL string, challengeToken []byte) (tokenToSend []byte, err error) {
18
+	return nil, errors.New("InitSecContext failed")
19
+}
20
+func (n *failingNegotiator) IsComplete() bool {
21
+	return false
22
+}
23
+func (n *failingNegotiator) Release() error {
24
+	n.releaseCalls++
25
+	return errors.New("Release failed")
26
+}
27
+
28
+type successfulNegotiator struct {
29
+	rounds              int
30
+	initSecContextCalls int
31
+	releaseCalls        int
32
+}
33
+
34
+func (n *successfulNegotiator) InitSecContext(requestURL string, challengeToken []byte) (tokenToSend []byte, err error) {
35
+	n.initSecContextCalls++
36
+
37
+	if n.initSecContextCalls > n.rounds {
38
+		return nil, fmt.Errorf("InitSecContext: expected %d calls, saw %d", n.rounds, n.initSecContextCalls)
39
+	}
40
+
41
+	if n.initSecContextCalls == 1 {
42
+		if len(challengeToken) > 0 {
43
+			return nil, errors.New("expected empty token for first challenge")
44
+		}
45
+	} else {
46
+		expectedChallengeToken := fmt.Sprintf("challenge%d", n.initSecContextCalls)
47
+		if string(challengeToken) != expectedChallengeToken {
48
+			return nil, fmt.Errorf("expected challenge token '%s', got '%s'", expectedChallengeToken, string(challengeToken))
49
+		}
50
+	}
51
+
52
+	return []byte(fmt.Sprintf("response%d", n.initSecContextCalls)), nil
53
+}
54
+func (n *successfulNegotiator) IsComplete() bool {
55
+	return n.initSecContextCalls == n.rounds
56
+}
57
+func (n *successfulNegotiator) Release() error {
58
+	n.releaseCalls++
59
+	return nil
60
+}
61
+
62
+func TestRequestToken(t *testing.T) {
63
+	type req struct {
64
+		authorization string
65
+	}
66
+	type resp struct {
67
+		status          int
68
+		location        string
69
+		wwwAuthenticate []string
70
+	}
71
+
72
+	type requestResponse struct {
73
+		expectedRequest req
74
+		serverResponse  resp
75
+	}
76
+
77
+	var verifyReleased func(test string, handler ChallengeHandler)
78
+	verifyReleased = func(test string, handler ChallengeHandler) {
79
+		switch handler := handler.(type) {
80
+		case *MultiHandler:
81
+			for _, subhandler := range handler.allHandlers {
82
+				verifyReleased(test, subhandler)
83
+			}
84
+		case *BasicChallengeHandler:
85
+			// we don't care
86
+		case *NegotiateChallengeHandler:
87
+			switch negotiator := handler.negotiater.(type) {
88
+			case *successfulNegotiator:
89
+				if negotiator.releaseCalls != 1 {
90
+					t.Errorf("%s: expected one call to Release(), saw %d", test, negotiator.releaseCalls)
91
+				}
92
+			case *failingNegotiator:
93
+				if negotiator.releaseCalls != 1 {
94
+					t.Errorf("%s: expected one call to Release(), saw %d", test, negotiator.releaseCalls)
95
+				}
96
+			default:
97
+				t.Errorf("%s: unrecognized negotiator: %#v", test, handler)
98
+			}
99
+		default:
100
+			t.Errorf("%s: unrecognized handler: %#v", test, handler)
101
+		}
102
+	}
103
+
104
+	initialRequest := req{}
105
+
106
+	basicChallenge1 := resp{401, "", []string{"Basic realm=foo"}}
107
+	basicRequest1 := req{"Basic bXl1c2VyOm15cGFzc3dvcmQ="} // base64("myuser:mypassword")
108
+	basicChallenge2 := resp{401, "", []string{"Basic realm=seriously...foo"}}
109
+
110
+	negotiateChallenge1 := resp{401, "", []string{"Negotiate"}}
111
+	negotiateRequest1 := req{"Negotiate cmVzcG9uc2Ux"}                           // base64("response1")
112
+	negotiateChallenge2 := resp{401, "", []string{"Negotiate Y2hhbGxlbmdlMg=="}} // base64("challenge2")
113
+	negotiateRequest2 := req{"Negotiate cmVzcG9uc2Uy"}                           // base64("response2")
114
+
115
+	doubleChallenge := resp{401, "", []string{"Negotiate", "Basic realm=foo"}}
116
+
117
+	successfulToken := "12345"
118
+	successfulLocation := fmt.Sprintf("/#access_token=%s", successfulToken)
119
+	success := resp{302, successfulLocation, nil}
120
+	successWithNegotiate := resp{302, successfulLocation, []string{"Negotiate Y2hhbGxlbmdlMg=="}}
121
+
122
+	testcases := map[string]struct {
123
+		Handler       ChallengeHandler
124
+		Requests      []requestResponse
125
+		ExpectedToken string
126
+		ExpectedError string
127
+	}{
128
+		// Defaulting basic handler
129
+		"defaulted basic handler, no challenge, success": {
130
+			Handler: &BasicChallengeHandler{Username: "myuser", Password: "mypassword"},
131
+			Requests: []requestResponse{
132
+				{initialRequest, success},
133
+			},
134
+			ExpectedToken: successfulToken,
135
+		},
136
+		"defaulted basic handler, basic challenge, success": {
137
+			Handler: &BasicChallengeHandler{Username: "myuser", Password: "mypassword"},
138
+			Requests: []requestResponse{
139
+				{initialRequest, basicChallenge1},
140
+				{basicRequest1, success},
141
+			},
142
+			ExpectedToken: successfulToken,
143
+		},
144
+		"defaulted basic handler, basic+negotiate challenge, success": {
145
+			Handler: &BasicChallengeHandler{Username: "myuser", Password: "mypassword"},
146
+			Requests: []requestResponse{
147
+				{initialRequest, doubleChallenge},
148
+				{basicRequest1, success},
149
+			},
150
+			ExpectedToken: successfulToken,
151
+		},
152
+		"defaulted basic handler, basic challenge, failure": {
153
+			Handler: &BasicChallengeHandler{Username: "myuser", Password: "mypassword"},
154
+			Requests: []requestResponse{
155
+				{initialRequest, basicChallenge1},
156
+				{basicRequest1, basicChallenge2},
157
+			},
158
+			ExpectedError: "challenger chose not to retry the request",
159
+		},
160
+		"defaulted basic handler, negotiate challenge, failure": {
161
+			Handler: &BasicChallengeHandler{Username: "myuser", Password: "mypassword"},
162
+			Requests: []requestResponse{
163
+				{initialRequest, negotiateChallenge1},
164
+			},
165
+			ExpectedError: "unhandled challenge",
166
+		},
167
+		"failing basic handler, basic challenge, failure": {
168
+			Handler: &BasicChallengeHandler{},
169
+			Requests: []requestResponse{
170
+				{initialRequest, basicChallenge1},
171
+			},
172
+			ExpectedError: "challenger chose not to retry the request",
173
+		},
174
+
175
+		// Prompting basic handler
176
+		"prompting basic handler, no challenge, success": {
177
+			Handler: &BasicChallengeHandler{Reader: bytes.NewBufferString("myuser\nmypassword\n")},
178
+			Requests: []requestResponse{
179
+				{initialRequest, success},
180
+			},
181
+			ExpectedToken: successfulToken,
182
+		},
183
+		"prompting basic handler, basic challenge, success": {
184
+			Handler: &BasicChallengeHandler{Reader: bytes.NewBufferString("myuser\nmypassword\n")},
185
+			Requests: []requestResponse{
186
+				{initialRequest, basicChallenge1},
187
+				{basicRequest1, success},
188
+			},
189
+			ExpectedToken: successfulToken,
190
+		},
191
+		"prompting basic handler, basic+negotiate challenge, success": {
192
+			Handler: &BasicChallengeHandler{Reader: bytes.NewBufferString("myuser\nmypassword\n")},
193
+			Requests: []requestResponse{
194
+				{initialRequest, doubleChallenge},
195
+				{basicRequest1, success},
196
+			},
197
+			ExpectedToken: successfulToken,
198
+		},
199
+		"prompting basic handler, basic challenge, failure": {
200
+			Handler: &BasicChallengeHandler{Reader: bytes.NewBufferString("myuser\nmypassword\n")},
201
+			Requests: []requestResponse{
202
+				{initialRequest, basicChallenge1},
203
+				{basicRequest1, basicChallenge2},
204
+			},
205
+			ExpectedError: "challenger chose not to retry the request",
206
+		},
207
+		"prompting basic handler, negotiate challenge, failure": {
208
+			Handler: &BasicChallengeHandler{Reader: bytes.NewBufferString("myuser\nmypassword\n")},
209
+			Requests: []requestResponse{
210
+				{initialRequest, negotiateChallenge1},
211
+			},
212
+			ExpectedError: "unhandled challenge",
213
+		},
214
+
215
+		// negotiate handler
216
+		"negotiate handler, no challenge, success": {
217
+			Handler: &NegotiateChallengeHandler{negotiater: &successfulNegotiator{rounds: 1}},
218
+			Requests: []requestResponse{
219
+				{initialRequest, success},
220
+			},
221
+			ExpectedToken: successfulToken,
222
+		},
223
+		"negotiate handler, negotiate challenge, success": {
224
+			Handler: &NegotiateChallengeHandler{negotiater: &successfulNegotiator{rounds: 1}},
225
+			Requests: []requestResponse{
226
+				{initialRequest, negotiateChallenge1},
227
+				{negotiateRequest1, success},
228
+			},
229
+			ExpectedToken: successfulToken,
230
+		},
231
+		"negotiate handler, negotiate challenge, 2 rounds, success": {
232
+			Handler: &NegotiateChallengeHandler{negotiater: &successfulNegotiator{rounds: 2}},
233
+			Requests: []requestResponse{
234
+				{initialRequest, negotiateChallenge1},
235
+				{negotiateRequest1, negotiateChallenge2},
236
+				{negotiateRequest2, success},
237
+			},
238
+			ExpectedToken: successfulToken,
239
+		},
240
+		"negotiate handler, negotiate challenge, 2 rounds, success with mutual auth": {
241
+			Handler: &NegotiateChallengeHandler{negotiater: &successfulNegotiator{rounds: 2}},
242
+			Requests: []requestResponse{
243
+				{initialRequest, negotiateChallenge1},
244
+				{negotiateRequest1, successWithNegotiate},
245
+			},
246
+			ExpectedToken: successfulToken,
247
+		},
248
+		"negotiate handler, negotiate challenge, 2 rounds expected, server success without client completion": {
249
+			Handler: &NegotiateChallengeHandler{negotiater: &successfulNegotiator{rounds: 2}},
250
+			Requests: []requestResponse{
251
+				{initialRequest, negotiateChallenge1},
252
+				{negotiateRequest1, success},
253
+			},
254
+			ExpectedError: "client requires final negotiate token, none provided",
255
+		},
256
+
257
+		// Failing negotiate handler
258
+		"failing negotiate handler, no challenge, success": {
259
+			Handler: &NegotiateChallengeHandler{negotiater: &failingNegotiator{}},
260
+			Requests: []requestResponse{
261
+				{initialRequest, success},
262
+			},
263
+			ExpectedToken: successfulToken,
264
+		},
265
+		"failing negotiate handler, negotiate challenge, failure": {
266
+			Handler: &NegotiateChallengeHandler{negotiater: &failingNegotiator{}},
267
+			Requests: []requestResponse{
268
+				{initialRequest, negotiateChallenge1},
269
+			},
270
+			ExpectedError: "InitSecContext failed",
271
+		},
272
+		"failing negotiate handler, basic challenge, failure": {
273
+			Handler: &NegotiateChallengeHandler{negotiater: &failingNegotiator{}},
274
+			Requests: []requestResponse{
275
+				{initialRequest, basicChallenge1},
276
+			},
277
+			ExpectedError: "unhandled challenge",
278
+		},
279
+
280
+		// Negotiate+Basic fallback cases
281
+		"failing negotiate+prompting basic handler, no challenge, success": {
282
+			Handler: NewMultiHandler(
283
+				&NegotiateChallengeHandler{negotiater: &failingNegotiator{}},
284
+				&BasicChallengeHandler{Reader: bytes.NewBufferString("myuser\nmypassword\n")},
285
+			),
286
+			Requests: []requestResponse{
287
+				{initialRequest, success},
288
+			},
289
+			ExpectedToken: successfulToken,
290
+		},
291
+		"failing negotiate+prompting basic handler, negotiate+basic challenge, success": {
292
+			Handler: NewMultiHandler(
293
+				&NegotiateChallengeHandler{negotiater: &failingNegotiator{}},
294
+				&BasicChallengeHandler{Reader: bytes.NewBufferString("myuser\nmypassword\n")},
295
+			),
296
+			Requests: []requestResponse{
297
+				{initialRequest, doubleChallenge},
298
+				{basicRequest1, success},
299
+			},
300
+			ExpectedToken: successfulToken,
301
+		},
302
+		"negotiate+failing basic handler, negotiate+basic challenge, success": {
303
+			Handler: NewMultiHandler(
304
+				&NegotiateChallengeHandler{negotiater: &successfulNegotiator{rounds: 2}},
305
+				&BasicChallengeHandler{},
306
+			),
307
+			Requests: []requestResponse{
308
+				{initialRequest, doubleChallenge},
309
+				{negotiateRequest1, negotiateChallenge2},
310
+				{negotiateRequest2, success},
311
+			},
312
+			ExpectedToken: successfulToken,
313
+		},
314
+		"negotiate+basic handler, negotiate+basic challenge, prefers negotiation, success": {
315
+			Handler: NewMultiHandler(
316
+				&NegotiateChallengeHandler{negotiater: &successfulNegotiator{rounds: 2}},
317
+				&BasicChallengeHandler{Reader: bytes.NewBufferString("myuser\nmypassword\n")},
318
+			),
319
+			Requests: []requestResponse{
320
+				{initialRequest, doubleChallenge},
321
+				{negotiateRequest1, negotiateChallenge2},
322
+				{negotiateRequest2, success},
323
+			},
324
+			ExpectedToken: successfulToken,
325
+		},
326
+		"negotiate+basic handler, negotiate+basic challenge, prefers negotiation, sticks with selected handler on failure": {
327
+			Handler: NewMultiHandler(
328
+				&NegotiateChallengeHandler{negotiater: &successfulNegotiator{rounds: 2}},
329
+				&BasicChallengeHandler{Reader: bytes.NewBufferString("myuser\nmypassword\n")},
330
+			),
331
+			Requests: []requestResponse{
332
+				{initialRequest, doubleChallenge},
333
+				{negotiateRequest1, negotiateChallenge2},
334
+				{negotiateRequest2, doubleChallenge},
335
+			},
336
+			ExpectedError: "InitSecContext: expected 2 calls, saw 3",
337
+		},
338
+	}
339
+
340
+	for k, tc := range testcases {
341
+		i := 0
342
+		s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
343
+			if i > len(tc.Requests) {
344
+				t.Errorf("%s: %d: more requests received than expected: %#v", k, i, req)
345
+				return
346
+			}
347
+			rr := tc.Requests[i]
348
+			i++
349
+			if req.Method != "GET" {
350
+				t.Errorf("%s: %d: Expected GET, got %s", k, i, req.Method)
351
+				return
352
+			}
353
+			if req.URL.Path != "/oauth/authorize" {
354
+				t.Errorf("%s: %d: Expected /oauth/authorize, got %s", k, i, req.URL.Path)
355
+				return
356
+			}
357
+			if e, a := rr.expectedRequest.authorization, req.Header.Get("Authorization"); e != a {
358
+				t.Errorf("%s: %d: expected 'Authorization: %s', got 'Authorization: %s'", k, i, e, a)
359
+				return
360
+			}
361
+			if len(rr.serverResponse.location) > 0 {
362
+				w.Header().Add("Location", rr.serverResponse.location)
363
+			}
364
+			for _, v := range rr.serverResponse.wwwAuthenticate {
365
+				w.Header().Add("WWW-Authenticate", v)
366
+			}
367
+			w.WriteHeader(rr.serverResponse.status)
368
+		}))
369
+		defer s.Close()
370
+
371
+		opts := &RequestTokenOptions{
372
+			ClientConfig: &restclient.Config{Host: s.URL},
373
+			Handler:      tc.Handler,
374
+		}
375
+		token, err := opts.RequestToken()
376
+		if token != tc.ExpectedToken {
377
+			t.Errorf("%s: expected token '%s', got '%s'", k, tc.ExpectedToken, token)
378
+		}
379
+		errStr := ""
380
+		if err != nil {
381
+			errStr = err.Error()
382
+		}
383
+		if errStr != tc.ExpectedError {
384
+			t.Errorf("%s: expected error '%s', got '%s'", k, tc.ExpectedError, errStr)
385
+		}
386
+		if i != len(tc.Requests) {
387
+			t.Errorf("%s: expected %d requests, saw %d", k, len(tc.Requests), i)
388
+		}
389
+		verifyReleased(k, tc.Handler)
390
+	}
391
+}
... ...
@@ -8,7 +8,10 @@ import (
8 8
 	etcdversion "github.com/coreos/etcd/version"
9 9
 	"github.com/prometheus/client_golang/prometheus"
10 10
 	"github.com/spf13/cobra"
11
+
11 12
 	kubeversion "k8s.io/kubernetes/pkg/version"
13
+
14
+	"github.com/openshift/origin/pkg/cmd/util/tokencmd"
12 15
 )
13 16
 
14 17
 var (
... ...
@@ -87,17 +90,34 @@ func (info Info) LastSemanticVersion() string {
87 87
 	return strings.Join(parts, "-")
88 88
 }
89 89
 
90
+type Options struct {
91
+	PrintEtcdVersion    bool
92
+	PrintClientFeatures bool
93
+}
94
+
90 95
 // NewVersionCommand creates a command for displaying the version of this binary
91
-func NewVersionCommand(basename string, printEtcdVersion bool) *cobra.Command {
96
+func NewVersionCommand(basename string, options Options) *cobra.Command {
92 97
 	return &cobra.Command{
93 98
 		Use:   "version",
94 99
 		Short: "Display version",
95 100
 		Run: func(c *cobra.Command, args []string) {
96 101
 			fmt.Printf("%s %v\n", basename, Get())
97 102
 			fmt.Printf("kubernetes %v\n", kubeversion.Get())
98
-			if printEtcdVersion {
103
+			if options.PrintEtcdVersion {
99 104
 				fmt.Printf("etcd %v\n", etcdversion.Version)
100 105
 			}
106
+			if options.PrintClientFeatures {
107
+				features := []string{}
108
+				if tokencmd.BasicEnabled() {
109
+					features = append(features, "Basic-Auth")
110
+				}
111
+				if tokencmd.GSSAPIEnabled() {
112
+					features = append(features, "GSSAPI")
113
+					features = append(features, "Kerberos") // GSSAPI or SSPI
114
+					features = append(features, "SPNEGO")   // GSSAPI or SSPI
115
+				}
116
+				fmt.Printf("features: %s\n", strings.Join(features, " "))
117
+			}
101 118
 		},
102 119
 	}
103 120
 }