diff --git a/.gitignore b/.gitignore index 27cd819fa..6b04c4ad3 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ Deploy/config/ui/env Deploy/config/ui/app.conf Deploy/config/db/env Deploy/harbor.cfg +ui/ui diff --git a/Deploy/templates/registry/config.yml b/Deploy/templates/registry/config.yml index f460bf13e..a1b19a8ed 100644 --- a/Deploy/templates/registry/config.yml +++ b/Deploy/templates/registry/config.yml @@ -11,6 +11,8 @@ storage: maintenance: uploadpurging: enabled: false + delete: + enabled: true http: addr: :5000 secret: placeholder diff --git a/README.md b/README.md index c7b4e2b06..9f66f175a 100644 --- a/README.md +++ b/README.md @@ -57,8 +57,8 @@ To simplify the installation process, a pre-built installation package of Harbor For information on how to use Harbor, please see [User Guide](docs/user_guide.md) . -### Deploy harbor on Kubernetes -Detailed instruction about deploying harbor on Kubernetes is described [here](https://github.com/vmware/harbor/blob/master/kubernetes_deployment.md). +### Deploy Harbor on Kubernetes +Detailed instruction about deploying Harbor on Kubernetes is described [here](docs/kubernetes_deployment.md). ### Contribution We welcome contributions from the community. If you wish to contribute code and you have not signed our contributor license agreement (CLA), our bot will update the issue when you open a pull request. For any questions about the CLA process, please refer to our [FAQ](https://cla.vmware.com/faq). diff --git a/api/base.go b/api/base.go index 33f20ad1f..70388009e 100644 --- a/api/base.go +++ b/api/base.go @@ -57,7 +57,10 @@ func (b *BaseAPI) ValidateUser() int { username, password, ok := b.Ctx.Request.BasicAuth() if ok { log.Infof("Requst with Basic Authentication header, username: %s", username) - user, err := auth.Login(models.AuthModel{username, password}) + user, err := auth.Login(models.AuthModel{ + Principal: username, + Password: password, + }) if err != nil { log.Errorf("Error while trying to login, username: %s, error: %v", username, err) user = nil diff --git a/api/repository.go b/api/repository.go index 0ce3cf8d1..6058b3608 100644 --- a/api/repository.go +++ b/api/repository.go @@ -18,6 +18,7 @@ package api import ( "encoding/json" "net/http" + "os" "strconv" "strings" "time" @@ -26,6 +27,9 @@ import ( "github.com/vmware/harbor/models" svc_utils "github.com/vmware/harbor/service/utils" "github.com/vmware/harbor/utils/log" + "github.com/vmware/harbor/utils/registry" + "github.com/vmware/harbor/utils/registry/auth" + "github.com/vmware/harbor/utils/registry/errors" ) // RepositoryAPI handles request to /api/repositories /api/repositories/tags /api/repositories/manifests, the parm has to be put @@ -36,6 +40,7 @@ type RepositoryAPI struct { BaseAPI userID int username string + registry *registry.Registry } // Prepare will set a non existent user ID in case the request tries to view repositories under a project he doesn't has permission. @@ -53,6 +58,43 @@ func (ra *RepositoryAPI) Prepare() { } else { ra.username = username } + + var client *http.Client + + //no session, initialize a standard auth handler + if ra.userID == dao.NonExistUserID && len(ra.username) == 0 { + username, password, _ := ra.Ctx.Request.BasicAuth() + + credential := auth.NewBasicAuthCredential(username, password) + client = registry.NewClientStandardAuthHandlerEmbeded(credential) + log.Debug("initializing standard auth handler") + + } else { + // session works, initialize a username auth handler + username := ra.username + if len(username) == 0 { + user, err := dao.GetUser(models.User{ + UserID: ra.userID, + }) + if err != nil { + log.Errorf("error occurred whiling geting user for initializing a username auth handler: %v", err) + return + } + + username = user.Username + } + + client = registry.NewClientUsernameAuthHandlerEmbeded(username) + log.Debug("initializing username auth handler: %s", username) + } + + endpoint := os.Getenv("REGISTRY_URL") + r, err := registry.New(endpoint, client) + if err != nil { + log.Fatalf("error occurred while initializing auth handler for repository API: %v", err) + } + + ra.registry = r } // Get ... @@ -77,11 +119,13 @@ func (ra *RepositoryAPI) Get() { ra.RenderError(http.StatusForbidden, "") return } + repoList, err := svc_utils.GetRepoFromCache() if err != nil { log.Errorf("Failed to get repo from cache, error: %v", err) ra.RenderError(http.StatusInternalServerError, "internal sever error") } + projectName := p.Name q := ra.GetString("q") var resp []string @@ -105,6 +149,56 @@ func (ra *RepositoryAPI) Get() { ra.ServeJSON() } +// Delete ... +func (ra *RepositoryAPI) Delete() { + repoName := ra.GetString("repo_name") + if len(repoName) == 0 { + ra.CustomAbort(http.StatusBadRequest, "repo_name is nil") + } + + tags := []string{} + tag := ra.GetString("tag") + if len(tag) == 0 { + tagList, err := ra.registry.ListTag(repoName) + if err != nil { + e, ok := errors.ParseError(err) + if ok { + log.Info(e) + ra.CustomAbort(e.StatusCode, e.Message) + } else { + log.Error(err) + ra.CustomAbort(http.StatusInternalServerError, "internal error") + } + + } + tags = append(tags, tagList...) + + } else { + tags = append(tags, tag) + } + + for _, t := range tags { + if err := ra.registry.DeleteTag(repoName, t); err != nil { + e, ok := errors.ParseError(err) + if ok { + ra.CustomAbort(e.StatusCode, e.Message) + } else { + log.Error(err) + ra.CustomAbort(http.StatusInternalServerError, "internal error") + } + } + log.Infof("delete tag: %s %s", repoName, t) + } + + go func() { + log.Debug("refreshing catalog cache") + if err := svc_utils.RefreshCatalogCache(); err != nil { + log.Errorf("error occurred while refresh catalog cache: %v", err) + } + }() + +} + type tag struct { Name string `json:"name"` Tags []string `json:"tags"` @@ -128,15 +222,19 @@ func (ra *RepositoryAPI) GetTags() { var tags []string repoName := ra.GetString("repo_name") - result, err := svc_utils.RegistryAPIGet(svc_utils.BuildRegistryURL(repoName, "tags", "list"), ra.username) + + tags, err := ra.registry.ListTag(repoName) if err != nil { - log.Errorf("Failed to get repo tags, repo name: %s, error: %v", repoName, err) - ra.RenderError(http.StatusInternalServerError, "Failed to get repo tags") - } else { - t := tag{} - json.Unmarshal(result, &t) - tags = t.Tags + e, ok := errors.ParseError(err) + if ok { + log.Info(e) + ra.CustomAbort(e.StatusCode, e.Message) + } else { + log.Error(err) + ra.CustomAbort(http.StatusInternalServerError, "internal error") + } } + ra.Data["json"] = tags ra.ServeJSON() } @@ -148,14 +246,20 @@ func (ra *RepositoryAPI) GetManifests() { item := models.RepoItem{} - result, err := svc_utils.RegistryAPIGet(svc_utils.BuildRegistryURL(repoName, "manifests", tag), ra.username) + _, _, payload, err := ra.registry.PullManifest(repoName, tag, registry.ManifestVersion1) if err != nil { - log.Errorf("Failed to get manifests for repo, repo name: %s, tag: %s, error: %v", repoName, tag, err) - ra.RenderError(http.StatusInternalServerError, "Internal Server Error") - return + e, ok := errors.ParseError(err) + if ok { + log.Info(e) + ra.CustomAbort(e.StatusCode, e.Message) + } else { + log.Error(err) + ra.CustomAbort(http.StatusInternalServerError, "internal error") + } } + mani := manifest{} - err = json.Unmarshal(result, &mani) + err = json.Unmarshal(payload, &mani) if err != nil { log.Errorf("Failed to decode json from response for manifests, repo name: %s, tag: %s, error: %v", repoName, tag, err) ra.RenderError(http.StatusInternalServerError, "Internal Server Error") @@ -169,7 +273,6 @@ func (ra *RepositoryAPI) GetManifests() { ra.RenderError(http.StatusInternalServerError, "Internal Server Error") return } - item.CreatedStr = item.Created.Format("2006-01-02 15:04:05") item.DurationDays = strconv.Itoa(int(time.Since(item.Created).Hours()/24)) + " days" ra.Data["json"] = item diff --git a/controllers/login.go b/controllers/login.go index b35b0aea1..f87c3dfc3 100644 --- a/controllers/login.go +++ b/controllers/login.go @@ -49,7 +49,10 @@ func (c *CommonController) Login() { principal := c.GetString("principal") password := c.GetString("password") - user, err := auth.Login(models.AuthModel{principal, password}) + user, err := auth.Login(models.AuthModel{ + Principal: principal, + Password: password, + }) if err != nil { log.Errorf("Error occurred in UserLogin: %v", err) c.CustomAbort(http.StatusUnauthorized, "") diff --git a/dao/base.go b/dao/base.go index 49419745e..fe1c5a0ed 100644 --- a/dao/base.go +++ b/dao/base.go @@ -77,12 +77,12 @@ func InitDB() { var err error var c net.Conn for { - c, err = net.Dial("tcp", addr+":"+port) + c, err = net.DialTimeout("tcp", addr+":"+port, 20*time.Second) if err == nil { c.Close() ch <- 1 } else { - log.Info("failed to connect to db, retry after 2 seconds...") + log.Errorf("failed to connect to db, retry after 2 seconds :%v", err) time.Sleep(2 * time.Second) } } diff --git a/dao/dao_test.go b/dao/dao_test.go index abb671b3f..67ccef833 100644 --- a/dao/dao_test.go +++ b/dao/dao_test.go @@ -206,7 +206,10 @@ func TestLoginByUserName(t *testing.T) { Password: "Abc12345", } - loginUser, err := LoginByDb(models.AuthModel{userQuery.Username, userQuery.Password}) + loginUser, err := LoginByDb(models.AuthModel{ + Principal: userQuery.Username, + Password: userQuery.Password, + }) if err != nil { t.Errorf("Error occurred in LoginByDb: %v", err) } @@ -226,7 +229,10 @@ func TestLoginByEmail(t *testing.T) { Password: "Abc12345", } - loginUser, err := LoginByDb(models.AuthModel{userQuery.Email, userQuery.Password}) + loginUser, err := LoginByDb(models.AuthModel{ + Principal: userQuery.Email, + Password: userQuery.Password, + }) if err != nil { t.Errorf("Error occurred in LoginByDb: %v", err) } diff --git a/dao/role.go b/dao/role.go index 43062a1fc..aafecc74d 100644 --- a/dao/role.go +++ b/dao/role.go @@ -58,6 +58,10 @@ func IsAdminRole(userIDOrUsername interface{}) (bool, error) { return false, fmt.Errorf("invalid parameter, only int and string are supported: %v", userIDOrUsername) } + if u.UserID == NonExistUserID && len(u.Username) == 0 { + return false, nil + } + user, err := GetUser(u) if err != nil { return false, err diff --git a/dao/user.go b/dao/user.go index 655e69d21..3d94d2a76 100644 --- a/dao/user.go +++ b/dao/user.go @@ -109,7 +109,7 @@ func ListUsers(query models.User) ([]models.User, error) { return u, err } -// ToggleUserAdminRole gives a user admim role. +// ToggleUserAdminRole gives a user admin role. func ToggleUserAdminRole(u models.User) error { o := orm.NewOrm() diff --git a/kubernetes_deployment.md b/docs/kubernetes_deployment.md similarity index 100% rename from kubernetes_deployment.md rename to docs/kubernetes_deployment.md diff --git a/docs/swagger.yaml b/docs/swagger.yaml index 170f0bd25..404a0b431 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -473,6 +473,36 @@ paths: description: Project ID does not exist. 500: description: Unexpected internal errors. + delete: + summary: Delete a repository or a tag in a repository. + description: | + This endpoint let user delete repositories and tags with repo name and tag. + parameters: + - name: repo_name + in: query + type: string + format: string + required: true + description: The name of repository which will be deleted. + - name: tag + in: query + type: string + format: string + required: false + description: Tag of a repository. + tags: + - Products + responses: + 200: + description: Delete repository or tag successfully. + 400: + description: Invalid repo_name. + 401: + description: Unauthorized. + 404: + description: Repository or tag not found. + 403: + description: Forbidden. /repositories/tags: get: summary: Get tags of a relevant repository. diff --git a/models/repo.go b/models/repo.go index 4ddb7afd0..6b8de742b 100644 --- a/models/repo.go +++ b/models/repo.go @@ -29,7 +29,6 @@ type RepoItem struct { ID string `json:"Id"` Parent string `json:"Parent"` Created time.Time `json:"Created"` - CreatedStr string `json:"CreatedStr"` DurationDays string `json:"Duration Days"` Author string `json:"Author"` Architecture string `json:"Architecture"` diff --git a/service/notification.go b/service/notification.go index 7f35f8b67..beab28778 100644 --- a/service/notification.go +++ b/service/notification.go @@ -38,7 +38,7 @@ const manifestPattern = `^application/vnd.docker.distribution.manifest.v\d\+json // Post handles POST request, and records audit log or refreshes cache based on event. func (n *NotificationHandler) Post() { var notification models.Notification - // log.Info("Notification Handler triggered!\n") + //log.Info("Notification Handler triggered!\n") // log.Infof("request body in string: %s", string(n.Ctx.Input.CopyBody())) err := json.Unmarshal(n.Ctx.Input.CopyBody(1<<32), ¬ification) diff --git a/service/utils/authutils.go b/service/token/authutils.go similarity index 97% rename from service/utils/authutils.go rename to service/token/authutils.go index c8bdd6d59..bb9d8ad92 100644 --- a/service/utils/authutils.go +++ b/service/token/authutils.go @@ -13,7 +13,7 @@ limitations under the License. */ -package utils +package token import ( "crypto" @@ -80,7 +80,7 @@ func FilterAccess(username string, authenticated bool, a *token.ResourceActions) return } if exist { - permission = "RW" + permission = "RWM" } else { permission = "" log.Infof("project %s does not exist, set empty permission for admin\n", projectName) @@ -96,6 +96,9 @@ func FilterAccess(username string, authenticated bool, a *token.ResourceActions) if strings.Contains(permission, "W") { a.Actions = append(a.Actions, "push") } + if strings.Contains(permission, "M") { + a.Actions = append(a.Actions, "*") + } if strings.Contains(permission, "R") || dao.IsProjectPublic(projectName) { a.Actions = append(a.Actions, "pull") } diff --git a/service/token.go b/service/token/token.go similarity index 69% rename from service/token.go rename to service/token/token.go index cd2386833..c12e4d7ad 100644 --- a/service/token.go +++ b/service/token/token.go @@ -13,53 +13,53 @@ limitations under the License. */ -package service +package token import ( "net/http" "github.com/vmware/harbor/auth" "github.com/vmware/harbor/models" - svc_utils "github.com/vmware/harbor/service/utils" + //svc_utils "github.com/vmware/harbor/service/utils" "github.com/vmware/harbor/utils/log" "github.com/astaxie/beego" "github.com/docker/distribution/registry/auth/token" ) -// TokenHandler handles request on /service/token, which is the auth provider for registry. -type TokenHandler struct { +// Handler handles request on /service/token, which is the auth provider for registry. +type Handler struct { beego.Controller } // Get handles GET request, it checks the http header for user credentials // and parse service and scope based on docker registry v2 standard, // checkes the permission agains local DB and generates jwt token. -func (a *TokenHandler) Get() { +func (h *Handler) Get() { - request := a.Ctx.Request + request := h.Ctx.Request log.Infof("request url: %v", request.URL.String()) username, password, _ := request.BasicAuth() authenticated := authenticate(username, password) - service := a.GetString("service") - scopes := a.GetStrings("scope") + service := h.GetString("service") + scopes := h.GetStrings("scope") log.Debugf("scopes: %+v", scopes) if len(scopes) == 0 && !authenticated { log.Info("login request with invalid credentials") - a.CustomAbort(http.StatusUnauthorized, "") + h.CustomAbort(http.StatusUnauthorized, "") } - access := svc_utils.GetResourceActions(scopes) + access := GetResourceActions(scopes) for _, a := range access { - svc_utils.FilterAccess(username, authenticated, a) + FilterAccess(username, authenticated, a) } - a.serveToken(username, service, access) + h.serveToken(username, service, access) } -func (a *TokenHandler) serveToken(username, service string, access []*token.ResourceActions) { - writer := a.Ctx.ResponseWriter +func (h *Handler) serveToken(username, service string, access []*token.ResourceActions) { + writer := h.Ctx.ResponseWriter //create token - rawToken, err := svc_utils.MakeToken(username, service, access) + rawToken, err := MakeToken(username, service, access) if err != nil { log.Errorf("Failed to make token, error: %v", err) writer.WriteHeader(http.StatusInternalServerError) @@ -67,12 +67,15 @@ func (a *TokenHandler) serveToken(username, service string, access []*token.Reso } tk := make(map[string]string) tk["token"] = rawToken - a.Data["json"] = tk - a.ServeJSON() + h.Data["json"] = tk + h.ServeJSON() } func authenticate(principal, password string) bool { - user, err := auth.Login(models.AuthModel{principal, password}) + user, err := auth.Login(models.AuthModel{ + Principal: principal, + Password: password, + }) if err != nil { log.Errorf("Error occurred in UserLogin: %v", err) return false diff --git a/service/utils/cache.go b/service/utils/cache.go index 81ec5a52b..a97a4599c 100644 --- a/service/utils/cache.go +++ b/service/utils/cache.go @@ -16,11 +16,11 @@ package utils import ( - "encoding/json" + "os" "time" - "github.com/vmware/harbor/models" "github.com/vmware/harbor/utils/log" + "github.com/vmware/harbor/utils/registry" "github.com/astaxie/beego/cache" ) @@ -28,6 +28,8 @@ import ( // Cache is the global cache in system. var Cache cache.Cache +var registryClient *registry.Registry + const catalogKey string = "catalog" func init() { @@ -36,20 +38,39 @@ func init() { if err != nil { log.Errorf("Failed to initialize cache, error:%v", err) } + + endpoint := os.Getenv("REGISTRY_URL") + client := registry.NewClientUsernameAuthHandlerEmbeded("admin") + registryClient, err = registry.New(endpoint, client) + if err != nil { + log.Fatalf("error occurred while initializing authentication handler used by cache: %v", err) + } } // RefreshCatalogCache calls registry's API to get repository list and write it to cache. func RefreshCatalogCache() error { - result, err := RegistryAPIGet(BuildRegistryURL("_catalog"), "") + log.Debug("refreshing catalog cache...") + rs, err := registryClient.Catalog() if err != nil { return err } - repoResp := models.Repo{} - err = json.Unmarshal(result, &repoResp) - if err != nil { - return err + + repos := []string{} + + for _, repo := range rs { + tags, err := registryClient.ListTag(repo) + if err != nil { + log.Errorf("error occurred while list tag for %s: %v", repo, err) + return err + } + + if len(tags) != 0 { + repos = append(repos, repo) + log.Debugf("add %s to catalog cache", repo) + } } - Cache.Put(catalogKey, repoResp.Repositories, 600*time.Second) + + Cache.Put(catalogKey, repos, 600*time.Second) return nil } diff --git a/service/utils/registryutils.go b/service/utils/registryutils.go deleted file mode 100644 index 37ada5426..000000000 --- a/service/utils/registryutils.go +++ /dev/null @@ -1,114 +0,0 @@ -/* - 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 ( - "errors" - "fmt" - "io/ioutil" - "net/http" - "os" - "regexp" - - "github.com/vmware/harbor/utils/log" -) - -// BuildRegistryURL ... -func BuildRegistryURL(segments ...string) string { - registryURL := os.Getenv("REGISTRY_URL") - if registryURL == "" { - registryURL = "http://localhost:5000" - } - url := registryURL + "/v2" - for _, s := range segments { - if s == "v2" { - log.Debugf("unnecessary v2 in %v", segments) - continue - } - url += "/" + s - } - return url -} - -// RegistryAPIGet triggers GET request to the URL which is the endpoint of registry and returns the response body. -// It will attach a valid jwt token to the request if registry requires. -func RegistryAPIGet(url, username string) ([]byte, error) { - - log.Debugf("Registry API url: %s", url) - response, err := http.Get(url) - if err != nil { - return nil, err - } - result, err := ioutil.ReadAll(response.Body) - if err != nil { - return nil, err - } - defer response.Body.Close() - if response.StatusCode == http.StatusOK { - return result, nil - } else if response.StatusCode == http.StatusUnauthorized { - authenticate := response.Header.Get("WWW-Authenticate") - log.Debugf("authenticate header: %s", authenticate) - var service string - var scopes []string - //Disregard the case for hanlding multiple scopes for http call initiated from UI, as there's refactor planned. - re := regexp.MustCompile(`service=\"(.*?)\".*scope=\"(.*?)\"`) - res := re.FindStringSubmatch(authenticate) - if len(res) > 2 { - service = res[1] - scopes = append(scopes, res[2]) - } - token, err := GenTokenForUI(username, service, scopes) - if err != nil { - return nil, err - } - request, err := http.NewRequest("GET", url, nil) - if err != nil { - return nil, err - } - request.Header.Add("Authorization", "Bearer "+token) - client := &http.Client{} - client.CheckRedirect = func(req *http.Request, via []*http.Request) error { - // log.Infof("via length: %d\n", len(via)) - if len(via) >= 10 { - return fmt.Errorf("too many redirects") - } - for k, v := range via[0].Header { - if _, ok := req.Header[k]; !ok { - req.Header[k] = v - } - } - return nil - } - response, err = client.Do(request) - if err != nil { - return nil, err - } - if response.StatusCode != http.StatusOK { - errMsg := fmt.Sprintf("Unexpected return code from registry: %d", response.StatusCode) - log.Error(errMsg) - return nil, fmt.Errorf(errMsg) - } - result, err = ioutil.ReadAll(response.Body) - if err != nil { - return nil, err - } - defer response.Body.Close() - return result, nil - } else { - return nil, errors.New(string(result)) - } -} diff --git a/static/i18n/locale_en-US.ini b/static/i18n/locale_en-US.ini index 7a44eddda..981213cf0 100644 --- a/static/i18n/locale_en-US.ini +++ b/static/i18n/locale_en-US.ini @@ -82,4 +82,4 @@ index_desc_2 = 2. Efficiency: A private registry server is set up within the org index_desc_3 = 3. Access Control: RBAC (Role Based Access Control) is provided. User management can be integrated with existing enterprise identity services like AD/LDAP. index_desc_4 = 4. Audit: All access to the registry are logged and can be used for audit purpose. index_desc_5 = 5. GUI: User friendly single-pane-of-glass management console. - +index_title = An enterprise-class registry server diff --git a/static/i18n/locale_zh-CN.ini b/static/i18n/locale_zh-CN.ini index 9e49c12ae..90ea4fc04 100644 --- a/static/i18n/locale_zh-CN.ini +++ b/static/i18n/locale_zh-CN.ini @@ -82,3 +82,4 @@ index_desc_2 = 2. 效率: 搭建组织内部的私有容器Registry服务,可 index_desc_3 = 3. 访问控制: 提供基于角色的访问控制,可集成企业目前拥有的用户管理系统(如:AD/LDAP)。 index_desc_4 = 4. 审计: 所有访问Registry服务的操作均被记录,便于日后审计。 index_desc_5 = 5. 管理界面: 具有友好易用图形管理界面。 +index_title = 企业级 Registry 服务 \ No newline at end of file diff --git a/static/resources/js/item-detail.js b/static/resources/js/item-detail.js index e13075ecf..1b6ffa771 100644 --- a/static/resources/js/item-detail.js +++ b/static/resources/js/item-detail.js @@ -130,9 +130,8 @@ jQuery(function(){ data[i] = "N/A"; } } - data.Created = data.CreatedStr; - delete data.CreatedStr; - + data.Created = moment(new Date(data.Created)).format("YYYY-MM-DD HH:mm:ss"); + $("#dlgModal").dialogModal({"title": i18n.getMessage("image_details"), "content": data}); } } @@ -246,7 +245,7 @@ jQuery(function(){ var userId = userList[i].UserId; var roleId = userList[i].RoleId; - var username = userList[i].Username; + var username = userList[i].username; var roleNameList = []; for(var j = i; j < userList.length; i++, j++){ diff --git a/ui/router.go b/ui/router.go index a1f9067c1..fe3ee5d8f 100644 --- a/ui/router.go +++ b/ui/router.go @@ -19,6 +19,7 @@ import ( "github.com/vmware/harbor/api" "github.com/vmware/harbor/controllers" "github.com/vmware/harbor/service" + "github.com/vmware/harbor/service/token" "github.com/astaxie/beego" ) @@ -63,5 +64,5 @@ func initRouters() { //external service that hosted on harbor process: beego.Router("/service/notifications", &service.NotificationHandler{}) - beego.Router("/service/token", &service.TokenHandler{}) + beego.Router("/service/token", &token.Handler{}) } diff --git a/utils/registry/auth/challenge.go b/utils/registry/auth/challenge.go new file mode 100644 index 000000000..523fc1341 --- /dev/null +++ b/utils/registry/auth/challenge.go @@ -0,0 +1,32 @@ +/* + 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 auth + +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/handler.go b/utils/registry/auth/handler.go new file mode 100644 index 000000000..61550178d --- /dev/null +++ b/utils/registry/auth/handler.go @@ -0,0 +1,197 @@ +/* + 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 auth + +import ( + "encoding/json" + "errors" + "fmt" + "io/ioutil" + "net/http" + "net/url" + "strings" + + token_util "github.com/vmware/harbor/service/token" + "github.com/vmware/harbor/utils/log" + registry_errors "github.com/vmware/harbor/utils/registry/errors" +) + +const ( + // credential type + basicAuth string = "basic_auth" + secretKey string = "secret_key" +) + +// Handler authorizes the request when encounters a 401 error +type Handler interface { + // Schema : basic, bearer + Schema() string + //AuthorizeRequest adds basic auth or token auth to the header of request + AuthorizeRequest(req *http.Request, params map[string]string) error +} + +// Credential ... +type Credential interface { + // AddAuthorization adds authorization information to request + AddAuthorization(req *http.Request) +} + +type basicAuthCredential struct { + username string + password string +} + +// NewBasicAuthCredential ... +func NewBasicAuthCredential(username, password string) Credential { + return &basicAuthCredential{ + username: username, + password: password, + } +} + +func (b *basicAuthCredential) AddAuthorization(req *http.Request) { + req.SetBasicAuth(b.username, b.password) +} + +type token struct { + Token string `json:"token"` +} + +type standardTokenHandler struct { + client *http.Client + credential Credential +} + +// NewStandardTokenHandler returns a standard token handler. The handler will request a token +// from token server whose URL is specified in the "WWW-authentication" header and add it to +// the origin request +// TODO deal with https +func NewStandardTokenHandler(credential Credential) Handler { + return &standardTokenHandler{ + client: &http.Client{ + Transport: http.DefaultTransport, + }, + credential: credential, + } +} + +// Schema implements the corresponding method in interface AuthHandler +func (t *standardTokenHandler) Schema() string { + return "bearer" +} + +// AuthorizeRequest implements the corresponding method in interface AuthHandler +func (t *standardTokenHandler) AuthorizeRequest(req *http.Request, params map[string]string) error { + realm, ok := params["realm"] + if !ok { + return errors.New("no realm") + } + + service := params["service"] + scope := params["scope"] + + u, err := url.Parse(realm) + if err != nil { + return err + } + + q := u.Query() + q.Add("service", service) + + for _, s := range strings.Split(scope, " ") { + q.Add("scope", s) + } + + u.RawQuery = q.Encode() + + r, err := http.NewRequest("GET", u.String(), nil) + if err != nil { + return err + } + + t.credential.AddAuthorization(r) + + resp, err := t.client.Do(r) + if err != nil { + return err + } + + defer resp.Body.Close() + + b, err := ioutil.ReadAll(resp.Body) + if err != nil { + return err + } + + if resp.StatusCode != http.StatusOK { + return registry_errors.Error{ + StatusCode: resp.StatusCode, + Message: string(b), + } + } + + decoder := json.NewDecoder(resp.Body) + + tk := &token{} + if err = decoder.Decode(tk); err != nil { + return err + } + + req.Header.Add(http.CanonicalHeaderKey("Authorization"), fmt.Sprintf("Bearer %s", tk.Token)) + + log.Debugf("standardTokenHandler generated token successfully | %s %s", req.Method, req.URL) + + return nil +} + +type usernameTokenHandler struct { + username string +} + +// NewUsernameTokenHandler returns a handler which will generate +// a token according the user's privileges +func NewUsernameTokenHandler(username string) Handler { + return &usernameTokenHandler{ + username: username, + } +} + +// Schema implements the corresponding method in interface AuthHandler +func (u *usernameTokenHandler) Schema() string { + return "bearer" +} + +// AuthorizeRequest implements the corresponding method in interface AuthHandler +func (u *usernameTokenHandler) AuthorizeRequest(req *http.Request, params map[string]string) error { + service := params["service"] + + scopes := []string{} + scope := params["scope"] + if len(scope) != 0 { + scopes = strings.Split(scope, " ") + } + + token, err := token_util.GenTokenForUI(u.username, service, scopes) + if err != nil { + return err + } + + req.Header.Add(http.CanonicalHeaderKey("Authorization"), fmt.Sprintf("Bearer %s", token)) + + log.Debugf("usernameTokenHandler generated token successfully | %s %s", req.Method, req.URL) + + return nil +} diff --git a/utils/registry/errors/error.go b/utils/registry/errors/error.go new file mode 100644 index 000000000..60b8d6ce5 --- /dev/null +++ b/utils/registry/errors/error.go @@ -0,0 +1,38 @@ +/* + 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 errors + +import ( + "fmt" +) + +// Error : if response's status code is not 200 or does not meet requirement, +// an Error instance will be returned +type Error struct { + StatusCode int + Message string +} + +// Error ... +func (e Error) Error() string { + return fmt.Sprintf("%d %s", e.StatusCode, e.Message) +} + +// ParseError parses err, if err is type Error, convert it to Error +func ParseError(err error) (Error, bool) { + e, ok := err.(Error) + return e, ok +} diff --git a/utils/registry/httpclient.go b/utils/registry/httpclient.go new file mode 100644 index 000000000..f23a2c064 --- /dev/null +++ b/utils/registry/httpclient.go @@ -0,0 +1,116 @@ +/* + 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" + + "github.com/vmware/harbor/utils/log" + "github.com/vmware/harbor/utils/registry/auth" +) + +// NewClient returns a http.Client according to the handlers provided +func NewClient(handlers []auth.Handler) *http.Client { + transport := NewAuthTransport(http.DefaultTransport, handlers) + + return &http.Client{ + Transport: transport, + } +} + +// NewClientStandardAuthHandlerEmbeded return a http.Client which will authorize the request +// according to the credential provided and send it again when encounters a 401 error +func NewClientStandardAuthHandlerEmbeded(credential auth.Credential) *http.Client { + handlers := []auth.Handler{} + + tokenHandler := auth.NewStandardTokenHandler(credential) + + handlers = append(handlers, tokenHandler) + + return NewClient(handlers) +} + +// NewClientUsernameAuthHandlerEmbeded return a http.Client which will authorize the request +// according to the user's privileges and send it again when encounters a 401 error +func NewClientUsernameAuthHandlerEmbeded(username string) *http.Client { + handlers := []auth.Handler{} + + tokenHandler := auth.NewUsernameTokenHandler(username) + + handlers = append(handlers, tokenHandler) + + return NewClient(handlers) +} + +type authTransport struct { + transport http.RoundTripper + handlers []auth.Handler +} + +// NewAuthTransport wraps the AuthHandlers to be http.RounTripper +func NewAuthTransport(transport http.RoundTripper, handlers []auth.Handler) http.RoundTripper { + return &authTransport{ + transport: transport, + handlers: handlers, + } +} + +// RoundTrip ... +func (a *authTransport) RoundTrip(req *http.Request) (*http.Response, error) { + originResp, originErr := a.transport.RoundTrip(req) + + if originErr != nil { + return originResp, originErr + } + + log.Debugf("%d | %s %s", originResp.StatusCode, req.Method, req.URL) + + if originResp.StatusCode != http.StatusUnauthorized { + return originResp, nil + } + + challenges := auth.ParseChallengeFromResponse(originResp) + + reqChanged := false + for _, challenge := range challenges { + + scheme := challenge.Scheme + + for _, handler := range a.handlers { + if scheme != handler.Schema() { + log.Debugf("scheme not match: %s %s, skip", scheme, handler.Schema()) + continue + } + + if err := handler.AuthorizeRequest(req, challenge.Parameters); err != nil { + return nil, err + } + reqChanged = true + } + } + + if !reqChanged { + log.Warning("no handler match scheme") + return originResp, nil + } + + resp, err := a.transport.RoundTrip(req) + if err == nil { + log.Debugf("%d | %s %s", resp.StatusCode, req.Method, req.URL) + } + + return resp, err +} diff --git a/utils/registry/manifest.go b/utils/registry/manifest.go new file mode 100644 index 000000000..1976bf9c3 --- /dev/null +++ b/utils/registry/manifest.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 ( + "github.com/docker/distribution" +) + +// UnMarshal converts []byte to be distribution.Manifest +func UnMarshal(mediaType string, data []byte) (distribution.Manifest, distribution.Descriptor, error) { + return distribution.UnmarshalManifest(mediaType, data) +} diff --git a/utils/registry/registry.go b/utils/registry/registry.go new file mode 100644 index 000000000..e390c4c0d --- /dev/null +++ b/utils/registry/registry.go @@ -0,0 +1,316 @@ +/* + 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 ( + "encoding/json" + "fmt" + "io/ioutil" + "net/http" + "net/url" + + "github.com/docker/distribution/manifest" + "github.com/docker/distribution/manifest/schema1" + "github.com/docker/distribution/manifest/schema2" + "github.com/vmware/harbor/utils/registry/errors" +) + +// Registry holds information of a registry entiry +type Registry struct { + Endpoint *url.URL + client *http.Client + ub *uRLBuilder +} + +type uRLBuilder struct { + root *url.URL +} + +var ( + // ManifestVersion1 : schema 1 + ManifestVersion1 = manifest.Versioned{ + SchemaVersion: 1, + MediaType: schema1.MediaTypeManifest, + } + // ManifestVersion2 : schema 2 + ManifestVersion2 = manifest.Versioned{ + SchemaVersion: 2, + MediaType: schema2.MediaTypeManifest, + } +) + +// New returns an instance of Registry +func New(endpoint string, client *http.Client) (*Registry, error) { + u, err := url.Parse(endpoint) + if err != nil { + return nil, err + } + + return &Registry{ + Endpoint: u, + client: client, + ub: &uRLBuilder{ + root: u, + }, + }, nil +} + +// Catalog ... +func (r *Registry) Catalog() ([]string, error) { + repos := []string{} + req, err := http.NewRequest("GET", r.ub.buildCatalogURL(), nil) + if err != nil { + return repos, err + } + + resp, err := r.client.Do(req) + if err != nil { + return repos, err + } + + defer resp.Body.Close() + + b, err := ioutil.ReadAll(resp.Body) + if err != nil { + return repos, err + } + + if resp.StatusCode == http.StatusOK { + catalogResp := struct { + Repositories []string `json:"repositories"` + }{} + + if err := json.Unmarshal(b, &catalogResp); err != nil { + return repos, err + } + + repos = catalogResp.Repositories + + return repos, nil + } + + return repos, errors.Error{ + StatusCode: resp.StatusCode, + Message: string(b), + } +} + +// ListTag ... +func (r *Registry) ListTag(name string) ([]string, error) { + tags := []string{} + req, err := http.NewRequest("GET", r.ub.buildTagListURL(name), nil) + if err != nil { + return tags, err + } + + resp, err := r.client.Do(req) + if err != nil { + return tags, err + } + + defer resp.Body.Close() + + b, err := ioutil.ReadAll(resp.Body) + if err != nil { + return tags, err + } + + if resp.StatusCode == http.StatusOK { + tagsResp := struct { + Tags []string `json:"tags"` + }{} + + if err := json.Unmarshal(b, &tagsResp); err != nil { + return tags, err + } + + tags = tagsResp.Tags + + return tags, nil + } + + return tags, errors.Error{ + StatusCode: resp.StatusCode, + Message: string(b), + } + +} + +// ManifestExist ... +func (r *Registry) ManifestExist(name, reference string) (digest string, exist bool, err error) { + req, err := http.NewRequest("HEAD", r.ub.buildManifestURL(name, reference), nil) + if err != nil { + return + } + + // request Schema 2 manifest, if the registry does not support it, + // Schema 1 manifest will be returned + req.Header.Set(http.CanonicalHeaderKey("Accept"), schema2.MediaTypeManifest) + + resp, err := r.client.Do(req) + if err != nil { + return + } + + if resp.StatusCode == http.StatusOK { + exist = true + digest = resp.Header.Get(http.CanonicalHeaderKey("Docker-Content-Digest")) + return + } + + if resp.StatusCode == http.StatusNotFound { + return + } + + defer resp.Body.Close() + + b, err := ioutil.ReadAll(resp.Body) + if err != nil { + return + } + + err = errors.Error{ + StatusCode: resp.StatusCode, + Message: string(b), + } + return +} + +// PullManifest ... +func (r *Registry) PullManifest(name, reference string, version manifest.Versioned) (digest, mediaType string, payload []byte, err error) { + req, err := http.NewRequest("GET", r.ub.buildManifestURL(name, reference), nil) + if err != nil { + return + } + + // if the registry does not support schema 2, schema 1 manifest will be returned + req.Header.Set(http.CanonicalHeaderKey("Accept"), version.MediaType) + + resp, err := r.client.Do(req) + if err != nil { + return + } + + defer resp.Body.Close() + b, err := ioutil.ReadAll(resp.Body) + if err != nil { + return + } + + if resp.StatusCode == http.StatusOK { + digest = resp.Header.Get(http.CanonicalHeaderKey("Docker-Content-Digest")) + mediaType = resp.Header.Get(http.CanonicalHeaderKey("Content-Type")) + payload = b + return + } + + err = errors.Error{ + StatusCode: resp.StatusCode, + Message: string(b), + } + + return +} + +// DeleteManifest ... +func (r *Registry) DeleteManifest(name, digest string) error { + req, err := http.NewRequest("DELETE", r.ub.buildManifestURL(name, digest), nil) + if err != nil { + return err + } + + resp, err := r.client.Do(req) + if err != nil { + return err + } + + if resp.StatusCode == http.StatusAccepted { + return nil + } + + defer resp.Body.Close() + + b, err := ioutil.ReadAll(resp.Body) + if err != nil { + return err + } + + return errors.Error{ + StatusCode: resp.StatusCode, + Message: string(b), + } +} + +// DeleteTag ... +func (r *Registry) DeleteTag(name, tag string) error { + digest, exist, err := r.ManifestExist(name, tag) + if err != nil { + return err + } + + if !exist { + return errors.Error{ + StatusCode: http.StatusNotFound, + } + } + + return r.DeleteManifest(name, digest) +} + +// DeleteBlob ... +func (r *Registry) DeleteBlob(name, digest string) error { + req, err := http.NewRequest("DELETE", r.ub.buildBlobURL(name, digest), nil) + if err != nil { + return err + } + + resp, err := r.client.Do(req) + if err != nil { + return err + } + + if resp.StatusCode == http.StatusAccepted { + return nil + } + + defer resp.Body.Close() + + b, err := ioutil.ReadAll(resp.Body) + if err != nil { + return err + } + + return errors.Error{ + StatusCode: resp.StatusCode, + Message: string(b), + } +} + +func (u *uRLBuilder) buildCatalogURL() string { + return fmt.Sprintf("%s/v2/_catalog", u.root.String()) +} + +func (u *uRLBuilder) buildTagListURL(name string) string { + return fmt.Sprintf("%s/v2/%s/tags/list", u.root.String(), name) +} + +func (u *uRLBuilder) buildManifestURL(name, reference string) string { + return fmt.Sprintf("%s/v2/%s/manifests/%s", u.root.String(), name, reference) +} + +func (u *uRLBuilder) buildBlobURL(name, reference string) string { + return fmt.Sprintf("%s/v2/%s/blobs/%s", u.root.String(), name, reference) +} diff --git a/views/index.tpl b/views/index.tpl index e86e813a4..e92eaa5cb 100644 --- a/views/index.tpl +++ b/views/index.tpl @@ -16,7 +16,7 @@
An enterprise-class registry server
+{{i18n .Lang "index_title"}}