Skip to content

Commit

Permalink
Bug 1763005 - add registry-proxy support (#184)
Browse files Browse the repository at this point in the history
* Bug 1763005 - add registry-proxy support

The registry-proxy GET /v2 returns a WWW-Authenticate header with only a
realm: .../v2/auth the APIV2Adapter simply gets a token for /v2/auth
which is not valid for registry-proxy which seems to require a scoped
token.

In this PR, we added a new RegistryProxyAdapter that now requires the
apbs to be listed in the configuration. For each of those images listed,
we will add them to the scope when we ask for the token. This allows the
broker access to fetch the apb images.

Also, added a test to ensure registry proxy errors when no images are defined
and added a unit test for getTokenWithScope.

* fix linter errors
  • Loading branch information
jmrodri authored Oct 21, 2019
1 parent 488d938 commit 793517a
Show file tree
Hide file tree
Showing 5 changed files with 188 additions and 16 deletions.
34 changes: 34 additions & 0 deletions registries/adapters/apiv2_adapter.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,11 @@ type PartnerRhccAdapter struct {
APIV2Adapter
}

// RegistryProxyAdapter - Registry Proxy Adapter
type RegistryProxyAdapter struct {
APIV2Adapter
}

// APIV2Adapter - API V2 Adapter
type APIV2Adapter struct {
config Configuration
Expand Down Expand Up @@ -75,6 +80,35 @@ func NewPartnerRhccAdapter(config Configuration) (PartnerRhccAdapter, error) {
return PartnerRhccAdapter{apiV2}, nil
}

// NewRegistryProxyAdapter - create a new Registry Proxy Adapter
func NewRegistryProxyAdapter(config Configuration) (RegistryProxyAdapter, error) {
// we want to use a different token for this adapter, so do not call
// NewAPIV2Adapter directly
apiv2a := APIV2Adapter{
config: config,
client: oauth.NewClient(config.User, config.Pass, config.SkipVerifyTLS, config.URL),
}

if len(config.Images) < 1 {
return RegistryProxyAdapter{},
fmt.Errorf("RegistryProxyAdapter requires at least one configured image")
}

// Authorization
err := apiv2a.client.Getv2WithScope(config.Images)
if err != nil {
log.Errorf("Failed to GET /v2 at %s - %v", config.URL, err)
return RegistryProxyAdapter{}, err
}

// set Tag to latest if empty
if apiv2a.config.Tag == "" {
apiv2a.config.Tag = "latest"
}

return RegistryProxyAdapter{apiv2a}, nil
}

// NewAPIV2Adapter - creates and returns a APIV2Adapter ready to use.
func NewAPIV2Adapter(config Configuration) (APIV2Adapter, error) {
apiv2a := APIV2Adapter{
Expand Down
51 changes: 48 additions & 3 deletions registries/adapters/apiv2_adapter_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,52 @@ func TestAPIV2NewAPIV2Adapter(t *testing.T) {
t.Fatal("Error: ", err)
}

ft.NotEqual(t, apiv2a, APIV2Adapter{}, "adaptor returned is not valid")
ft.NotEqual(t, apiv2a, APIV2Adapter{}, "adapter returned is not valid")
}

func TestNewRegistryProxyAdapter(t *testing.T) {
serv := adaptertest.GetAPIV2Server(t, true)
defer serv.Close()

testCases := []struct {
name string
expectederr bool
config Configuration
}{
{
name: "Registry Proxy Adapter created with no errors",
expectederr: false,
config: Configuration{
URL: adaptertest.GetURL(t, serv),
Images: []string{"foo/bar", "foo/baz"},
},
},
{
name: "Registry Proxy Adapter failed to create",
expectederr: true,
config: Configuration{
URL: adaptertest.GetURL(t, serv),
Images: []string{},
},
},
}

for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
rpAdapter, err := NewRegistryProxyAdapter(tc.config)
if tc.expectederr {
ft.Error(t, err)
ft.NotEmpty(t, err.Error())
} else if err != nil {
fmt.Println(err)
t.Fatalf("unexpected error during test: %v\n", err)
} else {
// no errors, make sure we returned the right type
ft.NotEqual(t, rpAdapter, RegistryProxyAdapter{},
"Registry Proxy adapter returned is not valid")
}
})
}
}

