Merge pull request #6756 from wy65701436/robot-api

Add API implementation of robot account
This commit is contained in:
Steven Zou 2019-01-17 16:47:01 +08:00 committed by GitHub
commit 4b7997250a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 588 additions and 4 deletions

View File

@ -15,6 +15,7 @@
package dao
import (
"errors"
"fmt"
"strconv"
"strings"
@ -32,6 +33,9 @@ const (
ClairDBAlias = "clair-db"
)
// ErrDupRows is returned by DAO when inserting failed with error "duplicate key value violates unique constraint"
var ErrDupRows = errors.New("sql: duplicate row in DB")
// Database is an interface of different databases
type Database interface {
// Name returns the name of database

View File

@ -17,6 +17,7 @@ package dao
import (
"github.com/astaxie/beego/orm"
"github.com/goharbor/harbor/src/common/models"
"strings"
"time"
)
@ -25,7 +26,14 @@ func AddRobot(robot *models.Robot) (int64, error) {
now := time.Now()
robot.CreationTime = now
robot.UpdateTime = now
return GetOrmer().Insert(robot)
id, err := GetOrmer().Insert(robot)
if err != nil {
if strings.Contains(err.Error(), "duplicate key value violates unique constraint") {
return 0, ErrDupRows
}
return 0, err
}
return id, nil
}
// GetRobotByID ...
@ -79,6 +87,11 @@ func getRobotQuerySetter(query *models.RobotQuery) orm.QuerySeter {
return qs
}
// CountRobot ...
func CountRobot(query *models.RobotQuery) (int64, error) {
return getRobotQuerySetter(query).Count()
}
// UpdateRobot ...
func UpdateRobot(robot *models.Robot) error {
robot.UpdateTime = time.Now()

View File

@ -15,6 +15,7 @@
package models
import (
"github.com/astaxie/beego/validation"
"time"
)
@ -42,7 +43,26 @@ type RobotQuery struct {
Pagination
}
// RobotReq ...
type RobotReq struct {
Name string `json:"name"`
Description string `json:"description"`
Disabled bool `json:"disabled"`
Access []*ResourceActions `json:"access"`
}
// Valid put request validation
func (rq *RobotReq) Valid(v *validation.Validation) {
// ToDo: add validation for access info.
}
// RobotRep ...
type RobotRep struct {
Name string
Token string
}
// TableName ...
func (u *Robot) TableName() string {
func (r *Robot) TableName() string {
return RobotTable
}

View File

@ -20,3 +20,9 @@ type Token struct {
ExpiresIn int `json:"expires_in"`
IssuedAt string `json:"issued_at"`
}
// ResourceActions ...
type ResourceActions struct {
Name string `json:"name"`
Actions []string `json:"actions"`
}

View File

@ -40,8 +40,8 @@ import (
)
var (
nonSysAdminID, projAdminID, projDeveloperID, projGuestID int64
projAdminPMID, projDeveloperPMID, projGuestPMID int
nonSysAdminID, projAdminID, projDeveloperID, projGuestID, projAdminRobotID int64
projAdminPMID, projDeveloperPMID, projGuestPMID, projAdminRobotPMID int
// The following users/credentials are registered and assigned roles at the beginning of
// running testing and cleaned up at the end.
// Do not try to change the system and project roles that the users have during
@ -67,6 +67,10 @@ var (
Name: "proj_guest",
Passwd: "Harbor12345",
}
projAdmin4Robot = &usrInfo{
Name: "proj_admin_robot",
Passwd: "Harbor12345",
}
)
type testingRequest struct {
@ -240,6 +244,25 @@ func prepare() error {
return err
}
// register projAdminRobots and assign project admin role
projAdminRobotID, err = dao.Register(models.User{
Username: projAdmin4Robot.Name,
Password: projAdmin4Robot.Passwd,
Email: projAdmin4Robot.Name + "@test.com",
})
if err != nil {
return err
}
if projAdminRobotPMID, err = project.AddProjectMember(models.Member{
ProjectID: 1,
Role: models.PROJECTADMIN,
EntityID: int(projAdminRobotID),
EntityType: common.UserMember,
}); err != nil {
return err
}
// register projDeveloper and assign project developer role
projDeveloperID, err = dao.Register(models.User{
Username: projDeveloper.Name,

View File

@ -153,6 +153,9 @@ func init() {
beego.Router("/api/system/gc/:id([0-9]+)/log", &GCAPI{}, "get:GetLog")
beego.Router("/api/system/gc/schedule", &GCAPI{}, "get:Get;put:Put;post:Post")
beego.Router("/api/projects/:pid([0-9]+)/robots/", &RobotAPI{}, "post:Post;get:List")
beego.Router("/api/projects/:pid([0-9]+)/robots/:id([0-9]+)", &RobotAPI{}, "get:Get;put:Put;delete:Delete")
// Charts are controlled under projects
chartRepositoryAPIType := &ChartRepositoryAPI{}
beego.Router("/api/chartrepo/health", chartRepositoryAPIType, "get:GetHealthStatus")

197
src/core/api/robot.go Normal file
View File

@ -0,0 +1,197 @@
// Copyright 2018 Project Harbor Authors
//
// 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 (
"fmt"
"github.com/goharbor/harbor/src/common/dao"
"github.com/goharbor/harbor/src/common/models"
"net/http"
"strconv"
)
// User this prefix to distinguish harbor user,
// The prefix contains a specific character($), so it cannot be registered as a harbor user.
const robotPrefix = "robot$"
// RobotAPI ...
type RobotAPI struct {
BaseController
project *models.Project
robot *models.Robot
}
// Prepare ...
func (r *RobotAPI) Prepare() {
r.BaseController.Prepare()
method := r.Ctx.Request.Method
if !r.SecurityCtx.IsAuthenticated() {
r.HandleUnauthorized()
return
}
pid, err := r.GetInt64FromPath(":pid")
if err != nil || pid <= 0 {
var errMsg string
if err != nil {
errMsg = "failed to get project ID " + err.Error()
} else {
errMsg = "invalid project ID: " + fmt.Sprintf("%d", pid)
}
r.HandleBadRequest(errMsg)
return
}
project, err := r.ProjectMgr.Get(pid)
if err != nil {
r.ParseAndHandleError(fmt.Sprintf("failed to get project %d", pid), err)
return
}
if project == nil {
r.HandleNotFound(fmt.Sprintf("project %d not found", pid))
return
}
r.project = project
if method == http.MethodPut || method == http.MethodDelete {
id, err := r.GetInt64FromPath(":id")
if err != nil || id <= 0 {
r.HandleBadRequest("invalid robot ID")
return
}
robot, err := dao.GetRobotByID(id)
if err != nil {
r.HandleInternalServerError(fmt.Sprintf("failed to get robot %d: %v", id, err))
return
}
if robot == nil {
r.HandleNotFound(fmt.Sprintf("robot %d not found", id))
return
}
r.robot = robot
}
if !(r.Ctx.Input.IsGet() && r.SecurityCtx.HasReadPerm(pid) ||
r.SecurityCtx.HasAllPerm(pid)) {
r.HandleForbidden(r.SecurityCtx.GetUsername())
return
}
}
// Post ...
func (r *RobotAPI) Post() {
var robotReq models.RobotReq
r.DecodeJSONReq(&robotReq)
createdName := robotPrefix + robotReq.Name
robot := models.Robot{
Name: createdName,
Description: robotReq.Description,
ProjectID: r.project.ProjectID,
// TODO: use token service to generate token per access information
Token: "this is a placeholder",
}
id, err := dao.AddRobot(&robot)
if err != nil {
if err == dao.ErrDupRows {
r.HandleConflict()
return
}
r.HandleInternalServerError(fmt.Sprintf("failed to create robot account: %v", err))
return
}
robotRep := models.RobotRep{
Name: robot.Name,
Token: robot.Token,
}
r.Redirect(http.StatusCreated, strconv.FormatInt(id, 10))
r.Data["json"] = robotRep
r.ServeJSON()
}
// List list all the robots of a project
func (r *RobotAPI) List() {
query := models.RobotQuery{
ProjectID: r.project.ProjectID,
}
count, err := dao.CountRobot(&query)
if err != nil {
r.HandleInternalServerError(fmt.Sprintf("failed to list robots on project: %d, %v", r.project.ProjectID, err))
return
}
query.Page, query.Size = r.GetPaginationParams()
robots, err := dao.ListRobots(&query)
if err != nil {
r.HandleInternalServerError(fmt.Sprintf("failed to get robots %v", err))
return
}
r.SetPaginationHeader(count, query.Page, query.Size)
r.Data["json"] = robots
r.ServeJSON()
}
// Get get robot by id
func (r *RobotAPI) Get() {
id, err := r.GetInt64FromPath(":id")
if err != nil || id <= 0 {
r.HandleBadRequest(fmt.Sprintf("invalid robot ID: %s", r.GetStringFromPath(":id")))
return
}
robot, err := dao.GetRobotByID(id)
if err != nil {
r.HandleInternalServerError(fmt.Sprintf("failed to get robot %d: %v", id, err))
return
}
if robot == nil {
r.HandleNotFound(fmt.Sprintf("robot %d not found", id))
return
}
r.Data["json"] = robot
r.ServeJSON()
}
// Put disable or enable a robot account
func (r *RobotAPI) Put() {
var robotReq models.RobotReq
r.DecodeJSONReqAndValidate(&robotReq)
r.robot.Disabled = robotReq.Disabled
if err := dao.UpdateRobot(r.robot); err != nil {
r.HandleInternalServerError(fmt.Sprintf("failed to update robot %d: %v", r.robot.ID, err))
return
}
}
// Delete delete robot by id
func (r *RobotAPI) Delete() {
if err := dao.DeleteRobot(r.robot.ID); err != nil {
r.HandleInternalServerError(fmt.Sprintf("failed to delete robot %d: %v", r.robot.ID, err))
return
}
}

314
src/core/api/robot_test.go Normal file
View File

@ -0,0 +1,314 @@
// Copyright 2018 Project Harbor Authors
//
// 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 (
"fmt"
"github.com/goharbor/harbor/src/common/models"
"net/http"
"testing"
)
var (
robotPath = "/api/projects/1/robots"
robotID int64
)
func TestRobotAPIPost(t *testing.T) {
cases := []*codeCheckingCase{
// 401
{
request: &testingRequest{
method: http.MethodPost,
url: robotPath,
},
code: http.StatusUnauthorized,
},
// 403
{
request: &testingRequest{
method: http.MethodPost,
url: robotPath,
bodyJSON: &models.Robot{},
credential: nonSysAdmin,
},
code: http.StatusForbidden,
},
// 201
{
request: &testingRequest{
method: http.MethodPost,
url: robotPath,
bodyJSON: &models.Robot{
Name: "test",
Description: "test desc",
},
credential: projAdmin4Robot,
},
code: http.StatusCreated,
},
// 403 -- developer
{
request: &testingRequest{
method: http.MethodPost,
url: robotPath,
bodyJSON: &models.Robot{
Name: "test2",
Description: "test2 desc",
},
credential: projDeveloper,
},
code: http.StatusForbidden,
},
// 409
{
request: &testingRequest{
method: http.MethodPost,
url: robotPath,
bodyJSON: &models.Robot{
Name: "test",
Description: "test desc",
ProjectID: 1,
},
credential: projAdmin4Robot,
},
code: http.StatusConflict,
},
}
runCodeCheckingCases(t, cases...)
}
func TestRobotAPIGet(t *testing.T) {
cases := []*codeCheckingCase{
// 400
{
request: &testingRequest{
method: http.MethodGet,
url: fmt.Sprintf("%s/%d", robotPath, 0),
},
code: http.StatusUnauthorized,
},
// 404
{
request: &testingRequest{
method: http.MethodGet,
url: fmt.Sprintf("%s/%d", robotPath, 1000),
credential: projDeveloper,
},
code: http.StatusNotFound,
},
// 200
{
request: &testingRequest{
method: http.MethodGet,
url: fmt.Sprintf("%s/%d", robotPath, 1),
credential: projDeveloper,
},
code: http.StatusOK,
},
// 200
{
request: &testingRequest{
method: http.MethodGet,
url: fmt.Sprintf("%s/%d", robotPath, 1),
credential: projAdmin4Robot,
},
code: http.StatusOK,
},
}
runCodeCheckingCases(t, cases...)
}
func TestRobotAPIList(t *testing.T) {
cases := []*codeCheckingCase{
// 401
{
request: &testingRequest{
method: http.MethodGet,
url: robotPath,
},
code: http.StatusUnauthorized,
},
// 400
{
request: &testingRequest{
method: http.MethodGet,
url: "/api/projects/0/robots",
credential: projAdmin4Robot,
},
code: http.StatusBadRequest,
},
// 200
{
request: &testingRequest{
method: http.MethodGet,
url: robotPath,
credential: projDeveloper,
},
code: http.StatusOK,
},
// 200
{
request: &testingRequest{
method: http.MethodGet,
url: robotPath,
credential: projAdmin4Robot,
},
code: http.StatusOK,
},
}
runCodeCheckingCases(t, cases...)
}
func TestRobotAPIPut(t *testing.T) {
cases := []*codeCheckingCase{
// 401
{
request: &testingRequest{
method: http.MethodPut,
url: fmt.Sprintf("%s/%d", robotPath, 1),
},
code: http.StatusUnauthorized,
},
// 400
{
request: &testingRequest{
method: http.MethodPut,
url: fmt.Sprintf("%s/%d", robotPath, 0),
credential: projAdmin4Robot,
},
code: http.StatusBadRequest,
},
// 404
{
request: &testingRequest{
method: http.MethodPut,
url: fmt.Sprintf("%s/%d", robotPath, 10000),
credential: projAdmin4Robot,
},
code: http.StatusNotFound,
},
// 403 non-member user
{
request: &testingRequest{
method: http.MethodPut,
url: fmt.Sprintf("%s/%d", robotPath, 1),
credential: nonSysAdmin,
},
code: http.StatusForbidden,
},
// 403 developer
{
request: &testingRequest{
method: http.MethodPut,
url: fmt.Sprintf("%s/%d", robotPath, 1),
credential: projDeveloper,
},
code: http.StatusForbidden,
},
// 200
{
request: &testingRequest{
method: http.MethodPut,
url: fmt.Sprintf("%s/%d", robotPath, 1),
bodyJSON: &models.Robot{
Disabled: true,
},
credential: projAdmin4Robot,
},
code: http.StatusOK,
},
}
runCodeCheckingCases(t, cases...)
}
func TestRobotAPIDelete(t *testing.T) {
cases := []*codeCheckingCase{
// 401
{
request: &testingRequest{
method: http.MethodDelete,
url: fmt.Sprintf("%s/%d", robotPath, 1),
},
code: http.StatusUnauthorized,
},
// 400
{
request: &testingRequest{
method: http.MethodDelete,
url: fmt.Sprintf("%s/%d", robotPath, 0),
credential: projAdmin4Robot,
},
code: http.StatusBadRequest,
},
// 404
{
request: &testingRequest{
method: http.MethodDelete,
url: fmt.Sprintf("%s/%d", robotPath, 10000),
credential: projAdmin4Robot,
},
code: http.StatusNotFound,
},
// 403 non-member user
{
request: &testingRequest{
method: http.MethodDelete,
url: fmt.Sprintf("%s/%d", robotPath, 1),
credential: nonSysAdmin,
},
code: http.StatusForbidden,
},
// 403 developer
{
request: &testingRequest{
method: http.MethodDelete,
url: fmt.Sprintf("%s/%d", robotPath, 1),
credential: projDeveloper,
},
code: http.StatusForbidden,
},
// 200
{
request: &testingRequest{
method: http.MethodDelete,
url: fmt.Sprintf("%s/%d", robotPath, 1),
credential: projAdmin4Robot,
},
code: http.StatusOK,
},
}
runCodeCheckingCases(t, cases...)
}

View File

@ -65,6 +65,10 @@ func initRouters() {
beego.Router("/api/projects/:id([0-9]+)/metadatas/?:name", &api.MetadataAPI{}, "get:Get")
beego.Router("/api/projects/:id([0-9]+)/metadatas/", &api.MetadataAPI{}, "post:Post")
beego.Router("/api/projects/:id([0-9]+)/metadatas/:name", &api.MetadataAPI{}, "put:Put;delete:Delete")
beego.Router("/api/projects/:pid([0-9]+)/robots", &api.RobotAPI{}, "post:Post;get:List")
beego.Router("/api/projects/:pid([0-9]+)/robots/:id([0-9]+)", &api.RobotAPI{}, "get:Get;put:Put;delete:Delete")
beego.Router("/api/repositories", &api.RepositoryAPI{}, "get:Get")
beego.Router("/api/repositories/scanAll", &api.RepositoryAPI{}, "post:ScanAll")
beego.Router("/api/repositories/*", &api.RepositoryAPI{}, "delete:Delete;put:Put")