Skip to content

Commit

Permalink
source: HTTP source authentication
Browse files Browse the repository at this point in the history
Support authentication for HTTP sources.

 - llb: Define general `llb.AuthOption` interface composed of
   `HTTPOption` and `GitOption`.
 - llb: Refactor `llb.AuthHeaderSecret` to return an `llb.AuthOption` so
   it may be used with both `llb.Git` and `llb.HTTP`.
 - llb: Define `HTTPInfo.AuthHeaderSecret`.
 - llb: Define and flag new `source.http.auth` capability when
   `HTTPInfo.AuthHeaderSecret` is set.
 - solver: Define new `http.auth` source attribute.
 - source/http: If an `http.auth` attribute is specified, resolve a
   secret named by its value and set the "Authorization" request header.

Signed-off-by: Dan Duvall <[email protected]>
  • Loading branch information
marxarelli committed Feb 13, 2025
1 parent 2f7007e commit 62a9c5e
Show file tree
Hide file tree
Showing 6 changed files with 112 additions and 19 deletions.
42 changes: 42 additions & 0 deletions client/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@ var allTests = []func(t *testing.T, sb integration.Sandbox){
testBuildMultiMount,
testBuildHTTPSource,
testBuildHTTPSourceEtagScope,
testBuildHTTPSourceAuthHeaderSecret,
testBuildPushAndValidate,
testBuildExportWithUncompressed,
testBuildExportScratch,
Expand Down Expand Up @@ -2982,6 +2983,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
7 changes: 7 additions & 0 deletions solver/pb/caps.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ const (
CapSourceHTTPChecksum apicaps.CapID = "source.http.checksum"
CapSourceHTTPPerm apicaps.CapID = "source.http.perm"
CapSourceHTTPUIDGID apicaps.CapID = "soruce.http.uidgid"
CapSourceHTTPAuth apicaps.CapID = "soruce.http.auth"

CapSourceOCILayout apicaps.CapID = "source.ocilayout"

Expand Down Expand Up @@ -229,6 +230,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
21 changes: 21 additions & 0 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 @@ -189,6 +192,24 @@ func (hs *httpSourceHandler) CacheKey(ctx context.Context, g session.Group, inde
req.Header.Add("User-Agent", version.UserAgent())
m := map[string]cacheRefMetadata{}

// Add an Authorization header if an HTTP auth header secret was defined
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.Add("Authorization", string(dt))

return nil
})

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

// If we request a single ETag in 'If-None-Match', some servers omit the
// unambiguous ETag in their response.
// See: https://github.com/moby/buildkit/issues/905
Expand Down

0 comments on commit 62a9c5e

Please sign in to comment.