Skip to content

Commit

Permalink
Merge pull request moby#5733 from marxarelli/feature/http-auth
Browse files Browse the repository at this point in the history
http: Support authentication
  • Loading branch information
tonistiigi authored Feb 20, 2025
2 parents 26b1f2b + ab1e99e commit 18db8b3
Show file tree
Hide file tree
Showing 6 changed files with 145 additions and 40 deletions.
42 changes: 42 additions & 0 deletions client/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@ var allTests = []func(t *testing.T, sb integration.Sandbox){
testBuildMultiMount,
testBuildHTTPSource,
testBuildHTTPSourceEtagScope,
testBuildHTTPSourceAuthHeaderSecret,
testBuildPushAndValidate,
testBuildExportWithUncompressed,
testBuildExportScratch,
Expand Down Expand Up @@ -3009,6 +3010,47 @@ func testBuildHTTPSourceEtagScope(t *testing.T, sb integration.Sandbox) {
require.NoError(t, os.RemoveAll(filepath.Join(out2, "foo")))
}

func testBuildHTTPSourceAuthHeaderSecret(t *testing.T, sb integration.Sandbox) {
c, err := New(sb.Context(), sb.Address())
require.NoError(t, err)
defer c.Close()

modTime := time.Now().Add(-24 * time.Hour) // avoid false positive with current time

resp := httpserver.Response{
Etag: identity.NewID(),
Content: []byte("content1"),
LastModified: &modTime,
}

server := httpserver.NewTestServer(map[string]httpserver.Response{
"/foo": resp,
})
defer server.Close()

st := llb.HTTP(server.URL+"/foo", llb.AuthHeaderSecret("http-secret"))

def, err := st.Marshal(sb.Context())
require.NoError(t, err)

_, err = c.Solve(
sb.Context(),
def,
SolveOpt{
Session: []session.Attachable{secretsprovider.FromMap(map[string][]byte{
"http-secret": []byte("Bearer foo"),
})},
},
nil,
)
require.NoError(t, err)

allReqs := server.Stats("/foo").Requests
require.Equal(t, 1, len(allReqs))
require.Equal(t, http.MethodGet, allReqs[0].Method)
require.Equal(t, "Bearer foo", allReqs[0].Header.Get("Authorization"))
}

func testResolveAndHosts(t *testing.T, sb integration.Sandbox) {
requiresLinux(t)
c, err := New(sb.Context(), sb.Address())
Expand Down
45 changes: 33 additions & 12 deletions client/llb/source.go
Original file line number Diff line number Diff line change
Expand Up @@ -360,13 +360,6 @@ func AuthTokenSecret(v string) GitOption {
})
}

func AuthHeaderSecret(v string) GitOption {
return gitOptionFunc(func(gi *GitInfo) {
gi.AuthHeaderSecret = v
gi.addAuthCap = true
})
}

