From 9d7ad6de68d2b456e7996840f222ef2ddcfe9ab8 Mon Sep 17 00:00:00 2001 From: Wenkai Yin Date: Wed, 26 Jul 2017 13:33:21 +0800 Subject: [PATCH] Add API to check whether a project can be deleted or not --- src/ui/api/harborapi_test.go | 26 ++++++++++++ src/ui/api/project.go | 78 +++++++++++++++++++++++++----------- src/ui/api/project_test.go | 43 ++++++++++++++++++++ src/ui/api/utils.go | 22 ---------- src/ui/router.go | 1 + 5 files changed, 125 insertions(+), 45 deletions(-) diff --git a/src/ui/api/harborapi_test.go b/src/ui/api/harborapi_test.go index d9f1d234a..088b0144f 100644 --- a/src/ui/api/harborapi_test.go +++ b/src/ui/api/harborapi_test.go @@ -25,6 +25,7 @@ import ( "net/http/httptest" "path/filepath" "runtime" + "strconv" "github.com/vmware/harbor/src/common/dao" "github.com/vmware/harbor/src/common/models" @@ -99,6 +100,7 @@ func init() { beego.Router("/api/users/:id/sysadmin", &UserAPI{}, "put:ToggleUserAdminRole") beego.Router("/api/projects/:id/publicity", &ProjectAPI{}, "put:ToggleProjectPublic") beego.Router("/api/projects/:id([0-9]+)/logs", &ProjectAPI{}, "get:Logs") + beego.Router("/api/projects/:id([0-9]+)/_deletable", &ProjectAPI{}, "get:Deletable") beego.Router("/api/projects/:pid([0-9]+)/members/?:mid", &ProjectMemberAPI{}, "get:Get;post:Post;delete:Delete;put:Put") beego.Router("/api/repositories", &RepositoryAPI{}) beego.Router("/api/statistics", &StatisticAPI{}) @@ -384,6 +386,30 @@ func (a testapi) ProjectLogs(prjUsr usrInfo, projectID string, query *apilib.Log return request(_sling, jsonAcceptHeader, prjUsr) } +// ProjectDeletable check whether a project can be deleted +func (a testapi) ProjectDeletable(prjUsr usrInfo, projectID int64) (int, bool, error) { + _sling := sling.New().Get(a.basePath). + Path("/api/projects/" + strconv.FormatInt(projectID, 10) + "/_deletable") + + code, body, err := request(_sling, jsonAcceptHeader, prjUsr) + if err != nil { + return 0, false, err + } + + if code != http.StatusOK { + return code, false, nil + } + + deletable := struct { + Deletable bool `json:"deletable"` + }{} + if err = json.Unmarshal(body, &deletable); err != nil { + return 0, false, err + } + + return code, deletable.Deletable, nil +} + //-------------------------Member Test---------------------------------------// //Return relevant role members of projectID diff --git a/src/ui/api/project.go b/src/ui/api/project.go index 255600929..b41110dec 100644 --- a/src/ui/api/project.go +++ b/src/ui/api/project.go @@ -30,6 +30,11 @@ import ( "time" ) +type deletableResp struct { + Deletable bool `json:"deletable"` + Message string `json:"message"` +} + // ProjectAPI handles request to /api/projects/{} /api/projects/{}/logs type ProjectAPI struct { BaseController @@ -202,22 +207,14 @@ func (p *ProjectAPI) Delete() { return } - contains, err := projectContainsRepo(p.project.Name) + result, err := deletable(p.project.ProjectID) if err != nil { - log.Errorf("failed to check whether project %s contains any repository: %v", p.project.Name, err) - p.CustomAbort(http.StatusInternalServerError, "") + p.HandleInternalServerError(fmt.Sprintf( + "failed to check the deletable of project %d: %v", p.project.ProjectID, err)) + return } - if contains { - p.CustomAbort(http.StatusPreconditionFailed, "project contains repositores, can not be deleted") - } - - contains, err = projectContainsPolicy(p.project.ProjectID) - if err != nil { - log.Errorf("failed to check whether project %s contains any policy: %v", p.project.Name, err) - p.CustomAbort(http.StatusInternalServerError, "") - } - if contains { - p.CustomAbort(http.StatusPreconditionFailed, "project contains policies, can not be deleted") + if !result.Deletable { + p.CustomAbort(http.StatusPreconditionFailed, result.Message) } if err = p.ProjectMgr.Delete(p.project.ProjectID); err != nil { @@ -239,22 +236,57 @@ func (p *ProjectAPI) Delete() { }() } -func projectContainsRepo(name string) (bool, error) { - repositories, err := getReposByProject(name) - if err != nil { - return false, err +// Deletable ... +func (p *ProjectAPI) Deletable() { + if !p.SecurityCtx.IsAuthenticated() { + p.HandleUnauthorized() + return } - return len(repositories) > 0, nil + if !p.SecurityCtx.HasAllPerm(p.project.ProjectID) { + p.HandleForbidden(p.SecurityCtx.GetUsername()) + return + } + + result, err := deletable(p.project.ProjectID) + if err != nil { + p.HandleInternalServerError(fmt.Sprintf( + "failed to check the deletable of project %d: %v", p.project.ProjectID, err)) + return + } + + p.Data["json"] = result + p.ServeJSON() } -func projectContainsPolicy(id int64) (bool, error) { - policies, err := dao.GetRepPolicyByProject(id) +func deletable(projectID int64) (*deletableResp, error) { + count, err := dao.GetTotalOfRepositoriesByProject([]int64{projectID}, "") if err != nil { - return false, err + return nil, err } - return len(policies) > 0, nil + if count > 0 { + return &deletableResp{ + Deletable: false, + Message: "the project contains repositories, can not be deleled", + }, nil + } + + policies, err := dao.GetRepPolicyByProject(projectID) + if err != nil { + return nil, err + } + + if len(policies) > 0 { + return &deletableResp{ + Deletable: false, + Message: "the project contains replication rules, can not be deleled", + }, nil + } + + return &deletableResp{ + Deletable: true, + }, nil } // List ... diff --git a/src/ui/api/project_test.go b/src/ui/api/project_test.go index 0bc143d80..bd66005ed 100644 --- a/src/ui/api/project_test.go +++ b/src/ui/api/project_test.go @@ -15,11 +15,15 @@ package api import ( "fmt" + "net/http" "strconv" "testing" "time" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/vmware/harbor/src/common/dao" + "github.com/vmware/harbor/src/common/models" "github.com/vmware/harbor/tests/apitests/apilib" ) @@ -361,3 +365,42 @@ func TestProjectLogsFilter(t *testing.T) { } fmt.Printf("\n") } + +func TestDeletable(t *testing.T) { + apiTest := newHarborAPI() + + project := models.Project{ + Name: "project_for_test_deletable", + OwnerID: 1, + } + id, err := dao.AddProject(project) + require.Nil(t, err) + + // non-exist project + code, del, err := apiTest.ProjectDeletable(*admin, 1000) + assert.Nil(t, err) + assert.Equal(t, http.StatusNotFound, code) + + // unauthorized + code, del, err = apiTest.ProjectDeletable(*unknownUsr, id) + assert.Nil(t, err) + assert.Equal(t, http.StatusUnauthorized, code) + + // can be deleted + code, del, err = apiTest.ProjectDeletable(*admin, id) + assert.Nil(t, err) + assert.Equal(t, http.StatusOK, code) + assert.True(t, del) + + err = dao.AddRepository(models.RepoRecord{ + Name: project.Name + "/golang", + ProjectID: id, + }) + require.Nil(t, err) + + // can not be deleted as contains repository + code, del, err = apiTest.ProjectDeletable(*admin, id) + assert.Nil(t, err) + assert.Equal(t, http.StatusOK, code) + assert.False(t, del) +} diff --git a/src/ui/api/utils.go b/src/ui/api/utils.go index a639057f9..15b17c4be 100644 --- a/src/ui/api/utils.go +++ b/src/ui/api/utils.go @@ -416,28 +416,6 @@ func buildReplicationActionURL() string { return fmt.Sprintf("%s/api/jobs/replication/actions", url) } -func getReposByProject(name string, keyword ...string) ([]string, error) { - repositories := []string{} - - repos, err := dao.GetRepositoryByProjectName(name) - if err != nil { - return repositories, err - } - - needMatchKeyword := len(keyword) > 0 && len(keyword[0]) != 0 - - for _, repo := range repos { - if needMatchKeyword && - !strings.Contains(repo.Name, keyword[0]) { - continue - } - - repositories = append(repositories, repo.Name) - } - - return repositories, nil -} - func repositoryExist(name string, client *registry.Repository) (bool, error) { tags, err := client.ListTag() if err != nil { diff --git a/src/ui/router.go b/src/ui/router.go index 127c02565..24b912b56 100644 --- a/src/ui/router.go +++ b/src/ui/router.go @@ -85,6 +85,7 @@ func initRouters() { beego.Router("/api/search", &api.SearchAPI{}) beego.Router("/api/projects/", &api.ProjectAPI{}, "get:List;post:Post") beego.Router("/api/projects/:id([0-9]+)/logs", &api.ProjectAPI{}, "get:Logs") + beego.Router("/api/projects/:id([0-9]+)/_deletable", &api.ProjectAPI{}, "get:Deletable") beego.Router("/api/internal/syncregistry", &api.InternalAPI{}, "post:SyncRegistry") beego.Router("/api/repositories", &api.RepositoryAPI{}, "get:Get") beego.Router("/api/repositories/scanAll", &api.RepositoryAPI{}, "post:ScanAll")