Skip to content

Commit

Permalink
Merge pull request #2067 from dmcgowan/add-repo-class
Browse files Browse the repository at this point in the history
Add support for repository class
  • Loading branch information
dmcgowan authored Nov 22, 2016
2 parents 7694c31 + e02278f commit a6bf3dd
Show file tree
Hide file tree
Showing 9 changed files with 217 additions and 8 deletions.
13 changes: 13 additions & 0 deletions configuration/configuration.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
18 changes: 18 additions & 0 deletions contrib/token-server/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ import (
"github.com/gorilla/mux"
)

var (
enforceRepoClass bool
)

func main() {
var (
issuer = &TokenIssuer{}
Expand All @@ -44,6 +48,8 @@ func main() {
flag.StringVar(&cert, "tlscert", "", "Certificate file for TLS")
flag.StringVar(&certKey, "tlskey", "", "Certificate key for TLS")

flag.BoolVar(&enforceRepoClass, "enforce-class", false, "Enforce policy for single repository class")

flag.Parse()

if debug {
Expand Down Expand Up @@ -157,6 +163,8 @@ type tokenResponse struct {
ExpiresIn int `json:"expires_in,omitempty"`
}

var repositoryClassCache = map[string]string{}

func filterAccessList(ctx context.Context, scope string, requestedAccessList []auth.Access) []auth.Access {
if !strings.HasSuffix(scope, "/") {
scope = scope + "/"
Expand All @@ -168,6 +176,16 @@ func filterAccessList(ctx context.Context, scope string, requestedAccessList []a
context.GetLogger(ctx).Debugf("Resource scope not allowed: %s", access.Name)
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" {
if access.Name != "catalog" {
context.GetLogger(ctx).Debugf("Unknown registry resource: %s", access.Name)
Expand Down
34 changes: 31 additions & 3 deletions contrib/token-server/token.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"encoding/json"
"fmt"
"io"
"regexp"
"strings"
"time"

Expand All @@ -32,12 +33,18 @@ func ResolveScopeSpecifiers(ctx context.Context, scopeSpecs []string) []auth.Acc

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.
for _, action := range strings.Split(actions, ",") {
requestedAccess := auth.Access{
Resource: auth.Resource{
Type: resourceType,
Name: resourceName,
Type: resourceType,
Class: resourceClass,
Name: resourceName,
},
Action: action,
}
Expand All @@ -55,19 +62,39 @@ func ResolveScopeSpecifiers(ctx context.Context, scopeSpecs []string) []auth.Acc
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
// `scope` parameter into a list of standard access objects.
func ResolveScopeList(ctx context.Context, scopeList string) []auth.Access {
scopes := strings.Split(scopeList, " ")
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
// scope list string
func ToScopeList(access []auth.Access) string {
var s []string
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, ",")
}
Expand Down Expand Up @@ -102,6 +129,7 @@ func (issuer *TokenIssuer) CreateJWT(subject string, audience string, grantedAcc

accessEntries = append(accessEntries, &token.ResourceActions{
Type: resource.Type,
Class: resource.Class,
Name: resource.Name,
Actions: actions,
})
Expand Down
15 changes: 13 additions & 2 deletions docs/spec/auth/scope.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
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

- `repository` - represents a single repository within a registry. A
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
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

Expand Down Expand Up @@ -78,7 +88,8 @@ scopes.
```
scope := resourcescope [ ' ' resourcescope ]*
resourcescope := resourcetype ":" resourcename ":" action [ ',' action ]*
resourcetype := /[a-z]*/
resourcetype := resourcetypevalue [ '(' resourcetypevalue ')' ]
resourcetypevalue := /[a-z0-9]+/
resourcename := [ hostname '/' ] component [ '/' component ]*
hostname := hostcomponent ['.' hostcomponent]* [':' port-number]
hostcomponent := /([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])/
Expand Down
38 changes: 36 additions & 2 deletions registry/auth/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -66,8 +66,9 @@ type UserInfo struct {

// Resource describes a resource by type and name.
type Resource struct {
Type string
Name string
Type string
Class string
Name string
}

// Access describes a specific action that is
Expand Down Expand Up @@ -135,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)
Expand Down
2 changes: 2 additions & 0 deletions registry/auth/token/accesscontroller.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand Down
24 changes: 24 additions & 0 deletions registry/auth/token/token.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ var (
// ResourceActions stores allowed actions on a named and typed resource.
type ResourceActions struct {
Type string `json:"type"`
Class string `json:"class,omitempty"`
Name string `json:"name"`
Actions []string `json:"actions"`
}
Expand Down Expand Up @@ -349,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))
}
7 changes: 6 additions & 1 deletion registry/client/auth/session.go
Original file line number Diff line number Diff line change
Expand Up @@ -147,13 +147,18 @@ type Scope interface {
// to a repository.
type RepositoryScope struct {
Repository string
Class string
Actions []string
}

// String returns the string representation of the repository
// using the scope grammar
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
Expand Down
74 changes: 74 additions & 0 deletions registry/handlers/images.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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")
Expand Down

0 comments on commit a6bf3dd

Please sign in to comment.