Skip to content

Commit

Permalink
Merge branch 'main' into feature/pre-release-api
Browse files Browse the repository at this point in the history
  • Loading branch information
natalieparellano committed Jun 2, 2022
2 parents fe1b347 + b5f8bad commit 09a21ce
Show file tree
Hide file tree
Showing 9 changed files with 946 additions and 562 deletions.
3 changes: 1 addition & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -242,8 +242,7 @@ format: install-goimports

tidy:
@echo "> Tidying modules..."
$(GOCMD) mod tidy
cd tools && $(GOCMD) mod tidy
$(GOCMD) mod tidy -compat=1.17

test: unit acceptance

Expand Down
176 changes: 116 additions & 60 deletions auth/env_keychain.go → auth/keychain.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,70 +4,98 @@ import (
"encoding/base64"
"encoding/json"
"fmt"
"io/ioutil"
"os"
"regexp"

ecr "github.com/awslabs/amazon-ecr-credential-helper/ecr-login"
"github.com/chrismellard/docker-credential-acr-env/pkg/credhelper"
"github.com/google/go-containerregistry/pkg/authn"
"github.com/google/go-containerregistry/pkg/name"
"github.com/pkg/errors"
)

const EnvRegistryAuth = "CNB_REGISTRY_AUTH"

var (
amazonKeychain = authn.NewKeychainFromHelper(ecr.NewECRHelper(ecr.WithLogger(ioutil.Discard)))
azureKeychain = authn.NewKeychainFromHelper(credhelper.NewACRCredentialsHelper())
)

// DefaultKeychain returns a keychain containing authentication configuration for the given images
// from the following sources, if they exist, in order of precedence:
// the provided environment variable
// the docker config.json file
// credential helpers for Amazon and Azure
func DefaultKeychain(images ...string) (authn.Keychain, error) {
envKeychain, err := EnvKeychain(EnvRegistryAuth)
envKeychain, err := NewEnvKeychain(EnvRegistryAuth)
if err != nil {
return nil, err
}

return authn.NewMultiKeychain(
envKeychain,
InMemoryKeychain(authn.DefaultKeychain, images...),
NewResolvedKeychain(authn.DefaultKeychain, images...),
NewResolvedKeychain(amazonKeychain, images...),
NewResolvedKeychain(azureKeychain, images...),
), nil
}

// ResolvedKeychain is an implementation of authn.Keychain that stores credentials in memory.
type ResolvedKeychain struct {
Auths map[string]string
}