func TestNewOpenShiftAdapter(t *testing.T) {
Expand All @@ -121,7 +166,7 @@ func TestNewOpenShiftAdapter(t *testing.T) {
if err != nil {
t.Fatal("Error: ", err)
}
ft.NotEqual(t, osAdapter, OpenShiftAdapter{}, "OpenShift adaptor returned is not valid")
ft.NotEqual(t, osAdapter, OpenShiftAdapter{}, "OpenShift adapter returned is not valid")
}

func TestNewPartnerRhccAdapter(t *testing.T) {
Expand All @@ -133,7 +178,7 @@ func TestNewPartnerRhccAdapter(t *testing.T) {
if err != nil {
t.Fatal("Error: ", err)
}
ft.NotEqual(t, prAdapter, PartnerRhccAdapter{}, "Partner RHCC adaptor returned is not valid")
ft.NotEqual(t, prAdapter, PartnerRhccAdapter{}, "Partner RHCC adapter returned is not valid")
}

func TestAPIV2GetImageNames(t *testing.T) {
Expand Down
52 changes: 39 additions & 13 deletions registries/adapters/oauth/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
package oauth

import (
"bytes"
"crypto/tls"
"encoding/json"
"errors"
Expand Down Expand Up @@ -103,12 +104,13 @@ func (c *Client) Do(req *http.Request) (*http.Response, error) {
return c.client.Do(req)
}

// Getv2 - makes a GET request to the registry's /v2/ endpoint. If a 401
// Unauthorized response is received, this method attempts to obtain an oauth
// token and tries again with the new token. If a username and password are
// available, they are used with Basic Auth in the request to the token
// service. This method is goroutine-safe.
func (c *Client) Getv2() error {
// Getv2WithScope - makes a GET request to the registry's /v2/ endpoint. If a
// 401 Unauthorized response is received, this method attempts to obtain an
// oauth token with the given imageNames as scopes and tries again with the
// new token. If a username and password are available, they are used with
// Basic Auth in the request to the token service. This method is
// goroutine-safe.
func (c *Client) Getv2WithScope(imageNames []string) error {
// lock to prevent multiple goroutines from retrieving, and especially
// writing, a new token at the same time
c.mutex.Lock()
Expand All @@ -126,7 +128,10 @@ func (c *Client) Getv2() error {
switch resp.StatusCode {
case http.StatusUnauthorized:
h := resp.Header.Get("www-authenticate")
c.getToken(h)
err := c.getTokenWithScope(h, imageNames)
if err != nil {
return err
}

// try the new token
tokenReq, err := c.NewRequest("/v2/")
Expand Down Expand Up @@ -161,18 +166,39 @@ func (c *Client) Getv2() error {
return nil
}

// getToken - parses a www-authenticate header and uses the information to
// retrieve an oauth token. The token is stored on the Client and automatically
// added to future requests.
func (c *Client) getToken(wwwauth string) error {
// Getv2 - makes a GET request to the registry's /v2/ endpoint. If a 401
// Unauthorized response is received, this method attempts to obtain an oauth
// token and tries again with the new token. If a username and password are
// available, they are used with Basic Auth in the request to the token
// service. This method is goroutine-safe.
func (c *Client) Getv2() error {
return c.Getv2WithScope([]string{})
}

// getTokenWithScope - parses a www-authenticate header and uses the
// information to retrieve an oauth token. The image names are also used to
// specify the scope of the token. The token is stored on the Client
// and automatically added to future requests.
func (c *Client) getTokenWithScope(wwwauth string, imageNames []string) error {
// compute the URL
u, err := parseAuthHeader(wwwauth)
if err != nil {
return err
}

// form the request
req, err := http.NewRequest("GET", u.String(), nil)
// build up the scopes, if imageNames is empty, the scope will be empty
var scope bytes.Buffer
for i, imageName := range imageNames {
if i > 0 {
scope.WriteString(fmt.Sprintf("&scope=repository:%s:pull", imageName))
} else {
scope.WriteString(fmt.Sprintf("?scope=repository:%s:pull", imageName))
}
}

log.Debug("token with scopes: "+fmt.Sprintf("%s%s", u.String(), scope.String()), nil)
// form the request, include scope if they exist
req, err := http.NewRequest("GET", fmt.Sprintf("%s%s", u.String(), scope.String()), nil)
if err != nil {
log.Errorf("could not form request: %s", err.Error())
return err
Expand Down
65 changes: 65 additions & 0 deletions registries/adapters/oauth/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,22 @@ package oauth

import (
"fmt"
"net/http"
"net/http/httptest"
"net/url"
"strings"
"testing"

"github.com/stretchr/testify/assert"
)

var apiV2AuthResponse = `
{
"token": "%s",
"expires_in": 300,
"issued_at": "2018-03-27T19:54:19Z"
}`

var headerCases = map[string]string{
"Bearer realm=\"http://foo/a/b/c\",service=\"bar\"": "http://foo/a/b/c?service=bar",
"Bearer service=\"bar\",realm=\"http://foo/a/b/c\"": "http://foo/a/b/c?service=bar",
Expand Down Expand Up @@ -98,6 +107,62 @@ func TestParseAuthTokenErrors(t *testing.T) {
}
}

func TestGetTokenWithScope(t *testing.T) {
authServ := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != "GET" {
t.Errorf("Expected 'GET' request, got '%s'", r.Method)
}

// see if we have any scopes
u, _ := url.Parse(r.RequestURI)
count := strings.Count(u.RawQuery, "scope")
token := fmt.Sprintf("fake.tokenTbTRUN3VZWHEwRW9oMEM2cEd-%d-scopes", count)
fmt.Fprintf(w, fmt.Sprintf(apiV2AuthResponse, token))
}))
defer authServ.Close()

hdr := fmt.Sprintf("Bearer realm=\"%s/v2/auth\"", authServ.URL)
u, err := url.Parse("http://automationbroker.io")
if err != nil {
t.Fatal("invalid url", err)
}
c := NewClient("", "", false, u)

testCases := []struct {
name string
imageNames []string
expectederr bool
expectedtoken string
}{
{
name: "no images",
imageNames: []string{},
expectederr: false,
expectedtoken: "fake.tokenTbTRUN3VZWHEwRW9oMEM2cEd-0-scopes",
},
{
name: "2 images",
imageNames: []string{"rh-osbs/postgresql", "rh-osbs/mysql"},
expectederr: false,
expectedtoken: "fake.tokenTbTRUN3VZWHEwRW9oMEM2cEd-2-scopes",
},
}

for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
err := c.getTokenWithScope(hdr, tc.imageNames)
if tc.expectederr {
assert.Error(t, err)
assert.NotEmpty(t, err.Error())
} else if err != nil {
fmt.Println(err.Error())
t.Fatalf("unexpected error during test: %v\n", err)
}
assert.Equal(t, c.token, tc.expectedtoken)
})
}
}

func TestNewRequest(t *testing.T) {
u, err := url.Parse("http://automationbroker.io")
if err != nil {
Expand Down
2 changes: 2 additions & 0 deletions registries/registry.go
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,8 @@ func NewCustomRegistry(configuration Config, adapter adapters.Adapter, asbNamesp
adapter = adapters.NewQuayAdapter(c)
case "galaxy":
adapter = &adapters.GalaxyAdapter{Config: c}
case "registry_proxy":
adapter, err = adapters.NewRegistryProxyAdapter(c)
default:
log.Errorf("Unknown registry type - %s", configuration.Type)
return Registry{}, errors.New("Unknown registry type")
Expand Down

0 comments on commit 793517a

Please sign in to comment.