func KnownSSHHosts(key string) GitOption {
key = strings.TrimSuffix(key, "\n")
return gitOptionFunc(func(gi *GitInfo) {
Expand All @@ -380,6 +373,29 @@ func MountSSHSock(sshID string) GitOption {
})
}

// AuthOption can be used with either HTTP or Git sources.
type AuthOption interface {
GitOption
HTTPOption
}

// AuthHeaderSecret returns an AuthOption that defines the name of a
// secret to use for HTTP based authentication.
func AuthHeaderSecret(secretName string) AuthOption {
return struct {
GitOption
HTTPOption
}{
GitOption: gitOptionFunc(func(gi *GitInfo) {
gi.AuthHeaderSecret = secretName
gi.addAuthCap = true
}),
HTTPOption: httpOptionFunc(func(hi *HTTPInfo) {
hi.AuthHeaderSecret = secretName
}),
}
}

// Scratch returns a state that represents an empty filesystem.
func Scratch() State {
return NewState(nil)
Expand Down Expand Up @@ -595,6 +611,10 @@ func HTTP(url string, opts ...HTTPOption) State {
attrs[pb.AttrHTTPGID] = strconv.Itoa(hi.GID)
addCap(&hi.Constraints, pb.CapSourceHTTPUIDGID)
}
if hi.AuthHeaderSecret != "" {
attrs[pb.AttrHTTPAuthHeaderSecret] = hi.AuthHeaderSecret
addCap(&hi.Constraints, pb.CapSourceHTTPAuth)
}

addCap(&hi.Constraints, pb.CapSourceHTTP)
source := NewSource(url, attrs, hi.Constraints)
Expand All @@ -603,11 +623,12 @@ func HTTP(url string, opts ...HTTPOption) State {

type HTTPInfo struct {
constraintsWrapper
Checksum digest.Digest
Filename string
Perm int
UID int
GID int
Checksum digest.Digest
Filename string
Perm int
UID int
GID int
AuthHeaderSecret string
}

type HTTPOption interface {
Expand Down
1 change: 1 addition & 0 deletions solver/pb/attr.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ const AttrHTTPFilename = "http.filename"
const AttrHTTPPerm = "http.perm"
const AttrHTTPUID = "http.uid"
const AttrHTTPGID = "http.gid"
const AttrHTTPAuthHeaderSecret = "http.authheadersecret"

const AttrImageResolveMode = "image.resolvemode"
const AttrImageResolveModeDefault = "default"
Expand Down
10 changes: 9 additions & 1 deletion solver/pb/caps.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,11 @@ const (
CapSourceGitSubdir apicaps.CapID = "source.git.subdir"

CapSourceHTTP apicaps.CapID = "source.http"
CapSourceHTTPAuth apicaps.CapID = "source.http.auth"
CapSourceHTTPChecksum apicaps.CapID = "source.http.checksum"
CapSourceHTTPPerm apicaps.CapID = "source.http.perm"
CapSourceHTTPUIDGID apicaps.CapID = "soruce.http.uidgid"
// NOTE the historical typo
CapSourceHTTPUIDGID apicaps.CapID = "soruce.http.uidgid"

CapSourceOCILayout apicaps.CapID = "source.ocilayout"

Expand Down Expand Up @@ -229,6 +231,12 @@ func init() {
Status: apicaps.CapStatusExperimental,
})

Caps.Init(apicaps.Cap{
ID: CapSourceHTTPAuth,
Enabled: true,
Status: apicaps.CapStatusExperimental,
})

Caps.Init(apicaps.Cap{
ID: CapSourceOCILayout,
Enabled: true,
Expand Down
15 changes: 8 additions & 7 deletions source/http/identifier.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,14 @@ func NewHTTPIdentifier(str string, tls bool) (*HTTPIdentifier, error) {
}

type HTTPIdentifier struct {
TLS bool
URL string
Checksum digest.Digest
Filename string
Perm int
UID int
GID int
TLS bool
URL string
Checksum digest.Digest
Filename string
Perm int
UID int
GID int
AuthHeaderSecret string
}

var _ source.Identifier = (*HTTPIdentifier)(nil)
Expand Down
72 changes: 52 additions & 20 deletions source/http/source.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import (
"github.com/docker/docker/pkg/idtools"
"github.com/moby/buildkit/cache"
"github.com/moby/buildkit/session"
"github.com/moby/buildkit/session/secrets"
"github.com/moby/buildkit/snapshot"
"github.com/moby/buildkit/solver"
"github.com/moby/buildkit/solver/pb"
Expand Down Expand Up @@ -92,6 +93,8 @@ func (hs *httpSource) Identifier(scheme, ref string, attrs map[string]string, pl
return nil, err
}
id.GID = int(i)
case pb.AttrHTTPAuthHeaderSecret:
id.AuthHeaderSecret = v
}
}

Expand Down Expand Up @@ -127,16 +130,18 @@ func (hs *httpSourceHandler) client(g session.Group) *http.Client {
// this package.
func (hs *httpSourceHandler) urlHash() (digest.Digest, error) {
dt, err := json.Marshal(struct {
Filename []byte
Perm, UID, GID int
Filename []byte
Perm, UID, GID int
AuthHeaderSecret string `json:",omitempty"`
}{
Filename: bytes.Join([][]byte{
[]byte(hs.src.URL),
[]byte(hs.src.Filename),
}, []byte{0}),
Perm: hs.src.Perm,
UID: hs.src.UID,
GID: hs.src.GID,
Perm: hs.src.Perm,
UID: hs.src.UID,
GID: hs.src.GID,
AuthHeaderSecret: hs.src.AuthHeaderSecret,
})
if err != nil {
return "", err
Expand All @@ -146,17 +151,19 @@ func (hs *httpSourceHandler) urlHash() (digest.Digest, error) {

func (hs *httpSourceHandler) formatCacheKey(filename string, dgst digest.Digest, lastModTime string) digest.Digest {
dt, err := json.Marshal(struct {
Filename string
Perm, UID, GID int
Checksum digest.Digest
LastModTime string `json:",omitempty"`
Filename string
Perm, UID, GID int
Checksum digest.Digest
LastModTime string `json:",omitempty"`
AuthHeaderSecret string `json:",omitempty"`
}{
Filename: filename,
Perm: hs.src.Perm,
UID: hs.src.UID,
GID: hs.src.GID,
Checksum: dgst,
LastModTime: lastModTime,
Filename: filename,
Perm: hs.src.Perm,
UID: hs.src.UID,
GID: hs.src.GID,
Checksum: dgst,
LastModTime: lastModTime,
AuthHeaderSecret: hs.src.AuthHeaderSecret,
})
if err != nil {
return dgst
Expand All @@ -181,12 +188,10 @@ func (hs *httpSourceHandler) CacheKey(ctx context.Context, g session.Group, inde
return "", "", nil, false, errors.Wrapf(err, "failed to search metadata for %s", uh)
}

req, err := http.NewRequest("GET", hs.src.URL, nil)
req, err := hs.newHTTPRequest(ctx, g)
if err != nil {
return "", "", nil, false, err
}
req = req.WithContext(ctx)
req.Header.Add("User-Agent", version.UserAgent())
m := map[string]cacheRefMetadata{}

// If we request a single ETag in 'If-None-Match', some servers omit the
Expand Down Expand Up @@ -443,11 +448,10 @@ func (hs *httpSourceHandler) Snapshot(ctx context.Context, g session.Group) (cac
}
}

req, err := http.NewRequest("GET", hs.src.URL, nil)
req, err := hs.newHTTPRequest(ctx, g)
if err != nil {
return nil, err
}
req = req.WithContext(ctx)

client := hs.client(g)

Expand All @@ -471,6 +475,34 @@ func (hs *httpSourceHandler) Snapshot(ctx context.Context, g session.Group) (cac
return ref, nil
}

func (hs *httpSourceHandler) newHTTPRequest(ctx context.Context, g session.Group) (*http.Request, error) {
req, err := http.NewRequest("GET", hs.src.URL, nil)
if err != nil {
return nil, err
}

req.Header.Set("User-Agent", version.UserAgent())

if hs.src.AuthHeaderSecret != "" {
err := hs.sm.Any(ctx, g, func(ctx context.Context, _ string, caller session.Caller) error {
dt, err := secrets.GetSecret(ctx, caller, hs.src.AuthHeaderSecret)
if err != nil {
return err
}

req.Header.Set("Authorization", string(dt))

return nil
})

if err != nil {
return nil, errors.Wrapf(err, "failed to retrieve HTTP auth secret %s", hs.src.AuthHeaderSecret)
}
}

return req.WithContext(ctx), nil
}

func getFileName(urlStr, manualFilename string, resp *http.Response) string {
if manualFilename != "" {
return manualFilename
Expand Down

0 comments on commit 18db8b3

Please sign in to comment.