harbor/src/server/v2.0/handler/artifact.go

527 lines
17 KiB
Go

// 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.
package handler
import (
"context"
"encoding/json"
"fmt"
"net/http"
"strings"
"time"
"github.com/docker/distribution/reference"
"github.com/go-openapi/runtime"
"github.com/go-openapi/runtime/middleware"
"github.com/opencontainers/go-digest"
"github.com/goharbor/harbor/src/common"
"github.com/goharbor/harbor/src/common/rbac"
"github.com/goharbor/harbor/src/common/utils"
"github.com/goharbor/harbor/src/controller/artifact"
"github.com/goharbor/harbor/src/controller/event/metadata"
"github.com/goharbor/harbor/src/controller/project"
"github.com/goharbor/harbor/src/controller/repository"
"github.com/goharbor/harbor/src/controller/scan"
"github.com/goharbor/harbor/src/controller/tag"
"github.com/goharbor/harbor/src/lib"
"github.com/goharbor/harbor/src/lib/errors"
"github.com/goharbor/harbor/src/lib/log"
"github.com/goharbor/harbor/src/lib/q"
"github.com/goharbor/harbor/src/pkg/accessory"
"github.com/goharbor/harbor/src/pkg/label"
"github.com/goharbor/harbor/src/pkg/notification"
"github.com/goharbor/harbor/src/pkg/scan/report"
"github.com/goharbor/harbor/src/server/v2.0/handler/assembler"
"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/artifact"
)
func newArtifactAPI() *artifactAPI {
return &artifactAPI{
accMgr: accessory.Mgr,
artCtl: artifact.Ctl,
proCtl: project.Ctl,
repoCtl: repository.Ctl,
scanCtl: scan.DefaultController,
tagCtl: tag.Ctl,
labelMgr: label.Mgr,
}
}
type artifactAPI struct {
BaseAPI
accMgr accessory.Manager
artCtl artifact.Controller
proCtl project.Controller
repoCtl repository.Controller
scanCtl scan.Controller
tagCtl tag.Controller
labelMgr label.Manager
}
func (a *artifactAPI) Prepare(ctx context.Context, _ string, params interface{}) middleware.Responder {
if err := unescapePathParams(params, "RepositoryName"); err != nil {
a.SendError(ctx, err)
}
return nil
}
func (a *artifactAPI) ListArtifacts(ctx context.Context, params operation.ListArtifactsParams) middleware.Responder {
if err := a.RequireProjectAccess(ctx, params.ProjectName, rbac.ActionList, rbac.ResourceArtifact); err != nil {
return a.SendError(ctx, err)
}
// set query
query, err := a.BuildQuery(ctx, params.Q, params.Sort, params.Page, params.PageSize)
if err != nil {
return a.SendError(ctx, err)
}
query.Keywords["RepositoryName"] = fmt.Sprintf("%s/%s", params.ProjectName, params.RepositoryName)
// set option
option := option(params.WithTag, params.WithImmutableStatus,
params.WithLabel, params.WithAccessory)
// get the total count of artifacts
total, err := a.artCtl.Count(ctx, query)
if err != nil {
return a.SendError(ctx, err)
}
// list artifacts according to the query and option
arts, err := a.artCtl.List(ctx, query, option)
if err != nil {
return a.SendError(ctx, err)
}
overviewOpts := model.NewOverviewOptions(model.WithSBOM(lib.BoolValue(params.WithSbomOverview)), model.WithVuln(lib.BoolValue(params.WithScanOverview)))
assembler := assembler.NewScanReportAssembler(overviewOpts, parseScanReportMimeTypes(params.XAcceptVulnerabilities))
var artifacts []*models.Artifact
for _, art := range arts {
artifact := &model.Artifact{}
artifact.Artifact = *art
_ = assembler.WithArtifacts(artifact).Assemble(ctx)
artifacts = append(artifacts, artifact.ToSwagger())
}
return operation.NewListArtifactsOK().
WithXTotalCount(total).
WithLink(a.Links(ctx, params.HTTPRequest.URL, total, query.PageNumber, query.PageSize).String()).
WithPayload(artifacts)
}
func (a *artifactAPI) GetArtifact(ctx context.Context, params operation.GetArtifactParams) middleware.Responder {
if err := a.RequireProjectAccess(ctx, params.ProjectName, rbac.ActionRead, rbac.ResourceArtifact); err != nil {
return a.SendError(ctx, err)
}
// set option
option := option(params.WithTag, params.WithImmutableStatus,
params.WithLabel, params.WithAccessory)
// get the artifact
artifact, err := a.artCtl.GetByReference(ctx, fmt.Sprintf("%s/%s", params.ProjectName, params.RepositoryName), params.Reference, option)
if err != nil {
return a.SendError(ctx, err)
}
art := &model.Artifact{}
art.Artifact = *artifact
overviewOpts := model.NewOverviewOptions(model.WithSBOM(lib.BoolValue(params.WithSbomOverview)), model.WithVuln(lib.BoolValue(params.WithScanOverview)))
err = assembler.NewScanReportAssembler(overviewOpts, parseScanReportMimeTypes(params.XAcceptVulnerabilities)).WithArtifacts(art).Assemble(ctx)
if err != nil {
log.Warningf("failed to assemble vulnerabilities with artifact, error: %v", err)
}
return operation.NewGetArtifactOK().WithPayload(art.ToSwagger())
}
func (a *artifactAPI) DeleteArtifact(ctx context.Context, params operation.DeleteArtifactParams) middleware.Responder {
if err := a.RequireProjectAccess(ctx, params.ProjectName, rbac.ActionDelete, rbac.ResourceArtifact); err != nil {
return a.SendError(ctx, err)
}
artifact, err := a.artCtl.GetByReference(ctx, fmt.Sprintf("%s/%s", params.ProjectName, params.RepositoryName), params.Reference, nil)
if err != nil {
return a.SendError(ctx, err)
}
if err = a.artCtl.Delete(ctx, artifact.ID); err != nil {
return a.SendError(ctx, err)
}
return operation.NewDeleteArtifactOK()
}
func (a *artifactAPI) CopyArtifact(ctx context.Context, params operation.CopyArtifactParams) middleware.Responder {
if err := a.RequireProjectAccess(ctx, params.ProjectName, rbac.ActionCreate, rbac.ResourceArtifact); err != nil {
return a.SendError(ctx, err)
}
if err := a.requireNonProxyCacheProject(ctx, params.ProjectName); err != nil {
return a.SendError(ctx, err)
}
srcRepo, ref, err := parse(params.From)
if err != nil {
return a.SendError(ctx, err)
}
srcPro, _ := utils.ParseRepository(srcRepo)
if err = a.RequireProjectAccess(ctx, srcPro, rbac.ActionRead, rbac.ResourceArtifact); err != nil {
return a.SendError(ctx, err)
}
srcArt, err := a.artCtl.GetByReference(ctx, srcRepo, ref, nil)
if err != nil {
return a.SendError(ctx, err)
}
accs, err := a.accMgr.List(ctx, q.New(q.KeyWords{"ArtifactID": srcArt.ID, "Digest": srcArt.Digest}))
if err != nil {
return a.SendError(ctx, err)
}
if len(accs) >= 1 && accs[0].IsHard() {
return a.SendError(ctx, errors.New(nil).WithCode(errors.DENIED).WithMessage("the operation isn't supported for an artifact accessory"))
}
dstRepo := fmt.Sprintf("%s/%s", params.ProjectName, params.RepositoryName)
_, _, err = a.repoCtl.Ensure(ctx, dstRepo)
if err != nil {
return a.SendError(ctx, err)
}
_, err = a.artCtl.Copy(ctx, srcRepo, ref, dstRepo)
if err != nil {
return a.SendError(ctx, err)
}
location := strings.TrimSuffix(params.HTTPRequest.URL.Path, "/") + "/" + ref
return operation.NewCopyArtifactCreated().WithLocation(location)
}
// parse "repository:tag" or "repository@digest" into repository and reference parts
func parse(s string) (string, string, error) {
matches := reference.ReferenceRegexp.FindStringSubmatch(s)
if matches == nil {
return "", "", errors.New(nil).WithCode(errors.BadRequestCode).
WithMessage("invalid input: %s", s)
}
repository := matches[1]
reference := matches[2]
if matches[3] != "" {
_, err := digest.Parse(matches[3])
if err != nil {
return "", "", errors.New(nil).WithCode(errors.BadRequestCode).
WithMessage("invalid input: %s", s)
}
reference = matches[3]
}
return repository, reference, nil
}
func (a *artifactAPI) CreateTag(ctx context.Context, params operation.CreateTagParams) middleware.Responder {
if err := a.RequireProjectAccess(ctx, params.ProjectName, rbac.ActionCreate, rbac.ResourceTag); err != nil {
return a.SendError(ctx, err)
}
if err := a.requireNonProxyCacheProject(ctx, params.ProjectName); err != nil {
return a.SendError(ctx, err)
}
art, err := a.artCtl.GetByReference(ctx, fmt.Sprintf("%s/%s", params.ProjectName, params.RepositoryName),
params.Reference, &artifact.Option{
WithTag: true,
})
if err != nil {
return a.SendError(ctx, err)
}
tag := &tag.Tag{}
tag.RepositoryID = art.RepositoryID
tag.ArtifactID = art.ID
tag.Name = params.Tag.Name
tag.PushTime = time.Now()
if _, err = a.tagCtl.Create(ctx, tag); err != nil {
return a.SendError(ctx, err)
}
// fire event
notification.AddEvent(ctx, &metadata.CreateTagEventMetadata{
Ctx: ctx,
Tag: tag.Name,
AttachedArtifact: &art.Artifact,
})
// as we provide no API for get the single tag, ignore setting the location header here
return operation.NewCreateTagCreated()
}
func (a *artifactAPI) requireNonProxyCacheProject(ctx context.Context, name string) error {
pro, err := a.proCtl.GetByName(ctx, name)
if err != nil {
return err
}
if pro.IsProxy() {
return errors.New(nil).WithCode(errors.MethodNotAllowedCode).
WithMessage("the operation isn't supported for a proxy cache project")
}
return nil
}
func (a *artifactAPI) DeleteTag(ctx context.Context, params operation.DeleteTagParams) middleware.Responder {
if err := a.RequireProjectAccess(ctx, params.ProjectName, rbac.ActionDelete, rbac.ResourceTag); err != nil {
return a.SendError(ctx, err)
}
artifact, err := a.artCtl.GetByReference(ctx, fmt.Sprintf("%s/%s", params.ProjectName, params.RepositoryName),
params.Reference, &artifact.Option{
WithTag: true,
})
if err != nil {
return a.SendError(ctx, err)
}
var id int64
for _, tag := range artifact.Tags {
if tag.Name == params.TagName {
id = tag.ID
break
}
}
// the tag not found
if id == 0 {
err = errors.New(nil).WithCode(errors.NotFoundCode).WithMessage(
"tag %s attached to artifact %d not found", params.TagName, artifact.ID)
return a.SendError(ctx, err)
}
if err = a.tagCtl.Delete(ctx, id); err != nil {
return a.SendError(ctx, err)
}
// fire event
notification.AddEvent(ctx, &metadata.DeleteTagEventMetadata{
Ctx: ctx,
Tag: params.TagName,
AttachedArtifact: &artifact.Artifact,
})
return operation.NewDeleteTagOK()
}
func (a *artifactAPI) ListTags(ctx context.Context, params operation.ListTagsParams) middleware.Responder {
if err := a.RequireProjectAccess(ctx, params.ProjectName, rbac.ActionList, rbac.ResourceTag); err != nil {
return a.SendError(ctx, err)
}
// set query
query, err := a.BuildQuery(ctx, params.Q, params.Sort, params.Page, params.PageSize)
if err != nil {
return a.SendError(ctx, err)
}
artifact, err := a.artCtl.GetByReference(ctx, fmt.Sprintf("%s/%s", params.ProjectName, params.RepositoryName), params.Reference, nil)
if err != nil {
return a.SendError(ctx, err)
}
query.Keywords["ArtifactID"] = artifact.ID
// get the total count of tags
total, err := a.tagCtl.Count(ctx, query)
if err != nil {
return a.SendError(ctx, err)
}
// set option
option := &tag.Option{}
if params.WithImmutableStatus != nil {
option.WithImmutableStatus = *params.WithImmutableStatus
}
// list tags according to the query and option
tags, err := a.tagCtl.List(ctx, query, option)
if err != nil {
return a.SendError(ctx, err)
}
var ts []*models.Tag
for _, tag := range tags {
ts = append(ts, model.NewTag(tag).ToSwagger())
}
return operation.NewListTagsOK().
WithXTotalCount(total).
WithLink(a.Links(ctx, params.HTTPRequest.URL, total, query.PageNumber, query.PageSize).String()).
WithPayload(ts)
}
func (a *artifactAPI) ListAccessories(ctx context.Context, params operation.ListAccessoriesParams) middleware.Responder {
if err := a.RequireProjectAccess(ctx, params.ProjectName, rbac.ActionList, rbac.ResourceAccessory); err != nil {
return a.SendError(ctx, err)
}
// set query
query, err := a.BuildQuery(ctx, params.Q, params.Sort, params.Page, params.PageSize)
if err != nil {
return a.SendError(ctx, err)
}
artifact, err := a.artCtl.GetByReference(ctx, fmt.Sprintf("%s/%s", params.ProjectName, params.RepositoryName), params.Reference, nil)
if err != nil {
return a.SendError(ctx, err)
}
query.Keywords["SubjectArtifactID"] = artifact.ID
// list accessories according to the query
total, err := a.accMgr.Count(ctx, query)
if err != nil {
return a.SendError(ctx, err)
}
accs, err := a.accMgr.List(ctx, query)
if err != nil {
return a.SendError(ctx, err)
}
var res []*models.Accessory
for _, acc := range accs {
res = append(res, model.NewAccessory(acc.GetData()).ToSwagger())
}
return operation.NewListAccessoriesOK().
WithXTotalCount(total).
WithLink(a.Links(ctx, params.HTTPRequest.URL, total, query.PageNumber, query.PageSize).String()).
WithPayload(res)
}
func (a *artifactAPI) GetVulnerabilitiesAddition(ctx context.Context, params operation.GetVulnerabilitiesAdditionParams) middleware.Responder {
if err := a.RequireProjectAccess(ctx, params.ProjectName, rbac.ActionRead, rbac.ResourceArtifactAddition); err != nil {
return a.SendError(ctx, err)
}
artifact, err := a.artCtl.GetByReference(ctx, fmt.Sprintf("%s/%s", params.ProjectName, params.RepositoryName), params.Reference, nil)
if err != nil {
return a.SendError(ctx, err)
}
vulnerabilities := make(map[string]interface{})
for _, mimeType := range parseScanReportMimeTypes(params.XAcceptVulnerabilities) {
reports, err := a.scanCtl.GetReport(ctx, artifact, []string{mimeType})
if err != nil {
return a.SendError(ctx, err)
}
vrp, err := report.Reports(reports).ResolveData(mimeType)
if err != nil {
return a.SendError(ctx, err)
}
if vrp == nil {
continue
}
vulnerabilities[mimeType] = vrp
if len(vulnerabilities) != 0 {
break
}
}
content, _ := json.Marshal(vulnerabilities)
return middleware.ResponderFunc(func(w http.ResponseWriter, p runtime.Producer) {
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write(content)
})
}
func (a *artifactAPI) GetAddition(ctx context.Context, params operation.GetAdditionParams) middleware.Responder {
if err := a.RequireProjectAccess(ctx, params.ProjectName, rbac.ActionRead, rbac.ResourceArtifactAddition); err != nil {
return a.SendError(ctx, err)
}
artifact, err := a.artCtl.GetByReference(ctx, fmt.Sprintf("%s/%s", params.ProjectName, params.RepositoryName), params.Reference, nil)
if err != nil {
return a.SendError(ctx, err)
}
addition, err := a.artCtl.GetAddition(ctx, artifact.ID, strings.ToUpper(params.Addition))
if err != nil {
return a.SendError(ctx, err)
}
return middleware.ResponderFunc(func(w http.ResponseWriter, p runtime.Producer) {
w.Header().Set("Content-Type", addition.ContentType)
_, _ = w.Write(addition.Content)
})
}
func (a *artifactAPI) AddLabel(ctx context.Context, params operation.AddLabelParams) middleware.Responder {
projectID, err := getProjectID(ctx, params.ProjectName)
if err != nil {
return a.SendError(ctx, err)
}
if err := a.RequireProjectAccess(ctx, projectID, rbac.ActionCreate, rbac.ResourceArtifactLabel); err != nil {
return a.SendError(ctx, err)
}
if err := a.RequireLabelInProject(ctx, projectID, params.Label.ID); err != nil {
return a.SendError(ctx, err)
}
art, err := a.artCtl.GetByReference(ctx, fmt.Sprintf("%s/%s", params.ProjectName, params.RepositoryName), params.Reference, nil)
if err != nil {
return a.SendError(ctx, err)
}
if err = a.artCtl.AddLabel(ctx, art.ID, params.Label.ID); err != nil {
return a.SendError(ctx, err)
}
return operation.NewAddLabelOK()
}
func (a *artifactAPI) RemoveLabel(ctx context.Context, params operation.RemoveLabelParams) middleware.Responder {
if err := a.RequireProjectAccess(ctx, params.ProjectName, rbac.ActionDelete, rbac.ResourceArtifactLabel); err != nil {
return a.SendError(ctx, err)
}
art, err := a.artCtl.GetByReference(ctx, fmt.Sprintf("%s/%s", params.ProjectName, params.RepositoryName), params.Reference, nil)
if err != nil {
return a.SendError(ctx, err)
}
if err = a.artCtl.RemoveLabel(ctx, art.ID, params.LabelID); err != nil {
return a.SendError(ctx, err)
}
return operation.NewRemoveLabelOK()
}
func (a *artifactAPI) RequireLabelInProject(ctx context.Context, projectID, labelID int64) error {
l, err := a.labelMgr.Get(ctx, labelID)
if err != nil {
return err
}
if l.Scope == common.LabelScopeProject && l.ProjectID != projectID {
return errors.NotFoundError(nil).WithMessage("project id %d, label %d not found", projectID, labelID)
}
return nil
}
func option(withTag, withImmutableStatus, withLabel, withAccessory *bool) *artifact.Option {
option := &artifact.Option{
WithTag: true, // return the tag by default
WithLabel: lib.BoolValue(withLabel),
WithAccessory: true, // return the accessory by default
}
if withTag != nil {
option.WithTag = *(withTag)
}
if withAccessory != nil {
option.WithAccessory = *(withAccessory)
}
if option.WithTag {
option.TagOption = &tag.Option{
WithImmutableStatus: lib.BoolValue(withImmutableStatus),
}
}
return option
}