mirror of
https://github.com/goharbor/harbor
synced 2024-09-20 18:39:52 +00:00
Fix azurecr use ACR token failed to list tags, the root cause is the scope action of acr token is 'metadata_read' not 'pull' when list v2 tags API. Signed-off-by: chlins <chenyuzh@vmware.com>
This commit is contained in:
parent
e08ad05659
commit
6cfbf76781
|
@ -1,3 +1,17 @@
|
|||
// Copyright Project Harbor Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package azurecr
|
||||
|
||||
import (
|
||||
|
@ -17,7 +31,7 @@ func init() {
|
|||
|
||||
func newAdapter(registry *model.Registry) (adp.Adapter, error) {
|
||||
return &adapter{
|
||||
Adapter: native.NewAdapter(registry),
|
||||
Adapter: native.NewAdapterWithAuthorizer(registry, newAuthorizer(registry)),
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
|
|
@ -1,3 +1,17 @@
|
|||
// Copyright Project Harbor Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package azurecr
|
||||
|
||||
import (
|
||||
|
|
164
src/pkg/reg/adapter/azurecr/auth.go
Normal file
164
src/pkg/reg/adapter/azurecr/auth.go
Normal file
|
@ -0,0 +1,164 @@
|
|||
// Copyright Project Harbor Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package azurecr
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
||||
"github.com/docker/distribution/registry/client/auth/challenge"
|
||||
commonhttp "github.com/goharbor/harbor/src/common/http"
|
||||
"github.com/goharbor/harbor/src/lib"
|
||||
"github.com/goharbor/harbor/src/lib/errors"
|
||||
"github.com/goharbor/harbor/src/pkg/reg/model"
|
||||
"github.com/goharbor/harbor/src/pkg/registry/auth"
|
||||
)
|
||||
|
||||
var (
|
||||
// scopeActionMetadataRead represents metadata_read action
|
||||
scopeActionMetadataRead = "metadata_read"
|
||||
// scopeTypeRepository represents repository resource
|
||||
scopeTypeRepository = "repository"
|
||||
)
|
||||
|
||||
var _ lib.Authorizer = &authorizer{}
|
||||
|
||||
type token struct {
|
||||
AccessToken string `json:"access_token"`
|
||||
}
|
||||
|
||||
// authorizer is a customize authorizer for azurecr adapter which
|
||||
// inherits lib authorizer.
|
||||
type authorizer struct {
|
||||
registry *model.Registry
|
||||
innerAuthorizer lib.Authorizer
|
||||
client *http.Client
|
||||
}
|
||||
|
||||
func newAuthorizer(registry *model.Registry) *authorizer {
|
||||
var username, password string
|
||||
if registry.Credential != nil {
|
||||
username = registry.Credential.AccessKey
|
||||
password = registry.Credential.AccessSecret
|
||||
}
|
||||
|
||||
return &authorizer{
|
||||
registry: registry,
|
||||
innerAuthorizer: auth.NewAuthorizer(username, password, registry.Insecure),
|
||||
client: &http.Client{Transport: commonhttp.GetHTTPTransport(commonhttp.WithInsecure(registry.Insecure))},
|
||||
}
|
||||
}
|
||||
|
||||
func (a *authorizer) Modify(req *http.Request) error {
|
||||
if !isTagList(req.URL) {
|
||||
// pass through non tag list api
|
||||
return a.innerAuthorizer.Modify(req)
|
||||
}
|
||||
|
||||
// tag list api should fetch token
|
||||
url, err := a.buildTokenAPI(req.URL)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
tokenReq, err := http.NewRequest(http.MethodGet, url.String(), nil)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if a.registry.Credential != nil {
|
||||
tokenReq.SetBasicAuth(a.registry.Credential.AccessKey, a.registry.Credential.AccessSecret)
|
||||
}
|
||||
|
||||
resp, err := a.client.Do(tokenReq)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
defer resp.Body.Close()
|
||||
body, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var tk token
|
||||
if err = json.Unmarshal(body, &tk); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if tk.AccessToken != "" {
|
||||
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", tk.AccessToken))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// buildTokenAPI builds token request API path.
|
||||
func (a *authorizer) buildTokenAPI(u *url.URL) (*url.URL, error) {
|
||||
v2URL, err := url.Parse(u.Scheme + "://" + u.Host + "/v2/")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
resp, err := a.client.Get(v2URL.String())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
challenges := challenge.ResponseChallenges(resp)
|
||||
if len(challenges) == 0 {
|
||||
return nil, errors.New("invalid response challenges")
|
||||
}
|
||||
cm := map[string]challenge.Challenge{}
|
||||
for _, challenge := range challenges {
|
||||
cm[challenge.Scheme] = challenge
|
||||
}
|
||||
|
||||
challenge, exist := cm["bearer"]
|
||||
if !exist {
|
||||
return nil, errors.New("no bearer challenge found")
|
||||
}
|
||||
|
||||
tokenURL, err := url.Parse(challenge.Parameters["realm"])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
query := tokenURL.Query()
|
||||
query.Add("service", challenge.Parameters["service"])
|
||||
|
||||
var repository string
|
||||
if subs := lib.V2TagListURLRe.FindStringSubmatch(u.Path); len(subs) >= 2 {
|
||||
// tag
|
||||
repository = subs[1]
|
||||
}
|
||||
|
||||
if repository == "" {
|
||||
return nil, errors.Errorf("invalid repository name, url: %s", u.String())
|
||||
}
|
||||
|
||||
query.Add("scope", fmt.Sprintf("%s:%s:%s", scopeTypeRepository, repository, scopeActionMetadataRead))
|
||||
tokenURL.RawQuery = query.Encode()
|
||||
return tokenURL, nil
|
||||
}
|
||||
|
||||
// isTagList checks the request whether tag list API.
|
||||
func isTagList(u *url.URL) bool {
|
||||
return lib.V2TagListURLRe.Match([]byte(u.Path))
|
||||
}
|
64
src/pkg/reg/adapter/azurecr/auth_test.go
Normal file
64
src/pkg/reg/adapter/azurecr/auth_test.go
Normal file
|
@ -0,0 +1,64 @@
|
|||
// Copyright Project Harbor Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package azurecr
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"github.com/goharbor/harbor/src/pkg/reg/model"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"gopkg.in/h2non/gock.v1"
|
||||
)
|
||||
|
||||
var (
|
||||
mockURL = "https://test.azurecr.io"
|
||||
mockUsername = "user"
|
||||
mockPassword = "password"
|
||||
mockToken = "test-token"
|
||||
)
|
||||
|
||||
func TestAuth(t *testing.T) {
|
||||
// mock server
|
||||
defer gock.Off()
|
||||
// mock v2 API
|
||||
gock.New(mockURL).
|
||||
Get("/v2/").
|
||||
Reply(401).
|
||||
SetHeader("Www-Authenticate", `Bearer realm="https://test.azurecr.io/oauth2/token",service="test.azurecr.io"`)
|
||||
// mock token API
|
||||
gock.New(mockURL).
|
||||
Get("/oauth2/token").
|
||||
MatchParam("service", "test.azurecr.io").
|
||||
MatchParam("scope", `repository:library/busybox:metadata_read`).
|
||||
BasicAuth(mockUsername, mockPassword).
|
||||
Reply(200).
|
||||
JSON(fmt.Sprintf(`{"access_token": "%s"}`, mockToken))
|
||||
|
||||
a := newAuthorizer(&model.Registry{URL: mockURL, Credential: &model.Credential{AccessKey: mockUsername, AccessSecret: mockPassword}})
|
||||
ct := &http.Client{}
|
||||
a.client = ct
|
||||
gock.InterceptClient(ct)
|
||||
|
||||
// test authorize
|
||||
req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("%s%s", mockURL, "/v2/library/busybox/tags/list"), nil)
|
||||
assert.NoError(t, err)
|
||||
err = a.Modify(req)
|
||||
assert.NoError(t, err)
|
||||
// check whether set bearer token
|
||||
tokenHeader := req.Header.Get("Authorization")
|
||||
assert.Equal(t, fmt.Sprintf("Bearer %s", mockToken), tokenHeader)
|
||||
}
|
Loading…
Reference in New Issue
Block a user