add robot account 2 api handler

Signed-off-by: Wang Yan <wangyan@vmware.com>
This commit is contained in:
Wang Yan 2020-11-19 17:57:57 +08:00
parent 9571af84bd
commit 8e61a3ea31
6 changed files with 738 additions and 7 deletions

View File

@ -1410,6 +1410,154 @@ paths:
$ref: '#/responses/404'
'500':
$ref: '#/responses/500'
/projects/{project_id_or_name}/robots:
get:
summary: Get all robot accounts of specified project
description: Get all robot accounts of specified project
parameters:
- $ref: '#/parameters/requestId'
- $ref: '#/parameters/projectIDOrName'
- $ref: '#/parameters/page'
- $ref: '#/parameters/pageSize'
- $ref: '#/parameters/query'
tags:
- robotv1
operationId: ListRobotV1
responses:
'200':
description: Success
headers:
X-Total-Count:
description: The total count of robot accounts
type: integer
Link:
description: Link refers to the previous page and next page
type: string
schema:
type: array
items:
$ref: '#/definitions/Robot'
'400':
$ref: '#/responses/400'
'404':
$ref: '#/responses/404'
'500':
$ref: '#/responses/500'
post:
summary: Create a robot account
description: Create a robot account
tags:
- robotv1
operationId: CreateRobotV1
parameters:
- $ref: '#/parameters/requestId'
- $ref: '#/parameters/projectIDOrName'
- name: robot
in: body
description: The JSON object of a robot account.
required: true
schema:
$ref: '#/definitions/RobotCreateV1'
responses:
'201':
description: Created
headers:
X-Request-Id:
description: The ID of the corresponding request for the response
type: string
Location:
description: The location of the resource
type: string
schema:
$ref: '#/definitions/RobotCreated'
'400':
$ref: '#/responses/400'
'401':
$ref: '#/responses/401'
'403':
$ref: '#/responses/403'
'404':
$ref: '#/responses/404'
'500':
$ref: '#/responses/500'
/projects/{project_id_or_name}/robots/{robot_id}:
get:
summary: Get a robot account
description: This endpoint returns specific robot account information by robot ID.
tags:
- robotv1
operationId: GetRobotByIDV1
parameters:
- $ref: '#/parameters/requestId'
- $ref: '#/parameters/projectIDOrName'
- $ref: '#/parameters/robotId'
responses:
'200':
description: Return matched robot information.
schema:
$ref: '#/definitions/Robot'
'401':
$ref: '#/responses/401'
'403':
$ref: '#/responses/403'
'404':
$ref: '#/responses/404'
'500':
$ref: '#/responses/500'
put:
summary: Update status of robot account.
description: Used to disable/enable a specified robot account.
tags:
- robotv1
operationId: UpdateRobotV1
parameters:
- $ref: '#/parameters/requestId'
- $ref: '#/parameters/projectIDOrName'
- $ref: '#/parameters/robotId'
- name: robot
in: body
description: The JSON object of a robot account.
required: true
schema:
$ref: '#/definitions/Robot'
responses:
'200':
$ref: '#/responses/200'
'400':
$ref: '#/responses/400'
'401':
$ref: '#/responses/401'
'403':
$ref: '#/responses/403'
'404':
$ref: '#/responses/404'
'409':
$ref: '#/responses/409'
'500':
$ref: '#/responses/500'
delete:
summary: Delete a robot account
description: This endpoint deletes specific robot account information by robot ID.
tags:
- robotv1
operationId: DeleteRobotV1
parameters:
- $ref: '#/parameters/requestId'
- $ref: '#/parameters/projectIDOrName'
- $ref: '#/parameters/robotId'
responses:
'200':
$ref: '#/responses/200'
'400':
$ref: '#/responses/400'
'401':
$ref: '#/responses/401'
'403':
$ref: '#/responses/403'
'404':
$ref: '#/responses/404'
'500':
$ref: '#/responses/500'
/icons/{digest}:
get:
summary: Get artifact icon
@ -1431,6 +1579,149 @@ paths:
$ref: '#/responses/404'
'500':
$ref: '#/responses/500'
/robots:
get:
summary: Get robot account
description: List the robot accounts with the specified level and project.
tags:
- robot
operationId: ListRobot
parameters:
- $ref: '#/parameters/requestId'
- $ref: '#/parameters/query'
- $ref: '#/parameters/page'
- $ref: '#/parameters/pageSize'
responses:
'200':
description: Success
headers:
X-Total-Count:
description: The total count of robot accounts
type: integer
Link:
description: Link refers to the previous page and next page
type: string
schema:
type: array
items:
$ref: '#/definitions/Robot'
'400':
$ref: '#/responses/400'
'404':
$ref: '#/responses/404'
'500':
$ref: '#/responses/500'
post:
summary: Create a robot account
description: Create a robot account
tags:
- robot
operationId: CreateRobot
parameters:
- $ref: '#/parameters/requestId'
- name: robot
in: body
description: The JSON object of a robot account.
required: true
schema:
$ref: '#/definitions/RobotCreate'
responses:
'201':
description: Created
headers:
X-Request-Id:
description: The ID of the corresponding request for the response
type: string
Location:
description: The location of the resource
type: string
schema:
$ref: '#/definitions/RobotCreated'
'400':
$ref: '#/responses/400'
'401':
$ref: '#/responses/401'
'403':
$ref: '#/responses/403'
'404':
$ref: '#/responses/404'
'500':
$ref: '#/responses/500'
/robots/{robot_id}:
get:
summary: Get a robot account
description: This endpoint returns specific robot account information by robot ID.
tags:
- robot
operationId: GetRobotByID
parameters:
- $ref: '#/parameters/requestId'
- $ref: '#/parameters/robotId'
responses:
'200':
description: Return matched robot information.
schema:
$ref: '#/definitions/Robot'
'401':
$ref: '#/responses/401'
'403':
$ref: '#/responses/403'
'404':
$ref: '#/responses/404'
'500':
$ref: '#/responses/500'
put:
summary: Update a robot account
description: This endpoint updates specific robot account information by robot ID.
tags:
- robot
operationId: UpdateRobot
parameters:
- $ref: '#/parameters/requestId'
- $ref: '#/parameters/robotId'
- name: robot
in: body
description: The JSON object of a robot account.
required: true
schema:
$ref: '#/definitions/RobotCreate'
responses:
'200':
$ref: '#/responses/200'
'400':
$ref: '#/responses/400'
'401':
$ref: '#/responses/401'
'403':
$ref: '#/responses/403'
'404':
$ref: '#/responses/404'
'409':
$ref: '#/responses/409'
'500':
$ref: '#/responses/500'
delete:
summary: Delete a robot account
description: This endpoint deletes specific robot account information by robot ID.
tags:
- robot
operationId: DeleteRobot
parameters:
- $ref: '#/parameters/requestId'
- $ref: '#/parameters/robotId'
responses:
'200':
$ref: '#/responses/200'
'400':
$ref: '#/responses/400'
'401':
$ref: '#/responses/401'
'403':
$ref: '#/responses/403'
'404':
$ref: '#/responses/404'
'500':
$ref: '#/responses/500'
/replication/executions:
get:
summary: List replication executions
@ -1661,6 +1952,12 @@ parameters:
required: true
type: integer
format: int64
projectIDOrName:
name: project_id_or_name
in: path
description: The id or name of the project
required: true
type: string
repositoryName:
name: repository_name
in: path
@ -1726,6 +2023,12 @@ parameters:
description: Task ID
required: true
type: integer
robotId:
name: robot_id
in: path
description: Robot ID
required: true
type: integer
responses:
'200':
description: Success
@ -2518,7 +2821,7 @@ definitions:
$ref: "#/definitions/ResourceList"
description: The used status of the quota
registry:
$ref: "#/definitions/Registry"
$ref: "#/definitions/Registry"
CVEAllowlist:
type: object
description: The CVE Allowlist for system or project
@ -2693,3 +2996,129 @@ definitions:
type: string
format: date-time
description: The end time of the task
Robot:
type: object
properties:
id:
type: integer
format: int64
description: The ID of the robot
name:
type: string
description: The name of the tag
description:
type: string
description: The description of the robot
secret:
type: string
description: The secret of the robot
level:
type: string
description: The level of the robot, project or system
disable:
type: boolean
x-omitempty: false
description: The disable status of the robot
expires_at:
type: integer
format: int64
description: The expiration data of the robot
permissions:
type: array
items:
$ref: '#/definitions/Permission'
creation_time:
type: string
format: date-time
description: The creation time of the robot.
update_time:
type: string
format: date-time
description: The update time of the robot.
RobotCreate:
type: object
description: The request for robot account creation.
properties:
name:
type: string
description: The name of the tag
description:
type: string
description: The description of the robot
secret:
type: string
description: The secret of the robot
level:
type: string
description: The level of the robot, project or system
disable:
type: boolean
description: The disable status of the robot
expires_at:
type: integer
format: int64
description: The expiration data of the robot
permissions:
type: array
items:
$ref: '#/definitions/Permission'
RobotCreated:
type: object
description: The response for robot account creation.
properties:
id:
type: integer
format: int64
description: The ID of the robot
name:
type: string
description: The name of the tag
secret:
type: string
description: The secret of the robot
creation_time:
type: string
format: date-time
description: The creation time of the robot.
Permission:
type: object
properties:
kind:
type: string
description: The kind of the permission
namespace:
type: string
description: The namespace of the permission
access:
type: array
items:
$ref: '#/definitions/Access'
Access:
type: object
properties:
resource:
type: string
description: The resource of the access
action:
type: string
description: The action of the access
effect:
type: string
description: The effect of the access
RobotCreateV1:
type: object
properties:
name:
type: string
description: The name of robot account
description:
type: string
description: The description of robot account
expires_at:
type: integer
description: The expiration time on or after which the JWT MUST NOT be accepted for processing.
access:
type: array
description: The permission of robot account
items:
$ref: '#/definitions/Access'

