Update registry server to support repository class
Use whitelist of allowed repository classes to enforce. By default all repository classes are allowed. Add authorized resources to context after authorization. Signed-off-by: Derek McGowan <derek@mcgstyle.net> (github: dmcgowan)master
							parent
							
								
									61e65ecd9d
								
							
						
					
					
						commit
						e02278f22a
					
				|  | @ -203,6 +203,19 @@ type Configuration struct { | ||||||
| 			} `yaml:"urls,omitempty"` | 			} `yaml:"urls,omitempty"` | ||||||
| 		} `yaml:"manifests,omitempty"` | 		} `yaml:"manifests,omitempty"` | ||||||
| 	} `yaml:"validation,omitempty"` | 	} `yaml:"validation,omitempty"` | ||||||
|  | 
 | ||||||
|  | 	// Policy configures registry policy options.
 | ||||||
|  | 	Policy struct { | ||||||
|  | 		// Repository configures policies for repositories
 | ||||||
|  | 		Repository struct { | ||||||
|  | 			// Classes is a list of repository classes which the
 | ||||||
|  | 			// registry allows content for. This class is matched
 | ||||||
|  | 			// against the configuration media type inside uploaded
 | ||||||
|  | 			// manifests. When non-empty, the registry will enforce
 | ||||||
|  | 			// the class in authorized resources.
 | ||||||
|  | 			Classes []string `yaml:"classes"` | ||||||
|  | 		} `yaml:"repository,omitempty"` | ||||||
|  | 	} `yaml:"policy,omitempty"` | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // LogHook is composed of hook Level and Type.
 | // LogHook is composed of hook Level and Type.
 | ||||||