// EnvKeychain returns an authn.Keychain that uses the provided environment variable as a source of credentials.
// NewEnvKeychain returns an authn.Keychain that uses the provided environment variable as a source of credentials.
// The value of the environment variable should be a JSON object that maps OCI registry hostnames to Authorization headers.
func EnvKeychain(envVar string) (authn.Keychain, error) {
func NewEnvKeychain(envVar string) (authn.Keychain, error) {
authHeaders, err := ReadEnvVar(envVar)
if err != nil {
return nil, errors.Wrap(err, "reading auth env var")
}
return &ResolvedKeychain{Auths: authHeaders}, nil
return &EnvKeychain{AuthHeaders: authHeaders}, nil
}

// InMemoryKeychain resolves credentials for the given images from the given keychain and returns a new keychain
// that stores the pre-resolved credentials in memory and returns them on demand. This is useful in cases where the
// backing credential store may become inaccessible in the the future.
func InMemoryKeychain(keychain authn.Keychain, images ...string) authn.Keychain {
return &ResolvedKeychain{
Auths: buildAuthMap(keychain, images...),
}
// EnvKeychain is an implementation of authn.Keychain that stores credentials as auth headers.
type EnvKeychain struct {
AuthHeaders map[string]string
}

func (k *ResolvedKeychain) Resolve(resource authn.Resource) (authn.Authenticator, error) {
header, ok := k.Auths[resource.RegistryStr()]
func (k *EnvKeychain) Resolve(resource authn.Resource) (authn.Authenticator, error) {
header, ok := k.AuthHeaders[resource.RegistryStr()]
if ok {
authConfig, err := authHeaderToConfig(header)
if err != nil {
return nil, errors.Wrap(err, "parsing auth header")
}

return &providedAuth{config: authConfig}, nil
}

return authn.Anonymous, nil
}

var (
basicAuthRegExp = regexp.MustCompile("(?i)^basic (.*)$")
bearerAuthRegExp = regexp.MustCompile("(?i)^bearer (.*)$")
identityTokenRegExp = regexp.MustCompile("(?i)^x-identity (.*)$")
)

func authHeaderToConfig(header string) (*authn.AuthConfig, error) {
if matches := basicAuthRegExp.FindAllStringSubmatch(header, -1); len(matches) != 0 {
return &authn.AuthConfig{
Auth: matches[0][1],
}, nil
}

if matches := bearerAuthRegExp.FindAllStringSubmatch(header, -1); len(matches) != 0 {
return &authn.AuthConfig{
RegistryToken: matches[0][1],
}, nil
}

if matches := identityTokenRegExp.FindAllStringSubmatch(header, -1); len(matches) != 0 {
return &authn.AuthConfig{
IdentityToken: matches[0][1],
}, nil
}

return nil, errors.New("unknown auth type from header")
}

type providedAuth struct {
config *authn.AuthConfig
}
Expand All @@ -76,6 +104,50 @@ func (p *providedAuth) Authorization() (*authn.AuthConfig, error) {
return p.config, nil
}

// NewResolvedKeychain resolves credentials for the given images from the given keychain and returns a new keychain
// that stores the pre-resolved credentials in memory and returns them on demand. This is useful in cases where the
// backing credential store may become inaccessible in the the future.
func NewResolvedKeychain(keychain authn.Keychain, images ...string) authn.Keychain {
return &ResolvedKeychain{
AuthConfigs: buildAuthConfigs(keychain, images...),
}
}

func buildAuthConfigs(keychain authn.Keychain, images ...string) map[string]*authn.AuthConfig {
registryAuths := map[string]*authn.AuthConfig{}
for _, image := range images {
reference, authenticator, err := ReferenceForRepoName(keychain, image)
if err != nil {
continue
}
if authenticator == authn.Anonymous {
continue
}
authConfig, err := authenticator.Authorization()
if err != nil {
continue
}
if *authConfig == (authn.AuthConfig{}) {
continue
}
registryAuths[reference.Context().Registry.Name()] = authConfig
}
return registryAuths
}

// ResolvedKeychain is an implementation of authn.Keychain that stores credentials in memory.
type ResolvedKeychain struct {
AuthConfigs map[string]*authn.AuthConfig
}

func (k *ResolvedKeychain) Resolve(resource authn.Resource) (authn.Authenticator, error) {
authConfig, ok := k.AuthConfigs[resource.RegistryStr()]
if ok {
return &providedAuth{config: authConfig}, nil
}
return authn.Anonymous, nil
}

// ReadEnvVar parses an environment variable to produce a map of 'registry url' to 'authorization header'.
//
// Complementary to `BuildEnvVar`.
Expand All @@ -100,47 +172,47 @@ func ReadEnvVar(envVar string) (map[string]string, error) {
return authMap, nil
}

func buildAuthMap(keychain authn.Keychain, images ...string) map[string]string {
registryAuths := map[string]string{}
// BuildEnvVar creates the contents to use for authentication environment variable.
//
// Complementary to `ReadEnvVar`.
func BuildEnvVar(keychain authn.Keychain, images ...string) (string, error) {
registryAuths := buildAuthHeaders(keychain, images...)

authData, err := json.Marshal(registryAuths)
if err != nil {
return "", err
}
return string(authData), nil
}

func buildAuthHeaders(keychain authn.Keychain, images ...string) map[string]string {
registryAuths := map[string]string{}
for _, image := range images {
reference, authenticator, err := ReferenceForRepoName(keychain, image)
if err != nil {
continue
}

if authenticator == authn.Anonymous {
continue
}

authConfig, err := authenticator.Authorization()
if err != nil {
continue
}

header, err := authConfigToHeader(authConfig)
if err != nil {
continue
}
registryAuths[reference.Context().Registry.Name()] = header
}

return registryAuths
}

// BuildEnvVar creates the contents to use for authentication environment variable.
//
// Complementary to `ReadEnvVar`.
func BuildEnvVar(keychain authn.Keychain, images ...string) (string, error) {
registryAuths := buildAuthMap(keychain, images...)

authData, err := json.Marshal(registryAuths)
if err != nil {
return "", err
}
return string(authData), nil
}

// authConfigToHeader accepts an authn.AuthConfig and returns an Authorization header,
// or an error if the config cannot be processed.
// Note that when resolving credentials, the header is simply used to reconstruct the originally provided authn.AuthConfig,
// making it essentially a stringification (the actual value is unimportant as long as it is consistent and contains
// all the necessary information).
func authConfigToHeader(config *authn.AuthConfig) (string, error) {
if config.Auth != "" {
return fmt.Sprintf("Basic %s", config.Auth), nil
Expand All @@ -156,28 +228,12 @@ func authConfigToHeader(config *authn.AuthConfig) (string, error) {
return fmt.Sprintf("Basic %s", encoded), nil
}

return "", nil
}

var (
basicAuthRegExp = regexp.MustCompile("(?i)^basic (.*)$")
bearerAuthRegExp = regexp.MustCompile("(?i)^bearer (.*)$")
)

func authHeaderToConfig(header string) (*authn.AuthConfig, error) {
if matches := basicAuthRegExp.FindAllStringSubmatch(header, -1); len(matches) != 0 {
return &authn.AuthConfig{
Auth: matches[0][1],
}, nil
if config.IdentityToken != "" {
// There isn't an Authorization header for identity tokens, but we just need a way to represent the data.
return fmt.Sprintf("X-Identity %s", config.IdentityToken), nil
}

if matches := bearerAuthRegExp.FindAllStringSubmatch(header, -1); len(matches) != 0 {
return &authn.AuthConfig{
RegistryToken: matches[0][1],
}, nil
}

return nil, errors.New("unknown auth type from header")
return "", errors.New("failed to find authorization information")
}

// ReferenceForRepoName returns a reference and an authenticator for a given image name and keychain.
Expand Down
Loading

0 comments on commit 09a21ce

Please sign in to comment.