forked from TrueCloudLab/distribution
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:
parent
61e65ecd9d
commit
e02278f22a
5 changed files with 146 additions and 1 deletions
|
@ -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 a new issue