|  |  | ||||||
|  | @ -136,6 +136,39 @@ func (uic userInfoContext) Value(key interface{}) interface{} { | ||||||
| 	return uic.Context.Value(key) | 	return uic.Context.Value(key) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | // WithResources returns a context with the authorized resources.
 | ||||||
|  | func WithResources(ctx context.Context, resources []Resource) context.Context { | ||||||
|  | 	return resourceContext{ | ||||||
|  | 		Context:   ctx, | ||||||
|  | 		resources: resources, | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | type resourceContext struct { | ||||||
|  | 	context.Context | ||||||
|  | 	resources []Resource | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | type resourceKey struct{} | ||||||
|  | 
 | ||||||
|  | func (rc resourceContext) Value(key interface{}) interface{} { | ||||||
|  | 	if key == (resourceKey{}) { | ||||||
|  | 		return rc.resources | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	return rc.Context.Value(key) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // AuthorizedResources returns the list of resources which have
 | ||||||
|  | // been authorized for this request.
 | ||||||
|  | func AuthorizedResources(ctx context.Context) []Resource { | ||||||
|  | 	if resources, ok := ctx.Value(resourceKey{}).([]Resource); ok { | ||||||
|  | 		return resources | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
|  | 
 | ||||||
| // InitFunc is the type of an AccessController factory function and is used
 | // InitFunc is the type of an AccessController factory function and is used
 | ||||||
| // to register the constructor for different AccesController backends.
 | // to register the constructor for different AccesController backends.
 | ||||||
| type InitFunc func(options map[string]interface{}) (AccessController, error) | type InitFunc func(options map[string]interface{}) (AccessController, error) | ||||||
|  |  | ||||||
|  | @ -261,6 +261,8 @@ func (ac *accessController) Authorized(ctx context.Context, accessItems ...auth. | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  | 	ctx = auth.WithResources(ctx, token.resources()) | ||||||
|  | 
 | ||||||
| 	return auth.WithUser(ctx, auth.UserInfo{Name: token.Claims.Subject}), nil | 	return auth.WithUser(ctx, auth.UserInfo{Name: token.Claims.Subject}), nil | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -34,7 +34,7 @@ var ( | ||||||
| // ResourceActions stores allowed actions on a named and typed resource.
 | // ResourceActions stores allowed actions on a named and typed resource.
 | ||||||
| type ResourceActions struct { | type ResourceActions struct { | ||||||
| 	Type    string   `json:"type"` | 	Type    string   `json:"type"` | ||||||
| 	Class   string   `json:"class"` | 	Class   string   `json:"class,omitempty"` | ||||||
| 	Name    string   `json:"name"` | 	Name    string   `json:"name"` | ||||||
| 	Actions []string `json:"actions"` | 	Actions []string `json:"actions"` | ||||||
| } | } | ||||||
|  | @ -350,6 +350,29 @@ func (t *Token) accessSet() accessSet { | ||||||
| 	return accessSet | 	return accessSet | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | func (t *Token) resources() []auth.Resource { | ||||||
|  | 	if t.Claims == nil { | ||||||
|  | 		return nil | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	resourceSet := map[auth.Resource]struct{}{} | ||||||
|  | 	for _, resourceActions := range t.Claims.Access { | ||||||
|  | 		resource := auth.Resource{ | ||||||
|  | 			Type:  resourceActions.Type, | ||||||
|  | 			Class: resourceActions.Class, | ||||||
|  | 			Name:  resourceActions.Name, | ||||||
|  | 		} | ||||||
|  | 		resourceSet[resource] = struct{}{} | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	resources := make([]auth.Resource, 0, len(resourceSet)) | ||||||
|  | 	for resource := range resourceSet { | ||||||
|  | 		resources = append(resources, resource) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	return resources | ||||||
|  | } | ||||||
|  | 
 | ||||||
| func (t *Token) compactRaw() string { | func (t *Token) compactRaw() string { | ||||||
| 	return fmt.Sprintf("%s.%s", t.Raw, joseBase64UrlEncode(t.Signature)) | 	return fmt.Sprintf("%s.%s", t.Raw, joseBase64UrlEncode(t.Signature)) | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -15,6 +15,7 @@ import ( | ||||||
| 	"github.com/docker/distribution/reference" | 	"github.com/docker/distribution/reference" | ||||||
| 	"github.com/docker/distribution/registry/api/errcode" | 	"github.com/docker/distribution/registry/api/errcode" | ||||||
| 	"github.com/docker/distribution/registry/api/v2" | 	"github.com/docker/distribution/registry/api/v2" | ||||||
|  | 	"github.com/docker/distribution/registry/auth" | ||||||
| 	"github.com/gorilla/handlers" | 	"github.com/gorilla/handlers" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
|  | @ -269,6 +270,12 @@ func (imh *imageManifestHandler) PutImageManifest(w http.ResponseWriter, r *http | ||||||
| 	if imh.Tag != "" { | 	if imh.Tag != "" { | ||||||
| 		options = append(options, distribution.WithTag(imh.Tag)) | 		options = append(options, distribution.WithTag(imh.Tag)) | ||||||
| 	} | 	} | ||||||
|  | 
 | ||||||
|  | 	if err := imh.applyResourcePolicy(manifest); err != nil { | ||||||
|  | 		imh.Errors = append(imh.Errors, err) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
| 	_, err = manifests.Put(imh, manifest, options...) | 	_, err = manifests.Put(imh, manifest, options...) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		// TODO(stevvooe): These error handling switches really need to be
 | 		// TODO(stevvooe): These error handling switches really need to be
 | ||||||
|  | @ -339,6 +346,73 @@ func (imh *imageManifestHandler) PutImageManifest(w http.ResponseWriter, r *http | ||||||
| 	w.WriteHeader(http.StatusCreated) | 	w.WriteHeader(http.StatusCreated) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | // applyResourcePolicy checks whether the resource class matches what has
 | ||||||
|  | // been authorized and allowed by the policy configuration.
 | ||||||
|  | func (imh *imageManifestHandler) applyResourcePolicy(manifest distribution.Manifest) error { | ||||||
|  | 	allowedClasses := imh.App.Config.Policy.Repository.Classes | ||||||
|  | 	if len(allowedClasses) == 0 { | ||||||
|  | 		return nil | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	var class string | ||||||
|  | 	switch m := manifest.(type) { | ||||||
|  | 	case *schema1.SignedManifest: | ||||||
|  | 		class = "image" | ||||||
|  | 	case *schema2.DeserializedManifest: | ||||||
|  | 		switch m.Config.MediaType { | ||||||
|  | 		case schema2.MediaTypeConfig: | ||||||
|  | 			class = "image" | ||||||
|  | 		case schema2.MediaTypePluginConfig: | ||||||
|  | 			class = "plugin" | ||||||
|  | 		default: | ||||||
|  | 			message := fmt.Sprintf("unknown manifest class for %s", m.Config.MediaType) | ||||||
|  | 			return errcode.ErrorCodeDenied.WithMessage(message) | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	if class == "" { | ||||||
|  | 		return nil | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// Check to see if class is allowed in registry
 | ||||||
|  | 	var allowedClass bool | ||||||
|  | 	for _, c := range allowedClasses { | ||||||
|  | 		if class == c { | ||||||
|  | 			allowedClass = true | ||||||
|  | 			break | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 	if !allowedClass { | ||||||
|  | 		message := fmt.Sprintf("registry does not allow %s manifest", class) | ||||||
|  | 		return errcode.ErrorCodeDenied.WithMessage(message) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	resources := auth.AuthorizedResources(imh) | ||||||
|  | 	n := imh.Repository.Named().Name() | ||||||
|  | 
 | ||||||
|  | 	var foundResource bool | ||||||
|  | 	for _, r := range resources { | ||||||
|  | 		if r.Name == n { | ||||||
|  | 			if r.Class == "" { | ||||||
|  | 				r.Class = "image" | ||||||
|  | 			} | ||||||
|  | 			if r.Class == class { | ||||||
|  | 				return nil | ||||||
|  | 			} | ||||||
|  | 			foundResource = true | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// resource was found but no matching class was found
 | ||||||
|  | 	if foundResource { | ||||||
|  | 		message := fmt.Sprintf("repository not authorized for %s manifest", class) | ||||||
|  | 		return errcode.ErrorCodeDenied.WithMessage(message) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	return nil | ||||||
|  | 
 | ||||||
|  | } | ||||||
|  | 
 | ||||||
| // DeleteImageManifest removes the manifest with the given digest from the registry.
 | // DeleteImageManifest removes the manifest with the given digest from the registry.
 | ||||||
| func (imh *imageManifestHandler) DeleteImageManifest(w http.ResponseWriter, r *http.Request) { | func (imh *imageManifestHandler) DeleteImageManifest(w http.ResponseWriter, r *http.Request) { | ||||||
| 	ctxu.GetLogger(imh).Debug("DeleteImageManifest") | 	ctxu.GetLogger(imh).Debug("DeleteImageManifest") | ||||||
|  |  | ||||||
		Loading…
	
		Reference in New Issue