mirror of
https://github.com/goharbor/harbor.git
synced 2024-12-19 23:28:20 +01:00
add Middleware policycheck into v2 handler
1, add content trust middleware in new v2 handler 2, add vulnable middlware in new v2 hanlder Signed-off-by: wang yan <wangyan@vmware.com>
This commit is contained in:
parent
302b210938
commit
9db84f2880
@ -92,6 +92,8 @@ const (
|
|||||||
GeneralCode = "UNKNOWN"
|
GeneralCode = "UNKNOWN"
|
||||||
// DENIED it's used by middleware(readonly, vul and content trust) and returned to docker client to index the request is denied.
|
// DENIED it's used by middleware(readonly, vul and content trust) and returned to docker client to index the request is denied.
|
||||||
DENIED = "DENIED"
|
DENIED = "DENIED"
|
||||||
|
// PROJECTPOLICYVIOLATION ...
|
||||||
|
PROJECTPOLICYVIOLATION = "PROJECTPOLICYVIOLATION"
|
||||||
// ViolateForeignKeyConstraintCode is the error code for violating foreign key constraint error
|
// ViolateForeignKeyConstraintCode is the error code for violating foreign key constraint error
|
||||||
ViolateForeignKeyConstraintCode = "VIOLATE_FOREIGN_KEY_CONSTRAINT"
|
ViolateForeignKeyConstraintCode = "VIOLATE_FOREIGN_KEY_CONSTRAINT"
|
||||||
)
|
)
|
||||||
|
99
src/server/middleware/contenttrust/contenttrust.go
Normal file
99
src/server/middleware/contenttrust/contenttrust.go
Normal file
@ -0,0 +1,99 @@
|
|||||||
|
package contenttrust
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/goharbor/harbor/src/common/utils/log"
|
||||||
|
"github.com/goharbor/harbor/src/common/utils/notary"
|
||||||
|
"github.com/goharbor/harbor/src/core/config"
|
||||||
|
"github.com/goharbor/harbor/src/core/middlewares/util"
|
||||||
|
internal_errors "github.com/goharbor/harbor/src/internal/error"
|
||||||
|
"github.com/goharbor/harbor/src/server/middleware"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
)
|
||||||
|
|
||||||
|
// NotaryEndpoint ...
|
||||||
|
var NotaryEndpoint = ""
|
||||||
|
|
||||||
|
// Middleware handle docker pull content trust check
|
||||||
|
func Middleware() func(http.Handler) http.Handler {
|
||||||
|
return func(next http.Handler) http.Handler {
|
||||||
|
return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
|
||||||
|
doContentTrustCheck, mf := validate(req)
|
||||||
|
if !doContentTrustCheck {
|
||||||
|
next.ServeHTTP(rw, req)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
rec := httptest.NewRecorder()
|
||||||
|
next.ServeHTTP(rec, req)
|
||||||
|
if rec.Result().StatusCode == http.StatusOK {
|
||||||
|
match, err := matchNotaryDigest(mf)
|
||||||
|
if err != nil {
|
||||||
|
pkgE := internal_errors.New(nil).WithCode(internal_errors.PROJECTPOLICYVIOLATION).WithMessage("Failed in communication with Notary please check the log")
|
||||||
|
msg := internal_errors.NewErrs(pkgE).Error()
|
||||||
|
http.Error(rw, msg, http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !match {
|
||||||
|
log.Debugf("digest mismatch, failing the response.")
|
||||||
|
pkgE := internal_errors.New(nil).WithCode(internal_errors.PROJECTPOLICYVIOLATION).WithMessage("The image is not signed in Notary.")
|
||||||
|
msg := internal_errors.NewErrs(pkgE).Error()
|
||||||
|
http.Error(rw, msg, http.StatusPreconditionFailed)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
middleware.CopyResp(rec, rw)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func validate(req *http.Request) (bool, *middleware.ManifestInfo) {
|
||||||
|
mf, ok := middleware.ManifestInfoFromContext(req.Context())
|
||||||
|
if !ok {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
_, err := mf.ManifestExists(req.Context())
|
||||||
|
if err != nil {
|
||||||
|
return false, mf
|
||||||
|
}
|
||||||
|
if scannerPull, ok := middleware.ScannerPullFromContext(req.Context()); ok && scannerPull {
|
||||||
|
return false, mf
|
||||||
|
}
|
||||||
|
if !middleware.GetPolicyChecker().ContentTrustEnabled(mf.ProjectName) {
|
||||||
|
return false, mf
|
||||||
|
}
|
||||||
|
return true, mf
|
||||||
|
}
|
||||||
|
|
||||||
|
func matchNotaryDigest(mf *middleware.ManifestInfo) (bool, error) {
|
||||||
|
if NotaryEndpoint == "" {
|
||||||
|
NotaryEndpoint = config.InternalNotaryEndpoint()
|
||||||
|
}
|
||||||
|
targets, err := notary.GetInternalTargets(NotaryEndpoint, util.TokenUsername, mf.Repository)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
for _, t := range targets {
|
||||||
|
if mf.Digest != "" {
|
||||||
|
d, err := notary.DigestFromTarget(t)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
if mf.Digest == d {
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if t.Tag == mf.Tag {
|
||||||
|
log.Debugf("found reference: %s in notary, try to match digest.", mf.Tag)
|
||||||
|
d, err := notary.DigestFromTarget(t)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
if mf.Digest == d {
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
log.Debugf("image: %#v, not found in notary", mf)
|
||||||
|
return false, nil
|
||||||
|
}
|
1
src/server/middleware/contenttrust/contenttrust_test.go
Normal file
1
src/server/middleware/contenttrust/contenttrust_test.go
Normal file
@ -0,0 +1 @@
|
|||||||
|
package contenttrust
|
@ -49,8 +49,9 @@ func parseManifestInfoFromPath(req *http.Request) (*middleware.ManifestInfo, err
|
|||||||
}
|
}
|
||||||
|
|
||||||
info := &middleware.ManifestInfo{
|
info := &middleware.ManifestInfo{
|
||||||
ProjectID: project.ProjectID,
|
ProjectID: project.ProjectID,
|
||||||
Repository: repository,
|
ProjectName: projectName,
|
||||||
|
Repository: repository,
|
||||||
}
|
}
|
||||||
|
|
||||||
dgt, err := digest.Parse(reference)
|
dgt, err := digest.Parse(reference)
|
||||||
|
@ -2,6 +2,20 @@ package middleware
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"github.com/goharbor/harbor/src/common/models"
|
||||||
|
"github.com/goharbor/harbor/src/common/utils/log"
|
||||||
|
"github.com/goharbor/harbor/src/core/config"
|
||||||
|
"github.com/goharbor/harbor/src/core/promgr"
|
||||||
|
"github.com/goharbor/harbor/src/pkg/artifact"
|
||||||
|
"github.com/goharbor/harbor/src/pkg/q"
|
||||||
|
"github.com/goharbor/harbor/src/pkg/repository"
|
||||||
|
"github.com/goharbor/harbor/src/pkg/scan/vuln"
|
||||||
|
"github.com/goharbor/harbor/src/pkg/scan/whitelist"
|
||||||
|
"github.com/goharbor/harbor/src/pkg/tag"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"sync"
|
||||||
)
|
)
|
||||||
|
|
||||||
type contextKey string
|
type contextKey string
|
||||||
@ -15,10 +29,67 @@ const (
|
|||||||
|
|
||||||
// ManifestInfo ...
|
// ManifestInfo ...
|
||||||
type ManifestInfo struct {
|
type ManifestInfo struct {
|
||||||
ProjectID int64
|
ProjectID int64
|
||||||
Repository string
|
ProjectName string
|
||||||
Tag string
|
Repository string
|
||||||
Digest string
|
Tag string
|
||||||
|
Digest string
|
||||||
|
|
||||||
|
manifestExist bool
|
||||||
|
manifestExistErr error
|
||||||
|
manifestExistOnce sync.Once
|
||||||
|
}
|
||||||
|
|
||||||
|
// ManifestExists ...
|
||||||
|
func (info *ManifestInfo) ManifestExists(ctx context.Context) (bool, error) {
|
||||||
|
info.manifestExistOnce.Do(func() {
|
||||||
|
|
||||||
|
// ToDo: use the artifact controller method
|
||||||
|
total, repos, err := repository.Mgr.List(ctx, &q.Query{
|
||||||
|
Keywords: map[string]interface{}{
|
||||||
|
"Name": info.Repository,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
info.manifestExistErr = err
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if total == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
total, tags, err := tag.Mgr.List(ctx, &q.Query{
|
||||||
|
Keywords: map[string]interface{}{
|
||||||
|
"Name": info.Tag,
|
||||||
|
"RepositoryID": repos[0].RepositoryID,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
info.manifestExistErr = err
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if total == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
total, afs, err := artifact.Mgr.List(ctx, &q.Query{
|
||||||
|
Keywords: map[string]interface{}{
|
||||||
|
"ID": tags[0].ArtifactID,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
info.manifestExistErr = err
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if total == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
info.Digest = afs[0].Digest
|
||||||
|
info.manifestExist = total > 0
|
||||||
|
})
|
||||||
|
|
||||||
|
return info.manifestExist, info.manifestExistErr
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewManifestInfoContext returns context with manifest info
|
// NewManifestInfoContext returns context with manifest info
|
||||||
@ -36,3 +107,89 @@ func ManifestInfoFromContext(ctx context.Context) (*ManifestInfo, bool) {
|
|||||||
func NewScannerPullContext(ctx context.Context, scannerPull bool) context.Context {
|
func NewScannerPullContext(ctx context.Context, scannerPull bool) context.Context {
|
||||||
return context.WithValue(ctx, ScannerPullCtxKey, scannerPull)
|
return context.WithValue(ctx, ScannerPullCtxKey, scannerPull)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ScannerPullFromContext returns whether to bypass policy check
|
||||||
|
func ScannerPullFromContext(ctx context.Context) (bool, bool) {
|
||||||
|
info, ok := ctx.Value(ScannerPullCtxKey).(bool)
|
||||||
|
return info, ok
|
||||||
|
}
|
||||||
|
|
||||||
|
// CopyResp ...
|
||||||
|
func CopyResp(rec *httptest.ResponseRecorder, rw http.ResponseWriter) {
|
||||||
|
for k, v := range rec.Header() {
|
||||||
|
rw.Header()[k] = v
|
||||||
|
}
|
||||||
|
rw.WriteHeader(rec.Result().StatusCode)
|
||||||
|
rw.Write(rec.Body.Bytes())
|
||||||
|
}
|
||||||
|
|
||||||
|
// PolicyChecker checks the policy of a project by project name, to determine if it's needed to check the image's status under this project.
|
||||||
|
type PolicyChecker interface {
|
||||||
|
// contentTrustEnabled returns whether a project has enabled content trust.
|
||||||
|
ContentTrustEnabled(name string) bool
|
||||||
|
// vulnerablePolicy returns whether a project has enabled vulnerable, and the project's severity.
|
||||||
|
VulnerablePolicy(name string) (bool, vuln.Severity, models.CVEWhitelist)
|
||||||
|
}
|
||||||
|
|
||||||
|
// PmsPolicyChecker ...
|
||||||
|
type PmsPolicyChecker struct {
|
||||||
|
pm promgr.ProjectManager
|
||||||
|
}
|
||||||
|
|
||||||
|
// ContentTrustEnabled ...
|
||||||
|
func (pc PmsPolicyChecker) ContentTrustEnabled(name string) bool {
|
||||||
|
project, err := pc.pm.Get(name)
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("Unexpected error when getting the project, error: %v", err)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if project == nil {
|
||||||
|
log.Debugf("project %s not found", name)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return project.ContentTrustEnabled()
|
||||||
|
}
|
||||||
|
|
||||||
|
// VulnerablePolicy ...
|
||||||
|
func (pc PmsPolicyChecker) VulnerablePolicy(name string) (bool, vuln.Severity, models.CVEWhitelist) {
|
||||||
|
project, err := pc.pm.Get(name)
|
||||||
|
wl := models.CVEWhitelist{}
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("Unexpected error when getting the project, error: %v", err)
|
||||||
|
return true, vuln.Unknown, wl
|
||||||
|
}
|
||||||
|
|
||||||
|
mgr := whitelist.NewDefaultManager()
|
||||||
|
if project.ReuseSysCVEWhitelist() {
|
||||||
|
w, err := mgr.GetSys()
|
||||||
|
if err != nil {
|
||||||
|
log.Error(errors.Wrap(err, "policy checker: vulnerable policy"))
|
||||||
|
} else {
|
||||||
|
wl = *w
|
||||||
|
|
||||||
|
// Use the real project ID
|
||||||
|
wl.ProjectID = project.ProjectID
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
w, err := mgr.Get(project.ProjectID)
|
||||||
|
if err != nil {
|
||||||
|
log.Error(errors.Wrap(err, "policy checker: vulnerable policy"))
|
||||||
|
} else {
|
||||||
|
wl = *w
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return project.VulPrevented(), vuln.ParseSeverityVersion3(project.Severity()), wl
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewPMSPolicyChecker returns an instance of an pmsPolicyChecker
|
||||||
|
func NewPMSPolicyChecker(pm promgr.ProjectManager) PolicyChecker {
|
||||||
|
return &PmsPolicyChecker{
|
||||||
|
pm: pm,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetPolicyChecker ...
|
||||||
|
func GetPolicyChecker() PolicyChecker {
|
||||||
|
return NewPMSPolicyChecker(config.GlobalProjectMgr)
|
||||||
|
}
|
||||||
|
119
src/server/middleware/vulnerable/vulnerable.go
Normal file
119
src/server/middleware/vulnerable/vulnerable.go
Normal file
@ -0,0 +1,119 @@
|
|||||||
|
package vulnerable
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/goharbor/harbor/src/common/models"
|
||||||
|
"github.com/goharbor/harbor/src/common/utils/log"
|
||||||
|
internal_errors "github.com/goharbor/harbor/src/internal/error"
|
||||||
|
sc "github.com/goharbor/harbor/src/pkg/scan/api/scan"
|
||||||
|
"github.com/goharbor/harbor/src/pkg/scan/report"
|
||||||
|
v1 "github.com/goharbor/harbor/src/pkg/scan/rest/v1"
|
||||||
|
"github.com/goharbor/harbor/src/pkg/scan/vuln"
|
||||||
|
"github.com/goharbor/harbor/src/server/middleware"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Middleware handle docker pull vulnerable check
|
||||||
|
func Middleware() func(http.Handler) http.Handler {
|
||||||
|
return func(next http.Handler) http.Handler {
|
||||||
|
return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
|
||||||
|
doVulCheck, img, projectVulnerableSeverity, wl := validate(req)
|
||||||
|
if !doVulCheck {
|
||||||
|
next.ServeHTTP(rw, req)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
rec := httptest.NewRecorder()
|
||||||
|
next.ServeHTTP(rec, req)
|
||||||
|
// only enable vul policy check the response 200
|
||||||
|
if rec.Result().StatusCode == http.StatusOK {
|
||||||
|
// Invalid project ID
|
||||||
|
if wl.ProjectID == 0 {
|
||||||
|
err := errors.Errorf("project verification error: project %s", img.ProjectName)
|
||||||
|
sendError(err, rw)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the vulnerability summary
|
||||||
|
artifact := &v1.Artifact{
|
||||||
|
NamespaceID: wl.ProjectID,
|
||||||
|
Repository: img.Repository,
|
||||||
|
Tag: img.Tag,
|
||||||
|
Digest: img.Digest,
|
||||||
|
MimeType: v1.MimeTypeDockerArtifact,
|
||||||
|
}
|
||||||
|
|
||||||
|
cve := report.CVESet(wl.CVESet())
|
||||||
|
summaries, err := sc.DefaultController.GetSummary(
|
||||||
|
artifact,
|
||||||
|
[]string{v1.MimeTypeNativeReport},
|
||||||
|
report.WithCVEWhitelist(&cve),
|
||||||
|
)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
err = errors.Wrap(err, "middleware: vulnerable handler")
|
||||||
|
sendError(err, rw)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
rawSummary, ok := summaries[v1.MimeTypeNativeReport]
|
||||||
|
// No report yet?
|
||||||
|
if !ok {
|
||||||
|
err = errors.Errorf("no scan report existing for the artifact: %s:%s@%s", img.Repository, img.Tag, img.Digest)
|
||||||
|
sendError(err, rw)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
summary := rawSummary.(*vuln.NativeReportSummary)
|
||||||
|
|
||||||
|
// Do judgement
|
||||||
|
if summary.Severity.Code() >= projectVulnerableSeverity.Code() {
|
||||||
|
err = errors.Errorf("current image with '%q vulnerable' cannot be pulled due to configured policy in 'Prevent images with vulnerability severity of %q from running.' "+
|
||||||
|
"Please contact your project administrator for help'", summary.Severity, projectVulnerableSeverity)
|
||||||
|
sendError(err, rw)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Print scannerPull CVE list
|
||||||
|
if len(summary.CVEBypassed) > 0 {
|
||||||
|
for _, cve := range summary.CVEBypassed {
|
||||||
|
log.Infof("Vulnerable policy check: scannerPull CVE %s", cve)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
middleware.CopyResp(rec, rw)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func validate(req *http.Request) (bool, *middleware.ManifestInfo, vuln.Severity, models.CVEWhitelist) {
|
||||||
|
var vs vuln.Severity
|
||||||
|
var wl models.CVEWhitelist
|
||||||
|
var mf *middleware.ManifestInfo
|
||||||
|
mf, ok := middleware.ManifestInfoFromContext(req.Context())
|
||||||
|
if !ok {
|
||||||
|
return false, nil, vs, wl
|
||||||
|
}
|
||||||
|
|
||||||
|
exist, err := mf.ManifestExists(req.Context())
|
||||||
|
if err != nil || !exist {
|
||||||
|
return false, nil, vs, wl
|
||||||
|
}
|
||||||
|
|
||||||
|
if scannerPull, ok := middleware.ScannerPullFromContext(req.Context()); ok && scannerPull {
|
||||||
|
return false, mf, vs, wl
|
||||||
|
}
|
||||||
|
// Is vulnerable policy set?
|
||||||
|
projectVulnerableEnabled, projectVulnerableSeverity, wl := middleware.GetPolicyChecker().VulnerablePolicy(mf.ProjectName)
|
||||||
|
if !projectVulnerableEnabled {
|
||||||
|
return false, mf, vs, wl
|
||||||
|
}
|
||||||
|
return true, mf, projectVulnerableSeverity, wl
|
||||||
|
}
|
||||||
|
|
||||||
|
func sendError(err error, rw http.ResponseWriter) {
|
||||||
|
log.Error(err)
|
||||||
|
pkgE := internal_errors.New(err).WithCode(internal_errors.PROJECTPOLICYVIOLATION)
|
||||||
|
msg := internal_errors.NewErrs(pkgE).Error()
|
||||||
|
http.Error(rw, msg, http.StatusPreconditionFailed)
|
||||||
|
}
|
1
src/server/middleware/vulnerable/vulnerable_test.go
Normal file
1
src/server/middleware/vulnerable/vulnerable_test.go
Normal file
@ -0,0 +1 @@
|
|||||||
|
package vulnerable
|
@ -15,6 +15,8 @@
|
|||||||
package registry
|
package registry
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"github.com/goharbor/harbor/src/server/middleware/contenttrust"
|
||||||
|
"github.com/goharbor/harbor/src/server/middleware/vulnerable"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httputil"
|
"net/http/httputil"
|
||||||
"net/url"
|
"net/url"
|
||||||
@ -53,7 +55,7 @@ func New(url *url.URL) http.Handler {
|
|||||||
// handle manifest
|
// handle manifest
|
||||||
// TODO maybe we should split it into several sub routers based on the method
|
// TODO maybe we should split it into several sub routers based on the method
|
||||||
manifestRouter := rootRouter.Path("/v2/{name:.*}/manifests/{reference}").Subrouter()
|
manifestRouter := rootRouter.Path("/v2/{name:.*}/manifests/{reference}").Subrouter()
|
||||||
manifestRouter.NewRoute().Methods(http.MethodGet).Handler(middleware.WithMiddlewares(manifest.NewHandler(project.Mgr, proxy), manifestinfo.Middleware(), regtoken.Middleware()))
|
manifestRouter.NewRoute().Methods(http.MethodGet).Handler(middleware.WithMiddlewares(manifest.NewHandler(project.Mgr, proxy), manifestinfo.Middleware(), regtoken.Middleware(), contenttrust.Middleware(), vulnerable.Middleware()))
|
||||||
manifestRouter.NewRoute().Methods(http.MethodHead).Handler(manifest.NewHandler(project.Mgr, proxy))
|
manifestRouter.NewRoute().Methods(http.MethodHead).Handler(manifest.NewHandler(project.Mgr, proxy))
|
||||||
manifestRouter.NewRoute().Methods(http.MethodDelete).Handler(middleware.WithMiddlewares(manifest.NewHandler(project.Mgr, proxy), readonly.Middleware(), manifestinfo.Middleware(), immutable.MiddlewareDelete()))
|
manifestRouter.NewRoute().Methods(http.MethodDelete).Handler(middleware.WithMiddlewares(manifest.NewHandler(project.Mgr, proxy), readonly.Middleware(), manifestinfo.Middleware(), immutable.MiddlewareDelete()))
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user