feat(api): support project name in the path of apis (#13744)

Support project name in the path of projects and robotsV1 APIs.

Signed-off-by: He Weiwei <hweiwei@vmware.com>
This commit is contained in:
He Weiwei 2020-12-14 15:48:52 +08:00 committed by GitHub
parent 949379f7bc
commit af24a073dc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 122 additions and 78 deletions

View File

@ -98,6 +98,7 @@ paths:
operationId: createProject
parameters:
- $ref: '#/parameters/requestId'
- $ref: '#/parameters/resourceNameInLocation'
- name: project
in: body
description: New created project.
@ -115,7 +116,7 @@ paths:
$ref: '#/responses/409'
'500':
$ref: '#/responses/500'
'/projects/{project_id}':
'/projects/{project_name_or_id}':
get:
summary: Return specific project detail information
description: This endpoint returns specific project information by project ID.
@ -124,7 +125,8 @@ paths:
operationId: getProject
parameters:
- $ref: '#/parameters/requestId'
- $ref: '#/parameters/projectId'
- $ref: '#/parameters/isResourceName'
- $ref: '#/parameters/projectNameOrId'
responses:
'200':
description: Return matched project information.
@ -142,7 +144,8 @@ paths:
operationId: updateProject
parameters:
- $ref: '#/parameters/requestId'
- $ref: '#/parameters/projectId'
- $ref: '#/parameters/isResourceName'
- $ref: '#/parameters/projectNameOrId'
- name: project
in: body
required: true
@ -170,7 +173,8 @@ paths:
operationId: deleteProject
parameters:
- $ref: '#/parameters/requestId'
- $ref: '#/parameters/projectId'
- $ref: '#/parameters/isResourceName'
- $ref: '#/parameters/projectNameOrId'
responses:
'200':
$ref: '#/responses/200'
@ -184,7 +188,7 @@ paths:
$ref: '#/responses/412'
'500':
$ref: '#/responses/500'
/projects/{project_id}/_deletable:
/projects/{project_name_or_id}/_deletable:
get:
summary: Get the deletable status of the project
description: Get the deletable status of the project
@ -193,7 +197,8 @@ paths:
operationId: getProjectDeletable
parameters:
- $ref: '#/parameters/requestId'
- $ref: '#/parameters/projectId'
- $ref: '#/parameters/isResourceName'
- $ref: '#/parameters/projectNameOrId'
responses:
'200':
description: Return deletable status of the project.
@ -207,7 +212,7 @@ paths:
$ref: '#/responses/404'
'500':
$ref: '#/responses/500'
'/projects/{project_id}/summary':
'/projects/{project_name_or_id}/summary':
get:
summary: Get summary of the project.
description: Get summary of the project.
@ -216,7 +221,8 @@ paths:
operationId: getProjectSummary
parameters:
- $ref: '#/parameters/requestId'
- $ref: '#/parameters/projectId'
- $ref: '#/parameters/isResourceName'
- $ref: '#/parameters/projectNameOrId'
responses:
'200':
description: Get summary of the project successfully.
@ -1416,13 +1422,14 @@ paths:
$ref: '#/responses/404'
'500':
$ref: '#/responses/500'
/projects/{project_id_or_name}/robots:
/projects/{project_name_or_id}/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/isResourceName'
- $ref: '#/parameters/projectNameOrId'
- $ref: '#/parameters/page'
- $ref: '#/parameters/pageSize'
- $ref: '#/parameters/query'
@ -1457,7 +1464,8 @@ paths:
operationId: CreateRobotV1
parameters:
- $ref: '#/parameters/requestId'
- $ref: '#/parameters/projectIDOrName'
- $ref: '#/parameters/isResourceName'
- $ref: '#/parameters/projectNameOrId'
- name: robot
in: body
description: The JSON object of a robot account.
@ -1486,7 +1494,7 @@ paths:
$ref: '#/responses/404'
'500':
$ref: '#/responses/500'
/projects/{project_id_or_name}/robots/{robot_id}:
/projects/{project_name_or_id}/robots/{robot_id}:
get:
summary: Get a robot account
description: This endpoint returns specific robot account information by robot ID.
@ -1495,7 +1503,8 @@ paths:
operationId: GetRobotByIDV1
parameters:
- $ref: '#/parameters/requestId'
- $ref: '#/parameters/projectIDOrName'
- $ref: '#/parameters/isResourceName'
- $ref: '#/parameters/projectNameOrId'
- $ref: '#/parameters/robotId'
responses:
'200':
@ -1518,7 +1527,8 @@ paths:
operationId: UpdateRobotV1
parameters:
- $ref: '#/parameters/requestId'
- $ref: '#/parameters/projectIDOrName'
- $ref: '#/parameters/isResourceName'
- $ref: '#/parameters/projectNameOrId'
- $ref: '#/parameters/robotId'
- name: robot
in: body
@ -1549,7 +1559,8 @@ paths:
operationId: DeleteRobotV1
parameters:
- $ref: '#/parameters/requestId'
- $ref: '#/parameters/projectIDOrName'
- $ref: '#/parameters/isResourceName'
- $ref: '#/parameters/projectNameOrId'
- $ref: '#/parameters/robotId'
responses:
'200':
@ -2193,23 +2204,30 @@ parameters:
type: string
required: false
minLength: 1
resourceNameInLocation:
name: X-Resource-Name-In-Location
description: The flag to indicate whether to return the name of the resource in Location. When X-Resource-Name-In-Location is true, the Location will return the name of the resource.
in: header
type: boolean
required: false
default: false
isResourceName:
name: X-Is-Resource-Name
description: The flag to indicate whether the parameter which supports both name and id in the path is the name of the resource. When the X-Is-Resource-Name is false and the parameter can be converted to an integer, the parameter will be as an id, otherwise, it will be as a name.
in: header
type: boolean
required: false
default: false
projectName:
name: project_name
in: path
description: The name of the project
required: true
type: string
projectId:
name: project_id
projectNameOrId:
name: project_name_or_id
in: path
description: The ID of the project
required: true
type: integer
format: int64
projectIDOrName:
name: project_id_or_name
in: path
description: The id or name of the project
description: The name or id of the project
required: true
type: string
repositoryName:

View File

@ -183,16 +183,23 @@ func (a *projectAPI) CreateProject(ctx context.Context, params operation.CreateP
}
}
location := fmt.Sprintf("%s/%d", strings.TrimSuffix(params.HTTPRequest.URL.Path, "/"), projectID)
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)
}
return operation.NewCreateProjectCreated().WithLocation(location)
}
func (a *projectAPI) DeleteProject(ctx context.Context, params operation.DeleteProjectParams) middleware.Responder {
if err := a.RequireProjectAccess(ctx, params.ProjectID, rbac.ActionDelete); err != nil {
projectNameOrID := parseProjectNameOrID(params.ProjectNameOrID, params.XIsResourceName)
if err := a.RequireProjectAccess(ctx, projectNameOrID, rbac.ActionDelete); err != nil {
return a.SendError(ctx, err)
}
result, err := a.deletable(ctx, params.ProjectID)
p, result, err := a.deletable(ctx, projectNameOrID)
if err != nil {
return a.SendError(ctx, err)
}
@ -201,19 +208,19 @@ func (a *projectAPI) DeleteProject(ctx context.Context, params operation.DeleteP
return a.SendError(ctx, errors.PreconditionFailedError(errors.New(result.Message)))
}
if err := a.projectCtl.Delete(ctx, params.ProjectID); err != nil {
if err := a.projectCtl.Delete(ctx, p.ProjectID); err != nil {
return a.SendError(ctx, err)
}
// remove the robot associated with the project
if err := a.robotMgr.DeleteByProjectID(ctx, params.ProjectID); err != nil {
if err := a.robotMgr.DeleteByProjectID(ctx, p.ProjectID); err != nil {
return a.SendError(ctx, err)
}
referenceID := quota.ReferenceID(params.ProjectID)
referenceID := quota.ReferenceID(p.ProjectID)
q, err := a.quotaCtl.GetByRef(ctx, quota.ProjectReference, referenceID)
if err != nil {
log.Warningf("failed to get quota for project %d, error: %v", params.ProjectID, err)
log.Warningf("failed to get quota for project %s, error: %v", projectNameOrID, err)
} 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))
@ -221,7 +228,7 @@ func (a *projectAPI) DeleteProject(ctx context.Context, params operation.DeleteP
}
// preheat policies under the project should be deleted after deleting the project
if err = a.preheatCtl.DeletePoliciesOfProject(ctx, params.ProjectID); err != nil {
if err = a.preheatCtl.DeletePoliciesOfProject(ctx, p.ProjectID); err != nil {
return a.SendError(ctx, err)
}
@ -269,11 +276,12 @@ func (a *projectAPI) GetLogs(ctx context.Context, params operation.GetLogsParams
}
func (a *projectAPI) GetProject(ctx context.Context, params operation.GetProjectParams) middleware.Responder {
if err := a.RequireProjectAccess(ctx, params.ProjectID, rbac.ActionRead); err != nil {
projectNameOrID := parseProjectNameOrID(params.ProjectNameOrID, params.XIsResourceName)
if err := a.RequireProjectAccess(ctx, projectNameOrID, rbac.ActionRead); err != nil {
return a.SendError(ctx, err)
}
p, err := a.getProject(ctx, params.ProjectID, project.WithCVEAllowlist(), project.WithOwner())
p, err := a.getProject(ctx, projectNameOrID, project.WithCVEAllowlist(), project.WithOwner())
if err != nil {
return a.SendError(ctx, err)
}
@ -282,11 +290,12 @@ func (a *projectAPI) GetProject(ctx context.Context, params operation.GetProject
}
func (a *projectAPI) GetProjectDeletable(ctx context.Context, params operation.GetProjectDeletableParams) middleware.Responder {
if err := a.RequireProjectAccess(ctx, params.ProjectID, rbac.ActionDelete); err != nil {
projectNameOrID := parseProjectNameOrID(params.ProjectNameOrID, params.XIsResourceName)
if err := a.RequireProjectAccess(ctx, projectNameOrID, rbac.ActionDelete); err != nil {
return a.SendError(ctx, err)
}
result, err := a.deletable(ctx, params.ProjectID)
_, result, err := a.deletable(ctx, projectNameOrID)
if err != nil {
return a.SendError(ctx, err)
}
@ -295,11 +304,12 @@ func (a *projectAPI) GetProjectDeletable(ctx context.Context, params operation.G
}
func (a *projectAPI) GetProjectSummary(ctx context.Context, params operation.GetProjectSummaryParams) middleware.Responder {
if err := a.RequireProjectAccess(ctx, params.ProjectID, rbac.ActionRead); err != nil {
projectNameOrID := parseProjectNameOrID(params.ProjectNameOrID, params.XIsResourceName)
if err := a.RequireProjectAccess(ctx, projectNameOrID, rbac.ActionRead); err != nil {
return a.SendError(ctx, err)
}
p, err := a.getProject(ctx, params.ProjectID)
p, err := a.getProject(ctx, projectNameOrID)
if err != nil {
return a.SendError(ctx, err)
}
@ -440,11 +450,12 @@ func (a *projectAPI) ListProjects(ctx context.Context, params operation.ListProj
}
func (a *projectAPI) UpdateProject(ctx context.Context, params operation.UpdateProjectParams) middleware.Responder {
if err := a.RequireProjectAccess(ctx, params.ProjectID, rbac.ActionUpdate); err != nil {
projectNameOrID := parseProjectNameOrID(params.ProjectNameOrID, params.XIsResourceName)
if err := a.RequireProjectAccess(ctx, projectNameOrID, rbac.ActionUpdate); err != nil {
return a.SendError(ctx, err)
}
p, err := a.projectCtl.Get(ctx, params.ProjectID, project.Metadata(false))
p, err := a.projectCtl.Get(ctx, projectNameOrID, project.Metadata(false))
if err != nil {
return a.SendError(ctx, err)
}
@ -452,10 +463,10 @@ func (a *projectAPI) UpdateProject(ctx context.Context, params operation.UpdateP
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
params.Project.CVEAllowlist.ProjectID = params.ProjectID
} else if params.Project.CVEAllowlist.ProjectID != params.ProjectID {
params.Project.CVEAllowlist.ProjectID = p.ProjectID
} else if params.Project.CVEAllowlist.ProjectID != p.ProjectID {
return a.SendError(ctx, errors.BadRequestError(nil).
WithMessage("project_id in cve_allowlist must be %d but it's %d", params.ProjectID, params.Project.CVEAllowlist.ProjectID))
WithMessage("project_id in cve_allowlist must be %d but it's %d", p.ProjectID, params.Project.CVEAllowlist.ProjectID))
}
if err := lib.JSONCopy(&p.CVEAllowlist, params.Project.CVEAllowlist); err != nil {
@ -477,26 +488,26 @@ func (a *projectAPI) UpdateProject(ctx context.Context, params operation.UpdateP
return operation.NewUpdateProjectOK()
}
func (a *projectAPI) deletable(ctx context.Context, projectID int64) (*models.ProjectDeletable, error) {
proj, err := a.getProject(ctx, projectID)
func (a *projectAPI) deletable(ctx context.Context, projectNameOrID interface{}) (*project.Project, *models.ProjectDeletable, error) {
p, err := a.getProject(ctx, projectNameOrID)
if err != nil {
return nil, err
return nil, nil, err
}
result := &models.ProjectDeletable{Deletable: true}
if proj.RepoCount > 0 {
if p.RepoCount > 0 {
result.Deletable = false
result.Message = "the project contains repositories, can not be deleted"
} else if proj.ChartCount > 0 {
} else if p.ChartCount > 0 {
result.Deletable = false
result.Message = "the project contains helm charts, can not be deleted"
}
return result, nil
return p, result, nil
}
func (a *projectAPI) getProject(ctx context.Context, projectID int64, options ...project.Option) (*project.Project, error) {
p, err := a.projectCtl.Get(ctx, projectID, options...)
func (a *projectAPI) getProject(ctx context.Context, projectNameOrID interface{}, options ...project.Option) (*project.Project, error) {
p, err := a.projectCtl.Get(ctx, projectNameOrID, options...)
if err != nil {
return nil, err
}

View File

@ -3,10 +3,12 @@ package handler
import (
"context"
"fmt"
"regexp"
"strings"
"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/project"
"github.com/goharbor/harbor/src/controller/robot"
"github.com/goharbor/harbor/src/core/config"
@ -20,8 +22,6 @@ import (
handler_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/robotv1"
"regexp"
"strings"
)
func newRobotV1API() *robotV1API {
@ -40,7 +40,8 @@ type robotV1API struct {
}
func (rAPI *robotV1API) CreateRobotV1(ctx context.Context, params operation.CreateRobotV1Params) middleware.Responder {
if err := rAPI.RequireProjectAccess(ctx, params.ProjectIDOrName, rbac.ActionCreate, rbac.ResourceRobot); err != nil {
projectNameOrID := parseProjectNameOrID(params.ProjectNameOrID, params.XIsResourceName)
if err := rAPI.RequireProjectAccess(ctx, projectNameOrID, rbac.ActionCreate, rbac.ResourceRobot); err != nil {
return rAPI.SendError(ctx, err)
}
@ -57,19 +58,11 @@ func (rAPI *robotV1API) CreateRobotV1(ctx context.Context, params operation.Crea
Level: robot.LEVELPROJECT,
}
projectID, projectName, err := utils.ParseProjectIDOrName(params.ProjectIDOrName)
if err != nil {
return rAPI.SendError(ctx, err)
}
if projectID != 0 {
p, err := project.Ctl.Get(ctx, projectID)
projectName, ok := projectNameOrID.(string)
if !ok {
p, err := rAPI.projectCtr.Get(ctx, projectNameOrID, project.Metadata(false))
if err != nil {
log.Errorf("failed to get project %s: %v", projectName, err)
return rAPI.SendError(ctx, err)
}
if p == nil {
log.Warningf("project %s not found", projectName)
log.Errorf("failed to get project %s: %v", projectNameOrID, err)
return rAPI.SendError(ctx, err)
}
projectName = p.Name
@ -116,11 +109,12 @@ func (rAPI *robotV1API) CreateRobotV1(ctx context.Context, params operation.Crea
}
func (rAPI *robotV1API) DeleteRobotV1(ctx context.Context, params operation.DeleteRobotV1Params) middleware.Responder {
if err := rAPI.RequireProjectAccess(ctx, params.ProjectIDOrName, rbac.ActionDelete, rbac.ResourceRobot); err != nil {
projectNameOrID := parseProjectNameOrID(params.ProjectNameOrID, params.XIsResourceName)
if err := rAPI.RequireProjectAccess(ctx, projectNameOrID, rbac.ActionDelete, rbac.ResourceRobot); err != nil {
return rAPI.SendError(ctx, err)
}
pro, err := rAPI.projectCtr.Get(ctx, params.ProjectIDOrName)
pro, err := rAPI.projectCtr.Get(ctx, projectNameOrID)
if err != nil {
return rAPI.SendError(ctx, err)
}
@ -142,7 +136,8 @@ func (rAPI *robotV1API) DeleteRobotV1(ctx context.Context, params operation.Dele
}
func (rAPI *robotV1API) ListRobotV1(ctx context.Context, params operation.ListRobotV1Params) middleware.Responder {
if err := rAPI.RequireProjectAccess(ctx, params.ProjectIDOrName, rbac.ActionList, rbac.ResourceRobot); err != nil {
projectNameOrID := parseProjectNameOrID(params.ProjectNameOrID, params.XIsResourceName)
if err := rAPI.RequireProjectAccess(ctx, projectNameOrID, rbac.ActionList, rbac.ResourceRobot); err != nil {
return rAPI.SendError(ctx, err)
}
@ -151,7 +146,7 @@ func (rAPI *robotV1API) ListRobotV1(ctx context.Context, params operation.ListRo
return rAPI.SendError(ctx, err)
}
pro, err := rAPI.projectCtr.Get(ctx, params.ProjectIDOrName)
pro, err := rAPI.projectCtr.Get(ctx, projectNameOrID)
if err != nil {
return rAPI.SendError(ctx, err)
}
@ -182,11 +177,12 @@ func (rAPI *robotV1API) ListRobotV1(ctx context.Context, params operation.ListRo
}
func (rAPI *robotV1API) GetRobotByIDV1(ctx context.Context, params operation.GetRobotByIDV1Params) middleware.Responder {
if err := rAPI.RequireProjectAccess(ctx, params.ProjectIDOrName, rbac.ActionRead, rbac.ResourceRobot); err != nil {
projectNameOrID := parseProjectNameOrID(params.ProjectNameOrID, params.XIsResourceName)
if err := rAPI.RequireProjectAccess(ctx, projectNameOrID, rbac.ActionRead, rbac.ResourceRobot); err != nil {
return rAPI.SendError(ctx, err)
}
pro, err := rAPI.projectCtr.Get(ctx, params.ProjectIDOrName)
pro, err := rAPI.projectCtr.Get(ctx, projectNameOrID)
if err != nil {
return rAPI.SendError(ctx, err)
}
@ -205,11 +201,12 @@ func (rAPI *robotV1API) GetRobotByIDV1(ctx context.Context, params operation.Get
}
func (rAPI *robotV1API) UpdateRobotV1(ctx context.Context, params operation.UpdateRobotV1Params) middleware.Responder {
if err := rAPI.RequireProjectAccess(ctx, params.ProjectIDOrName, rbac.ActionUpdate, rbac.ResourceRobot); err != nil {
projectNameOrID := parseProjectNameOrID(params.ProjectNameOrID, params.XIsResourceName)
if err := rAPI.RequireProjectAccess(ctx, projectNameOrID, rbac.ActionUpdate, rbac.ResourceRobot); err != nil {
return rAPI.SendError(ctx, err)
}
pro, err := rAPI.projectCtr.Get(ctx, params.ProjectIDOrName)
pro, err := rAPI.projectCtr.Get(ctx, projectNameOrID)
if err != nil {
return rAPI.SendError(ctx, err)
}
@ -251,7 +248,8 @@ func (rAPI *robotV1API) validate(ctx context.Context, params operation.CreateRob
return errors.New(nil).WithMessage("bad request no access").WithCode(errors.BadRequestCode)
}
pro, err := rAPI.projectCtr.Get(ctx, params.ProjectIDOrName)
projectNameOrID := parseProjectNameOrID(params.ProjectNameOrID, params.XIsResourceName)
pro, err := rAPI.projectCtr.Get(ctx, projectNameOrID)
if err != nil {
return err
}
@ -267,7 +265,7 @@ func (rAPI *robotV1API) validate(ctx context.Context, params operation.CreateRob
p := &types.Policy{}
lib.JSONCopy(p, policy)
if !mp[p.String()] {
return errors.New(nil).WithMessage("%s action of %s resource not exist in project %s", policy.Action, policy.Resource, params.ProjectIDOrName).WithCode(errors.BadRequestCode)
return errors.New(nil).WithMessage("%s action of %s resource not exist in project %s", policy.Action, policy.Resource, projectNameOrID).WithCode(errors.BadRequestCode)
}
}

View File

@ -20,10 +20,12 @@ import (
"fmt"
"net/url"
"reflect"
"strconv"
"github.com/goharbor/harbor/src/controller/artifact"
"github.com/goharbor/harbor/src/controller/artifact/processor"
"github.com/goharbor/harbor/src/controller/scan"
"github.com/goharbor/harbor/src/lib"
"github.com/goharbor/harbor/src/lib/log"
"github.com/goharbor/harbor/src/pkg/scan/report"
v1 "github.com/goharbor/harbor/src/pkg/scan/rest/v1"
@ -126,3 +128,18 @@ func unescapePathParams(params interface{}, fieldNames ...string) error {
return nil
}
func parseProjectNameOrID(str string, isResourceName *bool) interface{} {
if lib.BoolValue(isResourceName) {
// always as projectName
return str
}
v, err := strconv.ParseInt(str, 10, 64)
if err != nil {
// it's projectName
return str
}
return v // projectID
}