2023-04-25 11:18:42 +08:00
// Copyright Project Harbor Authors
2022-06-22 18:22:33 +08:00
//
2023-04-25 11:18:42 +08: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 18:22:33 +08:00
//
2023-04-25 11:18:42 +08:00
// http://www.apache.org/licenses/LICENSE-2.0
2022-06-22 18:22:33 +08:00
//
2023-04-25 11:18:42 +08: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 18:22:33 +08:00
2020-11-19 17:57:57 +08:00
package handler
import (
"context"
"fmt"
2022-07-20 11:33:08 +08:00
"math"
"regexp"
"strconv"
"strings"
2020-11-19 17:57:57 +08:00
"github.com/go-openapi/runtime/middleware"
"github.com/go-openapi/strfmt"
2022-07-20 11:33:08 +08:00
2020-11-19 17:57:57 +08:00
"github.com/goharbor/harbor/src/common/rbac"
2024-09-14 12:49:32 +08:00
"github.com/goharbor/harbor/src/common/security/local"
robotSc "github.com/goharbor/harbor/src/common/security/robot"
2020-11-19 17:57:57 +08:00
"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 17:00:36 +08:00
"github.com/goharbor/harbor/src/lib/log"
2023-11-22 12:51:03 +08:00
"github.com/goharbor/harbor/src/pkg/permission/types"
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"
)
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 )
}
2024-09-26 19:09:50 +08:00
sc , err := rAPI . GetSecurityContext ( ctx )
if err != nil {
2020-11-19 17:57:57 +08:00
return rAPI . SendError ( ctx , err )
}
2024-09-26 19:09:50 +08:00
r := & robot . Robot {
Robot : pkg . Robot {
Name : params . Robot . Name ,
Description : params . Robot . Description ,
Duration : params . Robot . Duration ,
Visible : true ,
} ,
Level : params . Robot . Level ,
ProjectNameOrID : params . Robot . Permissions [ 0 ] . Namespace ,
}
if err := rAPI . requireAccess ( ctx , r , rbac . ActionCreate ) ; err != nil {
2024-08-15 14:43:19 +08:00
return rAPI . SendError ( ctx , err )
}
2024-09-14 12:49:32 +08:00
var creatorRef int64
switch s := sc . ( type ) {
case * local . SecurityContext :
creatorRef = int64 ( s . User ( ) . UserID )
case * robotSc . SecurityContext :
2025-09-30 11:57:09 +08:00
if s . User ( ) == nil {
return rAPI . SendError ( ctx , errors . New ( nil ) . WithMessage ( "invalid security context: empty robot account" ) )
}
if ! isValidPermissionScope ( params . Robot . Permissions , s . User ( ) . Permissions ) {
return rAPI . SendError ( ctx , errors . New ( nil ) . WithMessagef ( "permission scope is invalid. It must be equal to or more restrictive than the creator robot's permissions: %s" , s . User ( ) . Name ) . WithCode ( errors . DENIED ) )
}
2024-09-14 12:49:32 +08:00
creatorRef = s . User ( ) . ID
default :
return rAPI . SendError ( ctx , errors . New ( nil ) . WithMessage ( "invalid security context" ) )
}
2024-09-26 19:09:50 +08:00
r . CreatorType = sc . Name ( )
r . CreatorRef = creatorRef
2020-12-01 18:31:34 +08:00
2022-06-07 17:00:36 +08: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 17:57:57 +08:00
2024-09-26 19:09:50 +08:00
if err := robot . SetProject ( ctx , r ) ; err != nil {
return rAPI . SendError ( ctx , err )
}
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 )
}
2024-09-26 19:09:50 +08:00
if err := rAPI . requireAccess ( ctx , r , rbac . ActionDelete ) ; err != nil {
2020-11-19 17:57:57 +08:00
return rAPI . SendError ( ctx , err )
}
if err := rAPI . robotCtl . Delete ( ctx , params . RobotID ) ; err != nil {
2022-06-22 18:22:33 +08:00
// for the version 1 robot account, has to ignore the no permission error.
2020-12-01 18:31:34 +08:00
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 )
}
2021-03-03 16:31:02 +08:00
query , err := rAPI . BuildQuery ( ctx , params . Q , params . Sort , params . Page , params . PageSize )
2020-11-19 17:57:57 +08:00
if err != nil {
return rAPI . SendError ( ctx , err )
}
var projectID int64
var level string
2025-03-27 16:43:13 +08:00
// GET /api/v2.0/robots or GET /api/v2.0/robots?q=Level=system to get all of system level robots.
// GET /api/v2.0/robots?q=Level=project,ProjectID=1
2020-11-19 17:57:57 +08:00
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 )
2025-03-27 16:43:13 +08:00
if err != nil || pid <= 0 {
return rAPI . SendError ( ctx , errors . BadRequestError ( nil ) . WithMessage ( "ProjectID must be a positive integer" ) )
2020-11-19 17:57:57 +08:00
}
projectID = pid
2025-03-27 16:43:13 +08:00
} else if level == robot . LEVELSYSTEM {
query . Keywords [ "ProjectID" ] = 0
2020-11-19 17:57:57 +08:00
}
} 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
2024-09-26 19:09:50 +08:00
r := & robot . Robot {
ProjectNameOrID : projectID ,
Level : level ,
}
if err := rAPI . requireAccess ( ctx , r , rbac . ActionList ) ; err != nil {
2020-11-19 17:57:57 +08:00
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 )
}
2024-09-26 19:09:50 +08:00
if err := rAPI . requireAccess ( ctx , r , rbac . ActionRead ) ; err != nil {
2020-11-19 17:57:57 +08:00
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 {
2021-10-01 00:41:54 +08:00
err = errors . DeniedError ( nil ) . WithMessage ( "editing of legacy robot is not allowed" )
2020-12-18 20:01:26 +08:00
} 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 )
}
2024-09-26 19:09:50 +08:00
if err := rAPI . requireAccess ( ctx , r , 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 != "" {
2022-07-08 02:05:32 +08:00
if ! robot . IsValidSec ( params . RobotSec . Secret ) {
2023-07-12 17:04:50 +08: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 18:13:06 +08:00
}
secret = utils . Encrypt ( params . RobotSec . Secret , r . Salt , utils . SHA256 )
robotSec . Secret = ""
} else {
2022-07-08 02:05:32 +08:00
sec , pwd , _ , err := robot . CreateSec ( r . Salt )
if err != nil {
return rAPI . SendError ( ctx , err )
}
secret = sec
2020-12-03 18:13:06 +08:00
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
}
2024-09-26 19:09:50 +08:00
func ( rAPI * robotAPI ) requireAccess ( ctx context . Context , r * robot . Robot , action rbac . Action ) error {
if r . Level == robot . LEVELSYSTEM {
2021-01-07 15:45:04 +08:00
return rAPI . RequireSystemAccess ( ctx , action , rbac . ResourceRobot )
2024-09-26 19:09:50 +08:00
} else if r . Level == robot . LEVELPROJECT {
2025-05-08 19:02:49 +08:00
var ns any
2024-09-26 19:09:50 +08:00
if r . ProjectNameOrID != nil {
ns = r . ProjectNameOrID
} else if r . ProjectID > 0 {
ns = r . ProjectID
} else if r . ProjectName != "" {
ns = r . ProjectName
}
return rAPI . RequireProjectAccess ( ctx , ns , action , rbac . ResourceRobot )
2020-11-19 17:57:57 +08:00
}
2024-09-26 19:09:50 +08:00
2020-11-19 17:57:57 +08:00
return errors . ForbiddenError ( nil )
}
// more validation
2021-02-04 15:32:44 +08:00
func ( rAPI * robotAPI ) validate ( d int64 , level string , permissions [ ] * models . RobotPermission ) error {
2020-12-01 18:31:34 +08:00
if ! isValidDuration ( d ) {
2024-10-10 13:36:18 +08:00
return errors . New ( nil ) . WithMessagef ( "bad request error duration input: %d, duration must be either -1(Never) or a positive integer" , d ) . WithCode ( errors . BadRequestCode )
2020-12-01 18:31:34 +08:00
}
if ! isValidLevel ( level ) {
2024-10-10 13:36:18 +08:00
return errors . New ( nil ) . WithMessagef ( "bad request error level input: %s" , level ) . WithCode ( errors . BadRequestCode )
2020-11-19 17:57:57 +08:00
}
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
}
2023-11-22 12:51:03 +08:00
2024-09-26 19:09:50 +08:00
provider := rbac . GetPermissionProvider ( )
2023-11-22 12:51:03 +08:00
// to validate the access scope
for _ , perm := range permissions {
if perm . Kind == robot . LEVELSYSTEM {
2024-09-26 19:09:50 +08:00
polices := provider . GetPermissions ( rbac . ScopeSystem )
2023-11-22 12:51:03 +08:00
for _ , acc := range perm . Access {
if ! containsAccess ( polices , acc ) {
2024-10-10 13:36:18 +08:00
return errors . New ( nil ) . WithMessagef ( "bad request permission: %s:%s" , acc . Resource , acc . Action ) . WithCode ( errors . BadRequestCode )
2023-11-22 12:51:03 +08:00
}
}
} else if perm . Kind == robot . LEVELPROJECT {
2024-09-26 19:09:50 +08:00
polices := provider . GetPermissions ( rbac . ScopeProject )
2023-11-22 12:51:03 +08:00
for _ , acc := range perm . Access {
if ! containsAccess ( polices , acc ) {
2024-10-10 13:36:18 +08:00
return errors . New ( nil ) . WithMessagef ( "bad request permission: %s:%s" , acc . Resource , acc . Action ) . WithCode ( errors . BadRequestCode )
2023-11-22 12:51:03 +08:00
}
}
} else {
2024-10-10 13:36:18 +08:00
return errors . New ( nil ) . WithMessagef ( "bad request permission level: %s" , perm . Kind ) . WithCode ( errors . BadRequestCode )
2023-11-22 12:51:03 +08:00
}
}
2020-11-19 17:57:57 +08:00
return nil
}
2020-12-18 20:01:26 +08:00
func ( rAPI * robotAPI ) updateV2Robot ( ctx context . Context , params operation . UpdateRobotParams , r * robot . Robot ) error {
2024-01-15 13:25:56 +08: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 20:01:26 +08:00
return err
}
2022-07-27 20:13:46 +08: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 18:22:33 +08:00
}
2024-09-26 19:09:50 +08:00
r . ProjectNameOrID = params . Robot . Permissions [ 0 ] . Namespace
if err := rAPI . requireAccess ( ctx , r , rbac . ActionUpdate ) ; err != nil {
2020-12-18 20:01:26 +08: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 13:25:56 +08:00
if r . Duration != * params . Robot . Duration {
r . Duration = * params . Robot . Duration
if * params . Robot . Duration == - 1 {
2020-12-18 20:01:26 +08:00
r . ExpiresAt = - 1
} else {
2024-01-15 13:25:56 +08:00
r . ExpiresAt = r . CreationTime . AddDate ( 0 , 0 , int ( * params . Robot . Duration ) ) . Unix ( )
2020-12-18 20:01:26 +08:00
}
}
r . Description = params . Robot . Description
r . Disabled = params . Robot . Disable
if len ( params . Robot . Permissions ) != 0 {
2022-06-07 17:00:36 +08: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 20:01:26 +08:00
}
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 {
2024-01-17 16:41:45 +08:00
return d == - 1 || ( d > 0 && d < math . MaxInt32 )
2020-12-01 18:31:34 +08:00
}
2020-12-03 18:13:06 +08:00
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
}
2023-11-22 12:51:03 +08: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
}
2024-09-26 19:09:50 +08:00
// isValidPermissionScope checks if permission slice A is a subset of permission slice B
func isValidPermissionScope ( creating [ ] * models . RobotPermission , creator [ ] * robot . Permission ) bool {
creatorMap := make ( map [ string ] * robot . Permission )
for _ , creatorPerm := range creator {
key := fmt . Sprintf ( "%s:%s" , creatorPerm . Kind , creatorPerm . Namespace )
creatorMap [ key ] = creatorPerm
}
hasLessThanOrEqualAccess := func ( creating [ ] * models . Access , creator [ ] * types . Policy ) bool {
creatorMap := make ( map [ string ] * types . Policy )
for _ , creatorP := range creator {
key := fmt . Sprintf ( "%s:%s:%s" , creatorP . Resource , creatorP . Action , creatorP . Effect )
creatorMap [ key ] = creatorP
}
for _ , creatingP := range creating {
key := fmt . Sprintf ( "%s:%s:%s" , creatingP . Resource , creatingP . Action , creatingP . Effect )
if _ , found := creatorMap [ key ] ; ! found {
return false
}
}
return true
}
for _ , pCreating := range creating {
key := fmt . Sprintf ( "%s:%s" , pCreating . Kind , pCreating . Namespace )
2024-12-13 11:11:44 +08:00
creatorPerm , found := creatorMap [ key ]
2024-09-26 19:09:50 +08:00
if ! found {
2024-12-13 11:11:44 +08:00
allProjects := fmt . Sprintf ( "%s:*" , pCreating . Kind )
if creatorPerm , found = creatorMap [ allProjects ] ; ! found {
return false
}
2024-09-26 19:09:50 +08:00
}
2024-12-13 11:11:44 +08:00
if ! hasLessThanOrEqualAccess ( pCreating . Access , creatorPerm . Access ) {
2024-09-26 19:09:50 +08:00
return false
}
}
return true
}