Compare commits
13 commits
master
...
v2.6.1-rc.
Author | SHA1 | Date | |
---|---|---|---|
|
74278cdaa6 | ||
|
5c43c3b0ee | ||
|
d0b7c92004 | ||
|
325b0804fe | ||
|
1642cd85d5 | ||
|
7f3c4b5c65 | ||
|
df1ddd8e46 | ||
|
0241c48be5 | ||
|
438b8a1d4e | ||
|
4d0424b470 | ||
|
07d2f1aac7 | ||
|
f982e05861 | ||
|
74c5c2fee4 |
14 changed files with 1246 additions and 2081 deletions
103
CHANGELOG.md
103
CHANGELOG.md
|
@ -1,67 +1,74 @@
|
|||
# Changelog
|
||||
|
||||
## 2.6.0-rc1 (2016-10-10)
|
||||
|
||||
#### Storage
|
||||
- S3: fixed bug in delete due to read-after-write inconsistency
|
||||
- S3: allow EC2 IAM roles to be used when authorizing region endpoints
|
||||
- S3: add Object ACL Support
|
||||
- S3: fix delete method's notion of subpaths
|
||||
- S3: use multipart upload API in `Move` method for performance
|
||||
- S3: add v2 signature signing for legacy S3 clones
|
||||
- Swift: add simple heuristic to detect incomplete DLOs during read ops
|
||||
- Swift: support different user and tenant domains
|
||||
- Swift: bulk deletes in chunks
|
||||
- Aliyun OSS: fix delete method's notion of subpaths
|
||||
- Aliyun OSS: optimize data copy after upload finishes
|
||||
- Azure: close leaking response body
|
||||
- Fix storage drivers dropping non-EOF errors when listing repositories
|
||||
- Compare path properly when listing repositories in catalog
|
||||
- Add a foreign layer URL host whitelist
|
||||
- Improve catalog enumerate runtime
|
||||
## 2.6.1-rc1 (2017-03-21)
|
||||
|
||||
#### Registry
|
||||
- Override media type returned from `Stat()` for existing manifests
|
||||
- Export `storage.CreateOptions` in top-level package
|
||||
- Enable notifications to endpoints that use self-signed certificates
|
||||
- Properly validate multi-URL foreign layers
|
||||
- Add control over validation of URLs in pushed manifests
|
||||
- Proxy mode: fix socket leak when pull is cancelled
|
||||
- Tag service: properly handle error responses on HEAD request
|
||||
- Support for custom authentication URL in proxying registry
|
||||
- Add configuration option to disable access logging
|
||||
- Add notification filtering by target media type
|
||||
- Manifest: `References()` returns all children
|
||||
- Honor `X-Forwarded-Port` and Forwarded headers
|
||||
- Reference: Preserve tag and digest in With* functions
|
||||
- Fix `Forwarded` header handling, revert use of `X-Forwarded-Port`
|
||||
|
||||
## 2.6.0 (2017-01-18)
|
||||
|
||||
#### Storage
|
||||
- S3: fixed bug in delete due to read-after-write inconsistency
|
||||
- S3: allow EC2 IAM roles to be used when authorizing region endpoints
|
||||
- S3: add Object ACL Support
|
||||
- S3: fix delete method's notion of subpaths
|
||||
- S3: use multipart upload API in `Move` method for performance
|
||||
- S3: add v2 signature signing for legacy S3 clones
|
||||
- Swift: add simple heuristic to detect incomplete DLOs during read ops
|
||||
- Swift: support different user and tenant domains
|
||||
- Swift: bulk deletes in chunks
|
||||
- Aliyun OSS: fix delete method's notion of subpaths
|
||||
- Aliyun OSS: optimize data copy after upload finishes
|
||||
- Azure: close leaking response body
|
||||
- Fix storage drivers dropping non-EOF errors when listing repositories
|
||||
- Compare path properly when listing repositories in catalog
|
||||
- Add a foreign layer URL host whitelist
|
||||
- Improve catalog enumerate runtime
|
||||
|
||||
#### Registry
|
||||
- Export `storage.CreateOptions` in top-level package
|
||||
- Enable notifications to endpoints that use self-signed certificates
|
||||
- Properly validate multi-URL foreign layers
|
||||
- Add control over validation of URLs in pushed manifests
|
||||
- Proxy mode: fix socket leak when pull is cancelled
|
||||
- Tag service: properly handle error responses on HEAD request
|
||||
- Support for custom authentication URL in proxying registry
|
||||
- Add configuration option to disable access logging
|
||||
- Add notification filtering by target media type
|
||||
- Manifest: `References()` returns all children
|
||||
- Honor `X-Forwarded-Port` and Forwarded headers
|
||||
- Reference: Preserve tag and digest in With* functions
|
||||
- Add policy configuration for enforcing repository classes
|
||||
|
||||
#### Client
|
||||
- Changes the client Tags `All()` method to follow links
|
||||
- Allow registry clients to connect via HTTP2
|
||||
- Better handling of OAuth errors in client
|
||||
- Changes the client Tags `All()` method to follow links
|
||||
- Allow registry clients to connect via HTTP2
|
||||
- Better handling of OAuth errors in client
|
||||
|
||||
#### Spec
|
||||
- Manifest: clarify relationship between urls and foreign layers
|
||||
- Manifest: clarify relationship between urls and foreign layers
|
||||
- Authorization: add support for repository classes
|
||||
|
||||
#### Manifest
|
||||
- Add plugin mediatype to distribution manifest
|
||||
- Override media type returned from `Stat()` for existing manifests
|
||||
- Add plugin mediatype to distribution manifest
|
||||
|
||||
#### Docs
|
||||
|
||||
- Document `TOOMANYREQUESTS` error code
|
||||
- Document required Let's Encrypt port
|
||||
- Improve documentation around implementation of OAuth2
|
||||
- Document `TOOMANYREQUESTS` error code
|
||||
- Document required Let's Encrypt port
|
||||
- Improve documentation around implementation of OAuth2
|
||||
- Improve documentation for configuration
|
||||
|
||||
#### Auth
|
||||
- Add support for registry type in scope
|
||||
- Add support for using v2 ping challenges for v1
|
||||
- Add leeway to JWT `nbf` and `exp` checking
|
||||
- htpasswd: dynamically parse htpasswd file
|
||||
- Fix missing auth headers with PATCH HTTP request when pushing to default port
|
||||
- Add support for registry type in scope
|
||||
- Add support for using v2 ping challenges for v1
|
||||
- Add leeway to JWT `nbf` and `exp` checking
|
||||
- htpasswd: dynamically parse htpasswd file
|
||||
- Fix missing auth headers with PATCH HTTP request when pushing to default port
|
||||
|
||||
#### Dockerfile
|
||||
- Update to go1.7
|
||||
- Reorder Dockerfile steps for better layer caching
|
||||
- Update to go1.7
|
||||
- Reorder Dockerfile steps for better layer caching
|
||||
|
||||
#### Notes
|
||||
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -18,6 +18,10 @@ import (
|
|||
"github.com/gorilla/mux"
|
||||
)
|
||||
|
||||
var (
|
||||
enforceRepoClass bool
|
||||
)
|
||||
|
||||
func main() {
|
||||
var (
|
||||
issuer = &TokenIssuer{}
|
||||
|
@ -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 {
|
||||
|
@ -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 + "/"
|
||||
|
@ -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)
|
||||
|
|
|
@ -7,6 +7,7 @@ import (
|
|||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
|
@ -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,
|
||||
}
|
||||
|
@ -55,6 +62,19 @@ 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 {
|
||||
|
@ -62,12 +82,19 @@ func ResolveScopeList(ctx context.Context, scopeList string) []auth.Access {
|
|||
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, ",")
|
||||
}
|
||||
|
@ -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,
|
||||
})
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -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
|
||||
|
||||
|
@ -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])/
|
||||
|
|
|
@ -1,10 +1,8 @@
|
|||
package v2
|
||||
|
||||
import (
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/docker/distribution/reference"
|
||||
|
@ -48,66 +46,42 @@ func NewURLBuilderFromString(root string, relative bool) (*URLBuilder, error) {
|
|||
// NewURLBuilderFromRequest uses information from an *http.Request to
|
||||
// construct the root url.
|
||||
func NewURLBuilderFromRequest(r *http.Request, relative bool) *URLBuilder {
|
||||
var scheme string
|
||||
|
||||
forwardedProto := r.Header.Get("X-Forwarded-Proto")
|
||||
// TODO: log the error
|
||||
forwardedHeader, _, _ := parseForwardedHeader(r.Header.Get("Forwarded"))
|
||||
|
||||
switch {
|
||||
case len(forwardedProto) > 0:
|
||||
scheme = forwardedProto
|
||||
case len(forwardedHeader["proto"]) > 0:
|
||||
scheme = forwardedHeader["proto"]
|
||||
case r.TLS != nil:
|
||||
scheme = "https"
|
||||
case len(r.URL.Scheme) > 0:
|
||||
scheme = r.URL.Scheme
|
||||
default:
|
||||
var (
|
||||
scheme = "http"
|
||||
host = r.Host
|
||||
)
|
||||
|
||||
if r.TLS != nil {
|
||||
scheme = "https"
|
||||
} else if len(r.URL.Scheme) > 0 {
|
||||
scheme = r.URL.Scheme
|
||||
}
|
||||
|
||||
host := r.Host
|
||||
|
||||
if forwardedHost := r.Header.Get("X-Forwarded-Host"); len(forwardedHost) > 0 {
|
||||
// According to the Apache mod_proxy docs, X-Forwarded-Host can be a
|
||||
// comma-separated list of hosts, to which each proxy appends the
|
||||
// requested host. We want to grab the first from this comma-separated
|
||||
// list.
|
||||
hosts := strings.SplitN(forwardedHost, ",", 2)
|
||||
host = strings.TrimSpace(hosts[0])
|
||||
} else if addr, exists := forwardedHeader["for"]; exists {
|
||||
host = addr
|
||||
} else if h, exists := forwardedHeader["host"]; exists {
|
||||
host = h
|
||||
}
|
||||
|
||||
portLessHost, port := host, ""
|
||||
if !isIPv6Address(portLessHost) {
|
||||
// with go 1.6, this would treat the last part of IPv6 address as a port
|
||||
portLessHost, port, _ = net.SplitHostPort(host)
|
||||
}
|
||||
if forwardedPort := r.Header.Get("X-Forwarded-Port"); len(port) == 0 && len(forwardedPort) > 0 {
|
||||
ports := strings.SplitN(forwardedPort, ",", 2)
|
||||
forwardedPort = strings.TrimSpace(ports[0])
|
||||
if _, err := strconv.ParseInt(forwardedPort, 10, 32); err == nil {
|
||||
port = forwardedPort
|
||||
// Handle fowarded headers
|
||||
// Prefer "Forwarded" header as defined by rfc7239 if given
|
||||
// see https://tools.ietf.org/html/rfc7239
|
||||
if forwarded := r.Header.Get("Forwarded"); len(forwarded) > 0 {
|
||||
forwardedHeader, _, err := parseForwardedHeader(forwarded)
|
||||
if err == nil {
|
||||
if fproto := forwardedHeader["proto"]; len(fproto) > 0 {
|
||||
scheme = fproto
|
||||
}
|
||||
if fhost := forwardedHeader["host"]; len(fhost) > 0 {
|
||||
host = fhost
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(portLessHost) > 0 {
|
||||
host = portLessHost
|
||||
}
|
||||
if len(port) > 0 {
|
||||
// remove enclosing brackets of ipv6 address otherwise they will be duplicated
|
||||
if len(host) > 1 && host[0] == '[' && host[len(host)-1] == ']' {
|
||||
host = host[1 : len(host)-1]
|
||||
} else {
|
||||
if forwardedProto := r.Header.Get("X-Forwarded-Proto"); len(forwardedProto) > 0 {
|
||||
scheme = forwardedProto
|
||||
}
|
||||
if forwardedHost := r.Header.Get("X-Forwarded-Host"); len(forwardedHost) > 0 {
|
||||
// According to the Apache mod_proxy docs, X-Forwarded-Host can be a
|
||||
// comma-separated list of hosts, to which each proxy appends the
|
||||
// requested host. We want to grab the first from this comma-separated
|
||||
// list.
|
||||
hosts := strings.SplitN(forwardedHost, ",", 2)
|
||||
host = strings.TrimSpace(hosts[0])
|
||||
}
|
||||
// JoinHostPort properly encloses ipv6 addresses in square brackets
|
||||
host = net.JoinHostPort(host, port)
|
||||
} else if isIPv6Address(host) && host[0] != '[' {
|
||||
// ipv6 needs to be enclosed in square brackets in urls
|
||||
host = "[" + host + "]"
|
||||
}
|
||||
|
||||
basePath := routeDescriptorsMap[RouteNameBase].Path
|
||||
|
@ -287,28 +261,3 @@ func appendValues(u string, values ...url.Values) string {
|
|||
|
||||
return appendValuesURL(up, values...).String()
|
||||
}
|
||||
|
||||
// isIPv6Address returns true if given string is a valid IPv6 address. No port is allowed. The address may be
|
||||
// enclosed in square brackets.
|
||||
func isIPv6Address(host string) bool {
|
||||
if len(host) > 1 && host[0] == '[' && host[len(host)-1] == ']' {
|
||||
host = host[1 : len(host)-1]
|
||||
}
|
||||
// The IPv6 scoped addressing zone identifier starts after the last percent sign.
|
||||
if i := strings.LastIndexByte(host, '%'); i > 0 {
|
||||
host = host[:i]
|
||||
}
|
||||
ip := net.ParseIP(host)
|
||||
if ip == nil {
|
||||
return false
|
||||
}
|
||||
if ip.To16() == nil {
|
||||
return false
|
||||
}
|
||||
if ip.To4() == nil {
|
||||
return true
|
||||
}
|
||||
// dot can be present in ipv4-mapped address, it needs to come after a colon though
|
||||
i := strings.IndexAny(host, ":.")
|
||||
return i >= 0 && host[i] == ':'
|
||||
}
|
||||
|
|
|
@ -179,7 +179,7 @@ func TestBuilderFromRequest(t *testing.T) {
|
|||
{
|
||||
name: "https protocol forwarded with a non-standard header",
|
||||
request: &http.Request{URL: u, Host: u.Host, Header: http.Header{
|
||||
"X-Forwarded-Proto": []string{"https"},
|
||||
"X-Custom-Forwarded-Proto": []string{"https"},
|
||||
}},
|
||||
base: "http://example.com",
|
||||
},
|
||||
|
@ -225,6 +225,7 @@ func TestBuilderFromRequest(t *testing.T) {
|
|||
{
|
||||
name: "forwarded port with a non-standard header",
|
||||
request: &http.Request{URL: u, Host: u.Host, Header: http.Header{
|
||||
"X-Forwarded-Host": []string{"example.com:5000"},
|
||||
"X-Forwarded-Port": []string{"5000"},
|
||||
}},
|
||||
base: "http://example.com:5000",
|
||||
|
@ -234,16 +235,33 @@ func TestBuilderFromRequest(t *testing.T) {
|
|||
request: &http.Request{URL: u, Host: u.Host, Header: http.Header{
|
||||
"X-Forwarded-Port": []string{"443 , 5001"},
|
||||
}},
|
||||
base: "http://example.com:443",
|
||||
base: "http://example.com",
|
||||
},
|
||||
{
|
||||
name: "forwarded standard port with non-standard headers",
|
||||
request: &http.Request{URL: u, Host: u.Host, Header: http.Header{
|
||||
"X-Forwarded-Proto": []string{"https"},
|
||||
"X-Forwarded-Host": []string{"example.com"},
|
||||
"X-Forwarded-Port": []string{"443"},
|
||||
}},
|
||||
base: "https://example.com",
|
||||
},
|
||||
{
|
||||
name: "forwarded standard port with non-standard headers and explicit port",
|
||||
request: &http.Request{URL: u, Host: u.Host + ":443", Header: http.Header{
|
||||
"X-Forwarded-Proto": []string{"https"},
|
||||
"X-Forwarded-Host": []string{u.Host + ":443"},
|
||||
"X-Forwarded-Port": []string{"443"},
|
||||
}},
|
||||
base: "https://example.com:443",
|
||||
},
|
||||
{
|
||||
name: "several non-standard headers",
|
||||
request: &http.Request{URL: u, Host: u.Host, Header: http.Header{
|
||||
"X-Forwarded-Proto": []string{"https"},
|
||||
"X-Forwarded-Host": []string{" first.example.com "},
|
||||
"X-Forwarded-Port": []string{" 12345 \t"},
|
||||
"X-Forwarded-Host": []string{" first.example.com:12345 "},
|
||||
}},
|
||||
base: "http://first.example.com:12345",
|
||||
base: "https://first.example.com:12345",
|
||||
},
|
||||
{
|
||||
name: "forwarded host with port supplied takes priority",
|
||||
|
@ -264,16 +282,16 @@ func TestBuilderFromRequest(t *testing.T) {
|
|||
{
|
||||
name: "forwarded protocol and addr using standard header",
|
||||
request: &http.Request{URL: u, Host: u.Host, Header: http.Header{
|
||||
"Forwarded": []string{`proto=https;for="192.168.22.30:80"`},
|
||||
"Forwarded": []string{`proto=https;host="192.168.22.30:80"`},
|
||||
}},
|
||||
base: "https://192.168.22.30:80",
|
||||
},
|
||||
{
|
||||
name: "forwarded addr takes priority over host",
|
||||
name: "forwarded host takes priority over for",
|
||||
request: &http.Request{URL: u, Host: u.Host, Header: http.Header{
|
||||
"Forwarded": []string{`host=reg.example.com;for="192.168.22.30:5000"`},
|
||||
"Forwarded": []string{`host="reg.example.com:5000";for="192.168.22.30"`},
|
||||
}},
|
||||
base: "http://192.168.22.30:5000",
|
||||
base: "http://reg.example.com:5000",
|
||||
},
|
||||
{
|
||||
name: "forwarded host and protocol using standard header",
|
||||
|
@ -292,73 +310,26 @@ func TestBuilderFromRequest(t *testing.T) {
|
|||
{
|
||||
name: "process just the first list element of standard header",
|
||||
request: &http.Request{URL: u, Host: u.Host, Header: http.Header{
|
||||
"Forwarded": []string{`for="reg.example.com:443";proto=https, for="reg.example.com:80";proto=http`},
|
||||
"Forwarded": []string{`host="reg.example.com:443";proto=https, host="reg.example.com:80";proto=http`},
|
||||
}},
|
||||
base: "https://reg.example.com:443",
|
||||
},
|
||||
{
|
||||
name: "IPv6 address override port",
|
||||
name: "IPv6 address use host",
|
||||
request: &http.Request{URL: u, Host: u.Host, Header: http.Header{
|
||||
"Forwarded": []string{`for="2607:f0d0:1002:51::4"`},
|
||||
"X-Forwarded-Port": []string{"5001"},
|
||||
"Forwarded": []string{`for="2607:f0d0:1002:51::4";host="[2607:f0d0:1002:51::4]:5001"`},
|
||||
"X-Forwarded-Port": []string{"5002"},
|
||||
}},
|
||||
base: "http://[2607:f0d0:1002:51::4]:5001",
|
||||
},
|
||||
{
|
||||
name: "IPv6 address with port",
|
||||
request: &http.Request{URL: u, Host: u.Host, Header: http.Header{
|
||||
"Forwarded": []string{`for="[2607:f0d0:1002:51::4]:4000"`},
|
||||
"Forwarded": []string{`host="[2607:f0d0:1002:51::4]:4000"`},
|
||||
"X-Forwarded-Port": []string{"5001"},
|
||||
}},
|
||||
base: "http://[2607:f0d0:1002:51::4]:4000",
|
||||
},
|
||||
{
|
||||
name: "IPv6 long address override port",
|
||||
request: &http.Request{URL: u, Host: u.Host, Header: http.Header{
|
||||
"Forwarded": []string{`for="2607:f0d0:1002:0051:0000:0000:0000:0004"`},
|
||||
"X-Forwarded-Port": []string{"5001"},
|
||||
}},
|
||||
base: "http://[2607:f0d0:1002:0051:0000:0000:0000:0004]:5001",
|
||||
},
|
||||
{
|
||||
name: "IPv6 long address enclosed in brackets - be benevolent",
|
||||
request: &http.Request{URL: u, Host: u.Host, Header: http.Header{
|
||||
"Forwarded": []string{`for="[2607:f0d0:1002:0051:0000:0000:0000:0004]"`},
|
||||
"X-Forwarded-Port": []string{"5001"},
|
||||
}},
|
||||
base: "http://[2607:f0d0:1002:0051:0000:0000:0000:0004]:5001",
|
||||
},
|
||||
{
|
||||
name: "IPv6 long address with port",
|
||||
request: &http.Request{URL: u, Host: u.Host, Header: http.Header{
|
||||
"Forwarded": []string{`for="[2607:f0d0:1002:0051:0000:0000:0000:0004]:4321"`},
|
||||
"X-Forwarded-Port": []string{"5001"},
|
||||
}},
|
||||
base: "http://[2607:f0d0:1002:0051:0000:0000:0000:0004]:4321",
|
||||
},
|
||||
{
|
||||
name: "IPv6 address with zone ID",
|
||||
request: &http.Request{URL: u, Host: u.Host, Header: http.Header{
|
||||
"Forwarded": []string{`for="fe80::bd0f:a8bc:6480:238b%11"`},
|
||||
"X-Forwarded-Port": []string{"5001"},
|
||||
}},
|
||||
base: "http://[fe80::bd0f:a8bc:6480:238b%2511]:5001",
|
||||
},
|
||||
{
|
||||
name: "IPv6 address with zone ID and port",
|
||||
request: &http.Request{URL: u, Host: u.Host, Header: http.Header{
|
||||
"Forwarded": []string{`for="[fe80::bd0f:a8bc:6480:238b%eth0]:12345"`},
|
||||
"X-Forwarded-Port": []string{"5001"},
|
||||
}},
|
||||
base: "http://[fe80::bd0f:a8bc:6480:238b%25eth0]:12345",
|
||||
},
|
||||
{
|
||||
name: "IPv6 address without port",
|
||||
request: &http.Request{URL: u, Host: u.Host, Header: http.Header{
|
||||
"Forwarded": []string{`for="::FFFF:129.144.52.38"`},
|
||||
}},
|
||||
base: "http://[::FFFF:129.144.52.38]",
|
||||
},
|
||||
{
|
||||
name: "non-standard and standard forward headers",
|
||||
request: &http.Request{URL: u, Host: u.Host, Header: http.Header{
|
||||
|
@ -370,14 +341,34 @@ func TestBuilderFromRequest(t *testing.T) {
|
|||
base: "https://first.example.com",
|
||||
},
|
||||
{
|
||||
name: "non-standard headers take precedence over standard one",
|
||||
name: "standard header takes precedence over non-standard headers",
|
||||
request: &http.Request{URL: u, Host: u.Host, Header: http.Header{
|
||||
"X-Forwarded-Proto": []string{`http`},
|
||||
"Forwarded": []string{`host=second.example.com; proto=https`},
|
||||
"X-Forwarded-Host": []string{`first.example.com`},
|
||||
"X-Forwarded-Port": []string{`4000`},
|
||||
}},
|
||||
base: "http://first.example.com:4000",
|
||||
base: "https://second.example.com",
|
||||
},
|
||||
{
|
||||
name: "incomplete standard header uses default",
|
||||
request: &http.Request{URL: u, Host: u.Host, Header: http.Header{
|
||||
"X-Forwarded-Proto": []string{`https`},
|
||||
"Forwarded": []string{`for=127.0.0.1`},
|
||||
"X-Forwarded-Host": []string{`first.example.com`},
|
||||
"X-Forwarded-Port": []string{`4000`},
|
||||
}},
|
||||
base: "http://" + u.Host,
|
||||
},
|
||||
{
|
||||
name: "standard with just proto",
|
||||
request: &http.Request{URL: u, Host: u.Host, Header: http.Header{
|
||||
"X-Forwarded-Proto": []string{`https`},
|
||||
"Forwarded": []string{`proto=https`},
|
||||
"X-Forwarded-Host": []string{`first.example.com`},
|
||||
"X-Forwarded-Port": []string{`4000`},
|
||||
}},
|
||||
base: "https://" + u.Host,
|
||||
},
|
||||
}
|
||||
|
||||
|
@ -396,23 +387,9 @@ func TestBuilderFromRequest(t *testing.T) {
|
|||
t.Fatalf("[relative=%t, request=%q, case=%q]: error building url: %v", relative, tr.name, testCase.description, err)
|
||||
}
|
||||
|
||||
var expectedURL string
|
||||
proto, ok := tr.request.Header["X-Forwarded-Proto"]
|
||||
if !ok {
|
||||
expectedURL = testCase.expectedPath
|
||||
if !relative {
|
||||
expectedURL = tr.base + expectedURL
|
||||
}
|
||||
} else {
|
||||
urlBase, err := url.Parse(tr.base)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
urlBase.Scheme = proto[0]
|
||||
expectedURL = testCase.expectedPath
|
||||
if !relative {
|
||||
expectedURL = urlBase.String() + expectedURL
|
||||
}
|
||||
expectedURL := testCase.expectedPath
|
||||
if !relative {
|
||||
expectedURL = tr.base + expectedURL
|
||||
}
|
||||
|
||||
if buildURL != expectedURL {
|
||||
|
@ -505,119 +482,3 @@ func TestBuilderFromRequestWithPrefix(t *testing.T) {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsIPv6Address(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
address string
|
||||
isIPv6 bool
|
||||
}{
|
||||
{
|
||||
name: "IPv6 short address",
|
||||
address: `2607:f0d0:1002:51::4`,
|
||||
isIPv6: true,
|
||||
},
|
||||
{
|
||||
name: "IPv6 short address enclosed in brackets",
|
||||
address: "[2607:f0d0:1002:51::4]",
|
||||
isIPv6: true,
|
||||
},
|
||||
{
|
||||
name: "IPv6 address",
|
||||
address: `2607:f0d0:1002:0051:0000:0000:0000:0004`,
|
||||
isIPv6: true,
|
||||
},
|
||||
{
|
||||
name: "IPv6 address with numeric zone ID",
|
||||
address: `fe80::bd0f:a8bc:6480:238b%11`,
|
||||
isIPv6: true,
|
||||
},
|
||||
{
|
||||
name: "IPv6 address with device name as zone ID",
|
||||
address: `fe80::bd0f:a8bc:6480:238b%eth0`,
|
||||
isIPv6: true,
|
||||
},
|
||||
{
|
||||
name: "IPv6 address with device name as zone ID enclosed in brackets",
|
||||
address: `[fe80::bd0f:a8bc:6480:238b%eth0]`,
|
||||
isIPv6: true,
|
||||
},
|
||||
{
|
||||
name: "IPv4-mapped address",
|
||||
address: "::FFFF:129.144.52.38",
|
||||
isIPv6: true,
|
||||
},
|
||||
{
|
||||
name: "localhost",
|
||||
address: "::1",
|
||||
isIPv6: true,
|
||||
},
|
||||
{
|
||||
name: "localhost",
|
||||
address: "::1",
|
||||
isIPv6: true,
|
||||
},
|
||||
{
|
||||
name: "long localhost address",
|
||||
address: "0:0:0:0:0:0:0:1",
|
||||
isIPv6: true,
|
||||
},
|
||||
{
|
||||
name: "IPv6 long address with port",
|
||||
address: "[2607:f0d0:1002:0051:0000:0000:0000:0004]:4321",
|
||||
isIPv6: false,
|
||||
},
|
||||
{
|
||||
name: "too many groups",
|
||||
address: "2607:f0d0:1002:0051:0000:0000:0000:0004:4321",
|
||||
isIPv6: false,
|
||||
},
|
||||
{
|
||||
name: "square brackets don't make an IPv6 address",
|
||||
address: "[2607:f0d0]",
|
||||
isIPv6: false,
|
||||
},
|
||||
{
|
||||
name: "require two consecutive colons in localhost",
|
||||
address: ":1",
|
||||
isIPv6: false,
|
||||
},
|
||||
{
|
||||
name: "more then 4 hexadecimal digits",
|
||||
address: "2607:f0d0b:1002:0051:0000:0000:0000:0004",
|
||||
isIPv6: false,
|
||||
},
|
||||
{
|
||||
name: "too short address",
|
||||
address: `2607:f0d0:1002:0000:0000:0000:0004`,
|
||||
isIPv6: false,
|
||||
},
|
||||
{
|
||||
name: "IPv4 address",
|
||||
address: `192.168.100.1`,
|
||||
isIPv6: false,
|
||||
},
|
||||
{
|
||||
name: "unclosed bracket",
|
||||
address: `[2607:f0d0:1002:0051:0000:0000:0000:0004`,
|
||||
isIPv6: false,
|
||||
},
|
||||
{
|
||||
name: "trailing bracket",
|
||||
address: `2607:f0d0:1002:0051:0000:0000:0000:0004]`,
|
||||
isIPv6: false,
|
||||
},
|
||||
{
|
||||
name: "domain name",
|
||||
address: `localhost`,
|
||||
isIPv6: false,
|
||||
},
|
||||
} {
|
||||
isIPv6 := isIPv6Address(tc.address)
|
||||
if isIPv6 && !tc.isIPv6 {
|
||||
t.Errorf("[%s] address %q falsely detected as IPv6 address", tc.name, tc.address)
|
||||
} else if !isIPv6 && tc.isIPv6 {
|
||||
t.Errorf("[%s] address %q not recognized as IPv6", tc.name, tc.address)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
@ -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)
|
||||
|
|
|
@ -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,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"`
|
||||
}
|
||||
|
@ -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))
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -80,7 +80,7 @@ func MarkAndSweep(ctx context.Context, storageDriver driver.StorageDriver, regis
|
|||
})
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to mark: %v\n", err)
|
||||
return fmt.Errorf("failed to mark: %v", err)
|
||||
}
|
||||
|
||||
// sweep
|
||||
|
@ -106,7 +106,7 @@ func MarkAndSweep(ctx context.Context, storageDriver driver.StorageDriver, regis
|
|||
}
|
||||
err = vacuum.RemoveBlob(string(dgst))
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue