0
Fork 0
mirror of https://github.com/project-zot/zot.git synced 2024-12-16 21:56:37 -05:00

feat(ui): let UI delete manifests if current user has permissions to do so (#2132)

- added a new field 'IsDeletable' for graphql ImageSummary struct.
- apply cors on DeleteManifest route

Signed-off-by: Petu Eusebiu <peusebiu@cisco.com>
This commit is contained in:
peusebiu 2023-12-13 19:06:08 +02:00 committed by GitHub
parent 86b0a226f3
commit dbb1c3519f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 118 additions and 7 deletions

View file

@ -639,7 +639,7 @@ func TestAllowMethodsHeader(t *testing.T) {
// /v2/{name}/manifests/{reference} // /v2/{name}/manifests/{reference}
resp, err = simpleUserClient.Options(baseURL + "/v2/reponame/manifests/" + digest.String()) resp, err = simpleUserClient.Options(baseURL + "/v2/reponame/manifests/" + digest.String())
So(err, ShouldBeNil) 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} // /v2/{name}/referrers/{digest}
resp, err = simpleUserClient.Options(baseURL + "/v2/reponame/referrers/" + digest.String()) resp, err = simpleUserClient.Options(baseURL + "/v2/reponame/referrers/" + digest.String())

View file

@ -134,14 +134,14 @@ func (rh *RouteHandler) SetupRoutes() {
getUIHeadersHandler(rh.c.Config, http.MethodGet, http.MethodOptions)( getUIHeadersHandler(rh.c.Config, http.MethodGet, http.MethodOptions)(
applyCORSHeaders(rh.ListTags))).Methods(http.MethodGet, http.MethodOptions) applyCORSHeaders(rh.ListTags))).Methods(http.MethodGet, http.MethodOptions)
prefixedDistSpecRouter.HandleFunc(fmt.Sprintf("/{name:%s}/manifests/{reference}", zreg.NameRegexp.String()), 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) applyCORSHeaders(rh.CheckManifest))).Methods(http.MethodHead, http.MethodOptions)
prefixedDistSpecRouter.HandleFunc(fmt.Sprintf("/{name:%s}/manifests/{reference}", zreg.NameRegexp.String()), prefixedDistSpecRouter.HandleFunc(fmt.Sprintf("/{name:%s}/manifests/{reference}", zreg.NameRegexp.String()),
applyCORSHeaders(rh.GetManifest)).Methods(http.MethodGet) applyCORSHeaders(rh.GetManifest)).Methods(http.MethodGet)
prefixedDistSpecRouter.HandleFunc(fmt.Sprintf("/{name:%s}/manifests/{reference}", zreg.NameRegexp.String()), prefixedDistSpecRouter.HandleFunc(fmt.Sprintf("/{name:%s}/manifests/{reference}", zreg.NameRegexp.String()),
rh.UpdateManifest).Methods(http.MethodPut) rh.UpdateManifest).Methods(http.MethodPut)
prefixedDistSpecRouter.HandleFunc(fmt.Sprintf("/{name:%s}/manifests/{reference}", zreg.NameRegexp.String()), 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()), prefixedDistSpecRouter.HandleFunc(fmt.Sprintf("/{name:%s}/blobs/{digest}", zreg.NameRegexp.String()),
rh.CheckBlob).Methods(http.MethodHead) rh.CheckBlob).Methods(http.MethodHead)
prefixedDistSpecRouter.HandleFunc(fmt.Sprintf("/{name:%s}/blobs/{digest}", zreg.NameRegexp.String()), prefixedDistSpecRouter.HandleFunc(fmt.Sprintf("/{name:%s}/blobs/{digest}", zreg.NameRegexp.String()),

View file

@ -18,6 +18,7 @@ import (
"zotregistry.io/zot/pkg/log" "zotregistry.io/zot/pkg/log"
"zotregistry.io/zot/pkg/meta/boltdb" "zotregistry.io/zot/pkg/meta/boltdb"
mTypes "zotregistry.io/zot/pkg/meta/types" 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/image-utils"
"zotregistry.io/zot/pkg/test/mocks" "zotregistry.io/zot/pkg/test/mocks"
ociutils "zotregistry.io/zot/pkg/test/oci-utils" ociutils "zotregistry.io/zot/pkg/test/oci-utils"
@ -815,5 +816,35 @@ func TestConvertErrors(t *testing.T) {
) )
So(len(imgSums), ShouldEqual, 0) 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)
})
}) })
} }

View file

