From cc264f85e71980990c7eb9b6b616eb66bb368258 Mon Sep 17 00:00:00 2001 From: Wenkai Yin Date: Mon, 24 Jul 2017 17:41:06 +0800 Subject: [PATCH 1/5] do not ping if using raw token authorizer --- src/common/utils/notary/helper.go | 9 +- src/common/utils/notary/helper_test.go | 8 +- src/common/utils/registry/auth/authorizer.go | 107 ----- .../utils/registry/auth/authorizer_test.go | 108 ----- src/common/utils/registry/auth/path.go | 56 +++ src/common/utils/registry/auth/path_test.go | 43 ++ .../utils/registry/auth/tokenauthorizer.go | 399 +++++++++++------- .../registry/auth/tokenauthorizer_test.go | 202 +++++++-- src/common/utils/registry/auth/util.go | 11 +- src/jobservice/api/scan.go | 2 +- src/jobservice/replication/transfer.go | 4 +- src/jobservice/scan/handlers.go | 2 +- src/jobservice/utils/utils.go | 15 +- src/ui/api/repository.go | 20 +- src/ui/api/search.go | 9 +- src/ui/api/target.go | 24 +- src/ui/api/utils.go | 48 +-- src/ui/proxy/interceptors.go | 2 +- src/ui/service/token/authutils.go | 3 - src/ui/service/token/creator.go | 14 +- src/ui/utils/utils.go | 24 +- 21 files changed, 567 insertions(+), 543 deletions(-) delete mode 100644 src/common/utils/registry/auth/authorizer.go delete mode 100644 src/common/utils/registry/auth/authorizer_test.go create mode 100644 src/common/utils/registry/auth/path.go create mode 100644 src/common/utils/registry/auth/path_test.go diff --git a/src/common/utils/notary/helper.go b/src/common/utils/notary/helper.go index 60b7dbb89..b6e1abb1c 100644 --- a/src/common/utils/notary/helper.go +++ b/src/common/utils/notary/helper.go @@ -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 { diff --git a/src/common/utils/notary/helper_test.go b/src/common/utils/notary/helper_test.go index 83988d8a6..ee8065e01 100644 --- a/src/common/utils/notary/helper_test.go +++ b/src/common/utils/notary/helper_test.go @@ -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" @@ -36,9 +37,10 @@ func TestMain(m *testing.M) { notaryServer = notarytest.NewNotaryServer(endpoint) defer notaryServer.Close() var defaultConfig = map[string]interface{}{ - common.ExtEndpoint: "https://" + endpoint, - common.WithNotary: true, - common.CfgExpiration: 5, + common.ExtEndpoint: "https://" + endpoint, + common.WithNotary: true, + common.CfgExpiration: 5, + common.TokenExpiration: 30, } adminServer, err := utilstest.NewAdminserver(defaultConfig) if err != nil { diff --git a/src/common/utils/registry/auth/authorizer.go b/src/common/utils/registry/auth/authorizer.go deleted file mode 100644 index 63476bdfa..000000000 --- a/src/common/utils/registry/auth/authorizer.go +++ /dev/null @@ -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 -} diff --git a/src/common/utils/registry/auth/authorizer_test.go b/src/common/utils/registry/auth/authorizer_test.go deleted file mode 100644 index 3b368e143..000000000 --- a/src/common/utils/registry/auth/authorizer_test.go +++ /dev/null @@ -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") - } -} diff --git a/src/common/utils/registry/auth/path.go b/src/common/utils/registry/auth/path.go new file mode 100644 index 000000000..954847dac --- /dev/null +++ b/src/common/utils/registry/auth/path.go @@ -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 "" +} diff --git a/src/common/utils/registry/auth/path_test.go b/src/common/utils/registry/auth/path_test.go new file mode 100644 index 000000000..995af6f7d --- /dev/null +++ b/src/common/utils/registry/auth/path_test.go @@ -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)) + } +} diff --git a/src/common/utils/registry/auth/tokenauthorizer.go b/src/common/utils/registry/auth/tokenauthorizer.go index 3bbc74f14..905bc1487 100644 --- a/src/common/utils/registry/auth/tokenauthorizer.go +++ b/src/common/utils/registry/auth/tokenauthorizer.go @@ -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" -} +// 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 + } -// 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 + if !goon { + log.Debugf("the request %s is not sent to registry, skip", req.URL.String()) + return nil + } - hasFrom := false - from := req.URL.Query().Get("from") - if len(from) != 0 { - s := &Scope{ - Type: "repository", - Name: from, - Actions: []string{"pull"}, + // 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() } - scopes = append(scopes, s) - // do not cache the token if "from" appears - hasFrom = true + token = t.getCachedToken(key) } - 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()) + // 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 } - to, expiresIn, _, err := t.tg(params["realm"], params["service"], scopeStrs) + token, err = t.generator.generate(t.realm, t.service, scopes) if err != nil { return err } - token = to - - if !hasFrom { - t.updateCachedToken(to, expiresIn) + // 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) } - } else { - token = cachedToken } - req.Header.Add(http.CanonicalHeaderKey("Authorization"), fmt.Sprintf("Bearer %s", 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 + } + + v2Index := strings.Index(req.URL.Path, "/v2/") + if v2Index == -1 { + return false, nil + } + + 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 } -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 +// 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 } -// Implements interface Authorizer -type standardTokenAuthorizer struct { - tokenAuthorizer - client *http.Client - credential Credential - tokenServiceEndpoint string +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{ - Transport: registry.GetHTTPTransport(insecure), - Timeout: 30 * time.Second, - }, - credential: credential, - tokenServiceEndpoint: tokenServiceEndpoint, + customizedTokenService ...string) registry.Modifier { + client := &http.Client{ + Transport: registry.GetHTTPTransport(insecure), + Timeout: 30 * time.Second, } - if len(scopeType) != 0 || len(scopeName) != 0 { - authorizer.scope = &Scope{ - Type: scopeType, - Name: scopeName, - Actions: scopeActions, - } + generator := &standardTokenGenerator{ + credential: credential, + client: client, + } + if len(customizedTokenService) > 0 { + generator.customizedTokenService = customizedTokenService[0] } - authorizer.tg = authorizer.generateToken - - return authorizer + return &tokenAuthorizer{ + cachedTokens: make(map[string]*models.Token), + generator: generator, + client: client, + } } -func (s *standardTokenAuthorizer) generateToken(realm, service string, scopes []string) (string, int, *time.Time, error) { +// standardTokenGenerator implements interface tokenGenerator +type standardTokenGenerator struct { + credential Credential + customizedTokenService string + client *http.Client +} + +// 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 +} + +// rawTokenGenerator implements interface tokenGenerator +type rawTokenGenerator struct { + username string +} + +// 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()) } - return authorizer + token, expiresIn, issuedAt, err := token_util.RegistryTokenForUI(r.username, service, strs) + if err != nil { + return nil, err + } + + return &models.Token{ + Token: token, + ExpiresIn: expiresIn, + IssuedAt: issuedAt.Format(time.RFC3339), + }, nil } -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 -} - -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 -} - -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 +func buildPingURL(endpoint string) string { + return fmt.Sprintf("%s/v2/", endpoint) } diff --git a/src/common/utils/registry/auth/tokenauthorizer_test.go b/src/common/utils/registry/auth/tokenauthorizer_test.go index c93a35802..751fa1893 100644 --- a/src/common/utils/registry/auth/tokenauthorizer_test.go +++ b/src/common/utils/registry/auth/tokenauthorizer_test.go @@ -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{ - Method: "GET", - Pattern: "/token", - Handler: handler, + tokenServer := test.NewServer( + &test.RequestHandlerMapping{ + Method: "GET", + Pattern: "/service/token", + Handler: tokenHandler, + }) + defer tokenServer.Close() + + 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, + }, }) - defer server.Close() + registryServer := test.NewServer( + &test.RequestHandlerMapping{ + Method: "GET", + Pattern: "/v2", + Handler: pingHandler, + }) + defer registryServer.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) - } + req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("%s/v2/", registryServer.URL), nil) + require.Nil(t, err) - params := map[string]string{ - "realm": server.URL + "/token", - } + authorizer := NewStandardTokenAuthorizer(nil, false) - if err := authorizer.Authorize(req, params); err != nil { - t.Fatalf("failed to authorize request: %v", err) - } + 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)) } diff --git a/src/common/utils/registry/auth/util.go b/src/common/utils/registry/auth/util.go index 373c95890..47d5ac963 100644 --- a/src/common/utils/registry/auth/util.go +++ b/src/common/utils/registry/auth/util.go @@ -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() diff --git a/src/jobservice/api/scan.go b/src/jobservice/api/scan.go index 214590881..2a7b986eb 100644 --- a/src/jobservice/api/scan.go +++ b/src/jobservice/api/scan.go @@ -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") diff --git a/src/jobservice/replication/transfer.go b/src/jobservice/replication/transfer.go index dadf6a11d..5fae137aa 100644 --- a/src/jobservice/replication/transfer.go +++ b/src/jobservice/replication/transfer.go @@ -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 diff --git a/src/jobservice/scan/handlers.go b/src/jobservice/scan/handlers.go index 66f29b2f4..cee15a2b6 100644 --- a/src/jobservice/scan/handlers.go +++ b/src/jobservice/scan/handlers.go @@ -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 diff --git a/src/jobservice/utils/utils.go b/src/jobservice/utils/utils.go index ce6b775f5..1c4f50af6 100644 --- a/src/jobservice/utils/utils.go +++ b/src/jobservice/utils/utils.go @@ -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 { diff --git a/src/ui/api/repository.go b/src/ui/api/repository.go index 038824d6f..3abfa20b8 100644 --- a/src/ui/api/repository.go +++ b/src/ui/api/repository.go @@ -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) } diff --git a/src/ui/api/search.go b/src/ui/api/search.go index a17be9a31..bf260edac 100644 --- a/src/ui/api/search.go +++ b/src/ui/api/search.go @@ -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 } diff --git a/src/ui/api/target.go b/src/ui/api/target.go index eaa0c3f98..6d27f5b23 100644 --- a/src/ui/api/target.go +++ b/src/ui/api/target.go @@ -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 ... diff --git a/src/ui/api/utils.go b/src/ui/api/utils.go index a639057f9..35a7b8aa1 100644 --- a/src/ui/api/utils.go +++ b/src/ui/api/utils.go @@ -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{} diff --git a/src/ui/proxy/interceptors.go b/src/ui/proxy/interceptors.go index 61518f053..3b0020244 100644 --- a/src/ui/proxy/interceptors.go +++ b/src/ui/proxy/interceptors.go @@ -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. diff --git a/src/ui/service/token/authutils.go b/src/ui/service/token/authutils.go index 4544818f6..dbdeb6943 100644 --- a/src/ui/service/token/authutils.go +++ b/src/ui/service/token/authutils.go @@ -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) diff --git a/src/ui/service/token/creator.go b/src/ui/service/token/creator.go index 359398bb9..0db22bc4d 100644 --- a/src/ui/service/token/creator.go +++ b/src/ui/service/token/creator.go @@ -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, } } diff --git a/src/ui/utils/utils.go b/src/ui/utils/utils.go index 025c452d5..3ad5f4f3f 100644 --- a/src/ui/utils/utils.go +++ b/src/ui/utils/utils.go @@ -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) } From 0a74a0f1e4a036d4bd9369a9afcc9c9fe17b573d Mon Sep 17 00:00:00 2001 From: Wenkai Yin Date: Wed, 26 Jul 2017 18:53:23 +0800 Subject: [PATCH 2/5] update --- .../utils/registry/auth/tokenauthorizer.go | 84 +++++++++---------- src/ui/config/config.go | 1 + src/ui/proxy/interceptor_test.go | 8 +- 3 files changed, 48 insertions(+), 45 deletions(-) diff --git a/src/common/utils/registry/auth/tokenauthorizer.go b/src/common/utils/registry/auth/tokenauthorizer.go index 905bc1487..8c57ee41b 100644 --- a/src/common/utils/registry/auth/tokenauthorizer.go +++ b/src/common/utils/registry/auth/tokenauthorizer.go @@ -45,15 +45,13 @@ func (s *Scope) string() string { } type tokenGenerator interface { - generate(realm, service string, scopes []*Scope) (*models.Token, error) + generate(scopes []*Scope, endpoint string) (*models.Token, error) } // 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 { - realm string - service string registryURL *url.URL // used to filter request generator tokenGenerator client *http.Client @@ -90,23 +88,13 @@ func (t *tokenAuthorizer) Modify(req *http.Request) error { // 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) + token, err = t.generator.generate(scopes, t.registryURL.String()) if err != nil { return err } + if token == nil { + return nil + } // only cache the token for empty scope(login) or single scope request if len(scopes) <= 1 { key := "" @@ -253,13 +241,14 @@ func ping(client *http.Client, endpoint string) (string, string, error) { return realm, service, nil } } - return "", "", fmt.Errorf("schemes %v are unsupportted", challenges) + + log.Warningf("schemes %v are unsupportted", challenges) + return "", "", nil } // NewStandardTokenAuthorizer returns a standard token authorizer. The authorizer will request a token // from token server and add it to the origin request // 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, customizedTokenService ...string) registry.Modifier { client := &http.Client{ @@ -271,8 +260,16 @@ func NewStandardTokenAuthorizer(credential Credential, insecure bool, credential: credential, client: client, } + + // when the registry client is used inside Harbor, the token request + // can be posted to token service directly rather than going through nginx. + // If realm is set as the internal url of token service, this can resolve + // two problems: + // 1. performance issue + // 2. the realm field returned by registry is an IP which can not reachable + // inside Harbor if len(customizedTokenService) > 0 { - generator.customizedTokenService = customizedTokenService[0] + generator.realm = customizedTokenService[0] } return &tokenAuthorizer{ @@ -284,40 +281,42 @@ func NewStandardTokenAuthorizer(credential Credential, insecure bool, // standardTokenGenerator implements interface tokenGenerator type standardTokenGenerator struct { - credential Credential - customizedTokenService string - client *http.Client + realm string + service string + credential Credential + client *http.Client } // get token from token service -func (s *standardTokenGenerator) generate(realm, service string, scopes []*Scope) (*models.Token, error) { - realm = s.tokenURL(realm) - return getToken(s.client, s.credential, realm, service, scopes) -} - -// when the registry client is used inside Harbor, the token request -// can be posted to token service directly rather than going through nginx. -// If realm is set as the internal url of token service, this can resolve -// two problems: -// 1. performance issue -// 2. the realm field returned by registry is an IP which can not reachable -// inside Harbor -func (s *standardTokenGenerator) tokenURL(realm string) string { - if len(s.customizedTokenService) != 0 { - return s.customizedTokenService +func (s *standardTokenGenerator) generate(scopes []*Scope, endpoint string) (*models.Token, error) { + // ping first if the realm or service is null + if len(s.realm) == 0 || len(s.service) == 0 { + realm, service, err := ping(s.client, endpoint) + if err != nil { + return nil, err + } + if len(realm) == 0 { + log.Warning("empty realm, skip") + return nil, nil + } + if len(s.realm) == 0 { + s.realm = realm + } + s.service = service } - return realm + + return getToken(s.client, s.credential, s.realm, s.service, scopes) } // NewRawTokenAuthorizer returns a token authorizer which calls method to create // token directly func NewRawTokenAuthorizer(username, service string) registry.Modifier { generator := &rawTokenGenerator{ + service: service, username: username, } return &tokenAuthorizer{ - service: service, cachedTokens: make(map[string]*models.Token), generator: generator, } @@ -325,16 +324,17 @@ func NewRawTokenAuthorizer(username, service string) registry.Modifier { // rawTokenGenerator implements interface tokenGenerator type rawTokenGenerator struct { + service string username string } // generate token directly -func (r *rawTokenGenerator) generate(realm, service string, scopes []*Scope) (*models.Token, error) { +func (r *rawTokenGenerator) generate(scopes []*Scope, endpoint string) (*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) + token, expiresIn, issuedAt, err := token_util.RegistryTokenForUI(r.username, r.service, strs) if err != nil { return nil, err } diff --git a/src/ui/config/config.go b/src/ui/config/config.go index 9abb1462f..ca37b80d6 100644 --- a/src/ui/config/config.go +++ b/src/ui/config/config.go @@ -191,6 +191,7 @@ func TokenExpiration() (int, error) { if err != nil { return 0, err } + return int(cfg[common.TokenExpiration].(float64)), nil } diff --git a/src/ui/proxy/interceptor_test.go b/src/ui/proxy/interceptor_test.go index d938421c0..f7406652d 100644 --- a/src/ui/proxy/interceptor_test.go +++ b/src/ui/proxy/interceptor_test.go @@ -30,9 +30,10 @@ func TestMain(m *testing.M) { defer notaryServer.Close() NotaryEndpoint = notaryServer.URL var defaultConfig = map[string]interface{}{ - common.ExtEndpoint: "https://" + endpoint, - common.WithNotary: true, - common.CfgExpiration: 5, + common.ExtEndpoint: "https://" + endpoint, + common.WithNotary: true, + common.CfgExpiration: 5, + common.TokenExpiration: 30, } adminServer, err := utilstest.NewAdminserver(defaultConfig) if err != nil { @@ -117,6 +118,7 @@ func TestPMSPolicyChecker(t *testing.T) { common.WithNotary: true, common.CfgExpiration: 5, common.AdmiralEndpoint: admiralEndpoint, + common.TokenExpiration: 30, } adminServer, err := utilstest.NewAdminserver(defaultConfigAdmiral) if err != nil { From 1da9b8653bea54ffc51c00c071ee011c062ed9df Mon Sep 17 00:00:00 2001 From: Wenkai Yin Date: Thu, 27 Jul 2017 18:02:29 +0800 Subject: [PATCH 3/5] update according to the comments --- src/common/utils/registry/auth/path.go | 3 +++ .../utils/registry/auth/tokenauthorizer.go | 25 ++++++++++++------- .../registry/auth/tokenauthorizer_test.go | 18 ++++++++++--- 3 files changed, 33 insertions(+), 13 deletions(-) diff --git a/src/common/utils/registry/auth/path.go b/src/common/utils/registry/auth/path.go index 954847dac..9801d3414 100644 --- a/src/common/utils/registry/auth/path.go +++ b/src/common/utils/registry/auth/path.go @@ -45,6 +45,9 @@ func parseRepository(path string) string { } // match + // the subs should contain at least 2 matching texts, the first one matches + // the whole regular expression, and the second one matches the repository + // part if len(subs) < 2 { log.Warningf("unexpected length of sub matches: %d, should >= 2 ", len(subs)) continue diff --git a/src/common/utils/registry/auth/tokenauthorizer.go b/src/common/utils/registry/auth/tokenauthorizer.go index 8c57ee41b..f54eee707 100644 --- a/src/common/utils/registry/auth/tokenauthorizer.go +++ b/src/common/utils/registry/auth/tokenauthorizer.go @@ -56,7 +56,7 @@ type tokenAuthorizer struct { generator tokenGenerator client *http.Client cachedTokens map[string]*models.Token - sync.RWMutex + sync.Mutex } // add token to the request @@ -73,7 +73,10 @@ func (t *tokenAuthorizer) Modify(req *http.Request) error { } // parse scopes from request - scopes := parseScopes(req) + scopes, err := parseScopes(req) + if err != nil { + return err + } var token *models.Token // try to get token from cache if the request is for empty scope(login) @@ -92,6 +95,8 @@ func (t *tokenAuthorizer) Modify(req *http.Request) error { if err != nil { return err } + // if the token is null(this happens if the registry needs no authentication), return + // directly. Or the token will be cached if token == nil { return nil } @@ -137,7 +142,7 @@ func (t *tokenAuthorizer) filterReq(req *http.Request) (bool, error) { } // parse scopes from the request according to its method, path and query string -func parseScopes(req *http.Request) []*Scope { +func parseScopes(req *http.Request) ([]*Scope, error) { scopes := []*Scope{} from := req.URL.Query().Get("from") @@ -159,7 +164,7 @@ func parseScopes(req *http.Request) []*Scope { Name: repository, } switch req.Method { - case http.MethodGet: + case http.MethodGet, http.MethodHead: scope.Actions = []string{"pull"} case http.MethodPost, http.MethodPut, http.MethodPatch: scope.Actions = []string{"push"} @@ -181,7 +186,7 @@ func parseScopes(req *http.Request) []*Scope { scope = nil } else { // unknow - log.Warningf("can not parse scope from the request: %s %s", req.Method, req.URL.Path) + return scopes, fmt.Errorf("can not parse scope from the request: %s %s", req.Method, req.URL.Path) } if scope != nil { @@ -194,12 +199,12 @@ func parseScopes(req *http.Request) []*Scope { } log.Debugf("scopses parsed from request: %s", strings.Join(strs, " ")) - return scopes + return scopes, nil } func (t *tokenAuthorizer) getCachedToken(scope string) *models.Token { - t.RLock() - defer t.RUnlock() + t.Lock() + defer t.Unlock() token := t.cachedTokens[scope] if token == nil { return nil @@ -208,14 +213,16 @@ func (t *tokenAuthorizer) getCachedToken(scope string) *models.Token { issueAt, err := time.Parse(time.RFC3339, token.IssuedAt) if err != nil { log.Errorf("failed parse %s: %v", token.IssuedAt, err) + delete(t.cachedTokens, scope) return nil } if issueAt.Add(time.Duration(token.ExpiresIn-latency) * time.Second).Before(time.Now().UTC()) { + delete(t.cachedTokens, scope) return nil } - log.Debug("get token from cache") + log.Debugf("get token for scope %s from cache", scope) return token } diff --git a/src/common/utils/registry/auth/tokenauthorizer_test.go b/src/common/utils/registry/auth/tokenauthorizer_test.go index 751fa1893..d3614f316 100644 --- a/src/common/utils/registry/auth/tokenauthorizer_test.go +++ b/src/common/utils/registry/auth/tokenauthorizer_test.go @@ -78,7 +78,8 @@ 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) + scopses, err := parseScopes(req) + assert.Nil(t, err) assert.Equal(t, 1, len(scopses)) assert.EqualValues(t, &Scope{ Type: "repository", @@ -90,13 +91,15 @@ func TestParseScopes(t *testing.T) { // v2 req, err = http.NewRequest(http.MethodGet, "http://registry/v2", nil) require.Nil(t, err) - scopses = parseScopes(req) + scopses, err = parseScopes(req) + assert.Nil(t, err) 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) + scopses, err = parseScopes(req) + assert.Nil(t, err) assert.Equal(t, 1, len(scopses)) assert.EqualValues(t, &Scope{ Type: "registry", @@ -108,7 +111,8 @@ func TestParseScopes(t *testing.T) { // manifest req, err = http.NewRequest(http.MethodPut, "http://registry/v2/library/mysql/5.6/manifests/1", nil) require.Nil(t, err) - scopses = parseScopes(req) + scopses, err = parseScopes(req) + assert.Nil(t, err) assert.Equal(t, 1, len(scopses)) assert.EqualValues(t, &Scope{ Type: "repository", @@ -116,6 +120,12 @@ func TestParseScopes(t *testing.T) { Actions: []string{ "push"}, }, scopses[0]) + + // invalid + req, err = http.NewRequest(http.MethodPut, "http://registry/other", nil) + require.Nil(t, err) + scopses, err = parseScopes(req) + assert.NotNil(t, err) } func TestGetAndUpdateCachedToken(t *testing.T) { From a8dc75dd15fafc56b21d73078c9180e582424b3a Mon Sep 17 00:00:00 2001 From: Wenkai Yin Date: Fri, 28 Jul 2017 12:59:23 +0800 Subject: [PATCH 4/5] update --- src/common/utils/notary/helper.go | 31 ++++++++-- .../utils/registry/auth/tokenauthorizer.go | 58 +++++++------------ .../registry/auth/tokenauthorizer_test.go | 7 ++- src/common/utils/registry/auth/util.go | 8 +-- src/ui/service/token/authutils.go | 41 +++---------- src/ui/service/token/creator.go | 2 +- src/ui/service/token/token_test.go | 2 +- src/ui/utils/utils.go | 3 +- 8 files changed, 68 insertions(+), 84 deletions(-) diff --git a/src/common/utils/notary/helper.go b/src/common/utils/notary/helper.go index b6e1abb1c..772ba2061 100644 --- a/src/common/utils/notary/helper.go +++ b/src/common/utils/notary/helper.go @@ -17,19 +17,20 @@ package notary import ( "encoding/hex" "fmt" + "net/http" "os" "path" "strings" + "github.com/docker/distribution/registry/auth/token" "github.com/docker/notary" "github.com/docker/notary/client" "github.com/docker/notary/trustpinning" "github.com/docker/notary/tuf/data" "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" "github.com/vmware/harbor/src/ui/config" - "github.com/vmware/harbor/src/ui/service/token" + tokenutil "github.com/vmware/harbor/src/ui/service/token" "github.com/opencontainers/go-digest" ) @@ -72,10 +73,22 @@ func GetInternalTargets(notaryEndpoint string, username string, repo string) ([] // GetTargets is a help function called by API to fetch signature information of a given repository. // Per docker's convention the repository should contain the information of endpoint, i.e. it should look -// like "10.117.4.117/library/ubuntu", instead of "library/ubuntu" (fqRepo for fully-qualified repo) +// like "192.168.0.1/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.NewRawTokenAuthorizer(username, token.Notary) + t, err := tokenutil.MakeToken(username, tokenutil.Notary, + []*token.ResourceActions{ + &token.ResourceActions{ + Type: "repository", + Name: fqRepo, + Actions: []string{"pull"}, + }}) + if err != nil { + return nil, err + } + authorizer := ¬aryAuthorizer{ + token: t.Token, + } tr := registry.NewTransport(registry.GetHTTPTransport(true), authorizer) gun := data.GUN(fqRepo) notaryRepo, err := client.NewFileCachedNotaryRepository(notaryCachePath, gun, notaryEndpoint, tr, mockRetriever, trustPin) @@ -109,3 +122,13 @@ func DigestFromTarget(t Target) (string, error) { } return digest.NewDigestFromHex("sha256", hex.EncodeToString(sha)).String(), nil } + +type notaryAuthorizer struct { + token string +} + +func (n *notaryAuthorizer) Modify(req *http.Request) error { + req.Header.Add(http.CanonicalHeaderKey("Authorization"), + fmt.Sprintf("Bearer %s", n.token)) + return nil +} diff --git a/src/common/utils/registry/auth/tokenauthorizer.go b/src/common/utils/registry/auth/tokenauthorizer.go index f54eee707..dee4a5640 100644 --- a/src/common/utils/registry/auth/tokenauthorizer.go +++ b/src/common/utils/registry/auth/tokenauthorizer.go @@ -22,6 +22,7 @@ import ( "sync" "time" + "github.com/docker/distribution/registry/auth/token" "github.com/vmware/harbor/src/common/models" "github.com/vmware/harbor/src/common/utils/log" "github.com/vmware/harbor/src/common/utils/registry" @@ -33,19 +34,8 @@ const ( scheme = "bearer" ) -// Scope ... -type Scope struct { - Type string - Name string - Actions []string -} - -func (s *Scope) string() string { - return fmt.Sprintf("%s:%s:%s", s.Type, s.Name, strings.Join(s.Actions, ",")) -} - type tokenGenerator interface { - generate(scopes []*Scope, endpoint string) (*models.Token, error) + generate(scopes []*token.ResourceActions, endpoint string) (*models.Token, error) } // tokenAuthorizer implements registry.Modifier interface. It parses scopses @@ -84,7 +74,7 @@ func (t *tokenAuthorizer) Modify(req *http.Request) error { if len(scopes) <= 1 { key := "" if len(scopes) == 1 { - key = scopes[0].string() + key = scopeString(scopes[0]) } token = t.getCachedToken(key) } @@ -104,7 +94,7 @@ func (t *tokenAuthorizer) Modify(req *http.Request) error { if len(scopes) <= 1 { key := "" if len(scopes) == 1 { - key = scopes[0].string() + key = scopeString(scopes[0]) } t.updateCachedToken(key, token) } @@ -115,6 +105,13 @@ func (t *tokenAuthorizer) Modify(req *http.Request) error { return nil } +func scopeString(scope *token.ResourceActions) string { + if scope == nil { + return "" + } + return fmt.Sprintf("%s:%s:%s", scope.Type, scope.Name, strings.Join(scope.Actions, ",")) +} + // 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) { @@ -142,24 +139,24 @@ func (t *tokenAuthorizer) filterReq(req *http.Request) (bool, error) { } // parse scopes from the request according to its method, path and query string -func parseScopes(req *http.Request) ([]*Scope, error) { - scopes := []*Scope{} +func parseScopes(req *http.Request) ([]*token.ResourceActions, error) { + scopes := []*token.ResourceActions{} from := req.URL.Query().Get("from") if len(from) != 0 { - scopes = append(scopes, &Scope{ + scopes = append(scopes, &token.ResourceActions{ Type: "repository", Name: from, Actions: []string{"pull"}, }) } - var scope *Scope + var scope *token.ResourceActions path := strings.TrimRight(req.URL.Path, "/") repository := parseRepository(path) if len(repository) > 0 { // pull, push, delete blob/manifest - scope = &Scope{ + scope = &token.ResourceActions{ Type: "repository", Name: repository, } @@ -176,7 +173,7 @@ func parseScopes(req *http.Request) ([]*Scope, error) { } } else if catalog.MatchString(path) { // catalog - scope = &Scope{ + scope = &token.ResourceActions{ Type: "registry", Name: "catalog", Actions: []string{"*"}, @@ -195,7 +192,7 @@ func parseScopes(req *http.Request) ([]*Scope, error) { strs := []string{} for _, s := range scopes { - strs = append(strs, s.string()) + strs = append(strs, scopeString(s)) } log.Debugf("scopses parsed from request: %s", strings.Join(strs, " ")) @@ -295,7 +292,7 @@ type standardTokenGenerator struct { } // get token from token service -func (s *standardTokenGenerator) generate(scopes []*Scope, endpoint string) (*models.Token, error) { +func (s *standardTokenGenerator) generate(scopes []*token.ResourceActions, endpoint string) (*models.Token, error) { // ping first if the realm or service is null if len(s.realm) == 0 || len(s.service) == 0 { realm, service, err := ping(s.client, endpoint) @@ -336,21 +333,8 @@ type rawTokenGenerator struct { } // generate token directly -func (r *rawTokenGenerator) generate(scopes []*Scope, endpoint string) (*models.Token, error) { - strs := []string{} - for _, scope := range scopes { - strs = append(strs, scope.string()) - } - token, expiresIn, issuedAt, err := token_util.RegistryTokenForUI(r.username, r.service, strs) - if err != nil { - return nil, err - } - - return &models.Token{ - Token: token, - ExpiresIn: expiresIn, - IssuedAt: issuedAt.Format(time.RFC3339), - }, nil +func (r *rawTokenGenerator) generate(scopes []*token.ResourceActions, endpoint string) (*models.Token, error) { + return token_util.MakeToken(r.username, r.service, scopes) } func buildPingURL(endpoint string) string { diff --git a/src/common/utils/registry/auth/tokenauthorizer_test.go b/src/common/utils/registry/auth/tokenauthorizer_test.go index d3614f316..5cdcc6e48 100644 --- a/src/common/utils/registry/auth/tokenauthorizer_test.go +++ b/src/common/utils/registry/auth/tokenauthorizer_test.go @@ -22,6 +22,7 @@ import ( "testing" "time" + "github.com/docker/distribution/registry/auth/token" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/vmware/harbor/src/common/models" @@ -81,7 +82,7 @@ func TestParseScopes(t *testing.T) { scopses, err := parseScopes(req) assert.Nil(t, err) assert.Equal(t, 1, len(scopses)) - assert.EqualValues(t, &Scope{ + assert.EqualValues(t, &token.ResourceActions{ Type: "repository", Name: "library", Actions: []string{ @@ -101,7 +102,7 @@ func TestParseScopes(t *testing.T) { scopses, err = parseScopes(req) assert.Nil(t, err) assert.Equal(t, 1, len(scopses)) - assert.EqualValues(t, &Scope{ + assert.EqualValues(t, &token.ResourceActions{ Type: "registry", Name: "catalog", Actions: []string{ @@ -114,7 +115,7 @@ func TestParseScopes(t *testing.T) { scopses, err = parseScopes(req) assert.Nil(t, err) assert.Equal(t, 1, len(scopses)) - assert.EqualValues(t, &Scope{ + assert.EqualValues(t, &token.ResourceActions{ Type: "repository", Name: "library/mysql/5.6", Actions: []string{ diff --git a/src/common/utils/registry/auth/util.go b/src/common/utils/registry/auth/util.go index 47d5ac963..30c9bd1db 100644 --- a/src/common/utils/registry/auth/util.go +++ b/src/common/utils/registry/auth/util.go @@ -20,8 +20,8 @@ import ( "net/http" "net/url" + "github.com/docker/distribution/registry/auth/token" "github.com/vmware/harbor/src/common/models" - registry_error "github.com/vmware/harbor/src/common/utils/error" "github.com/vmware/harbor/src/common/utils/registry" ) @@ -32,7 +32,7 @@ const ( // GetToken requests a token against the endpoint using credetial provided func GetToken(endpoint string, insecure bool, credential Credential, - scopes []*Scope) (*models.Token, error) { + scopes []*token.ResourceActions) (*models.Token, error) { client := &http.Client{ Transport: registry.GetHTTPTransport(insecure), } @@ -41,7 +41,7 @@ func GetToken(endpoint string, insecure bool, credential Credential, } func getToken(client *http.Client, credential Credential, realm, service string, - scopes []*Scope) (*models.Token, error) { + scopes []*token.ResourceActions) (*models.Token, error) { u, err := url.Parse(realm) if err != nil { return nil, err @@ -49,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.string()) + query.Add("scope", scopeString(scope)) } u.RawQuery = query.Encode() diff --git a/src/ui/service/token/authutils.go b/src/ui/service/token/authutils.go index dbdeb6943..f1c47ef28 100644 --- a/src/ui/service/token/authutils.go +++ b/src/ui/service/token/authutils.go @@ -93,51 +93,26 @@ func filterAccess(access []*token.ResourceActions, ctx security.Context, return nil } -//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) -} - -//NotaryTokenForUI calls genTokenForUI to get raw token for notary -func NotaryTokenForUI(username string, service string, scopes []string) (string, int, *time.Time, error) { - return genTokenForUI(username, service, scopes) -} - -// genTokenForUI is for the UI process to call, so it won't establish a https connection from UI to proxy. -func genTokenForUI(username string, service string, - scopes []string) (string, int, *time.Time, error) { - access := GetResourceActions(scopes) - return MakeRawToken(username, service, access) -} - -// MakeRawToken makes a valid jwt token based on parms. -func MakeRawToken(username, service string, access []*token.ResourceActions) (token string, expiresIn int, issuedAt *time.Time, err error) { +// MakeToken makes a valid jwt token based on parms. +func MakeToken(username, service string, access []*token.ResourceActions) (*models.Token, error) { pk, err := libtrust.LoadKeyFile(privateKey) if err != nil { - return "", 0, nil, err + return nil, err } expiration, err := config.TokenExpiration() if err != nil { - return "", 0, nil, err + return nil, err } tk, expiresIn, issuedAt, err := makeTokenCore(issuer, username, service, expiration, access, pk) - if err != nil { - return "", 0, nil, err - } - rs := fmt.Sprintf("%s.%s", tk.Raw, base64UrlEncode(tk.Signature)) - return rs, expiresIn, issuedAt, nil -} - -func makeToken(username, service string, access []*token.ResourceActions) (*models.Token, error) { - raw, expires, issued, err := MakeRawToken(username, service, access) if err != nil { return nil, err } + rs := fmt.Sprintf("%s.%s", tk.Raw, base64UrlEncode(tk.Signature)) return &models.Token{ - Token: raw, - ExpiresIn: expires, - IssuedAt: issued.Format(time.RFC3339), + Token: rs, + ExpiresIn: expiresIn, + IssuedAt: issuedAt.Format(time.RFC3339), }, nil } diff --git a/src/ui/service/token/creator.go b/src/ui/service/token/creator.go index 0db22bc4d..ee1b628b5 100644 --- a/src/ui/service/token/creator.go +++ b/src/ui/service/token/creator.go @@ -202,7 +202,7 @@ func (g generalCreator) Create(r *http.Request) (*models.Token, error) { if err != nil { return nil, err } - return makeToken(ctx.GetUsername(), g.service, access) + return MakeToken(ctx.GetUsername(), g.service, access) } func parseScopes(u *url.URL) []string { diff --git a/src/ui/service/token/token_test.go b/src/ui/service/token/token_test.go index 50b86e3ce..18d48be21 100644 --- a/src/ui/service/token/token_test.go +++ b/src/ui/service/token/token_test.go @@ -111,7 +111,7 @@ func TestMakeToken(t *testing.T) { }} svc := "harbor-registry" u := "tester" - tokenJSON, err := makeToken(u, svc, ra) + tokenJSON, err := MakeToken(u, svc, ra) if err != nil { t.Errorf("Error while making token: %v", err) } diff --git a/src/ui/utils/utils.go b/src/ui/utils/utils.go index 7a20f6a52..5f7858bca 100644 --- a/src/ui/utils/utils.go +++ b/src/ui/utils/utils.go @@ -122,7 +122,8 @@ func TriggerImageScan(repository string, tag string) error { return RequestAsUI("POST", url, bytes.NewBuffer(b), NewStatusRespHandler(http.StatusOK)) } -// NewRepositoryClientForUI ... +// NewRepositoryClientForUI creates a repository client that can only be used to +// access the internal registry func NewRepositoryClientForUI(username, repository string) (*registry.Repository, error) { endpoint, err := config.RegistryURL() if err != nil { From eb9a4dfff9e043d4119d142250cbbee12e1bfefb Mon Sep 17 00:00:00 2001 From: Wenkai Yin Date: Fri, 28 Jul 2017 13:21:34 +0800 Subject: [PATCH 5/5] update --- src/jobservice/utils/utils.go | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/src/jobservice/utils/utils.go b/src/jobservice/utils/utils.go index 1c4f50af6..303572fa8 100644 --- a/src/jobservice/utils/utils.go +++ b/src/jobservice/utils/utils.go @@ -18,6 +18,7 @@ import ( "fmt" "net/http" + "github.com/docker/distribution/registry/auth/token" "github.com/vmware/harbor/src/common/models" "github.com/vmware/harbor/src/common/utils/registry" "github.com/vmware/harbor/src/common/utils/registry/auth" @@ -56,14 +57,15 @@ func BuildBlobURL(endpoint, repository, digest string) string { func GetTokenForRepo(repository string) (string, error) { c := &http.Cookie{Name: models.UISecretCookie, Value: config.JobserviceSecret()} credentail := auth.NewCookieCredential(c) - token, err := auth.GetToken(config.InternalTokenServiceEndpoint(), true, credentail, []*auth.Scope{&auth.Scope{ - Type: "repository", - Name: repository, - Actions: []string{"pull"}, - }}) + t, err := auth.GetToken(config.InternalTokenServiceEndpoint(), true, credentail, + []*token.ResourceActions{&token.ResourceActions{ + Type: "repository", + Name: repository, + Actions: []string{"pull"}, + }}) if err != nil { return "", err } - return token.Token, nil + return t.Token, nil }