Skip to content

Commit

Permalink
auth: refactor auth plumbing to make room for more auth methods
Browse files Browse the repository at this point in the history
  • Loading branch information
shoenig committed Jul 5, 2023
1 parent 2c6b899 commit eebb9f9
Show file tree
Hide file tree
Showing 5 changed files with 206 additions and 52 deletions.
18 changes: 0 additions & 18 deletions api/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,6 @@ package api

import (
"context"
"encoding/base64"
"encoding/json"
"fmt"
"io"
"net"
Expand All @@ -30,12 +28,6 @@ type ClientConfig struct {
HttpTimeout time.Duration
}

type ImageAuthConfig struct {
TLSVerify bool
Username string
Password string
}

func DefaultClientConfig() ClientConfig {
cfg := ClientConfig{
HttpTimeout: 60 * time.Second,
Expand Down Expand Up @@ -124,16 +116,6 @@ func (c *API) Delete(ctx context.Context, path string) (*http.Response, error) {
return c.Do(req)
}

// NewAuthHeader encodes auth configuration to a docker X-Registry-Auth header payload.
func NewAuthHeader(auth ImageAuthConfig) (string, error) {
jsonBytes, err := json.Marshal(auth)
if err != nil {
return "", err
}
header := base64.StdEncoding.EncodeToString(jsonBytes)
return header, nil
}

func ignoreClose(c io.Closer) {
_ = c.Close()
}
61 changes: 32 additions & 29 deletions api/image_pull.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,52 +10,55 @@ import (
"fmt"
"io"
"net/http"

"github.com/hashicorp/nomad-driver-podman/registry"
)

// ImagePull pulls a image from a remote location to local storage
func (c *API) ImagePull(ctx context.Context, nameWithTag string, auth ImageAuthConfig) (string, error) {
var id string
headers := map[string]string{}

// handle authentication
usesAuth := auth.Username != "" && auth.Password != ""
if usesAuth {
authHeader, err := NewAuthHeader(auth)
if err != nil {
return "", err
}
headers["X-Registry-Auth"] = authHeader
func (c *API) ImagePull(ctx context.Context, pullConfig *registry.PullConfig) (string, error) {
pullConfig.Log(c.logger)

var (
headers = make(map[string]string)
repository = pullConfig.Repository
tlsVerify = pullConfig.TLSVerify
)

auth, err := registry.ResolveRegistryAuthentication(repository, pullConfig)
if err != nil {
return "", fmt.Errorf("failed to determine authentication for %q", repository)
}
auth.SetHeader(headers)
c.logger.Info("HEADERS", "headers", headers)

c.logger.Trace("image pull details", "tls_verify", auth.TLSVerify, "reference", nameWithTag, "uses_auth", usesAuth)
urlPath := fmt.Sprintf("/v1.0.0/libpod/images/pull?reference=%s&tlsVerify=%t", repository, tlsVerify)
c.logger.Info("URL PATH", "urlPath", urlPath)

urlPath := fmt.Sprintf("/v1.0.0/libpod/images/pull?reference=%s&tlsVerify=%t", nameWithTag, auth.TLSVerify)
res, err := c.PostWithHeaders(ctx, urlPath, nil, headers)
if err != nil {
return "", err
return "", fmt.Errorf("failed to pull image: %w", err)
}

defer ignoreClose(res.Body)

if res.StatusCode != http.StatusOK {
body, _ := io.ReadAll(res.Body)
return "", fmt.Errorf("cannot pull image, status code: %d: %s", res.StatusCode, body)
}

dec := json.NewDecoder(res.Body)
var report ImagePullReport
var (
dec = json.NewDecoder(res.Body)
report ImagePullReport
id string
)
for {
decErr := dec.Decode(&report)
if errors.Is(decErr, io.EOF) {
decodeErr := dec.Decode(&report)
if errors.Is(decodeErr, io.EOF) {
break
} else if decErr != nil {
return "", fmt.Errorf("Error reading response: %w", decErr)
}

if report.Error != "" {
return "", errors.New(report.Error)
}

if report.ID != "" {
} else if decodeErr != nil {
return "", fmt.Errorf("failed to read image pull response: %w", decodeErr)
} else if report.Error != "" {
return "", fmt.Errorf("image pull report indicates error: %s", report.Error)
} else if report.ID != "" {
id = report.ID
}
}
Expand Down
8 changes: 8 additions & 0 deletions config.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,11 @@ import (
var (
// configSpec is the hcl specification returned by the ConfigSchema RPC
configSpec = hclspec.NewObject(map[string]*hclspec.Spec{
// image registry authentication options
"auth": hclspec.NewBlock("auth", false, hclspec.NewObject(map[string]*hclspec.Spec{
"config": hclspec.NewAttr("config", "string", false),
})),

// volume options
"volumes": hclspec.NewDefault(hclspec.NewBlock("volumes", false, hclspec.NewObject(map[string]*hclspec.Spec{
"enabled": hclspec.NewDefault(
Expand Down Expand Up @@ -59,6 +64,7 @@ var (
hclspec.NewLiteral("true"),
),
})),
"auth_soft_fail": hclspec.NewAttr("auth_soft_fail", "bool", false),
"command": hclspec.NewAttr("command", "string", false),
"cap_add": hclspec.NewAttr("cap_add", "list(string)", false),
"cap_drop": hclspec.NewAttr("cap_drop", "list(string)", false),
Expand Down Expand Up @@ -102,6 +108,7 @@ var (
)

// AuthConfig is the tasks authentication configuration
// (there is also auth_soft_fail on the top level)
type AuthConfig struct {
Username string `codec:"username"`
Password string `codec:"password"`
Expand Down Expand Up @@ -148,6 +155,7 @@ type TaskConfig struct {
ApparmorProfile string `codec:"apparmor_profile"`
Args []string `codec:"args"`
Auth AuthConfig `codec:"auth"`
AuthSoftFail bool `codec:"auth_soft_fail"`
Ports []string `codec:"ports"`
Tmpfs []string `codec:"tmpfs"`
Volumes []string `codec:"volumes"`
Expand Down
14 changes: 9 additions & 5 deletions driver.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import (
"github.com/containers/image/v5/types"
"github.com/hashicorp/go-hclog"
"github.com/hashicorp/nomad-driver-podman/api"
"github.com/hashicorp/nomad-driver-podman/registry"
"github.com/hashicorp/nomad-driver-podman/version"
"github.com/hashicorp/nomad/client/stats"
"github.com/hashicorp/nomad/client/taskenv"
Expand Down Expand Up @@ -961,18 +962,21 @@ func (d *Driver) createImage(image string, auth *AuthConfig, forcePull bool, ima
Message: "Pulling image " + imageName,
})

imageAuth := api.ImageAuthConfig{
TLSVerify: auth.TLSVerify,
Username: auth.Username,
Password: auth.Password,
pc := &registry.PullConfig{
Repository: imageName,
TLSVerify: auth.TLSVerify,
RegistryConfig: &registry.RegistryAuthConfig{
Username: auth.Username,
Password: auth.Password,
},
}

result, err, _ := d.pullGroup.Do(imageName, func() (interface{}, error) {

ctx, cancel := context.WithTimeout(context.Background(), imagePullTimeout)
defer cancel()

if imageID, err = d.slowPodman.ImagePull(ctx, imageName, imageAuth); err != nil {
if imageID, err = d.slowPodman.ImagePull(ctx, pc); err != nil {
return imageID, fmt.Errorf("failed to start task, unable to pull image %s : %w", imageName, err)
}
return imageID, nil
Expand Down
157 changes: 157 additions & 0 deletions registry/authentication.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
package registry

import (
"encoding/base64"
"encoding/json"
"fmt"

"github.com/hashicorp/go-hclog"
)

// PullConfig represents the necessary information needed for an image pull query.
type PullConfig struct {
Repository string
RegistryConfig *RegistryAuthConfig
TLSVerify bool

// TODO: config file
// TODO: creds helper
}

// BasicConfig returns the Basic-Auth level of configuration.
//
// For the podman driver, this is what can be supplied in the task config block.
func (pc *PullConfig) BasicConfig() *TaskAuthConfig {
if pc == nil || pc.RegistryConfig == nil {
return nil
}
return &TaskAuthConfig{
Username: pc.RegistryConfig.Username,
Password: pc.RegistryConfig.Password,
Email: pc.RegistryConfig.Email,
ServerAddress: pc.RegistryConfig.ServerAddress,
}
}

// Log the components of PullConfig in a safe way.
func (pc *PullConfig) Log(logger hclog.Logger) {
var (
repository = pc.Repository
tls bool
username = "<unset>"
email = "<unset>"
server = "<unset>"
password bool
token bool
)

if r := pc.RegistryConfig; r != nil {
tls = pc.TLSVerify
username = r.Username
email = r.Email
server = r.ServerAddress
password = r.Password != ""
token = r.IdentityToken != ""
}

// todo: trace
logger.Info("pull config",
"repository", repository,
"username", username,
"password", password,
"email", email,
"server", server,
"tls", tls,
"token", token,
)
}

// RegistryAuthConfig represents the actualized authentication for accessing a
// container registry.
//
// Note that IdentityToken is mutually exclusive with the remaining fields.
type RegistryAuthConfig struct {
Username string `json:"username,omitempty"`
Password string `json:"password,omitempty"`
Email string `json:"email,omitempty"`
ServerAddress string `json:"serveraddress,omitempty"`
IdentityToken string `json:"identitytoken,omitempty"`
}

// SetHeader will apply the X-Registry-Auth header (if necessary) to headers.
func (r *RegistryAuthConfig) SetHeader(headers map[string]string) {
if !r.Empty() {
b, err := json.Marshal(r)
if err != nil {
return
}
header := base64.StdEncoding.EncodeToString(b)
headers["X-Registry-Auth"] = header
}
}

// Empty returns true if all of username, password, and identity token are unset.
func (r *RegistryAuthConfig) Empty() bool {
return r == nil || (r.Username == "" && r.Password == "" && r.IdentityToken == "")
}

// TaskAuthConfig represents the "auth" section of the config block for
// the podman task driver.
type TaskAuthConfig struct {
Username string
Password string
Email string
ServerAddress string
}

func (c *TaskAuthConfig) IsEmpty() bool {
return c == nil || (c.Username == "" && c.Password == "")
}

// ResolveRegistryAuthentication will find a compatible AuthBackend for the given repository.
// In order, try using
// - auth block from task
// - auth from auth.json file specified by plugin config
// - auth from a credentials helper specified by plugin config
func ResolveRegistryAuthentication(repository string, pullConfig *PullConfig) (*RegistryAuthConfig, error) {
return firstValidAuth(repository, []AuthBackend{
authFromTaskConfig(pullConfig.BasicConfig()),
})
}

// An AuthBackend is a function that resolves registry credentials for a given
// repository. If no auth exitsts for the given repository, (nil, nil) is
// returned and should be skipped.
type AuthBackend func(string) (*RegistryAuthConfig, error)

func firstValidAuth(repository string, authBackends []AuthBackend) (*RegistryAuthConfig, error) {
for _, backend := range authBackends {
auth, err := backend(repository)
if auth != nil || err != nil {
return auth, err
}
}
return nil, nil
}

func authFromTaskConfig(taskAuthConfig *TaskAuthConfig) AuthBackend {
return func(string) (*RegistryAuthConfig, error) {
if taskAuthConfig.IsEmpty() {
return nil, nil
}
return &RegistryAuthConfig{
Username: taskAuthConfig.Username,
Password: taskAuthConfig.Password,
Email: taskAuthConfig.Email,
ServerAddress: taskAuthConfig.ServerAddress,
}, nil
}
}

func authFromFileConfig(filename string) (*RegistryAuthConfig, error) {
return nil, fmt.Errorf("not implemented yet")
}

func authFromCredsHelper(helper string) (*RegistryAuthConfig, error) {
return nil, fmt.Errorf("not implemented yet")
}

0 comments on commit eebb9f9

Please sign in to comment.