fix(storage): deleting manifests with identical digests (#951)

Suppose we push two identical manifests (sharing same digest) but with
different tags, then deleting by digest should throw an error otherwise
we end up deleting all image tags (with gc) or dangling references
(without gc)

This behaviour is controlled via Authorization, added a new policy
action named detectManifestsCollision which enables this behaviour

Signed-off-by: Ramkumar Chinchani <rchincha@cisco.com>
Signed-off-by: Petu Eusebiu <peusebiu@cisco.com>

Co-authored-by: Ramkumar Chinchani <rchincha@cisco.com>
This commit is contained in:
peusebiu
2022-11-18 19:35:28 +02:00
committed by GitHub
parent 4e13619dc8
commit 168d21da1e
22 changed files with 507 additions and 141 deletions
+31 -37
View File
@@ -18,11 +18,13 @@ import (
)
const (
// actions.
CREATE = "create"
READ = "read"
UPDATE = "update"
DELETE = "delete"
// method actions.
Create = "create"
Read = "read"
Update = "update"
Delete = "delete"
// behaviour actions.
DetectManifestCollision = "detectManifestCollision"
)
// AccessController authorizes users to act on resources.
@@ -38,19 +40,27 @@ func NewAccessController(config *config.Config) *AccessController {
}
}
// getReadRepos get glob patterns from config file that the user has or doesn't have READ perms.
// getGlobPatterns gets glob patterns from authz config on which <username> has <action> perms.
// used to filter /v2/_catalog repositories based on user rights.
func (ac *AccessController) getReadGlobPatterns(username string) map[string]bool {
func (ac *AccessController) getGlobPatterns(username string, action string) map[string]bool {
globPatterns := make(map[string]bool)
for pattern, policyGroup := range ac.Config.Repositories {
// check default policy
if common.Contains(policyGroup.DefaultPolicy, READ) {
globPatterns[pattern] = true
if username == "" {
// check anonymous policy
if common.Contains(policyGroup.AnonymousPolicy, action) {
globPatterns[pattern] = true
}
} else {
// check default policy (authenticated user)
if common.Contains(policyGroup.DefaultPolicy, action) {
globPatterns[pattern] = true
}
}
// check user based policy
for _, p := range policyGroup.Policies {
if common.Contains(p.Users, username) && common.Contains(p.Actions, READ) {
if common.Contains(p.Users, username) && common.Contains(p.Actions, action) {
globPatterns[pattern] = true
}
}
@@ -102,10 +112,13 @@ func (ac *AccessController) isAdmin(username string) bool {
// getContext builds ac context(allowed to read repos and if user is admin) and returns it.
func (ac *AccessController) getContext(username string, request *http.Request) context.Context {
readGlobPatterns := ac.getReadGlobPatterns(username)
readGlobPatterns := ac.getGlobPatterns(username, Read)
dmcGlobPatterns := ac.getGlobPatterns(username, DetectManifestCollision)
acCtx := localCtx.AccessControlContext{
GlobPatterns: readGlobPatterns,
Username: username,
ReadGlobPatterns: readGlobPatterns,
DmcGlobPatterns: dmcGlobPatterns,
Username: username,
}
if ac.isAdmin(username) {
@@ -149,25 +162,6 @@ func isPermitted(username, action string, policyGroup config.PolicyGroup) bool {
return result
}
// returns either a user has or not rights on 'repository'.
func matchesRepo(globPatterns map[string]bool, repository string) bool {
var longestMatchedPattern string
// because of the longest path matching rule, we need to check all patterns from config
for pattern := range globPatterns {
matched, err := glob.Match(pattern, repository)
if err == nil {
if matched && len(pattern) > len(longestMatchedPattern) {
longestMatchedPattern = pattern
}
}
}
allowed := globPatterns[longestMatchedPattern]
return allowed
}
func AuthzHandler(ctlr *Controller) mux.MiddlewareFunc {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(response http.ResponseWriter, request *http.Request) {
@@ -231,25 +225,25 @@ func AuthzHandler(ctlr *Controller) mux.MiddlewareFunc {
var action string
if request.Method == http.MethodGet || request.Method == http.MethodHead {
action = READ
action = Read
}
if request.Method == http.MethodPut || request.Method == http.MethodPatch || request.Method == http.MethodPost {
// assume user wants to create
action = CREATE
action = Create
// if we get a reference (tag)
if ok {
is := ctlr.StoreController.GetImageStore(resource)
tags, err := is.GetImageTags(resource)
// if repo exists and request's tag exists then action is UPDATE
if err == nil && common.Contains(tags, reference) && reference != "latest" {
action = UPDATE
action = Update
}
}
}
if request.Method == http.MethodDelete {
action = DELETE
action = Delete
}
can := acCtrlr.can(identity, action, resource)