do not ping if using raw token authorizer

This commit is contained in:
Wenkai Yin 2017-07-24 17:41:06 +08:00
parent 274f764622
commit cc264f85e7
21 changed files with 567 additions and 543 deletions

View File

@ -29,6 +29,7 @@ import (
"github.com/vmware/harbor/src/common/utils/registry"
"github.com/vmware/harbor/src/common/utils/registry/auth"
"github.com/vmware/harbor/src/ui/config"
"github.com/vmware/harbor/src/ui/service/token"
"github.com/opencontainers/go-digest"
)
@ -74,12 +75,8 @@ func GetInternalTargets(notaryEndpoint string, username string, repo string) ([]
// like "10.117.4.117/library/ubuntu", instead of "library/ubuntu" (fqRepo for fully-qualified repo)
func GetTargets(notaryEndpoint string, username string, fqRepo string) ([]Target, error) {
res := []Target{}
authorizer := auth.NewNotaryUsernameTokenAuthorizer(username, "repository", fqRepo, "pull")
store, err := auth.NewAuthorizerStore(strings.Split(notaryEndpoint, "//")[1], true, authorizer)
if err != nil {
return res, err
}
tr := registry.NewTransport(registry.GetHTTPTransport(true), store)
authorizer := auth.NewRawTokenAuthorizer(username, token.Notary)
tr := registry.NewTransport(registry.GetHTTPTransport(true), authorizer)
gun := data.GUN(fqRepo)
notaryRepo, err := client.NewFileCachedNotaryRepository(notaryCachePath, gun, notaryEndpoint, tr, mockRetriever, trustPin)
if err != nil {

View File

@ -16,6 +16,7 @@ package notary
import (
"encoding/json"
"fmt"
"github.com/stretchr/testify/assert"
"github.com/vmware/harbor/src/common"
notarytest "github.com/vmware/harbor/src/common/utils/notary/test"
@ -39,6 +40,7 @@ func TestMain(m *testing.M) {
common.ExtEndpoint: "https://" + endpoint,
common.WithNotary: true,
common.CfgExpiration: 5,
common.TokenExpiration: 30,
}
adminServer, err := utilstest.NewAdminserver(defaultConfig)
if err != nil {

View File

@ -1,107 +0,0 @@
// Copyright (c) 2017 VMware, Inc. All Rights Reserved.
//
// 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 auth
import (
"fmt"
"net/http"
"net/url"
"strings"
"time"
"github.com/docker/distribution/registry/client/auth/challenge"
"github.com/vmware/harbor/src/common/utils"
"github.com/vmware/harbor/src/common/utils/registry"
)
// Authorizer authorizes requests according to the schema
type Authorizer interface {
// Scheme : basic, bearer
Scheme() string
//Authorize adds basic auth or token auth to the header of request
Authorize(req *http.Request, params map[string]string) error
}
// AuthorizerStore holds a authorizer list, which will authorize request.
// And it implements interface Modifier
type AuthorizerStore struct {
authorizers []Authorizer
ping *url.URL
challenges []challenge.Challenge
}
// NewAuthorizerStore ...
func NewAuthorizerStore(endpoint string, insecure bool, authorizers ...Authorizer) (*AuthorizerStore, error) {
endpoint = utils.FormatEndpoint(endpoint)
client := &http.Client{
Transport: registry.GetHTTPTransport(insecure),
Timeout: 30 * time.Second,
}
pingURL := buildPingURL(endpoint)
resp, err := client.Get(pingURL)
if err != nil {
return nil, err
}
defer resp.Body.Close()
challenges := ParseChallengeFromResponse(resp)
ping, err := url.Parse(pingURL)
if err != nil {
return nil, err
}
return &AuthorizerStore{
authorizers: authorizers,
ping: ping,
challenges: challenges,
}, nil
}
func buildPingURL(endpoint string) string {
return fmt.Sprintf("%s/v2/", endpoint)
}
// Modify adds authorization to the request
func (a *AuthorizerStore) Modify(req *http.Request) error {
//only handle the requests sent to registry
v2Index := strings.Index(req.URL.Path, "/v2/")
if v2Index == -1 {
return nil
}
ping := url.URL{
Host: req.URL.Host,
Scheme: req.URL.Scheme,
Path: req.URL.Path[:v2Index+4],
}
if ping.Host != a.ping.Host || ping.Scheme != a.ping.Scheme ||
ping.Path != a.ping.Path {
return nil
}
for _, challenge := range a.challenges {
for _, authorizer := range a.authorizers {
if authorizer.Scheme() == challenge.Scheme {
if err := authorizer.Authorize(req, challenge.Parameters); err != nil {
return err
}
}
}
}
return nil
}

View File

@ -1,108 +0,0 @@
// Copyright (c) 2017 VMware, Inc. All Rights Reserved.
//
// 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 auth
import (
"net/http"
"net/url"
"strings"
"testing"
ch "github.com/docker/distribution/registry/client/auth/challenge"
"github.com/vmware/harbor/src/common/utils/test"
)
func TestNewAuthorizerStore(t *testing.T) {
handler := test.Handler(&test.Response{
StatusCode: http.StatusUnauthorized,
Headers: map[string]string{
"Www-Authenticate": "Bearer realm=\"https://auth.docker.io/token\",service=\"registry.docker.io\"",
},
})
server := test.NewServer(&test.RequestHandlerMapping{
Method: "GET",
Pattern: "/v2/",
Handler: handler,
})
defer server.Close()
_, err := NewAuthorizerStore(server.URL, false, nil)
if err != nil {
t.Fatalf("failed to create authorizer store: %v", err)
}
}
type simpleAuthorizer struct {
}
func (s *simpleAuthorizer) Scheme() string {
return "bearer"
}
func (s *simpleAuthorizer) Authorize(req *http.Request,
params map[string]string) error {
req.Header.Set("Authorization", "Bearer token")
return nil
}
func TestModify(t *testing.T) {
authorizer := &simpleAuthorizer{}
challenge := ch.Challenge{
Scheme: "bearer",
}
ping, err := url.Parse("http://example.com/v2/")
if err != nil {
t.Fatalf("failed to parse URL: %v", err)
}
as := &AuthorizerStore{
authorizers: []Authorizer{authorizer},
ping: ping,
challenges: []ch.Challenge{challenge},
}
req, err := http.NewRequest("GET", "http://example.com/v2/ubuntu/manifests/14.04", nil)
if err != nil {
t.Fatalf("failed to create request: %v", err)
}
if err = as.Modify(req); err != nil {
t.Fatalf("failed to modify request: %v", err)
}
header := req.Header.Get("Authorization")
if len(header) == 0 {
t.Fatal("\"Authorization\" header not found")
}
if !strings.HasPrefix(header, "Bearer") {
t.Fatal("\"Authorization\" header does not start with \"Bearer\"")
}
req, err = http.NewRequest("GET", "http://example.com", nil)
if err != nil {
t.Fatalf("failed to create request: %v", err)
}
if err = as.Modify(req); err != nil {
t.Fatalf("failed to modify request: %v", err)
}
header = req.Header.Get("Authorization")
if len(header) != 0 {
t.Fatal("\"Authorization\" header should not be added")
}
}

View File

@ -0,0 +1,56 @@
// Copyright (c) 2017 VMware, Inc. All Rights Reserved.
//
// 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 auth
import (
"regexp"
"github.com/docker/distribution/digest"
"github.com/docker/distribution/reference"
"github.com/vmware/harbor/src/common/utils/log"
)
var (
base = regexp.MustCompile("/v2")
catalog = regexp.MustCompile("/v2/_catalog")
tag = regexp.MustCompile("/v2/(" + reference.NameRegexp.String() + ")/tags/list")
manifest = regexp.MustCompile("/v2/(" + reference.NameRegexp.String() + ")/manifests/(" + reference.TagRegexp.String() + "|" + digest.DigestRegexp.String() + ")")
blob = regexp.MustCompile("/v2/(" + reference.NameRegexp.String() + ")/blobs/" + digest.DigestRegexp.String())
blobUpload = regexp.MustCompile("/v2/(" + reference.NameRegexp.String() + ")/blobs/uploads")
blobUploadChunk = regexp.MustCompile("/v2/(" + reference.NameRegexp.String() + ")/blobs/uploads/[a-zA-Z0-9-_.=]+")
repoRegExps = []*regexp.Regexp{tag, manifest, blob, blobUploadChunk, blobUpload}
)
// parse the repository name from path, if the path doesn't match any
// regular expressions in repoRegExps, nil string will be returned
func parseRepository(path string) string {
for _, regExp := range repoRegExps {
subs := regExp.FindStringSubmatch(path)
// no match
if subs == nil {
continue
}
// match
if len(subs) < 2 {
log.Warningf("unexpected length of sub matches: %d, should >= 2 ", len(subs))
continue
}
return subs[1]
}
return ""
}

View File

@ -0,0 +1,43 @@
// Copyright (c) 2017 VMware, Inc. All Rights Reserved.
//
// 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 auth
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestParseRepository(t *testing.T) {
cases := []struct {
input string
output string
}{
{"/v2", ""},
{"/v2/_catalog", ""},
{"/v2/library/tags/list", "library"},
{"/v2/tags/list", ""},
{"/v2/tags/list/tags/list", "tags/list"},
{"/v2/library/manifests/latest", "library"},
{"/v2/library/manifests/sha256:1234567890", "library"},
{"/v2/library/blobs/sha256:1234567890", "library"},
{"/v2/library/blobs/uploads", "library"},
{"/v2/library/blobs/uploads/1234567890", "library"},
}
for _, c := range cases {
assert.Equal(t, c.output, parseRepository(c.input))
}
}

View File

@ -17,16 +17,20 @@ package auth
import (
"fmt"
"net/http"
"net/url"
"strings"
"sync"
"time"
"github.com/vmware/harbor/src/common/models"
"github.com/vmware/harbor/src/common/utils/log"
"github.com/vmware/harbor/src/common/utils/registry"
token_util "github.com/vmware/harbor/src/ui/service/token"
)
const (
latency int = 10 //second, the network latency when token is received
scheme = "bearer"
)
// Scope ...
@ -40,144 +44,255 @@ func (s *Scope) string() string {
return fmt.Sprintf("%s:%s:%s", s.Type, s.Name, strings.Join(s.Actions, ","))
}
type tokenGenerator func(realm, service string, scopes []string) (token string, expiresIn int, issuedAt *time.Time, err error)
type tokenGenerator interface {
generate(realm, service string, scopes []*Scope) (*models.Token, error)
}
// Implements interface Authorizer
// tokenAuthorizer implements registry.Modifier interface. It parses scopses
// from the request, generates authentication token and modifies the requset
// by adding the token
type tokenAuthorizer struct {
scope *Scope
tg tokenGenerator
cache string // cached token
expiresAt *time.Time // The UTC standard time at when the token will expire
sync.Mutex
realm string
service string
registryURL *url.URL // used to filter request
generator tokenGenerator
client *http.Client
cachedTokens map[string]*models.Token
sync.RWMutex
}
// Scheme returns the scheme that the handler can handle
func (t *tokenAuthorizer) Scheme() string {
return "bearer"
}
// AuthorizeRequest will add authorization header which contains a token before the request is sent
func (t *tokenAuthorizer) Authorize(req *http.Request, params map[string]string) error {
var scopes []*Scope
var token string
hasFrom := false
from := req.URL.Query().Get("from")
if len(from) != 0 {
s := &Scope{
Type: "repository",
Name: from,
Actions: []string{"pull"},
}
scopes = append(scopes, s)
// do not cache the token if "from" appears
hasFrom = true
}
if t.scope != nil {
scopes = append(scopes, t.scope)
}
expired := true
cachedToken, cachedExpiredAt := t.getCachedToken()
if len(cachedToken) != 0 && cachedExpiredAt != nil {
expired = cachedExpiredAt.Before(time.Now().UTC())
}
if expired || hasFrom {
scopeStrs := []string{}
for _, scope := range scopes {
scopeStrs = append(scopeStrs, scope.string())
}
to, expiresIn, _, err := t.tg(params["realm"], params["service"], scopeStrs)
// add token to the request
func (t *tokenAuthorizer) Modify(req *http.Request) error {
//only handle requests sent to registry
goon, err := t.filterReq(req)
if err != nil {
return err
}
token = to
if !hasFrom {
t.updateCachedToken(to, expiresIn)
}
} else {
token = cachedToken
if !goon {
log.Debugf("the request %s is not sent to registry, skip", req.URL.String())
return nil
}
req.Header.Add(http.CanonicalHeaderKey("Authorization"), fmt.Sprintf("Bearer %s", token))
// parse scopes from request
scopes := parseScopes(req)
var token *models.Token
// try to get token from cache if the request is for empty scope(login)
// or single scope
if len(scopes) <= 1 {
key := ""
if len(scopes) == 1 {
key = scopes[0].string()
}
token = t.getCachedToken(key)
}
// request a new token if the token is null
if token == nil {
// ping first if the realm and service are both null
if len(t.realm) == 0 && len(t.service) == 0 {
realm, service, err := ping(t.client, t.registryURL.String())
if err != nil {
return err
}
if len(realm) == 0 {
log.Warning("empty realm, skip")
return nil
}
t.realm = realm
t.service = service
}
token, err = t.generator.generate(t.realm, t.service, scopes)
if err != nil {
return err
}
// only cache the token for empty scope(login) or single scope request
if len(scopes) <= 1 {
key := ""
if len(scopes) == 1 {
key = scopes[0].string()
}
t.updateCachedToken(key, token)
}
}
req.Header.Add(http.CanonicalHeaderKey("Authorization"), fmt.Sprintf("Bearer %s", token.Token))
return nil
}
func (t *tokenAuthorizer) getCachedToken() (string, *time.Time) {
t.Lock()
defer t.Unlock()
return t.cache, t.expiresAt
// some requests are sent to backend storage, such as s3, this method filters
// the requests only sent to registry
func (t *tokenAuthorizer) filterReq(req *http.Request) (bool, error) {
// the registryURL is nil when the first request comes, init it with
// the scheme and host of the request which must be sent to the registry
if t.registryURL == nil {
u, err := url.Parse(buildPingURL(req.URL.Scheme + "://" + req.URL.Host))
if err != nil {
return false, err
}
t.registryURL = u
}
func (t *tokenAuthorizer) updateCachedToken(token string, expiresIn int) {
t.Lock()
defer t.Unlock()
t.cache = token
n := (time.Duration)(expiresIn - latency)
e := time.Now().Add(n * time.Second).UTC()
t.expiresAt = &e
v2Index := strings.Index(req.URL.Path, "/v2/")
if v2Index == -1 {
return false, nil
}
// Implements interface Authorizer
type standardTokenAuthorizer struct {
tokenAuthorizer
client *http.Client
credential Credential
tokenServiceEndpoint string
if req.URL.Host != t.registryURL.Host || req.URL.Scheme != t.registryURL.Scheme ||
req.URL.Path[:v2Index+4] != t.registryURL.Path {
return false, nil
}
return true, nil
}
// parse scopes from the request according to its method, path and query string
func parseScopes(req *http.Request) []*Scope {
scopes := []*Scope{}
from := req.URL.Query().Get("from")
if len(from) != 0 {
scopes = append(scopes, &Scope{
Type: "repository",
Name: from,
Actions: []string{"pull"},
})
}
var scope *Scope
path := strings.TrimRight(req.URL.Path, "/")
repository := parseRepository(path)
if len(repository) > 0 {
// pull, push, delete blob/manifest
scope = &Scope{
Type: "repository",
Name: repository,
}
switch req.Method {
case http.MethodGet:
scope.Actions = []string{"pull"}
case http.MethodPost, http.MethodPut, http.MethodPatch:
scope.Actions = []string{"push"}
case http.MethodDelete:
scope.Actions = []string{"*"}
default:
scope = nil
log.Warningf("unsupported method: %s", req.Method)
}
} else if catalog.MatchString(path) {
// catalog
scope = &Scope{
Type: "registry",
Name: "catalog",
Actions: []string{"*"},
}
} else if base.MatchString(path) {
// base
scope = nil
} else {
// unknow
log.Warningf("can not parse scope from the request: %s %s", req.Method, req.URL.Path)
}
if scope != nil {
scopes = append(scopes, scope)
}
strs := []string{}
for _, s := range scopes {
strs = append(strs, s.string())
}
log.Debugf("scopses parsed from request: %s", strings.Join(strs, " "))
return scopes
}
func (t *tokenAuthorizer) getCachedToken(scope string) *models.Token {
t.RLock()
defer t.RUnlock()
token := t.cachedTokens[scope]
if token == nil {
return nil
}
issueAt, err := time.Parse(time.RFC3339, token.IssuedAt)
if err != nil {
log.Errorf("failed parse %s: %v", token.IssuedAt, err)
return nil
}
if issueAt.Add(time.Duration(token.ExpiresIn-latency) * time.Second).Before(time.Now().UTC()) {
return nil
}
log.Debug("get token from cache")
return token
}
func (t *tokenAuthorizer) updateCachedToken(scope string, token *models.Token) {
t.Lock()
defer t.Unlock()
t.cachedTokens[scope] = token
}
// ping returns the realm, service and error
func ping(client *http.Client, endpoint string) (string, string, error) {
resp, err := client.Get(endpoint)
if err != nil {
return "", "", err
}
defer resp.Body.Close()
challenges := ParseChallengeFromResponse(resp)
for _, challenge := range challenges {
if scheme == challenge.Scheme {
realm := challenge.Parameters["realm"]
service := challenge.Parameters["service"]
return realm, service, nil
}
}
return "", "", fmt.Errorf("schemes %v are unsupportted", challenges)
}
// NewStandardTokenAuthorizer returns a standard token authorizer. The authorizer will request a token
// from token server and add it to the origin request
// If tokenServiceEndpoint is set, the token request will be sent to it instead of the server get from authorizer
// If customizedTokenService is set, the token request will be sent to it instead of the server get from authorizer
// The usage please refer to the function tokenURL
func NewStandardTokenAuthorizer(credential Credential, insecure bool,
tokenServiceEndpoint string, scopeType, scopeName string, scopeActions ...string) Authorizer {
authorizer := &standardTokenAuthorizer{
client: &http.Client{
customizedTokenService ...string) registry.Modifier {
client := &http.Client{
Transport: registry.GetHTTPTransport(insecure),
Timeout: 30 * time.Second,
},
}
generator := &standardTokenGenerator{
credential: credential,
tokenServiceEndpoint: tokenServiceEndpoint,
client: client,
}
if len(customizedTokenService) > 0 {
generator.customizedTokenService = customizedTokenService[0]
}
if len(scopeType) != 0 || len(scopeName) != 0 {
authorizer.scope = &Scope{
Type: scopeType,
Name: scopeName,
Actions: scopeActions,
return &tokenAuthorizer{
cachedTokens: make(map[string]*models.Token),
generator: generator,
client: client,
}
}
authorizer.tg = authorizer.generateToken
return authorizer
// standardTokenGenerator implements interface tokenGenerator
type standardTokenGenerator struct {
credential Credential
customizedTokenService string
client *http.Client
}
func (s *standardTokenAuthorizer) generateToken(realm, service string, scopes []string) (string, int, *time.Time, error) {
// get token from token service
func (s *standardTokenGenerator) generate(realm, service string, scopes []*Scope) (*models.Token, error) {
realm = s.tokenURL(realm)
tk, err := getToken(s.client, s.credential, realm,
service, scopes)
if err != nil {
return "", 0, nil, err
}
if len(tk.IssuedAt) == 0 {
return tk.Token, tk.ExpiresIn, nil, nil
}
issuedAt, err := time.Parse(time.RFC3339, tk.IssuedAt)
if err != nil {
return "", 0, nil, err
}
return tk.Token, tk.ExpiresIn, &issuedAt, nil
return getToken(s.client, s.credential, realm, service, scopes)
}
// when the registry client is used inside Harbor, the token request
@ -187,62 +302,50 @@ func (s *standardTokenAuthorizer) generateToken(realm, service string, scopes []
// 1. performance issue
// 2. the realm field returned by registry is an IP which can not reachable
// inside Harbor
func (s *standardTokenAuthorizer) tokenURL(realm string) string {
if len(s.tokenServiceEndpoint) != 0 {
return s.tokenServiceEndpoint
func (s *standardTokenGenerator) tokenURL(realm string) string {
if len(s.customizedTokenService) != 0 {
return s.customizedTokenService
}
return realm
}
// Implements interface Handler
type usernameTokenAuthorizer struct {
tokenAuthorizer
username string
}
// NewRegistryUsernameTokenAuthorizer returns an authorizer to generate token for registry according to
// the user's privileges
func NewRegistryUsernameTokenAuthorizer(username, scopeType, scopeName string, scopeActions ...string) Authorizer {
return newUsernameTokenAuthorizer(false, username, scopeType, scopeName, scopeActions...)
}
// NewNotaryUsernameTokenAuthorizer returns an authorizer to generate token for notary according to
// the user's privileges
func NewNotaryUsernameTokenAuthorizer(username, scopeType, scopeName string, scopeActions ...string) Authorizer {
return newUsernameTokenAuthorizer(true, username, scopeType, scopeName, scopeActions...)
}
// newUsernameTokenAuthorizer returns a authorizer which will generate a token according to
// the user's privileges
func newUsernameTokenAuthorizer(notary bool, username, scopeType, scopeName string, scopeActions ...string) Authorizer {
authorizer := &usernameTokenAuthorizer{
// NewRawTokenAuthorizer returns a token authorizer which calls method to create
// token directly
func NewRawTokenAuthorizer(username, service string) registry.Modifier {
generator := &rawTokenGenerator{
username: username,
}
authorizer.scope = &Scope{
Type: scopeType,
Name: scopeName,
Actions: scopeActions,
return &tokenAuthorizer{
service: service,
cachedTokens: make(map[string]*models.Token),
generator: generator,
}
if notary {
authorizer.tg = authorizer.genNotaryToken
} else {
authorizer.tg = authorizer.genRegistryToken
}
return authorizer
}
func (u *usernameTokenAuthorizer) generateToken(realm, service string, scopes []string) (token string, expiresIn int, issuedAt *time.Time, err error) {
token, expiresIn, issuedAt, err = token_util.RegistryTokenForUI(u.username, service, scopes)
return
// rawTokenGenerator implements interface tokenGenerator
type rawTokenGenerator struct {
username string
}
func (u *usernameTokenAuthorizer) genRegistryToken(realm, service string, scopes []string) (token string, expiresIn int, issuedAt *time.Time, err error) {
token, expiresIn, issuedAt, err = token_util.RegistryTokenForUI(u.username, service, scopes)
return
// generate token directly
func (r *rawTokenGenerator) generate(realm, service string, scopes []*Scope) (*models.Token, error) {
strs := []string{}
for _, scope := range scopes {
strs = append(strs, scope.string())
}
token, expiresIn, issuedAt, err := token_util.RegistryTokenForUI(r.username, service, strs)
if err != nil {
return nil, err
}
func (u *usernameTokenAuthorizer) genNotaryToken(realm, service string, scopes []string) (token string, expiresIn int, issuedAt *time.Time, err error) {
token, expiresIn, issuedAt, err = token_util.NotaryTokenForUI(u.username, service, scopes)
return
return &models.Token{
Token: token,
ExpiresIn: expiresIn,
IssuedAt: issuedAt.Format(time.RFC3339),
}, nil
}
func buildPingURL(endpoint string) string {
return fmt.Sprintf("%s/v2/", endpoint)
}

View File

@ -15,54 +15,184 @@
package auth
import (
"encoding/json"
"fmt"
"net/http"
"strings"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/vmware/harbor/src/common/models"
"github.com/vmware/harbor/src/common/utils/test"
)
func TestAuthorizeOfStandardTokenAuthorizer(t *testing.T) {
handler := test.Handler(&test.Response{
Body: []byte(`
{
"token":"token",
"expires_in":300,
"issued_at":"2016-08-17T23:17:58+08:00"
func TestFilterReq(t *testing.T) {
authorizer := tokenAuthorizer{}
// v2
req, err := http.NewRequest(http.MethodGet, "http://registry/v2/", nil)
require.Nil(t, err)
goon, err := authorizer.filterReq(req)
assert.Nil(t, err)
assert.True(t, goon)
// catalog
req, err = http.NewRequest(http.MethodGet, "http://registry/v2/_catalog?n=1000", nil)
require.Nil(t, err)
goon, err = authorizer.filterReq(req)
assert.Nil(t, err)
assert.True(t, goon)
// contains two v2 in path
req, err = http.NewRequest(http.MethodGet, "http://registry/v2/library/v2/tags/list", nil)
require.Nil(t, err)
goon, err = authorizer.filterReq(req)
assert.Nil(t, err)
assert.True(t, goon)
// different scheme
req, err = http.NewRequest(http.MethodGet, "https://registry/v2/library/golang/tags/list", nil)
require.Nil(t, err)
goon, err = authorizer.filterReq(req)
assert.Nil(t, err)
assert.False(t, goon)
// different host
req, err = http.NewRequest(http.MethodGet, "http://vmware.com/v2/library/golang/tags/list", nil)
require.Nil(t, err)
goon, err = authorizer.filterReq(req)
assert.Nil(t, err)
assert.False(t, goon)
// different path
req, err = http.NewRequest(http.MethodGet, "https://registry/s3/ssss", nil)
require.Nil(t, err)
goon, err = authorizer.filterReq(req)
assert.Nil(t, err)
assert.False(t, goon)
}
`),
func TestParseScopes(t *testing.T) {
// contains from in query string
req, err := http.NewRequest(http.MethodGet, "http://registry/v2?from=library", nil)
require.Nil(t, err)
scopses := parseScopes(req)
assert.Equal(t, 1, len(scopses))
assert.EqualValues(t, &Scope{
Type: "repository",
Name: "library",
Actions: []string{
"pull"},
}, scopses[0])
// v2
req, err = http.NewRequest(http.MethodGet, "http://registry/v2", nil)
require.Nil(t, err)
scopses = parseScopes(req)
assert.Equal(t, 0, len(scopses))
// catalog
req, err = http.NewRequest(http.MethodGet, "http://registry/v2/_catalog", nil)
require.Nil(t, err)
scopses = parseScopes(req)
assert.Equal(t, 1, len(scopses))
assert.EqualValues(t, &Scope{
Type: "registry",
Name: "catalog",
Actions: []string{
"*"},
}, scopses[0])
// manifest
req, err = http.NewRequest(http.MethodPut, "http://registry/v2/library/mysql/5.6/manifests/1", nil)
require.Nil(t, err)
scopses = parseScopes(req)
assert.Equal(t, 1, len(scopses))
assert.EqualValues(t, &Scope{
Type: "repository",
Name: "library/mysql/5.6",
Actions: []string{
"push"},
}, scopses[0])
}
func TestGetAndUpdateCachedToken(t *testing.T) {
authorizer := &tokenAuthorizer{
cachedTokens: make(map[string]*models.Token),
}
// empty cache
token := authorizer.getCachedToken("")
assert.Nil(t, token)
// put a valid token into cache
token = &models.Token{
Token: "token",
ExpiresIn: 60,
IssuedAt: time.Now().Format(time.RFC3339),
}
authorizer.updateCachedToken("", token)
token2 := authorizer.getCachedToken("")
assert.EqualValues(t, token, token2)
// put a expired token into cache
token = &models.Token{
Token: "token",
ExpiresIn: 60,
IssuedAt: time.Now().Add(-time.Second * 120).Format("2006-01-02 15:04:05.999999999 -0700 MST"),
}
authorizer.updateCachedToken("", token)
token2 = authorizer.getCachedToken("")
assert.Nil(t, token2)
}
func TestModifyOfStandardTokenAuthorizer(t *testing.T) {
token := &models.Token{
Token: "token",
ExpiresIn: 3600,
IssuedAt: time.Now().String(),
}
data, err := json.Marshal(token)
require.Nil(t, err)
tokenHandler := test.Handler(&test.Response{
Body: data,
})
server := test.NewServer(&test.RequestHandlerMapping{
tokenServer := test.NewServer(
&test.RequestHandlerMapping{
Method: "GET",
Pattern: "/token",
Handler: handler,
Pattern: "/service/token",
Handler: tokenHandler,
})
defer server.Close()
defer tokenServer.Close()
authorizer := NewStandardTokenAuthorizer(nil, false, "", "repository", "library/ubuntu", "pull")
req, err := http.NewRequest("GET", "http://registry", nil)
if err != nil {
t.Fatalf("failed to create request: %v", err)
}
header := fmt.Sprintf("Bearer realm=\"%s/service/token\",service=\"registry\"",
tokenServer.URL)
pingHandler := test.Handler(&test.Response{
StatusCode: http.StatusUnauthorized,
Headers: map[string]string{
"WWW-Authenticate": header,
},
})
registryServer := test.NewServer(
&test.RequestHandlerMapping{
Method: "GET",
Pattern: "/v2",
Handler: pingHandler,
})
defer registryServer.Close()
params := map[string]string{
"realm": server.URL + "/token",
}
req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("%s/v2/", registryServer.URL), nil)
require.Nil(t, err)
if err := authorizer.Authorize(req, params); err != nil {
t.Fatalf("failed to authorize request: %v", err)
}
authorizer := NewStandardTokenAuthorizer(nil, false)
err = authorizer.Modify(req)
require.Nil(t, err)
tk := req.Header.Get("Authorization")
if tk != "Bearer token" {
t.Errorf("unexpected token: %s != %s", tk, "Bearer token")
}
}
func TestSchemeOfStandardTokenAuthorizer(t *testing.T) {
authorizer := &standardTokenAuthorizer{}
if authorizer.Scheme() != "bearer" {
t.Errorf("unexpected scheme: %s != %s", authorizer.Scheme(), "bearer")
}
assert.Equal(t, strings.ToLower("Bearer "+token.Token), strings.ToLower(tk))
}

View File

@ -37,16 +37,11 @@ func GetToken(endpoint string, insecure bool, credential Credential,
Transport: registry.GetHTTPTransport(insecure),
}
scopesStr := []string{}
for _, scope := range scopes {
scopesStr = append(scopesStr, scope.string())
}
return getToken(client, credential, endpoint, service, scopesStr)
return getToken(client, credential, endpoint, service, scopes)
}
func getToken(client *http.Client, credential Credential, realm, service string,
scopes []string) (*models.Token, error) {
scopes []*Scope) (*models.Token, error) {
u, err := url.Parse(realm)
if err != nil {
return nil, err
@ -54,7 +49,7 @@ func getToken(client *http.Client, credential Credential, realm, service string,
query := u.Query()
query.Add("service", service)
for _, scope := range scopes {
query.Add("scope", scope)
query.Add("scope", scope.string())
}
u.RawQuery = query.Encode()

View File

@ -50,7 +50,7 @@ func (isj *ImageScanJob) Post() {
}
c := &http.Cookie{Name: models.UISecretCookie, Value: config.JobserviceSecret()}
repoClient, err := utils.NewRepositoryClient(regURL, false, auth.NewCookieCredential(c),
config.InternalTokenServiceEndpoint(), data.Repo, "pull", "push", "*")
config.InternalTokenServiceEndpoint(), data.Repo)
if err != nil {
log.Errorf("An error occurred while creating repository client: %v", err)
isj.RenderError(http.StatusInternalServerError, "Failed to repository client")

View File

@ -132,7 +132,7 @@ func (i *Initializer) enter() (string, error) {
c := &http.Cookie{Name: models.UISecretCookie, Value: i.srcSecret}
srcCred := auth.NewCookieCredential(c)
srcClient, err := utils.NewRepositoryClient(i.srcURL, i.insecure, srcCred,
config.InternalTokenServiceEndpoint(), i.repository, "pull", "push", "*")
config.InternalTokenServiceEndpoint(), i.repository)
if err != nil {
i.logger.Errorf("an error occurred while creating source repository client: %v", err)
return "", err
@ -141,7 +141,7 @@ func (i *Initializer) enter() (string, error) {
dstCred := auth.NewBasicAuthCredential(i.dstUsr, i.dstPwd)
dstClient, err := utils.NewRepositoryClient(i.dstURL, i.insecure, dstCred,
"", i.repository, "pull", "push", "*")
"", i.repository)
if err != nil {
i.logger.Errorf("an error occurred while creating destination repository client: %v", err)
return "", err

View File

@ -43,7 +43,7 @@ func (iz *Initializer) Enter() (string, error) {
}
c := &http.Cookie{Name: models.UISecretCookie, Value: config.JobserviceSecret()}
repoClient, err := utils.NewRepositoryClient(regURL, false, auth.NewCookieCredential(c),
config.InternalTokenServiceEndpoint(), iz.Context.Repository, "pull")
config.InternalTokenServiceEndpoint(), iz.Context.Repository)
if err != nil {
logger.Errorf("An error occurred while creating repository client: %v", err)
return "", err

View File

@ -26,24 +26,15 @@ import (
//NewRepositoryClient create a repository client with scope type "reopsitory" and scope as the repository it would access.
func NewRepositoryClient(endpoint string, insecure bool, credential auth.Credential,
tokenServiceEndpoint, repository string, actions ...string) (*registry.Repository, error) {
tokenServiceEndpoint, repository string) (*registry.Repository, error) {
authorizer := auth.NewStandardTokenAuthorizer(credential, insecure,
tokenServiceEndpoint, "repository", repository, actions...)
store, err := auth.NewAuthorizerStore(endpoint, insecure, authorizer)
if err != nil {
return nil, err
}
tokenServiceEndpoint)
uam := &userAgentModifier{
userAgent: "harbor-registry-client",
}
client, err := registry.NewRepositoryWithModifiers(repository, endpoint, insecure, store, uam)
if err != nil {
return nil, err
}
return client, nil
return registry.NewRepositoryWithModifiers(repository, endpoint, insecure, authorizer, uam)
}
type userAgentModifier struct {

View File

@ -189,7 +189,7 @@ func (ra *RepositoryAPI) Delete() {
return
}
rc, err := ra.initRepositoryClient(repoName)
rc, err := uiutils.NewRepositoryClientForUI(ra.SecurityCtx.GetUsername(), repoName)
if err != nil {
log.Errorf("error occurred while initializing repository client for %s: %v", repoName, err)
ra.CustomAbort(http.StatusInternalServerError, "internal error")
@ -305,7 +305,7 @@ func (ra *RepositoryAPI) GetTag() {
return
}
client, err := ra.initRepositoryClient(repository)
client, err := uiutils.NewRepositoryClientForUI(ra.SecurityCtx.GetUsername(), repository)
if err != nil {
ra.HandleInternalServerError(fmt.Sprintf("failed to initialize the client for %s: %v",
repository, err))
@ -354,7 +354,7 @@ func (ra *RepositoryAPI) GetTags() {
return
}
client, err := ra.initRepositoryClient(repoName)
client, err := uiutils.NewRepositoryClientForUI(ra.SecurityCtx.GetUsername(), repoName)
if err != nil {
log.Errorf("error occurred while initializing repository client for %s: %v", repoName, err)
ra.CustomAbort(http.StatusInternalServerError, "internal error")
@ -495,7 +495,7 @@ func (ra *RepositoryAPI) GetManifests() {
return
}
rc, err := ra.initRepositoryClient(repoName)
rc, err := uiutils.NewRepositoryClientForUI(ra.SecurityCtx.GetUsername(), repoName)
if err != nil {
log.Errorf("error occurred while initializing repository client for %s: %v", repoName, err)
ra.CustomAbort(http.StatusInternalServerError, "internal error")
@ -557,16 +557,6 @@ func getManifest(client *registry.Repository,
return result, nil
}
func (ra *RepositoryAPI) initRepositoryClient(repoName string) (r *registry.Repository, err error) {
endpoint, err := config.RegistryURL()
if err != nil {
return nil, err
}
return uiutils.NewRepositoryClientForUI(endpoint, true, ra.SecurityCtx.GetUsername(),
repoName, "pull", "push", "*")
}
//GetTopRepos returns the most populor repositories
func (ra *RepositoryAPI) GetTopRepos() {
count, err := ra.GetInt("count", 10)
@ -790,7 +780,7 @@ func (ra *RepositoryAPI) checkExistence(repository, tag string) (bool, string, e
log.Errorf("project %s not found", project)
return false, "", nil
}
client, err := ra.initRepositoryClient(repository)
client, err := uiutils.NewRepositoryClientForUI(ra.SecurityCtx.GetUsername(), repository)
if err != nil {
return false, "", fmt.Errorf("failed to initialize the client for %s: %v", repository, err)
}

View File

@ -25,7 +25,6 @@ import (
"github.com/vmware/harbor/src/common/models"
"github.com/vmware/harbor/src/common/utils"
"github.com/vmware/harbor/src/common/utils/log"
"github.com/vmware/harbor/src/ui/config"
uiutils "github.com/vmware/harbor/src/ui/utils"
)
@ -165,13 +164,7 @@ func filterRepositories(projects []*models.Project, keyword string) (
}
func getTags(repository string) ([]string, error) {
url, err := config.RegistryURL()
if err != nil {
return nil, err
}
client, err := uiutils.NewRepositoryClientForUI(url, true,
"admin", repository, "pull")
client, err := uiutils.NewRepositoryClientForUI("harbor-ui", repository)
if err != nil {
return nil, err
}

View File

@ -24,10 +24,10 @@ import (
"github.com/vmware/harbor/src/common/dao"
"github.com/vmware/harbor/src/common/models"
"github.com/vmware/harbor/src/common/utils"
registry_error "github.com/vmware/harbor/src/common/utils/error"
"github.com/vmware/harbor/src/common/utils/log"
"github.com/vmware/harbor/src/common/utils/registry"
"github.com/vmware/harbor/src/common/utils/registry/auth"
registry_error "github.com/vmware/harbor/src/common/utils/error"
"github.com/vmware/harbor/src/ui/config"
)
@ -64,8 +64,7 @@ func (t *TargetAPI) ping(endpoint, username, password string) {
log.Errorf("failed to check whether insecure or not: %v", err)
t.CustomAbort(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError))
}
registry, err := newRegistryClient(endpoint, !verify, username, password,
"", "", "")
registry, err := newRegistryClient(endpoint, !verify, username, password)
if err != nil {
// timeout, dns resolve error, connection refused, etc.
if urlErr, ok := err.(*url.Error); ok {
@ -345,23 +344,10 @@ func (t *TargetAPI) Delete() {
}
}
func newRegistryClient(endpoint string, insecure bool, username, password, scopeType, scopeName string,
scopeActions ...string) (*registry.Registry, error) {
func newRegistryClient(endpoint string, insecure bool, username, password string) (*registry.Registry, error) {
credential := auth.NewBasicAuthCredential(username, password)
authorizer := auth.NewStandardTokenAuthorizer(credential, insecure,
"", scopeType, scopeName, scopeActions...)
store, err := auth.NewAuthorizerStore(endpoint, insecure, authorizer)
if err != nil {
return nil, err
}
client, err := registry.NewRegistryWithModifiers(endpoint, insecure, store)
if err != nil {
return nil, err
}
return client, nil
authorizer := auth.NewStandardTokenAuthorizer(credential, insecure)
return registry.NewRegistryWithModifiers(endpoint, insecure, authorizer)
}
// ListPolicies ...

View File

@ -34,6 +34,7 @@ import (
"github.com/vmware/harbor/src/common/utils/registry/auth"
"github.com/vmware/harbor/src/ui/config"
"github.com/vmware/harbor/src/ui/projectmanager"
"github.com/vmware/harbor/src/ui/service/token"
uiutils "github.com/vmware/harbor/src/ui/utils"
)
@ -279,12 +280,7 @@ func diffRepos(reposInRegistry []string, reposInDB []string,
}
// TODO remove the workaround when the bug of registry is fixed
endpoint, err := config.RegistryURL()
if err != nil {
return needsAdd, needsDel, err
}
client, err := uiutils.NewRepositoryClientForUI(endpoint, true,
"admin", repoInR, "pull")
client, err := uiutils.NewRepositoryClientForUI("harbor-ui", repoInR)
if err != nil {
return needsAdd, needsDel, err
}
@ -304,11 +300,7 @@ func diffRepos(reposInRegistry []string, reposInDB []string,
j++
} else {
// TODO remove the workaround when the bug of registry is fixed
endpoint, err := config.RegistryURL()
if err != nil {
return needsAdd, needsDel, err
}
client, err := uiutils.NewRepositoryClientForUI(endpoint, true, "admin", repoInR, "pull")
client, err := uiutils.NewRepositoryClientForUI("harbor-ui", repoInR)
if err != nil {
return needsAdd, needsDel, err
}
@ -340,12 +332,7 @@ func diffRepos(reposInRegistry []string, reposInDB []string,
continue
}
endpoint, err := config.RegistryURL()
if err != nil {
log.Errorf("failed to get registry URL: %v", err)
continue
}
client, err := uiutils.NewRepositoryClientForUI(endpoint, true, "admin", repoInR, "pull")
client, err := uiutils.NewRepositoryClientForUI("harbor-ui", repoInR)
if err != nil {
log.Errorf("failed to create repository client: %v", err)
continue
@ -377,7 +364,6 @@ func projectExists(pm projectmanager.ProjectManager, repository string) (bool, e
return pm.Exist(project)
}
// TODO need a registry client which accept a raw token as param
func initRegistryClient() (r *registry.Registry, err error) {
endpoint, err := config.RegistryURL()
if err != nil {
@ -393,12 +379,8 @@ func initRegistryClient() (r *registry.Registry, err error) {
return nil, err
}
registryClient, err := NewRegistryClient(endpoint, true, "admin",
"registry", "catalog", "*")
if err != nil {
return nil, err
}
return registryClient, nil
authorizer := auth.NewRawTokenAuthorizer("harbor-ui", token.Registry)
return registry.NewRegistryWithModifiers(endpoint, true, authorizer)
}
func buildReplicationURL() string {
@ -449,24 +431,6 @@ func repositoryExist(name string, client *registry.Repository) (bool, error) {
return len(tags) != 0, nil
}
// NewRegistryClient ...
// TODO need a registry client which accept a raw token as param
func NewRegistryClient(endpoint string, insecure bool, username, scopeType, scopeName string,
scopeActions ...string) (*registry.Registry, error) {
authorizer := auth.NewRegistryUsernameTokenAuthorizer(username, scopeType, scopeName, scopeActions...)
store, err := auth.NewAuthorizerStore(endpoint, insecure, authorizer)
if err != nil {
return nil, err
}
client, err := registry.NewRegistryWithModifiers(endpoint, insecure, store)
if err != nil {
return nil, err
}
return client, nil
}
// transformVulnerabilities transforms the returned value of Clair API to a list of VulnerabilityItem
func transformVulnerabilities(layerWithVuln *models.ClairLayerEnvelope) []*models.VulnerabilityItem {
res := []*models.VulnerabilityItem{}

View File

@ -27,7 +27,7 @@ const (
manifestURLPattern = `^/v2/((?:[a-z0-9]+(?:[._-][a-z0-9]+)*/)+)manifests/([\w][\w.:-]{0,127})`
imageInfoCtxKey = contextKey("ImageInfo")
//TODO: temp solution, remove after vmware/harbor#2242 is resolved.
tokenUsername = "admin"
tokenUsername = "harbor-ui"
)
// Record the docker deamon raw response.

View File

@ -93,9 +93,6 @@ func filterAccess(access []*token.ResourceActions, ctx security.Context,
return nil
}
// TODO merge RegistryTokenForUI NotaryTokenForUI genTokenForUI
// to one function
//RegistryTokenForUI calls genTokenForUI to get raw token for registry
func RegistryTokenForUI(username string, service string, scopes []string) (string, int, *time.Time, error) {
return genTokenForUI(username, service, scopes)

View File

@ -33,8 +33,10 @@ var registryFilterMap map[string]accessFilter
var notaryFilterMap map[string]accessFilter
const (
notary = "harbor-notary"
registry = "harbor-registry"
// Notary service
Notary = "harbor-notary"
// Registry service
Registry = "harbor-registry"
)
//InitCreators initialize the token creators for different services
@ -57,14 +59,14 @@ func InitCreators() {
},
},
}
creatorMap[notary] = &generalCreator{
service: notary,
creatorMap[Notary] = &generalCreator{
service: Notary,
filterMap: notaryFilterMap,
}
}
creatorMap[registry] = &generalCreator{
service: registry,
creatorMap[Registry] = &generalCreator{
service: Registry,
filterMap: registryFilterMap,
}
}

View File

@ -22,6 +22,7 @@ import (
"github.com/vmware/harbor/src/common/utils/registry"
"github.com/vmware/harbor/src/common/utils/registry/auth"
"github.com/vmware/harbor/src/ui/config"
"github.com/vmware/harbor/src/ui/service/token"
"bytes"
"encoding/json"
@ -32,11 +33,6 @@ import (
// ScanAllImages scans all images of Harbor by submiting jobs to jobservice, the whole process will move one if failed to subit any job of a single image.
func ScanAllImages() error {
regURL, err := config.RegistryURL()
if err != nil {
log.Errorf("Failed to load registry url")
return err
}
repos, err := dao.GetAllRepositories()
if err != nil {
log.Errorf("Failed to list all repositories, error: %v", err)
@ -49,7 +45,7 @@ func ScanAllImages() error {
var err error
var tags []string
for _, r := range repos {
repoClient, err = NewRepositoryClientForUI(regURL, true, "harbor-ui", r.Name, "pull")
repoClient, err = NewRepositoryClientForUI("harbor-ui", r.Name)
if err != nil {
log.Errorf("Failed to initialize client for repository: %s, error: %v, skip scanning", r.Name, err)
continue
@ -113,19 +109,13 @@ func TriggerImageScan(repository string, tag string) error {
}
// NewRepositoryClientForUI ...
// TODO need a registry client which accept a raw token as param
func NewRepositoryClientForUI(endpoint string, insecure bool, username, repository string,
scopeActions ...string) (*registry.Repository, error) {
authorizer := auth.NewRegistryUsernameTokenAuthorizer(username, "repository", repository, scopeActions...)
store, err := auth.NewAuthorizerStore(endpoint, insecure, authorizer)
func NewRepositoryClientForUI(username, repository string) (*registry.Repository, error) {
endpoint, err := config.RegistryURL()
if err != nil {
return nil, err
}
client, err := registry.NewRepositoryWithModifiers(repository, endpoint, insecure, store)
if err != nil {
return nil, err
}
return client, nil
insecure := true
authorizer := auth.NewRawTokenAuthorizer(username, token.Registry)
return registry.NewRepositoryWithModifiers(repository, endpoint, insecure, authorizer)
}