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:"manifests,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.
 | ||||
|  |  | |||
|  | @ -136,6 +136,39 @@ func (uic userInfoContext) Value(key interface{}) interface{} { | |||
| 	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
 | ||||
| // to register the constructor for different AccesController backends.
 | ||||
| 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 | ||||
| } | ||||
| 
 | ||||
|  |  | |||
|  | @ -34,7 +34,7 @@ var ( | |||
| // ResourceActions stores allowed actions on a named and typed resource.
 | ||||
| type ResourceActions struct { | ||||
| 	Type    string   `json:"type"` | ||||
| 	Class   string   `json:"class"` | ||||
| 	Class   string   `json:"class,omitempty"` | ||||
| 	Name    string   `json:"name"` | ||||
| 	Actions []string `json:"actions"` | ||||
| } | ||||
|  | @ -350,6 +350,29 @@ func (t *Token) accessSet() 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 { | ||||
| 	return fmt.Sprintf("%s.%s", t.Raw, joseBase64UrlEncode(t.Signature)) | ||||
| } | ||||
|  |  | |||
|  | @ -15,6 +15,7 @@ import ( | |||
| 	"github.com/docker/distribution/reference" | ||||
| 	"github.com/docker/distribution/registry/api/errcode" | ||||
| 	"github.com/docker/distribution/registry/api/v2" | ||||
| 	"github.com/docker/distribution/registry/auth" | ||||
| 	"github.com/gorilla/handlers" | ||||
| ) | ||||
| 
 | ||||
|  | @ -269,6 +270,12 @@ func (imh *imageManifestHandler) PutImageManifest(w http.ResponseWriter, r *http | |||
| 	if 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...) | ||||
| 	if err != nil { | ||||
| 		// 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) | ||||
| } | ||||
| 
 | ||||
| // 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.
 | ||||
| func (imh *imageManifestHandler) DeleteImageManifest(w http.ResponseWriter, r *http.Request) { | ||||
| 	ctxu.GetLogger(imh).Debug("DeleteImageManifest") | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue