Skip to content

Commit

Permalink
Auth code refactor: credstore and registry URL
Browse files Browse the repository at this point in the history
Signed-off-by: apostasie <[email protected]>
  • Loading branch information
apostasie committed Aug 12, 2024
1 parent 728421a commit 0afef56
Show file tree
Hide file tree
Showing 13 changed files with 1,415 additions and 385 deletions.
440 changes: 326 additions & 114 deletions cmd/nerdctl/login_linux_test.go

Large diffs are not rendered by default.

62 changes: 16 additions & 46 deletions cmd/nerdctl/logout.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,14 @@
package main

import (
"fmt"

"github.com/containerd/nerdctl/v2/pkg/imgutil/dockerconfigresolver"
dockercliconfig "github.com/docker/cli/cli/config"
"github.com/spf13/cobra"

"github.com/containerd/log"
"github.com/containerd/nerdctl/v2/pkg/cmd/logout"
)

func newLogoutCommand() *cobra.Command {
var logoutCommand = &cobra.Command{
return &cobra.Command{
Use: "logout [flags] [SERVER]",
Args: cobra.MaximumNArgs(1),
Short: "Log out from a container registry",
Expand All @@ -34,62 +33,33 @@ func newLogoutCommand() *cobra.Command {
SilenceUsage: true,
SilenceErrors: true,
}
return logoutCommand
}

// code inspired from XXX
func logoutAction(cmd *cobra.Command, args []string) error {
serverAddress := dockerconfigresolver.IndexServer
isDefaultRegistry := true
if len(args) >= 1 {
serverAddress = args[0]
isDefaultRegistry = false
}

var (
regsToLogout = []string{serverAddress}
hostnameAddress = serverAddress
)

if !isDefaultRegistry {
hostnameAddress = dockerconfigresolver.ConvertToHostname(serverAddress)
// the tries below are kept for backward compatibility where a user could have
// saved the registry in one of the following format.
regsToLogout = append(regsToLogout, hostnameAddress, "http://"+hostnameAddress, "https://"+hostnameAddress)
logoutServer := ""
if len(args) > 0 {
logoutServer = args[0]
}

fmt.Fprintf(cmd.OutOrStdout(), "Removing login credentials for %s\n", hostnameAddress)

dockerConfigFile, err := dockercliconfig.Load("")
errGroup, err := logout.Logout(cmd.Context(), logoutServer)
if err != nil {
return err
log.L.WithError(err).Errorf("Failed to erase credentials for: %s", logoutServer)
}
errs := make(map[string]error)
for _, r := range regsToLogout {
if err := dockerConfigFile.GetCredentialsStore(r).Erase(r); err != nil {
errs[r] = err
if errGroup != nil {
log.L.Error("None of the following entries could be found")
for _, v := range errGroup {
log.L.Errorf("%s", v)
}
}

// if at least one removal succeeded, report success. Otherwise report errors
if len(errs) == len(regsToLogout) {
fmt.Fprintln(cmd.ErrOrStderr(), "WARNING: could not erase credentials:")
for k, v := range errs {
fmt.Fprintf(cmd.OutOrStdout(), "%s: %s\n", k, v)
}
}

return nil
return err
}

func logoutShellComplete(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
dockerConfigFile, err := dockercliconfig.Load("")
candidates, err := logout.ShellCompletion()
if err != nil {
return nil, cobra.ShellCompDirectiveError
}
candidates := []string{}
for key := range dockerConfigFile.AuthConfigs {
candidates = append(candidates, key)
}

return candidates, cobra.ShellCompDirectiveNoFileComp
}
131 changes: 33 additions & 98 deletions pkg/cmd/login/login.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,6 @@ import (
"os"
"strings"

dockercliconfig "github.com/docker/cli/cli/config"
dockercliconfigtypes "github.com/docker/cli/cli/config/types"
"github.com/docker/docker/api/types/registry"
"golang.org/x/net/context/ctxhttp"
"golang.org/x/term"

Expand All @@ -47,106 +44,64 @@ Configure a credential helper to remove this warning. See
https://docs.docker.com/engine/reference/commandline/login/#credentials-store
`

type isFileStore interface {
IsFileStore() bool
GetFilename() string
}

func Login(ctx context.Context, options types.LoginCommandOptions, stdout io.Writer) error {
var serverAddress string
if options.ServerAddress == "" || options.ServerAddress == "docker.io" || options.ServerAddress == "index.docker.io" || options.ServerAddress == "registry-1.docker.io" {
serverAddress = dockerconfigresolver.IndexServer
} else {
serverAddress = options.ServerAddress
registryURL, err := dockerconfigresolver.Parse(options.ServerAddress)
if err != nil {
return err
}

credStore, err := dockerconfigresolver.NewCredentialsStore("")
if err != nil {
return err
}

var responseIdentityToken string
isDefaultRegistry := serverAddress == dockerconfigresolver.IndexServer

authConfig, err := GetDefaultAuthConfig(options.Username == "" && options.Password == "", serverAddress, isDefaultRegistry)
if authConfig == nil {
authConfig = &registry.AuthConfig{ServerAddress: serverAddress}
}
if err == nil && authConfig.Username != "" && authConfig.Password != "" {
// login With StoreCreds
responseIdentityToken, err = loginClientSide(ctx, options.GOptions, *authConfig)
credentials, err := credStore.Retrieve(registryURL, options.Username == "" && options.Password == "")
credentials.IdentityToken = ""

if err == nil && credentials.Username != "" && credentials.Password != "" {
responseIdentityToken, err = loginClientSide(ctx, options.GOptions, registryURL, credentials)
}

if err != nil || authConfig.Username == "" || authConfig.Password == "" {
err = ConfigureAuthentication(authConfig, options.Username, options.Password)
if err != nil || credentials.Username == "" || credentials.Password == "" {
err = configureAuthentication(credentials, options.Username, options.Password)
if err != nil {
return err
}

responseIdentityToken, err = loginClientSide(ctx, options.GOptions, *authConfig)
responseIdentityToken, err = loginClientSide(ctx, options.GOptions, registryURL, credentials)
if err != nil {
return err
}
}

if responseIdentityToken != "" {
authConfig.Password = ""
authConfig.IdentityToken = responseIdentityToken
}

dockerConfigFile, err := dockercliconfig.Load("")
if err != nil {
return err
credentials.Password = ""
credentials.IdentityToken = responseIdentityToken
}

creds := dockerConfigFile.GetCredentialsStore(serverAddress)

store, isFile := creds.(isFileStore)
// Display a warning if we're storing the users password (not a token) and credentials store type is file.
if isFile && authConfig.Password != "" {
_, err = fmt.Fprintln(stdout, fmt.Sprintf(unencryptedPasswordWarning, store.GetFilename()))
storageFileLocation := credStore.FileStorageLocation(registryURL)
if storageFileLocation != "" && credentials.Password != "" {
_, err = fmt.Fprintln(stdout, fmt.Sprintf(unencryptedPasswordWarning, storageFileLocation))
if err != nil {
return err
}
}

if err := creds.Store(dockercliconfigtypes.AuthConfig(*(authConfig))); err != nil {
err = credStore.Store(registryURL, credentials)
if err != nil {
return fmt.Errorf("error saving credentials: %w", err)
}

fmt.Fprintln(stdout, "Login Succeeded")

return nil
}
_, err = fmt.Fprintln(stdout, "Login Succeeded")

// 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.
// Code from github.com/docker/cli/cli/command (v20.10.3).
func GetDefaultAuthConfig(checkCredStore bool, serverAddress string, isDefaultRegistry bool) (*registry.AuthConfig, error) {
if !isDefaultRegistry {
var err error
serverAddress, err = convertToHostname(serverAddress)
if err != nil {
return nil, err
}
}
authconfig := dockercliconfigtypes.AuthConfig{}
if checkCredStore {
dockerConfigFile, err := dockercliconfig.Load("")
if err != nil {
return nil, err
}
authconfig, err = dockerConfigFile.GetAuthConfig(serverAddress)
if err != nil {
return nil, err
}
}
authconfig.ServerAddress = serverAddress
authconfig.IdentityToken = ""
res := registry.AuthConfig(authconfig)
return &res, nil
return err
}

func loginClientSide(ctx context.Context, globalOptions types.GlobalCommandOptions, auth registry.AuthConfig) (string, error) {
host, err := convertToHostname(auth.ServerAddress)
if err != nil {
return "", err
}
func loginClientSide(ctx context.Context, globalOptions types.GlobalCommandOptions, registryURL *dockerconfigresolver.RegistryURL, credentials *dockerconfigresolver.Credentials) (string, error) {
host := registryURL.Host
var dOpts []dockerconfigresolver.Opt
if globalOptions.InsecureRegistry {
log.G(ctx).Warnf("skipping verifying HTTPS certs for %q", host)
Expand All @@ -156,12 +111,12 @@ func loginClientSide(ctx context.Context, globalOptions types.GlobalCommandOptio

authCreds := func(acArg string) (string, string, error) {
if acArg == host {
if auth.RegistryToken != "" {
if credentials.RegistryToken != "" {
// Even containerd/CRI does not support RegistryToken as of v1.4.3,
// so, nobody is actually using RegistryToken?
log.G(ctx).Warnf("RegistryToken (for %q) is not supported yet (FIXME)", host)
}
return auth.Username, auth.Password, nil
return credentials.Username, credentials.Password, nil
}
return "", "", fmt.Errorf("expected acArg to be %q, got %q", host, acArg)
}
Expand Down Expand Up @@ -250,10 +205,9 @@ func tryLoginWithRegHost(ctx context.Context, rh docker.RegistryHost) error {
return errors.New("too many 401 (probably)")
}

func ConfigureAuthentication(authConfig *registry.AuthConfig, username, password string) error {
authConfig.Username = strings.TrimSpace(authConfig.Username)
func configureAuthentication(credentials *dockerconfigresolver.Credentials, username, password string) error {
if username = strings.TrimSpace(username); username == "" {
username = authConfig.Username
username = credentials.Username
}
if username == "" {
fmt.Print("Enter Username: ")
Expand All @@ -280,8 +234,8 @@ func ConfigureAuthentication(authConfig *registry.AuthConfig, username, password
return fmt.Errorf("error: Password is Required")
}

authConfig.Username = username
authConfig.Password = password
credentials.Username = username
credentials.Password = password

return nil
}
Expand All @@ -303,22 +257,3 @@ func readUsername() (string, error) {

return username, nil
}

func convertToHostname(serverAddress string) (string, error) {
// Ensure that URL contains scheme for a good parsing process
if strings.Contains(serverAddress, "://") {
u, err := url.Parse(serverAddress)
if err != nil {
return "", err
}
serverAddress = u.Host
} else {
u, err := url.Parse("https://" + serverAddress)
if err != nil {
return "", err
}
serverAddress = u.Host
}

return serverAddress, nil
}
46 changes: 46 additions & 0 deletions pkg/cmd/logout/logout.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
/*
Copyright The containerd Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package logout

import (
"context"

"github.com/containerd/nerdctl/v2/pkg/imgutil/dockerconfigresolver"
)

func Logout(ctx context.Context, logoutServer string) (map[string]error, error) {
reg, err := dockerconfigresolver.Parse(logoutServer)
if err != nil {
return nil, err
}

credentialsStore, err := dockerconfigresolver.NewCredentialsStore("")
if err != nil {
return nil, err
}

return credentialsStore.Erase(reg)
}

func ShellCompletion() ([]string, error) {
credentialsStore, err := dockerconfigresolver.NewCredentialsStore("")
if err != nil {
return nil, err
}

return credentialsStore.ShellCompletion(), nil
}
Loading

0 comments on commit 0afef56

Please sign in to comment.