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)
This commit is contained in:
Derek McGowan 2016-11-21 16:36:36 -08:00
parent 4d0424b470
commit 438b8a1d4e
No known key found for this signature in database
GPG key ID: F58C5D0A4405ACDB
5 changed files with 146 additions and 1 deletions

View file

@ -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.

View file

@ -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)

View file

@ -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
} }

View file

@ -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))
} }

View file

@ -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")