1. modify registry client 2. add https support

This commit is contained in:
Wenkai Yin 2016-06-21 16:39:03 +08:00
parent 5fc3c7dbeb
commit c1a8846a68
17 changed files with 381 additions and 266 deletions

View File

@ -159,7 +159,7 @@ func (p *ProjectAPI) List() {
if len(isPublic) > 0 { if len(isPublic) > 0 {
public, err = strconv.Atoi(isPublic) public, err = strconv.Atoi(isPublic)
if err != nil { 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") p.CustomAbort(http.StatusBadRequest, "invalid project Id")
} }
} }

View File

@ -255,11 +255,13 @@ func (ra *RepositoryAPI) GetManifests() {
func (ra *RepositoryAPI) initRepositoryClient(repoName string) (r *registry.Repository, err error) { func (ra *RepositoryAPI) initRepositoryClient(repoName string) (r *registry.Repository, err error) {
endpoint := os.Getenv("REGISTRY_URL") endpoint := os.Getenv("REGISTRY_URL")
// TODO read variable from config file
insecure := true
username, password, ok := ra.Ctx.Request.BasicAuth() username, password, ok := ra.Ctx.Request.BasicAuth()
if ok { if ok {
credential := auth.NewBasicAuthCredential(username, password) return newRepositoryClient(endpoint, insecure, username, password,
return registry.NewRepositoryWithCredentialForUI(repoName, endpoint, credential) repoName, "repository", repoName, "pull", "push", "*")
} }
username, err = ra.getUsername() username, err = ra.getUsername()
@ -267,7 +269,8 @@ func (ra *RepositoryAPI) initRepositoryClient(repoName string) (r *registry.Repo
return nil, err 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) { func (ra *RepositoryAPI) getUsername() (string, error) {
@ -327,3 +330,21 @@ func (ra *RepositoryAPI) GetTopRepos() {
ra.Data["json"] = repos ra.Data["json"] = repos
ra.ServeJSON() 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
}

View File

@ -26,7 +26,7 @@ import (
"github.com/vmware/harbor/models" "github.com/vmware/harbor/models"
"github.com/vmware/harbor/utils" "github.com/vmware/harbor/utils"
"github.com/vmware/harbor/utils/log" "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" "github.com/vmware/harbor/utils/registry/auth"
registry_error "github.com/vmware/harbor/utils/registry/error" registry_error "github.com/vmware/harbor/utils/registry/error"
) )
@ -92,8 +92,10 @@ func (t *TargetAPI) Ping() {
password = t.GetString("password") password = t.GetString("password")
} }
credential := auth.NewBasicAuthCredential(username, password) // TODO read variable from config file
registry, err := registry_util.NewRegistryWithCredential(endpoint, credential) insecure := true
registry, err := newRegistryClient(endpoint, insecure, username, password,
"", "", "")
if err != nil { if err != nil {
// timeout, dns resolve error, connection refused, etc. // timeout, dns resolve error, connection refused, etc.
if urlErr, ok := err.(*url.Error); ok { if urlErr, ok := err.(*url.Error); ok {
@ -273,3 +275,20 @@ func (t *TargetAPI) Delete() {
t.CustomAbort(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError)) 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
}

View File

@ -16,6 +16,7 @@
package replication package replication
import ( import (
"crypto/tls"
"fmt" "fmt"
"io/ioutil" "io/ioutil"
"net/http" "net/http"
@ -39,17 +40,20 @@ type Deleter struct {
dstUsr string // username ... dstUsr string // username ...
dstPwd string // username ... dstPwd string // username ...
insecure bool
logger *log.Logger logger *log.Logger
} }
// NewDeleter returns a Deleter // 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{ deleter := &Deleter{
repository: repository, repository: repository,
tags: tags, tags: tags,
dstURL: dstURL, dstURL: dstURL,
dstUsr: dstUsr, dstUsr: dstUsr,
dstPwd: dstPwd, dstPwd: dstPwd,
insecure: insecure,
logger: logger, logger: logger,
} }
deleter.logger.Infof("initialization completed: repository: %s, tags: %v, destination URL: %s, destination user: %s", 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 // delete repository
if len(d.tags) == 0 { if len(d.tags) == 0 {
u := url + "?repo_name=" + d.repository 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) 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 return "", err
} }
@ -82,7 +86,7 @@ func (d *Deleter) Enter() (string, error) {
// delele tags // delele tags
for _, tag := range d.tags { for _, tag := range d.tags {
u := url + "?repo_name=" + d.repository + "&tag=" + tag 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) 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 return "", err
} }
@ -93,14 +97,23 @@ func (d *Deleter) Enter() (string, error) {
return models.JobFinished, nil 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) req, err := http.NewRequest("DELETE", url, nil)
if err != nil { if err != nil {
return err return err
} }
req.SetBasicAuth(username, password) 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 { if err != nil {
return err return err
} }

View File

@ -61,6 +61,8 @@ type BaseHandler struct {
dstUsr string // username ... dstUsr string // username ...
dstPwd string // password ... dstPwd string // password ...
insecure bool // whether skip secure check when using https
srcClient *registry.Repository srcClient *registry.Repository
dstClient *registry.Repository dstClient *registry.Repository
@ -75,7 +77,7 @@ type BaseHandler struct {
// InitBaseHandler initializes a BaseHandler: creating clients for source and destination registry, // InitBaseHandler initializes a BaseHandler: creating clients for source and destination registry,
// listing tags of the repository if parameter tags is nil. // listing tags of the repository if parameter tags is nil.
func InitBaseHandler(repository, srcURL, srcSecret, 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", logger.Infof("initializing: repository: %s, tags: %v, source URL: %s, destination URL: %s, destination user: %s",
repository, tags, srcURL, dstURL, dstUsr) repository, tags, srcURL, dstURL, dstUsr)
@ -96,14 +98,16 @@ func InitBaseHandler(repository, srcURL, srcSecret,
c := &http.Cookie{Name: models.UISecretCookie, Value: srcSecret} c := &http.Cookie{Name: models.UISecretCookie, Value: srcSecret}
srcCred := auth.NewCookieCredential(c) srcCred := auth.NewCookieCredential(c)
// srcCred := auth.NewBasicAuthCredential("admin", "Harbor12345") // 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 { if err != nil {
return nil, err return nil, err
} }
base.srcClient = srcClient base.srcClient = srcClient
dstCred := auth.NewBasicAuthCredential(base.dstUsr, base.dstPwd) 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 { if err != nil {
return nil, err return nil, err
} }
@ -416,3 +420,34 @@ func (m *ManifestPusher) Enter() (string, error) {
return StatePullManifest, nil 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
}

View File

@ -258,9 +258,12 @@ func (sm *SM) Reset(jid int64) error {
} }
func addImgTransferTransition(sm *SM) 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(), base, err := replication.InitBaseHandler(sm.Parms.Repository, sm.Parms.LocalRegURL, config.UISecret(),
sm.Parms.TargetURL, sm.Parms.TargetUsername, sm.Parms.TargetPassword, sm.Parms.TargetURL, sm.Parms.TargetUsername, sm.Parms.TargetPassword,
sm.Parms.Tags, sm.Logger) insecure, sm.Parms.Tags, sm.Logger)
if err != nil { if err != nil {
return err return err
} }
@ -274,8 +277,10 @@ func addImgTransferTransition(sm *SM) error {
} }
func addImgDeleteTransition(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, 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(models.JobRunning, replication.StateDelete, deleter)
sm.AddTransition(replication.StateDelete, models.JobFinished, &StatusUpdater{DummyHandler{JobID: sm.JobID}, models.JobFinished}) sm.AddTransition(replication.StateDelete, models.JobFinished, &StatusUpdater{DummyHandler{JobID: sm.JobID}, models.JobFinished})

View File

@ -16,11 +16,13 @@
package cache package cache
import ( import (
"errors"
"os" "os"
"time" "time"
"github.com/vmware/harbor/utils/log" "github.com/vmware/harbor/utils/log"
"github.com/vmware/harbor/utils/registry" "github.com/vmware/harbor/utils/registry"
"github.com/vmware/harbor/utils/registry/auth"
"github.com/astaxie/beego/cache" "github.com/astaxie/beego/cache"
) )
@ -29,9 +31,9 @@ var (
// Cache is the global cache in system. // Cache is the global cache in system.
Cache cache.Cache Cache cache.Cache
endpoint string endpoint string
insecure bool
username string username string
registryClient *registry.Registry registryClient *registry.Registry
repositoryClients map[string]*registry.Repository
) )
const catalogKey string = "catalog" const catalogKey string = "catalog"
@ -44,8 +46,15 @@ func init() {
} }
endpoint = os.Getenv("REGISTRY_URL") endpoint = os.Getenv("REGISTRY_URL")
// TODO read variable from config file
insecure = true
username = "admin" 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. // 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...") log.Debug("refreshing catalog cache...")
if registryClient == nil { if registryClient == nil {
var err error return errors.New("registry client is nil")
registryClient, err = registry.NewRegistryWithUsername(endpoint, username)
if err != nil {
log.Errorf("error occurred while initializing registry client used by cache: %v", err)
return err
}
} }
var err error var err error
@ -70,15 +74,13 @@ func RefreshCatalogCache() error {
repos := []string{} repos := []string{}
for _, repo := range rs { for _, repo := range rs {
rc, ok := repositoryClients[repo] rc, err := NewRepositoryClient(endpoint, insecure, username,
if !ok { repo, "repository", repo, "pull", "push", "*")
rc, err = registry.NewRepositoryWithUsernameForUI(repo, endpoint, username)
if err != nil { if err != nil {
log.Errorf("error occurred while initializing repository client used by cache: %s %v", repo, err) log.Errorf("error occurred while initializing repository client used by cache: %s %v", repo, err)
continue continue
} }
repositoryClients[repo] = rc
}
tags, err := rc.ListTag() tags, err := rc.ListTag()
if err != nil { if err != nil {
log.Errorf("error occurred while list tag for %s: %v", repo, err) log.Errorf("error occurred while list tag for %s: %v", repo, err)
@ -112,3 +114,38 @@ func GetRepoFromCache() ([]string, error) {
} }
return result.([]string), nil 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
}

View File

@ -25,7 +25,6 @@ import (
"github.com/vmware/harbor/models" "github.com/vmware/harbor/models"
"github.com/vmware/harbor/service/cache" "github.com/vmware/harbor/service/cache"
"github.com/vmware/harbor/utils/log" "github.com/vmware/harbor/utils/log"
"github.com/vmware/harbor/utils/registry"
"github.com/astaxie/beego" "github.com/astaxie/beego"
) )
@ -57,7 +56,7 @@ func (n *NotificationHandler) Post() {
matched = false matched = false
} }
if matched && (strings.HasPrefix(e.Request.UserAgent, "docker") || 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 username = e.Actor.Name
action = e.Action action = e.Action
repo = e.Target.Repository repo = e.Target.Repository

View File

@ -16,40 +16,54 @@
package auth package auth
import ( import (
"fmt"
"net/http" "net/http"
au "github.com/docker/distribution/registry/client/auth" au "github.com/docker/distribution/registry/client/auth"
"github.com/vmware/harbor/utils/registry/utils"
) )
// Handler authorizes requests according to the schema // Authorizer authorizes requests according to the schema
type Handler interface { type Authorizer interface {
// Scheme : basic, bearer // Scheme : basic, bearer
Scheme() string Scheme() string
//AuthorizeRequest adds basic auth or token auth to the header of request //Authorize adds basic auth or token auth to the header of request
AuthorizeRequest(req *http.Request, params map[string]string) error Authorize(req *http.Request, params map[string]string) error
} }
// RequestAuthorizer holds a handler list, which will authorize request. // AuthorizerStore holds a authorizer list, which will authorize request.
// Implements interface RequestModifier // And it implements interface Modifier
type RequestAuthorizer struct { type AuthorizerStore struct {
handlers []Handler authorizers []Authorizer
challenges []au.Challenge challenges []au.Challenge
} }
// NewRequestAuthorizer ... // NewAuthorizerStore ...
func NewRequestAuthorizer(handlers []Handler, challenges []au.Challenge) *RequestAuthorizer { func NewAuthorizerStore(endpoint string, authorizers ...Authorizer) (*AuthorizerStore, error) {
return &RequestAuthorizer{ endpoint = utils.FormatEndpoint(endpoint)
handlers: handlers,
challenges: challenges, 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 buildPingURL(endpoint string) string {
func (r *RequestAuthorizer) ModifyRequest(req *http.Request) error { return fmt.Sprintf("%s/v2/", endpoint)
for _, challenge := range r.challenges { }
for _, handler := range r.handlers {
if handler.Scheme() == challenge.Scheme { // Modify adds authorization to the request
if err := handler.AuthorizeRequest(req, challenge.Parameters); err != nil { 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 return err
} }
} }

View File

@ -19,14 +19,11 @@ import (
"net/http" "net/http"
au "github.com/docker/distribution/registry/client/auth" au "github.com/docker/distribution/registry/client/auth"
"github.com/vmware/harbor/utils/log"
) )
// ParseChallengeFromResponse ... // ParseChallengeFromResponse ...
func ParseChallengeFromResponse(resp *http.Response) []au.Challenge { func ParseChallengeFromResponse(resp *http.Response) []au.Challenge {
challenges := au.ResponseChallenges(resp) challenges := au.ResponseChallenges(resp)
log.Debugf("challenges: %v", challenges)
return challenges return challenges
} }

View File

@ -16,6 +16,7 @@
package auth package auth
import ( import (
"crypto/tls"
"encoding/json" "encoding/json"
"fmt" "fmt"
"io/ioutil" "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) type tokenGenerator func(realm, service string, scopes []string) (token string, expiresIn int, issuedAt *time.Time, err error)
// Implements interface Handler // Implements interface Authorizer
type tokenHandler struct { type tokenAuthorizer struct {
scope *scope scope *scope
tg tokenGenerator tg tokenGenerator
cache string // cached token cache string // cached token
@ -53,12 +54,12 @@ type tokenHandler struct {
} }
// Scheme returns the scheme that the handler can handle // Scheme returns the scheme that the handler can handle
func (t *tokenHandler) Scheme() string { func (t *tokenAuthorizer) Scheme() string {
return "bearer" return "bearer"
} }
// AuthorizeRequest will add authorization header which contains a token before the request is sent // 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 scopes []*scope
var token string var token string
@ -100,26 +101,23 @@ func (t *tokenHandler) AuthorizeRequest(req *http.Request, params map[string]str
if !hasFrom { if !hasFrom {
t.updateCachedToken(to, expiresIn, issuedAt) t.updateCachedToken(to, expiresIn, issuedAt)
log.Debug("add token to cache")
} }
} else { } else {
token = cachedToken token = cachedToken
log.Debug("get token from cache")
} }
req.Header.Add(http.CanonicalHeaderKey("Authorization"), fmt.Sprintf("Bearer %s", token)) 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 return nil
} }
func (t *tokenHandler) getCachedToken() (string, int, *time.Time) { func (t *tokenAuthorizer) getCachedToken() (string, int, *time.Time) {
t.Lock() t.Lock()
defer t.Unlock() defer t.Unlock()
return t.cache, t.expiresIn, t.issuedAt 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() t.Lock()
defer t.Unlock() defer t.Unlock()
t.cache = token t.cache = token
@ -127,38 +125,43 @@ func (t *tokenHandler) updateCachedToken(token string, expiresIn int, issuedAt *
t.issuedAt = issuedAt t.issuedAt = issuedAt
} }
// Implements interface Handler // Implements interface Authorizer
type standardTokenHandler struct { type standardTokenAuthorizer struct {
tokenHandler tokenAuthorizer
client *http.Client client *http.Client
credential Credential 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 // from token server and add it to the origin request
// TODO deal with https func NewStandardTokenAuthorizer(credential Credential, insecure bool, scopeType, scopeName string, scopeActions ...string) Authorizer {
func NewStandardTokenHandler(credential Credential, scopeType, scopeName string, scopeActions ...string) Handler { t := &http.Transport{
handler := &standardTokenHandler{ TLSClientConfig: &tls.Config{
InsecureSkipVerify: insecure,
},
}
authorizer := &standardTokenAuthorizer{
client: &http.Client{ client: &http.Client{
Transport: http.DefaultTransport, Transport: t,
}, },
credential: credential, credential: credential,
} }
if len(scopeType) != 0 || len(scopeName) != 0 { if len(scopeType) != 0 || len(scopeName) != 0 {
handler.scope = &scope{ authorizer.scope = &scope{
Type: scopeType, Type: scopeType,
Name: scopeName, Name: scopeName,
Actions: scopeActions, 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) u, err := url.Parse(realm)
if err != nil { if err != nil {
return return
@ -217,37 +220,34 @@ func (s *standardTokenHandler) generateToken(realm, service string, scopes []str
} }
} }
log.Debug("get token from token server")
return return
} }
// Implements interface Handler // Implements interface Handler
type usernameTokenHandler struct { type usernameTokenAuthorizer struct {
tokenHandler tokenAuthorizer
username string 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 // the user's privileges
func NewUsernameTokenHandler(username string, scopeType, scopeName string, scopeActions ...string) Handler { func NewUsernameTokenAuthorizer(username string, scopeType, scopeName string, scopeActions ...string) Authorizer {
handler := &usernameTokenHandler{ authorizer := &usernameTokenAuthorizer{
username: username, username: username,
} }
handler.scope = &scope{ authorizer.scope = &scope{
Type: scopeType, Type: scopeType,
Name: scopeName, Name: scopeName,
Actions: scopeActions, 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) token, expiresIn, issuedAt, err = token_util.GenTokenForUI(u.username, service, scopes)
log.Debug("get token by calling GenTokenForUI directly")
return return
} }

View File

@ -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
}

View File

@ -16,20 +16,15 @@
package registry package registry
import ( import (
"crypto/tls"
"encoding/json" "encoding/json"
"fmt" "fmt"
"io/ioutil" "io/ioutil"
"net/http" "net/http"
"net/url" "net/url"
"strings"
"github.com/vmware/harbor/utils/registry/auth"
registry_error "github.com/vmware/harbor/utils/registry/error" registry_error "github.com/vmware/harbor/utils/registry/error"
) "github.com/vmware/harbor/utils/registry/utils"
const (
// UserAgent is used to decorate the request so it can be identified by webhook.
UserAgent string = "registry-client"
) )
// Registry holds information of a registry entity // Registry holds information of a registry entity
@ -40,14 +35,7 @@ type Registry struct {
// NewRegistry returns an instance of registry // NewRegistry returns an instance of registry
func NewRegistry(endpoint string, client *http.Client) (*Registry, error) { func NewRegistry(endpoint string, client *http.Client) (*Registry, error) {
endpoint = strings.TrimSpace(endpoint) u, err := utils.ParseEndpoint(endpoint)
endpoint = strings.TrimRight(endpoint, "/")
if !strings.HasPrefix(endpoint, "http://") &&
!strings.HasPrefix(endpoint, "https://") {
endpoint = "http://" + endpoint
}
u, err := url.Parse(endpoint)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -60,27 +48,27 @@ func NewRegistry(endpoint string, client *http.Client) (*Registry, error) {
return registry, nil return registry, nil
} }
// NewRegistryWithUsername returns a Registry instance which will authorize the request // NewRegistryWithModifiers returns an instance of Registry according to the modifiers
// according to the privileges of user func NewRegistryWithModifiers(endpoint string, insecure bool, modifiers ...Modifier) (*Registry, error) {
func NewRegistryWithUsername(endpoint, username string) (*Registry, error) { u, err := utils.ParseEndpoint(endpoint)
client, err := newClient(endpoint, username, nil, true, "registry", "catalog", "*")
if err != nil { if err != nil {
return nil, err return nil, err
} }
return NewRegistry(endpoint, client) t := &http.Transport{
} TLSClientConfig: &tls.Config{
InsecureSkipVerify: insecure,
// 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
} }
return NewRegistry(endpoint, client) transport := NewTransport(t, modifiers...)
return &Registry{
Endpoint: u,
client: &http.Client{
Transport: transport,
},
}, nil
} }
// Catalog ... // Catalog ...
@ -133,16 +121,6 @@ func (r *Registry) Ping() error {
resp, err := r.client.Do(req) resp, err := r.client.Do(req)
if err != nil { if err != nil {
// if urlErr, ok := err.(*url.Error); ok {
// if regErr, ok := urlErr.Err.(*registry_error.Error); ok {
// return &registry_error.Error{
// StatusCode: regErr.StatusCode,
// Detail: regErr.Detail,
// }
// }
// return urlErr.Err
// }
return parseError(err) return parseError(err)
} }
@ -166,44 +144,3 @@ func (r *Registry) Ping() error {
func buildCatalogURL(endpoint string) string { func buildCatalogURL(endpoint string) string {
return fmt.Sprintf("%s/v2/_catalog", endpoint) 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
}

View File

@ -17,6 +17,7 @@ package registry
import ( import (
"bytes" "bytes"
"crypto/tls"
"encoding/json" "encoding/json"
"fmt" "fmt"
"io" "io"
@ -28,8 +29,9 @@ import (
"github.com/docker/distribution/manifest/schema1" "github.com/docker/distribution/manifest/schema1"
"github.com/docker/distribution/manifest/schema2" "github.com/docker/distribution/manifest/schema2"
"github.com/vmware/harbor/utils/registry/auth"
registry_error "github.com/vmware/harbor/utils/registry/error" registry_error "github.com/vmware/harbor/utils/registry/error"
"github.com/vmware/harbor/utils/registry/utils"
) )
// Repository holds information of a repository entity // Repository holds information of a repository entity
@ -39,20 +41,11 @@ type Repository struct {
client *http.Client client *http.Client
} }
// TODO add agent to header of request, notifications need it
// NewRepository returns an instance of Repository // NewRepository returns an instance of Repository
func NewRepository(name, endpoint string, client *http.Client) (*Repository, error) { func NewRepository(name, endpoint string, client *http.Client) (*Repository, error) {
name = strings.TrimSpace(name) name = strings.TrimSpace(name)
endpoint = strings.TrimSpace(endpoint) u, err := utils.ParseEndpoint(endpoint)
endpoint = strings.TrimRight(endpoint, "/")
if !strings.HasPrefix(endpoint, "http://") &&
!strings.HasPrefix(endpoint, "https://") {
endpoint = "http://" + endpoint
}
u, err := url.Parse(endpoint)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -66,48 +59,30 @@ func NewRepository(name, endpoint string, client *http.Client) (*Repository, err
return repository, nil return repository, nil
} }
// NewRepositoryWithCredential returns a Repository instance which will authorize the request // NewRepositoryWithModifiers returns an instance of Repository according to the modifiers
// according to the credenttial func NewRepositoryWithModifiers(name, endpoint string, insecure bool, modifiers ...Modifier) (*Repository, error) {
func NewRepositoryWithCredential(name, endpoint string, credential auth.Credential) (*Repository, error) { name = strings.TrimSpace(name)
client, err := newClient(endpoint, "", credential, true, "repository", name, "pull", "push")
u, err := utils.ParseEndpoint(endpoint)
if err != nil { if err != nil {
return nil, err return nil, err
} }
return NewRepository(name, endpoint, client) t := &http.Transport{
} TLSClientConfig: &tls.Config{
InsecureSkipVerify: insecure,
// 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
} }
return NewRepository(name, endpoint, client) transport := NewTransport(t, modifiers...)
}
// NewRepositoryWithUsername returns a Repository instance which will authorize the request return &Repository{
// according to the privileges of user Name: name,
func NewRepositoryWithUsername(name, endpoint, username string) (*Repository, error) { Endpoint: u,
client, err := newClient(endpoint, username, nil, true, "repository", name, "pull", "push") client: &http.Client{
if err != nil { Transport: transport,
return nil, err },
} }, nil
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)
} }
func parseError(err error) error { func parseError(err error) error {

View File

@ -26,7 +26,7 @@ import (
"time" "time"
"github.com/vmware/harbor/utils/registry/auth" "github.com/vmware/harbor/utils/registry/auth"
"github.com/vmware/harbor/utils/registry/error" registry_error "github.com/vmware/harbor/utils/registry/error"
) )
var ( var (
@ -139,7 +139,8 @@ func serveToken(w http.ResponseWriter, r *http.Request) {
} }
func TestListTag(t *testing.T) { 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 { if err != nil {
t.Error(err) t.Error(err)
} }
@ -158,13 +159,14 @@ func TestListTag(t *testing.T) {
func TestListTagWithInvalidCredential(t *testing.T) { func TestListTagWithInvalidCredential(t *testing.T) {
credential := auth.NewBasicAuthCredential(username, "wrong_password") 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 { if err != nil {
t.Error(err) t.Error(err)
} }
if _, err = client.ListTag(); err != nil { if _, err = client.ListTag(); err != nil {
e, ok := err.(*error.Error) e, ok := err.(*registry_error.Error)
if ok && e.StatusCode == http.StatusUnauthorized { if ok && e.StatusCode == http.StatusUnauthorized {
return return
} }
@ -173,3 +175,20 @@ func TestListTagWithInvalidCredential(t *testing.T) {
return 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
}

View File

@ -21,39 +21,14 @@ import (
"github.com/vmware/harbor/utils/log" "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 // Transport holds information about base transport and modifiers
type Transport struct { type Transport struct {
transport http.RoundTripper transport http.RoundTripper
modifiers []RequestModifier modifiers []Modifier
} }
// NewTransport ... // NewTransport ...
func NewTransport(transport http.RoundTripper, modifiers []RequestModifier) *Transport { func NewTransport(transport http.RoundTripper, modifiers ...Modifier) *Transport {
return &Transport{ return &Transport{
transport: transport, transport: transport,
modifiers: modifiers, modifiers: modifiers,
@ -63,7 +38,7 @@ func NewTransport(transport http.RoundTripper, modifiers []RequestModifier) *Tra
// RoundTrip ... // RoundTrip ...
func (t *Transport) RoundTrip(req *http.Request) (*http.Response, error) { func (t *Transport) RoundTrip(req *http.Request) (*http.Response, error) {
for _, modifier := range t.modifiers { for _, modifier := range t.modifiers {
if err := modifier.ModifyRequest(req); err != nil { if err := modifier.Modify(req); err != nil {
return nil, err return nil, err
} }
} }

View File

@ -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
}