From af24a073dc9bf9db6e8a7c55a06a6a6882fe80d9 Mon Sep 17 00:00:00 2001 From: He Weiwei Date: Mon, 14 Dec 2020 15:48:52 +0800 Subject: [PATCH] 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 --- api/v2.0/swagger.yaml | 68 +++++++++++++++++++----------- src/server/v2.0/handler/project.go | 65 ++++++++++++++++------------ src/server/v2.0/handler/robotV1.go | 50 +++++++++++----------- src/server/v2.0/handler/util.go | 17 ++++++++ 4 files changed, 122 insertions(+), 78 deletions(-) diff --git a/api/v2.0/swagger.yaml b/api/v2.0/swagger.yaml index 550e78321..32d9bcbf7 100644 --- a/api/v2.0/swagger.yaml +++ b/api/v2.0/swagger.yaml @@ -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: diff --git a/src/server/v2.0/handler/project.go b/src/server/v2.0/handler/project.go index b099d73ce..0a1a87e67 100644 --- a/src/server/v2.0/handler/project.go +++ b/src/server/v2.0/handler/project.go @@ -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 } diff --git a/src/server/v2.0/handler/robotV1.go b/src/server/v2.0/handler/robotV1.go index edb116bee..545d6be29 100644 --- a/src/server/v2.0/handler/robotV1.go +++ b/src/server/v2.0/handler/robotV1.go @@ -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) } } diff --git a/src/server/v2.0/handler/util.go b/src/server/v2.0/handler/util.go index 944551243..d2ed4bc5e 100644 --- a/src/server/v2.0/handler/util.go +++ b/src/server/v2.0/handler/util.go @@ -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 +}