@ -17,6 +17,7 @@ import (
"zotregistry.io/zot/pkg/extensions/search/pagination" "zotregistry.io/zot/pkg/extensions/search/pagination"
"zotregistry.io/zot/pkg/log" "zotregistry.io/zot/pkg/log"
mTypes "zotregistry.io/zot/pkg/meta/types" mTypes "zotregistry.io/zot/pkg/meta/types"
reqCtx "zotregistry.io/zot/pkg/requestcontext"
) )
type SkipQGLField struct { type SkipQGLField struct {
@ -136,6 +137,8 @@ func RepoMeta2ExpandedRepoInfo(ctx context.Context, repoMeta mTypes.RepoMeta,
repoName := repoMeta.Name repoName := repoMeta.Name
imageSummaries := make([]*gql_generated.ImageSummary, 0, len(repoMeta.Tags)) imageSummaries := make([]*gql_generated.ImageSummary, 0, len(repoMeta.Tags))
userCanDeleteTag, _ := reqCtx.CanDelete(ctx, repoName)
for tag, descriptor := range repoMeta.Tags { for tag, descriptor := range repoMeta.Tags {
imageMeta := imageMetaMap[descriptor.Digest] imageMeta := imageMetaMap[descriptor.Digest]
@ -147,6 +150,8 @@ func RepoMeta2ExpandedRepoInfo(ctx context.Context, repoMeta mTypes.RepoMeta,
continue continue
} }
imageSummary.IsDeletable = &userCanDeleteTag
updateImageSummaryVulnerabilities(ctx, imageSummary, skip, cveInfo) updateImageSummaryVulnerabilities(ctx, imageSummary, skip, cveInfo)
imageSummaries = append(imageSummaries, imageSummary) imageSummaries = append(imageSummaries, imageSummary)

View file

@ -86,6 +86,7 @@ type ComplexityRoot struct {
Digest func(childComplexity int) int Digest func(childComplexity int) int
Documentation func(childComplexity int) int Documentation func(childComplexity int) int
DownloadCount func(childComplexity int) int DownloadCount func(childComplexity int) int
IsDeletable func(childComplexity int) int
IsSigned func(childComplexity int) int IsSigned func(childComplexity int) int
Labels func(childComplexity int) int Labels func(childComplexity int) int
LastUpdated 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 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": case "ImageSummary.IsSigned":
if e.complexity.ImageSummary.IsSigned == nil { if e.complexity.ImageSummary.IsSigned == nil {
break break
@ -1346,6 +1354,10 @@ type ImageSummary {
Information about objects that reference this image Information about objects that reference this image
""" """
Referrers: [Referrer] 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. 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) return ec.fieldContext_ImageSummary_Vulnerabilities(ctx, field)
case "Referrers": case "Referrers":
return ec.fieldContext_ImageSummary_Referrers(ctx, field) 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) 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 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) { func (ec *executionContext) _ImageVulnerabilitySummary_MaxSeverity(ctx context.Context, field graphql.CollectedField, obj *ImageVulnerabilitySummary) (ret graphql.Marshaler) {
fc, err := ec.fieldContext_ImageVulnerabilitySummary_MaxSeverity(ctx, field) fc, err := ec.fieldContext_ImageVulnerabilitySummary_MaxSeverity(ctx, field)
if err != nil { if err != nil {
@ -5295,6 +5350,8 @@ func (ec *executionContext) fieldContext_PaginatedImagesResult_Results(ctx conte
return ec.fieldContext_ImageSummary_Vulnerabilities(ctx, field) return ec.fieldContext_ImageSummary_Vulnerabilities(ctx, field)
case "Referrers": case "Referrers":
return ec.fieldContext_ImageSummary_Referrers(ctx, field) 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) 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) return ec.fieldContext_ImageSummary_Vulnerabilities(ctx, field)
case "Referrers": case "Referrers":
return ec.fieldContext_ImageSummary_Referrers(ctx, field) 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) 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) return ec.fieldContext_ImageSummary_Vulnerabilities(ctx, field)
case "Referrers": case "Referrers":
return ec.fieldContext_ImageSummary_Referrers(ctx, field) 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) 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) return ec.fieldContext_ImageSummary_Vulnerabilities(ctx, field)
case "Referrers": case "Referrers":
return ec.fieldContext_ImageSummary_Referrers(ctx, field) 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) 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) out.Values[i] = ec._ImageSummary_Vulnerabilities(ctx, field, obj)
case "Referrers": case "Referrers":
out.Values[i] = ec._ImageSummary_Referrers(ctx, field, obj) out.Values[i] = ec._ImageSummary_Referrers(ctx, field, obj)
case "IsDeletable":
out.Values[i] = ec._ImageSummary_IsDeletable(ctx, field, obj)
default: default:
panic("unknown field " + strconv.Quote(field.Name)) panic("unknown field " + strconv.Quote(field.Name))
} }

View file

@ -134,6 +134,8 @@ type ImageSummary struct {
Vulnerabilities *ImageVulnerabilitySummary `json:"Vulnerabilities,omitempty"` Vulnerabilities *ImageVulnerabilitySummary `json:"Vulnerabilities,omitempty"`
// Information about objects that reference this image // Information about objects that reference this image
Referrers []*Referrer `json:"Referrers,omitempty"` 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 // Contains summary of vulnerabilities found in a specific image

View file

@ -200,6 +200,10 @@ type ImageSummary {
Information about objects that reference this image Information about objects that reference this image
""" """
Referrers: [Referrer] 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. Details about a specific version of an image for a certain operating system and architecture.

View file

@ -246,10 +246,14 @@ func RepoIsUserAvailable(ctx context.Context, repoName string) (bool, error) {
return false, err return false, err
} }
// no authn/authz enabled on server return uac.Can(constants.ReadPermission, repoName), nil
if uac == nil { }
return true, 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
} }