Merge pull request #307 from wemeya/develop

api for logs, updating user profile and listing top repositories
This commit is contained in:
Wenkai Yin 2016-06-08 17:59:10 +08:00
commit a7845df552
19 changed files with 578 additions and 115 deletions

86
api/log.go Normal file
View File

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

View File

@ -18,6 +18,7 @@ package api
import ( import (
"fmt" "fmt"
"net/http" "net/http"
"unicode"
"github.com/vmware/harbor/dao" "github.com/vmware/harbor/dao"
"github.com/vmware/harbor/models" "github.com/vmware/harbor/models"
@ -40,6 +41,7 @@ type projectReq struct {
} }
const projectNameMaxLen int = 30 const projectNameMaxLen int = 30
const projectNameMinLen int = 4
// Prepare validates the URL and the user // Prepare validates the URL and the user
func (p *ProjectAPI) Prepare() { func (p *ProjectAPI) Prepare() {
@ -197,10 +199,9 @@ func (p *ProjectAPI) List() {
p.ServeJSON() p.ServeJSON()
} }
// Put ... // ToggleProjectPublic ...
func (p *ProjectAPI) Put() { func (p *ProjectAPI) ToggleProjectPublic() {
p.userID = p.ValidateUser() p.userID = p.ValidateUser()
var req projectReq var req projectReq
var public int var public int
@ -287,8 +288,16 @@ func validateProjectReq(req projectReq) error {
if len(pn) == 0 { if len(pn) == 0 {
return fmt.Errorf("Project name can not be empty") return fmt.Errorf("Project name can not be empty")
} }
if len(pn) > projectNameMaxLen { if isIllegalLength(req.ProjectName, projectNameMinLen, projectNameMaxLen) {
return fmt.Errorf("Project name is too long") 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 return nil
} }

View File

@ -294,3 +294,30 @@ func (ra *RepositoryAPI) getUsername() (string, error) {
return "", nil 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()
}

View File

@ -16,8 +16,10 @@
package api package api
import ( import (
"fmt"
"net/http" "net/http"
"os" "os"
"regexp"
"strconv" "strconv"
"strings" "strings"
@ -133,14 +135,52 @@ func (ua *UserAPI) Get() {
} }
// Put ... // 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 { if !ua.IsAdmin {
log.Warningf("current user, id: %d does not have admin role, can not update other user's role", ua.currentUserID) if ua.userID != ua.currentUserID {
ua.RenderError(http.StatusForbidden, "User does not have admin role") 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 return
} }
userQuery := models.User{UserID: ua.userID} 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 ... // Post ...
@ -157,12 +197,36 @@ func (ua *UserAPI) Post() {
user := models.User{} user := models.User{}
ua.DecodeJSONReq(&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) userID, err := dao.Register(user)
if err != nil { if err != nil {
log.Errorf("Error occurred in Register: %v", err) log.Errorf("Error occurred in Register: %v", err)
ua.RenderError(http.StatusInternalServerError, "Internal error.") ua.CustomAbort(http.StatusInternalServerError, "Internal error.")
return
} }
ua.Redirect(http.StatusCreated, strconv.FormatInt(userID, 10)) ua.Redirect(http.StatusCreated, strconv.FormatInt(userID, 10))
@ -186,7 +250,6 @@ func (ua *UserAPI) Delete() {
// ChangePassword handles PUT to /api/users/{}/password // ChangePassword handles PUT to /api/users/{}/password
func (ua *UserAPI) ChangePassword() { func (ua *UserAPI) ChangePassword() {
ldapAdminUser := (ua.AuthMode == "ldap_auth" && ua.userID == 1 && ua.userID == ua.currentUserID) ldapAdminUser := (ua.AuthMode == "ldap_auth" && ua.userID == 1 && ua.userID == ua.currentUserID)
if !(ua.AuthMode == "db_auth" || ldapAdminUser) { if !(ua.AuthMode == "db_auth" || ldapAdminUser) {
@ -228,3 +291,80 @@ func (ua *UserAPI) ChangePassword() {
ua.CustomAbort(http.StatusInternalServerError, "Internal error.") 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
}

View File

@ -111,6 +111,9 @@ func (l *Auth) Authenticate(m models.AuthModel) (*models.User, error) {
u.Realname = m.Principal u.Realname = m.Principal
u.Password = "12345678AbC" u.Password = "12345678AbC"
u.Comment = "registered from LDAP." u.Comment = "registered from LDAP."
if u.Email == "" {
u.Email = u.Username + "@placeholder.com"
}
userID, err := dao.Register(u) userID, err := dao.Register(u)
if err != nil { if err != nil {
return nil, err return nil, err

View File

@ -115,3 +115,49 @@ func AccessLog(username, projectName, repoName, repoTag, action string) error {
} }
return err 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
}

View File

@ -18,39 +18,18 @@ package dao
import ( import (
"net" "net"
"github.com/vmware/harbor/utils/log"
"os" "os"
"strings"
"sync" "sync"
"time" "time"
"github.com/astaxie/beego/orm" "github.com/astaxie/beego/orm"
_ "github.com/go-sql-driver/mysql" //register mysql driver _ "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. // NonExistUserID : if a user does not exist, the ID of the user will be 0.
const NonExistUserID = 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 // GenerateRandomString generates a random string
func GenerateRandomString() (string, error) { func GenerateRandomString() (string, error) {
o := orm.NewOrm() o := orm.NewOrm()

View File

@ -689,7 +689,7 @@ func TestDeleteProjectMember(t *testing.T) {
} }
func TestToggleAdminRole(t *testing.T) { func TestToggleAdminRole(t *testing.T) {
err := ToggleUserAdminRole(*currentUser) err := ToggleUserAdminRole(currentUser.UserID, 1)
if err != nil { if err != nil {
t.Errorf("Error in toggle ToggleUserAdmin role: %v, user: %+v", err, currentUser) t.Errorf("Error in toggle ToggleUserAdmin role: %v, user: %+v", err, currentUser)
} }
@ -700,7 +700,7 @@ func TestToggleAdminRole(t *testing.T) {
if !isAdmin { if !isAdmin {
t.Errorf("User is not admin after toggled, user id: %d", currentUser.UserID) t.Errorf("User is not admin after toggled, user id: %d", currentUser.UserID)
} }
err = ToggleUserAdminRole(*currentUser) err = ToggleUserAdminRole(currentUser.UserID, 0)
if err != nil { if err != nil {
t.Errorf("Error in toggle ToggleUserAdmin role: %v, user: %+v", err, currentUser) 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) { func TestDeleteUser(t *testing.T) {
err := DeleteUser(currentUser.UserID) err := DeleteUser(currentUser.UserID)
if err != nil { if err != nil {

View File

@ -18,7 +18,6 @@ package dao
import ( import (
"github.com/vmware/harbor/models" "github.com/vmware/harbor/models"
"errors"
"fmt" "fmt"
"time" "time"
@ -30,15 +29,7 @@ import (
// AddProject adds a project to the database along with project roles information and access log records. // AddProject adds a project to the database along with project roles information and access log records.
func AddProject(project models.Project) (int64, error) { 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() o := GetOrmer()
p, err := o.Raw("insert into project (owner_id, name, creation_time, update_time, deleted, public) values (?, ?, ?, ?, ?, ?)").Prepare() p, err := o.Raw("insert into project (owner_id, name, creation_time, update_time, deleted, public) values (?, ?, ?, ?, ?, ?)").Prepare()
if err != nil { if err != nil {
return 0, err return 0, err

View File

@ -17,7 +17,6 @@ package dao
import ( import (
"errors" "errors"
"regexp"
"time" "time"
"github.com/vmware/harbor/models" "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. // 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) { func Register(user models.User) (int64, error) {
err := validate(user)
if err != nil {
return 0, err
}
o := GetOrmer() o := GetOrmer()
p, err := o.Raw("insert into user (username, password, realname, email, comment, salt, sysadmin_flag, creation_time, update_time) values (?, ?, ?, ?, ?, ?, ?, ?, ?)").Prepare() p, err := o.Raw("insert into user (username, password, realname, email, comment, salt, sysadmin_flag, creation_time, update_time) values (?, ?, ?, ?, ?, ?, ?, ?, ?)").Prepare()
if err != nil { if err != nil {
return 0, err return 0, err
@ -59,46 +51,6 @@ func Register(user models.User) (int64, error) {
return userID, nil 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. // UserExists returns whether a user exists according username or Email.
func UserExists(user models.User, target string) (bool, error) { func UserExists(user models.User, target string) (bool, error) {

View File

@ -109,12 +109,13 @@ func ListUsers(query models.User) ([]models.User, error) {
} }
// ToggleUserAdminRole gives a user admin role. // ToggleUserAdminRole gives a user admin role.
func ToggleUserAdminRole(u models.User) error { func ToggleUserAdminRole(userID, hasAdmin int) error {
o := GetOrmer() o := GetOrmer()
queryParams := make([]interface{}, 1)
sql := `update user set sysadmin_flag =not sysadmin_flag where user_id = ?` sql := `update user set sysadmin_flag = ? where user_id = ?`
queryParams = append(queryParams, hasAdmin)
r, err := o.Raw(sql, u.UserID).Exec() queryParams = append(queryParams, userID)
r, err := o.Raw(sql, queryParams).Exec()
if err != nil { if err != nil {
return err return err
} }
@ -229,3 +230,13 @@ func DeleteUser(userID int) error {
_, err := o.Raw(`update user set deleted = 1 where user_id = ?`, userID).Exec() _, err := o.Raw(`update user set deleted = 1 where user_id = ?`, userID).Exec()
return err 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
}

View File

@ -354,6 +354,23 @@ paths:
description: Project ID does not exist. description: Project ID does not exist.
500: 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: /users:
get: get:
summary: Get registered users of Harbor. summary: Get registered users of Harbor.
@ -407,10 +424,9 @@ paths:
description: Unexpected internal errors. description: Unexpected internal errors.
/users/{user_id}: /users/{user_id}:
put: put:
summary: Update a registered user to change to be an administrator of Harbor. summary: Update a registered user to change his profile.
description: | description: |
This endpoint let a registered user change to be an administrator This endpoint let a registered user change his profile.
of Harbor.
parameters: parameters:
- name: user_id - name: user_id
in: path in: path
@ -418,6 +434,12 @@ paths:
format: int32 format: int32
required: true required: true
description: Registered user ID description: Registered user ID
- name: profile
in: body
description: Only email, realname and comment can be modified.
required: true
schema:
$ref: '#/definitions/User'
tags: tags:
- Products - Products
responses: responses:
@ -491,6 +513,34 @@ paths:
description: Guests can only change their own account. description: Guests can only change their own account.
500: 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: /repositories:
get: get:
summary: Get repositories accompany with relevant project and repo name. summary: Get repositories accompany with relevant project and repo name.
@ -597,6 +647,70 @@ paths:
description: Retrieved manifests from a relevant repository successfully. description: Retrieved manifests from a relevant repository successfully.
500: 500:
description: Unexpected internal errors. 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: definitions:
Search: Search:
type: object type: object
@ -744,3 +858,45 @@ definitions:
user_name: user_name:
type: string type: string
description: Username relevant to a project role member. 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.

View File

@ -21,7 +21,7 @@ import (
// AccessLog holds information about logs which are used to record the actions that user take to the resourses. // AccessLog holds information about logs which are used to record the actions that user take to the resourses.
type AccessLog struct { 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"` UserID int `orm:"column(user_id)" json:"UserId"`
ProjectID int64 `orm:"column(project_id)" json:"ProjectId"` ProjectID int64 `orm:"column(project_id)" json:"ProjectId"`
RepoName string `orm:"column(repo_name)"` RepoName string `orm:"column(repo_name)"`

View File

@ -7,5 +7,9 @@ import (
func init() { func init() {
orm.RegisterModel(new(RepTarget), orm.RegisterModel(new(RepTarget),
new(RepPolicy), new(RepPolicy),
new(RepJob)) new(RepJob),
new(User),
new(Project),
new(Role),
new(AccessLog))
} }

View File

@ -21,7 +21,7 @@ import (
// Project holds the details of a project. // Project holds the details of a project.
type Project struct { 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"` OwnerID int `orm:"column(owner_id)" json:"OwnerId"`
Name string `orm:"column(name)"` Name string `orm:"column(name)"`
CreationTime time.Time `orm:"column(creation_time)"` CreationTime time.Time `orm:"column(creation_time)"`

View File

@ -26,7 +26,7 @@ const (
// Role holds the details of a role. // Role holds the details of a role.
type Role struct { 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"` RoleCode string `orm:"column(role_code)" json:"role_code"`
Name string `orm:"column(name)" json:"role_name"` Name string `orm:"column(name)" json:"role_name"`

22
models/toprepo.go Normal file
View File

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

View File

@ -21,7 +21,7 @@ import (
// User holds the details of a user. // User holds the details of a user.
type User struct { 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"` Username string `orm:"column(username)" json:"username"`
Email string `orm:"column(email)" json:"email"` Email string `orm:"column(email)" json:"email"`
Password string `orm:"column(password)" json:"password"` Password string `orm:"column(password)" json:"password"`
@ -30,7 +30,7 @@ type User struct {
Deleted int `orm:"column(deleted)"` Deleted int `orm:"column(deleted)"`
Rolename string Rolename string
RoleID int `json:"RoleId"` RoleID int `json:"RoleId"`
RoleList []Role RoleList []*Role `orm:"rel(m2m)"`
HasAdminRole int `orm:"column(sysadmin_flag)"` HasAdminRole int `orm:"column(sysadmin_flag)"`
ResetUUID string `orm:"column(reset_uuid)" json:"ResetUuid"` ResetUUID string `orm:"column(reset_uuid)" json:"ResetUuid"`
Salt string `orm:"column(salt)"` Salt string `orm:"column(salt)"`

View File

@ -55,6 +55,7 @@ func initRouters() {
beego.Router("/api/projects/:pid([0-9]+)/members/?:mid", &api.ProjectMemberAPI{}) beego.Router("/api/projects/:pid([0-9]+)/members/?:mid", &api.ProjectMemberAPI{})
beego.Router("/api/projects/", &api.ProjectAPI{}, "get:List") beego.Router("/api/projects/", &api.ProjectAPI{}, "get:List")
beego.Router("/api/projects/?:id", &api.ProjectAPI{}) 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/statistics", &api.StatisticAPI{})
beego.Router("/api/projects/:id([0-9]+)/logs/filter", &api.ProjectAPI{}, "post:FilterAccessLog") beego.Router("/api/projects/:id([0-9]+)/logs/filter", &api.ProjectAPI{}, "post:FilterAccessLog")
beego.Router("/api/users/?:id", &api.UserAPI{}) 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/policies/replication/:id([0-9]+)/enablement", &api.RepPolicyAPI{}, "put:UpdateEnablement")
beego.Router("/api/targets/?:id([0-9]+)", &api.TargetAPI{}) beego.Router("/api/targets/?:id([0-9]+)", &api.TargetAPI{})
beego.Router("/api/targets/ping", &api.TargetAPI{}, "post:Ping") 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: //external service that hosted on harbor process:
beego.Router("/service/notifications", &service.NotificationHandler{}) beego.Router("/service/notifications", &service.NotificationHandler{})