Skip to content

Commit

Permalink
GitLab registry header auth
Browse files Browse the repository at this point in the history
* add auth to oci_downloader for gitlab registries
* authentication has same workflow as public auth but with authenticated token fetch
  • Loading branch information
gitu committed Jun 23, 2023
1 parent 39c3ab4 commit 25096d6
Show file tree
Hide file tree
Showing 5 changed files with 174 additions and 9 deletions.
21 changes: 16 additions & 5 deletions download/oci_download.go
Original file line number Diff line number Diff line change
Expand Up @@ -330,12 +330,23 @@ func dockerResolver(plugin rest.HTTPAuthPlugin, config *rest.Config, logger logg
return nil, fmt.Errorf("failed to parse url: %w", err)
}

dockerAuthorizerOpts := []docker.AuthorizerOpt{
docker.WithAuthClient(client),
}

// for token flow containerd ignores the client's headers thus we add it manually here if it's set in the config
if authHeaderPlugin, isHeaderAuthPlugin := plugin.(rest.HTTPHeaderAuthPlugin); isHeaderAuthPlugin {
header, err := authHeaderPlugin.AuthHeader()
if err != nil {
return nil, fmt.Errorf("failed to create auth header: %w", err)
}
dockerAuthorizerOpts = append(dockerAuthorizerOpts, docker.WithAuthHeader(header))
}

authorizer := pluginAuthorizer{
plugin: plugin,
authorizer: docker.NewDockerAuthorizer(
docker.WithAuthClient(client),
),
logger: logger,
plugin: plugin,
authorizer: docker.NewDockerAuthorizer(dockerAuthorizerOpts...),
logger: logger,
}

registryHost := docker.RegistryHost{
Expand Down
37 changes: 37 additions & 0 deletions download/oci_download_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,43 @@ func TestOCIPublicRegistryAuth(t *testing.T) {
}
}

// TestOCITokenAuth
func TestOCITokenAuth(t *testing.T) {
ctx := context.Background()
fixture := newTestFixture(t, withGitlabRegistryAuth())
plainToken := "secret"
token := base64.StdEncoding.EncodeToString([]byte(plainToken)) // token should be base64 encoded
fixture.server.expAuth = fmt.Sprintf("Bearer %s", token) // test on private repository
fixture.server.expEtag = "sha256:c5834dbce332cabe6ae68a364de171a50bf5b08024c27d7c08cc72878b4df7ff"

restConf := fmt.Sprintf(`{
"url": %q,
"type": "oci",
"credentials": {
"bearer": {
"token": %q
}
}
}`, fixture.server.server.URL, plainToken)

client, err := rest.New([]byte(restConf), map[string]*keys.Config{})
if err != nil {
t.Fatalf("failed to create rest client: %s", err)
}
fixture.setClient(client)

config := Config{}
if err := config.ValidateAndInjectDefaults(); err != nil {
t.Fatal(err)
}

d := NewOCI(Config{}, fixture.client, "ghcr.io/org/repo:latest", t.TempDir())

if err := d.oneShot(ctx); err != nil {
t.Fatalf("Unexpected error: %s", err)
}
}

func TestOCICustomAuthPlugin(t *testing.T) {
fixture := newTestFixture(t)
defer fixture.server.stop()
Expand Down
94 changes: 94 additions & 0 deletions download/testharness.go
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,100 @@ func tokenHandler(token string) http.HandlerFunc {
}
}

