| ... | ... |
@@ -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 |
} |