2023-04-25 05:18:42 +02:00
// Copyright Project Harbor Authors
2022-06-22 12:22:33 +02:00
//
2023-04-25 05:18:42 +02:00
// 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
2022-06-22 12:22:33 +02:00
//
2023-04-25 05:18:42 +02:00
// http://www.apache.org/licenses/LICENSE-2.0
2022-06-22 12:22:33 +02:00
//
2023-04-25 05:18:42 +02:00
// 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.
2022-06-22 12:22:33 +02:00
2020-11-19 10:57:57 +01:00
package handler
import (
"context"
"fmt"
2022-07-20 05:33:08 +02:00
"math"
"regexp"
"strconv"
"strings"
2020-11-19 10:57:57 +01:00
"github.com/go-openapi/runtime/middleware"
"github.com/go-openapi/strfmt"
2022-07-20 05:33:08 +02:00
2020-11-19 10:57:57 +01:00
"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/lib"
"github.com/goharbor/harbor/src/lib/errors"
2022-06-07 11:00:36 +02:00
"github.com/goharbor/harbor/src/lib/log"
2023-11-22 05:51:03 +01:00
"github.com/goharbor/harbor/src/pkg/permission/types"
2021-01-04 03:24:31 +01:00
pkg "github.com/goharbor/harbor/src/pkg/robot/model"
2020-11-19 10:57:57 +01: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"
)
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 11:41:00 +01:00
if err := validateName ( params . Robot . Name ) ; err != nil {
return rAPI . SendError ( ctx , err )
}
2020-12-01 11:31:34 +01:00
if err := rAPI . validate ( params . Robot . Duration , params . Robot . Level , params . Robot . Permissions ) ; err != nil {
2020-11-19 10:57:57 +01: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 11:31:34 +01:00
Duration : params . Robot . Duration ,
2021-01-04 03:24:31 +01:00
Visible : true ,
2020-11-19 10:57:57 +01:00
} ,
Level : params . Robot . Level ,
}
2020-12-01 11:31:34 +01:00
2022-06-07 11:00:36 +02:00
if err := lib . JSONCopy ( & r . Permissions , params . Robot . Permissions ) ; err != nil {
log . Warningf ( "failed to call JSONCopy on robot permission when CreateRobot, error: %v" , err )
}
2020-11-19 10:57:57 +01:00
2020-12-03 11:13:06 +01:00
rid , pwd , err := rAPI . robotCtl . Create ( ctx , r )
2020-11-19 10:57:57 +01: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 11:13:06 +01:00
Secret : pwd ,
2020-11-19 10:57:57 +01:00
CreationTime : strfmt . DateTime ( created . CreationTime ) ,
2020-12-01 11:31:34 +01:00
ExpiresAt : created . ExpiresAt ,
2020-11-19 10:57:57 +01: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 {
2022-06-22 12:22:33 +02:00
// for the version 1 robot account, has to ignore the no permission error.
2020-12-01 11:31:34 +01:00
if ! r . Editable && errors . IsNotFoundErr ( err ) {
return operation . NewDeleteRobotOK ( )
}
2020-11-19 10:57:57 +01: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 )
}
2021-03-03 09:31:02 +01:00
query , err := rAPI . BuildQuery ( ctx , params . Q , params . Sort , params . Page , params . PageSize )
2020-11-19 10:57:57 +01:00
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 03:24:31 +01:00
query . Keywords [ "Visible" ] = true
2020-11-19 10:57:57 +01: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 13:01:26 +01:00
var err error
if err := rAPI . RequireAuthenticated ( ctx ) ; err != nil {
2020-11-19 10:57:57 +01: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 13:01:26 +01:00
if ! r . Editable {
2021-09-30 18:41:54 +02:00
err = errors . DeniedError ( nil ) . WithMessage ( "editing of legacy robot is not allowed" )
2020-12-18 13:01:26 +01:00
} else {
err = rAPI . updateV2Robot ( ctx , params , r )
2020-11-19 10:57:57 +01:00
}
2020-12-18 13:01:26 +01:00
if err != nil {
2020-11-19 10:57:57 +01:00
return rAPI . SendError ( ctx , err )
}
return operation . NewUpdateRobotOK ( )
}
2020-12-01 11:31:34 +01:00
func ( rAPI * robotAPI ) RefreshSec ( ctx context . Context , params operation . RefreshSecParams ) middleware . Responder {
2020-12-03 11:13:06 +01:00
if err := rAPI . RequireAuthenticated ( ctx ) ; err != nil {
2020-12-01 11:31:34 +01:00
return rAPI . SendError ( ctx , err )
}
2020-12-03 11:13:06 +01:00
r , err := rAPI . robotCtl . Get ( ctx , params . RobotID , nil )
if err != nil {
2020-12-01 11:31:34 +01:00
return rAPI . SendError ( ctx , err )
}
2020-12-03 11:13:06 +01:00
if err := rAPI . requireAccess ( ctx , r . Level , r . ProjectID , rbac . ActionUpdate ) ; err != nil {
2020-12-01 11:31:34 +01:00
return rAPI . SendError ( ctx , err )
}
2020-12-03 11:13:06 +01:00
var secret string
robotSec := & models . RobotSec { }
if params . RobotSec . Secret != "" {
2022-07-07 20:05:32 +02:00
if ! robot . IsValidSec ( params . RobotSec . Secret ) {
2023-07-12 11:04:50 +02:00
return rAPI . SendError ( ctx , errors . New ( "the secret must be 8-128, inclusively, characters long with at least 1 uppercase letter, 1 lowercase letter and 1 number" ) . WithCode ( errors . BadRequestCode ) )
2020-12-03 11:13:06 +01:00
}
secret = utils . Encrypt ( params . RobotSec . Secret , r . Salt , utils . SHA256 )
robotSec . Secret = ""
} else {
2022-07-07 20:05:32 +02:00
sec , pwd , _ , err := robot . CreateSec ( r . Salt )
if err != nil {
return rAPI . SendError ( ctx , err )
}
secret = sec
2020-12-03 11:13:06 +01:00
robotSec . Secret = pwd
2020-12-01 11:31:34 +01:00
}
r . Secret = secret
2020-12-03 11:13:06 +01:00
if err := rAPI . robotCtl . Update ( ctx , r , nil ) ; err != nil {
2020-12-01 11:31:34 +01:00
return rAPI . SendError ( ctx , err )
}
2020-12-03 11:13:06 +01:00
return operation . NewRefreshSecOK ( ) . WithPayload ( robotSec )
2020-12-01 11:31:34 +01:00
}
2020-11-19 10:57:57 +01:00
func ( rAPI * robotAPI ) requireAccess ( ctx context . Context , level string , projectIDOrName interface { } , action rbac . Action ) error {
if level == robot . LEVELSYSTEM {
2021-01-07 08:45:04 +01:00
return rAPI . RequireSystemAccess ( ctx , action , rbac . ResourceRobot )
2020-11-19 10:57:57 +01:00
} else if level == robot . LEVELPROJECT {
return rAPI . RequireProjectAccess ( ctx , projectIDOrName , action , rbac . ResourceRobot )
}
return errors . ForbiddenError ( nil )
}
// more validation
2021-02-04 08:32:44 +01:00
func ( rAPI * robotAPI ) validate ( d int64 , level string , permissions [ ] * models . RobotPermission ) error {
2020-12-01 11:31:34 +01:00
if ! isValidDuration ( d ) {
2024-01-15 06:25:56 +01:00
return errors . New ( nil ) . WithMessage ( "bad request error duration input: %d, duration must be either -1(Never) or a positive integer" , d ) . WithCode ( errors . BadRequestCode )
2020-12-01 11:31:34 +01:00
}
if ! isValidLevel ( level ) {
2021-01-13 09:17:34 +01:00
return errors . New ( nil ) . WithMessage ( "bad request error level input: %s" , level ) . WithCode ( errors . BadRequestCode )
2020-11-19 10:57:57 +01:00
}
2020-12-01 11:31:34 +01:00
if len ( permissions ) == 0 {
2020-11-19 10:57:57 +01:00
return errors . New ( nil ) . WithMessage ( "bad request empty permission" ) . WithCode ( errors . BadRequestCode )
}
2020-12-18 13:01:26 +01: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 06:13:12 +01:00
// to create a project robot, the permission must be only one project scope.
2020-12-01 11:31:34 +01:00
if level == robot . LEVELPROJECT && len ( permissions ) > 1 {
2020-11-20 06:13:12 +01:00
return errors . New ( nil ) . WithMessage ( "bad request permission" ) . WithCode ( errors . BadRequestCode )
2020-11-19 10:57:57 +01:00
}
2023-11-22 05:51:03 +01:00
// to validate the access scope
for _ , perm := range permissions {
if perm . Kind == robot . LEVELSYSTEM {
polices := rbac . PoliciesMap [ "System" ]
for _ , acc := range perm . Access {
if ! containsAccess ( polices , acc ) {
return errors . New ( nil ) . WithMessage ( "bad request permission: %s:%s" , acc . Resource , acc . Action ) . WithCode ( errors . BadRequestCode )
}
}
} else if perm . Kind == robot . LEVELPROJECT {
polices := rbac . PoliciesMap [ "Project" ]
for _ , acc := range perm . Access {
if ! containsAccess ( polices , acc ) {
return errors . New ( nil ) . WithMessage ( "bad request permission: %s:%s" , acc . Resource , acc . Action ) . WithCode ( errors . BadRequestCode )
}
}
} else {
return errors . New ( nil ) . WithMessage ( "bad request permission level: %s" , perm . Kind ) . WithCode ( errors . BadRequestCode )
}
}
2020-11-19 10:57:57 +01:00
return nil
}
2020-12-18 13:01:26 +01:00
func ( rAPI * robotAPI ) updateV2Robot ( ctx context . Context , params operation . UpdateRobotParams , r * robot . Robot ) error {
2024-01-15 06:25:56 +01:00
if params . Robot . Duration == nil {
params . Robot . Duration = & r . Duration
}
if err := rAPI . validate ( * params . Robot . Duration , params . Robot . Level , params . Robot . Permissions ) ; err != nil {
2020-12-18 13:01:26 +01:00
return err
}
2022-07-27 14:13:46 +02:00
if r . Level != robot . LEVELSYSTEM {
projectID , err := getProjectID ( ctx , params . Robot . Permissions [ 0 ] . Namespace )
if err != nil {
return err
}
if r . ProjectID != projectID {
return errors . BadRequestError ( nil ) . WithMessage ( "cannot update the project id of robot" )
}
2022-06-22 12:22:33 +02:00
}
2022-07-27 14:13:46 +02:00
if err := rAPI . requireAccess ( ctx , params . Robot . Level , params . Robot . Permissions [ 0 ] . Namespace , rbac . ActionUpdate ) ; err != nil {
2020-12-18 13:01:26 +01:00
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" )
}
2024-01-15 06:25:56 +01:00
if r . Duration != * params . Robot . Duration {
r . Duration = * params . Robot . Duration
if * params . Robot . Duration == - 1 {
2020-12-18 13:01:26 +01:00
r . ExpiresAt = - 1
} else {
2024-01-15 06:25:56 +01:00
r . ExpiresAt = r . CreationTime . AddDate ( 0 , 0 , int ( * params . Robot . Duration ) ) . Unix ( )
2020-12-18 13:01:26 +01:00
}
}
r . Description = params . Robot . Description
r . Disabled = params . Robot . Disable
if len ( params . Robot . Permissions ) != 0 {
2022-06-07 11:00:36 +02:00
if err := lib . JSONCopy ( & r . Permissions , params . Robot . Permissions ) ; err != nil {
log . Warningf ( "failed to call JSONCopy on robot permission when updateV2Robot, error: %v" , err )
}
2020-12-18 13:01:26 +01:00
}
if err := rAPI . robotCtl . Update ( ctx , r , & robot . Option {
WithPermission : true ,
} ) ; err != nil {
return err
}
return nil
}
2020-11-19 10:57:57 +01:00
func isValidLevel ( l string ) bool {
2020-11-20 06:13:12 +01:00
return l == robot . LEVELSYSTEM || l == robot . LEVELPROJECT
2020-11-19 10:57:57 +01:00
}
2020-12-01 11:31:34 +01:00
func isValidDuration ( d int64 ) bool {
2024-01-17 09:41:45 +01:00
return d == - 1 || ( d > 0 && d < math . MaxInt32 )
2020-12-01 11:31:34 +01:00
}
2020-12-03 11:13:06 +01:00
2020-12-03 11:41:00 +01: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
}
2023-11-22 05:51:03 +01:00
func containsAccess ( policies [ ] * types . Policy , item * models . Access ) bool {
for _ , po := range policies {
if po . Resource . String ( ) == item . Resource && po . Action . String ( ) == item . Action {
return true
}
}
return false
}