-
Notifications
You must be signed in to change notification settings - Fork 4.7k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Signed-off-by: Monis Khan <[email protected]>
- Loading branch information
Showing
3 changed files
with
206 additions
and
84 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -4,29 +4,51 @@ package tokencmd | |
|
||
import ( | ||
"fmt" | ||
"io" | ||
"os" | ||
"strings" | ||
"syscall" | ||
|
||
"k8s.io/apimachinery/pkg/util/errors" | ||
"k8s.io/apimachinery/pkg/util/runtime" | ||
|
||
"github.com/openshift/origin/pkg/cmd/util/term" | ||
|
||
"github.com/alexbrainman/sspi" | ||
"github.com/alexbrainman/sspi/negotiate" | ||
"github.com/golang/glog" | ||
) | ||
|
||
const ( | ||
// sane set of default flags, see sspiNegotiator.flags | ||
// sane set of default flags, see sspiNegotiator.desiredFlags | ||
// TODO make configurable? | ||
flags = sspi.ISC_REQ_CONFIDENTIALITY | | ||
desiredFlags = sspi.ISC_REQ_CONFIDENTIALITY | | ||
sspi.ISC_REQ_INTEGRITY | | ||
sspi.ISC_REQ_MUTUAL_AUTH | | ||
sspi.ISC_REQ_REPLAY_DETECT | | ||
sspi.ISC_REQ_SEQUENCE_DETECT | ||
// subset of desiredFlags that must be set, see sspiNegotiator.requiredFlags | ||
// TODO make configurable? | ||
requiredFlags = sspi.ISC_REQ_CONFIDENTIALITY | | ||
sspi.ISC_REQ_INTEGRITY | | ||
sspi.ISC_REQ_MUTUAL_AUTH | ||
|
||
// separator used in fully qualified user name format | ||
// various windows user name formats | ||
// https://msdn.microsoft.com/en-us/library/windows/desktop/aa380525(v=vs.85).aspx | ||
// https://msdn.microsoft.com/en-us/library/ms724268(VS.85).aspx | ||
// separator used in fully qualified user name or down-level logon name format (DOMAIN\Username) | ||
domainSeparator = `\` | ||
// https://msdn.microsoft.com/en-us/library/ms677605(v=vs.85).aspx#userPrincipalName | ||
// separator used in user principal name (UPN) format ([email protected]) | ||
upnSeparator = "@" | ||
// https://msdn.microsoft.com/en-us/library/system.environment.userdomainname(v=vs.110).aspx | ||
// environment variable that holds the network domain name associated with the current user | ||
// this is the NetBIOS domain name which should fit within the length requirement (see maxDomain) | ||
shortDomainEnvVar = "USERDOMAIN" | ||
|
||
// max lengths for various fields, see sspiNegotiator.principalName | ||
// max lengths for various fields, see sspiNegotiator.getDomainAndUsername and sspiNegotiator.getPassword | ||
// When using the Negotiate package, the maximum character lengths for user name, password, and domain are | ||
// 256, 256, and 15, respectively. | ||
maxUsername = 256 | ||
maxPassword = 256 | ||
maxDomain = 15 | ||
|
@@ -39,15 +61,22 @@ func SSPIEnabled() bool { | |
// sspiNegotiator handles negotiate flows on windows via SSPI | ||
// It expects sspiNegotiator.InitSecContext to be called until sspiNegotiator.IsComplete returns true | ||
type sspiNegotiator struct { | ||
// optional DOMAIN\Username and password | ||
// principalName is an optional username (in fully qualified, user principal name or short format). | ||
// https://msdn.microsoft.com/en-us/library/windows/desktop/aa374714(v=vs.85).aspx | ||
// https://msdn.microsoft.com/en-us/library/windows/desktop/aa380131(v=vs.85).aspx | ||
// pAuthData [in]: If credentials are supplied, they are passed via a pointer to a sspi.SEC_WINNT_AUTH_IDENTITY | ||
// structure that includes those credentials. | ||
// When using the Negotiate package, the maximum character lengths for user name, password, and domain are | ||
// 256, 256, and 15, respectively. | ||
principalName string | ||
password string | ||
// password is an optional password used to log into a specific account if principalName is non-empty. | ||
// This allows logging in via username and password even when basic auth is not enabled. | ||
password string | ||
|
||
// reader is used to prompt for a password if principalName is non-empty and password is empty. | ||
reader io.Reader | ||
// writer is used to output prompts when prompting for password. | ||
writer io.Writer | ||
// host is the server being authenticated to. Used only for displaying messages when prompting for password. | ||
host string | ||
|
||
// https://msdn.microsoft.com/en-us/library/windows/desktop/ms721572(v=vs.85).aspx#_security_credentials_gly | ||
// phCredential [in, optional]: A handle to the credentials returned by AcquireCredentialsHandle (Negotiate). | ||
|
@@ -58,15 +87,25 @@ type sspiNegotiator struct { | |
ctx *negotiate.ClientContext | ||
// https://msdn.microsoft.com/en-us/library/windows/desktop/aa375509(v=vs.85).aspx | ||
// fContextReq [in]: Bit flags that indicate requests for the context. | ||
flags uint32 | ||
desiredFlags uint32 | ||
// requiredFlags is the subset of desiredFlags that must be set for flag verification to succeed | ||
requiredFlags uint32 | ||
// https://msdn.microsoft.com/en-us/library/windows/desktop/aa375509(v=vs.85).aspx | ||
// https://msdn.microsoft.com/en-us/library/windows/desktop/aa374764(v=vs.85).aspx | ||
// Set to true once InitializeSecurityContext or CompleteAuthToken return sspi.SEC_E_OK | ||
complete bool | ||
} | ||
|
||
func NewSSPINegotiator(principalName, password string) Negotiator { | ||
return &sspiNegotiator{principalName: principalName, password: password, flags: flags} | ||
func NewSSPINegotiator(principalName, password, host string, reader io.Reader) Negotiator { | ||
return &sspiNegotiator{ | ||
principalName: principalName, | ||
password: password, | ||
reader: reader, | ||
writer: os.Stdout, | ||
host: host, | ||
desiredFlags: desiredFlags, | ||
requiredFlags: requiredFlags, | ||
} | ||
} | ||
|
||
func (s *sspiNegotiator) Load() error { | ||
|
@@ -75,47 +114,16 @@ func (s *sspiNegotiator) Load() error { | |
return nil | ||
} | ||
|
||
func (s *sspiNegotiator) InitSecContext(requestURL string, challengeToken []byte) ([]byte, error) { | ||
func (s *sspiNegotiator) InitSecContext(requestURL string, challengeToken []byte) (tokenToSend []byte, err error) { | ||
defer runtime.HandleCrash() | ||
if s.cred == nil || s.ctx == nil { | ||
glog.V(5).Infof("Start SSPI flow: %s", requestURL) | ||
|
||
cred, err := s.getUserCredentials() | ||
if err != nil { | ||
glog.V(5).Infof("getUserCredentials returned error: %v", err) | ||
return nil, err | ||
} | ||
s.cred = cred | ||
glog.V(5).Info("getUserCredentials successful") | ||
|
||
serviceName, err := getServiceName('/', requestURL) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
glog.V(5).Infof("importing service name %s", serviceName) | ||
ctx, outputToken, err := negotiate.NewClientContext(s.cred, serviceName) // TODO send s.flags | ||
if err != nil { | ||
glog.V(5).Infof("NewClientContext returned error: %v", err) | ||
return nil, err | ||
} | ||
s.ctx = ctx | ||
glog.V(5).Info("NewClientContext successful") | ||
return outputToken, nil | ||
if needsInit := s.cred == nil || s.ctx == nil; needsInit { | ||
logSSPI("Start SSPI flow: %s", requestURL) | ||
return s.initContext(requestURL) | ||
} | ||
|
||
glog.V(5).Info("Continue SSPI flow") | ||
|
||
complete, outputToken, err := s.ctx.Update(challengeToken) | ||
if err != nil { | ||
glog.V(5).Infof("context Update returned error: %v", err) | ||
return nil, err | ||
} | ||
// TODO we need a way to verify s.ctx.sctxt.EstablishedFlags matches s.ctx.sctxt.RequestedFlags (s.flags) | ||
// we will need to update upstream to add the verification or use reflection hacks here | ||
s.complete = complete | ||
glog.V(5).Infof("context Update successful, complete=%v", s.complete) | ||
return outputToken, nil | ||
return s.updateContext(challengeToken) | ||
} | ||
|
||
func (s *sspiNegotiator) IsComplete() bool { | ||
|
@@ -126,63 +134,175 @@ func (s *sspiNegotiator) Release() error { | |
defer runtime.HandleCrash() | ||
glog.V(5).Info("Attempt to release SSPI") | ||
var errs []error | ||
if s.ctx != nil { | ||
if err := s.ctx.Release(); err != nil { | ||
glog.V(5).Infof("SSPI context release failed: %v", err) | ||
errs = append(errs, err) | ||
} | ||
if err := s.ctx.Release(); err != nil { | ||
logSSPI("SSPI context release failed: %v", err) | ||
errs = append(errs, err) | ||
} | ||
if s.cred != nil { | ||
if err := s.cred.Release(); err != nil { | ||
glog.V(5).Infof("SSPI credential release failed: %v", err) | ||
errs = append(errs, err) | ||
} | ||
if err := s.cred.Release(); err != nil { | ||
logSSPI("SSPI credential release failed: %v", err) | ||
errs = append(errs, err) | ||
} | ||
if len(errs) == 1 { | ||
return errs[0] | ||
return errors.Reduce(errors.NewAggregate(errs)) | ||
} | ||
|
||
func (s *sspiNegotiator) initContext(requestURL string) (outputToken []byte, err error) { | ||
cred, err := s.getUserCredentials() | ||
if err != nil { | ||
logSSPI("getUserCredentials failed: %v", err) | ||
return nil, err | ||
} | ||
return errors.NewAggregate(errs) | ||
s.cred = cred | ||
glog.V(5).Info("getUserCredentials successful") | ||
|
||
serviceName, err := getServiceName('/', requestURL) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
logSSPI("importing service name %s", serviceName) | ||
ctx, outputToken, err := negotiate.NewClientContextWithFlags(s.cred, serviceName, s.desiredFlags) | ||
if err != nil { | ||
logSSPI("NewClientContextWithFlags failed: %v", err) | ||
return nil, err | ||
} | ||
s.ctx = ctx | ||
glog.V(5).Info("NewClientContextWithFlags successful") | ||
return outputToken, nil | ||
} | ||
|
||
func (s *sspiNegotiator) getUserCredentials() (*sspi.Credentials, error) { | ||
if len(s.principalName) == 0 && len(s.password) > 0 { | ||
return nil, fmt.Errorf("username cannot be empty with non-empty password") | ||
} | ||
|
||
// Try to use principalName if specified | ||
if len(s.principalName) > 0 { | ||
domain, username, err := s.splitDomainAndUsername() | ||
domain, username, err := s.getDomainAndUsername() | ||
if err != nil { | ||
return nil, err | ||
} | ||
password, err := s.getPassword(domain, username) | ||
if err != nil { | ||
return nil, err | ||
} | ||
glog.V(5).Infof( | ||
"Using AcquireUserCredentials because principalName is not empty, principalName=%s, username=%s, domain=%s", | ||
|
||
logSSPI("Using AcquireUserCredentials because principalName is not empty, principalName=%s, username=%s, domain=%s", | ||
s.principalName, username, domain) | ||
cred, err := negotiate.AcquireUserCredentials(domain, username, s.password) | ||
// this call seems to never fail, even when domain / username / password are nonsense | ||
cred, err := negotiate.AcquireUserCredentials(domain, username, password) | ||
if err != nil { | ||
glog.V(5).Infof("AcquireUserCredentials failed: %v", err) | ||
logSSPI("AcquireUserCredentials failed: %v", err) | ||
return nil, err | ||
} | ||
glog.V(5).Info("AcquireUserCredentials successful") | ||
return cred, nil | ||
} | ||
|
||
glog.V(5).Info("Using AcquireCurrentUserCredentials because principalName is empty") | ||
return negotiate.AcquireCurrentUserCredentials() | ||
} | ||
|
||
func (s *sspiNegotiator) splitDomainAndUsername() (string, string, error) { | ||
data := strings.Split(s.principalName, domainSeparator) | ||
if len(data) != 2 { | ||
return "", "", fmt.Errorf(`invalid username %s, must be in Fully Qualified User Name format (ex: DOMAIN\Username)`, | ||
s.principalName) | ||
} | ||
domain := data[0] | ||
username := data[1] | ||
if domainLen, | ||
usernameLen, | ||
passwordLen := len(domain), | ||
len(username), | ||
len(s.password); domainLen > maxDomain || usernameLen > maxUsername || passwordLen > maxPassword { | ||
return "", "", fmt.Errorf( | ||
"the maximum character lengths for user name, password, and domain are 256, 256, and 15, respectively:\n"+ | ||
"fully qualifed username=%s username=%s,len=%d domain=%s,len=%d password=<redacted>,len=%d", | ||
s.principalName, username, usernameLen, domain, domainLen, passwordLen) | ||
func (s *sspiNegotiator) getDomainAndUsername() (domain, username string, err error) { | ||
switch { | ||
case strings.Contains(s.principalName, domainSeparator): | ||
data := strings.Split(s.principalName, domainSeparator) | ||
// try to provide useful error messages | ||
if len(data) != 2 || len(data[1]) == 0 { | ||
return "", "", fmt.Errorf( | ||
`invalid username %s, fully qualified user name format must have single backslash and non-empty user (ex: DOMAIN\Username)`, | ||
s.principalName) | ||
} | ||
domain = data[0] | ||
username = data[1] | ||
|
||
case strings.Contains(s.principalName, upnSeparator): | ||
// leave domain empty and assume it is qualified in the username (UPN format) | ||
username = s.principalName | ||
|
||
default: | ||
// this is a short name meaning we will need to lookup the current user's domain | ||
// TODO should we use syscall.NetGetJoinInformation first and then fallback to the env var? | ||
domain, _ = os.LookupEnv(shortDomainEnvVar) | ||
username = s.principalName | ||
} | ||
|
||
// try to provide useful error messages | ||
if domainLen, usernameLen := len(domain), len(username); domainLen > maxDomain || usernameLen > maxUsername { | ||
return "", "", | ||
fmt.Errorf("the maximum character lengths for user name and domain are %d and %d, respectively:\n"+ | ||
"input username=%s username=%s,len=%d domain=%s,len=%d", | ||
maxUsername, maxDomain, s.principalName, username, usernameLen, domain, domainLen) | ||
} | ||
|
||
return domain, username, nil | ||
} | ||
|
||
func (s *sspiNegotiator) getPassword(domain, username string) (string, error) { | ||
password := s.password | ||
|
||
if missingPassword := len(password) == 0; missingPassword { | ||
// mimic output from basic auth prompt | ||
if hasDomain := len(domain) > 0; hasDomain { | ||
fmt.Fprintf(s.writer, "Authentication required for %s (%s)\n", s.host, domain) | ||
} else { | ||
fmt.Fprintf(s.writer, "Authentication required for %s\n", s.host) | ||
} | ||
fmt.Fprintf(s.writer, "Username: %s\n", username) | ||
// empty password from prompt is ok | ||
// we do not need to worry about being stuck in a prompt loop because ClientContext.Update | ||
// will fail if the password is incorrect and that will end the challenge flow | ||
password = term.PromptForPasswordString(s.reader, s.writer, "Password: ") | ||
} | ||
|
||
// try to provide useful error messages | ||
if passwordLen := len(password); passwordLen > maxPassword { | ||
return "", fmt.Errorf("the maximum character length for password is %d: password=<redacted>,len=%d", | ||
maxPassword, passwordLen) | ||
} | ||
|
||
return password, nil | ||
} | ||
|
||
func (s *sspiNegotiator) updateContext(challengeToken []byte) (outputToken []byte, err error) { | ||
// ClientContext.Update does not return errors for success codes: | ||
// 1. sspi.SEC_E_OK (complete=true and err=nil) | ||
// 2. sspi.SEC_I_CONTINUE_NEEDED (complete=false and err=nil) | ||
// 3. sspi.SEC_I_COMPLETE_AND_CONTINUE and sspi.SEC_I_COMPLETE_NEEDED | ||
// complete=false and err=nil as long as sspi.CompleteAuthToken returns sspi.SEC_E_OK | ||
// Thus we can safely assume that any error returned here is an error code | ||
authCompleted, outputToken, err := s.ctx.Update(challengeToken) | ||
if err != nil { | ||
logSSPI("ClientContext.Update failed: %v", err) | ||
return nil, err | ||
} | ||
s.complete = authCompleted | ||
logSSPI("ClientContext.Update successful, complete=%v", s.complete) | ||
|
||
// TODO should we skip the flag check if complete = true? | ||
if nonFatalErr := s.ctx.VerifyFlags(); nonFatalErr == nil { | ||
glog.V(5).Info("ClientContext.VerifyFlags successful") | ||
} else { | ||
logSSPI("ClientContext.VerifyFlags failed: %v", nonFatalErr) | ||
if fatalErr := s.ctx.VerifySelectiveFlags(s.requiredFlags); fatalErr != nil { | ||
logSSPI("ClientContext.VerifySelectiveFlags failed: %v", fatalErr) | ||
return nil, fatalErr | ||
} | ||
glog.V(5).Info("ClientContext.VerifySelectiveFlags successful") | ||
} | ||
|
||
return outputToken, nil | ||
} | ||
|
||
// logSSPI is the equivalent of glog.V(5).Infof(format, args) except it | ||
// includes error code information for any syscall.Errno contained in args | ||
func logSSPI(format string, args ...interface{}) { | ||
if glog.V(5) { | ||
for i, arg := range args { | ||
if errno, ok := arg.(syscall.Errno); ok { | ||
args[i] = fmt.Sprintf("%v, code=%#v", errno, errno) | ||
} | ||
} | ||
s := fmt.Sprintf(format, args...) | ||
glog.InfoDepth(1, s) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters