Skip to content

Commit

Permalink
feat: Support username and password prompt in login (notaryproject#566)
Browse files Browse the repository at this point in the history
notaryproject#503

Added e2e tests. The current unit tests don't have RunE() testing code.
Adding the unit test could lead to lots of refactoring. I'm happy to
help the refactoring and start the unit tests of RunE() but I think it
is better to be in a separated PR. Adding lots of unit test refactoring
could mess up this PR.

TODO: Seems login requires credential helper. Need to install credential
helper as a test step.

Signed-off-by: Ziwen Ning <[email protected]>
  • Loading branch information
ningziwen authored Apr 13, 2023
1 parent 0ec3b3d commit 14a2d2b
Show file tree
Hide file tree
Showing 9 changed files with 126 additions and 5 deletions.
54 changes: 51 additions & 3 deletions cmd/notation/login.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package main

import (
"bufio"
"context"
"errors"
"fmt"
Expand All @@ -11,6 +12,7 @@ import (
"github.com/notaryproject/notation/internal/cmd"
"github.com/notaryproject/notation/pkg/auth"
"github.com/spf13/cobra"
"golang.org/x/term"
orasauth "oras.land/oras-go/v2/registry/remote/auth"
)

Expand Down Expand Up @@ -65,6 +67,23 @@ func runLogin(ctx context.Context, opts *loginOpts) error {
// initialize
serverAddress := opts.server

// input username and password by prompt
reader := bufio.NewReader(os.Stdin)
var err error
if opts.Username == "" {
opts.Username, err = readUsernameFromPrompt(reader)
if err != nil {
return err
}
}

if opts.Password == "" {
opts.Password, err = readPasswordFromPrompt(reader)
if err != nil {
return err
}
}

if err := validateAuthConfig(ctx, opts, serverAddress); err != nil {
return err
}
Expand Down Expand Up @@ -108,7 +127,7 @@ func newCredentialFromInput(username, password string) orasauth.Credential {

func readPassword(opts *loginOpts) error {
if opts.passwordStdin {
password, err := readLine()
password, err := readLine(os.Stdin)
if err != nil {
return err
}
Expand All @@ -117,12 +136,41 @@ func readPassword(opts *loginOpts) error {
return nil
}

func readLine() (string, error) {
passwordBytes, err := io.ReadAll(os.Stdin)
func readLine(r io.Reader) (string, error) {
passwordBytes, err := io.ReadAll(r)
if err != nil {
return "", err
}
password := strings.TrimSuffix(string(passwordBytes), "\n")
password = strings.TrimSuffix(password, "\r")
return password, nil
}

func readUsernameFromPrompt(reader *bufio.Reader) (string, error) {
fmt.Print("Username: ")
username, err := reader.ReadString('\n')
if err != nil {
return "", fmt.Errorf("error reading username: %w", err)
}
username = strings.TrimSpace(username)
return username, nil
}

func readPasswordFromPrompt(reader *bufio.Reader) (string, error) {
fmt.Print("Password: ")
if term.IsTerminal(int(os.Stdin.Fd())) {
bytePassword, err := term.ReadPassword(int(os.Stdin.Fd()))
if err != nil {
return "", fmt.Errorf("error reading password: %w", err)
}
fmt.Println()
return string(bytePassword), nil
} else {
password, err := readLine(reader)
if err != nil {
return "", fmt.Errorf("error reading password: %w", err)
}
fmt.Println()
return password, nil
}
}
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ require (
github.com/sirupsen/logrus v1.9.0
github.com/spf13/cobra v1.6.1
github.com/spf13/pflag v1.0.5
golang.org/x/term v0.5.0
oras.land/oras-go/v2 v2.0.2
)

Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,8 @@ golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.5.0 h1:n2a8QNdAb0sZNpU9R1ALUXBbY+w51fCQDN+7EdxNBsY=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
Expand Down
21 changes: 21 additions & 0 deletions test/e2e/internal/notation/host.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,17 @@ func BaseOptions() []utils.HostOption {
)
}

// TestLoginOptions returns the BaseOptions with removing AuthOption and adding ConfigOption.
// testing environment.
func TestLoginOptions() []utils.HostOption {
return Opts(
AddKeyOption("e2e.key", "e2e.crt"),
AddTrustStoreOption("e2e", filepath.Join(NotationE2ELocalKeysDir, "e2e.crt")),
AddTrustPolicyOption("trustpolicy.json"),
AddConfigJsonOption("pass_credential_helper_config.json"),
)
}

// CreateNotationDirOption creates the notation directory in temp user dir.
func CreateNotationDirOption() utils.HostOption {
return func(vhost *utils.VirtualHost) error {
Expand Down Expand Up @@ -124,6 +135,16 @@ func AddTrustPolicyOption(trustpolicyName string) utils.HostOption {
}
}

// AddConfigJsonOption adds a valid config.json for testing.
func AddConfigJsonOption(configJsonName string) utils.HostOption {
return func(vhost *utils.VirtualHost) error {
return copyFile(
filepath.Join(NotationE2EConfigJsonDir, configJsonName),
vhost.AbsolutePath(NotationDirName, ConfigJsonName),
)
}
}

// AddPlugin adds a pluginkeys.json config file and installs an e2e-plugin.
func AddPlugin(pluginPath string) utils.HostOption {
return func(vhost *utils.VirtualHost) error {
Expand Down
3 changes: 3 additions & 0 deletions test/e2e/internal/notation/init.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ const (
TrustStoreTypeCA = "ca"
PluginDirName = "plugins"
PluginName = "e2e-plugin"
ConfigJsonName = "config.json"
)

const (
Expand All @@ -41,6 +42,7 @@ var (
NotationE2EConfigPath string
NotationE2ELocalKeysDir string
NotationE2ETrustPolicyDir string
NotationE2EConfigJsonDir string
)

var (
Expand Down Expand Up @@ -78,6 +80,7 @@ func setUpNotationValues() {
setPathValue(envKeyNotationConfigPath, &NotationE2EConfigPath)
NotationE2ETrustPolicyDir = filepath.Join(NotationE2EConfigPath, "trustpolicies")
NotationE2ELocalKeysDir = filepath.Join(NotationE2EConfigPath, LocalKeysDirName)
NotationE2EConfigJsonDir = filepath.Join(NotationE2EConfigPath, LocalConfigJsonsDirName)
}

func setPathValue(envKey string, value *string) {
Expand Down
5 changes: 3 additions & 2 deletions test/e2e/internal/notation/key.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,9 @@ import (
)

const (
SigningKeysFileName = "signingkeys.json"
LocalKeysDirName = "localkeys"
SigningKeysFileName = "signingkeys.json"
LocalKeysDirName = "localkeys"
LocalConfigJsonsDirName = "configjsons"
)

// X509KeyPair contains the paths of a public/private key pair files.
Expand Down
40 changes: 40 additions & 0 deletions test/e2e/suite/command/login.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package command

import (
"fmt"

. "github.com/notaryproject/notation/test/e2e/internal/notation"
"github.com/notaryproject/notation/test/e2e/internal/utils"
. "github.com/notaryproject/notation/test/e2e/suite/common"
. "github.com/onsi/ginkgo/v2"
"github.com/onsi/gomega/gbytes"
)

var _ = Describe("notation login", func() {
BeforeEach(func() {
Skip("The login tests require setting up credential helper running in host and it is not available in Github runner. Issue to remove this skip: https://github.com/notaryproject/notation/issues/587")
})
It("should sign an image after successfully logging in the registry by prompt with a correct credential", func() {
Host(TestLoginOptions(), func(notation *utils.ExecOpts, artifact *Artifact, vhost *utils.VirtualHost) {
notation.WithInput(gbytes.BufferWithBytes([]byte(fmt.Sprintf("%s\n%s\n", TestRegistry.Username, TestRegistry.Password)))).
Exec("login", artifact.Host).
MatchKeyWords(LoginSuccessfully)
notation.Exec("sign", artifact.ReferenceWithDigest()).
MatchKeyWords(SignSuccessfully)
notation.Exec("logout", artifact.Host).
MatchKeyWords(LogoutSuccessfully)
})
})

It("should fail to sign an image after failing to log in the registry with a wrong credential", func() {
Host(TestLoginOptions(), func(notation *utils.ExecOpts, artifact *Artifact, vhost *utils.VirtualHost) {
notation.WithInput(gbytes.BufferWithBytes([]byte(fmt.Sprintf("%s\n%s\n", "invalidUser", "invalidPassword")))).
ExpectFailure().
Exec("login", artifact.Host).
MatchErrKeyWords("unauthorized")
notation.ExpectFailure().
Exec("sign", artifact.ReferenceWithDigest()).
MatchErrKeyWords("credential required for basic auth")
})
})
})
2 changes: 2 additions & 0 deletions test/e2e/suite/common/common.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package common

const (
LoginSuccessfully = "Login Succeeded"
LogoutSuccessfully = "Logout Succeeded"
SignSuccessfully = "Successfully signed"
VerifySuccessfully = "Successfully verified"
VerifyFailed = "signature verification failed"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"credsStore": "pass"
}

0 comments on commit 14a2d2b

Please sign in to comment.