Merge pull request #138 from mtrmac/location-namespaced-signatures

Use transport abstraction and transport-abstracted references
This commit is contained in:
Miloslav Trmač 2016-07-18 21:39:45 +02:00 committed by GitHub
commit ecc745d124
14 changed files with 505 additions and 132 deletions

View File

@ -37,7 +37,10 @@ var layersCmd = cli.Command{
if err != nil {
return err
}
tmpDirRef := directory.NewReference(tmpDir)
tmpDirRef, err := directory.NewReference(tmpDir)
if err != nil {
return err
}
dest, err := tmpDirRef.NewImageDestination("", true)
if err != nil {
return err

View File

@ -1,6 +1,11 @@
package directory
import (
"errors"
"fmt"
"strings"
"github.com/containers/image/directory/explicitfilepath"
"github.com/containers/image/image"
"github.com/containers/image/types"
"github.com/docker/docker/reference"
@ -17,13 +22,35 @@ func (t dirTransport) Name() string {
// ParseReference converts a string, which should not start with the ImageTransport.Name prefix, into an ImageReference.
func (t dirTransport) ParseReference(reference string) (types.ImageReference, error) {
return NewReference(reference), nil
return NewReference(reference)
}
// ValidatePolicyConfigurationScope checks that scope is a valid name for a signature.PolicyTransportScopes keys
// (i.e. a valid PolicyConfigurationIdentity() or PolicyConfigurationNamespaces() return value).
// It is acceptable to allow an invalid value which will never be matched, it can "only" cause user confusion.
// scope passed to this function will not be "", that value is always allowed.
func (t dirTransport) ValidatePolicyConfigurationScope(scope string) error {
if !strings.HasPrefix(scope, "/") {
return fmt.Errorf("Invalid scope %s: must be an absolute path", scope)
}
// Refuse also "/", otherwise "/" and "" would have the same semantics,
// and "" could be unexpectedly shadowed by the "/" entry.
if scope == "/" {
return errors.New(`Invalid scope "/": Use the generic default scope ""`)
}
return nil
}
// dirReference is an ImageReference for directory paths.
type dirReference struct {
// Note that the interpretation of paths below depends on the underlying filesystem state, which may change under us at any time!
path string // As specified by the user. May be relative, contain symlinks, etc.
// Either of the paths may point to a different, or no, inode over time. resolvedPath may contain symbolic links, and so on.
// Generally we follow the intent of the user, and use the "path" member for filesystem operations (e.g. the user can use a relative path to avoid
// being exposed to symlinks and renames in the parent directories to the working directory).
// (But in general, we make no attempt to be completely safe against concurrent hostile filesystem modifications.)
path string // As specified by the user. May be relative, contain symlinks, etc.
resolvedPath string // Absolute path with no symlinks, at least at the time of its creation. Primarily used for policy namespaces.
}
// There is no directory.ParseReference because it is rather pointless.
@ -32,8 +59,15 @@ type dirReference struct {
// can use directory.NewReference.
// NewReference returns a directory reference for a specified path.
func NewReference(path string) types.ImageReference {
return dirReference{path: path}
//
// We do not expose an API supplying the resolvedPath; we could, but recomputing it
// is generally cheap enough that we prefer being confident about the properties of resolvedPath.
func NewReference(path string) (types.ImageReference, error) {
resolved, err := explicitfilepath.ResolvePathToFullyExplicit(path)
if err != nil {
return nil, err
}
return dirReference{path: path, resolvedPath: resolved}, nil
}
func (ref dirReference) Transport() types.ImageTransport {
@ -56,6 +90,38 @@ func (ref dirReference) DockerReference() reference.Named {
return nil
}
// PolicyConfigurationIdentity returns a string representation of the reference, suitable for policy lookup.
// This MUST reflect user intent, not e.g. after processing of third-party redirects or aliases;
// The value SHOULD be fully explicit about its semantics, with no hidden defaults, AND canonical
// (i.e. various references with exactly the same semantics should return the same configuration identity)
// It is fine for the return value to be equal to StringWithinTransport(), and it is desirable but
// not required/guaranteed that it will be a valid input to Transport().ParseReference().
// Returns "" if configuration identities for these references are not supported.
func (ref dirReference) PolicyConfigurationIdentity() string {
return ref.resolvedPath
}
// PolicyConfigurationNamespaces returns a list of other policy configuration namespaces to search
// for if explicit configuration for PolicyConfigurationIdentity() is not set. The list will be processed
// in order, terminating on first match, and an implicit "" is always checked at the end.
// It is STRONGLY recommended for the first element, if any, to be a prefix of PolicyConfigurationIdentity(),
// and each following element to be a prefix of the element preceding it.
func (ref dirReference) PolicyConfigurationNamespaces() []string {
res := []string{}
path := ref.resolvedPath
for {
lastSlash := strings.LastIndex(path, "/")
if lastSlash == -1 || lastSlash == 0 {
break
}
path = path[:lastSlash]
res = append(res, path)
}
// Note that we do not include "/"; it is redundant with the default "" global default,
// and rejected by dirTransport.ValidatePolicyConfigurationScope above.
return res
}
// NewImage returns a types.Image for this reference.
func (ref dirReference) NewImage(certPath string, tlsVerify bool) (types.Image, error) {
src := newImageSource(ref)

View File

@ -0,0 +1,55 @@
package explicitfilepath
import (
"fmt"
"os"
"path/filepath"
)
// ResolvePathToFullyExplicit returns the input path converted to an absolute, no-symlinks, cleaned up path.
// To do so, all elements of the input path must exist; as a special case, the final component may be
// a non-existent name (but not a symlink pointing to a non-existent name)
// This is intended as a a helper for implementations of types.ImageReference.PolicyConfigurationIdentity etc.
func ResolvePathToFullyExplicit(path string) (string, error) {
switch _, err := os.Lstat(path); {
case err == nil:
return resolveExistingPathToFullyExplicit(path)
case os.IsNotExist(err):
parent, file := filepath.Split(path)
resolvedParent, err := resolveExistingPathToFullyExplicit(parent)
if err != nil {
return "", err
}
if file == "." || file == ".." {
// Coverage: This can happen, but very rarely: if we have successfully resolved the parent, both "." and ".." in it should have been resolved as well.
// This can still happen if there is a filesystem race condition, causing the Lstat() above to fail but the later resolution to succeed.
// We do not care to promise anything if such filesystem race conditions can happen, but we definitely don't want to return "."/".." components
// in the resulting path, and especially not at the end.
return "", fmt.Errorf("Unexpectedly missing special filename component in %s", path)
}
resolvedPath := filepath.Join(resolvedParent, file)
// As a sanity check, ensure that there are no "." or ".." components.
cleanedResolvedPath := filepath.Clean(resolvedPath)
if cleanedResolvedPath != resolvedPath {
// Coverage: This should never happen.
return "", fmt.Errorf("Internal inconsistency: Path %s resolved to %s still cleaned up to %s", path, resolvedPath, cleanedResolvedPath)
}
return resolvedPath, nil
default: // err != nil, unrecognized
return "", err
}
}
// resolveExistingPathToFullyExplicit is the same as ResolvePathToFullyExplicit,
// but without the special case for missing final component.
func resolveExistingPathToFullyExplicit(path string) (string, error) {
resolved, err := filepath.Abs(path)
if err != nil {
return "", err // Coverage: This can fail only if os.Getwd() fails.
}
resolved, err = filepath.EvalSymlinks(resolved)
if err != nil {
return "", err
}
return filepath.Clean(resolved), nil
}

View File

@ -11,6 +11,7 @@ import (
"os"
"path/filepath"
"strings"
"time"
"github.com/Sirupsen/logrus"
"github.com/docker/docker/pkg/homedir"
@ -39,7 +40,7 @@ type dockerClient struct {
password string
wwwAuthenticate string // Cache of a value set by ping() if scheme is not empty
scheme string // Cache of a value returned by a successful ping() if not empty
transport *http.Transport
client *http.Client
}
// newDockerClient returns a new dockerClient instance for refHostname (a host a specified in the Docker image reference, not canonicalized to dockerRegistry)
@ -70,11 +71,17 @@ func newDockerClient(refHostname, certPath string, tlsVerify bool) (*dockerClien
TLSClientConfig: tlsc,
}
}
client := &http.Client{
Timeout: 1 * time.Minute,
}
if tr != nil {
client.Transport = tr
}
return &dockerClient{
registry: registry,
username: username,
password: password,
transport: tr,
registry: registry,
username: username,
password: password,
client: client,
}, nil
}
@ -112,12 +119,8 @@ func (c *dockerClient) makeRequestToResolvedURL(method, url string, headers map[
return nil, err
}
}
client := &http.Client{}
if c.transport != nil {
client.Transport = c.transport
}
logrus.Debugf("%s %s", method, url)
res, err := client.Do(req)
res, err := c.client.Do(req)
if err != nil {
return nil, err
}
@ -134,11 +137,7 @@ func (c *dockerClient) setupRequestAuth(req *http.Request) error {
req.SetBasicAuth(c.username, c.password)
return nil
case "Bearer":
client := &http.Client{}
if c.transport != nil {
client.Transport = c.transport
}
res, err := client.Do(req)
res, err := c.client.Do(req)
if err != nil {
return err
}
@ -294,13 +293,9 @@ type pingResponse struct {
}
func (c *dockerClient) ping() (*pingResponse, error) {
client := &http.Client{}
if c.transport != nil {
client.Transport = c.transport
}
ping := func(scheme string) (*pingResponse, error) {
url := fmt.Sprintf(baseURL, scheme, c.registry)
resp, err := client.Get(url)
resp, err := c.client.Get(url)
logrus.Debugf("Ping %s err %#v", url, err)
if err != nil {
return nil, err

View File

@ -4,6 +4,7 @@ import (
"fmt"
"strings"
"github.com/containers/image/docker/policyconfiguration"
"github.com/containers/image/types"
"github.com/docker/docker/reference"
)
@ -22,6 +23,17 @@ func (t dockerTransport) ParseReference(reference string) (types.ImageReference,
return ParseReference(reference)
}
// ValidatePolicyConfigurationScope checks that scope is a valid name for a signature.PolicyTransportScopes keys
// (i.e. a valid PolicyConfigurationIdentity() or PolicyConfigurationNamespaces() return value).
// It is acceptable to allow an invalid value which will never be matched, it can "only" cause user confusion.
// scope passed to this function will not be "", that value is always allowed.
func (t dockerTransport) ValidatePolicyConfigurationScope(scope string) error {
// FIXME? We could be verifying the various character set and length restrictions
// from docker/distribution/reference.regexp.go, but other than that there
// are few semantically invalid strings.
return nil
}
// dockerReference is an ImageReference for Docker images.
type dockerReference struct {
ref reference.Named // By construction we know that !reference.IsNameOnly(ref)
@ -79,6 +91,30 @@ func (ref dockerReference) DockerReference() reference.Named {
return ref.ref
}
// PolicyConfigurationIdentity returns a string representation of the reference, suitable for policy lookup.
// This MUST reflect user intent, not e.g. after processing of third-party redirects or aliases;
// The value SHOULD be fully explicit about its semantics, with no hidden defaults, AND canonical
// (i.e. various references with exactly the same semantics should return the same configuration identity)
// It is fine for the return value to be equal to StringWithinTransport(), and it is desirable but
// not required/guaranteed that it will be a valid input to Transport().ParseReference().
// Returns "" if configuration identities for these references are not supported.
func (ref dockerReference) PolicyConfigurationIdentity() string {
res, err := policyconfiguration.DockerReferenceIdentity(ref.ref)
if res == "" || err != nil { // Coverage: Should never happen, NewReference above should refuse values which could cause a failure.
panic(fmt.Sprintf("Internal inconsistency: policyconfiguration.DockerReferenceIdentity returned %#v, %v", res, err))
}
return res
}
// PolicyConfigurationNamespaces returns a list of other policy configuration namespaces to search
// for if explicit configuration for PolicyConfigurationIdentity() is not set. The list will be processed
// in order, terminating on first match, and an implicit "" is always checked at the end.
// It is STRONGLY recommended for the first element, if any, to be a prefix of PolicyConfigurationIdentity(),
// and each following element to be a prefix of the element preceding it.
func (ref dockerReference) PolicyConfigurationNamespaces() []string {
return policyconfiguration.DockerReferenceNamespaces(ref.ref)
}
// NewImage returns a types.Image for this reference.
func (ref dockerReference) NewImage(certPath string, tlsVerify bool) (types.Image, error) {
return newImage(ref, certPath, tlsVerify)

View File

@ -0,0 +1,57 @@
package policyconfiguration
import (
"errors"
"fmt"
"strings"
"github.com/docker/docker/reference"
)
// DockerReferenceIdentity returns a string representation of the reference, suitable for policy lookup,
// as a backend for ImageReference.PolicyConfigurationIdentity.
// The reference must satisfy !reference.IsNameOnly().
func DockerReferenceIdentity(ref reference.Named) (string, error) {
res := ref.FullName()
tagged, isTagged := ref.(reference.NamedTagged)
digested, isDigested := ref.(reference.Canonical)
switch {
case isTagged && isDigested: // This should not happen, docker/reference.ParseNamed drops the tag.
return "", fmt.Errorf("Unexpected Docker reference %s with both a name and a digest", ref.String())
case !isTagged && !isDigested: // This should not happen, the caller is expected to ensure !reference.IsNameOnly()
return "", fmt.Errorf("Internal inconsistency: Docker reference %s with neither a tag nor a digest", ref.String())
case isTagged:
res = res + ":" + tagged.Tag()
case isDigested:
res = res + "@" + digested.Digest().String()
default: // Coverage: The above was supposed to be exhaustive.
return "", errors.New("Internal inconsistency, unexpected default branch")
}
return res, nil
}
// DockerReferenceNamespaces returns a list of other policy configuration namespaces to search,
// as a backend for ImageReference.PolicyConfigurationIdentity.
// The reference must satisfy !reference.IsNameOnly().
func DockerReferenceNamespaces(ref reference.Named) []string {
// Look for a match of the repository, and then of the possible parent
// namespaces. Note that this only happens on the expanded host names
// and repository names, i.e. "busybox" is looked up as "docker.io/library/busybox",
// then in its parent "docker.io/library"; in none of "busybox",
// un-namespaced "library" nor in "" supposedly implicitly representing "library/".
//
// ref.FullName() == ref.Hostname() + "/" + ref.RemoteName(), so the last
// iteration matches the host name (for any namespace).
res := []string{}
name := ref.FullName()
for {
res = append(res, name)
lastSlash := strings.LastIndex(name, "/")
if lastSlash == -1 {
break
}
name = name[:lastSlash]
}
return res
}

View File

@ -6,6 +6,7 @@ import (
"regexp"
"strings"
"github.com/containers/image/directory/explicitfilepath"
"github.com/containers/image/types"
"github.com/docker/docker/reference"
)
@ -24,14 +25,53 @@ func (t ociTransport) ParseReference(reference string) (types.ImageReference, er
return ParseReference(reference)
}
var refRegexp = regexp.MustCompile(`^([A-Za-z0-9._-]+)+$`)
// ValidatePolicyConfigurationScope checks that scope is a valid name for a signature.PolicyTransportScopes keys
// (i.e. a valid PolicyConfigurationIdentity() or PolicyConfigurationNamespaces() return value).
// It is acceptable to allow an invalid value which will never be matched, it can "only" cause user confusion.
// scope passed to this function will not be "", that value is always allowed.
func (t ociTransport) ValidatePolicyConfigurationScope(scope string) error {
var dir string
sep := strings.LastIndex(scope, ":")
if sep == -1 {
dir = scope
} else {
dir = scope[:sep]
tag := scope[sep+1:]
if !refRegexp.MatchString(tag) {
return fmt.Errorf("Invalid tag %s", tag)
}
}
if strings.Contains(dir, ":") {
return fmt.Errorf("Invalid OCI reference %s: path contains a colon", scope)
}
if !strings.HasPrefix(dir, "/") {
return fmt.Errorf("Invalid scope %s: must be an absolute path", scope)
}
// Refuse also "/", otherwise "/" and "" would have the same semantics,
// and "" could be unexpectedly shadowed by the "/" entry.
// (Note: we do allow "/:sometag", a bit ridiculous but why refuse it?)
if scope == "/" {
return errors.New(`Invalid scope "/": Use the generic default scope ""`)
}
return nil
}
// ociReference is an ImageReference for OCI directory paths.
type ociReference struct {
// Note that the interpretation of paths below depends on the underlying filesystem state, which may change under us at any time!
dir string // As specified by the user. May be relative, contain symlinks, etc.
tag string
}
// Either of the paths may point to a different, or no, inode over time. resolvedDir may contain symbolic links, and so on.
var refRegexp = regexp.MustCompile(`^([A-Za-z0-9._-]+)+$`)
// Generally we follow the intent of the user, and use the "dir" member for filesystem operations (e.g. the user can use a relative path to avoid
// being exposed to symlinks and renames in the parent directories to the working directory).
// (But in general, we make no attempt to be completely safe against concurrent hostile filesystem modifications.)
dir string // As specified by the user. May be relative, contain symlinks, etc.
resolvedDir string // Absolute path with no symlinks, at least at the time of its creation. Primarily used for policy namespaces.
tag string
}
// ParseReference converts a string, which should not start with the ImageTransport.Name prefix, into an OCI ImageReference.
func ParseReference(reference string) (types.ImageReference, error) {
@ -47,12 +87,24 @@ func ParseReference(reference string) (types.ImageReference, error) {
return nil, fmt.Errorf("Invalid tag %s", tag)
}
}
return NewReference(dir, tag), nil
return NewReference(dir, tag)
}
// NewReference returns an OCI reference for a directory and a tag.
func NewReference(dir, tag string) types.ImageReference {
return ociReference{dir: dir, tag: tag}
//
// We do not expose an API supplying the resolvedDir; we could, but recomputing it
// is generally cheap enough that we prefer being confident about the properties of resolvedDir.
func NewReference(dir, tag string) (types.ImageReference, error) {
resolved, err := explicitfilepath.ResolvePathToFullyExplicit(dir)
if err != nil {
return nil, err
}
// This is necessary to prevent directory paths returned by PolicyConfigurationNamespaces
// from being ambiguous with values of PolicyConfigurationIdentity.
if strings.Contains(resolved, ":") {
return nil, fmt.Errorf("Invalid OCI reference %s:%s: path %s contains a colon", dir, tag, resolved)
}
return ociReference{dir: dir, resolvedDir: resolved, tag: tag}, nil
}
func (ref ociReference) Transport() types.ImageTransport {
@ -75,6 +127,38 @@ func (ref ociReference) DockerReference() reference.Named {
return nil
}
// PolicyConfigurationIdentity returns a string representation of the reference, suitable for policy lookup.
// This MUST reflect user intent, not e.g. after processing of third-party redirects or aliases;
// The value SHOULD be fully explicit about its semantics, with no hidden defaults, AND canonical
// (i.e. various references with exactly the same semantics should return the same configuration identity)
// It is fine for the return value to be equal to StringWithinTransport(), and it is desirable but
// not required/guaranteed that it will be a valid input to Transport().ParseReference().
// Returns "" if configuration identities for these references are not supported.
func (ref ociReference) PolicyConfigurationIdentity() string {
return fmt.Sprintf("%s:%s", ref.resolvedDir, ref.tag)
}
// PolicyConfigurationNamespaces returns a list of other policy configuration namespaces to search
// for if explicit configuration for PolicyConfigurationIdentity() is not set. The list will be processed
// in order, terminating on first match, and an implicit "" is always checked at the end.
// It is STRONGLY recommended for the first element, if any, to be a prefix of PolicyConfigurationIdentity(),
// and each following element to be a prefix of the element preceding it.
func (ref ociReference) PolicyConfigurationNamespaces() []string {
res := []string{}
path := ref.resolvedDir
for {
lastSlash := strings.LastIndex(path, "/")
// Note that we do not include "/"; it is redundant with the default "" global default,
// and rejected by ociTransport.ValidatePolicyConfigurationScope above.
if lastSlash == -1 || path == "/" {
break
}
res = append(res, path)
path = path[:lastSlash]
}
return res
}
// NewImage returns a types.Image for this reference.
func (ref ociReference) NewImage(certPath string, tlsVerify bool) (types.Image, error) {
return nil, errors.New("Full Image support not implemented for oci: image names")

View File

@ -9,6 +9,7 @@ import (
"io/ioutil"
"net/http"
"strings"
"time"
"github.com/Sirupsen/logrus"
"github.com/containers/image/docker"
@ -52,6 +53,7 @@ func newOpenshiftClient(ref openshiftReference) (*openshiftClient, error) {
if *baseURL != *ref.baseURL {
return nil, fmt.Errorf("Unexpected baseURL mismatch: default %#v, reference %#v", *baseURL, *ref.baseURL)
}
httpClient.Timeout = 1 * time.Minute
return &openshiftClient{
ref: ref,

View File

@ -7,6 +7,7 @@ import (
"regexp"
"github.com/Sirupsen/logrus"
"github.com/containers/image/docker/policyconfiguration"
"github.com/containers/image/types"
"github.com/docker/docker/reference"
)
@ -25,6 +26,22 @@ func (t openshiftTransport) ParseReference(reference string) (types.ImageReferen
return ParseReference(reference)
}
// Note that imageNameRegexp is namespace/stream:tag, this
// is HOSTNAME/namespace/stream:tag or parent prefixes.
// Keep this in sync with imageNameRegexp!
var scopeRegexp = regexp.MustCompile("^[^/]*(/[^:/]*(/[^:/]*(:[^:/]*)?)?)?$")
// ValidatePolicyConfigurationScope checks that scope is a valid name for a signature.PolicyTransportScopes keys
// (i.e. a valid PolicyConfigurationIdentity() or PolicyConfigurationNamespaces() return value).
// It is acceptable to allow an invalid value which will never be matched, it can "only" cause user confusion.
// scope passed to this function will not be "", that value is always allowed.
func (t openshiftTransport) ValidatePolicyConfigurationScope(scope string) error {
if scopeRegexp.FindStringIndex(scope) == nil {
return fmt.Errorf("Invalid scope name %s", scope)
}
return nil
}
// openshiftReference is an ImageReference for OpenShift images.
type openshiftReference struct {
baseURL *url.URL
@ -35,6 +52,7 @@ type openshiftReference struct {
}
// FIXME: Is imageName like this a good way to refer to OpenShift images?
// Keep this in sync with scopeRegexp!
var imageNameRegexp = regexp.MustCompile("^([^:/]*)/([^:/]*):([^:/]*)$")
// ParseReference converts a string, which should not start with the ImageTransport.Name prefix, into an OpenShift ImageReference.
@ -111,6 +129,30 @@ func (ref openshiftReference) DockerReference() reference.Named {
return ref.dockerReference
}
// PolicyConfigurationIdentity returns a string representation of the reference, suitable for policy lookup.
// This MUST reflect user intent, not e.g. after processing of third-party redirects or aliases;
// The value SHOULD be fully explicit about its semantics, with no hidden defaults, AND canonical
// (i.e. various references with exactly the same semantics should return the same configuration identity)
// It is fine for the return value to be equal to StringWithinTransport(), and it is desirable but
// not required/guaranteed that it will be a valid input to Transport().ParseReference().
// Returns "" if configuration identities for these references are not supported.
func (ref openshiftReference) PolicyConfigurationIdentity() string {
res, err := policyconfiguration.DockerReferenceIdentity(ref.dockerReference)
if res == "" || err != nil { // Coverage: Should never happen, NewReference constructs a valid tagged reference.
panic(fmt.Sprintf("Internal inconsistency: policyconfiguration.DockerReferenceIdentity returned %#v, %v", res, err))
}
return res
}
// PolicyConfigurationNamespaces returns a list of other policy configuration namespaces to search
// for if explicit configuration for PolicyConfigurationIdentity() is not set. The list will be processed
// in order, terminating on first match, and an implicit "" is always checked at the end.
// It is STRONGLY recommended for the first element, if any, to be a prefix of PolicyConfigurationIdentity(),
// and each following element to be a prefix of the element preceding it.
func (ref openshiftReference) PolicyConfigurationNamespaces() []string {
return policyconfiguration.DockerReferenceNamespaces(ref.dockerReference)
}
// NewImage returns a types.Image for this reference.
func (ref openshiftReference) NewImage(certPath string, tlsVerify bool) (types.Image, error) {
return nil, errors.New("Full Image support not implemented for atomic: image names")

View File

@ -59,7 +59,7 @@ func stringField(m map[string]interface{}, fieldName string) (string, error) {
// (including duplicated keys, unrecognized keys, and non-matching types). Uses fieldResolver to
// determine the destination for a field value, which should return a pointer to the destination if valid, or nil if the key is rejected.
//
// The fieldResolver approach is useful for decoding the Policy.Specific map; using it for structs is a bit lazy,
// The fieldResolver approach is useful for decoding the Policy.Transports map; using it for structs is a bit lazy,
// we could use reflection to automate this. Later?
func paranoidUnmarshalJSONObject(data []byte, fieldResolver func(string) interface{}) error {
seenKeys := map[string]struct{}{}

View File

@ -15,9 +15,12 @@ package signature
import (
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"github.com/containers/image/transports"
"github.com/containers/image/types"
"github.com/docker/docker/reference"
)
@ -55,13 +58,13 @@ var _ json.Unmarshaler = (*Policy)(nil)
// UnmarshalJSON implements the json.Unmarshaler interface.
func (p *Policy) UnmarshalJSON(data []byte) error {
*p = Policy{}
specific := policySpecificMap{}
transports := policyTransportsMap{}
if err := paranoidUnmarshalJSONObject(data, func(key string) interface{} {
switch key {
case "default":
return &p.Default
case "specific":
return &specific
case "transports":
return &transports
default:
return nil
}
@ -72,29 +75,79 @@ func (p *Policy) UnmarshalJSON(data []byte) error {
if p.Default == nil {
return InvalidPolicyFormatError("Default policy is missing")
}
p.Specific = map[string]PolicyRequirements(specific)
p.Transports = map[string]PolicyTransportScopes(transports)
return nil
}
// policySpecificMap is a specialization of this map type for the strict JSON parsing semantics appropriate for the Policy.Specific member.
type policySpecificMap map[string]PolicyRequirements
// policyTransportsMap is a specialization of this map type for the strict JSON parsing semantics appropriate for the Policy.Transports member.
type policyTransportsMap map[string]PolicyTransportScopes
// Compile-time check that policySpecificMap implements json.Unmarshaler.
var _ json.Unmarshaler = (*policySpecificMap)(nil)
// Compile-time check that policyTransportsMap implements json.Unmarshaler.
var _ json.Unmarshaler = (*policyTransportsMap)(nil)
// UnmarshalJSON implements the json.Unmarshaler interface.
func (m *policySpecificMap) UnmarshalJSON(data []byte) error {
func (m *policyTransportsMap) UnmarshalJSON(data []byte) error {
// We can't unmarshal directly into map values because it is not possible to take an address of a map value.
// So, use a temporary map of pointers-to-slices and convert.
tmpMap := map[string]*PolicyTransportScopes{}
if err := paranoidUnmarshalJSONObject(data, func(key string) interface{} {
transport, ok := transports.KnownTransports[key]
if !ok {
return nil
}
// paranoidUnmarshalJSONObject detects key duplication for us, check just to be safe.
if _, ok := tmpMap[key]; ok {
return nil
}
ptsWithTransport := policyTransportScopesWithTransport{
transport: transport,
dest: &PolicyTransportScopes{}, // This allocates a new instance on each call.
}
tmpMap[key] = ptsWithTransport.dest
return &ptsWithTransport
}); err != nil {
return err
}
for key, ptr := range tmpMap {
(*m)[key] = *ptr
}
return nil
}
// Compile-time check that PolicyTransportScopes "implements"" json.Unmarshaler.
// we want to only use policyTransportScopesWithTransport
var _ json.Unmarshaler = (*PolicyTransportScopes)(nil)
// UnmarshalJSON implements the json.Unmarshaler interface.
func (m *PolicyTransportScopes) UnmarshalJSON(data []byte) error {
return errors.New("Do not try to unmarshal PolicyTransportScopes directly")
}
// policyTransportScopesWithTransport is a way to unmarshal a PolicyTransportScopes
// while validating using a specific ImageTransport.
type policyTransportScopesWithTransport struct {
transport types.ImageTransport
dest *PolicyTransportScopes
}
// Compile-time check that policyTransportScopesWithTransport implements json.Unmarshaler.
var _ json.Unmarshaler = (*policyTransportScopesWithTransport)(nil)
// UnmarshalJSON implements the json.Unmarshaler interface.
func (m *policyTransportScopesWithTransport) UnmarshalJSON(data []byte) error {
// We can't unmarshal directly into map values because it is not possible to take an address of a map value.
// So, use a temporary map of pointers-to-slices and convert.
tmpMap := map[string]*PolicyRequirements{}
if err := paranoidUnmarshalJSONObject(data, func(key string) interface{} {
// FIXME? We might want to validate the scope format.
// Note that reference.ParseNamed is unsuitable; it would understand "example.com" as
// "docker.io/library/example.com"
// paranoidUnmarshalJSONObject detects key duplication for us, check just to be safe.
if _, ok := tmpMap[key]; ok {
return nil
}
if key != "" {
if err := m.transport.ValidatePolicyConfigurationScope(key); err != nil {
return nil
}
}
ptr := &PolicyRequirements{} // This allocates a new instance on each call.
tmpMap[key] = ptr
return ptr
@ -102,7 +155,7 @@ func (m *policySpecificMap) UnmarshalJSON(data []byte) error {
return err
}
for key, ptr := range tmpMap {
(*m)[key] = *ptr
(*m.dest)[key] = *ptr
}
return nil
}

View File

@ -7,12 +7,9 @@ package signature
import (
"fmt"
"strings"
"github.com/Sirupsen/logrus"
"github.com/containers/image/transports"
"github.com/containers/image/types"
"github.com/docker/docker/reference"
)
// PolicyRequirementError is an explanatory text for rejecting a signature or an image.
@ -127,75 +124,41 @@ func (pc *PolicyContext) Destroy() error {
return pc.changeState(pcDestroying, pcDestroyed)
}
// fullyExpandedDockerReference converts a reference.Named into a fully expanded format;
// i.e. soft of an opposite to ref.String(), which is a fully canonicalized/minimized format.
// This is guaranteed to be the same as reference.FullName(), with a tag or digest appended, if available.
// FIXME? This feels like it should be provided by skopeo/reference.
func fullyExpandedDockerReference(ref reference.Named) (string, error) {
res := ref.FullName()
tagged, isTagged := ref.(reference.NamedTagged)
digested, isDigested := ref.(reference.Canonical)
// A github.com/distribution/reference value can have a tag and a digest at the same time!
// github.com/docker/reference does not handle that, so fail.
// (Even if it were supported, the semantics of policy namespaces are unclear - should we drop
// the tag or the digest first?)
switch {
case isTagged && isDigested:
// Coverage: This should currently not happen, the way docker/reference sets up types,
// isTagged and isDigested is mutually exclusive.
return "", fmt.Errorf("Names with both a tag and digest are not currently supported")
case isTagged:
res = res + ":" + tagged.Tag()
case isDigested:
res = res + "@" + digested.Digest().String()
default:
// res is already OK.
}
return res, nil
// policyIdentityLogName returns a string description of the image identity for policy purposes.
// ONLY use this for log messages, not for any decisions!
func policyIdentityLogName(ref types.ImageReference) string {
return ref.Transport().Name() + ":" + ref.PolicyConfigurationIdentity()
}
// requirementsForImage selects the appropriate requirements for image.
func (pc *PolicyContext) requirementsForImage(image types.Image) (PolicyRequirements, error) {
ref := image.Reference().DockerReference()
if ref == nil {
return nil, fmt.Errorf("Can not determine policy for image %s with no known Docker reference identity", transports.ImageName(image.Reference()))
}
ref = reference.WithDefaultTag(ref) // This should not be needed, but if we did receive a name-only reference, this is a reasonable thing to do.
// Look for a full match.
fullyExpanded, err := fullyExpandedDockerReference(ref)
if err != nil { // Coverage: This cannot currently happen.
return nil, err
}
if req, ok := pc.Policy.Specific[fullyExpanded]; ok {
logrus.Debugf(" Using specific policy section %s", fullyExpanded)
return req, nil
}
// Look for a match of the repository, and then of the possible parent
// namespaces. Note that this only happens on the expanded host names
// and repository names, i.e. "busybox" is looked up as "docker.io/library/busybox",
// then in its parent "docker.io/library"; in none of "busybox",
// un-namespaced "library" nor in "" implicitly representing "library/".
//
// ref.FullName() == ref.Hostname() + "/" + ref.RemoteName(), so the last
// iteration matches the host name (for any namespace).
name := ref.FullName()
for {
if req, ok := pc.Policy.Specific[name]; ok {
logrus.Debugf(" Using specific policy section %s", name)
return req, nil
// requirementsForImageRef selects the appropriate requirements for ref.
func (pc *PolicyContext) requirementsForImageRef(ref types.ImageReference) PolicyRequirements {
// Do we have a PolicyTransportScopes for this transport?
transportName := ref.Transport().Name()
if transportScopes, ok := pc.Policy.Transports[transportName]; ok {
// Look for a full match.
identity := ref.PolicyConfigurationIdentity()
if req, ok := transportScopes[identity]; ok {
logrus.Debugf(` Using transport "%s" policy section %s`, transportName, identity)
return req
}
lastSlash := strings.LastIndex(name, "/")
if lastSlash == -1 {
break
// Look for a match of the possible parent namespaces.
for _, name := range ref.PolicyConfigurationNamespaces() {
if req, ok := transportScopes[name]; ok {
logrus.Debugf(` Using transport "%s" specific policy section %s`, transportName, name)
return req
}
}
// Look for a default match for the transport.
if req, ok := transportScopes[""]; ok {
logrus.Debugf(` Using transport "%s" policy section ""`, transportName)
return req
}
name = name[:lastSlash]
}
logrus.Debugf(" Using default policy section")
return pc.Policy.Default, nil
return pc.Policy.Default
}
// GetSignaturesWithAcceptedAuthor returns those signatures from an image
@ -222,12 +185,8 @@ func (pc *PolicyContext) GetSignaturesWithAcceptedAuthor(image types.Image) (sig
}
}()
logrus.Debugf("GetSignaturesWithAcceptedAuthor for image %s", image.Reference().DockerReference())
reqs, err := pc.requirementsForImage(image)
if err != nil {
return nil, err
}
logrus.Debugf("GetSignaturesWithAcceptedAuthor for image %s", policyIdentityLogName(image.Reference()))
reqs := pc.requirementsForImageRef(image.Reference())
// FIXME: rename Signatures to UnverifiedSignatures
unverifiedSignatures, err := image.Signatures()
@ -306,12 +265,8 @@ func (pc *PolicyContext) IsRunningImageAllowed(image types.Image) (res bool, fin
}
}()
logrus.Debugf("IsRunningImageAllowed for image %s", image.Reference().DockerReference())
reqs, err := pc.requirementsForImage(image)
if err != nil {
return false, err
}
logrus.Debugf("IsRunningImageAllowed for image %s", policyIdentityLogName(image.Reference()))
reqs := pc.requirementsForImageRef(image.Reference())
if len(reqs) == 0 {
return false, PolicyRequirementError("List of verification policy requirements must not be empty")

View File

@ -8,18 +8,22 @@ package signature
// Policy defines requirements for considering a signature valid.
type Policy struct {
// Default applies to any image which does not have a matching policy in Specific.
Default PolicyRequirements `json:"default"`
// Specific applies to images matching scope, the map key.
// Scope is hostname[/zero/or/more/namespaces[/repository[:tag|@digest]]]; note that in order to be
// unambiguous, this must use a fully expanded format, e.g. "docker.io/library/busybox" or
// "docker.io/library", not "busybox" or "library".
// FIXME: Scope syntax - should it be namespaced docker:something ? Or, in the worst case, a composite object (we couldn't use a JSON map)
// Most specific scope wins, duplication is prohibited (hard failure).
// Defaults to an empty map if not specified.
Specific map[string]PolicyRequirements `json:"specific"`
// Default applies to any image which does not have a matching policy in Transports.
// Note that this can happen even if a matching PolicyTransportScopes exists in Transports
// if the image matches none of the scopes.
Default PolicyRequirements `json:"default"`
Transports map[string]PolicyTransportScopes `json:"transports"`
}
// PolicyTransportScopes defines policies for images for a specific transport,
// for various scopes, the map keys.
// Scopes are defined by the transport (types.ImageReference.PolicyConfigurationIdentity etc.);
// there is one scope precisely matching to a single image, and namespace scopes as prefixes
// of the single-image scope. (e.g. hostname[/zero[/or[/more[/namespaces[/individualimage]]]]])
// The empty scope, if exists, is considered a parent namespace of all other scopes.
// Most specific scope wins, duplication is prohibited (hard failure).
type PolicyTransportScopes map[string]PolicyRequirements
// PolicyRequirements is a set of requirements applying to a set of images; each of them must be satisfied (though perhaps each by a different signature).
// Must not be empty, frequently will only contain a single element.
type PolicyRequirements []PolicyRequirement

View File

@ -24,6 +24,11 @@ type ImageTransport interface {
Name() string
// ParseReference converts a string, which should not start with the ImageTransport.Name prefix, into an ImageReference.
ParseReference(reference string) (ImageReference, error)
// ValidatePolicyConfigurationScope checks that scope is a valid name for a signature.PolicyTransportScopes keys
// (i.e. a valid PolicyConfigurationIdentity() or PolicyConfigurationNamespaces() return value).
// It is acceptable to allow an invalid value which will never be matched, it can "only" cause user confusion.
// scope passed to this function will not be "", that value is always allowed.
ValidatePolicyConfigurationScope(scope string) error
}
// ImageReference is an abstracted way to refer to an image location, namespaced within an ImageTransport.
@ -49,6 +54,22 @@ type ImageReference interface {
// not e.g. after redirect or alias processing), or nil if unknown/not applicable.
DockerReference() reference.Named
// PolicyConfigurationIdentity returns a string representation of the reference, suitable for policy lookup.
// This MUST reflect user intent, not e.g. after processing of third-party redirects or aliases;
// The value SHOULD be fully explicit about its semantics, with no hidden defaults, AND canonical
// (i.e. various references with exactly the same semantics should return the same configuration identity)
// It is fine for the return value to be equal to StringWithinTransport(), and it is desirable but
// not required/guaranteed that it will be a valid input to Transport().ParseReference().
// Returns "" if configuration identities for these references are not supported.
PolicyConfigurationIdentity() string
// PolicyConfigurationNamespaces returns a list of other policy configuration namespaces to search
// for if explicit configuration for PolicyConfigurationIdentity() is not set. The list will be processed
// in order, terminating on first match, and an implicit "" is always checked at the end.
// It is STRONGLY recommended for the first element, if any, to be a prefix of PolicyConfigurationIdentity(),
// and each following element to be a prefix of the element preceding it.
PolicyConfigurationNamespaces() []string
// NewImage returns a types.Image for this reference.
NewImage(certPath string, tlsVerify bool) (Image, error)
// NewImageSource returns a types.ImageSource for this reference.