Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Silent login: use credentials from cred store to login #139

Merged
merged 1 commit into from
Feb 27, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 30 additions & 19 deletions cli/command/registry.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,11 +52,15 @@ func RegistryAuthenticationPrivilegedFunc(cli Cli, index *registrytypes.IndexInf
fmt.Fprintf(cli.Out(), "\nPlease login prior to %s:\n", cmdName)
indexServer := registry.GetAuthConfigKey(index)
isDefaultRegistry := indexServer == ElectAuthServer(context.Background(), cli)
authConfig, err := ConfigureAuth(cli, "", "", indexServer, isDefaultRegistry)
authConfig, err := GetDefaultAuthConfig(cli, true, indexServer, isDefaultRegistry)
if err != nil {
fmt.Fprintf(cli.Err(), "Unable to retrieve stored credentials for %s, error: %s.\n", indexServer, err)
}
err = ConfigureAuth(cli, "", "", authConfig, isDefaultRegistry)
if err != nil {
return "", err
}
return EncodeAuthToBase64(authConfig)
return EncodeAuthToBase64(*authConfig)
}
}

Expand All @@ -73,20 +77,29 @@ func ResolveAuthConfig(ctx context.Context, cli Cli, index *registrytypes.IndexI
return a
}

// ConfigureAuth returns an AuthConfig from the specified user, password and server.
func ConfigureAuth(cli Cli, flUser, flPassword, serverAddress string, isDefaultRegistry bool) (types.AuthConfig, error) {
// On Windows, force the use of the regular OS stdin stream. Fixes #14336/#14210
if runtime.GOOS == "windows" {
cli.SetIn(NewInStream(os.Stdin))
}

// GetDefaultAuthConfig gets the default auth config given a serverAddress
// If credentials for given serverAddress exists in the credential store, the configuration will be populated with values in it
func GetDefaultAuthConfig(cli Cli, checkCredStore bool, serverAddress string, isDefaultRegistry bool) (*types.AuthConfig, error) {
if !isDefaultRegistry {
serverAddress = registry.ConvertToHostname(serverAddress)
}
var authconfig types.AuthConfig
var err error
if checkCredStore {
authconfig, err = cli.ConfigFile().GetAuthConfig(serverAddress)
} else {
authconfig = types.AuthConfig{}
}
authconfig.ServerAddress = serverAddress
authconfig.IdentityToken = ""
return &authconfig, err
}

authconfig, err := cli.ConfigFile().GetAuthConfig(serverAddress)
if err != nil {
return authconfig, err
// ConfigureAuth handles prompting of user's username and password if needed
func ConfigureAuth(cli Cli, flUser, flPassword string, authconfig *types.AuthConfig, isDefaultRegistry bool) error {
// On Windows, force the use of the regular OS stdin stream. Fixes #14336/#14210
if runtime.GOOS == "windows" {
cli.SetIn(NewInStream(os.Stdin))
}

// Some links documenting this:
Expand All @@ -97,7 +110,7 @@ func ConfigureAuth(cli Cli, flUser, flPassword, serverAddress string, isDefaultR
// will hit this if you attempt docker login from mintty where stdin
// is a pipe, not a character based console.
if flPassword == "" && !cli.In().IsTerminal() {
return authconfig, errors.Errorf("Error: Cannot perform an interactive login from a non TTY device")
return errors.Errorf("Error: Cannot perform an interactive login from a non TTY device")
}

authconfig.Username = strings.TrimSpace(authconfig.Username)
Expand All @@ -115,12 +128,12 @@ func ConfigureAuth(cli Cli, flUser, flPassword, serverAddress string, isDefaultR
}
}
if flUser == "" {
return authconfig, errors.Errorf("Error: Non-null Username Required")
return errors.Errorf("Error: Non-null Username Required")
}
if flPassword == "" {
oldState, err := term.SaveState(cli.In().FD())
if err != nil {
return authconfig, err
return err
}
fmt.Fprintf(cli.Out(), "Password: ")
term.DisableEcho(cli.In().FD(), oldState)
Expand All @@ -130,16 +143,14 @@ func ConfigureAuth(cli Cli, flUser, flPassword, serverAddress string, isDefaultR

term.RestoreTerminal(cli.In().FD(), oldState)
if flPassword == "" {
return authconfig, errors.Errorf("Error: Password Required")
return errors.Errorf("Error: Password Required")
}
}

authconfig.Username = flUser
authconfig.Password = flPassword
authconfig.ServerAddress = serverAddress
authconfig.IdentityToken = ""

return authconfig, nil
return nil
}

func readInput(in io.Reader, out io.Writer) string {
Expand Down
55 changes: 43 additions & 12 deletions cli/command/registry/login.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ import (

"github.com/docker/cli/cli"
"github.com/docker/cli/cli/command"
"github.com/docker/docker/api/types"
registrytypes "github.com/docker/docker/api/types/registry"
"github.com/docker/docker/client"
"github.com/docker/docker/registry"
"github.com/pkg/errors"
"github.com/spf13/cobra"
Expand Down Expand Up @@ -47,10 +50,7 @@ func NewLoginCommand(dockerCli command.Cli) *cobra.Command {
return cmd
}

func runLogin(dockerCli command.Cli, opts loginOptions) error {
ctx := context.Background()
clnt := dockerCli.Client()

func verifyloginOptions(dockerCli command.Cli, opts *loginOptions) error {
if opts.password != "" {
fmt.Fprintln(dockerCli.Err(), "WARNING! Using --password via the CLI is insecure. Use --password-stdin.")
if opts.passwordStdin {
Expand All @@ -71,7 +71,15 @@ func runLogin(dockerCli command.Cli, opts loginOptions) error {
opts.password = strings.TrimSuffix(string(contents), "\n")
opts.password = strings.TrimSuffix(opts.password, "\r")
}
return nil
}

func runLogin(dockerCli command.Cli, opts loginOptions) error {
ctx := context.Background()
clnt := dockerCli.Client()
if err := verifyloginOptions(dockerCli, &opts); err != nil {
return err
}
var (
serverAddress string
authServer = command.ElectAuthServer(ctx, dockerCli)
Expand All @@ -82,21 +90,30 @@ func runLogin(dockerCli command.Cli, opts loginOptions) error {
serverAddress = authServer
}

var err error
var authConfig *types.AuthConfig
var response registrytypes.AuthenticateOKBody
isDefaultRegistry := serverAddress == authServer

authConfig, err := command.ConfigureAuth(dockerCli, opts.user, opts.password, serverAddress, isDefaultRegistry)
if err != nil {
return err
authConfig, err = command.GetDefaultAuthConfig(dockerCli, opts.user == "" && opts.password == "", serverAddress, isDefaultRegistry)
if err == nil && authConfig.Username != "" && authConfig.Password != "" {
response, err = loginWithCredStoreCreds(ctx, dockerCli, authConfig)
}
response, err := clnt.RegistryLogin(ctx, authConfig)
if err != nil {
return err
if err != nil || authConfig.Username == "" || authConfig.Password == "" {
err = command.ConfigureAuth(dockerCli, opts.user, opts.password, authConfig, isDefaultRegistry)
if err != nil {
return err
}

response, err = clnt.RegistryLogin(ctx, *authConfig)
if err != nil {
return err
}
}
if response.IdentityToken != "" {
authConfig.Password = ""
authConfig.IdentityToken = response.IdentityToken
}
if err := dockerCli.ConfigFile().GetCredentialsStore(serverAddress).Store(authConfig); err != nil {
if err := dockerCli.ConfigFile().GetCredentialsStore(serverAddress).Store(*authConfig); err != nil {
return errors.Errorf("Error saving credentials: %v", err)
}

Expand All @@ -105,3 +122,17 @@ func runLogin(dockerCli command.Cli, opts loginOptions) error {
}
return nil
}

func loginWithCredStoreCreds(ctx context.Context, dockerCli command.Cli, authConfig *types.AuthConfig) (registrytypes.AuthenticateOKBody, error) {
fmt.Fprintf(dockerCli.Out(), "Authenticating with existing credentials...\n")
cliClient := dockerCli.Client()
response, err := cliClient.RegistryLogin(ctx, *authConfig)
if err != nil {
if client.IsErrUnauthorized(err) {
fmt.Fprintf(dockerCli.Err(), "Stored credentials invalid or expired\n")
} else {
fmt.Fprintf(dockerCli.Err(), "Login did not succeed, error: %s\n", err)
}
}
return response, err
}
151 changes: 151 additions & 0 deletions cli/command/registry/login_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
package registry

import (
"bytes"
"fmt"
"testing"

"golang.org/x/net/context"

"github.com/docker/cli/internal/test"
"github.com/docker/docker/api/types"
registrytypes "github.com/docker/docker/api/types/registry"
"github.com/docker/docker/client"
"github.com/stretchr/testify/assert"
)

const userErr = "userunknownError"
const testAuthErrMsg = "UNKNOWN_ERR"

var testAuthErrors = map[string]error{
userErr: fmt.Errorf(testAuthErrMsg),
}

var expiredPassword = "I_M_EXPIRED"

type fakeClient struct {
client.Client
}

// nolint: unparam
func (c fakeClient) RegistryLogin(ctx context.Context, auth types.AuthConfig) (registrytypes.AuthenticateOKBody, error) {
if auth.Password == expiredPassword {
return registrytypes.AuthenticateOKBody{}, fmt.Errorf("Invalid Username or Password")
}
err := testAuthErrors[auth.Username]
return registrytypes.AuthenticateOKBody{}, err
}

func TestLoginWithCredStoreCreds(t *testing.T) {
testCases := []struct {
inputAuthConfig types.AuthConfig
expectedMsg string
expectedErr string
}{
{
inputAuthConfig: types.AuthConfig{},
expectedMsg: "Authenticating with existing credentials...\n",
},
{
inputAuthConfig: types.AuthConfig{
Username: userErr,
},
expectedMsg: "Authenticating with existing credentials...\n",
expectedErr: fmt.Sprintf("Login did not succeed, error: %s\n", testAuthErrMsg),
},
// can't easily test the 401 case because client.IsErrUnauthorized(err) involving
// creating an error of a private type
}
ctx := context.Background()
for _, tc := range testCases {
cli := (*test.FakeCli)(test.NewFakeCli(&fakeClient{}))
errBuf := new(bytes.Buffer)
cli.SetErr(errBuf)
loginWithCredStoreCreds(ctx, cli, &tc.inputAuthConfig)
outputString := cli.OutBuffer().String()
assert.Equal(t, tc.expectedMsg, outputString)
errorString := errBuf.String()
assert.Equal(t, tc.expectedErr, errorString)
}
}

func TestRunLogin(t *testing.T) {
const storedServerAddress = "reg1"
const validUsername = "u1"
const validPassword = "p1"
const validPassword2 = "p2"

validAuthConfig := types.AuthConfig{
ServerAddress: storedServerAddress,
Username: validUsername,
Password: validPassword,
}
expiredAuthConfig := types.AuthConfig{
ServerAddress: storedServerAddress,
Username: validUsername,
Password: expiredPassword,
}
testCases := []struct {
inputLoginOption loginOptions
inputStoredCred *types.AuthConfig
expectedErr string
expectedSavedCred types.AuthConfig
}{
{
inputLoginOption: loginOptions{
serverAddress: storedServerAddress,
},
inputStoredCred: &validAuthConfig,
expectedErr: "",
expectedSavedCred: validAuthConfig,
},
{
inputLoginOption: loginOptions{
serverAddress: storedServerAddress,
},
inputStoredCred: &expiredAuthConfig,
expectedErr: "Error: Cannot perform an interactive login from a non TTY device",
},
{
inputLoginOption: loginOptions{
serverAddress: storedServerAddress,
user: validUsername,
password: validPassword2,
},
inputStoredCred: &validAuthConfig,
expectedErr: "",
expectedSavedCred: types.AuthConfig{
ServerAddress: storedServerAddress,
Username: validUsername,
Password: validPassword2,
},
},
{
inputLoginOption: loginOptions{
serverAddress: storedServerAddress,
user: userErr,
password: validPassword,
},
inputStoredCred: &validAuthConfig,
expectedErr: testAuthErrMsg,
},
}
for _, tc := range testCases {
cli := test.NewFakeCli(&fakeClient{})
errBuf := new(bytes.Buffer)
cli.SetErr(errBuf)
if tc.inputStoredCred != nil {
cred := *tc.inputStoredCred
cli.ConfigFile().GetCredentialsStore(cred.ServerAddress).Store(cred)
}
loginErr := runLogin(cli, tc.inputLoginOption)
if tc.expectedErr != "" {
assert.Equal(t, tc.expectedErr, loginErr.Error())
} else {
assert.Nil(t, loginErr)
savedCred, credStoreErr := cli.ConfigFile().GetCredentialsStore(tc.inputStoredCred.ServerAddress).Get(tc.inputStoredCred.ServerAddress)
assert.Nil(t, credStoreErr)
assert.Equal(t, tc.expectedSavedCred, savedCred)
}
}
}
Loading