diff --git a/api/project.go b/api/project.go index 3d57abb02..da26b8a0a 100644 --- a/api/project.go +++ b/api/project.go @@ -159,7 +159,7 @@ func (p *ProjectAPI) List() { if len(isPublic) > 0 { public, err = strconv.Atoi(isPublic) if err != nil { - log.Errorf("Error parsing public property: %d, error: %v", isPublic, err) + log.Errorf("Error parsing public property: %v, error: %v", isPublic, err) p.CustomAbort(http.StatusBadRequest, "invalid project Id") } } diff --git a/api/repository.go b/api/repository.go index b64cef579..5148211db 100644 --- a/api/repository.go +++ b/api/repository.go @@ -255,11 +255,13 @@ func (ra *RepositoryAPI) GetManifests() { func (ra *RepositoryAPI) initRepositoryClient(repoName string) (r *registry.Repository, err error) { endpoint := os.Getenv("REGISTRY_URL") + // TODO read variable from config file + insecure := true username, password, ok := ra.Ctx.Request.BasicAuth() if ok { - credential := auth.NewBasicAuthCredential(username, password) - return registry.NewRepositoryWithCredentialForUI(repoName, endpoint, credential) + return newRepositoryClient(endpoint, insecure, username, password, + repoName, "repository", repoName, "pull", "push", "*") } username, err = ra.getUsername() @@ -267,7 +269,8 @@ func (ra *RepositoryAPI) initRepositoryClient(repoName string) (r *registry.Repo return nil, err } - return registry.NewRepositoryWithUsernameForUI(repoName, endpoint, username) + return cache.NewRepositoryClient(endpoint, insecure, username, repoName, + "repository", repoName, "pull", "push", "*") } func (ra *RepositoryAPI) getUsername() (string, error) { @@ -327,3 +330,21 @@ func (ra *RepositoryAPI) GetTopRepos() { ra.Data["json"] = repos ra.ServeJSON() } + +func newRepositoryClient(endpoint string, insecure bool, username, password, repository, scopeType, scopeName string, + scopeActions ...string) (*registry.Repository, error) { + + credential := auth.NewBasicAuthCredential(username, password) + authorizer := auth.NewStandardTokenAuthorizer(credential, insecure, scopeType, scopeName, scopeActions...) + + store, err := auth.NewAuthorizerStore(endpoint, authorizer) + if err != nil { + return nil, err + } + + client, err := registry.NewRepositoryWithModifiers(repository, endpoint, insecure, store) + if err != nil { + return nil, err + } + return client, nil +} diff --git a/api/target.go b/api/target.go index 089771c2b..2f1f22a6e 100644 --- a/api/target.go +++ b/api/target.go @@ -26,7 +26,7 @@ import ( "github.com/vmware/harbor/models" "github.com/vmware/harbor/utils" "github.com/vmware/harbor/utils/log" - registry_util "github.com/vmware/harbor/utils/registry" + "github.com/vmware/harbor/utils/registry" "github.com/vmware/harbor/utils/registry/auth" registry_error "github.com/vmware/harbor/utils/registry/error" ) @@ -92,8 +92,10 @@ func (t *TargetAPI) Ping() { password = t.GetString("password") } - credential := auth.NewBasicAuthCredential(username, password) - registry, err := registry_util.NewRegistryWithCredential(endpoint, credential) + // TODO read variable from config file + insecure := true + registry, err := newRegistryClient(endpoint, insecure, username, password, + "", "", "") if err != nil { // timeout, dns resolve error, connection refused, etc. if urlErr, ok := err.(*url.Error); ok { @@ -273,3 +275,20 @@ func (t *TargetAPI) Delete() { t.CustomAbort(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError)) } } + +func newRegistryClient(endpoint string, insecure bool, username, password, scopeType, scopeName string, + scopeActions ...string) (*registry.Registry, error) { + credential := auth.NewBasicAuthCredential(username, password) + authorizer := auth.NewStandardTokenAuthorizer(credential, insecure, scopeType, scopeName, scopeActions...) + + store, err := auth.NewAuthorizerStore(endpoint, authorizer) + if err != nil { + return nil, err + } + + client, err := registry.NewRegistryWithModifiers(endpoint, insecure, store) + if err != nil { + return nil, err + } + return client, nil +} diff --git a/job/replication/delete.go b/job/replication/delete.go index cc4fd0d5a..0318f361a 100644 --- a/job/replication/delete.go +++ b/job/replication/delete.go @@ -16,6 +16,7 @@ package replication import ( + "crypto/tls" "fmt" "io/ioutil" "net/http" @@ -39,17 +40,20 @@ type Deleter struct { dstUsr string // username ... dstPwd string // username ... + insecure bool + logger *log.Logger } // NewDeleter returns a Deleter -func NewDeleter(repository string, tags []string, dstURL, dstUsr, dstPwd string, logger *log.Logger) *Deleter { +func NewDeleter(repository string, tags []string, dstURL, dstUsr, dstPwd string, insecure bool, logger *log.Logger) *Deleter { deleter := &Deleter{ repository: repository, tags: tags, dstURL: dstURL, dstUsr: dstUsr, dstPwd: dstPwd, + insecure: insecure, logger: logger, } deleter.logger.Infof("initialization completed: repository: %s, tags: %v, destination URL: %s, destination user: %s", @@ -69,7 +73,7 @@ func (d *Deleter) Enter() (string, error) { // delete repository if len(d.tags) == 0 { u := url + "?repo_name=" + d.repository - if err := del(u, d.dstUsr, d.dstPwd); err != nil { + if err := del(u, d.dstUsr, d.dstPwd, d.insecure); err != nil { d.logger.Errorf("an error occurred while deleting repository %s on %s with user %s: %v", d.repository, d.dstURL, d.dstUsr, err) return "", err } @@ -82,7 +86,7 @@ func (d *Deleter) Enter() (string, error) { // delele tags for _, tag := range d.tags { u := url + "?repo_name=" + d.repository + "&tag=" + tag - if err := del(u, d.dstUsr, d.dstPwd); err != nil { + if err := del(u, d.dstUsr, d.dstPwd, d.insecure); err != nil { d.logger.Errorf("an error occurred while deleting repository %s:%s on %s with user %s: %v", d.repository, tag, d.dstURL, d.dstUsr, err) return "", err } @@ -93,14 +97,23 @@ func (d *Deleter) Enter() (string, error) { return models.JobFinished, nil } -func del(url, username, password string) error { +func del(url, username, password string, insecure bool) error { req, err := http.NewRequest("DELETE", url, nil) if err != nil { return err } req.SetBasicAuth(username, password) - resp, err := http.DefaultClient.Do(req) + + client := &http.Client{ + Transport: &http.Transport{ + TLSClientConfig: &tls.Config{ + InsecureSkipVerify: insecure, + }, + }, + } + + resp, err := client.Do(req) if err != nil { return err } diff --git a/job/replication/transfer.go b/job/replication/transfer.go index 5e63f9fcb..efce69cd8 100644 --- a/job/replication/transfer.go +++ b/job/replication/transfer.go @@ -61,6 +61,8 @@ type BaseHandler struct { dstUsr string // username ... dstPwd string // password ... + insecure bool // whether skip secure check when using https + srcClient *registry.Repository dstClient *registry.Repository @@ -75,7 +77,7 @@ type BaseHandler struct { // InitBaseHandler initializes a BaseHandler: creating clients for source and destination registry, // listing tags of the repository if parameter tags is nil. func InitBaseHandler(repository, srcURL, srcSecret, - dstURL, dstUsr, dstPwd string, tags []string, logger *log.Logger) (*BaseHandler, error) { + dstURL, dstUsr, dstPwd string, insecure bool, tags []string, logger *log.Logger) (*BaseHandler, error) { logger.Infof("initializing: repository: %s, tags: %v, source URL: %s, destination URL: %s, destination user: %s", repository, tags, srcURL, dstURL, dstUsr) @@ -96,14 +98,16 @@ func InitBaseHandler(repository, srcURL, srcSecret, c := &http.Cookie{Name: models.UISecretCookie, Value: srcSecret} srcCred := auth.NewCookieCredential(c) // srcCred := auth.NewBasicAuthCredential("admin", "Harbor12345") - srcClient, err := registry.NewRepositoryWithCredential(base.repository, base.srcURL, srcCred) + srcClient, err := newRepositoryClient(base.srcURL, base.insecure, srcCred, + base.repository, "repository", base.repository, "pull", "push", "*") if err != nil { return nil, err } base.srcClient = srcClient dstCred := auth.NewBasicAuthCredential(base.dstUsr, base.dstPwd) - dstClient, err := registry.NewRepositoryWithCredential(base.repository, base.dstURL, dstCred) + dstClient, err := newRepositoryClient(base.dstURL, base.insecure, dstCred, + base.repository, "repository", base.repository, "pull", "push", "*") if err != nil { return nil, err } @@ -416,3 +420,34 @@ func (m *ManifestPusher) Enter() (string, error) { return StatePullManifest, nil } + +func newRepositoryClient(endpoint string, insecure bool, credential auth.Credential, repository, scopeType, scopeName string, + scopeActions ...string) (*registry.Repository, error) { + + authorizer := auth.NewStandardTokenAuthorizer(credential, insecure, scopeType, scopeName, scopeActions...) + + store, err := auth.NewAuthorizerStore(endpoint, authorizer) + if err != nil { + return nil, err + } + + uam := &userAgentModifier{ + userAgent: "registry-client", + } + + client, err := registry.NewRepositoryWithModifiers(repository, endpoint, insecure, store, uam) + if err != nil { + return nil, err + } + return client, nil +} + +type userAgentModifier struct { + userAgent string +} + +// Modify adds user-agent header to the request +func (u *userAgentModifier) Modify(req *http.Request) error { + req.Header.Set(http.CanonicalHeaderKey("User-Agent"), u.userAgent) + return nil +} diff --git a/job/statemachine.go b/job/statemachine.go index 94c892a70..c26f175cb 100644 --- a/job/statemachine.go +++ b/job/statemachine.go @@ -1,16 +1,16 @@ /* - Copyright (c) 2016 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. + Copyright (c) 2016 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 job @@ -258,9 +258,12 @@ func (sm *SM) Reset(jid int64) error { } func addImgTransferTransition(sm *SM) error { + // TODO read variable from config file + insecure := true + base, err := replication.InitBaseHandler(sm.Parms.Repository, sm.Parms.LocalRegURL, config.UISecret(), sm.Parms.TargetURL, sm.Parms.TargetUsername, sm.Parms.TargetPassword, - sm.Parms.Tags, sm.Logger) + insecure, sm.Parms.Tags, sm.Logger) if err != nil { return err } @@ -274,8 +277,10 @@ func addImgTransferTransition(sm *SM) error { } func addImgDeleteTransition(sm *SM) error { + // TODO read variable from config file + insecure := true deleter := replication.NewDeleter(sm.Parms.Repository, sm.Parms.Tags, sm.Parms.TargetURL, - sm.Parms.TargetUsername, sm.Parms.TargetPassword, sm.Logger) + sm.Parms.TargetUsername, sm.Parms.TargetPassword, insecure, sm.Logger) sm.AddTransition(models.JobRunning, replication.StateDelete, deleter) sm.AddTransition(replication.StateDelete, models.JobFinished, &StatusUpdater{DummyHandler{JobID: sm.JobID}, models.JobFinished}) diff --git a/service/cache/cache.go b/service/cache/cache.go index 1fda211e1..2bdc2a266 100644 --- a/service/cache/cache.go +++ b/service/cache/cache.go @@ -16,22 +16,24 @@ package cache import ( + "errors" "os" "time" "github.com/vmware/harbor/utils/log" "github.com/vmware/harbor/utils/registry" + "github.com/vmware/harbor/utils/registry/auth" "github.com/astaxie/beego/cache" ) var ( // Cache is the global cache in system. - Cache cache.Cache - endpoint string - username string - registryClient *registry.Registry - repositoryClients map[string]*registry.Repository + Cache cache.Cache + endpoint string + insecure bool + username string + registryClient *registry.Registry ) const catalogKey string = "catalog" @@ -44,8 +46,15 @@ func init() { } endpoint = os.Getenv("REGISTRY_URL") + // TODO read variable from config file + insecure = true username = "admin" - repositoryClients = make(map[string]*registry.Repository, 10) + registryClient, err = NewRegistryClient(endpoint, insecure, username, + "registry", "catalog", "*") + if err != nil { + log.Errorf("failed to create registry client: %v", err) + return + } } // RefreshCatalogCache calls registry's API to get repository list and write it to cache. @@ -53,12 +62,7 @@ func RefreshCatalogCache() error { log.Debug("refreshing catalog cache...") if registryClient == nil { - var err error - registryClient, err = registry.NewRegistryWithUsername(endpoint, username) - if err != nil { - log.Errorf("error occurred while initializing registry client used by cache: %v", err) - return err - } + return errors.New("registry client is nil") } var err error @@ -70,15 +74,13 @@ func RefreshCatalogCache() error { repos := []string{} for _, repo := range rs { - rc, ok := repositoryClients[repo] - if !ok { - rc, err = registry.NewRepositoryWithUsernameForUI(repo, endpoint, username) - if err != nil { - log.Errorf("error occurred while initializing repository client used by cache: %s %v", repo, err) - continue - } - repositoryClients[repo] = rc + rc, err := NewRepositoryClient(endpoint, insecure, username, + repo, "repository", repo, "pull", "push", "*") + if err != nil { + log.Errorf("error occurred while initializing repository client used by cache: %s %v", repo, err) + continue } + tags, err := rc.ListTag() if err != nil { log.Errorf("error occurred while list tag for %s: %v", repo, err) @@ -112,3 +114,38 @@ func GetRepoFromCache() ([]string, error) { } return result.([]string), nil } + +// NewRegistryClient ... +func NewRegistryClient(endpoint string, insecure bool, username, scopeType, scopeName string, + scopeActions ...string) (*registry.Registry, error) { + authorizer := auth.NewUsernameTokenAuthorizer(username, scopeType, scopeName, scopeActions...) + + store, err := auth.NewAuthorizerStore(endpoint, authorizer) + if err != nil { + return nil, err + } + + client, err := registry.NewRegistryWithModifiers(endpoint, insecure, store) + if err != nil { + return nil, err + } + return client, nil +} + +// NewRepositoryClient ... +func NewRepositoryClient(endpoint string, insecure bool, username, repository, scopeType, scopeName string, + scopeActions ...string) (*registry.Repository, error) { + + authorizer := auth.NewUsernameTokenAuthorizer(username, scopeType, scopeName, scopeActions...) + + store, err := auth.NewAuthorizerStore(endpoint, authorizer) + if err != nil { + return nil, err + } + + client, err := registry.NewRepositoryWithModifiers(repository, endpoint, insecure, store) + if err != nil { + return nil, err + } + return client, nil +} diff --git a/service/notification.go b/service/notification.go index 20efcb42d..851b1a18f 100644 --- a/service/notification.go +++ b/service/notification.go @@ -25,7 +25,6 @@ import ( "github.com/vmware/harbor/models" "github.com/vmware/harbor/service/cache" "github.com/vmware/harbor/utils/log" - "github.com/vmware/harbor/utils/registry" "github.com/astaxie/beego" ) @@ -57,7 +56,7 @@ func (n *NotificationHandler) Post() { matched = false } if matched && (strings.HasPrefix(e.Request.UserAgent, "docker") || - strings.ToLower(strings.TrimSpace(e.Request.UserAgent)) == strings.ToLower(registry.UserAgent)) { + strings.ToLower(strings.TrimSpace(e.Request.UserAgent)) == "registry-client") { username = e.Actor.Name action = e.Action repo = e.Target.Repository diff --git a/utils/registry/auth/authorizer.go b/utils/registry/auth/authorizer.go index 26ea177a5..3b41e408c 100644 --- a/utils/registry/auth/authorizer.go +++ b/utils/registry/auth/authorizer.go @@ -16,40 +16,54 @@ package auth import ( + "fmt" "net/http" au "github.com/docker/distribution/registry/client/auth" + "github.com/vmware/harbor/utils/registry/utils" ) -// Handler authorizes requests according to the schema -type Handler interface { +// Authorizer authorizes requests according to the schema +type Authorizer interface { // Scheme : basic, bearer Scheme() string - //AuthorizeRequest adds basic auth or token auth to the header of request - AuthorizeRequest(req *http.Request, params map[string]string) error + //Authorize adds basic auth or token auth to the header of request + Authorize(req *http.Request, params map[string]string) error } -// RequestAuthorizer holds a handler list, which will authorize request. -// Implements interface RequestModifier -type RequestAuthorizer struct { - handlers []Handler - challenges []au.Challenge +// AuthorizerStore holds a authorizer list, which will authorize request. +// And it implements interface Modifier +type AuthorizerStore struct { + authorizers []Authorizer + challenges []au.Challenge } -// NewRequestAuthorizer ... -func NewRequestAuthorizer(handlers []Handler, challenges []au.Challenge) *RequestAuthorizer { - return &RequestAuthorizer{ - handlers: handlers, - challenges: challenges, +// NewAuthorizerStore ... +func NewAuthorizerStore(endpoint string, authorizers ...Authorizer) (*AuthorizerStore, error) { + endpoint = utils.FormatEndpoint(endpoint) + + resp, err := http.Get(buildPingURL(endpoint)) + if err != nil { + return nil, err } + + challenges := ParseChallengeFromResponse(resp) + return &AuthorizerStore{ + authorizers: authorizers, + challenges: challenges, + }, nil } -// ModifyRequest adds authorization to the request -func (r *RequestAuthorizer) ModifyRequest(req *http.Request) error { - for _, challenge := range r.challenges { - for _, handler := range r.handlers { - if handler.Scheme() == challenge.Scheme { - if err := handler.AuthorizeRequest(req, challenge.Parameters); err != 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 { + 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 } } diff --git a/utils/registry/auth/challenge.go b/utils/registry/auth/challenge.go index 523fc1341..322ea8238 100644 --- a/utils/registry/auth/challenge.go +++ b/utils/registry/auth/challenge.go @@ -19,14 +19,11 @@ import ( "net/http" au "github.com/docker/distribution/registry/client/auth" - "github.com/vmware/harbor/utils/log" ) // ParseChallengeFromResponse ... func ParseChallengeFromResponse(resp *http.Response) []au.Challenge { challenges := au.ResponseChallenges(resp) - log.Debugf("challenges: %v", challenges) - return challenges } diff --git a/utils/registry/auth/tokenhandler.go b/utils/registry/auth/tokenauthorizer.go similarity index 72% rename from utils/registry/auth/tokenhandler.go rename to utils/registry/auth/tokenauthorizer.go index 8aa5d3cbb..1eb930ee2 100644 --- a/utils/registry/auth/tokenhandler.go +++ b/utils/registry/auth/tokenauthorizer.go @@ -16,6 +16,7 @@ package auth import ( + "crypto/tls" "encoding/json" "fmt" "io/ioutil" @@ -42,8 +43,8 @@ func (s *scope) string() string { type tokenGenerator func(realm, service string, scopes []string) (token string, expiresIn int, issuedAt *time.Time, err error) -// Implements interface Handler -type tokenHandler struct { +// Implements interface Authorizer +type tokenAuthorizer struct { scope *scope tg tokenGenerator cache string // cached token @@ -53,12 +54,12 @@ type tokenHandler struct { } // Scheme returns the scheme that the handler can handle -func (t *tokenHandler) Scheme() string { +func (t *tokenAuthorizer) Scheme() string { return "bearer" } // AuthorizeRequest will add authorization header which contains a token before the request is sent -func (t *tokenHandler) AuthorizeRequest(req *http.Request, params map[string]string) error { +func (t *tokenAuthorizer) Authorize(req *http.Request, params map[string]string) error { var scopes []*scope var token string @@ -100,26 +101,23 @@ func (t *tokenHandler) AuthorizeRequest(req *http.Request, params map[string]str if !hasFrom { t.updateCachedToken(to, expiresIn, issuedAt) - log.Debug("add token to cache") } } else { token = cachedToken - log.Debug("get token from cache") } req.Header.Add(http.CanonicalHeaderKey("Authorization"), fmt.Sprintf("Bearer %s", token)) - log.Debugf("add token to request: %s %s", req.Method, req.URL.String()) return nil } -func (t *tokenHandler) getCachedToken() (string, int, *time.Time) { +func (t *tokenAuthorizer) getCachedToken() (string, int, *time.Time) { t.Lock() defer t.Unlock() return t.cache, t.expiresIn, t.issuedAt } -func (t *tokenHandler) updateCachedToken(token string, expiresIn int, issuedAt *time.Time) { +func (t *tokenAuthorizer) updateCachedToken(token string, expiresIn int, issuedAt *time.Time) { t.Lock() defer t.Unlock() t.cache = token @@ -127,38 +125,43 @@ func (t *tokenHandler) updateCachedToken(token string, expiresIn int, issuedAt * t.issuedAt = issuedAt } -// Implements interface Handler -type standardTokenHandler struct { - tokenHandler +// Implements interface Authorizer +type standardTokenAuthorizer struct { + tokenAuthorizer client *http.Client credential Credential } -// NewStandardTokenHandler returns a standard token handler. The handler will request a token +// NewStandardTokenAuthorizer returns a standard token authorizer. The authorizer will request a token // from token server and add it to the origin request -// TODO deal with https -func NewStandardTokenHandler(credential Credential, scopeType, scopeName string, scopeActions ...string) Handler { - handler := &standardTokenHandler{ +func NewStandardTokenAuthorizer(credential Credential, insecure bool, scopeType, scopeName string, scopeActions ...string) Authorizer { + t := &http.Transport{ + TLSClientConfig: &tls.Config{ + InsecureSkipVerify: insecure, + }, + } + + authorizer := &standardTokenAuthorizer{ client: &http.Client{ - Transport: http.DefaultTransport, + Transport: t, }, credential: credential, } if len(scopeType) != 0 || len(scopeName) != 0 { - handler.scope = &scope{ + authorizer.scope = &scope{ Type: scopeType, Name: scopeName, Actions: scopeActions, } } - handler.tg = handler.generateToken + authorizer.tg = authorizer.generateToken - return handler + return authorizer } -func (s *standardTokenHandler) generateToken(realm, service string, scopes []string) (token string, expiresIn int, issuedAt *time.Time, err error) { +func (s *standardTokenAuthorizer) generateToken(realm, service string, scopes []string) (token string, expiresIn int, issuedAt *time.Time, err error) { u, err := url.Parse(realm) if err != nil { return @@ -217,37 +220,34 @@ func (s *standardTokenHandler) generateToken(realm, service string, scopes []str } } - log.Debug("get token from token server") - return } // Implements interface Handler -type usernameTokenHandler struct { - tokenHandler +type usernameTokenAuthorizer struct { + tokenAuthorizer username string } -// NewUsernameTokenHandler returns a handler which will generate a token according to +// NewUsernameTokenAuthorizer returns a authorizer which will generate a token according to // the user's privileges -func NewUsernameTokenHandler(username string, scopeType, scopeName string, scopeActions ...string) Handler { - handler := &usernameTokenHandler{ +func NewUsernameTokenAuthorizer(username string, scopeType, scopeName string, scopeActions ...string) Authorizer { + authorizer := &usernameTokenAuthorizer{ username: username, } - handler.scope = &scope{ + authorizer.scope = &scope{ Type: scopeType, Name: scopeName, Actions: scopeActions, } - handler.tg = handler.generateToken + authorizer.tg = authorizer.generateToken - return handler + return authorizer } -func (u *usernameTokenHandler) generateToken(realm, service string, scopes []string) (token string, expiresIn int, issuedAt *time.Time, err error) { +func (u *usernameTokenAuthorizer) generateToken(realm, service string, scopes []string) (token string, expiresIn int, issuedAt *time.Time, err error) { token, expiresIn, issuedAt, err = token_util.GenTokenForUI(u.username, service, scopes) - log.Debug("get token by calling GenTokenForUI directly") return } diff --git a/utils/registry/modifier.go b/utils/registry/modifier.go new file mode 100644 index 000000000..938be4f9e --- /dev/null +++ b/utils/registry/modifier.go @@ -0,0 +1,25 @@ +/* + Copyright (c) 2016 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 registry + +import ( + "net/http" +) + +// Modifier modifies request +type Modifier interface { + Modify(*http.Request) error +} diff --git a/utils/registry/registry.go b/utils/registry/registry.go index 837e156a8..75ad5bfa3 100644 --- a/utils/registry/registry.go +++ b/utils/registry/registry.go @@ -16,20 +16,15 @@ package registry import ( + "crypto/tls" "encoding/json" "fmt" "io/ioutil" "net/http" "net/url" - "strings" - "github.com/vmware/harbor/utils/registry/auth" registry_error "github.com/vmware/harbor/utils/registry/error" -) - -const ( - // UserAgent is used to decorate the request so it can be identified by webhook. - UserAgent string = "registry-client" + "github.com/vmware/harbor/utils/registry/utils" ) // Registry holds information of a registry entity @@ -40,14 +35,7 @@ type Registry struct { // NewRegistry returns an instance of registry func NewRegistry(endpoint string, client *http.Client) (*Registry, error) { - endpoint = strings.TrimSpace(endpoint) - endpoint = strings.TrimRight(endpoint, "/") - if !strings.HasPrefix(endpoint, "http://") && - !strings.HasPrefix(endpoint, "https://") { - endpoint = "http://" + endpoint - } - - u, err := url.Parse(endpoint) + u, err := utils.ParseEndpoint(endpoint) if err != nil { return nil, err } @@ -60,27 +48,27 @@ func NewRegistry(endpoint string, client *http.Client) (*Registry, error) { return registry, nil } -// NewRegistryWithUsername returns a Registry instance which will authorize the request -// according to the privileges of user -func NewRegistryWithUsername(endpoint, username string) (*Registry, error) { - - client, err := newClient(endpoint, username, nil, true, "registry", "catalog", "*") +// NewRegistryWithModifiers returns an instance of Registry according to the modifiers +func NewRegistryWithModifiers(endpoint string, insecure bool, modifiers ...Modifier) (*Registry, error) { + u, err := utils.ParseEndpoint(endpoint) if err != nil { return nil, err } - return NewRegistry(endpoint, client) -} - -// NewRegistryWithCredential returns a Registry instance which associate to a crendential. -// And Credential is essentially a decorator for client to docorate the request before sending it to the registry. -func NewRegistryWithCredential(endpoint string, credential auth.Credential) (*Registry, error) { - client, err := newClient(endpoint, "", credential, true, "", "", "") - if err != nil { - return nil, err + t := &http.Transport{ + TLSClientConfig: &tls.Config{ + InsecureSkipVerify: insecure, + }, } - return NewRegistry(endpoint, client) + transport := NewTransport(t, modifiers...) + + return &Registry{ + Endpoint: u, + client: &http.Client{ + Transport: transport, + }, + }, nil } // Catalog ... @@ -133,16 +121,6 @@ func (r *Registry) Ping() error { resp, err := r.client.Do(req) if err != nil { - // if urlErr, ok := err.(*url.Error); ok { - // if regErr, ok := urlErr.Err.(*registry_error.Error); ok { - // return ®istry_error.Error{ - // StatusCode: regErr.StatusCode, - // Detail: regErr.Detail, - // } - // } - // return urlErr.Err - // } - return parseError(err) } @@ -166,44 +144,3 @@ func (r *Registry) Ping() error { func buildCatalogURL(endpoint string) string { return fmt.Sprintf("%s/v2/_catalog", endpoint) } - -func newClient(endpoint, username string, credential auth.Credential, - addUserAgent bool, scopeType, scopeName string, scopeActions ...string) (*http.Client, error) { - - endpoint = strings.TrimSpace(endpoint) - endpoint = strings.TrimRight(endpoint, "/") - if !strings.HasPrefix(endpoint, "http://") && - !strings.HasPrefix(endpoint, "https://") { - endpoint = "http://" + endpoint - } - - resp, err := http.Get(buildPingURL(endpoint)) - if err != nil { - return nil, err - } - - var handlers []auth.Handler - var handler auth.Handler - if credential != nil { - handler = auth.NewStandardTokenHandler(credential, scopeType, scopeName, scopeActions...) - } else { - handler = auth.NewUsernameTokenHandler(username, scopeType, scopeName, scopeActions...) - } - - handlers = append(handlers, handler) - - challenges := auth.ParseChallengeFromResponse(resp) - authorizer := auth.NewRequestAuthorizer(handlers, challenges) - - rm := []RequestModifier{} - rm = append(rm, authorizer) - if addUserAgent { - headerModifier := NewHeaderModifier(map[string]string{http.CanonicalHeaderKey("User-Agent"): UserAgent}) - rm = append(rm, headerModifier) - } - - transport := NewTransport(http.DefaultTransport, rm) - return &http.Client{ - Transport: transport, - }, nil -} diff --git a/utils/registry/repository.go b/utils/registry/repository.go index b3d039592..c4f48e04a 100644 --- a/utils/registry/repository.go +++ b/utils/registry/repository.go @@ -17,6 +17,7 @@ package registry import ( "bytes" + "crypto/tls" "encoding/json" "fmt" "io" @@ -28,8 +29,9 @@ import ( "github.com/docker/distribution/manifest/schema1" "github.com/docker/distribution/manifest/schema2" - "github.com/vmware/harbor/utils/registry/auth" + registry_error "github.com/vmware/harbor/utils/registry/error" + "github.com/vmware/harbor/utils/registry/utils" ) // Repository holds information of a repository entity @@ -39,20 +41,11 @@ type Repository struct { client *http.Client } -// TODO add agent to header of request, notifications need it - // NewRepository returns an instance of Repository func NewRepository(name, endpoint string, client *http.Client) (*Repository, error) { name = strings.TrimSpace(name) - endpoint = strings.TrimSpace(endpoint) - endpoint = strings.TrimRight(endpoint, "/") - if !strings.HasPrefix(endpoint, "http://") && - !strings.HasPrefix(endpoint, "https://") { - endpoint = "http://" + endpoint - } - - u, err := url.Parse(endpoint) + u, err := utils.ParseEndpoint(endpoint) if err != nil { return nil, err } @@ -66,48 +59,30 @@ func NewRepository(name, endpoint string, client *http.Client) (*Repository, err return repository, nil } -// NewRepositoryWithCredential returns a Repository instance which will authorize the request -// according to the credenttial -func NewRepositoryWithCredential(name, endpoint string, credential auth.Credential) (*Repository, error) { - client, err := newClient(endpoint, "", credential, true, "repository", name, "pull", "push") +// NewRepositoryWithModifiers returns an instance of Repository according to the modifiers +func NewRepositoryWithModifiers(name, endpoint string, insecure bool, modifiers ...Modifier) (*Repository, error) { + name = strings.TrimSpace(name) + + u, err := utils.ParseEndpoint(endpoint) if err != nil { return nil, err } - return NewRepository(name, endpoint, client) -} - -// NewRepositoryWithCredentialForUI returns a Repository instance for UI -// The "user-agent" header of this instance does not been set -func NewRepositoryWithCredentialForUI(name, endpoint string, credential auth.Credential) (*Repository, error) { - client, err := newClient(endpoint, "", credential, false, "repository", name, "pull", "push") - if err != nil { - return nil, err + t := &http.Transport{ + TLSClientConfig: &tls.Config{ + InsecureSkipVerify: insecure, + }, } - return NewRepository(name, endpoint, client) -} + transport := NewTransport(t, modifiers...) -// NewRepositoryWithUsername returns a Repository instance which will authorize the request -// according to the privileges of user -func NewRepositoryWithUsername(name, endpoint, username string) (*Repository, error) { - client, err := newClient(endpoint, username, nil, true, "repository", name, "pull", "push") - if err != nil { - return nil, err - } - - return NewRepository(name, endpoint, client) -} - -// NewRepositoryWithUsernameForUI returns a Repository instance for UI -// The "user-agent" header of this instance does not been set -func NewRepositoryWithUsernameForUI(name, endpoint, username string) (*Repository, error) { - client, err := newClient(endpoint, username, nil, false, "repository", name, "pull", "push") - if err != nil { - return nil, err - } - - return NewRepository(name, endpoint, client) + return &Repository{ + Name: name, + Endpoint: u, + client: &http.Client{ + Transport: transport, + }, + }, nil } func parseError(err error) error { diff --git a/utils/registry/repository_test.go b/utils/registry/repository_test.go index 2b75e1540..97fc1bb79 100644 --- a/utils/registry/repository_test.go +++ b/utils/registry/repository_test.go @@ -26,7 +26,7 @@ import ( "time" "github.com/vmware/harbor/utils/registry/auth" - "github.com/vmware/harbor/utils/registry/error" + registry_error "github.com/vmware/harbor/utils/registry/error" ) var ( @@ -139,7 +139,8 @@ func serveToken(w http.ResponseWriter, r *http.Request) { } func TestListTag(t *testing.T) { - client, err := NewRepositoryWithCredential(repo, registryServer.URL, credential) + client, err := newRepositoryClient(registryServer.URL, true, credential, + repo, "repository", repo, "pull", "push", "*") if err != nil { t.Error(err) } @@ -158,13 +159,14 @@ func TestListTag(t *testing.T) { func TestListTagWithInvalidCredential(t *testing.T) { credential := auth.NewBasicAuthCredential(username, "wrong_password") - client, err := NewRepositoryWithCredential(repo, registryServer.URL, credential) + client, err := newRepositoryClient(registryServer.URL, true, credential, + repo, "repository", repo, "pull", "push", "*") if err != nil { t.Error(err) } if _, err = client.ListTag(); err != nil { - e, ok := err.(*error.Error) + e, ok := err.(*registry_error.Error) if ok && e.StatusCode == http.StatusUnauthorized { return } @@ -173,3 +175,20 @@ func TestListTagWithInvalidCredential(t *testing.T) { return } } + +func newRepositoryClient(endpoint string, insecure bool, credential auth.Credential, repository, scopeType, scopeName string, + scopeActions ...string) (*Repository, error) { + + authorizer := auth.NewStandardTokenAuthorizer(credential, insecure, scopeType, scopeName, scopeActions...) + + store, err := auth.NewAuthorizerStore(endpoint, authorizer) + if err != nil { + return nil, err + } + + client, err := NewRepositoryWithModifiers(repository, endpoint, insecure, store) + if err != nil { + return nil, err + } + return client, nil +} diff --git a/utils/registry/transport.go b/utils/registry/transport.go index 9c9e6b8e4..c1798450d 100644 --- a/utils/registry/transport.go +++ b/utils/registry/transport.go @@ -21,39 +21,14 @@ import ( "github.com/vmware/harbor/utils/log" ) -// RequestModifier modifies request -type RequestModifier interface { - ModifyRequest(*http.Request) error -} - -// HeaderModifier adds headers to request -type HeaderModifier struct { - headers map[string]string -} - -// NewHeaderModifier ... -func NewHeaderModifier(headers map[string]string) *HeaderModifier { - return &HeaderModifier{ - headers: headers, - } -} - -// ModifyRequest adds headers to the request -func (h *HeaderModifier) ModifyRequest(req *http.Request) error { - for key, value := range h.headers { - req.Header.Add(key, value) - } - return nil -} - // Transport holds information about base transport and modifiers type Transport struct { transport http.RoundTripper - modifiers []RequestModifier + modifiers []Modifier } // NewTransport ... -func NewTransport(transport http.RoundTripper, modifiers []RequestModifier) *Transport { +func NewTransport(transport http.RoundTripper, modifiers ...Modifier) *Transport { return &Transport{ transport: transport, modifiers: modifiers, @@ -63,7 +38,7 @@ func NewTransport(transport http.RoundTripper, modifiers []RequestModifier) *Tra // RoundTrip ... func (t *Transport) RoundTrip(req *http.Request) (*http.Response, error) { for _, modifier := range t.modifiers { - if err := modifier.ModifyRequest(req); err != nil { + if err := modifier.Modify(req); err != nil { return nil, err } } diff --git a/utils/registry/utils/utils.go b/utils/registry/utils/utils.go new file mode 100644 index 000000000..dc62c4c13 --- /dev/null +++ b/utils/registry/utils/utils.go @@ -0,0 +1,44 @@ +/* + Copyright (c) 2016 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 utils + +import ( + "net/url" + "strings" +) + +// FormatEndpoint formats endpoint +func FormatEndpoint(endpoint string) string { + endpoint = strings.TrimSpace(endpoint) + endpoint = strings.TrimRight(endpoint, "/") + if !strings.HasPrefix(endpoint, "http://") && + !strings.HasPrefix(endpoint, "https://") { + endpoint = "http://" + endpoint + } + + return endpoint +} + +// ParseEndpoint parses endpoint to a URL +func ParseEndpoint(endpoint string) (*url.URL, error) { + endpoint = FormatEndpoint(endpoint) + + u, err := url.Parse(endpoint) + if err != nil { + return nil, err + } + return u, nil +}