Skip to content

Commit

Permalink
Merge pull request chainguard-dev#1182 from imjasonh/auth-helper
Browse files Browse the repository at this point in the history
auth: refactor into Authenticator interface
  • Loading branch information
imjasonh committed Jul 1, 2024
2 parents 2a47a3f + 10cc495 commit 057f3e2
Show file tree
Hide file tree
Showing 14 changed files with 135 additions and 87 deletions.
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ require (
golang.org/x/exp v0.0.0-20231108232855-2478ac86f678
golang.org/x/sync v0.7.0
golang.org/x/sys v0.21.0
golang.org/x/time v0.5.0
gopkg.in/ini.v1 v1.67.0
gopkg.in/yaml.v3 v3.0.1
k8s.io/apimachinery v0.30.2
Expand Down
13 changes: 0 additions & 13 deletions internal/cli/build.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@ import (
"log/slog"
"os"
"path/filepath"
"strings"
"sync"

"github.com/google/go-containerregistry/pkg/v1/layout"
Expand Down Expand Up @@ -94,17 +93,6 @@ Along the image, apko will generate SBOMs (software bill of materials) describin
}
defer os.RemoveAll(tmp)

var domain, user, pass string
if auth, ok := os.LookupEnv("HTTP_AUTH"); !ok {
// Fine, no auth.
} else if parts := strings.SplitN(auth, ":", 4); len(parts) != 4 {
return fmt.Errorf("HTTP_AUTH must be in the form 'basic:REALM:USERNAME:PASSWORD' (got %d parts)", len(parts))
} else if parts[0] != "basic" {
return fmt.Errorf("HTTP_AUTH must be in the form 'basic:REALM:USERNAME:PASSWORD' (got %q for first part)", parts[0])
} else {
domain, user, pass = parts[1], parts[2], parts[3]
}

return BuildCmd(cmd.Context(), args[1], args[2], archs,
[]string{args[1]},
writeSBOM,
Expand All @@ -124,7 +112,6 @@ Along the image, apko will generate SBOMs (software bill of materials) describin
build.WithCacheDir(cacheDir, offline),
build.WithLockFile(lockfile),
build.WithTempDir(tmp),
build.WithAuth(domain, user, pass),
build.WithIncludePaths(includePaths),
build.WithIgnoreSignatures(ignoreSignatures),
)
Expand Down
12 changes: 0 additions & 12 deletions internal/cli/publish.go
Original file line number Diff line number Diff line change
Expand Up @@ -103,17 +103,6 @@ in a keychain.`,
}
defer os.RemoveAll(tmp)

var domain, user, pass string
if auth, ok := os.LookupEnv("HTTP_AUTH"); !ok {
// Fine, no auth.
} else if parts := strings.SplitN(auth, ":", 4); len(parts) != 4 {
return fmt.Errorf("HTTP_AUTH must be in the form 'basic:REALM:USERNAME:PASSWORD' (got %d parts)", len(parts))
} else if parts[0] != "basic" {
return fmt.Errorf("HTTP_AUTH must be in the form 'basic:REALM:USERNAME:PASSWORD' (got %q for first part)", parts[0])
} else {
domain, user, pass = parts[1], parts[2], parts[3]
}

if err := PublishCmd(cmd.Context(), imageRefs, archs, remoteOpts,
sbomPath,
[]build.Option{
Expand All @@ -132,7 +121,6 @@ in a keychain.`,
build.WithCacheDir(cacheDir, offline),
build.WithLockFile(lockfile),
build.WithTempDir(tmp),
build.WithAuth(domain, user, pass),
build.WithIgnoreSignatures(ignoreSignatures),
},
[]PublishOption{
Expand Down
20 changes: 5 additions & 15 deletions pkg/apk/apk/implementation.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ import (
"golang.org/x/sys/unix"
"gopkg.in/ini.v1"

"chainguard.dev/apko/pkg/apk/auth"
"chainguard.dev/apko/pkg/apk/expandapk"
apkfs "chainguard.dev/apko/pkg/apk/fs"
"chainguard.dev/apko/pkg/apk/internal/tarfs"
Expand All @@ -65,7 +66,7 @@ type APK struct {
cache *cache
ignoreSignatures bool
noSignatureIndexes []string
auth map[string]auth
auth auth.Authenticator

// filename to owning package, last write wins
installedFiles map[string]*Package
Expand Down Expand Up @@ -411,16 +412,7 @@ func (a *APK) InitKeyring(ctx context.Context, keyFiles, extraKeyFiles []string)
if err != nil {
return err
}

// if the URL contains HTTP Basic Auth credentials, add them to the request
if asURL.User != nil {
user := asURL.User.Username()
pass, _ := asURL.User.Password()
req.SetBasicAuth(user, pass)
req.URL.User = nil
} else if a, ok := a.auth[asURL.Host]; ok && a.user != "" && a.pass != "" {
req.SetBasicAuth(a.user, a.pass)
}
a.auth.AddAuth(ctx, req)

resp, err := client.Do(req)
if err != nil {
Expand All @@ -429,7 +421,7 @@ func (a *APK) InitKeyring(ctx context.Context, keyFiles, extraKeyFiles []string)
defer resp.Body.Close()

if resp.StatusCode < 200 || resp.StatusCode > 299 {
return fmt.Errorf("failed to fetch apk key: http response indicated error code: %d", resp.StatusCode)
return fmt.Errorf("failed to fetch apk key from %s: http response indicated error code: %d", req.Host, resp.StatusCode)
}

data, err = io.ReadAll(resp.Body)
Expand Down Expand Up @@ -1073,9 +1065,7 @@ func (a *APK) FetchPackage(ctx context.Context, pkg InstallablePackage) (io.Read
if err != nil {
return nil, err
}
if a, ok := a.auth[asURL.Host]; ok && a.user != "" && a.pass != "" {
req.SetBasicAuth(a.user, a.pass)
}
a.auth.AddAuth(ctx, req)

// This will return a body that retries requests using Range requests if Read() hits an error.
rrt := newRangeRetryTransport(ctx, client)
Expand Down
9 changes: 5 additions & 4 deletions pkg/apk/apk/implementation_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import (

"github.com/stretchr/testify/require"

"chainguard.dev/apko/pkg/apk/auth"
apkfs "chainguard.dev/apko/pkg/apk/fs"
)

Expand Down Expand Up @@ -247,7 +248,7 @@ func TestInitKeyring(t *testing.T) {
err := src.MkdirAll("lib/apk/db", 0o755)
require.NoError(t, err, "unable to mkdir /lib/apk/db")

a, err := New(WithFS(src), WithAuth(host, testUser, testPass))
a, err := New(WithFS(src), WithAuthenticator(auth.StaticAuth(host, testUser, testPass)))
require.NoError(t, err, "unable to create APK")
err = a.InitDB(ctx)
require.NoError(t, err)
Expand All @@ -262,7 +263,7 @@ func TestInitKeyring(t *testing.T) {
err := src.MkdirAll("lib/apk/db", 0o755)
require.NoError(t, err, "unable to mkdir /lib/apk/db")

a, err := New(WithFS(src), WithAuth(host, "baduser", "badpass"))
a, err := New(WithFS(src), WithAuthenticator(auth.StaticAuth(host, "baduser", "badpass")))
require.NoError(t, err, "unable to create APK")
err = a.InitDB(ctx)
require.NoError(t, err)
Expand Down Expand Up @@ -556,7 +557,7 @@ func TestAuth_good(t *testing.T) {
err := src.MkdirAll("lib/apk/db", 0o755)
require.NoError(t, err, "unable to mkdir /lib/apk/db")

a, err := New(WithFS(src), WithAuth(host, testUser, testPass))
a, err := New(WithFS(src), WithAuthenticator(auth.StaticAuth(host, testUser, testPass)))
require.NoError(t, err, "unable to create APK")
err = a.InitDB(ctx)
require.NoError(t, err)
Expand Down Expand Up @@ -588,7 +589,7 @@ func TestAuth_bad(t *testing.T) {
err := src.MkdirAll("lib/apk/db", 0o755)
require.NoError(t, err, "unable to mkdir /lib/apk/db")

a, err := New(WithFS(src), WithAuth(host, "baduser", "badpass"))
a, err := New(WithFS(src), WithAuthenticator(auth.StaticAuth(host, "baduser", "badpass")))
require.NoError(t, err, "unable to create APK")
err = a.InitDB(ctx)
require.NoError(t, err)
Expand Down
19 changes: 5 additions & 14 deletions pkg/apk/apk/index.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import (
"go.lsp.dev/uri"
"go.opentelemetry.io/otel"

"chainguard.dev/apko/pkg/apk/auth"
sign "chainguard.dev/apko/pkg/apk/signature"
)

Expand Down Expand Up @@ -208,14 +209,7 @@ func getRepositoryIndex(ctx context.Context, u string, keys map[string][]byte, a
if err != nil {
return nil, err
}
// if the repo URL contains HTTP Basic Auth credentials, add them to the request
if asURL.User != nil {
user := asURL.User.Username()
pass, _ := asURL.User.Password()
req.SetBasicAuth(user, pass)
} else if a, ok := opts.auth[asURL.Host]; ok && a.user != "" || a.pass != "" {
req.SetBasicAuth(a.user, a.pass)
}
opts.auth.AddAuth(ctx, req)

// This will return a body that retries requests using Range requests if Read() hits an error.
rrt := newRangeRetryTransport(ctx, client)
Expand Down Expand Up @@ -320,7 +314,7 @@ type indexOpts struct {
ignoreSignatures bool
noSignatureIndexes []string
httpClient *http.Client
auth map[string]auth
auth auth.Authenticator
}
type IndexOption func(*indexOpts)

Expand All @@ -342,11 +336,8 @@ func WithHTTPClient(c *http.Client) IndexOption {
}
}

func WithIndexAuth(domain, user, pass string) IndexOption {
func WithIndexAuthenticator(a auth.Authenticator) IndexOption {
return func(o *indexOpts) {
if o.auth == nil {
o.auth = make(map[string]auth)
}
o.auth[domain] = auth{user, pass}
o.auth = a
}
}
13 changes: 5 additions & 8 deletions pkg/apk/apk/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import (
"path/filepath"
"runtime"

"chainguard.dev/apko/pkg/apk/auth"
apkfs "chainguard.dev/apko/pkg/apk/fs"
)

Expand All @@ -30,7 +31,7 @@ type opts struct {
version string
cache *cache
noSignatureIndexes []string
auth map[string]auth
auth auth.Authenticator
ignoreSignatures bool
}

Expand Down Expand Up @@ -116,14 +117,9 @@ func WithNoSignatureIndexes(noSignatureIndex ...string) Option {
}
}

type auth struct{ user, pass string }

func WithAuth(domain, user, pass string) Option {
func WithAuthenticator(a auth.Authenticator) Option {
return func(o *opts) error {
if o.auth == nil {
o.auth = make(map[string]auth)
}
o.auth[domain] = auth{user, pass}
o.auth = a
return nil
}
}
Expand All @@ -132,5 +128,6 @@ func defaultOpts() *opts {
return &opts{
arch: ArchToAPK(runtime.GOARCH),
ignoreMknodErrors: false,
auth: auth.DefaultAuthenciators,
}
}
5 changes: 2 additions & 3 deletions pkg/apk/apk/repo.go
Original file line number Diff line number Diff line change
Expand Up @@ -178,9 +178,8 @@ func (a *APK) GetRepositoryIndexes(ctx context.Context, ignoreSignatures bool) (
}
opts := []IndexOption{WithIgnoreSignatures(ignoreSignatures),
WithIgnoreSignatureForIndexes(a.noSignatureIndexes...),
WithHTTPClient(httpClient)}
for domain, auth := range a.auth {
opts = append(opts, WithIndexAuth(domain, auth.user, auth.pass))
WithHTTPClient(httpClient),
WithIndexAuthenticator(a.auth),
}
return GetRepositoryIndexes(ctx, repos, keys, arch, opts...)
}
Expand Down
9 changes: 5 additions & 4 deletions pkg/apk/apk/repo_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import (
"golang.org/x/exp/slices"
"golang.org/x/sync/errgroup"

"chainguard.dev/apko/pkg/apk/auth"
apkfs "chainguard.dev/apko/pkg/apk/fs"
)

Expand Down Expand Up @@ -321,8 +322,8 @@ func TestIndexAuth_good(t *testing.T) {
ctx := context.Background()

a, err := New(WithFS(apkfs.NewMemFS()),
WithAuth(host, testUser, testPass),
WithArch("x86_64"))
WithArch("x86_64"),
WithAuthenticator(auth.StaticAuth(host, testUser, testPass)))
require.NoErrorf(t, err, "unable to create APK")
err = a.InitDB(ctx)
require.NoError(t, err, "unable to init db")
Expand Down Expand Up @@ -350,8 +351,8 @@ func TestIndexAuth_bad(t *testing.T) {
ctx := context.Background()

a, err := New(WithFS(apkfs.NewMemFS()),
WithAuth(host, "baduser", "badpass"),
WithArch("x86_64"))
WithArch("x86_64"),
WithAuthenticator(auth.StaticAuth(host, "baduser", "badpass")))
require.NoErrorf(t, err, "unable to create APK")
err = a.InitDB(ctx)
require.NoError(t, err, "unable to init db")
Expand Down
97 changes: 97 additions & 0 deletions pkg/apk/auth/auth.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
package auth

import (
"context"
"net/http"
"os"
"os/exec"
"strings"
"time"

"github.com/chainguard-dev/clog"
"golang.org/x/time/rate"
)

// DefaultAuthenciators is a list of authenticators that are used by default.
var DefaultAuthenciators = multiAuthenticator{EnvAuth{}, CGRAuth{}}

// Authenticator is an interface for types that can add HTTP basic auth to a
// request.
type Authenticator interface {
AddAuth(ctx context.Context, req *http.Request)
}

// MultiAuthenticator returns an Authenticator that tries each of the given
// authenticators in order until one of them adds auth to the request.
func MultiAuthenticator(auths ...Authenticator) Authenticator { return multiAuthenticator(auths) }

type multiAuthenticator []Authenticator

func (m multiAuthenticator) AddAuth(ctx context.Context, req *http.Request) {
for _, a := range m {
if _, _, ok := req.BasicAuth(); ok {
// The request has auth, so we can stop here.
return
}
a.AddAuth(ctx, req)
}
}

// EnvAuth adds HTTP basic auth to the request if the request URL matches the
// HTTP_AUTH environment variable.
type EnvAuth struct{}

func (e EnvAuth) AddAuth(_ context.Context, req *http.Request) {
env := os.Getenv("HTTP_AUTH")
parts := strings.Split(env, ":")
if len(parts) != 4 || parts[0] != "basic" {
return
}
if req.URL.Host == parts[1] {
req.SetBasicAuth(parts[2], parts[3])
}
}

// CGRAuth adds HTTP basic auth to the request if the request URL matches
// apk.cgr.dev and the chainctl command is available.
type CGRAuth struct{}

var sometimes = rate.Sometimes{Interval: 10 * time.Minute}
var tok string

func (c CGRAuth) AddAuth(ctx context.Context, req *http.Request) {
log := clog.FromContext(ctx)

host := "apk.cgr.dev"
// TODO(jason): Use a more general way to get the host.
if h := os.Getenv("APKO_APK_HOST"); h != "" {
host = h
}
if req.Host != host {
return
}

sometimes.Do(func() {
out, err := exec.CommandContext(ctx, "chainctl", "auth", "token", "--audience", host).Output()
if err != nil {
log.Warnf("Error running `chainctl auth token`: %v", err)
return
}
tok = string(out)
})
req.SetBasicAuth("user", tok)
}

// StaticAuth is an Authenticator that adds HTTP basic auth to the request if
// the request URL matches the given domain.
func StaticAuth(domain, user, pass string) Authenticator {
return staticAuth{domain, user, pass}
}

type staticAuth struct{ domain, user, pass string }

func (s staticAuth) AddAuth(_ context.Context, req *http.Request) {
if req.Host == s.domain {
req.SetBasicAuth(s.user, s.pass)
}
}
Loading

0 comments on commit 057f3e2

Please sign in to comment.