diff --git a/docs/swagger.yaml b/docs/swagger.yaml index 6b85f001b..3bd2de958 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -1089,6 +1089,8 @@ paths: description: Forbidden. '404': description: Repository not found. + '412': + description: Precondition Failed. put: summary: Update description of the repository. description: | diff --git a/src/common/models/repo.go b/src/common/models/repo.go index 6562e9531..aa2bc24ec 100644 --- a/src/common/models/repo.go +++ b/src/common/models/repo.go @@ -73,6 +73,7 @@ type TagDetail struct { Author string `json:"author"` Created time.Time `json:"created"` Config *TagCfg `json:"config"` + Immutable bool `json:"immutable"` } // TagCfg ... diff --git a/src/core/api/repository.go b/src/core/api/repository.go index 6d9a1f7f5..ac3284bea 100755 --- a/src/core/api/repository.go +++ b/src/core/api/repository.go @@ -28,7 +28,7 @@ import ( "github.com/goharbor/harbor/src/jobservice/logger" "github.com/goharbor/harbor/src/pkg/scan/api/scan" - v1 "github.com/goharbor/harbor/src/pkg/scan/rest/v1" + "github.com/goharbor/harbor/src/pkg/scan/rest/v1" "github.com/docker/distribution/manifest/schema1" "github.com/docker/distribution/manifest/schema2" @@ -45,6 +45,8 @@ import ( "github.com/goharbor/harbor/src/core/config" notifierEvt "github.com/goharbor/harbor/src/core/notifier/event" coreutils "github.com/goharbor/harbor/src/core/utils" + "github.com/goharbor/harbor/src/pkg/art" + "github.com/goharbor/harbor/src/pkg/immutabletag/match/rule" "github.com/goharbor/harbor/src/replication" "github.com/goharbor/harbor/src/replication/event" "github.com/goharbor/harbor/src/replication/model" @@ -283,11 +285,6 @@ func (ra *RepositoryAPI) Delete() { } for _, t := range tags { - image := fmt.Sprintf("%s:%s", repoName, t) - if err = dao.DeleteLabelsOfResource(common.ResourceTypeImage, image); err != nil { - ra.SendInternalServerError(fmt.Errorf("failed to delete labels of image %s: %v", image, err)) - return - } if err = rc.DeleteTag(t); err != nil { if regErr, ok := err.(*commonhttp.Error); ok { if regErr.Code == http.StatusNotFound { @@ -298,6 +295,11 @@ func (ra *RepositoryAPI) Delete() { return } log.Infof("delete tag: %s:%s", repoName, t) + image := fmt.Sprintf("%s:%s", repoName, t) + if err = dao.DeleteLabelsOfResource(common.ResourceTypeImage, image); err != nil { + ra.SendInternalServerError(fmt.Errorf("failed to delete labels of image %s: %v", image, err)) + return + } go func(tag string) { e := &event.Event{ @@ -711,6 +713,9 @@ func assembleTag(c chan *models.TagResp, client *registry.Repository, projectID } } + // get immutable status + item.Immutable = isImmutable(projectID, repository, tag) + c <- item } @@ -791,6 +796,21 @@ func populateAuthor(detail *models.TagDetail) { } } +// check whether the tag is immutable +func isImmutable(projectID int64, repo string, tag string) bool { + _, repoName := utils.ParseRepository(repo) + matched, err := rule.NewRuleMatcher(projectID).Match(art.Candidate{ + Repository: repoName, + Tag: tag, + NamespaceID: projectID, + }) + if err != nil { + log.Error(err) + return false + } + return matched +} + // GetManifests returns the manifest of a tag func (ra *RepositoryAPI) GetManifests() { repoName := ra.GetString(":splat") diff --git a/src/core/middlewares/config.go b/src/core/middlewares/config.go index adc2a75c4..fa2b536f5 100644 --- a/src/core/middlewares/config.go +++ b/src/core/middlewares/config.go @@ -35,4 +35,4 @@ var ChartMiddlewares = []string{CHART} var Middlewares = []string{READONLY, URL, MUITIPLEMANIFEST, LISTREPO, CONTENTTRUST, VULNERABLE, SIZEQUOTA, IMMUTABLE, COUNTQUOTA} // MiddlewaresLocal ... -var MiddlewaresLocal = []string{SIZEQUOTA, COUNTQUOTA} +var MiddlewaresLocal = []string{SIZEQUOTA, IMMUTABLE, COUNTQUOTA} diff --git a/src/core/middlewares/immutable/builder.go b/src/core/middlewares/immutable/builder.go new file mode 100644 index 000000000..30707299d --- /dev/null +++ b/src/core/middlewares/immutable/builder.go @@ -0,0 +1,54 @@ +package immutable + +import ( + "fmt" + "github.com/goharbor/harbor/src/core/middlewares/interceptor" + "github.com/goharbor/harbor/src/core/middlewares/interceptor/immutable" + "github.com/goharbor/harbor/src/core/middlewares/util" + "net/http" +) + +var ( + defaultBuilders = []interceptor.Builder{ + &manifestDeletionBuilder{}, + &manifestCreationBuilder{}, + } +) + +type manifestDeletionBuilder struct{} + +func (*manifestDeletionBuilder) Build(req *http.Request) (interceptor.Interceptor, error) { + if match, _, _ := util.MatchDeleteManifest(req); !match { + return nil, nil + } + + info, ok := util.ManifestInfoFromContext(req.Context()) + if !ok { + var err error + info, err = util.ParseManifestInfoFromPath(req) + if err != nil { + return nil, fmt.Errorf("failed to parse manifest, error %v", err) + } + } + + return immutable.NewDeleteMFInteceptor(info), nil +} + +type manifestCreationBuilder struct{} + +func (*manifestCreationBuilder) Build(req *http.Request) (interceptor.Interceptor, error) { + if match, _, _ := util.MatchPushManifest(req); !match { + return nil, nil + } + + info, ok := util.ManifestInfoFromContext(req.Context()) + if !ok { + var err error + info, err = util.ParseManifestInfoFromReq(req) + if err != nil { + return nil, fmt.Errorf("failed to parse manifest, error %v", err) + } + } + + return immutable.NewPushMFInteceptor(info), nil +} diff --git a/src/core/middlewares/immutable/handler.go b/src/core/middlewares/immutable/handler.go index 0566d9df0..e9deb2dbf 100644 --- a/src/core/middlewares/immutable/handler.go +++ b/src/core/middlewares/immutable/handler.go @@ -16,78 +16,74 @@ package immutable import ( "fmt" - "github.com/goharbor/harbor/src/common/dao" - "github.com/goharbor/harbor/src/common/models" - common_util "github.com/goharbor/harbor/src/common/utils" "github.com/goharbor/harbor/src/common/utils/log" + "github.com/goharbor/harbor/src/core/middlewares/interceptor" "github.com/goharbor/harbor/src/core/middlewares/util" - "github.com/goharbor/harbor/src/pkg/art" - "github.com/goharbor/harbor/src/pkg/immutabletag/match/rule" + middlerware_err "github.com/goharbor/harbor/src/core/middlewares/util/error" "net/http" ) type immutableHandler struct { - next http.Handler + builders []interceptor.Builder + next http.Handler } // New ... -func New(next http.Handler) http.Handler { +func New(next http.Handler, builders ...interceptor.Builder) http.Handler { + if len(builders) == 0 { + builders = defaultBuilders + } + return &immutableHandler{ - next: next, + builders: builders, + next: next, } } // ServeHTTP ... -func (rh immutableHandler) ServeHTTP(rw http.ResponseWriter, req *http.Request) { - if match, _, _ := util.MatchPushManifest(req); !match { +func (rh *immutableHandler) ServeHTTP(rw http.ResponseWriter, req *http.Request) { + + interceptor, err := rh.getInterceptor(req) + if err != nil { + log.Warningf("Error occurred when to handle request in immutable handler: %v", err) + http.Error(rw, util.MarshalError("InternalError", fmt.Sprintf("Error occurred when to handle request in immutable handler: %v", err)), + http.StatusInternalServerError) + return + } + + if interceptor == nil { rh.next.ServeHTTP(rw, req) return } - info, ok := util.ManifestInfoFromContext(req.Context()) - if !ok { - var err error - info, err = util.ParseManifestInfoFromPath(req) - if err != nil { - log.Error(err) - rh.next.ServeHTTP(rw, req) + + if err := interceptor.HandleRequest(req); err != nil { + log.Warningf("Error occurred when to handle request in immutable handler: %v", err) + if _, ok := err.(middlerware_err.ErrImmutable); ok { + http.Error(rw, util.MarshalError("DENIED", + fmt.Sprintf("The tag is immutable, cannot be overwrite: %v", err)), http.StatusPreconditionFailed) return } + http.Error(rw, util.MarshalError("InternalError", fmt.Sprintf("Error occurred when to handle request in immutable handler: %v", err)), + http.StatusInternalServerError) + return + } + + rh.next.ServeHTTP(rw, req) + + interceptor.HandleResponse(rw, req) +} + +func (rh *immutableHandler) getInterceptor(req *http.Request) (interceptor.Interceptor, error) { + for _, builder := range rh.builders { + interceptor, err := builder.Build(req) + if err != nil { + return nil, err + } + + if interceptor != nil { + return interceptor, nil + } } - _, repoName := common_util.ParseRepository(info.Repository) - matched, err := rule.NewRuleMatcher(info.ProjectID).Match(art.Candidate{ - Repository: repoName, - Tag: info.Tag, - NamespaceID: info.ProjectID, - }) - if err != nil { - log.Error(err) - rh.next.ServeHTTP(rw, req) - return - } - if !matched { - rh.next.ServeHTTP(rw, req) - return - } - - artifactQuery := &models.ArtifactQuery{ - PID: info.ProjectID, - Repo: info.Repository, - Tag: info.Tag, - } - afs, err := dao.ListArtifacts(artifactQuery) - if err != nil { - log.Error(err) - rh.next.ServeHTTP(rw, req) - return - } - if len(afs) == 0 { - rh.next.ServeHTTP(rw, req) - return - } - - // rule matched and non-existent is a immutable tag - http.Error(rw, util.MarshalError("DENIED", - fmt.Sprintf("The tag:%s:%s is immutable, cannot be overwrite.", info.Repository, info.Tag)), http.StatusPreconditionFailed) - return + return nil, nil } diff --git a/src/core/middlewares/interceptor/immutable/deletemf.go b/src/core/middlewares/interceptor/immutable/deletemf.go new file mode 100644 index 000000000..b1b8e2495 --- /dev/null +++ b/src/core/middlewares/interceptor/immutable/deletemf.go @@ -0,0 +1,65 @@ +package immutable + +import ( + "github.com/goharbor/harbor/src/common/dao" + "github.com/goharbor/harbor/src/common/models" + common_util "github.com/goharbor/harbor/src/common/utils" + "github.com/goharbor/harbor/src/common/utils/log" + "github.com/goharbor/harbor/src/core/middlewares/interceptor" + "github.com/goharbor/harbor/src/core/middlewares/util" + middlerware_err "github.com/goharbor/harbor/src/core/middlewares/util/error" + "github.com/goharbor/harbor/src/pkg/art" + "github.com/goharbor/harbor/src/pkg/immutabletag/match/rule" + "net/http" +) + +// NewDeleteMFInteceptor .... +func NewDeleteMFInteceptor(mf *util.ManifestInfo) interceptor.Interceptor { + return &delmfInterceptor{ + mf: mf, + } +} + +type delmfInterceptor struct { + mf *util.ManifestInfo +} + +// HandleRequest ... +func (dmf *delmfInterceptor) HandleRequest(req *http.Request) (err error) { + + artifactQuery := &models.ArtifactQuery{ + Digest: dmf.mf.Digest, + } + var afs []*models.Artifact + afs, err = dao.ListArtifacts(artifactQuery) + if err != nil { + log.Error(err) + return + } + if len(afs) == 0 { + return + } + + for _, af := range afs { + _, repoName := common_util.ParseRepository(dmf.mf.Repository) + var matched bool + matched, err = rule.NewRuleMatcher(dmf.mf.ProjectID).Match(art.Candidate{ + Repository: repoName, + Tag: af.Tag, + NamespaceID: dmf.mf.ProjectID, + }) + if err != nil { + log.Error(err) + return + } + if matched { + return middlerware_err.NewErrImmutable(repoName) + } + } + + return +} + +// HandleRequest ... +func (dmf *delmfInterceptor) HandleResponse(w http.ResponseWriter, r *http.Request) { +} diff --git a/src/core/middlewares/interceptor/immutable/pushmf.go b/src/core/middlewares/interceptor/immutable/pushmf.go new file mode 100644 index 000000000..307579bd5 --- /dev/null +++ b/src/core/middlewares/interceptor/immutable/pushmf.go @@ -0,0 +1,65 @@ +package immutable + +import ( + "github.com/goharbor/harbor/src/common/dao" + "github.com/goharbor/harbor/src/common/models" + common_util "github.com/goharbor/harbor/src/common/utils" + "github.com/goharbor/harbor/src/common/utils/log" + "github.com/goharbor/harbor/src/core/middlewares/interceptor" + "github.com/goharbor/harbor/src/core/middlewares/util" + middlerware_err "github.com/goharbor/harbor/src/core/middlewares/util/error" + "github.com/goharbor/harbor/src/pkg/art" + "github.com/goharbor/harbor/src/pkg/immutabletag/match/rule" + "net/http" +) + +// NewPushMFInteceptor .... +func NewPushMFInteceptor(mf *util.ManifestInfo) interceptor.Interceptor { + return &pushmfInterceptor{ + mf: mf, + } +} + +type pushmfInterceptor struct { + mf *util.ManifestInfo +} + +// HandleRequest ... +func (pmf *pushmfInterceptor) HandleRequest(req *http.Request) (err error) { + + _, repoName := common_util.ParseRepository(pmf.mf.Repository) + var matched bool + matched, err = rule.NewRuleMatcher(pmf.mf.ProjectID).Match(art.Candidate{ + Repository: repoName, + Tag: pmf.mf.Tag, + NamespaceID: pmf.mf.ProjectID, + }) + if err != nil { + log.Error(err) + return + } + if !matched { + return + } + + artifactQuery := &models.ArtifactQuery{ + PID: pmf.mf.ProjectID, + Repo: pmf.mf.Repository, + Tag: pmf.mf.Tag, + } + var afs []*models.Artifact + afs, err = dao.ListArtifacts(artifactQuery) + if err != nil { + log.Error(err) + return + } + if len(afs) == 0 { + return + } + + return middlerware_err.NewErrImmutable(repoName) +} + +// HandleRequest ... +func (pmf *pushmfInterceptor) HandleResponse(w http.ResponseWriter, r *http.Request) { +} diff --git a/src/core/middlewares/util/error/immutable.go b/src/core/middlewares/util/error/immutable.go new file mode 100644 index 000000000..a7789300d --- /dev/null +++ b/src/core/middlewares/util/error/immutable.go @@ -0,0 +1,20 @@ +package error + +import ( + "fmt" +) + +// ErrImmutable ... +type ErrImmutable struct { + repo string +} + +// Error ... +func (ei ErrImmutable) Error() string { + return fmt.Sprintf("Failed to process request, due to immutable. '%s'", ei.repo) +} + +// NewErrImmutable ... +func NewErrImmutable(msg string) ErrImmutable { + return ErrImmutable{repo: msg} +}