diff --git a/src/common/utils/notary/helper.go b/src/common/utils/notary/helper.go index 60b7dbb89a..772ba20619 100644 --- a/src/common/utils/notary/helper.go +++ b/src/common/utils/notary/helper.go @@ -17,18 +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" + tokenutil "github.com/vmware/harbor/src/ui/service/token" "github.com/opencontainers/go-digest" ) @@ -71,15 +73,23 @@ 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.NewNotaryUsernameTokenAuthorizer(username, "repository", fqRepo, "pull") - store, err := auth.NewAuthorizerStore(strings.Split(notaryEndpoint, "//")[1], true, authorizer) + t, err := tokenutil.MakeToken(username, tokenutil.Notary, + []*token.ResourceActions{ + &token.ResourceActions{ + Type: "repository", + Name: fqRepo, + Actions: []string{"pull"}, + }}) if err != nil { - return res, err + return nil, err } - tr := registry.NewTransport(registry.GetHTTPTransport(true), store) + 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) if err != nil { @@ -112,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/notary/helper_test.go b/src/common/utils/notary/helper_test.go index 83988d8a6a..ee8065e013 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 63476bdfac..0000000000 --- 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 3b368e143d..0000000000 --- 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 0000000000..9801d3414d --- /dev/null +++ b/src/common/utils/registry/auth/path.go @@ -0,0 +1,59 @@ +// 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 + // 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 + } + 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 0000000000..995af6f7db --- /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 3bbc74f14c..dee4a56404 100644 --- a/src/common/utils/registry/auth/tokenauthorizer.go +++ b/src/common/utils/registry/auth/tokenauthorizer.go @@ -17,232 +17,326 @@ package auth import ( "fmt" "net/http" + "net/url" "strings" "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" 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 ... -type Scope struct { - Type string - Name string - Actions []string +type tokenGenerator interface { + generate(scopes []*token.ResourceActions, endpoint string) (*models.Token, error) } -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) - -// 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 + registryURL *url.URL // used to filter request + generator tokenGenerator + client *http.Client + cachedTokens map[string]*models.Token sync.Mutex } -// 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, 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) + // or single scope + if len(scopes) <= 1 { + key := "" + if len(scopes) == 1 { + key = scopeString(scopes[0]) } - 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()) - } - to, expiresIn, _, err := t.tg(params["realm"], params["service"], scopeStrs) + // request a new token if the token is null + if token == nil { + token, err = t.generator.generate(scopes, t.registryURL.String()) if err != nil { return err } - token = to - - if !hasFrom { - t.updateCachedToken(to, expiresIn) + // 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 + } + // only cache the token for empty scope(login) or single scope request + if len(scopes) <= 1 { + key := "" + if len(scopes) == 1 { + key = scopeString(scopes[0]) + } + 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 +func scopeString(scope *token.ResourceActions) string { + if scope == nil { + return "" + } + return fmt.Sprintf("%s:%s:%s", scope.Type, scope.Name, strings.Join(scope.Actions, ",")) } -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 +// 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 } -// Implements interface Authorizer -type standardTokenAuthorizer struct { - tokenAuthorizer - client *http.Client - credential Credential - tokenServiceEndpoint string +// parse scopes from the request according to its method, path and query string +func parseScopes(req *http.Request) ([]*token.ResourceActions, error) { + scopes := []*token.ResourceActions{} + + from := req.URL.Query().Get("from") + if len(from) != 0 { + scopes = append(scopes, &token.ResourceActions{ + Type: "repository", + Name: from, + Actions: []string{"pull"}, + }) + } + + var scope *token.ResourceActions + path := strings.TrimRight(req.URL.Path, "/") + repository := parseRepository(path) + if len(repository) > 0 { + // pull, push, delete blob/manifest + scope = &token.ResourceActions{ + Type: "repository", + Name: repository, + } + switch req.Method { + case http.MethodGet, http.MethodHead: + 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 = &token.ResourceActions{ + Type: "registry", + Name: "catalog", + Actions: []string{"*"}, + } + } else if base.MatchString(path) { + // base + scope = nil + } else { + // unknow + return scopes, fmt.Errorf("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, scopeString(s)) + } + log.Debugf("scopses parsed from request: %s", strings.Join(strs, " ")) + + return scopes, nil +} + +func (t *tokenAuthorizer) getCachedToken(scope string) *models.Token { + t.Lock() + defer t.Unlock() + 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) + 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.Debugf("get token for scope %s from cache", scope) + 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 + } + } + + 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 tokenServiceEndpoint 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 +// If customizedTokenService is set, the token request will be sent to it instead of the server get from authorizer 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, + } + + // 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.realm = customizedTokenService[0] + } + + return &tokenAuthorizer{ + cachedTokens: make(map[string]*models.Token), + generator: generator, + client: client, + } +} + +// standardTokenGenerator implements interface tokenGenerator +type standardTokenGenerator struct { + realm string + service string + credential Credential + client *http.Client +} + +// get token from token service +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) + 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 } - authorizer.tg = authorizer.generateToken - - return authorizer + return getToken(s.client, s.credential, s.realm, s.service, scopes) } -func (s *standardTokenAuthorizer) generateToken(realm, service string, scopes []string) (string, int, *time.Time, 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 -} - -// 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 *standardTokenAuthorizer) tokenURL(realm string) string { - if len(s.tokenServiceEndpoint) != 0 { - return s.tokenServiceEndpoint - } - 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{ + service: service, username: username, } - authorizer.scope = &Scope{ - Type: scopeType, - Name: scopeName, - Actions: scopeActions, + return &tokenAuthorizer{ + 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 { + service string + 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(scopes []*token.ResourceActions, endpoint string) (*models.Token, error) { + return token_util.MakeToken(r.username, r.service, scopes) } -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 c93a358024..5cdcc6e486 100644 --- a/src/common/utils/registry/auth/tokenauthorizer_test.go +++ b/src/common/utils/registry/auth/tokenauthorizer_test.go @@ -15,54 +15,195 @@ package auth import ( + "encoding/json" + "fmt" "net/http" + "strings" "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" "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, err := parseScopes(req) + assert.Nil(t, err) + assert.Equal(t, 1, len(scopses)) + assert.EqualValues(t, &token.ResourceActions{ + 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, 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, err = parseScopes(req) + assert.Nil(t, err) + assert.Equal(t, 1, len(scopses)) + assert.EqualValues(t, &token.ResourceActions{ + 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, err = parseScopes(req) + assert.Nil(t, err) + assert.Equal(t, 1, len(scopses)) + assert.EqualValues(t, &token.ResourceActions{ + Type: "repository", + Name: "library/mysql/5.6", + 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) { + 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 373c95890d..30c9bd1dbc 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,21 +32,16 @@ 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), } - 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 []*token.ResourceActions) (*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", scopeString(scope)) } u.RawQuery = query.Encode() diff --git a/src/jobservice/api/scan.go b/src/jobservice/api/scan.go index 2145908819..2a7b986eb5 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 dadf6a11d2..5fae137aa6 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 66f29b2f49..cee15a2b6b 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 ce6b775f52..303572fa8d 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" @@ -26,24 +27,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 { @@ -65,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 } diff --git a/src/ui/api/repository.go b/src/ui/api/repository.go index a730d302cd..8e691c0d1a 100644 --- a/src/ui/api/repository.go +++ b/src/ui/api/repository.go @@ -190,7 +190,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") @@ -306,7 +306,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)) @@ -355,7 +355,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") @@ -496,7 +496,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") @@ -558,16 +558,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) @@ -808,7 +798,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 a17be9a31a..bf260edace 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 eaa0c3f98b..6d27f5b237 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 8473df3bfc..9378b03aa1 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 { @@ -427,24 +409,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/config/config.go b/src/ui/config/config.go index 9abb1462f6..ca37b80d6f 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 d938421c09..f7406652db 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 { diff --git a/src/ui/proxy/interceptors.go b/src/ui/proxy/interceptors.go index 61518f053b..3b00202443 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 4544818f6f..f1c47ef286 100644 --- a/src/ui/service/token/authutils.go +++ b/src/ui/service/token/authutils.go @@ -93,54 +93,26 @@ 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) -} - -//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 359398bb99..ee1b628b5e 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, } } @@ -200,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 50b86e3ced..18d48be212 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 2d324de239..5f7858bcaa 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 on if failed to submit 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) @@ -44,33 +40,28 @@ func ScanAllImages() error { } log.Infof("Scanning all images on Harbor.") - go scanRepos(repos, regURL) + go scanRepos(repos) return nil } // ScanImagesByProjectID scans all images under a projet, the whole process will move on if failed to submit any job of a single image. func ScanImagesByProjectID(id int64) error { - regURL, err := config.RegistryURL() - if err != nil { - log.Errorf("Failed to load registry url") - return err - } repos, err := dao.GetRepositoriesByProject(id, "", 0, 0) if err != nil { log.Errorf("Failed list repositories in project %d, error: %v", id, err) return err } log.Infof("Scanning all images in project: %d ", id) - go scanRepos(repos, regURL) + go scanRepos(repos) return nil } -func scanRepos(repos []*models.RepoRecord, regURL string) { +func scanRepos(repos []*models.RepoRecord) { var repoClient *registry.Repository 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 @@ -131,20 +122,15 @@ func TriggerImageScan(repository string, tag string) error { return RequestAsUI("POST", url, bytes.NewBuffer(b), NewStatusRespHandler(http.StatusOK)) } -// 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) +// 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 { 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) }