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}
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())

View file

@ -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()),

View file

@ -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)
})
})
}

View file

@ -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)

View file

@ -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))
}

View file

@ -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

View file

@ -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.

View file

@ -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
}