diff --git a/pkg/api/controller_test.go b/pkg/api/controller_test.go index ca63430dad..cd94621b57 100644 --- a/pkg/api/controller_test.go +++ b/pkg/api/controller_test.go @@ -639,7 +639,7 @@ func TestAllowMethodsHeader(t *testing.T) { // /v2/{name}/manifests/{reference} resp, err = simpleUserClient.Options(baseURL + "/v2/reponame/manifests/" + digest.String()) So(err, ShouldBeNil) - So(resp.Header().Get("Access-Control-Allow-Methods"), ShouldResemble, "HEAD,GET,OPTIONS") + So(resp.Header().Get("Access-Control-Allow-Methods"), ShouldResemble, "HEAD,GET,DELETE,OPTIONS") // /v2/{name}/referrers/{digest} resp, err = simpleUserClient.Options(baseURL + "/v2/reponame/referrers/" + digest.String()) diff --git a/pkg/api/routes.go b/pkg/api/routes.go index 48a1e5ec1f..b6253ebd8a 100644 --- a/pkg/api/routes.go +++ b/pkg/api/routes.go @@ -134,14 +134,14 @@ func (rh *RouteHandler) SetupRoutes() { getUIHeadersHandler(rh.c.Config, http.MethodGet, http.MethodOptions)( applyCORSHeaders(rh.ListTags))).Methods(http.MethodGet, http.MethodOptions) prefixedDistSpecRouter.HandleFunc(fmt.Sprintf("/{name:%s}/manifests/{reference}", zreg.NameRegexp.String()), - getUIHeadersHandler(rh.c.Config, http.MethodHead, http.MethodGet, http.MethodOptions)( + getUIHeadersHandler(rh.c.Config, http.MethodHead, http.MethodGet, http.MethodDelete, http.MethodOptions)( applyCORSHeaders(rh.CheckManifest))).Methods(http.MethodHead, http.MethodOptions) prefixedDistSpecRouter.HandleFunc(fmt.Sprintf("/{name:%s}/manifests/{reference}", zreg.NameRegexp.String()), applyCORSHeaders(rh.GetManifest)).Methods(http.MethodGet) prefixedDistSpecRouter.HandleFunc(fmt.Sprintf("/{name:%s}/manifests/{reference}", zreg.NameRegexp.String()), rh.UpdateManifest).Methods(http.MethodPut) prefixedDistSpecRouter.HandleFunc(fmt.Sprintf("/{name:%s}/manifests/{reference}", zreg.NameRegexp.String()), - rh.DeleteManifest).Methods(http.MethodDelete) + applyCORSHeaders(rh.DeleteManifest)).Methods(http.MethodDelete) prefixedDistSpecRouter.HandleFunc(fmt.Sprintf("/{name:%s}/blobs/{digest}", zreg.NameRegexp.String()), rh.CheckBlob).Methods(http.MethodHead) prefixedDistSpecRouter.HandleFunc(fmt.Sprintf("/{name:%s}/blobs/{digest}", zreg.NameRegexp.String()), diff --git a/pkg/extensions/search/convert/convert_test.go b/pkg/extensions/search/convert/convert_test.go index 92568cfabb..cb647ef96c 100644 --- a/pkg/extensions/search/convert/convert_test.go +++ b/pkg/extensions/search/convert/convert_test.go @@ -18,6 +18,7 @@ import ( "zotregistry.io/zot/pkg/log" "zotregistry.io/zot/pkg/meta/boltdb" mTypes "zotregistry.io/zot/pkg/meta/types" + reqCtx "zotregistry.io/zot/pkg/requestcontext" . "zotregistry.io/zot/pkg/test/image-utils" "zotregistry.io/zot/pkg/test/mocks" ociutils "zotregistry.io/zot/pkg/test/oci-utils" @@ -815,5 +816,35 @@ func TestConvertErrors(t *testing.T) { ) So(len(imgSums), ShouldEqual, 0) }) + + Convey("RepoMeta2ExpandedRepoInfo - bad ctx value", func() { + uacKey := reqCtx.GetContextKey() + ctx := context.WithValue(ctx, uacKey, "bad context") + + _, imgSums := convert.RepoMeta2ExpandedRepoInfo(ctx, + mTypes.RepoMeta{}, + map[string]mTypes.ImageMeta{ + "digest": {}, + }, + convert.SkipQGLField{}, nil, + log, + ) + So(len(imgSums), ShouldEqual, 0) + }) + + Convey("RepoMeta2ExpandedRepoInfo - nil ctx value", func() { + uacKey := reqCtx.GetContextKey() + ctx := context.WithValue(ctx, uacKey, nil) + + _, imgSums := convert.RepoMeta2ExpandedRepoInfo(ctx, + mTypes.RepoMeta{}, + map[string]mTypes.ImageMeta{ + "digest": {}, + }, + convert.SkipQGLField{}, nil, + log, + ) + So(len(imgSums), ShouldEqual, 0) + }) }) } diff --git a/pkg/extensions/search/convert/metadb.go b/pkg/extensions/search/convert/metadb.go index 47fd65803d..75ed7b475a 100644 --- a/pkg/extensions/search/convert/metadb.go +++ b/pkg/extensions/search/convert/metadb.go @@ -17,6 +17,7 @@ import ( "zotregistry.io/zot/pkg/extensions/search/pagination" "zotregistry.io/zot/pkg/log" mTypes "zotregistry.io/zot/pkg/meta/types" + reqCtx "zotregistry.io/zot/pkg/requestcontext" ) type SkipQGLField struct { @@ -136,6 +137,8 @@ func RepoMeta2ExpandedRepoInfo(ctx context.Context, repoMeta mTypes.RepoMeta, repoName := repoMeta.Name imageSummaries := make([]*gql_generated.ImageSummary, 0, len(repoMeta.Tags)) + userCanDeleteTag, _ := reqCtx.CanDelete(ctx, repoName) + for tag, descriptor := range repoMeta.Tags { imageMeta := imageMetaMap[descriptor.Digest] @@ -147,6 +150,8 @@ func RepoMeta2ExpandedRepoInfo(ctx context.Context, repoMeta mTypes.RepoMeta, continue } + imageSummary.IsDeletable = &userCanDeleteTag + updateImageSummaryVulnerabilities(ctx, imageSummary, skip, cveInfo) imageSummaries = append(imageSummaries, imageSummary) diff --git a/pkg/extensions/search/gql_generated/generated.go b/pkg/extensions/search/gql_generated/generated.go index c61981060b..ef3258bd1a 100644 --- a/pkg/extensions/search/gql_generated/generated.go +++ b/pkg/extensions/search/gql_generated/generated.go @@ -86,6 +86,7 @@ type ComplexityRoot struct { Digest func(childComplexity int) int Documentation func(childComplexity int) int DownloadCount func(childComplexity int) int + IsDeletable func(childComplexity int) int IsSigned func(childComplexity int) int Labels func(childComplexity int) int LastUpdated func(childComplexity int) int @@ -422,6 +423,13 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.ImageSummary.DownloadCount(childComplexity), true + case "ImageSummary.IsDeletable": + if e.complexity.ImageSummary.IsDeletable == nil { + break + } + + return e.complexity.ImageSummary.IsDeletable(childComplexity), true + case "ImageSummary.IsSigned": if e.complexity.ImageSummary.IsSigned == nil { break @@ -1346,6 +1354,10 @@ type ImageSummary { Information about objects that reference this image """ Referrers: [Referrer] + """ + True if current user has delete permission on this tag. + """ + IsDeletable: Boolean } """ Details about a specific version of an image for a certain operating system and architecture. @@ -2919,6 +2931,8 @@ func (ec *executionContext) fieldContext_GlobalSearchResult_Images(ctx context.C return ec.fieldContext_ImageSummary_Vulnerabilities(ctx, field) case "Referrers": return ec.fieldContext_ImageSummary_Referrers(ctx, field) + case "IsDeletable": + return ec.fieldContext_ImageSummary_IsDeletable(ctx, field) } return nil, fmt.Errorf("no field named %q was found under type ImageSummary", field.Name) }, @@ -4117,6 +4131,47 @@ func (ec *executionContext) fieldContext_ImageSummary_Referrers(ctx context.Cont return fc, nil } +func (ec *executionContext) _ImageSummary_IsDeletable(ctx context.Context, field graphql.CollectedField, obj *ImageSummary) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_ImageSummary_IsDeletable(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return obj.IsDeletable, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + return graphql.Null + } + res := resTmp.(*bool) + fc.Result = res + return ec.marshalOBoolean2ᚖbool(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_ImageSummary_IsDeletable(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "ImageSummary", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return nil, errors.New("field of type Boolean does not have child fields") + }, + } + return fc, nil +} + func (ec *executionContext) _ImageVulnerabilitySummary_MaxSeverity(ctx context.Context, field graphql.CollectedField, obj *ImageVulnerabilitySummary) (ret graphql.Marshaler) { fc, err := ec.fieldContext_ImageVulnerabilitySummary_MaxSeverity(ctx, field) if err != nil { @@ -5295,6 +5350,8 @@ func (ec *executionContext) fieldContext_PaginatedImagesResult_Results(ctx conte return ec.fieldContext_ImageSummary_Vulnerabilities(ctx, field) case "Referrers": return ec.fieldContext_ImageSummary_Referrers(ctx, field) + case "IsDeletable": + return ec.fieldContext_ImageSummary_IsDeletable(ctx, field) } return nil, fmt.Errorf("no field named %q was found under type ImageSummary", field.Name) }, @@ -6194,6 +6251,8 @@ func (ec *executionContext) fieldContext_Query_Image(ctx context.Context, field return ec.fieldContext_ImageSummary_Vulnerabilities(ctx, field) case "Referrers": return ec.fieldContext_ImageSummary_Referrers(ctx, field) + case "IsDeletable": + return ec.fieldContext_ImageSummary_IsDeletable(ctx, field) } return nil, fmt.Errorf("no field named %q was found under type ImageSummary", field.Name) }, @@ -6820,6 +6879,8 @@ func (ec *executionContext) fieldContext_RepoInfo_Images(ctx context.Context, fi return ec.fieldContext_ImageSummary_Vulnerabilities(ctx, field) case "Referrers": return ec.fieldContext_ImageSummary_Referrers(ctx, field) + case "IsDeletable": + return ec.fieldContext_ImageSummary_IsDeletable(ctx, field) } return nil, fmt.Errorf("no field named %q was found under type ImageSummary", field.Name) }, @@ -7179,6 +7240,8 @@ func (ec *executionContext) fieldContext_RepoSummary_NewestImage(ctx context.Con return ec.fieldContext_ImageSummary_Vulnerabilities(ctx, field) case "Referrers": return ec.fieldContext_ImageSummary_Referrers(ctx, field) + case "IsDeletable": + return ec.fieldContext_ImageSummary_IsDeletable(ctx, field) } return nil, fmt.Errorf("no field named %q was found under type ImageSummary", field.Name) }, @@ -9652,6 +9715,8 @@ func (ec *executionContext) _ImageSummary(ctx context.Context, sel ast.Selection out.Values[i] = ec._ImageSummary_Vulnerabilities(ctx, field, obj) case "Referrers": out.Values[i] = ec._ImageSummary_Referrers(ctx, field, obj) + case "IsDeletable": + out.Values[i] = ec._ImageSummary_IsDeletable(ctx, field, obj) default: panic("unknown field " + strconv.Quote(field.Name)) } diff --git a/pkg/extensions/search/gql_generated/models_gen.go b/pkg/extensions/search/gql_generated/models_gen.go index c889595e31..9e654c5365 100644 --- a/pkg/extensions/search/gql_generated/models_gen.go +++ b/pkg/extensions/search/gql_generated/models_gen.go @@ -134,6 +134,8 @@ type ImageSummary struct { Vulnerabilities *ImageVulnerabilitySummary `json:"Vulnerabilities,omitempty"` // Information about objects that reference this image Referrers []*Referrer `json:"Referrers,omitempty"` + // True if current user has delete permission on this tag. + IsDeletable *bool `json:"IsDeletable,omitempty"` } // Contains summary of vulnerabilities found in a specific image diff --git a/pkg/extensions/search/schema.graphql b/pkg/extensions/search/schema.graphql index 19175c626c..c1b50453fe 100644 --- a/pkg/extensions/search/schema.graphql +++ b/pkg/extensions/search/schema.graphql @@ -200,6 +200,10 @@ type ImageSummary { Information about objects that reference this image """ Referrers: [Referrer] + """ + True if current user has delete permission on this tag. + """ + IsDeletable: Boolean } """ Details about a specific version of an image for a certain operating system and architecture. diff --git a/pkg/requestcontext/user_access_control.go b/pkg/requestcontext/user_access_control.go index fee4ec37ba..fffc68993b 100644 --- a/pkg/requestcontext/user_access_control.go +++ b/pkg/requestcontext/user_access_control.go @@ -246,10 +246,14 @@ func RepoIsUserAvailable(ctx context.Context, repoName string) (bool, error) { return false, err } - // no authn/authz enabled on server - if uac == nil { - return true, nil + return uac.Can(constants.ReadPermission, repoName), nil +} + +func CanDelete(ctx context.Context, repoName string) (bool, error) { + uac, err := UserAcFromContext(ctx) + if err != nil { + return false, err } - return uac.Can("read", repoName), nil + return uac.Can(constants.DeletePermission, repoName), nil }