View File

@ -37,9 +37,9 @@ func (eff Effect) String() string {
// Policy the type of policy
type Policy struct {
Resource
Action
Effect
Resource `json:"resource,omitempty"`
Action `json:"action,omitempty"`
Effect `json:"effect,omitempty"`
}
// GetEffect returns effect of resource, default is allow

View File

@ -36,6 +36,8 @@ func New() http.Handler {
ProjectAPI: newProjectAPI(),
PreheatAPI: newPreheatAPI(),
IconAPI: newIconAPI(),
RobotAPI: newRobotAPI(),
Robotv1API: newRobotV1API(),
ReplicationAPI: newReplicationAPI(),
})
if err != nil {

View File

@ -0,0 +1,41 @@
package model
import (
"github.com/go-openapi/strfmt"
"github.com/goharbor/harbor/src/controller/robot"
"github.com/goharbor/harbor/src/lib"
"github.com/goharbor/harbor/src/server/v2.0/models"
)
type Robot struct {
*robot.Robot
}
func (r *Robot) ToSwagger() *models.Robot {
perms := []*models.Permission{}
for _, p := range r.Permissions {
temp := &models.Permission{}
lib.JSONCopy(temp, p)
perms = append(perms, temp)
}
return &models.Robot{
ID: r.ID,
Name: r.Name,
Description: r.Description,
Secret: r.Secret,
ExpiresAt: r.ExpiresAt,
Level: r.Level,
Disable: r.Disabled,
CreationTime: strfmt.DateTime(r.CreationTime),
UpdateTime: strfmt.DateTime(r.UpdateTime),
Permissions: perms,
}
}
// NewRobot ...
func NewRobot(r *robot.Robot) *Robot {
return &Robot{
Robot: r,
}
}

View File

@ -29,7 +29,7 @@ import (
"github.com/goharbor/harbor/src/pkg/project/metadata"
"github.com/goharbor/harbor/src/pkg/quota/types"
"github.com/goharbor/harbor/src/pkg/retention/policy"
"github.com/goharbor/harbor/src/pkg/robot"
"github.com/goharbor/harbor/src/pkg/robot2"
"github.com/goharbor/harbor/src/pkg/user"
"github.com/goharbor/harbor/src/replication"
"github.com/goharbor/harbor/src/server/v2.0/handler/model"
@ -48,7 +48,7 @@ func newProjectAPI() *projectAPI {
repositoryCtl: repository.Ctl,
projectCtl: project.Ctl,
quotaCtl: quota.Ctl,
robotMgr: robot.Mgr,
robotMgr: robot2.Mgr,
preheatCtl: preheat.Ctl,
}
}
@ -61,7 +61,7 @@ type projectAPI struct {
repositoryCtl repository.Controller
projectCtl project.Controller
quotaCtl quota.Controller
robotMgr robot.Manager
robotMgr robot2.Manager
preheatCtl preheat.Controller
}

View File

@ -0,0 +1,259 @@
package handler
import (
"context"
"fmt"
"github.com/go-openapi/runtime/middleware"
"github.com/go-openapi/strfmt"
"github.com/goharbor/harbor/src/common/rbac"
"github.com/goharbor/harbor/src/common/utils"
"github.com/goharbor/harbor/src/controller/robot"
"github.com/goharbor/harbor/src/core/config"
"github.com/goharbor/harbor/src/lib"
"github.com/goharbor/harbor/src/lib/errors"
pkg "github.com/goharbor/harbor/src/pkg/robot2/model"
"github.com/goharbor/harbor/src/server/v2.0/handler/model"
"github.com/goharbor/harbor/src/server/v2.0/models"
operation "github.com/goharbor/harbor/src/server/v2.0/restapi/operations/robot"
"strconv"
"strings"
)
func newRobotAPI() *robotAPI {
return &robotAPI{
robotCtl: robot.Ctl,
}
}
type robotAPI struct {
BaseAPI
robotCtl robot.Controller
}
func (rAPI *robotAPI) CreateRobot(ctx context.Context, params operation.CreateRobotParams) middleware.Responder {
if err := rAPI.validate(params.Robot); err != nil {
return rAPI.SendError(ctx, err)
}
if err := rAPI.requireAccess(ctx, params.Robot.Level, params.Robot.Permissions[0].Namespace, rbac.ActionCreate); err != nil {
return rAPI.SendError(ctx, err)
}
r := &robot.Robot{
Robot: pkg.Robot{
Name: params.Robot.Name,
Description: params.Robot.Description,
ExpiresAt: params.Robot.ExpiresAt,
},
Level: params.Robot.Level,
}
lib.JSONCopy(&r.Permissions, params.Robot.Permissions)
rid, err := rAPI.robotCtl.Create(ctx, r)
if err != nil {
return rAPI.SendError(ctx, err)
}
created, err := rAPI.robotCtl.Get(ctx, rid, nil)
if err != nil {
return rAPI.SendError(ctx, err)
}
location := fmt.Sprintf("%s/%d", strings.TrimSuffix(params.HTTPRequest.URL.Path, "/"), created.ID)
return operation.NewCreateRobotCreated().WithLocation(location).WithPayload(&models.RobotCreated{
ID: created.ID,
Name: created.Name,
Secret: created.Secret,
CreationTime: strfmt.DateTime(created.CreationTime),
})
}
func (rAPI *robotAPI) DeleteRobot(ctx context.Context, params operation.DeleteRobotParams) middleware.Responder {
if err := rAPI.RequireAuthenticated(ctx); err != nil {
return rAPI.SendError(ctx, err)
}
r, err := rAPI.robotCtl.Get(ctx, params.RobotID, nil)
if err != nil {
return rAPI.SendError(ctx, err)
}
if err := rAPI.requireAccess(ctx, r.Level, r.ProjectID, rbac.ActionDelete); err != nil {
return rAPI.SendError(ctx, err)
}
if err := rAPI.robotCtl.Delete(ctx, params.RobotID); err != nil {
return rAPI.SendError(ctx, err)
}
return operation.NewDeleteRobotOK()
}
func (rAPI *robotAPI) ListRobot(ctx context.Context, params operation.ListRobotParams) middleware.Responder {
if err := rAPI.RequireAuthenticated(ctx); err != nil {
return rAPI.SendError(ctx, err)
}
query, err := rAPI.BuildQuery(ctx, params.Q, params.Page, params.PageSize)
if err != nil {
return rAPI.SendError(ctx, err)
}
var projectID int64
var level string
// GET /api/v2.0/robots or GET /api/v2.0/robots?level=system to get all of system level robots.
// GET /api/v2.0/robots?level=project&project_id=1
if _, ok := query.Keywords["Level"]; ok {
if !isValidLevel(query.Keywords["Level"].(string)) {
return rAPI.SendError(ctx, errors.New(nil).WithMessage("bad request error level input").WithCode(errors.BadRequestCode))
}
level = query.Keywords["Level"].(string)
if level == robot.LEVELPROJECT {
if _, ok := query.Keywords["ProjectID"]; !ok {
return rAPI.SendError(ctx, errors.BadRequestError(nil).WithMessage("must with project ID when to query project robots"))
}
pid, err := strconv.ParseInt(query.Keywords["ProjectID"].(string), 10, 64)
if err != nil {
return rAPI.SendError(ctx, errors.BadRequestError(nil).WithMessage("Project ID must be int type."))
}
projectID = pid
}
} else {
level = robot.LEVELSYSTEM
projectID = 0
query.Keywords["ProjectID"] = 0
}
if err := rAPI.requireAccess(ctx, level, projectID, rbac.ActionList); err != nil {
return rAPI.SendError(ctx, err)
}
total, err := rAPI.robotCtl.Count(ctx, query)
if err != nil {
return rAPI.SendError(ctx, err)
}
robots, err := rAPI.robotCtl.List(ctx, query, &robot.Option{
WithPermission: true,
})
if err != nil {
return rAPI.SendError(ctx, err)
}
var results []*models.Robot
for _, r := range robots {
results = append(results, model.NewRobot(r).ToSwagger())
}
return operation.NewListRobotOK().
WithXTotalCount(total).
WithLink(rAPI.Links(ctx, params.HTTPRequest.URL, total, query.PageNumber, query.PageSize).String()).
WithPayload(results)
}
func (rAPI *robotAPI) GetRobotByID(ctx context.Context, params operation.GetRobotByIDParams) middleware.Responder {
if err := rAPI.RequireAuthenticated(ctx); err != nil {
return rAPI.SendError(ctx, err)
}
r, err := rAPI.robotCtl.Get(ctx, params.RobotID, &robot.Option{
WithPermission: true,
})
if err != nil {
return rAPI.SendError(ctx, err)
}
if err := rAPI.requireAccess(ctx, r.Level, r.ProjectID, rbac.ActionRead); err != nil {
return rAPI.SendError(ctx, err)
}
return operation.NewGetRobotByIDOK().WithPayload(model.NewRobot(r).ToSwagger())
}
func (rAPI *robotAPI) UpdateRobot(ctx context.Context, params operation.UpdateRobotParams) middleware.Responder {
if err := rAPI.validate(params.Robot); err != nil {
return rAPI.SendError(ctx, err)
}
if err := rAPI.requireAccess(ctx, params.Robot.Level, params.Robot.Permissions[0].Namespace, rbac.ActionUpdate); err != nil {
return rAPI.SendError(ctx, err)
}
r, err := rAPI.robotCtl.Get(ctx, params.RobotID, &robot.Option{
WithPermission: true,
})
if err != nil {
return rAPI.SendError(ctx, err)
}
if params.Robot.Level != r.Level || params.Robot.Name != r.Name {
return rAPI.SendError(ctx, errors.BadRequestError(nil).WithMessage("cannot update the level or name of robot"))
}
// refresh secret only
if params.Robot.Secret != r.Secret && params.Robot.Secret != "" {
key, err := config.SecretKey()
if err != nil {
return rAPI.SendError(ctx, err)
}
secret, err := utils.ReversibleEncrypt(params.Robot.Secret, key)
if err != nil {
return rAPI.SendError(ctx, err)
}
r.Secret = secret
if err := rAPI.robotCtl.Update(ctx, r); err != nil {
return rAPI.SendError(ctx, err)
}
}
r.Description = params.Robot.Description
r.ExpiresAt = params.Robot.ExpiresAt
r.Disabled = params.Robot.Disable
if len(params.Robot.Permissions) != 0 {
lib.JSONCopy(&r.Permissions, params.Robot.Permissions)
}
if err := rAPI.robotCtl.Update(ctx, r); err != nil {
return rAPI.SendError(ctx, err)
}
return operation.NewUpdateRobotOK()
}
func (rAPI *robotAPI) requireAccess(ctx context.Context, level string, projectIDOrName interface{}, action rbac.Action) error {
if level == robot.LEVELSYSTEM {
return rAPI.RequireSysAdmin(ctx)
} else if level == robot.LEVELPROJECT {
return rAPI.RequireProjectAccess(ctx, projectIDOrName, action, rbac.ResourceRobot)
}
return errors.ForbiddenError(nil)
}
// more validation
func (rAPI *robotAPI) validate(r *models.RobotCreate) error {
if !isValidLevel(r.Level) {
return errors.New(nil).WithMessage("bad request error level input").WithCode(errors.BadRequestCode)
}
if len(r.Permissions) == 0 {
return errors.New(nil).WithMessage("bad request empty permission").WithCode(errors.BadRequestCode)
}
if r.Level == robot.LEVELPROJECT {
// to create a project robot, the permission must be only one project scope.
if len(r.Permissions) > 1 {
return errors.New(nil).WithMessage("bad request permission").WithCode(errors.BadRequestCode)
}
}
return nil
}
func isValidLevel(l string) bool {
switch l {
case
robot.LEVELSYSTEM,
robot.LEVELPROJECT:
return true
}
return false
}