diff --git a/configuration/configuration.go b/configuration/configuration.go index 55b9fcba1..ec50a6b18 100644 --- a/configuration/configuration.go +++ b/configuration/configuration.go @@ -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. diff --git a/registry/auth/auth.go b/registry/auth/auth.go index 5d40ea3dc..1c9af8821 100644 --- a/registry/auth/auth.go +++ b/registry/auth/auth.go @@ -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) diff --git a/registry/auth/token/accesscontroller.go b/registry/auth/token/accesscontroller.go index 52b7f3692..4e8b7f1ce 100644 --- a/registry/auth/token/accesscontroller.go +++ b/registry/auth/token/accesscontroller.go @@ -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 } diff --git a/registry/auth/token/token.go b/registry/auth/token/token.go index 74ce92994..850f5813f 100644 --- a/registry/auth/token/token.go +++ b/registry/auth/token/token.go @@ -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)) } diff --git a/registry/handlers/images.go b/registry/handlers/images.go index 96316348c..9518f6855 100644 --- a/registry/handlers/images.go +++ b/registry/handlers/images.go @@ -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")