diff --git a/api/log.go b/api/log.go new file mode 100644 index 000000000..bc8b05f74 --- /dev/null +++ b/api/log.go @@ -0,0 +1,86 @@ +/* + 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 api + +import ( + "net/http" + "strconv" + "time" + + "github.com/vmware/harbor/dao" + "github.com/vmware/harbor/models" + "github.com/vmware/harbor/utils/log" +) + +//LogAPI handles request api/logs +type LogAPI struct { + BaseAPI + userID int +} + +//Prepare validates the URL and the user +func (l *LogAPI) Prepare() { + l.userID = l.ValidateUser() +} + +//Get returns the recent logs according to parameters +func (l *LogAPI) Get() { + var err error + startTime := l.GetString("start_time") + if len(startTime) != 0 { + i, err := strconv.ParseInt(startTime, 10, 64) + if err != nil { + log.Errorf("Parse startTime to int error, err: %v", err) + l.CustomAbort(http.StatusBadRequest, "startTime is not a valid integer") + } + startTime = time.Unix(i, 0).String() + } + + endTime := l.GetString("end_time") + if len(endTime) != 0 { + j, err := strconv.ParseInt(endTime, 10, 64) + if err != nil { + log.Errorf("Parse endTime to int error, err: %v", err) + l.CustomAbort(http.StatusBadRequest, "endTime is not a valid integer") + } + endTime = time.Unix(j, 0).String() + } + + var linesNum int + lines := l.GetString("lines") + if len(lines) != 0 { + linesNum, err = strconv.Atoi(lines) + if err != nil { + log.Errorf("Get parameters error--lines, err: %v", err) + l.CustomAbort(http.StatusBadRequest, "bad request of lines") + } + if linesNum <= 0 { + log.Warning("lines must be a positive integer") + l.CustomAbort(http.StatusBadRequest, "lines is 0 or negative") + } + } else if len(startTime) == 0 && len(endTime) == 0 { + linesNum = 10 + } + + var logList []models.AccessLog + logList, err = dao.GetRecentLogs(l.userID, linesNum, startTime, endTime) + if err != nil { + log.Errorf("Get recent logs error, err: %v", err) + l.CustomAbort(http.StatusInternalServerError, "Internal error") + } + l.Data["json"] = logList + l.ServeJSON() +} diff --git a/api/project.go b/api/project.go index 5d2a3cf1f..98a8fa649 100644 --- a/api/project.go +++ b/api/project.go @@ -18,6 +18,7 @@ package api import ( "fmt" "net/http" + "unicode" "github.com/vmware/harbor/dao" "github.com/vmware/harbor/models" @@ -40,6 +41,7 @@ type projectReq struct { } const projectNameMaxLen int = 30 +const projectNameMinLen int = 4 // Prepare validates the URL and the user func (p *ProjectAPI) Prepare() { @@ -197,10 +199,9 @@ func (p *ProjectAPI) List() { p.ServeJSON() } -// Put ... -func (p *ProjectAPI) Put() { +// ToggleProjectPublic ... +func (p *ProjectAPI) ToggleProjectPublic() { p.userID = p.ValidateUser() - var req projectReq var public int @@ -287,8 +288,16 @@ func validateProjectReq(req projectReq) error { if len(pn) == 0 { return fmt.Errorf("Project name can not be empty") } - if len(pn) > projectNameMaxLen { - return fmt.Errorf("Project name is too long") + if isIllegalLength(req.ProjectName, projectNameMinLen, projectNameMaxLen) { + return fmt.Errorf("project name is illegal in length. (greater than 4 or less than 30)") + } + if isContainIllegalChar(req.ProjectName, []string{"~", "-", "$", "\\", "[", "]", "{", "}", "(", ")", "&", "^", "%", "*", "<", ">", "\"", "'", "/", "?", "@"}) { + return fmt.Errorf("project name contains illegal characters") + } + for _, v := range pn { + if !unicode.IsLower(v) { + return fmt.Errorf("project name must be in lower case") + } } return nil } diff --git a/api/repository.go b/api/repository.go index 3c0264ce3..719ab1f5a 100644 --- a/api/repository.go +++ b/api/repository.go @@ -294,3 +294,30 @@ func (ra *RepositoryAPI) getUsername() (string, error) { return "", nil } + +//GetTopRepos handles request GET /api/repositories/top +func (ra *RepositoryAPI) GetTopRepos() { + var err error + var countNum int + count := ra.GetString("count") + if len(count) == 0 { + countNum = 10 + } else { + countNum, err = strconv.Atoi(count) + if err != nil { + log.Errorf("Get parameters error--count, err: %v", err) + ra.CustomAbort(http.StatusBadRequest, "bad request of count") + } + if countNum <= 0 { + log.Warning("count must be a positive integer") + ra.CustomAbort(http.StatusBadRequest, "count is 0 or negative") + } + } + repos, err := dao.GetTopRepos(countNum) + if err != nil { + log.Errorf("error occured in get top 10 repos: %v", err) + ra.CustomAbort(http.StatusInternalServerError, "internal server error") + } + ra.Data["json"] = repos + ra.ServeJSON() +} diff --git a/api/user.go b/api/user.go index 45869fb6c..24f004c4b 100644 --- a/api/user.go +++ b/api/user.go @@ -16,8 +16,10 @@ package api import ( + "fmt" "net/http" "os" + "regexp" "strconv" "strings" @@ -133,14 +135,52 @@ func (ua *UserAPI) Get() { } // Put ... -func (ua *UserAPI) Put() { //currently only for toggle admin, so no request body +func (ua *UserAPI) Put() { + ldapAdminUser := (ua.AuthMode == "ldap_auth" && ua.userID == 1 && ua.userID == ua.currentUserID) + + if !(ua.AuthMode == "db_auth" || ldapAdminUser) { + ua.CustomAbort(http.StatusForbidden, "") + } if !ua.IsAdmin { - log.Warningf("current user, id: %d does not have admin role, can not update other user's role", ua.currentUserID) - ua.RenderError(http.StatusForbidden, "User does not have admin role") + if ua.userID != ua.currentUserID { + log.Warning("Guests can only change their own account.") + ua.CustomAbort(http.StatusForbidden, "Guests can only change their own account.") + } + } + user := models.User{UserID: ua.userID} + ua.DecodeJSONReq(&user) + err := commonValidate(user) + if err != nil { + log.Warning("Bad request in change user profile: %v", err) + ua.RenderError(http.StatusBadRequest, "change user profile error:"+err.Error()) return } userQuery := models.User{UserID: ua.userID} - dao.ToggleUserAdminRole(userQuery) + u, err := dao.GetUser(userQuery) + if err != nil { + log.Errorf("Error occurred in GetUser, error: %v", err) + ua.CustomAbort(http.StatusInternalServerError, "Internal error.") + } + if u == nil { + log.Errorf("User with Id: %d does not exist", ua.userID) + ua.CustomAbort(http.StatusNotFound, "") + } + if u.Email != user.Email { + emailExist, err := dao.UserExists(user, "email") + if err != nil { + log.Errorf("Error occurred in change user profile: %v", err) + ua.CustomAbort(http.StatusInternalServerError, "Internal error.") + } + if emailExist { + log.Warning("email has already been used!") + ua.RenderError(http.StatusConflict, "email has already been used!") + return + } + } + if err := dao.ChangeUserProfile(user); err != nil { + log.Errorf("Failed to update user profile, error: %v", err) + ua.CustomAbort(http.StatusInternalServerError, err.Error()) + } } // Post ... @@ -157,12 +197,36 @@ func (ua *UserAPI) Post() { user := models.User{} ua.DecodeJSONReq(&user) - + err := validate(user) + if err != nil { + log.Warning("Bad request in Register: %v", err) + ua.RenderError(http.StatusBadRequest, "register error:"+err.Error()) + return + } + userExist, err := dao.UserExists(user, "username") + if err != nil { + log.Errorf("Error occurred in Register: %v", err) + ua.CustomAbort(http.StatusInternalServerError, "Internal error.") + } + if userExist { + log.Warning("username has already been used!") + ua.RenderError(http.StatusConflict, "username has already been used!") + return + } + emailExist, err := dao.UserExists(user, "email") + if err != nil { + log.Errorf("Error occurred in change user profile: %v", err) + ua.CustomAbort(http.StatusInternalServerError, "Internal error.") + } + if emailExist { + log.Warning("email has already been used!") + ua.RenderError(http.StatusConflict, "email has already been used!") + return + } userID, err := dao.Register(user) if err != nil { log.Errorf("Error occurred in Register: %v", err) - ua.RenderError(http.StatusInternalServerError, "Internal error.") - return + ua.CustomAbort(http.StatusInternalServerError, "Internal error.") } ua.Redirect(http.StatusCreated, strconv.FormatInt(userID, 10)) @@ -186,9 +250,8 @@ func (ua *UserAPI) Delete() { // ChangePassword handles PUT to /api/users/{}/password func (ua *UserAPI) ChangePassword() { - ldapAdminUser := (ua.AuthMode == "ldap_auth" && ua.userID == 1 && ua.userID == ua.currentUserID) - + if !(ua.AuthMode == "db_auth" || ldapAdminUser) { ua.CustomAbort(http.StatusForbidden, "") } @@ -228,3 +291,80 @@ func (ua *UserAPI) ChangePassword() { ua.CustomAbort(http.StatusInternalServerError, "Internal error.") } } + +// ToggleUserAdminRole handles PUT api/users/{}/toggleadmin +func (ua *UserAPI) ToggleUserAdminRole() { + if !ua.IsAdmin { + log.Warningf("current user, id: %d does not have admin role, can not update other user's role", ua.currentUserID) + ua.RenderError(http.StatusForbidden, "User does not have admin role") + return + } + userQuery := models.User{UserID: ua.userID} + ua.DecodeJSONReq(&userQuery) + if err := dao.ToggleUserAdminRole(userQuery.UserID, userQuery.HasAdminRole); err != nil { + log.Errorf("Error occurred in ToggleUserAdminRole: %v", err) + ua.CustomAbort(http.StatusInternalServerError, "Internal error.") + } +} + +// validate only validate when user register +func validate(user models.User) error { + + if isIllegalLength(user.Username, 0, 20) { + return fmt.Errorf("Username with illegal length.") + } + if isContainIllegalChar(user.Username, []string{",", "~", "#", "$", "%"}) { + return fmt.Errorf("Username contains illegal characters.") + } + if isIllegalLength(user.Password, 0, 20) { + return fmt.Errorf("Password with illegal length.") + } + if err := commonValidate(user); err != nil { + return err + } + return nil +} + +//commonValidate validates email, realname, comment information when user register or change their profile +func commonValidate(user models.User) error { + + if len(user.Email) > 0 { + if m, _ := regexp.MatchString(`^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$`, user.Email); !m { + return fmt.Errorf("Email with illegal format.") + } + } else { + return fmt.Errorf("Email can't be empty") + } + + if isIllegalLength(user.Realname, 0, 20) { + return fmt.Errorf("Realname with illegal length.") + } + + if isContainIllegalChar(user.Realname, []string{",", "~", "#", "$", "%"}) { + return fmt.Errorf("Realname contains illegal characters.") + } + if isIllegalLength(user.Comment, -1, 30) { + return fmt.Errorf("Comment with illegal length.") + } + return nil + +} + +func isIllegalLength(s string, min int, max int) bool { + if min == -1 { + return (len(s) > max) + } + if max == -1 { + return (len(s) <= min) + } + return (len(s) < min || len(s) > max) +} + +func isContainIllegalChar(s string, illegalChar []string) bool { + for _, c := range illegalChar { + if strings.Index(s, c) >= 0 { + return true + } + } + return false +} diff --git a/auth/ldap/ldap.go b/auth/ldap/ldap.go index 8de4d47fb..6929147d1 100644 --- a/auth/ldap/ldap.go +++ b/auth/ldap/ldap.go @@ -111,6 +111,9 @@ func (l *Auth) Authenticate(m models.AuthModel) (*models.User, error) { u.Realname = m.Principal u.Password = "12345678AbC" u.Comment = "registered from LDAP." + if u.Email == "" { + u.Email = u.Username + "@placeholder.com" + } userID, err := dao.Register(u) if err != nil { return nil, err diff --git a/dao/accesslog.go b/dao/accesslog.go index dc18e9d66..b407acb37 100644 --- a/dao/accesslog.go +++ b/dao/accesslog.go @@ -115,3 +115,49 @@ func AccessLog(username, projectName, repoName, repoTag, action string) error { } return err } + +//GetRecentLogs returns recent logs according to parameters +func GetRecentLogs(userID, linesNum int, startTime, endTime string) ([]models.AccessLog, error) { + var recentLogList []models.AccessLog + queryParam := make([]interface{}, 1) + + sql := "select log_id, access_log.user_id, project_id, repo_name, repo_tag, GUID, operation, op_time, username from access_log left join user on access_log.user_id=user.user_id where project_id in (select distinct project_id from project_member where user_id = ?)" + queryParam = append(queryParam, userID) + if startTime != "" { + sql += " and op_time >= ?" + queryParam = append(queryParam, startTime) + } + + if endTime != "" { + sql += " and op_time <= ?" + queryParam = append(queryParam, endTime) + } + + sql += " order by op_time desc" + if linesNum != 0 { + sql += " limit ?" + queryParam = append(queryParam, linesNum) + } + o := GetOrmer() + _, err := o.Raw(sql, queryParam).QueryRows(&recentLogList) + if err != nil { + return nil, err + } + return recentLogList, nil +} + +//GetTopRepos return top accessed public repos +func GetTopRepos(countNum int) ([]models.TopRepo, error) { + + o := GetOrmer() + + sql := "select repo_name, COUNT(repo_name) as access_count from access_log left join project on access_log.project_id=project.project_id where project.public = 1 and access_log.operation = 'pull' group by repo_name order by access_count desc limit ? " + queryParam := make([]interface{}, 1) + queryParam = append(queryParam, countNum) + var lists []models.TopRepo + _, err := o.Raw(sql, queryParam).QueryRows(&lists) + if err != nil { + return nil, err + } + return lists, nil +} diff --git a/dao/base.go b/dao/base.go index a03db5cb3..97bc55ad0 100644 --- a/dao/base.go +++ b/dao/base.go @@ -18,39 +18,18 @@ package dao import ( "net" - "github.com/vmware/harbor/utils/log" - "os" - "strings" "sync" "time" "github.com/astaxie/beego/orm" _ "github.com/go-sql-driver/mysql" //register mysql driver + "github.com/vmware/harbor/utils/log" ) // NonExistUserID : if a user does not exist, the ID of the user will be 0. const NonExistUserID = 0 -func isIllegalLength(s string, min int, max int) bool { - if min == -1 { - return (len(s) > max) - } - if max == -1 { - return (len(s) <= min) - } - return (len(s) < min || len(s) > max) -} - -func isContainIllegalChar(s string, illegalChar []string) bool { - for _, c := range illegalChar { - if strings.Index(s, c) >= 0 { - return true - } - } - return false -} - // GenerateRandomString generates a random string func GenerateRandomString() (string, error) { o := orm.NewOrm() diff --git a/dao/dao_test.go b/dao/dao_test.go index 8655a179c..9e829fa36 100644 --- a/dao/dao_test.go +++ b/dao/dao_test.go @@ -689,7 +689,7 @@ func TestDeleteProjectMember(t *testing.T) { } func TestToggleAdminRole(t *testing.T) { - err := ToggleUserAdminRole(*currentUser) + err := ToggleUserAdminRole(currentUser.UserID, 1) if err != nil { t.Errorf("Error in toggle ToggleUserAdmin role: %v, user: %+v", err, currentUser) } @@ -700,7 +700,7 @@ func TestToggleAdminRole(t *testing.T) { if !isAdmin { t.Errorf("User is not admin after toggled, user id: %d", currentUser.UserID) } - err = ToggleUserAdminRole(*currentUser) + err = ToggleUserAdminRole(currentUser.UserID, 0) if err != nil { t.Errorf("Error in toggle ToggleUserAdmin role: %v, user: %+v", err, currentUser) } @@ -713,6 +713,39 @@ func TestToggleAdminRole(t *testing.T) { } } +func TestChangeUserProfile(t *testing.T) { + user := models.User{UserID: currentUser.UserID, Email: username + "@163.com", Realname: "test", Comment: "Unit Test"} + err := ChangeUserProfile(user) + if err != nil { + t.Errorf("Error occurred in ChangeUserProfile: %v", err) + } + loginedUser, err := GetUser(models.User{UserID: currentUser.UserID}) + if err != nil { + t.Errorf("Error occurred in GetUser: %v", err) + } + if loginedUser != nil { + if loginedUser.Email != username+"@163.com" { + t.Errorf("user email does not update, expected: %s, acutal: %s", username+"@163.com", loginedUser.Email) + } + if loginedUser.Realname != "test" { + t.Errorf("user realname does not update, expected: %s, acutal: %s", "test", loginedUser.Realname) + } + if loginedUser.Comment != "Unit Test" { + t.Errorf("user email does not update, expected: %s, acutal: %s", "Unit Test", loginedUser.Comment) + } + } +} + +func TestGetRecentLogs(t *testing.T) { + logs, err := GetRecentLogs(currentUser.UserID, 10, "2016-05-13 00:00:00", time.Now().String()) + if err != nil { + t.Errorf("error occured in getting recent logs, error: %v", err) + } + if len(logs) <= 0 { + t.Errorf("get logs error, expected: %d, actual: %d", 1, len(logs)) + } +} + func TestDeleteUser(t *testing.T) { err := DeleteUser(currentUser.UserID) if err != nil { diff --git a/dao/project.go b/dao/project.go index 60f32e367..b8a158692 100644 --- a/dao/project.go +++ b/dao/project.go @@ -18,7 +18,6 @@ package dao import ( "github.com/vmware/harbor/models" - "errors" "fmt" "time" @@ -30,15 +29,7 @@ import ( // AddProject adds a project to the database along with project roles information and access log records. func AddProject(project models.Project) (int64, error) { - if isIllegalLength(project.Name, 4, 30) { - return 0, errors.New("project name is illegal in length. (greater than 4 or less than 30)") - } - if isContainIllegalChar(project.Name, []string{"~", "-", "$", "\\", "[", "]", "{", "}", "(", ")", "&", "^", "%", "*", "<", ">", "\"", "'", "/", "?", "@"}) { - return 0, errors.New("project name contains illegal characters") - } - o := GetOrmer() - p, err := o.Raw("insert into project (owner_id, name, creation_time, update_time, deleted, public) values (?, ?, ?, ?, ?, ?)").Prepare() if err != nil { return 0, err diff --git a/dao/register.go b/dao/register.go index 3dc388c16..c7135df62 100644 --- a/dao/register.go +++ b/dao/register.go @@ -17,7 +17,6 @@ package dao import ( "errors" - "regexp" "time" "github.com/vmware/harbor/models" @@ -26,14 +25,7 @@ import ( // Register is used for user to register, the password is encrypted before the record is inserted into database. func Register(user models.User) (int64, error) { - - err := validate(user) - if err != nil { - return 0, err - } - o := GetOrmer() - p, err := o.Raw("insert into user (username, password, realname, email, comment, salt, sysadmin_flag, creation_time, update_time) values (?, ?, ?, ?, ?, ?, ?, ?, ?)").Prepare() if err != nil { return 0, err @@ -59,46 +51,6 @@ func Register(user models.User) (int64, error) { return userID, nil } -func validate(user models.User) error { - - if isIllegalLength(user.Username, 0, 20) { - return errors.New("Username with illegal length.") - } - if isContainIllegalChar(user.Username, []string{",", "~", "#", "$", "%"}) { - return errors.New("Username contains illegal characters.") - } - - if exist, _ := UserExists(models.User{Username: user.Username}, "username"); exist { - return errors.New("Username already exists.") - } - - if len(user.Email) > 0 { - if m, _ := regexp.MatchString(`^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$`, user.Email); !m { - return errors.New("Email with illegal format.") - } - if exist, _ := UserExists(models.User{Email: user.Email}, "email"); exist { - return errors.New("Email already exists.") - } - } - - if isIllegalLength(user.Realname, 0, 20) { - return errors.New("Realname with illegal length.") - } - - if isContainIllegalChar(user.Realname, []string{",", "~", "#", "$", "%"}) { - return errors.New("Realname contains illegal characters.") - } - - if isIllegalLength(user.Password, 0, 20) { - return errors.New("Password with illegal length.") - } - - if isIllegalLength(user.Comment, -1, 30) { - return errors.New("Comment with illegal length.") - } - return nil -} - // UserExists returns whether a user exists according username or Email. func UserExists(user models.User, target string) (bool, error) { diff --git a/dao/user.go b/dao/user.go index d337a55e2..673f684b0 100644 --- a/dao/user.go +++ b/dao/user.go @@ -109,12 +109,13 @@ func ListUsers(query models.User) ([]models.User, error) { } // ToggleUserAdminRole gives a user admin role. -func ToggleUserAdminRole(u models.User) error { +func ToggleUserAdminRole(userID, hasAdmin int) error { o := GetOrmer() - - sql := `update user set sysadmin_flag =not sysadmin_flag where user_id = ?` - - r, err := o.Raw(sql, u.UserID).Exec() + queryParams := make([]interface{}, 1) + sql := `update user set sysadmin_flag = ? where user_id = ?` + queryParams = append(queryParams, hasAdmin) + queryParams = append(queryParams, userID) + r, err := o.Raw(sql, queryParams).Exec() if err != nil { return err } @@ -229,3 +230,13 @@ func DeleteUser(userID int) error { _, err := o.Raw(`update user set deleted = 1 where user_id = ?`, userID).Exec() return err } + +// ChangeUserProfile ... +func ChangeUserProfile(user models.User) error { + o := GetOrmer() + if _, err := o.Update(&user, "Email", "Realname", "Comment"); err != nil { + log.Errorf("update user failed, error: %v", err) + return err + } + return nil +} diff --git a/docs/swagger.yaml b/docs/swagger.yaml index 38d7da892..e4c274984 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -353,7 +353,24 @@ paths: 404: description: Project ID does not exist. 500: - description: Unexpected internal errors. + description: Unexpected internal errors. + /statistics: + get: + summary: Get projects number and repositories number relevant to the user + description: | + This endpoint is aimed to statistic all of the projects number and repositories number relevant to the logined user, also the public projects number and repositories number. If the user is admin, he can also get total projects number and total repositories number. + tags: + - Products + responses: + 200: + description: Get the projects number and repositories number relevant to the user successfully. + schema: + $ref: '#/definitions/StatisticMap' + 401: + description: User need to log in first. + 500: + description: Unexpected internal errors. + /users: get: summary: Get registered users of Harbor. @@ -407,10 +424,9 @@ paths: description: Unexpected internal errors. /users/{user_id}: put: - summary: Update a registered user to change to be an administrator of Harbor. + summary: Update a registered user to change his profile. description: | - This endpoint let a registered user change to be an administrator - of Harbor. + This endpoint let a registered user change his profile. parameters: - name: user_id in: path @@ -418,6 +434,12 @@ paths: format: int32 required: true description: Registered user ID + - name: profile + in: body + description: Only email, realname and comment can be modified. + required: true + schema: + $ref: '#/definitions/User' tags: - Products responses: @@ -490,7 +512,35 @@ paths: 403: description: Guests can only change their own account. 500: - description: Unexpected internal errors. + description: Unexpected internal errors. + /users/{user_id}/sysadmin: + put: + summary: Update a registered user to change to be an administrator of Harbor. + description: | + This endpoint let a registered user change to be an administrator + of Harbor. + parameters: + - name: user_id + in: path + type: integer + format: int32 + required: true + description: Registered user ID + tags: + - Products + responses: + 200: + description: Updated user's admin role successfully. + 400: + description: Invalid user ID. + 401: + description: User need to log in first. + 403: + description: User does not have permission of admin role. + 404: + description: User ID does not exist. + 500: + description: Unexpected internal errors. /repositories: get: summary: Get repositories accompany with relevant project and repo name. @@ -597,6 +647,70 @@ paths: description: Retrieved manifests from a relevant repository successfully. 500: description: Unexpected internal errors. + /repositories/top: + get: + summary: Get public repositories which are accessed most. + description: | + This endpoint aims to let users see the most popular public repositories + parameters: + - name: count + in: query + type: integer + format: int32 + required: false + description: The number of the requested public repositories, default is 10 if not provided. + tags: + - Products + responses: + 200: + description: Retrieved top repositories successfully. + schema: + type: array + items: + $ref: '#/definitions/TopRepo' + 400: + description: Bad request because of invalid count. + 500: + description: Unexpected internal errors. + /logs: + get: + summary: Get recent logs of the projects which the user is a member of + description: | + This endpoint let user see the recent operation logs of the projects which he is member of + parameters: + - name: lines + in: query + type: integer + format: int32 + required: false + description: The number of logs to be shown, default is 10 if lines, start_time, end_time are not provided. + - name: start_time + in: query + type: integer + format: int64 + required: false + description: The start time of logs to be shown in unix timestap + - name: end_time + in: query + type: integer + format: int64 + required: false + description: The end time of logs to be shown in unix timestap + tags: + - Products + responses: + 200: + description: Get the required logs successfully. + schema: + type: array + items: + $ref: '#/definitions/AccessLog' + 400: + description: Bad request because of invalid parameter of lines or start_time or end_time. + 401: + description: User need to login first. + 500: + description: Unexpected internal errors. definitions: Search: type: object @@ -744,3 +858,45 @@ definitions: user_name: type: string description: Username relevant to a project role member. + TopRepo: + type: object + properties: + repo_name: + type: string + description: The name of the repo + access_count: + type: integer + format: int + description: The access count of the repo + StatisticMap: + type: object + properties: + my_project_count: + type: integer + format: int32 + description: The count of the projects which the user is a member of. + my_repo_count: + type: integer + format: int32 + description: The count of the repositories belonging to the projects which the user is a member of. + public_project_count: + type: integer + format: int32 + description: The count of the public projects. + public_repo_count: + type: integer + format: int32 + description: The count of the public repositories belonging to the public projects which the user is a member of. + total_project_count: + type: integer + format: int32 + description: The count of the total projects, only be seen when the is admin. + total_repo_count: + type: integer + format: int32 + description: The count of the total repositories, only be seen when the user is admin. + + + + + diff --git a/models/accesslog.go b/models/accesslog.go index 3111e3a44..b5d919943 100644 --- a/models/accesslog.go +++ b/models/accesslog.go @@ -21,7 +21,7 @@ import ( // AccessLog holds information about logs which are used to record the actions that user take to the resourses. type AccessLog struct { - LogID int `orm:"column(log_id)" json:"LogId"` + LogID int `orm:"pk;column(log_id)" json:"LogId"` UserID int `orm:"column(user_id)" json:"UserId"` ProjectID int64 `orm:"column(project_id)" json:"ProjectId"` RepoName string `orm:"column(repo_name)"` diff --git a/models/base.go b/models/base.go index fdd0204ac..52ac63925 100644 --- a/models/base.go +++ b/models/base.go @@ -7,5 +7,9 @@ import ( func init() { orm.RegisterModel(new(RepTarget), new(RepPolicy), - new(RepJob)) + new(RepJob), + new(User), + new(Project), + new(Role), + new(AccessLog)) } diff --git a/models/project.go b/models/project.go index 2ebd5fc32..f2a374e4c 100644 --- a/models/project.go +++ b/models/project.go @@ -21,7 +21,7 @@ import ( // Project holds the details of a project. type Project struct { - ProjectID int64 `orm:"column(project_id)" json:"ProjectId"` + ProjectID int64 `orm:"pk;column(project_id)" json:"ProjectId"` OwnerID int `orm:"column(owner_id)" json:"OwnerId"` Name string `orm:"column(name)"` CreationTime time.Time `orm:"column(creation_time)"` diff --git a/models/role.go b/models/role.go index c55b29441..dc120956b 100644 --- a/models/role.go +++ b/models/role.go @@ -26,7 +26,7 @@ const ( // Role holds the details of a role. type Role struct { - RoleID int `orm:"column(role_id)" json:"role_id"` + RoleID int `orm:"pk;column(role_id)" json:"role_id"` RoleCode string `orm:"column(role_code)" json:"role_code"` Name string `orm:"column(name)" json:"role_name"` diff --git a/models/toprepo.go b/models/toprepo.go new file mode 100644 index 000000000..14bcffa66 --- /dev/null +++ b/models/toprepo.go @@ -0,0 +1,22 @@ +/* + 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 models + +// TopRepo holds information about repository that accessed most +type TopRepo struct { + RepoName string `json:"name"` + AccessCount int64 `json:"count"` +} diff --git a/models/user.go b/models/user.go index 29fb41c13..3f0612c2e 100644 --- a/models/user.go +++ b/models/user.go @@ -21,7 +21,7 @@ import ( // User holds the details of a user. type User struct { - UserID int `orm:"column(user_id)" json:"UserId"` + UserID int `orm:"pk;column(user_id)" json:"UserId"` Username string `orm:"column(username)" json:"username"` Email string `orm:"column(email)" json:"email"` Password string `orm:"column(password)" json:"password"` @@ -29,11 +29,11 @@ type User struct { Comment string `orm:"column(comment)" json:"comment"` Deleted int `orm:"column(deleted)"` Rolename string - RoleID int `json:"RoleId"` - RoleList []Role - HasAdminRole int `orm:"column(sysadmin_flag)"` - ResetUUID string `orm:"column(reset_uuid)" json:"ResetUuid"` - Salt string `orm:"column(salt)"` + RoleID int `json:"RoleId"` + RoleList []*Role `orm:"rel(m2m)"` + HasAdminRole int `orm:"column(sysadmin_flag)"` + ResetUUID string `orm:"column(reset_uuid)" json:"ResetUuid"` + Salt string `orm:"column(salt)"` CreationTime time.Time `orm:"creation_time" json:"creation_time"` UpdateTime time.Time `orm:"update_time" json:"update_time"` diff --git a/ui/router.go b/ui/router.go index 14b788349..bc5eb9a16 100644 --- a/ui/router.go +++ b/ui/router.go @@ -55,6 +55,7 @@ func initRouters() { beego.Router("/api/projects/:pid([0-9]+)/members/?:mid", &api.ProjectMemberAPI{}) beego.Router("/api/projects/", &api.ProjectAPI{}, "get:List") beego.Router("/api/projects/?:id", &api.ProjectAPI{}) + beego.Router("/api/projects/:id/publicity", &api.ProjectAPI{}, "put:ToggleProjectPublic") beego.Router("/api/statistics", &api.StatisticAPI{}) beego.Router("/api/projects/:id([0-9]+)/logs/filter", &api.ProjectAPI{}, "post:FilterAccessLog") beego.Router("/api/users/?:id", &api.UserAPI{}) @@ -68,6 +69,9 @@ func initRouters() { beego.Router("/api/policies/replication/:id([0-9]+)/enablement", &api.RepPolicyAPI{}, "put:UpdateEnablement") beego.Router("/api/targets/?:id([0-9]+)", &api.TargetAPI{}) beego.Router("/api/targets/ping", &api.TargetAPI{}, "post:Ping") + beego.Router("/api/users/:id/sysadmin", &api.UserAPI{}, "put:ToggleUserAdminRole") + beego.Router("/api/repositories/top", &api.RepositoryAPI{}, "get:GetTopRepos") + beego.Router("api/logs", &api.LogAPI{}) //external service that hosted on harbor process: beego.Router("/service/notifications", &service.NotificationHandler{})