diff --git a/src/common/api/base.go b/src/common/api/base.go index 6eefc3d5e..ee55fd0a8 100644 --- a/src/common/api/base.go +++ b/src/common/api/base.go @@ -39,6 +39,17 @@ type BaseAPI struct { beego.Controller } +// GetStringFromPath gets the param from path and returns it as string +func (b *BaseAPI) GetStringFromPath(key string) string { + return b.Ctx.Input.Param(key) +} + +// GetInt64FromPath gets the param from path and returns it as int64 +func (b *BaseAPI) GetInt64FromPath(key string) (int64, error) { + value := b.Ctx.Input.Param(key) + return strconv.ParseInt(value, 10, 64) +} + // HandleNotFound ... func (b *BaseAPI) HandleNotFound(text string) { log.Info(text) diff --git a/src/common/const.go b/src/common/const.go index 0bc3ae2f4..0598dc2a3 100644 --- a/src/common/const.go +++ b/src/common/const.go @@ -24,10 +24,9 @@ const ( LDAPScopeOnelevel = "2" LDAPScopeSubtree = "3" - RoleSystemAdmin = 1 - RoleProjectAdmin = 2 - RoleDeveloper = 3 - RoleGuest = 4 + RoleProjectAdmin = 1 + RoleDeveloper = 2 + RoleGuest = 3 DeployModeStandAlone = "standalone" DeployModeIntegration = "integration" diff --git a/src/common/dao/dao_test.go b/src/common/dao/dao_test.go index 5f7f48164..12439a813 100644 --- a/src/common/dao/dao_test.go +++ b/src/common/dao/dao_test.go @@ -801,32 +801,8 @@ func TestProjectPermission(t *testing.T) { } } -func TestGetTotalOfUserRelevantProjects(t *testing.T) { - total, err := GetTotalOfUserRelevantProjects(currentUser.UserID, "") - if err != nil { - t.Fatalf("failed to get total of user relevant projects: %v", err) - } - - if total != 1 { - t.Errorf("unexpected total: %d != 1", total) - } -} - -func TestGetUserRelevantProjects(t *testing.T) { - projects, err := GetUserRelevantProjects(currentUser.UserID, "") - if err != nil { - t.Errorf("Error occurred in GetUserRelevantProjects: %v", err) - } - if len(projects) != 1 { - t.Errorf("Expected length of relevant projects is 1, but actual: %d, the projects: %+v", len(projects), projects) - } - if projects[0].Name != projectName { - t.Errorf("Expected project name in the list: %s, actual: %s", projectName, projects[1].Name) - } -} - func TestGetTotalOfProjects(t *testing.T) { - total, err := GetTotalOfProjects("") + total, err := GetTotalOfProjects("", "", "", "", 0) if err != nil { t.Fatalf("failed to get total of projects: %v", err) } @@ -837,7 +813,7 @@ func TestGetTotalOfProjects(t *testing.T) { } func TestGetProjects(t *testing.T) { - projects, err := GetProjects("") + projects, err := GetProjects("", "", "", "", 0, 0, 0) if err != nil { t.Errorf("Error occurred in GetAllProjects: %v", err) } @@ -850,7 +826,7 @@ func TestGetProjects(t *testing.T) { } func TestGetPublicProjects(t *testing.T) { - projects, err := GetProjects("", 1) + projects, err := GetProjects("", "", "true", "", 0, 0, 0) if err != nil { t.Errorf("Error occurred in getProjects: %v", err) } diff --git a/src/common/dao/project.go b/src/common/dao/project.go index f0c5aaf3b..9496c8892 100644 --- a/src/common/dao/project.go +++ b/src/common/dao/project.go @@ -15,6 +15,7 @@ package dao import ( + "github.com/vmware/harbor/src/common" "github.com/vmware/harbor/src/common/models" "fmt" @@ -159,7 +160,7 @@ func ToggleProjectPublicity(projectID int64, publicity int) error { // which satisfies the following conditions: // 1. the project is not deleted // 2. the prject is public or the user is a member of the project -func SearchProjects(userID int) ([]models.Project, error) { +func SearchProjects(userID int) ([]*models.Project, error) { o := GetOrmer() sql := @@ -171,7 +172,7 @@ func SearchProjects(userID int) ([]models.Project, error) { where (pm.user_id = ? or p.public = 1) and p.deleted = 0 ` - var projects []models.Project + var projects []*models.Project if _, err := o.Raw(sql, userID).QueryRows(&projects); err != nil { return nil, err @@ -180,101 +181,104 @@ func SearchProjects(userID int) ([]models.Project, error) { return projects, nil } -//GetTotalOfUserRelevantProjects returns the total count of -// user relevant projects -func GetTotalOfUserRelevantProjects(userID int, projectName string) (int64, error) { - o := GetOrmer() - sql := `select count(*) from project p - left join project_member pm - on p.project_id = pm.project_id - where p.deleted = 0 and pm.user_id= ?` +// GetTotalOfProjects returns the total count of projects +// according to the query conditions +func GetTotalOfProjects(owner, name, public, member string, + role int) (int64, error) { + sql, params := queryConditions(owner, name, public, member, role) - queryParam := []interface{}{} - queryParam = append(queryParam, userID) - if projectName != "" { - sql += " and p.name like ? " - queryParam = append(queryParam, "%"+escape(projectName)+"%") - } + sql = `select count(*) ` + sql var total int64 - err := o.Raw(sql, queryParam).QueryRow(&total) - + err := GetOrmer().Raw(sql, params).QueryRow(&total) return total, err } -// GetUserRelevantProjects returns the user relevant projects -// args[0]: public, args[1]: limit, args[2]: offset -func GetUserRelevantProjects(userID int, projectName string, args ...int64) ([]models.Project, error) { - return getProjects(userID, projectName, args...) -} +// GetProjects returns a project list according to the query conditions +func GetProjects(owner, name, public, member string, + role int, page, size int64) ([]*models.Project, error) { + sql, params := queryConditions(owner, name, public, member, role) -// GetTotalOfProjects returns the total count of projects -func GetTotalOfProjects(name string, public ...int) (int64, error) { - qs := GetOrmer(). - QueryTable(new(models.Project)). - Filter("Deleted", 0) + sql = `select distinct p.project_id, p.name, p.public, p.owner_id, + p.creation_time, p.update_time ` + sql + if size > 0 { + sql += ` limit ?` + params = append(params, size) - if len(name) > 0 { - qs = qs.Filter("Name__icontains", name) + if page > 0 { + sql += ` offset ?` + params = append(params, (page-1)*size) + } } - if len(public) > 0 { - qs = qs.Filter("Public", public[0]) - } - - return qs.Count() -} - -// GetProjects returns project list -// args[0]: public, args[1]: limit, args[2]: offset -func GetProjects(name string, args ...int64) ([]models.Project, error) { - return getProjects(0, name, args...) -} - -func getProjects(userID int, name string, args ...int64) ([]models.Project, error) { - projects := []models.Project{} - - o := GetOrmer() - sql := "" - queryParam := []interface{}{} - - if userID != 0 { //get user's projects - sql = `select distinct p.project_id, p.owner_id, p.name, - p.creation_time, p.update_time, p.public, pm.role role - from project p - left join project_member pm - on p.project_id = pm.project_id - where p.deleted = 0 and pm.user_id= ?` - queryParam = append(queryParam, userID) - } else { // get all projects - sql = `select * from project p where p.deleted = 0 ` - } - - if name != "" { - sql += ` and p.name like ? ` - queryParam = append(queryParam, "%"+escape(name)+"%") - } - - switch len(args) { - case 1: - sql += ` and p.public = ?` - queryParam = append(queryParam, args[0]) - sql += ` order by p.name ` - case 2: - sql += ` order by p.name ` - sql = paginateForRawSQL(sql, args[0], args[1]) - case 3: - sql += ` and p.public = ?` - queryParam = append(queryParam, args[0]) - sql += ` order by p.name ` - sql = paginateForRawSQL(sql, args[1], args[2]) - } - - _, err := o.Raw(sql, queryParam).QueryRows(&projects) - + var projects []*models.Project + _, err := GetOrmer().Raw(sql, params).QueryRows(&projects) return projects, err } +func queryConditions(owner, name, public, member string, + role int) (string, []interface{}) { + params := []interface{}{} + + sql := ` from project p` + + if len(owner) != 0 { + sql += ` join user u1 + on p.owner_id = u1.user_id` + } + + if len(member) != 0 { + sql += ` join project_member pm + on p.project_id = pm.project_id + join user u2 + on pm.user_id=u2.user_id` + } + sql += ` where p.deleted=0` + + if len(owner) != 0 { + sql += ` and u1.username=?` + params = append(params, owner) + } + + if len(name) != 0 { + sql += ` and p.name like ?` + params = append(params, "%"+escape(name)+"%") + } + + if len(public) != 0 { + sql += ` and p.public = ?` + if public == "true" { + params = append(params, 1) + } else { + params = append(params, 0) + } + } + + if len(member) != 0 { + sql += ` and u2.username=?` + params = append(params, member) + + if role > 0 { + sql += ` and pm.role = ?` + roleID := 0 + switch role { + case common.RoleProjectAdmin: + roleID = 1 + case common.RoleDeveloper: + roleID = 2 + case common.RoleGuest: + roleID = 3 + + } + params = append(params, roleID) + } + } + + sql += ` order by p.name` + + return sql, params +} + // DeleteProject ... func DeleteProject(id int64) error { project, err := GetProjectByID(id) diff --git a/src/common/models/accesslog.go b/src/common/models/accesslog.go index 52a447a33..f151a3077 100644 --- a/src/common/models/accesslog.go +++ b/src/common/models/accesslog.go @@ -19,6 +19,7 @@ import ( ) // AccessLog holds information about logs which are used to record the actions that user take to the resourses. +// TODO remove useless attrs type AccessLog struct { LogID int `orm:"pk;auto;column(log_id)" json:"log_id"` Username string `orm:"column(username)" json:"username"` diff --git a/src/common/models/project.go b/src/common/models/project.go index 33caea050..a85ec465c 100644 --- a/src/common/models/project.go +++ b/src/common/models/project.go @@ -19,6 +19,7 @@ import ( ) // Project holds the details of a project. +// TODO remove useless attrs type Project struct { ProjectID int64 `orm:"pk;auto;column(project_id)" json:"project_id"` OwnerID int `orm:"column(owner_id)" json:"owner_id"` @@ -39,7 +40,7 @@ type Project struct { // ProjectSorter holds an array of projects type ProjectSorter struct { - Projects []Project + Projects []*Project } // Len returns the length of array in ProjectSorter diff --git a/src/common/security/rbac/context_test.go b/src/common/security/rbac/context_test.go index 551664edf..371530871 100644 --- a/src/common/security/rbac/context_test.go +++ b/src/common/security/rbac/context_test.go @@ -15,6 +15,7 @@ package rbac import ( + "fmt" "testing" "github.com/stretchr/testify/assert" @@ -80,13 +81,34 @@ func (f *fakePM) Exist(projectIDOrName interface{}) bool { } // nil implement -func (f *fakePM) GetPublic() []models.Project { - return []models.Project{} +func (f *fakePM) GetPublic() []*models.Project { + return []*models.Project{} } // nil implement -func (f *fakePM) GetByMember(username string) []models.Project { - return []models.Project{} +func (f *fakePM) GetByMember(username string) []*models.Project { + return []*models.Project{} +} + +// nil implement +func (f *fakePM) Create(*models.Project) (int64, error) { + return 0, fmt.Errorf("not support") +} + +// nil implement +func (f *fakePM) Delete(projectIDOrName interface{}) error { + return fmt.Errorf("not support") +} + +// nil implement +func (f *fakePM) Update(projectIDOrName interface{}, project *models.Project) error { + return fmt.Errorf("not support") +} + +// nil implement +func (f *fakePM) GetAll(owner, name, public, member string, role int, page, + size int64) ([]*models.Project, int64) { + return []*models.Project{}, 0 } func TestIsAuthenticated(t *testing.T) { diff --git a/src/jobservice/replication/delete.go b/src/jobservice/replication/delete.go index 0f628c9ac..a57897eaa 100644 --- a/src/jobservice/replication/delete.go +++ b/src/jobservice/replication/delete.go @@ -90,7 +90,7 @@ func (d *Deleter) enter() (string, error) { // delete repository if len(d.tags) == 0 { - u := url + d.repository + "/tags" + u := url + d.repository if err := del(u, d.dstUsr, d.dstPwd, d.insecure); err != nil { if err == errNotFound { d.logger.Warningf("repository %s does not exist on %s", d.repository, d.dstURL) diff --git a/src/ui/api/project.go b/src/ui/api/project.go index 93243fe7a..5505b5ee0 100644 --- a/src/ui/api/project.go +++ b/src/ui/api/project.go @@ -19,7 +19,7 @@ import ( "net/http" "regexp" - "github.com/vmware/harbor/src/common/api" + "github.com/vmware/harbor/src/common" "github.com/vmware/harbor/src/common/dao" "github.com/vmware/harbor/src/common/models" "github.com/vmware/harbor/src/common/utils/log" @@ -31,10 +31,7 @@ import ( // ProjectAPI handles request to /api/projects/{} /api/projects/{}/logs type ProjectAPI struct { - api.BaseAPI - userID int - projectID int64 - projectName string + BaseController } type projectReq struct { @@ -47,35 +44,11 @@ const projectNameMinLen int = 2 const restrictedNameChars = `[a-z0-9]+(?:[._-][a-z0-9]+)*` const dupProjectPattern = `Duplicate entry '\w+' for key 'name'` -// Prepare validates the URL and the user -func (p *ProjectAPI) Prepare() { - idStr := p.Ctx.Input.Param(":id") - if len(idStr) > 0 { - var err error - p.projectID, err = strconv.ParseInt(idStr, 10, 64) - if err != nil { - log.Errorf("Error parsing project id: %s, error: %v", idStr, err) - p.CustomAbort(http.StatusBadRequest, "invalid project id") - } - - project, err := dao.GetProjectByID(p.projectID) - if err != nil { - log.Errorf("failed to get project %d: %v", p.projectID, err) - p.CustomAbort(http.StatusInternalServerError, "Internal error.") - } - if project == nil { - p.CustomAbort(http.StatusNotFound, fmt.Sprintf("project does not exist, id: %v", p.projectID)) - } - p.projectName = project.Name - } -} - // Post ... func (p *ProjectAPI) Post() { - p.userID = p.ValidateUser() - isSysAdmin, err := dao.IsAdminRole(p.userID) - if err != nil { - log.Errorf("Failed to check admin role: %v", err) + if !p.SecurityCtx.IsAuthenticated() { + p.HandleUnauthorized() + return } onlyAdmin, err := config.OnlyAdminCreateProject() @@ -83,31 +56,30 @@ func (p *ProjectAPI) Post() { log.Errorf("failed to determine whether only admin can create projects: %v", err) p.CustomAbort(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError)) } - if !isSysAdmin && onlyAdmin { + if onlyAdmin && !p.SecurityCtx.IsSysAdmin() { log.Errorf("Only sys admin can create project") p.RenderError(http.StatusForbidden, "Only system admin can create project") return } - var req projectReq - p.DecodeJSONReq(&req) - public := req.Public - err = validateProjectReq(req) + var pro projectReq + p.DecodeJSONReq(&pro) + err = validateProjectReq(pro) if err != nil { log.Errorf("Invalid project request, error: %v", err) p.RenderError(http.StatusBadRequest, fmt.Sprintf("invalid request: %v", err)) return } - projectName := req.ProjectName - exist, err := dao.ProjectExists(projectName) - if err != nil { - log.Errorf("Error happened checking project existence in db, error: %v, project name: %s", err, projectName) - } - if exist { + + if p.ProjectMgr.Exist(pro.ProjectName) { p.RenderError(http.StatusConflict, "") return } - project := models.Project{OwnerID: p.userID, Name: projectName, CreationTime: time.Now(), Public: public} - projectID, err := dao.AddProject(project) + + projectID, err := p.ProjectMgr.Create(&models.Project{ + Name: pro.ProjectName, + Public: pro.Public, + OwnerName: p.SecurityCtx.GetUsername(), + }) if err != nil { log.Errorf("Failed to add project, error: %v", err) dup, _ := regexp.MatchString(dupProjectPattern, err.Error()) @@ -120,24 +92,15 @@ func (p *ProjectAPI) Post() { } go func() { - user, err := dao.GetUser(models.User{ - UserID: p.userID, - }) - if err != nil { - log.Errorf("failed to get user by ID %d: %v", p.userID, err) - p.CustomAbort(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError)) - } - - accessLog := models.AccessLog{ - Username: user.Username, - ProjectID: projectID, - RepoName: project.Name + "/", - RepoTag: "N/A", - GUID: "N/A", - Operation: "create", - OpTime: time.Now(), - } - if err = dao.AddAccessLog(accessLog); err != nil { + if err = dao.AddAccessLog( + models.AccessLog{ + Username: p.SecurityCtx.GetUsername(), + ProjectID: projectID, + RepoName: pro.ProjectName + "/", + RepoTag: "N/A", + Operation: "create", + OpTime: time.Now(), + }); err != nil { log.Errorf("failed to add access log: %v", err) p.CustomAbort(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError)) } @@ -148,40 +111,48 @@ func (p *ProjectAPI) Post() { // Head ... func (p *ProjectAPI) Head() { - projectName := p.GetString("project_name") - if len(projectName) == 0 { - p.CustomAbort(http.StatusBadRequest, "project_name is needed") - } - - project, err := dao.GetProjectByName(projectName) - if err != nil { - log.Errorf("error occurred in GetProjectByName: %v", err) - p.CustomAbort(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError)) - } - - // only public project can be Headed by user without login - if project != nil && project.Public == 1 { + name := p.GetString("project_name") + if len(name) == 0 { + p.HandleBadRequest("project_name is needed") return } - _ = p.ValidateUser() + project := p.ProjectMgr.Get(name) if project == nil { - p.CustomAbort(http.StatusNotFound, http.StatusText(http.StatusNotFound)) + p.HandleNotFound(fmt.Sprintf("project %s not found", name)) + return } } // Get ... func (p *ProjectAPI) Get() { - project, err := dao.GetProjectByID(p.projectID) - if err != nil { - log.Errorf("failed to get project %d: %v", p.projectID, err) - p.CustomAbort(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError)) + id, err := p.GetInt64FromPath(":id") + if err != nil || id <= 0 { + text := "invalid project ID: " + if err != nil { + text += err.Error() + } else { + text += fmt.Sprintf("%d", id) + } + p.HandleBadRequest(text) + return + } + + project := p.ProjectMgr.Get(id) + if project == nil { + p.HandleNotFound(fmt.Sprintf("project %d not found", id)) + return } if project.Public == 0 { - userID := p.ValidateUser() - if !checkProjectPermission(userID, p.projectID) { - p.CustomAbort(http.StatusUnauthorized, http.StatusText(http.StatusUnauthorized)) + if !p.SecurityCtx.IsAuthenticated() { + p.HandleUnauthorized() + return + } + + if !p.SecurityCtx.HasReadPerm(id) { + p.HandleForbidden(p.SecurityCtx.GetUsername()) + return } } @@ -191,52 +162,63 @@ func (p *ProjectAPI) Get() { // Delete ... func (p *ProjectAPI) Delete() { - if p.projectID == 0 { - p.CustomAbort(http.StatusBadRequest, "project ID is required") + id, err := p.GetInt64FromPath(":id") + if err != nil || id <= 0 { + text := "invalid project ID: " + if err != nil { + text += err.Error() + } else { + text += fmt.Sprintf("%d", id) + } + p.HandleBadRequest(text) + return } - userID := p.ValidateUser() - - if !hasProjectAdminRole(userID, p.projectID) { - p.CustomAbort(http.StatusForbidden, "") + project := p.ProjectMgr.Get(id) + if project == nil { + p.HandleNotFound(fmt.Sprintf("project %d not found", id)) + return } - contains, err := projectContainsRepo(p.projectName) + if !p.SecurityCtx.IsAuthenticated() { + p.HandleUnauthorized() + return + } + + if !p.SecurityCtx.HasAllPerm(id) { + p.HandleForbidden(p.SecurityCtx.GetUsername()) + return + } + + contains, err := projectContainsRepo(project.Name) if err != nil { - log.Errorf("failed to check whether project %s contains any repository: %v", p.projectName, err) + log.Errorf("failed to check whether project %s contains any repository: %v", project.Name, err) p.CustomAbort(http.StatusInternalServerError, "") } if contains { p.CustomAbort(http.StatusPreconditionFailed, "project contains repositores, can not be deleted") } - contains, err = projectContainsPolicy(p.projectID) + contains, err = projectContainsPolicy(id) if err != nil { - log.Errorf("failed to check whether project %s contains any policy: %v", p.projectName, err) + log.Errorf("failed to check whether project %s contains any policy: %v", project.Name, err) p.CustomAbort(http.StatusInternalServerError, "") } if contains { p.CustomAbort(http.StatusPreconditionFailed, "project contains policies, can not be deleted") } - if err = dao.DeleteProject(p.projectID); err != nil { - log.Errorf("failed to delete project %d: %v", p.projectID, err) - p.CustomAbort(http.StatusInternalServerError, "") + if err = p.ProjectMgr.Delete(id); err != nil { + p.HandleInternalServerError( + fmt.Sprintf("failed to delete project %d: %v", id, err)) + return } go func() { - user, err := dao.GetUser(models.User{ - UserID: userID, - }) - if err != nil { - log.Errorf("failed to get user by ID %d: %v", userID, err) - p.CustomAbort(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError)) - } - if err := dao.AddAccessLog(models.AccessLog{ - Username: user.Username, - ProjectID: p.projectID, - RepoName: p.projectName + "/", + Username: p.SecurityCtx.GetUsername(), + ProjectID: id, + RepoName: project.Name + "/", RepoTag: "N/A", Operation: "delete", OpTime: time.Now(), @@ -265,139 +247,159 @@ func projectContainsPolicy(id int64) (bool, error) { } // List ... +// TODO refacter pattern to: +// /api/repositories?owner=xxx&name=xxx&public=true&member=xxx&role=1&page=1&size=3 func (p *ProjectAPI) List() { - var total int64 - var public int - var err error - page, pageSize := p.GetPaginationParams() + // query conditions: + var ( + owner string // the username of project owner + name string // the project name + public string // the project is public or not + member string // the username of the member + role int // role of the member specified by member parameter + page int64 // pagination + size int64 // pagination + ) - var projectList []models.Project - projectName := p.GetString("project_name") - - isPublic := p.GetString("is_public") - if len(isPublic) > 0 { - public, err = strconv.Atoi(isPublic) - if err != nil { - log.Errorf("Error parsing public property: %v, error: %v", isPublic, err) - p.CustomAbort(http.StatusBadRequest, "invalid project Id") + name = p.GetString("project_name") + public = p.GetString("is_public") + if len(public) != 0 { + if public != "0" && public != "1" { + p.HandleBadRequest("is_public should be 0 or 1") + return + } + if public == "1" { + public = "true" } } - isAdmin := false - if public == 1 { - total, err = dao.GetTotalOfProjects(projectName, 1) - if err != nil { - log.Errorf("failed to get total of projects: %v", err) - p.CustomAbort(http.StatusInternalServerError, "") - } - projectList, err = dao.GetProjects(projectName, 1, pageSize, pageSize*(page-1)) - if err != nil { - log.Errorf("failed to get projects: %v", err) - p.CustomAbort(http.StatusInternalServerError, "") - } - } else { + + page, size = p.GetPaginationParams() + + if public != "true" { //if the request is not for public projects, user must login or provide credential - p.userID = p.ValidateUser() - isAdmin, err = dao.IsAdminRole(p.userID) - if err != nil { - log.Errorf("Error occured in check admin, error: %v", err) - p.CustomAbort(http.StatusInternalServerError, "Internal error.") + if !p.SecurityCtx.IsAuthenticated() { + p.HandleUnauthorized() + return } - if isAdmin { - total, err = dao.GetTotalOfProjects(projectName) - if err != nil { - log.Errorf("failed to get total of projects: %v", err) - p.CustomAbort(http.StatusInternalServerError, "") - } - projectList, err = dao.GetProjects(projectName, pageSize, pageSize*(page-1)) - if err != nil { - log.Errorf("failed to get projects: %v", err) - p.CustomAbort(http.StatusInternalServerError, "") - } - } else { - total, err = dao.GetTotalOfUserRelevantProjects(p.userID, projectName) - if err != nil { - log.Errorf("failed to get total of projects: %v", err) - p.CustomAbort(http.StatusInternalServerError, "") - } - projectList, err = dao.GetUserRelevantProjects(p.userID, projectName, pageSize, pageSize*(page-1)) - if err != nil { - log.Errorf("failed to get projects: %v", err) - p.CustomAbort(http.StatusInternalServerError, "") - } + + public = "" + member = p.SecurityCtx.GetUsername() + + if p.SecurityCtx.IsSysAdmin() { + member = "" } } - for i := 0; i < len(projectList); i++ { - if public != 1 { - roles, err := dao.GetUserProjectRoles(p.userID, projectList[i].ProjectID) - if err != nil { - log.Errorf("failed to get user's project role: %v", err) - p.CustomAbort(http.StatusInternalServerError, "") - } + projects, total := p.ProjectMgr.GetAll(owner, name, public, member, + role, page, size) + + for _, project := range projects { + if public != "true" { + roles := p.ProjectMgr.GetRoles(p.SecurityCtx.GetUsername(), project.ProjectID) if len(roles) != 0 { - projectList[i].Role = roles[0].RoleID + project.Role = roles[0] } - if projectList[i].Role == models.PROJECTADMIN || - isAdmin { - projectList[i].Togglable = true + if project.Role == common.RoleProjectAdmin || + p.SecurityCtx.IsSysAdmin() { + project.Togglable = true } } - repos, err := dao.GetRepositoryByProjectName(projectList[i].Name) + repos, err := dao.GetRepositoryByProjectName(project.Name) if err != nil { - log.Errorf("failed to get repositories of project %s: %v", projectList[i].Name, err) + log.Errorf("failed to get repositories of project %s: %v", project.Name, err) p.CustomAbort(http.StatusInternalServerError, "") } - projectList[i].RepoCount = len(repos) + project.RepoCount = len(repos) } - p.SetPaginationHeader(total, page, pageSize) - p.Data["json"] = projectList + p.SetPaginationHeader(total, page, size) + p.Data["json"] = projects p.ServeJSON() } // ToggleProjectPublic ... func (p *ProjectAPI) ToggleProjectPublic() { - p.userID = p.ValidateUser() + id, err := p.GetInt64FromPath(":id") + if err != nil || id <= 0 { + text := "invalid project ID: " + if err != nil { + text += err.Error() + } else { + text += fmt.Sprintf("%d", id) + } + p.HandleBadRequest(text) + return + } + + project := p.ProjectMgr.Get(id) + if project == nil { + p.HandleNotFound(fmt.Sprintf("project %d not found", id)) + return + } + + if !p.SecurityCtx.IsAuthenticated() { + p.HandleUnauthorized() + return + } + + if !p.SecurityCtx.HasAllPerm(id) { + p.HandleForbidden(p.SecurityCtx.GetUsername()) + return + } + var req projectReq - - projectID, err := strconv.ParseInt(p.Ctx.Input.Param(":id"), 10, 64) - if err != nil { - log.Errorf("Error parsing project id: %d, error: %v", projectID, err) - p.RenderError(http.StatusBadRequest, "invalid project id") - return - } - p.DecodeJSONReq(&req) - public := req.Public - if !isProjectAdmin(p.userID, projectID) { - log.Warningf("Current user, id: %d does not have project admin role for project, id: %d", p.userID, projectID) - p.RenderError(http.StatusForbidden, "") + if req.Public != 0 && req.Public != 1 { + p.HandleBadRequest("public should be 0 or 1") return } - err = dao.ToggleProjectPublicity(p.projectID, public) - if err != nil { - log.Errorf("Error while updating project, project id: %d, error: %v", projectID, err) - p.RenderError(http.StatusInternalServerError, "Failed to update project") + + if err := p.ProjectMgr.Update(id, &models.Project{ + Public: req.Public, + }); err != nil { + p.HandleInternalServerError(fmt.Sprintf("failed to update project %d: %v", id, err)) + return } } // FilterAccessLog handles GET to /api/projects/{}/logs func (p *ProjectAPI) FilterAccessLog() { - p.userID = p.ValidateUser() + id, err := p.GetInt64FromPath(":id") + if err != nil || id <= 0 { + text := "invalid project ID: " + if err != nil { + text += err.Error() + } else { + text += fmt.Sprintf("%d", id) + } + p.HandleBadRequest(text) + return + } + + project := p.ProjectMgr.Get(id) + if project == nil { + p.HandleNotFound(fmt.Sprintf("project %d not found", id)) + return + } + + if !p.SecurityCtx.IsAuthenticated() { + p.HandleUnauthorized() + return + } + + if !p.SecurityCtx.HasReadPerm(id) { + p.HandleForbidden(p.SecurityCtx.GetUsername()) + return + } var query models.AccessLog p.DecodeJSONReq(&query) - if !checkProjectPermission(p.userID, p.projectID) { - log.Warningf("Current user, user id: %d does not have permission to read accesslog of project, id: %d", p.userID, p.projectID) - p.RenderError(http.StatusForbidden, "") - return - } - query.ProjectID = p.projectID + query.ProjectID = id query.BeginTime = time.Unix(query.BeginTimestamp, 0) query.EndTime = time.Unix(query.EndTimestamp, 0) @@ -422,34 +424,7 @@ func (p *ProjectAPI) FilterAccessLog() { p.ServeJSON() } -func isProjectAdmin(userID int, pid int64) bool { - isSysAdmin, err := dao.IsAdminRole(userID) - if err != nil { - log.Errorf("Error occurred in IsAdminRole, returning false, error: %v", err) - return false - } - - if isSysAdmin { - return true - } - - rolelist, err := dao.GetUserProjectRoles(userID, pid) - if err != nil { - log.Errorf("Error occurred in GetUserProjectRoles, returning false, error: %v", err) - return false - } - - hasProjectAdminRole := false - for _, role := range rolelist { - if role.RoleID == models.PROJECTADMIN { - hasProjectAdminRole = true - break - } - } - - return hasProjectAdminRole -} - +// TODO move this to package models func validateProjectReq(req projectReq) error { pn := req.ProjectName if isIllegalLength(req.ProjectName, projectNameMinLen, projectNameMaxLen) { diff --git a/src/ui/api/project_test.go b/src/ui/api/project_test.go index 849703795..3f725af6a 100644 --- a/src/ui/api/project_test.go +++ b/src/ui/api/project_test.go @@ -15,11 +15,12 @@ package api import ( "fmt" - "github.com/stretchr/testify/assert" - "github.com/vmware/harbor/tests/apitests/apilib" "strconv" "testing" "time" + + "github.com/stretchr/testify/assert" + "github.com/vmware/harbor/tests/apitests/apilib" ) var addProject *apilib.ProjectReq @@ -264,18 +265,8 @@ func TestProHead(t *testing.T) { } else { assert.Equal(int(404), httpStatusCode, "httpStatusCode should be 404") } - //----------------------------case 3 : Response Code=401:User need to log in first..----------------------------// - fmt.Println("case 3: respose code:401,User need to log in first..") - httpStatusCode, err = apiTest.ProjectsHead(*unknownUsr, "libra") - if err != nil { - t.Error("Error while search project by proName", err.Error()) - t.Log(err) - } else { - assert.Equal(int(401), httpStatusCode, "httpStatusCode should be 401") - } fmt.Printf("\n") - } func TestToggleProjectPublicity(t *testing.T) { @@ -313,7 +304,7 @@ func TestToggleProjectPublicity(t *testing.T) { } //-------------------case4: Response Code=404 Not found the project------------------------------// fmt.Println("case 4: respose code:404, Not found the project") - httpStatusCode, err = apiTest.ToggleProjectPublicity(*admin, "0", 1) + httpStatusCode, err = apiTest.ToggleProjectPublicity(*admin, "1234", 1) if err != nil { t.Error("Error while search project by proId", err.Error()) t.Log(err) diff --git a/src/ui/api/search.go b/src/ui/api/search.go index ebdef8c2c..1f54f031c 100644 --- a/src/ui/api/search.go +++ b/src/ui/api/search.go @@ -33,7 +33,7 @@ type SearchAPI struct { } type searchResult struct { - Project []models.Project `json:"project"` + Project []*models.Project `json:"project"` Repository []map[string]interface{} `json:"repository"` } @@ -52,10 +52,10 @@ func (s *SearchAPI) Get() { s.CustomAbort(http.StatusInternalServerError, "internal error") } - var projects []models.Project + var projects []*models.Project if isSysAdmin { - projects, err = dao.GetProjects("") + projects, err = dao.GetProjects("", "", "", "", 0, 0, 0) if err != nil { log.Errorf("failed to get all projects: %v", err) s.CustomAbort(http.StatusInternalServerError, "internal error") @@ -70,7 +70,7 @@ func (s *SearchAPI) Get() { projectSorter := &models.ProjectSorter{Projects: projects} sort.Sort(projectSorter) - projectResult := []models.Project{} + projectResult := []*models.Project{} for _, p := range projects { if len(keyword) > 0 && !strings.Contains(p.Name, keyword) { continue @@ -113,7 +113,7 @@ func (s *SearchAPI) Get() { s.ServeJSON() } -func filterRepositories(projects []models.Project, keyword string) ( +func filterRepositories(projects []*models.Project, keyword string) ( []map[string]interface{}, error) { repositories, err := dao.GetAllRepositories() diff --git a/src/ui/api/statistic.go b/src/ui/api/statistic.go index 0f68252d5..c151c2e84 100644 --- a/src/ui/api/statistic.go +++ b/src/ui/api/statistic.go @@ -17,9 +17,10 @@ package api import ( "net/http" + "github.com/vmware/harbor/src/common/api" "github.com/vmware/harbor/src/common/dao" + "github.com/vmware/harbor/src/common/models" "github.com/vmware/harbor/src/common/utils/log" - "github.com/vmware/harbor/src/common/api" ) const ( @@ -52,7 +53,7 @@ func (s *StatisticAPI) Prepare() { func (s *StatisticAPI) Get() { statistic := map[string]int64{} - n, err := dao.GetTotalOfProjects("", 1) + n, err := dao.GetTotalOfProjects("", "", "true", "", 0) if err != nil { log.Errorf("failed to get total of public projects: %v", err) s.CustomAbort(http.StatusInternalServerError, "") @@ -73,7 +74,7 @@ func (s *StatisticAPI) Get() { } if isAdmin { - n, err := dao.GetTotalOfProjects("") + n, err := dao.GetTotalOfProjects("", "", "", "", 0) if err != nil { log.Errorf("failed to get total of projects: %v", err) s.CustomAbort(http.StatusInternalServerError, "") @@ -89,7 +90,14 @@ func (s *StatisticAPI) Get() { statistic[MRC] = n statistic[TRC] = n } else { - n, err := dao.GetTotalOfUserRelevantProjects(s.userID, "") + user, err := dao.GetUser(models.User{ + UserID: s.userID, + }) + if err != nil { + log.Errorf("failed to get user %d: %v", s.userID, err) + s.CustomAbort(http.StatusInternalServerError, "") + } + n, err := dao.GetTotalOfProjects("", "", "", user.Username, 0) if err != nil { log.Errorf("failed to get total of projects for user %d: %v", s.userID, err) s.CustomAbort(http.StatusInternalServerError, "") diff --git a/src/ui/projectmanager/db/pm.go b/src/ui/projectmanager/db/pm.go index d5f603e4d..d90342119 100644 --- a/src/ui/projectmanager/db/pm.go +++ b/src/ui/projectmanager/db/pm.go @@ -15,6 +15,9 @@ package db import ( + "fmt" + "time" + "github.com/vmware/harbor/src/common" "github.com/vmware/harbor/src/common/dao" "github.com/vmware/harbor/src/common/models" @@ -106,29 +109,107 @@ func (p *ProjectManager) GetRoles(username string, projectIDOrName interface{}) } // GetPublic returns all public projects -func (p *ProjectManager) GetPublic() []models.Project { - projects, err := dao.GetProjects("", 1) - if err != nil { - log.Errorf("failed to get all public projects: %v", err) - return []models.Project{} - } - - return projects +func (p *ProjectManager) GetPublic() []*models.Project { + return filter("", "", "true", "", 0, 0, 0) } // GetByMember returns all projects which the user is a member of -func (p *ProjectManager) GetByMember(username string) []models.Project { - user, err := dao.GetUser(models.User{ - Username: username, - }) - if err != nil { - log.Errorf("failed to get user %s: %v", username, err) - return []models.Project{} +func (p *ProjectManager) GetByMember(username string) []*models.Project { + return filter("", "", "", username, 0, 0, 0) +} + +// Create ... +func (p *ProjectManager) Create(project *models.Project) (int64, error) { + if project == nil { + return 0, fmt.Errorf("project is nil") } - projects, err := dao.GetUserRelevantProjects(user.UserID, "") + + if len(project.Name) == 0 { + return 0, fmt.Errorf("project name is nil") + } + + if project.OwnerID == 0 { + if len(project.OwnerName) == 0 { + return 0, fmt.Errorf("owner ID and owner name are both nil") + } + + user, err := dao.GetUser(models.User{ + Username: project.OwnerName, + }) + if err != nil { + return 0, err + } + if user == nil { + return 0, fmt.Errorf("can not get owner whose name is %s", project.OwnerName) + } + project.OwnerID = user.UserID + } + + t := time.Now() + pro := &models.Project{ + Name: project.Name, + Public: project.Public, + OwnerID: project.OwnerID, + CreationTime: t, + UpdateTime: t, + } + + return dao.AddProject(*pro) +} + +// Delete ... +func (p *ProjectManager) Delete(projectIDOrName interface{}) error { + id, ok := projectIDOrName.(int64) + if !ok { + project := p.Get(projectIDOrName) + if project == nil { + return fmt.Errorf(fmt.Sprintf("project %v not found", projectIDOrName)) + } + id = project.ProjectID + } + + return dao.DeleteProject(id) +} + +// Update ... +func (p *ProjectManager) Update(projectIDOrName interface{}, + project *models.Project) error { + id, ok := projectIDOrName.(int64) + if !ok { + pro := p.Get(projectIDOrName) + if pro == nil { + return fmt.Errorf(fmt.Sprintf("project %v not found", projectIDOrName)) + } + id = pro.ProjectID + } + return dao.ToggleProjectPublicity(id, project.Public) +} + +// GetAll ... +func (p *ProjectManager) GetAll(owner, name, public, member string, + role int, page, size int64) ([]*models.Project, int64) { + total, err := dao.GetTotalOfProjects(owner, name, public, member, role) if err != nil { - log.Errorf("failed to get projects of %s: %v", username, err) - return []models.Project{} + log.Errorf("failed to get total of projects: %v", err) + return []*models.Project{}, 0 + } + + return filter(owner, name, public, member, role, page, size), total +} + +func filter(owner, name, public, member string, + role int, page, size int64) []*models.Project { + projects := []*models.Project{} + + list, err := dao.GetProjects(owner, name, public, member, role, + page, size) + if err != nil { + log.Errorf("failed to get projects: %v", err) + return projects + } + + if len(list) != 0 { + projects = append(projects, list...) } return projects diff --git a/src/ui/projectmanager/db/pm_test.go b/src/ui/projectmanager/db/pm_test.go index 7bce75435..621de06ad 100644 --- a/src/ui/projectmanager/db/pm_test.go +++ b/src/ui/projectmanager/db/pm_test.go @@ -142,3 +142,101 @@ func TestGetByMember(t *testing.T) { projects := pm.GetByMember("admin") assert.NotEqual(t, 0, len(projects)) } + +func TestCreateAndDelete(t *testing.T) { + pm := &ProjectManager{} + + // nil project + _, err := pm.Create(nil) + assert.NotNil(t, err) + + // nil project name + _, err = pm.Create(&models.Project{ + OwnerID: 1, + }) + assert.NotNil(t, err) + + // nil owner id and nil owner name + _, err = pm.Create(&models.Project{ + Name: "test", + OwnerName: "non_exist_user", + }) + assert.NotNil(t, err) + + // valid project, owner id + id, err := pm.Create(&models.Project{ + Name: "test", + OwnerID: 1, + }) + assert.Nil(t, err) + assert.Nil(t, pm.Delete(id)) + + // valid project, owner name + id, err = pm.Create(&models.Project{ + Name: "test", + OwnerName: "admin", + }) + assert.Nil(t, err) + assert.Nil(t, pm.Delete(id)) +} + +func TestUpdate(t *testing.T) { + pm := &ProjectManager{} + + id, err := pm.Create(&models.Project{ + Name: "test", + OwnerID: 1, + }) + assert.Nil(t, err) + defer pm.Delete(id) + + project := pm.Get(id) + assert.Equal(t, 0, project.Public) + + project.Public = 1 + assert.Nil(t, pm.Update(id, project)) + + project = pm.Get(id) + assert.Equal(t, 1, project.Public) +} + +func TestGetAll(t *testing.T) { + pm := &ProjectManager{} + + id, err := pm.Create(&models.Project{ + Name: "get_all_test", + OwnerID: 1, + Public: 1, + }) + assert.Nil(t, err) + defer pm.Delete(id) + + // get by name + projects, total := pm.GetAll("", "get_all_test", "", "", 0, 0, 0) + assert.Equal(t, int64(1), total) + assert.Equal(t, id, projects[0].ProjectID) + + // get by owner + projects, total = pm.GetAll("admin", "", "", "", 0, 0, 0) + assert.NotEqual(t, 0, total) + exist := false + for _, project := range projects { + if project.ProjectID == id { + exist = true + break + } + } + assert.True(t, exist) + + // get by public + projects, total = pm.GetAll("", "", "true", "", 0, 0, 0) + assert.NotEqual(t, 0, total) + exist = false + for _, project := range projects { + if project.ProjectID == id { + exist = true + break + } + } + assert.True(t, exist) +} diff --git a/src/ui/projectmanager/pm.go b/src/ui/projectmanager/pm.go index 8f3763c8b..87fb966b0 100644 --- a/src/ui/projectmanager/pm.go +++ b/src/ui/projectmanager/pm.go @@ -26,7 +26,20 @@ type ProjectManager interface { Exist(projectIDOrName interface{}) bool GetRoles(username string, projectIDOrName interface{}) []int // get all public project - GetPublic() []models.Project + GetPublic() []*models.Project // get projects which the user is a member of - GetByMember(username string) []models.Project + GetByMember(username string) []*models.Project + Create(*models.Project) (int64, error) + Delete(projectIDOrName interface{}) error + Update(projectIDOrName interface{}, project *models.Project) error + // GetAll returns a project list and the total count according to + // the query conditions: + // owner: username of owner + // name: name of project + // public: public or not, can be "true", "false" or "" + // member: username of the member + // role: the role of member specified by member parameter + // page, size: pagination parameters + GetAll(owner, name, public, member string, role int, page, + size int64) ([]*models.Project, int64) }