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
|
# Changelog
|
||||||
|
|
||||||
## 2.6.0-rc1 (2016-10-10)
|
## 2.6.1-rc1 (2017-03-21)
|
||||||
|
|
||||||
#### 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
|
#### Registry
|
||||||
- Override media type returned from `Stat()` for existing manifests
|
- Fix `Forwarded` header handling, revert use of `X-Forwarded-Port`
|
||||||
- Export `storage.CreateOptions` in top-level package
|
|
||||||
- Enable notifications to endpoints that use self-signed certificates
|
## 2.6.0 (2017-01-18)
|
||||||
- Properly validate multi-URL foreign layers
|
|
||||||
- Add control over validation of URLs in pushed manifests
|
#### Storage
|
||||||
- Proxy mode: fix socket leak when pull is cancelled
|
- S3: fixed bug in delete due to read-after-write inconsistency
|
||||||
- Tag service: properly handle error responses on HEAD request
|
- S3: allow EC2 IAM roles to be used when authorizing region endpoints
|
||||||
- Support for custom authentication URL in proxying registry
|
- S3: add Object ACL Support
|
||||||
- Add configuration option to disable access logging
|
- S3: fix delete method's notion of subpaths
|
||||||
- Add notification filtering by target media type
|
- S3: use multipart upload API in `Move` method for performance
|
||||||
- Manifest: `References()` returns all children
|
- S3: add v2 signature signing for legacy S3 clones
|
||||||
- Honor `X-Forwarded-Port` and Forwarded headers
|
- Swift: add simple heuristic to detect incomplete DLOs during read ops
|
||||||
- Reference: Preserve tag and digest in With* functions
|
- 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
|
#### Client
|
||||||
- Changes the client Tags `All()` method to follow links
|
- Changes the client Tags `All()` method to follow links
|
||||||
- Allow registry clients to connect via HTTP2
|
- Allow registry clients to connect via HTTP2
|
||||||
- Better handling of OAuth errors in client
|
- Better handling of OAuth errors in client
|
||||||
|
|
||||||
#### Spec
|
#### Spec
|
||||||
- Manifest: clarify relationship between urls and foreign layers
|
- Manifest: clarify relationship between urls and foreign layers
|
||||||
|
- Authorization: add support for repository classes
|
||||||
|
|
||||||
#### Manifest
|
#### Manifest
|
||||||
- Add plugin mediatype to distribution manifest
|
- Override media type returned from `Stat()` for existing manifests
|
||||||
|
- Add plugin mediatype to distribution manifest
|
||||||
|
|
||||||
#### Docs
|
#### Docs
|
||||||
|
- Document `TOOMANYREQUESTS` error code
|
||||||
- Document `TOOMANYREQUESTS` error code
|
- Document required Let's Encrypt port
|
||||||
- Document required Let's Encrypt port
|
- Improve documentation around implementation of OAuth2
|
||||||
- Improve documentation around implementation of OAuth2
|
- Improve documentation for configuration
|
||||||
|
|
||||||
#### Auth
|
#### Auth
|
||||||
- Add support for registry type in scope
|
- Add support for registry type in scope
|
||||||
- Add support for using v2 ping challenges for v1
|
- Add support for using v2 ping challenges for v1
|
||||||
- Add leeway to JWT `nbf` and `exp` checking
|
- Add leeway to JWT `nbf` and `exp` checking
|
||||||
- htpasswd: dynamically parse htpasswd file
|
- htpasswd: dynamically parse htpasswd file
|
||||||
- Fix missing auth headers with PATCH HTTP request when pushing to default port
|
- Fix missing auth headers with PATCH HTTP request when pushing to default port
|
||||||
|
|
||||||
#### Dockerfile
|
#### Dockerfile
|
||||||
- Update to go1.7
|
- Update to go1.7
|
||||||
- Reorder Dockerfile steps for better layer caching
|
- Reorder Dockerfile steps for better layer caching
|
||||||
|
|
||||||
#### Notes
|
#### Notes
|
||||||
|
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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,
|
||||||
})
|
})
|
||||||
|
|
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
|
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])/
|
||||||
|
|
|
@ -1,10 +1,8 @@
|
||||||
package v2
|
package v2
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"net"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"strconv"
|
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/docker/distribution/reference"
|
"github.com/docker/distribution/reference"
|
||||||
|
@ -48,27 +46,34 @@ func NewURLBuilderFromString(root string, relative bool) (*URLBuilder, error) {
|
||||||
// NewURLBuilderFromRequest uses information from an *http.Request to
|
// NewURLBuilderFromRequest uses information from an *http.Request to
|
||||||
// construct the root url.
|
// construct the root url.
|
||||||
func NewURLBuilderFromRequest(r *http.Request, relative bool) *URLBuilder {
|
func NewURLBuilderFromRequest(r *http.Request, relative bool) *URLBuilder {
|
||||||
var scheme string
|
var (
|
||||||
|
|
||||||
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:
|
|
||||||
scheme = "http"
|
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
|
// 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} 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 {
|
if forwardedHost := r.Header.Get("X-Forwarded-Host"); len(forwardedHost) > 0 {
|
||||||
// According to the Apache mod_proxy docs, X-Forwarded-Host can be a
|
// According to the Apache mod_proxy docs, X-Forwarded-Host can be a
|
||||||
// comma-separated list of hosts, to which each proxy appends the
|
// comma-separated list of hosts, to which each proxy appends the
|
||||||
|
@ -76,38 +81,7 @@ func NewURLBuilderFromRequest(r *http.Request, relative bool) *URLBuilder {
|
||||||
// list.
|
// list.
|
||||||
hosts := strings.SplitN(forwardedHost, ",", 2)
|
hosts := strings.SplitN(forwardedHost, ",", 2)
|
||||||
host = strings.TrimSpace(hosts[0])
|
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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]
|
|
||||||
}
|
|
||||||
// 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
|
basePath := routeDescriptorsMap[RouteNameBase].Path
|
||||||
|
@ -287,28 +261,3 @@ func appendValues(u string, values ...url.Values) string {
|
||||||
|
|
||||||
return appendValuesURL(up, 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",
|
name: "https protocol forwarded with a non-standard header",
|
||||||
request: &http.Request{URL: u, Host: u.Host, Header: http.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",
|
base: "http://example.com",
|
||||||
},
|
},
|
||||||
|
@ -225,6 +225,7 @@ func TestBuilderFromRequest(t *testing.T) {
|
||||||
{
|
{
|
||||||
name: "forwarded port with a non-standard header",
|
name: "forwarded port with a non-standard header",
|
||||||
request: &http.Request{URL: u, Host: u.Host, Header: http.Header{
|
request: &http.Request{URL: u, Host: u.Host, Header: http.Header{
|
||||||
|
"X-Forwarded-Host": []string{"example.com:5000"},
|
||||||
"X-Forwarded-Port": []string{"5000"},
|
"X-Forwarded-Port": []string{"5000"},
|
||||||
}},
|
}},
|
||||||
base: "http://example.com: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{
|
request: &http.Request{URL: u, Host: u.Host, Header: http.Header{
|
||||||
"X-Forwarded-Port": []string{"443 , 5001"},
|
"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",
|
name: "several non-standard headers",
|
||||||
request: &http.Request{URL: u, Host: u.Host, Header: http.Header{
|
request: &http.Request{URL: u, Host: u.Host, Header: http.Header{
|
||||||
"X-Forwarded-Proto": []string{"https"},
|
"X-Forwarded-Proto": []string{"https"},
|
||||||
"X-Forwarded-Host": []string{" first.example.com "},
|
"X-Forwarded-Host": []string{" first.example.com:12345 "},
|
||||||
"X-Forwarded-Port": []string{" 12345 \t"},
|
|
||||||
}},
|
}},
|
||||||
base: "http://first.example.com:12345",
|
base: "https://first.example.com:12345",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "forwarded host with port supplied takes priority",
|
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",
|
name: "forwarded protocol and addr using standard header",
|
||||||
request: &http.Request{URL: u, Host: u.Host, Header: http.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",
|
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{
|
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",
|
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",
|
name: "process just the first list element of standard header",
|
||||||
request: &http.Request{URL: u, Host: u.Host, Header: http.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",
|
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{
|
request: &http.Request{URL: u, Host: u.Host, Header: http.Header{
|
||||||
"Forwarded": []string{`for="2607:f0d0:1002:51::4"`},
|
"Forwarded": []string{`for="2607:f0d0:1002:51::4";host="[2607:f0d0:1002:51::4]:5001"`},
|
||||||
"X-Forwarded-Port": []string{"5001"},
|
"X-Forwarded-Port": []string{"5002"},
|
||||||
}},
|
}},
|
||||||
base: "http://[2607:f0d0:1002:51::4]:5001",
|
base: "http://[2607:f0d0:1002:51::4]:5001",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "IPv6 address with port",
|
name: "IPv6 address with port",
|
||||||
request: &http.Request{URL: u, Host: u.Host, Header: http.Header{
|
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"},
|
"X-Forwarded-Port": []string{"5001"},
|
||||||
}},
|
}},
|
||||||
base: "http://[2607:f0d0:1002:51::4]:4000",
|
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",
|
name: "non-standard and standard forward headers",
|
||||||
request: &http.Request{URL: u, Host: u.Host, Header: http.Header{
|
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",
|
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{
|
request: &http.Request{URL: u, Host: u.Host, Header: http.Header{
|
||||||
"X-Forwarded-Proto": []string{`http`},
|
"X-Forwarded-Proto": []string{`http`},
|
||||||
"Forwarded": []string{`host=second.example.com; proto=https`},
|
"Forwarded": []string{`host=second.example.com; proto=https`},
|
||||||
"X-Forwarded-Host": []string{`first.example.com`},
|
"X-Forwarded-Host": []string{`first.example.com`},
|
||||||
"X-Forwarded-Port": []string{`4000`},
|
"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,24 +387,10 @@ func TestBuilderFromRequest(t *testing.T) {
|
||||||
t.Fatalf("[relative=%t, request=%q, case=%q]: error building url: %v", relative, tr.name, testCase.description, err)
|
t.Fatalf("[relative=%t, request=%q, case=%q]: error building url: %v", relative, tr.name, testCase.description, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
var expectedURL string
|
expectedURL := testCase.expectedPath
|
||||||
proto, ok := tr.request.Header["X-Forwarded-Proto"]
|
|
||||||
if !ok {
|
|
||||||
expectedURL = testCase.expectedPath
|
|
||||||
if !relative {
|
if !relative {
|
||||||
expectedURL = tr.base + expectedURL
|
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if buildURL != expectedURL {
|
if buildURL != expectedURL {
|
||||||
t.Errorf("[relative=%t, request=%q, case=%q]: %q != %q", relative, tr.name, testCase.description, buildURL, expectedURL)
|
t.Errorf("[relative=%t, request=%q, case=%q]: %q != %q", relative, tr.name, testCase.description, 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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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,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))
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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")
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue