2021-02-25 09:19:55 +01:00
// Copyright 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.
2020-03-17 04:58:43 +01:00
package handler
import (
"context"
2020-08-15 18:09:06 +02:00
"fmt"
"strconv"
"strings"
"sync"
2020-03-17 04:58:43 +01:00
"github.com/go-openapi/runtime/middleware"
2020-07-30 11:29:25 +02:00
"github.com/go-openapi/strfmt"
2022-07-20 05:33:08 +02:00
2020-08-15 18:09:06 +02:00
"github.com/goharbor/harbor/src/common"
2020-03-17 04:58:43 +01:00
"github.com/goharbor/harbor/src/common/rbac"
2020-08-15 18:09:06 +02:00
"github.com/goharbor/harbor/src/common/security"
"github.com/goharbor/harbor/src/common/security/local"
2021-08-17 10:35:36 +02:00
robotSec "github.com/goharbor/harbor/src/common/security/robot"
2020-08-31 11:02:15 +02:00
"github.com/goharbor/harbor/src/controller/p2p/preheat"
2020-03-24 13:45:45 +01:00
"github.com/goharbor/harbor/src/controller/project"
2020-08-15 18:09:06 +02:00
"github.com/goharbor/harbor/src/controller/quota"
2021-03-31 09:49:23 +02:00
"github.com/goharbor/harbor/src/controller/registry"
2020-08-15 18:09:06 +02:00
"github.com/goharbor/harbor/src/controller/repository"
2021-02-25 09:19:55 +01:00
"github.com/goharbor/harbor/src/controller/retention"
2021-03-03 05:23:36 +01:00
"github.com/goharbor/harbor/src/controller/scanner"
2021-08-24 03:34:02 +02:00
"github.com/goharbor/harbor/src/controller/user"
2020-08-15 18:09:06 +02:00
"github.com/goharbor/harbor/src/lib"
2021-05-14 05:27:23 +02:00
"github.com/goharbor/harbor/src/lib/config"
2020-08-15 18:09:06 +02:00
"github.com/goharbor/harbor/src/lib/errors"
"github.com/goharbor/harbor/src/lib/log"
"github.com/goharbor/harbor/src/lib/orm"
"github.com/goharbor/harbor/src/lib/q"
2022-05-23 03:08:27 +02:00
"github.com/goharbor/harbor/src/pkg"
2020-03-17 04:58:43 +01:00
"github.com/goharbor/harbor/src/pkg/audit"
2021-05-14 05:27:23 +02:00
"github.com/goharbor/harbor/src/pkg/member"
2020-08-15 18:09:06 +02:00
"github.com/goharbor/harbor/src/pkg/project/metadata"
2021-08-17 10:35:36 +02:00
pkgModels "github.com/goharbor/harbor/src/pkg/project/models"
2020-08-15 18:09:06 +02:00
"github.com/goharbor/harbor/src/pkg/quota/types"
"github.com/goharbor/harbor/src/pkg/retention/policy"
2021-01-04 03:24:31 +01:00
"github.com/goharbor/harbor/src/pkg/robot"
2021-08-24 03:34:02 +02:00
userModels "github.com/goharbor/harbor/src/pkg/user/models"
2020-08-15 18:09:06 +02:00
"github.com/goharbor/harbor/src/server/v2.0/handler/model"
2020-03-17 04:58:43 +01:00
"github.com/goharbor/harbor/src/server/v2.0/models"
operation "github.com/goharbor/harbor/src/server/v2.0/restapi/operations/project"
)
2020-08-15 18:09:06 +02:00
// for the proxy cache type project, we will create a 7 days retention policy for it by default
const defaultDaysToRetentionForProxyCacheProject = 7
2020-03-17 04:58:43 +01:00
func newProjectAPI ( ) * projectAPI {
return & projectAPI {
2020-08-15 18:09:06 +02:00
auditMgr : audit . Mgr ,
2022-05-23 03:08:27 +02:00
metadataMgr : pkg . ProjectMetaMgr ,
2021-08-24 03:34:02 +02:00
userCtl : user . Ctl ,
2020-08-15 18:09:06 +02:00
repositoryCtl : repository . Ctl ,
projectCtl : project . Ctl ,
2021-04-02 08:22:18 +02:00
memberMgr : member . Mgr ,
2020-08-15 18:09:06 +02:00
quotaCtl : quota . Ctl ,
2021-01-04 03:24:31 +01:00
robotMgr : robot . Mgr ,
2020-08-31 11:02:15 +02:00
preheatCtl : preheat . Ctl ,
2021-01-08 01:09:34 +01:00
retentionCtl : retention . Ctl ,
2021-03-03 05:23:36 +01:00
scannerCtl : scanner . DefaultController ,
2020-03-17 04:58:43 +01:00
}
}
type projectAPI struct {
BaseAPI
2020-08-15 18:09:06 +02:00
auditMgr audit . Manager
metadataMgr metadata . Manager
2021-08-24 03:34:02 +02:00
userCtl user . Controller
2020-08-15 18:09:06 +02:00
repositoryCtl repository . Controller
projectCtl project . Controller
2021-04-02 08:22:18 +02:00
memberMgr member . Manager
2020-08-15 18:09:06 +02:00
quotaCtl quota . Controller
2021-01-04 03:24:31 +01:00
robotMgr robot . Manager
2020-08-31 11:02:15 +02:00
preheatCtl preheat . Controller
2021-01-08 01:09:34 +01:00
retentionCtl retention . Controller
2021-03-03 05:23:36 +01:00
scannerCtl scanner . Controller
2020-08-15 18:09:06 +02:00
}
func ( a * projectAPI ) CreateProject ( ctx context . Context , params operation . CreateProjectParams ) middleware . Responder {
if err := a . RequireAuthenticated ( ctx ) ; err != nil {
return a . SendError ( ctx , err )
}
2021-03-11 13:25:51 +01:00
onlyAdmin , err := config . OnlyAdminCreateProject ( ctx )
2020-08-15 18:09:06 +02:00
if err != nil {
return a . SendError ( ctx , fmt . Errorf ( "failed to determine whether only admin can create projects: %v" , err ) )
}
secCtx , _ := security . FromContext ( ctx )
2021-08-24 03:34:02 +02:00
if r , ok := secCtx . ( * robotSec . SecurityContext ) ; ok && ! r . User ( ) . IsSysLevel ( ) {
log . Errorf ( "Only system level robot can create project" )
return a . SendError ( ctx , errors . ForbiddenError ( nil ) . WithMessage ( "Only system level robot can create project" ) )
}
2021-01-07 08:45:04 +01:00
if onlyAdmin && ! ( a . isSysAdmin ( ctx , rbac . ActionCreate ) || secCtx . IsSolutionUser ( ) ) {
2020-08-15 18:09:06 +02:00
log . Errorf ( "Only sys admin can create project" )
return a . SendError ( ctx , errors . ForbiddenError ( nil ) . WithMessage ( "Only system admin can create project" ) )
}
req := params . Project
2021-01-07 08:45:04 +01:00
if req . RegistryID != nil && ! a . isSysAdmin ( ctx , rbac . ActionCreate ) {
2020-08-15 18:09:06 +02:00
return a . SendError ( ctx , errors . ForbiddenError ( nil ) . WithMessage ( "Only system admin can create proxy cache project" ) )
}
// populate storage limit
2021-03-11 13:25:51 +01:00
if config . QuotaPerProjectEnable ( ctx ) {
2020-08-15 18:09:06 +02:00
// the security context is not sys admin, set the StorageLimit the global StoragePerProject
2021-01-07 08:45:04 +01:00
if req . StorageLimit == nil || * req . StorageLimit == 0 || ! a . isSysAdmin ( ctx , rbac . ActionCreate ) {
2021-03-11 13:25:51 +01:00
setting , err := config . QuotaSetting ( ctx )
2020-08-15 18:09:06 +02:00
if err != nil {
log . Errorf ( "failed to get quota setting: %v" , err )
return a . SendError ( ctx , fmt . Errorf ( "failed to get quota setting: %v" , err ) )
}
defaultStorageLimit := setting . StoragePerProject
req . StorageLimit = & defaultStorageLimit
}
} else {
// ignore storage limit when quota per project disabled
req . StorageLimit = nil
}
if req . Metadata == nil {
req . Metadata = & models . ProjectMetadata { }
}
// accept the "public" property to make replication work well with old versions(<=1.2.0)
if req . Public != nil && req . Metadata . Public == "" {
req . Metadata . Public = strconv . FormatBool ( * req . Public )
}
// populate public metadata as false if it isn't set
if req . Metadata . Public == "" {
req . Metadata . Public = strconv . FormatBool ( false )
}
2022-03-28 09:06:27 +02:00
// validate metadata.public value, should only be "true" or "false"
if p := req . Metadata . Public ; p != "" {
if p != "true" && p != "false" {
2022-03-29 08:57:17 +02:00
return a . SendError ( ctx , errors . BadRequestError ( nil ) . WithMessage ( fmt . Sprintf ( "metadata.public should only be 'true' or 'false', but got: '%s'" , p ) ) )
2022-03-28 09:06:27 +02:00
}
}
2020-09-02 17:23:27 +02:00
// ignore enable_content_trust metadata for proxy cache project
// see https://github.com/goharbor/harbor/issues/12940 to get more info
if req . RegistryID != nil {
req . Metadata . EnableContentTrust = nil
}
2023-02-06 07:42:15 +01:00
// validate the RetentionID, RegistryID and StorageLimit in the body of the request
2020-08-15 18:09:06 +02:00
if err := a . validateProjectReq ( ctx , req ) ; err != nil {
return a . SendError ( ctx , err )
}
var ownerID int
2021-08-24 03:34:02 +02:00
// TODO: revise the ownerID in project model.
2020-08-15 18:09:06 +02:00
// set the owner as the system admin when the API being called by replication
// it's a solution to workaround the restriction of project creation API:
// only normal users can create projects
2021-08-24 03:34:02 +02:00
// Remove the assumption of user id 1 is the system admin. And use the minimum system admin id as the owner ID,
// in most case, it's 1
if _ , ok := secCtx . ( * robotSec . SecurityContext ) ; ok || secCtx . IsSolutionUser ( ) {
q := & q . Query {
Keywords : map [ string ] interface { } {
"sysadmin_flag" : true ,
} ,
Sorts : [ ] * q . Sort {
q . NewSort ( "user_id" , false ) ,
} ,
}
admins , err := a . userCtl . List ( ctx , q , userModels . WithDefaultAdmin ( ) )
if err != nil {
return a . SendError ( ctx , err )
}
if len ( admins ) == 0 {
return a . SendError ( ctx , errors . New ( nil ) . WithMessage ( "cannot create project as no system admin found" ) )
}
ownerID = admins [ 0 ] . UserID
2020-08-15 18:09:06 +02:00
} else {
ownerName := secCtx . GetUsername ( )
2021-08-24 03:34:02 +02:00
user , err := a . userCtl . GetByName ( ctx , ownerName )
2020-08-15 18:09:06 +02:00
if err != nil {
return a . SendError ( ctx , err )
}
ownerID = user . UserID
}
p := & project . Project {
Name : req . ProjectName ,
OwnerID : ownerID ,
RegistryID : lib . Int64Value ( req . RegistryID ) ,
}
2022-06-07 11:00:36 +02:00
if err := lib . JSONCopy ( & p . Metadata , req . Metadata ) ; err != nil {
log . Warningf ( "failed to call JSONCopy on project metadata when CreateProject, error: %v" , err )
}
2023-02-06 07:42:15 +01:00
delete ( p . Metadata , "retention_id" )
2020-08-15 18:09:06 +02:00
projectID , err := a . projectCtl . Create ( ctx , p )
if err != nil {
return a . SendError ( ctx , err )
}
// StorageLimit is provided in the request body and it's valid,
// create the quota for the project
if req . StorageLimit != nil {
referenceID := quota . ReferenceID ( projectID )
hardLimits := types . ResourceList { types . ResourceStorage : * req . StorageLimit }
if _ , err := a . quotaCtl . Create ( ctx , quota . ProjectReference , referenceID , hardLimits ) ; err != nil {
return a . SendError ( ctx , fmt . Errorf ( "failed to create quota for project: %v" , err ) )
}
}
// RegistryID is provided in the request body and it's valid,
// create a default retention policy for proxy project
if req . RegistryID != nil {
plc := policy . WithNDaysSinceLastPull ( projectID , defaultDaysToRetentionForProxyCacheProject )
2021-01-08 01:09:34 +01:00
retentionID , err := a . retentionCtl . CreateRetention ( ctx , plc )
2020-08-15 18:09:06 +02:00
if err != nil {
return a . SendError ( ctx , err )
}
md := map [ string ] string { "retention_id" : strconv . FormatInt ( retentionID , 10 ) }
if err := a . metadataMgr . Add ( ctx , projectID , md ) ; err != nil {
return a . SendError ( ctx , err )
}
}
2020-12-14 08:48:52 +01:00
var location string
if lib . BoolValue ( params . XResourceNameInLocation ) {
location = fmt . Sprintf ( "%s/%s" , strings . TrimSuffix ( params . HTTPRequest . URL . Path , "/" ) , req . ProjectName )
} else {
location = fmt . Sprintf ( "%s/%d" , strings . TrimSuffix ( params . HTTPRequest . URL . Path , "/" ) , projectID )
}
2020-08-15 18:09:06 +02:00
return operation . NewCreateProjectCreated ( ) . WithLocation ( location )
}
func ( a * projectAPI ) DeleteProject ( ctx context . Context , params operation . DeleteProjectParams ) middleware . Responder {
2020-12-14 08:48:52 +01:00
projectNameOrID := parseProjectNameOrID ( params . ProjectNameOrID , params . XIsResourceName )
if err := a . RequireProjectAccess ( ctx , projectNameOrID , rbac . ActionDelete ) ; err != nil {
2020-08-15 18:09:06 +02:00
return a . SendError ( ctx , err )
}
2020-12-14 08:48:52 +01:00
p , result , err := a . deletable ( ctx , projectNameOrID )
2020-08-15 18:09:06 +02:00
if err != nil {
return a . SendError ( ctx , err )
}
if ! result . Deletable {
return a . SendError ( ctx , errors . PreconditionFailedError ( errors . New ( result . Message ) ) )
}
2020-12-14 08:48:52 +01:00
if err := a . projectCtl . Delete ( ctx , p . ProjectID ) ; err != nil {
2020-08-15 18:09:06 +02:00
return a . SendError ( ctx , err )
}
2020-08-18 04:38:45 +02:00
// remove the robot associated with the project
2020-12-14 08:48:52 +01:00
if err := a . robotMgr . DeleteByProjectID ( ctx , p . ProjectID ) ; err != nil {
2020-08-18 04:38:45 +02:00
return a . SendError ( ctx , err )
}
2020-12-14 08:48:52 +01:00
referenceID := quota . ReferenceID ( p . ProjectID )
2020-08-15 18:09:06 +02:00
q , err := a . quotaCtl . GetByRef ( ctx , quota . ProjectReference , referenceID )
if err != nil {
2020-12-14 08:48:52 +01:00
log . Warningf ( "failed to get quota for project %s, error: %v" , projectNameOrID , err )
2020-08-15 18:09:06 +02:00
} else {
if err := a . quotaCtl . Delete ( ctx , q . ID ) ; err != nil {
return a . SendError ( ctx , fmt . Errorf ( "failed to delete quota for project: %v" , err ) )
}
}
2020-08-31 11:02:15 +02:00
// preheat policies under the project should be deleted after deleting the project
2020-12-14 08:48:52 +01:00
if err = a . preheatCtl . DeletePoliciesOfProject ( ctx , p . ProjectID ) ; err != nil {
2020-08-31 11:02:15 +02:00
return a . SendError ( ctx , err )
}
2020-08-15 18:09:06 +02:00
return operation . NewDeleteProjectOK ( )
2020-03-17 04:58:43 +01:00
}
func ( a * projectAPI ) GetLogs ( ctx context . Context , params operation . GetLogsParams ) middleware . Responder {
2020-03-19 07:56:37 +01:00
if err := a . RequireProjectAccess ( ctx , params . ProjectName , rbac . ActionList , rbac . ResourceLog ) ; err != nil {
2020-03-17 04:58:43 +01:00
return a . SendError ( ctx , err )
}
2020-08-15 18:09:06 +02:00
pro , err := a . projectCtl . GetByName ( ctx , params . ProjectName )
2020-03-17 04:58:43 +01:00
if err != nil {
return a . SendError ( ctx , err )
}
2021-03-03 09:31:02 +01:00
query , err := a . BuildQuery ( ctx , params . Q , params . Sort , params . Page , params . PageSize )
2020-03-17 04:58:43 +01:00
if err != nil {
return a . SendError ( ctx , err )
}
query . Keywords [ "ProjectID" ] = pro . ProjectID
total , err := a . auditMgr . Count ( ctx , query )
if err != nil {
return a . SendError ( ctx , err )
}
logs , err := a . auditMgr . List ( ctx , query )
if err != nil {
return a . SendError ( ctx , err )
}
var auditLogs [ ] * models . AuditLog
for _ , log := range logs {
auditLogs = append ( auditLogs , & models . AuditLog {
ID : log . ID ,
Resource : log . Resource ,
ResourceType : log . ResourceType ,
Username : log . Username ,
Operation : log . Operation ,
2020-07-30 11:29:25 +02:00
OpTime : strfmt . DateTime ( log . OpTime ) ,
2020-03-17 04:58:43 +01:00
} )
}
return operation . NewGetLogsOK ( ) .
WithXTotalCount ( total ) .
WithLink ( a . Links ( ctx , params . HTTPRequest . URL , total , query . PageNumber , query . PageSize ) . String ( ) ) .
WithPayload ( auditLogs )
}
2020-08-15 18:09:06 +02:00
func ( a * projectAPI ) GetProject ( ctx context . Context , params operation . GetProjectParams ) middleware . Responder {
2020-12-14 08:48:52 +01:00
projectNameOrID := parseProjectNameOrID ( params . ProjectNameOrID , params . XIsResourceName )
if err := a . RequireProjectAccess ( ctx , projectNameOrID , rbac . ActionRead ) ; err != nil {
2020-08-15 18:09:06 +02:00
return a . SendError ( ctx , err )
}
2020-12-14 08:48:52 +01:00
p , err := a . getProject ( ctx , projectNameOrID , project . WithCVEAllowlist ( ) , project . WithOwner ( ) )
2020-08-15 18:09:06 +02:00
if err != nil {
return a . SendError ( ctx , err )
}
return operation . NewGetProjectOK ( ) . WithPayload ( model . NewProject ( p ) . ToSwagger ( ) )
}
func ( a * projectAPI ) GetProjectDeletable ( ctx context . Context , params operation . GetProjectDeletableParams ) middleware . Responder {
2020-12-14 08:48:52 +01:00
projectNameOrID := parseProjectNameOrID ( params . ProjectNameOrID , params . XIsResourceName )
if err := a . RequireProjectAccess ( ctx , projectNameOrID , rbac . ActionDelete ) ; err != nil {
2020-08-15 18:09:06 +02:00
return a . SendError ( ctx , err )
}
2020-12-14 08:48:52 +01:00
_ , result , err := a . deletable ( ctx , projectNameOrID )
2020-08-15 18:09:06 +02:00
if err != nil {
return a . SendError ( ctx , err )
}
return operation . NewGetProjectDeletableOK ( ) . WithPayload ( result )
}
func ( a * projectAPI ) GetProjectSummary ( ctx context . Context , params operation . GetProjectSummaryParams ) middleware . Responder {
2020-12-14 08:48:52 +01:00
projectNameOrID := parseProjectNameOrID ( params . ProjectNameOrID , params . XIsResourceName )
if err := a . RequireProjectAccess ( ctx , projectNameOrID , rbac . ActionRead ) ; err != nil {
2020-08-15 18:09:06 +02:00
return a . SendError ( ctx , err )
}
2020-12-14 08:48:52 +01:00
p , err := a . getProject ( ctx , projectNameOrID )
2020-08-15 18:09:06 +02:00
if err != nil {
return a . SendError ( ctx , err )
}
summary := & models . ProjectSummary {
2023-02-16 11:11:05 +01:00
RepoCount : p . RepoCount ,
2020-08-15 18:09:06 +02:00
}
var fetchSummaries [ ] func ( context . Context , * project . Project , * models . ProjectSummary )
if hasPerm := a . HasProjectPermission ( ctx , p . ProjectID , rbac . ActionRead , rbac . ResourceQuota ) ; hasPerm {
fetchSummaries = append ( fetchSummaries , getProjectQuotaSummary )
}
if hasPerm := a . HasProjectPermission ( ctx , p . ProjectID , rbac . ActionList , rbac . ResourceMember ) ; hasPerm {
2021-04-02 08:22:18 +02:00
fetchSummaries = append ( fetchSummaries , a . getProjectMemberSummary )
2020-08-15 18:09:06 +02:00
}
2020-09-02 17:23:27 +02:00
if p . IsProxy ( ) {
2020-08-15 18:09:06 +02:00
fetchSummaries = append ( fetchSummaries , getProjectRegistrySummary )
}
var wg sync . WaitGroup
for _ , fn := range fetchSummaries {
fn := fn
wg . Add ( 1 )
go func ( ) {
defer wg . Done ( )
fn ( ctx , p , summary )
} ( )
}
wg . Wait ( )
return operation . NewGetProjectSummaryOK ( ) . WithPayload ( summary )
}
func ( a * projectAPI ) HeadProject ( ctx context . Context , params operation . HeadProjectParams ) middleware . Responder {
if err := a . RequireAuthenticated ( ctx ) ; err != nil {
return a . SendError ( ctx , err )
}
if _ , err := a . projectCtl . GetByName ( ctx , params . ProjectName ) ; err != nil {
return a . SendError ( ctx , err )
}
return operation . NewHeadProjectOK ( )
}
func ( a * projectAPI ) ListProjects ( ctx context . Context , params operation . ListProjectsParams ) middleware . Responder {
2021-03-12 08:04:08 +01:00
query , err := a . BuildQuery ( ctx , params . Q , params . Sort , params . Page , params . PageSize )
if err != nil {
return a . SendError ( ctx , err )
}
2020-08-15 18:09:06 +02:00
if name := lib . StringValue ( params . Name ) ; name != "" {
query . Keywords [ "name" ] = & q . FuzzyMatchValue { Value : name }
}
if owner := lib . StringValue ( params . Owner ) ; owner != "" {
query . Keywords [ "owner" ] = owner
}
if params . Public != nil {
query . Keywords [ "public" ] = lib . BoolValue ( params . Public )
}
secCtx , ok := security . FromContext ( ctx )
if ok && secCtx . IsAuthenticated ( ) {
2021-01-07 08:45:04 +01:00
if ! a . isSysAdmin ( ctx , rbac . ActionList ) && ! secCtx . IsSolutionUser ( ) {
2020-08-15 18:09:06 +02:00
// authenticated but not system admin or solution user,
// return public projects and projects that the user is member of
if l , ok := secCtx . ( * local . SecurityContext ) ; ok {
currentUser := l . User ( )
member := & project . MemberQuery {
2020-09-07 08:35:26 +02:00
UserID : currentUser . UserID ,
2020-08-15 18:09:06 +02:00
GroupIDs : currentUser . GroupIDs ,
}
// not filter by public or filter by the public with true,
// so also return public projects for the member
if public , ok := query . Keywords [ "public" ] ; ! ok || lib . ToBool ( public ) {
member . WithPublic = true
}
query . Keywords [ "member" ] = member
2021-08-17 10:35:36 +02:00
} else if r , ok := secCtx . ( * robotSec . SecurityContext ) ; ok {
// for the system level robot that covers all the project, see it as the system admin.
var coverAll bool
var names [ ] string
for _ , p := range r . User ( ) . Permissions {
2021-08-24 03:34:02 +02:00
if p . IsCoverAll ( ) {
2021-08-17 10:35:36 +02:00
coverAll = true
break
}
names = append ( names , p . Namespace )
}
if ! coverAll {
namesQuery := & pkgModels . NamesQuery {
Names : names ,
}
if public , ok := query . Keywords [ "public" ] ; ! ok || lib . ToBool ( public ) {
namesQuery . WithPublic = true
}
query . Keywords [ "names" ] = namesQuery
}
2020-08-15 18:09:06 +02:00
} else {
// can't get the user info, force to return public projects
query . Keywords [ "public" ] = true
}
}
} else {
if params . Public != nil && ! * params . Public {
// anonymous want to query private projects return empty projects directly
return operation . NewListProjectsOK ( ) . WithXTotalCount ( 0 ) . WithPayload ( [ ] * models . Project { } )
}
// force to return public projects for anonymous
query . Keywords [ "public" ] = true
}
total , err := a . projectCtl . Count ( ctx , query )
if err != nil {
return a . SendError ( ctx , err )
}
if total == 0 {
// no projects found for the query return directly
return operation . NewListProjectsOK ( ) . WithXTotalCount ( 0 ) . WithPayload ( [ ] * models . Project { } )
}
2020-11-30 09:13:07 +01:00
projects , err := a . projectCtl . List ( ctx , query , project . Detail ( lib . BoolValue ( params . WithDetail ) ) , project . WithCVEAllowlist ( ) , project . WithOwner ( ) )
2020-08-15 18:09:06 +02:00
if err != nil {
return a . SendError ( ctx , err )
}
var wg sync . WaitGroup
for _ , p := range projects {
wg . Add ( 1 )
go func ( p * project . Project ) {
defer wg . Done ( )
2022-01-27 04:09:37 +01:00
// simultaneous queries in transaction will fail, so clone a ctx with new ormer here
2020-08-15 18:09:06 +02:00
if err := a . populateProperties ( orm . Clone ( ctx ) , p ) ; err != nil {
2022-01-27 04:09:37 +01:00
log . G ( ctx ) . Errorf ( "failed to populate properties for project %s, error: %v" , p . Name , err )
2020-08-15 18:09:06 +02:00
}
} ( p )
}
wg . Wait ( )
var payload [ ] * models . Project
for _ , p := range projects {
payload = append ( payload , model . NewProject ( p ) . ToSwagger ( ) )
}
return operation . NewListProjectsOK ( ) .
WithXTotalCount ( total ) .
WithLink ( a . Links ( ctx , params . HTTPRequest . URL , total , query . PageNumber , query . PageSize ) . String ( ) ) .
WithPayload ( payload )
}
func ( a * projectAPI ) UpdateProject ( ctx context . Context , params operation . UpdateProjectParams ) middleware . Responder {
2020-12-14 08:48:52 +01:00
projectNameOrID := parseProjectNameOrID ( params . ProjectNameOrID , params . XIsResourceName )
if err := a . RequireProjectAccess ( ctx , projectNameOrID , rbac . ActionUpdate ) ; err != nil {
2020-08-15 18:09:06 +02:00
return a . SendError ( ctx , err )
}
2020-12-14 08:48:52 +01:00
p , err := a . projectCtl . Get ( ctx , projectNameOrID , project . Metadata ( false ) )
2020-08-15 18:09:06 +02:00
if err != nil {
return a . SendError ( ctx , err )
}
if params . Project . CVEAllowlist != nil {
if params . Project . CVEAllowlist . ProjectID == 0 {
// project_id in cve_allowlist not provided or provided as 0, let it to be the id of the project which will be updating
2020-12-14 08:48:52 +01:00
params . Project . CVEAllowlist . ProjectID = p . ProjectID
} else if params . Project . CVEAllowlist . ProjectID != p . ProjectID {
2020-08-15 18:09:06 +02:00
return a . SendError ( ctx , errors . BadRequestError ( nil ) .
2020-12-14 08:48:52 +01:00
WithMessage ( "project_id in cve_allowlist must be %d but it's %d" , p . ProjectID , params . Project . CVEAllowlist . ProjectID ) )
2020-08-15 18:09:06 +02:00
}
if err := lib . JSONCopy ( & p . CVEAllowlist , params . Project . CVEAllowlist ) ; err != nil {
return a . SendError ( ctx , errors . UnknownError ( nil ) . WithMessage ( "failed to process cve_allowlist, error: %v" , err ) )
}
}
2020-09-02 17:23:27 +02:00
// ignore enable_content_trust metadata for proxy cache project
// see https://github.com/goharbor/harbor/issues/12940 to get more info
if params . Project . Metadata != nil && p . IsProxy ( ) {
params . Project . Metadata . EnableContentTrust = nil
}
2022-06-07 11:00:36 +02:00
if err := lib . JSONCopy ( & p . Metadata , params . Project . Metadata ) ; err != nil {
log . Warningf ( "failed to call JSONCopy on project metadata when UpdateProject, error: %v" , err )
}
2020-08-15 18:09:06 +02:00
2023-02-06 07:42:15 +01:00
// validate retention_id
if ridParam , ok := p . Metadata [ "retention_id" ] ; ok {
md , err := a . metadataMgr . Get ( ctx , p . ProjectID )
if err != nil {
return a . SendError ( ctx , err )
}
if rid , ok := md [ "retention_id" ] ; ! ok || rid != ridParam {
errMsg := "the retention_id in the request's payload when updating a project should be omitted, alternatively passing the one that has already been associated to this project"
return a . SendError ( ctx , errors . BadRequestError ( fmt . Errorf ( errMsg ) ) )
}
}
2020-08-15 18:09:06 +02:00
if err := a . projectCtl . Update ( ctx , p ) ; err != nil {
return a . SendError ( ctx , err )
}
return operation . NewUpdateProjectOK ( )
}
2021-03-03 05:23:36 +01:00
func ( a * projectAPI ) GetScannerOfProject ( ctx context . Context , params operation . GetScannerOfProjectParams ) middleware . Responder {
2021-05-14 05:27:23 +02:00
if err := a . RequireAuthenticated ( ctx ) ; err != nil {
return a . SendError ( ctx , err )
}
2021-03-03 05:23:36 +01:00
projectNameOrID := parseProjectNameOrID ( params . ProjectNameOrID , params . XIsResourceName )
if err := a . RequireProjectAccess ( ctx , projectNameOrID , rbac . ActionRead , rbac . ResourceScanner ) ; err != nil {
return a . SendError ( ctx , err )
}
p , err := a . projectCtl . Get ( ctx , projectNameOrID , project . Metadata ( false ) )
if err != nil {
return a . SendError ( ctx , err )
}
2024-04-22 11:43:04 +02:00
s , err := a . scannerCtl . GetRegistrationByProject ( ctx , p . ProjectID )
2021-03-03 05:23:36 +01:00
if err != nil {
return a . SendError ( ctx , err )
}
2024-04-22 11:43:04 +02:00
if s != nil {
if err := a . scannerCtl . RetrieveCap ( ctx , s ) ; err != nil {
log . Warningf ( scanner . RetrieveCapFailMsg , err )
2024-04-16 15:34:19 +02:00
}
}
2024-04-22 11:43:04 +02:00
return operation . NewGetScannerOfProjectOK ( ) . WithPayload ( model . NewScannerRegistration ( s ) . ToSwagger ( ctx ) )
2021-03-03 05:23:36 +01:00
}
func ( a * projectAPI ) ListScannerCandidatesOfProject ( ctx context . Context , params operation . ListScannerCandidatesOfProjectParams ) middleware . Responder {
2021-05-14 05:27:23 +02:00
if err := a . RequireAuthenticated ( ctx ) ; err != nil {
return a . SendError ( ctx , err )
}
2021-03-03 05:23:36 +01:00
projectNameOrID := parseProjectNameOrID ( params . ProjectNameOrID , params . XIsResourceName )
if err := a . RequireProjectAccess ( ctx , projectNameOrID , rbac . ActionCreate , rbac . ResourceScanner ) ; err != nil {
return a . SendError ( ctx , err )
}
2021-03-03 09:31:02 +01:00
query , err := a . BuildQuery ( ctx , params . Q , params . Sort , params . Page , params . PageSize )
2021-03-03 05:23:36 +01:00
if err != nil {
return a . SendError ( ctx , err )
}
total , err := a . scannerCtl . GetTotalOfRegistrations ( ctx , query )
if err != nil {
return a . SendError ( ctx , err )
}
scanners , err := a . scannerCtl . ListRegistrations ( ctx , query )
if err != nil {
return a . SendError ( ctx , err )
}
payload := make ( [ ] * models . ScannerRegistration , len ( scanners ) )
for i , scanner := range scanners {
payload [ i ] = model . NewScannerRegistration ( scanner ) . ToSwagger ( ctx )
}
return operation . NewListScannerCandidatesOfProjectOK ( ) .
WithXTotalCount ( total ) .
WithLink ( a . Links ( ctx , params . HTTPRequest . URL , total , query . PageNumber , query . PageSize ) . String ( ) ) .
WithPayload ( payload )
}
func ( a * projectAPI ) SetScannerOfProject ( ctx context . Context , params operation . SetScannerOfProjectParams ) middleware . Responder {
2021-05-14 05:27:23 +02:00
if err := a . RequireAuthenticated ( ctx ) ; err != nil {
return a . SendError ( ctx , err )
}
2021-03-03 05:23:36 +01:00
projectNameOrID := parseProjectNameOrID ( params . ProjectNameOrID , params . XIsResourceName )
if err := a . RequireProjectAccess ( ctx , projectNameOrID , rbac . ActionCreate , rbac . ResourceScanner ) ; err != nil {
return a . SendError ( ctx , err )
}
p , err := a . projectCtl . Get ( ctx , projectNameOrID , project . Metadata ( false ) )
if err != nil {
return a . SendError ( ctx , err )
}
if err := a . scannerCtl . SetRegistrationByProject ( ctx , p . ProjectID , * params . Payload . UUID ) ; err != nil {
return a . SendError ( ctx , err )
}
return operation . NewSetScannerOfProjectOK ( )
}
2020-12-14 08:48:52 +01:00
func ( a * projectAPI ) deletable ( ctx context . Context , projectNameOrID interface { } ) ( * project . Project , * models . ProjectDeletable , error ) {
p , err := a . getProject ( ctx , projectNameOrID )
2020-08-15 18:09:06 +02:00
if err != nil {
2020-12-14 08:48:52 +01:00
return nil , nil , err
2020-08-15 18:09:06 +02:00
}
result := & models . ProjectDeletable { Deletable : true }
2020-12-14 08:48:52 +01:00
if p . RepoCount > 0 {
2020-08-15 18:09:06 +02:00
result . Deletable = false
result . Message = "the project contains repositories, can not be deleted"
}
2020-12-14 08:48:52 +01:00
return p , result , nil
2020-08-15 18:09:06 +02:00
}
2020-12-14 08:48:52 +01:00
func ( a * projectAPI ) getProject ( ctx context . Context , projectNameOrID interface { } , options ... project . Option ) ( * project . Project , error ) {
p , err := a . projectCtl . Get ( ctx , projectNameOrID , options ... )
2020-08-15 18:09:06 +02:00
if err != nil {
return nil , err
}
if err := a . populateProperties ( ctx , p ) ; err != nil {
return nil , err
}
return p , nil
}
func ( a * projectAPI ) validateProjectReq ( ctx context . Context , req * models . ProjectReq ) error {
2023-02-06 07:42:15 +01:00
if req . Metadata . RetentionID != nil && * req . Metadata . RetentionID != "" {
return errors . BadRequestError ( fmt . Errorf ( "the retention_id in the request's payload when creating a project should be omitted, alternatively passing an empty string" ) )
}
2020-08-15 18:09:06 +02:00
if req . RegistryID != nil {
if * req . RegistryID <= 0 {
return errors . BadRequestError ( fmt . Errorf ( "%d is invalid value of registry_id, it should be geater than 0" , * req . RegistryID ) )
}
2021-03-31 09:49:23 +02:00
registry , err := registry . Ctl . Get ( ctx , * req . RegistryID )
2020-08-15 18:09:06 +02:00
if err != nil {
return fmt . Errorf ( "failed to get the registry %d: %v" , * req . RegistryID , err )
}
permitted := false
for _ , t := range config . GetPermittedRegistryTypesForProxyCache ( ) {
if string ( registry . Type ) == t {
permitted = true
break
}
}
if ! permitted {
return errors . BadRequestError ( fmt . Errorf ( "unsupported registry type %s" , string ( registry . Type ) ) )
}
}
if req . StorageLimit != nil {
hardLimits := types . ResourceList { types . ResourceStorage : * req . StorageLimit }
if err := quota . Validate ( ctx , quota . ProjectReference , hardLimits ) ; err != nil {
return errors . BadRequestError ( err )
}
}
return nil
}
func ( a * projectAPI ) populateProperties ( ctx context . Context , p * project . Project ) error {
if secCtx , ok := security . FromContext ( ctx ) ; ok {
if sc , ok := secCtx . ( * local . SecurityContext ) ; ok {
2021-03-12 08:04:08 +01:00
roles , err := a . projectCtl . ListRoles ( ctx , p . ProjectID , sc . User ( ) )
2020-08-15 18:09:06 +02:00
if err != nil {
return err
}
p . RoleList = roles
p . Role = highestRole ( roles )
}
}
total , err := a . repositoryCtl . Count ( ctx , q . New ( q . KeyWords { "project_id" : p . ProjectID } ) )
if err != nil {
return err
}
p . RepoCount = total
return nil
}
2021-01-07 08:45:04 +01:00
func ( a * projectAPI ) isSysAdmin ( ctx context . Context , action rbac . Action ) bool {
if err := a . RequireSystemAccess ( ctx , action , rbac . ResourceProject ) ; err != nil {
return false
}
return true
}
2020-08-15 18:09:06 +02:00
func getProjectQuotaSummary ( ctx context . Context , p * project . Project , summary * models . ProjectSummary ) {
2021-03-11 13:25:51 +01:00
if ! config . QuotaPerProjectEnable ( ctx ) {
2022-03-23 03:56:00 +01:00
log . Debug ( "Quota per project deactivated" )
2020-08-15 18:09:06 +02:00
return
}
q , err := quota . Ctl . GetByRef ( ctx , quota . ProjectReference , quota . ReferenceID ( p . ProjectID ) )
if err != nil {
log . Warningf ( "failed to get quota for project: %d" , p . ProjectID )
return
}
summary . Quota = & models . ProjectSummaryQuota { }
if hard , err := q . GetHard ( ) ; err == nil {
2021-11-04 16:39:36 +01:00
summary . Quota . Hard = model . NewResourceList ( hard ) . ToSwagger ( )
2020-08-15 18:09:06 +02:00
}
if used , err := q . GetUsed ( ) ; err == nil {
2021-11-04 16:39:36 +01:00
summary . Quota . Used = model . NewResourceList ( used ) . ToSwagger ( )
2020-08-15 18:09:06 +02:00
}
}
2021-04-02 08:22:18 +02:00
func ( a * projectAPI ) getProjectMemberSummary ( ctx context . Context , p * project . Project , summary * models . ProjectSummary ) {
2020-08-15 18:09:06 +02:00
var wg sync . WaitGroup
for _ , e := range [ ] struct {
role int
count * int64
} {
{ common . RoleProjectAdmin , & summary . ProjectAdminCount } ,
{ common . RoleMaintainer , & summary . MaintainerCount } ,
{ common . RoleDeveloper , & summary . DeveloperCount } ,
{ common . RoleGuest , & summary . GuestCount } ,
{ common . RoleLimitedGuest , & summary . LimitedGuestCount } ,
} {
wg . Add ( 1 )
go func ( role int , count * int64 ) {
defer wg . Done ( )
2021-04-02 08:22:18 +02:00
total , err := a . memberMgr . GetTotalOfProjectMembers ( orm . Clone ( ctx ) , p . ProjectID , nil , role )
2020-08-15 18:09:06 +02:00
if err != nil {
log . Warningf ( "failed to get total of project members of role %d" , role )
return
}
2021-04-02 08:22:18 +02:00
* count = int64 ( total )
2020-08-15 18:09:06 +02:00
} ( e . role , e . count )
}
wg . Wait ( )
}
func getProjectRegistrySummary ( ctx context . Context , p * project . Project , summary * models . ProjectSummary ) {
if p . RegistryID <= 0 {
return
}
2021-03-31 09:49:23 +02:00
registry , err := registry . Ctl . Get ( ctx , p . RegistryID )
2020-08-15 18:09:06 +02:00
if err != nil {
log . Warningf ( "failed to get registry %d: %v" , p . RegistryID , err )
} else if registry != nil {
registry . Credential = nil
2022-06-07 11:00:36 +02:00
if err := lib . JSONCopy ( & summary . Registry , registry ) ; err != nil {
log . Warningf ( "failed to call JSONCopy on project registry summary, error: %v" , err )
}
2020-08-15 18:09:06 +02:00
}
}
// Returns the highest role in the role list.
// This func should be removed once we deprecate the "current_user_role_id" in project API
// A user can have multiple roles and they may not have a strict ranking relationship
func highestRole ( roles [ ] int ) int {
if roles == nil {
return 0
}
rolePower := map [ int ] int {
common . RoleProjectAdmin : 50 ,
common . RoleMaintainer : 40 ,
common . RoleDeveloper : 30 ,
common . RoleGuest : 20 ,
common . RoleLimitedGuest : 10 ,
}
var highest , highestPower int
for _ , role := range roles {
if p , ok := rolePower [ role ] ; ok && p > highestPower {
highest = role
highestPower = p
}
}
return highest
}