Compare commits

...

6 commits

Author SHA1 Message Date
Derek McGowan
0241c48be5
Release notes for v2.6.0-rc2
Signed-off-by: Derek McGowan <derek@mcgstyle.net> (github: dmcgowan)
2016-12-20 15:42:12 -08:00
Derek McGowan
438b8a1d4e
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)
2016-12-20 15:42:12 -08:00
Derek McGowan
4d0424b470
Update contrib token server to support repository class
Signed-off-by: Derek McGowan <derek@mcgstyle.net> (github: dmcgowan)
2016-12-20 15:42:11 -08:00
Derek McGowan
07d2f1aac7
Add class to repository scope
Signed-off-by: Derek McGowan <derek@mcgstyle.net> (github: dmcgowan)
2016-12-20 15:42:11 -08:00
Derek McGowan
f982e05861
Update scope specification for resource class
Update grammar to support a resource class. Add
example for plugin repository class.

Signed-off-by: Derek McGowan <derek@mcgstyle.net> (github: dmcgowan)
2016-12-20 15:42:11 -08:00
Derek McGowan
74c5c2fee4
Remove newlines from end of error strings
Golint now checks for new lines at the end of go error strings,
remove these unneeded new lines.

Signed-off-by: Derek McGowan <derek@mcgstyle.net> (github: dmcgowan)
2016-12-20 15:42:11 -08:00
11 changed files with 228 additions and 10 deletions

View file

@ -1,5 +1,14 @@
# Changelog # Changelog
## 2.6.0-rc2 (2016-12-20)
#### Spec
- Authorization: add support for repository classes
#### Registry
- Add policy configuration for enforcing repository classes
## 2.6.0-rc1 (2016-10-10) ## 2.6.0-rc1 (2016-10-10)
#### Storage #### Storage

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