// withGitlabRegistryAuth sets up a token auth flow according to
// the spec https://docs.docker.com/registry/spec/auth/token/.
//
// The flow is the same as for public registries but additionally
// the request for fetching the token also has to be authenticated.
//
// The token issuing and validation differs between providers
// and we only use a minimal version for testing.
func withGitlabRegistryAuth() fixtureOpt {
const token = "some-test-token"
tokenServer := httptest.NewServer(tokenHandlerAuth("c2VjcmV0", token))

const wwwAuthenticateFmt = "Bearer realm=%q service=%q scope=%q"
tokenServiceURL := tokenServer.URL + "/token"
wwwAuthenticate := fmt.Sprintf(wwwAuthenticateFmt,
tokenServiceURL,
"testRegistry.io",
"[pull]")

return func(tf *testFixture) error {
tf.server.customAuth = func(w http.ResponseWriter, r *http.Request) error {

authHeader := r.Header.Get("Authorization")

println(r.URL.String() + " -- " + r.Header.Get("Authorization"))
if authHeader == "" {
w.Header().Set("WWW-Authenticate", wwwAuthenticate)
return fmt.Errorf("no authorization header: %w", errUnauthorized)
}

if !strings.HasPrefix(authHeader, "Bearer ") {
w.Header().Set("WWW-Authenticate", wwwAuthenticate)
return fmt.Errorf("expects bearer scheme: %w", errUnauthorized)
}

bearerToken := strings.TrimPrefix(authHeader, "Bearer ")
if bearerToken != token {
w.Header().Set("WWW-Authenticate", wwwAuthenticate)
return fmt.Errorf("token %q doesn't match %q: %w", bearerToken, token, errUnauthorized)
}

return nil
}

return nil
}
}

// tokenHandler returns an http.Handler that responds with the
// specified token to GET /token requests.
func tokenHandlerAuth(expectedToken, issuedToken string) http.HandlerFunc {
tokenResponse := struct {
Token string `json:"token"`
}{
Token: issuedToken,
}

responseBody, err := json.Marshal(tokenResponse)
if err != nil {
panic("failed to marshal token response: " + err.Error())
}

return func(w http.ResponseWriter, r *http.Request) {
if r.Method != "GET" {
w.WriteHeader(http.StatusMethodNotAllowed)
return
}

if r.URL.Path != "/token" {
w.WriteHeader(http.StatusBadRequest)
return
}

authHeader := r.Header.Get("Authorization")
if authHeader == "" {
w.WriteHeader(http.StatusUnauthorized)
return
}

if !strings.HasPrefix(authHeader, "Bearer ") {
w.WriteHeader(http.StatusUnauthorized)
return
}

bearerToken := strings.TrimPrefix(authHeader, "Bearer ")
if bearerToken != expectedToken {
w.WriteHeader(http.StatusUnauthorized)
return
}

w.Write(responseBody)
}
}

func (t *testFixture) setClient(client rest.Client) {
t.client = client
}
Expand Down
27 changes: 23 additions & 4 deletions plugins/rest/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -145,22 +145,41 @@ func (ap *bearerAuthPlugin) NewClient(c Config) (*http.Client, error) {
}

func (ap *bearerAuthPlugin) Prepare(req *http.Request) error {
token, err := ap.buildToken()
if err != nil {
return err
}

req.Header.Add("Authorization", fmt.Sprintf("%v %v", ap.Scheme, token))
return nil
}

func (ap *bearerAuthPlugin) AuthHeader() (http.Header, error) {
token, err := ap.buildToken()
if err != nil {
return nil, err
}

header := make(http.Header)
header.Add("Authorization", fmt.Sprintf("%v %v", ap.Scheme, token))
return header, err
}

func (ap *bearerAuthPlugin) buildToken() (string, error) {
token := ap.Token

if ap.TokenPath != "" {
bytes, err := os.ReadFile(ap.TokenPath)
if err != nil {
return err
return "", err
}
token = strings.TrimSpace(string(bytes))
}

if ap.encode {
token = base64.StdEncoding.EncodeToString([]byte(token))
}

req.Header.Add("Authorization", fmt.Sprintf("%v %v", ap.Scheme, token))
return nil
return token, nil
}

type tokenEndpointResponse struct {
Expand Down
4 changes: 4 additions & 0 deletions plugins/rest/rest.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,10 @@ type HTTPAuthPlugin interface {
Prepare(*http.Request) error
}

type HTTPHeaderAuthPlugin interface {
AuthHeader() (http.Header, error)
}

// Config represents configuration for a REST client.
type Config struct {
Name string `json:"name"`
Expand Down

0 comments on commit 25096d6

Please sign in to comment.