2020-11-19 17:57:57 +08:00
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"
2021-01-04 10:24:31 +08:00
pkg "github.com/goharbor/harbor/src/pkg/robot/model"
2020-11-19 17:57:57 +08:00
"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"
2020-12-03 18:13:06 +08:00
"regexp"
2020-11-19 17:57:57 +08:00
"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 {
2020-12-03 18:41:00 +08:00
if err := validateName ( params . Robot . Name ) ; err != nil {
return rAPI . SendError ( ctx , err )
}
2020-12-01 18:31:34 +08:00
if err := rAPI . validate ( params . Robot . Duration , params . Robot . Level , params . Robot . Permissions ) ; err != nil {
2020-11-19 17:57:57 +08:00
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 ,
2020-12-01 18:31:34 +08:00
Duration : params . Robot . Duration ,
2021-01-04 10:24:31 +08:00
Visible : true ,
2020-11-19 17:57:57 +08:00
} ,
Level : params . Robot . Level ,
}
2020-12-01 18:31:34 +08:00
2020-11-19 17:57:57 +08:00
lib . JSONCopy ( & r . Permissions , params . Robot . Permissions )
2020-12-03 18:13:06 +08:00
rid , pwd , err := rAPI . robotCtl . Create ( ctx , r )
2020-11-19 17:57:57 +08:00
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 ,
2020-12-03 18:13:06 +08:00
Secret : pwd ,
2020-11-19 17:57:57 +08:00
CreationTime : strfmt . DateTime ( created . CreationTime ) ,
2020-12-01 18:31:34 +08:00
ExpiresAt : created . ExpiresAt ,
2020-11-19 17:57:57 +08:00
} )
}
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 {
2020-12-01 18:31:34 +08:00
// for the version 1 robot account, has to ignore the no permissions error.
if ! r . Editable && errors . IsNotFoundErr ( err ) {
return operation . NewDeleteRobotOK ( )
}
2020-11-19 17:57:57 +08:00
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
query . Keywords [ "ProjectID" ] = 0
}
2021-01-04 10:24:31 +08:00
query . Keywords [ "Visible" ] = true
2020-11-19 17:57:57 +08:00
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 {
2020-12-18 20:01:26 +08:00
var err error
if err := rAPI . RequireAuthenticated ( ctx ) ; err != nil {
2020-11-19 17:57:57 +08:00
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 )
}
2020-12-18 20:01:26 +08:00
if ! r . Editable {
err = rAPI . updateV1Robot ( ctx , params , r )
} else {
err = rAPI . updateV2Robot ( ctx , params , r )
2020-11-19 17:57:57 +08:00
}
2020-12-18 20:01:26 +08:00
if err != nil {
2020-11-19 17:57:57 +08:00
return rAPI . SendError ( ctx , err )
}
return operation . NewUpdateRobotOK ( )
}
2020-12-01 18:31:34 +08:00
func ( rAPI * robotAPI ) RefreshSec ( ctx context . Context , params operation . RefreshSecParams ) middleware . Responder {
2020-12-03 18:13:06 +08:00
if err := rAPI . RequireAuthenticated ( ctx ) ; err != nil {
2020-12-01 18:31:34 +08:00
return rAPI . SendError ( ctx , err )
}
2020-12-03 18:13:06 +08:00
r , err := rAPI . robotCtl . Get ( ctx , params . RobotID , nil )
if err != nil {
2020-12-01 18:31:34 +08:00
return rAPI . SendError ( ctx , err )
}
2020-12-03 18:13:06 +08:00
if err := rAPI . requireAccess ( ctx , r . Level , r . ProjectID , rbac . ActionUpdate ) ; err != nil {
2020-12-01 18:31:34 +08:00
return rAPI . SendError ( ctx , err )
}
2020-12-03 18:13:06 +08:00
var secret string
robotSec := & models . RobotSec { }
if params . RobotSec . Secret != "" {
if ! isValidSec ( params . RobotSec . Secret ) {
return rAPI . SendError ( ctx , errors . New ( "the secret must longer than 8 chars with at least 1 uppercase letter, 1 lowercase letter and 1 number" ) . WithCode ( errors . BadRequestCode ) )
}
secret = utils . Encrypt ( params . RobotSec . Secret , r . Salt , utils . SHA256 )
robotSec . Secret = ""
} else {
pwd := utils . GenerateRandomString ( )
secret = utils . Encrypt ( pwd , r . Salt , utils . SHA256 )
robotSec . Secret = pwd
2020-12-01 18:31:34 +08:00
}
r . Secret = secret
2020-12-03 18:13:06 +08:00
if err := rAPI . robotCtl . Update ( ctx , r , nil ) ; err != nil {
2020-12-01 18:31:34 +08:00
return rAPI . SendError ( ctx , err )
}
2020-12-03 18:13:06 +08:00
return operation . NewRefreshSecOK ( ) . WithPayload ( robotSec )
2020-12-01 18:31:34 +08:00
}
2020-11-19 17:57:57 +08:00
func ( rAPI * robotAPI ) requireAccess ( ctx context . Context , level string , projectIDOrName interface { } , action rbac . Action ) error {
if level == robot . LEVELSYSTEM {
2021-01-07 15:45:04 +08:00
return rAPI . RequireSystemAccess ( ctx , action , rbac . ResourceRobot )
2020-11-19 17:57:57 +08:00
} else if level == robot . LEVELPROJECT {
return rAPI . RequireProjectAccess ( ctx , projectIDOrName , action , rbac . ResourceRobot )
}
return errors . ForbiddenError ( nil )
}
// more validation
2020-12-01 18:31:34 +08:00
func ( rAPI * robotAPI ) validate ( d int64 , level string , permissions [ ] * models . Permission ) error {
if ! isValidDuration ( d ) {
return errors . New ( nil ) . WithMessage ( "bad request error duration input" ) . WithCode ( errors . BadRequestCode )
}
if ! isValidLevel ( level ) {
2020-11-19 17:57:57 +08:00
return errors . New ( nil ) . WithMessage ( "bad request error level input" ) . WithCode ( errors . BadRequestCode )
}
2020-12-01 18:31:34 +08:00
if len ( permissions ) == 0 {
2020-11-19 17:57:57 +08:00
return errors . New ( nil ) . WithMessage ( "bad request empty permission" ) . WithCode ( errors . BadRequestCode )
}
2020-12-18 20:01:26 +08:00
for _ , perm := range permissions {
if len ( perm . Access ) == 0 {
return errors . New ( nil ) . WithMessage ( "bad request empty access" ) . WithCode ( errors . BadRequestCode )
}
}
2020-11-20 13:13:12 +08:00
// to create a project robot, the permission must be only one project scope.
2020-12-01 18:31:34 +08:00
if level == robot . LEVELPROJECT && len ( permissions ) > 1 {
2020-11-20 13:13:12 +08:00
return errors . New ( nil ) . WithMessage ( "bad request permission" ) . WithCode ( errors . BadRequestCode )
2020-11-19 17:57:57 +08:00
}
return nil
}
2020-12-18 20:01:26 +08:00
// only disable can be updated for v1 robot
func ( rAPI * robotAPI ) updateV1Robot ( ctx context . Context , params operation . UpdateRobotParams , r * robot . Robot ) error {
if err := rAPI . requireAccess ( ctx , params . Robot . Level , r . ProjectID , rbac . ActionUpdate ) ; err != nil {
return err
}
r . Disabled = params . Robot . Disable
r . Description = params . Robot . Description
if err := rAPI . robotCtl . Update ( ctx , r , & robot . Option {
WithPermission : false ,
} ) ; err != nil {
return err
}
return nil
}
func ( rAPI * robotAPI ) updateV2Robot ( ctx context . Context , params operation . UpdateRobotParams , r * robot . Robot ) error {
if err := rAPI . validate ( params . Robot . Duration , params . Robot . Level , params . Robot . Permissions ) ; err != nil {
return err
}
if err := rAPI . requireAccess ( ctx , params . Robot . Level , params . Robot . Permissions [ 0 ] . Namespace , rbac . ActionUpdate ) ; err != nil {
return err
}
if params . Robot . Level != r . Level || params . Robot . Name != r . Name {
return errors . BadRequestError ( nil ) . WithMessage ( "cannot update the level or name of robot" )
}
if r . Duration != params . Robot . Duration {
r . Duration = params . Robot . Duration
if params . Robot . Duration == - 1 {
r . ExpiresAt = - 1
} else if params . Robot . Duration == 0 {
r . Duration = int64 ( config . RobotTokenDuration ( ) )
r . ExpiresAt = r . CreationTime . AddDate ( 0 , 0 , config . RobotTokenDuration ( ) ) . Unix ( )
} else {
r . ExpiresAt = r . CreationTime . AddDate ( 0 , 0 , int ( params . Robot . Duration ) ) . Unix ( )
}
}
r . Description = params . Robot . Description
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 , & robot . Option {
WithPermission : true ,
} ) ; err != nil {
return err
}
return nil
}
2020-11-19 17:57:57 +08:00
func isValidLevel ( l string ) bool {
2020-11-20 13:13:12 +08:00
return l == robot . LEVELSYSTEM || l == robot . LEVELPROJECT
2020-11-19 17:57:57 +08:00
}
2020-12-01 18:31:34 +08:00
func isValidDuration ( d int64 ) bool {
return d >= int64 ( - 1 )
}
2020-12-03 18:13:06 +08:00
func isValidSec ( sec string ) bool {
hasLower := regexp . MustCompile ( ` [a-z] ` )
hasUpper := regexp . MustCompile ( ` [A-Z] ` )
hasNumber := regexp . MustCompile ( ` [0-9] ` )
if len ( sec ) >= 8 && hasLower . MatchString ( sec ) && hasUpper . MatchString ( sec ) && hasNumber . MatchString ( sec ) {
return true
}
return false
}
2020-12-03 18:41:00 +08:00
// validateName validates the robot name, especially '+' cannot be a valid character
func validateName ( name string ) error {
robotNameReg := ` ^[a-z0-9]+(?:[._-][a-z0-9]+)*$ `
legal := regexp . MustCompile ( robotNameReg ) . MatchString ( name )
if ! legal {
return errors . BadRequestError ( nil ) . WithMessage ( "robot name is not in lower case or contains illegal characters" )
}
return nil
}