@ -18,6 +18,10 @@ import (
"github.com/gorilla/mux" "github.com/gorilla/mux"
) )
var (
enforceRepoClass bool
)
func main() { func main() {
var ( var (
issuer = &TokenIssuer{} issuer = &TokenIssuer{}
@ -44,6 +48,8 @@ func main() {
flag.StringVar(&cert, "tlscert", "", "Certificate file for TLS") flag.StringVar(&cert, "tlscert", "", "Certificate file for TLS")
flag.StringVar(&certKey, "tlskey", "", "Certificate key for TLS") flag.StringVar(&certKey, "tlskey", "", "Certificate key for TLS")
flag.BoolVar(&enforceRepoClass, "enforce-class", false, "Enforce policy for single repository class")
flag.Parse() flag.Parse()
if debug { if debug {
@ -157,6 +163,8 @@ type tokenResponse struct {
ExpiresIn int `json:"expires_in,omitempty"` ExpiresIn int `json:"expires_in,omitempty"`
} }
var repositoryClassCache = map[string]string{}
func filterAccessList(ctx context.Context, scope string, requestedAccessList []auth.Access) []auth.Access { func filterAccessList(ctx context.Context, scope string, requestedAccessList []auth.Access) []auth.Access {
if !strings.HasSuffix(scope, "/") { if !strings.HasSuffix(scope, "/") {
scope = scope + "/" scope = scope + "/"
@ -168,6 +176,16 @@ func filterAccessList(ctx context.Context, scope string, requestedAccessList []a
context.GetLogger(ctx).Debugf("Resource scope not allowed: %s", access.Name) context.GetLogger(ctx).Debugf("Resource scope not allowed: %s", access.Name)
continue continue
} }
if enforceRepoClass {
if class, ok := repositoryClassCache[access.Name]; ok {
if class != access.Class {
context.GetLogger(ctx).Debugf("Different repository class: %q, previously %q", access.Class, class)
continue
}
} else if strings.EqualFold(access.Action, "push") {
repositoryClassCache[access.Name] = access.Class
}
}
} else if access.Type == "registry" { } else if access.Type == "registry" {
if access.Name != "catalog" { if access.Name != "catalog" {
context.GetLogger(ctx).Debugf("Unknown registry resource: %s", access.Name) context.GetLogger(ctx).Debugf("Unknown registry resource: %s", access.Name)

View file

@ -7,6 +7,7 @@ import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"io" "io"
"regexp"
"strings" "strings"
"time" "time"
@ -32,11 +33,17 @@ func ResolveScopeSpecifiers(ctx context.Context, scopeSpecs []string) []auth.Acc
resourceType, resourceName, actions := parts[0], parts[1], parts[2] resourceType, resourceName, actions := parts[0], parts[1], parts[2]
resourceType, resourceClass := splitResourceClass(resourceType)
if resourceType == "" {
continue
}
// Actions should be a comma-separated list of actions. // Actions should be a comma-separated list of actions.
for _, action := range strings.Split(actions, ",") { for _, action := range strings.Split(actions, ",") {
requestedAccess := auth.Access{ requestedAccess := auth.Access{
Resource: auth.Resource{ Resource: auth.Resource{
Type: resourceType, Type: resourceType,
Class: resourceClass,
Name: resourceName, Name: resourceName,
}, },
Action: action, Action: action,
@ -55,6 +62,19 @@ func ResolveScopeSpecifiers(ctx context.Context, scopeSpecs []string) []auth.Acc
return requestedAccessList return requestedAccessList
} }
var typeRegexp = regexp.MustCompile(`^([a-z0-9]+)(\([a-z0-9]+\))?$`)
func splitResourceClass(t string) (string, string) {
matches := typeRegexp.FindStringSubmatch(t)
if len(matches) < 2 {
return "", ""
}
if len(matches) == 2 || len(matches[2]) < 2 {
return matches[1], ""
}
return matches[1], matches[2][1 : len(matches[2])-1]
}
// ResolveScopeList converts a scope list from a token request's // ResolveScopeList converts a scope list from a token request's
// `scope` parameter into a list of standard access objects. // `scope` parameter into a list of standard access objects.
func ResolveScopeList(ctx context.Context, scopeList string) []auth.Access { func ResolveScopeList(ctx context.Context, scopeList string) []auth.Access {
@ -62,12 +82,19 @@ func ResolveScopeList(ctx context.Context, scopeList string) []auth.Access {
return ResolveScopeSpecifiers(ctx, scopes) return ResolveScopeSpecifiers(ctx, scopes)
} }
func scopeString(a auth.Access) string {
if a.Class != "" {
return fmt.Sprintf("%s(%s):%s:%s", a.Type, a.Class, a.Name, a.Action)
}
return fmt.Sprintf("%s:%s:%s", a.Type, a.Name, a.Action)
}
// ToScopeList converts a list of access to a // ToScopeList converts a list of access to a
// scope list string // scope list string
func ToScopeList(access []auth.Access) string { func ToScopeList(access []auth.Access) string {
var s []string var s []string
for _, a := range access { for _, a := range access {
s = append(s, fmt.Sprintf("%s:%s:%s", a.Type, a.Name, a.Action)) s = append(s, scopeString(a))
} }
return strings.Join(s, ",") return strings.Join(s, ",")
} }
@ -102,6 +129,7 @@ func (issuer *TokenIssuer) CreateJWT(subject string, audience string, grantedAcc
accessEntries = append(accessEntries, &token.ResourceActions{ accessEntries = append(accessEntries, &token.ResourceActions{
Type: resource.Type, Type: resource.Type,
Class: resource.Class,
Name: resource.Name, Name: resource.Name,
Actions: actions, Actions: actions,
}) })

View file

@ -39,13 +39,23 @@ intended to represent. This type may be specific to a resource provider but must
be understood by the authorization server in order to validate the subject be understood by the authorization server in order to validate the subject
is authorized for a specific resource. is authorized for a specific resource.
#### Resource Class
The resource type might have a resource class which further classifies the
the resource name within the resource type. A class is not required and
is specific to the resource type.
#### Example Resource Types #### Example Resource Types
- `repository` - represents a single repository within a registry. A - `repository` - represents a single repository within a registry. A
repository may represent many manifest or content blobs, but the resource type repository may represent many manifest or content blobs, but the resource type
is considered the collections of those items. Actions which may be performed on is considered the collections of those items. Actions which may be performed on
a `repository` are `pull` for accessing the collection and `push` for adding to a `repository` are `pull` for accessing the collection and `push` for adding to
it. it. By default the `repository` type has the class of `image`.
- `repository(plugin)` - represents a single repository of plugins within a
registry. A plugin repository has the same content and actions as a repository.
- `registry` - represents the entire registry. Used for administrative actions
or lookup operations that span an entire registry.
### Resource Name ### Resource Name
@ -78,7 +88,8 @@ scopes.
``` ```
scope := resourcescope [ ' ' resourcescope ]* scope := resourcescope [ ' ' resourcescope ]*
resourcescope := resourcetype ":" resourcename ":" action [ ',' action ]* resourcescope := resourcetype ":" resourcename ":" action [ ',' action ]*
resourcetype := /[a-z]*/ resourcetype := resourcetypevalue [ '(' resourcetypevalue ')' ]
resourcetypevalue := /[a-z0-9]+/
resourcename := [ hostname '/' ] component [ '/' component ]* resourcename := [ hostname '/' ] component [ '/' component ]*
hostname := hostcomponent ['.' hostcomponent]* [':' port-number] hostname := hostcomponent ['.' hostcomponent]* [':' port-number]
hostcomponent := /([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])/ hostcomponent := /([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])/

View file

@ -67,6 +67,7 @@ type UserInfo struct {
// Resource describes a resource by type and name. // Resource describes a resource by type and name.
type Resource struct { type Resource struct {
Type string Type string
Class string
Name string Name string
} }
@ -135,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,6 +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,omitempty"`
Name string `json:"name"` Name string `json:"name"`
Actions []string `json:"actions"` Actions []string `json:"actions"`
} }
@ -349,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

@ -147,13 +147,18 @@ type Scope interface {
// to a repository. // to a repository.
type RepositoryScope struct { type RepositoryScope struct {
Repository string Repository string
Class string
Actions []string Actions []string
} }
// String returns the string representation of the repository // String returns the string representation of the repository
// using the scope grammar // using the scope grammar
func (rs RepositoryScope) String() string { func (rs RepositoryScope) String() string {
return fmt.Sprintf("repository:%s:%s", rs.Repository, strings.Join(rs.Actions, ",")) repoType := "repository"
if rs.Class != "" {
repoType = fmt.Sprintf("%s(%s)", repoType, rs.Class)
}
return fmt.Sprintf("%s:%s:%s", repoType, rs.Repository, strings.Join(rs.Actions, ","))
} }
// RegistryScope represents a token scope for access // RegistryScope represents a token scope for access

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

View file

@ -80,7 +80,7 @@ func MarkAndSweep(ctx context.Context, storageDriver driver.StorageDriver, regis
}) })
if err != nil { if err != nil {
return fmt.Errorf("failed to mark: %v\n", err) return fmt.Errorf("failed to mark: %v", err)
} }
// sweep // sweep
@ -106,7 +106,7 @@ func MarkAndSweep(ctx context.Context, storageDriver driver.StorageDriver, regis
} }
err = vacuum.RemoveBlob(string(dgst)) err = vacuum.RemoveBlob(string(dgst))
if err != nil { if err != nil {
return fmt.Errorf("failed to delete blob %s: %v\n", dgst, err) return fmt.Errorf("failed to delete blob %s: %v", dgst, err)
} }
} }