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:
parent
86b0a226f3
commit
dbb1c3519f
8 changed files with 118 additions and 7 deletions
|
@ -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())
|
||||||
|
|
|
@ -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()),
|
||||||
|
|
|
@ -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)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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))
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue