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:"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 a new issue