Merge pull request #8320 from dmcgowan/provenance_pull
Official image provenance pull flow
This commit is contained in:
commit
eaaf9e3125
21 changed files with 2146 additions and 112 deletions
|
@ -38,6 +38,7 @@ import (
|
|||
"github.com/docker/docker/pkg/sysinfo"
|
||||
"github.com/docker/docker/pkg/truncindex"
|
||||
"github.com/docker/docker/runconfig"
|
||||
"github.com/docker/docker/trust"
|
||||
"github.com/docker/docker/utils"
|
||||
"github.com/docker/docker/volumes"
|
||||
)
|
||||
|
@ -98,6 +99,7 @@ type Daemon struct {
|
|||
containerGraph *graphdb.Database
|
||||
driver graphdriver.Driver
|
||||
execDriver execdriver.Driver
|
||||
trustStore *trust.TrustStore
|
||||
}
|
||||
|
||||
// Install installs daemon capabilities to eng.
|
||||
|
@ -136,6 +138,9 @@ func (daemon *Daemon) Install(eng *engine.Engine) error {
|
|||
if err := daemon.Repositories().Install(eng); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := daemon.trustStore.Install(eng); err != nil {
|
||||
return err
|
||||
}
|
||||
// FIXME: this hack is necessary for legacy integration tests to access
|
||||
// the daemon object.
|
||||
eng.Hack_SetGlobalVar("httpapi.daemon", daemon)
|
||||
|
@ -835,6 +840,15 @@ func NewDaemonFromDirectory(config *Config, eng *engine.Engine) (*Daemon, error)
|
|||
return nil, fmt.Errorf("Couldn't create Tag store: %s", err)
|
||||
}
|
||||
|
||||
trustDir := path.Join(config.Root, "trust")
|
||||
if err := os.MkdirAll(trustDir, 0700); err != nil && !os.IsExist(err) {
|
||||
return nil, err
|
||||
}
|
||||
t, err := trust.NewTrustStore(trustDir)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not create trust store: %s", err)
|
||||
}
|
||||
|
||||
if !config.DisableNetwork {
|
||||
job := eng.Job("init_networkdriver")
|
||||
|
||||
|
@ -899,6 +913,7 @@ func NewDaemonFromDirectory(config *Config, eng *engine.Engine) (*Daemon, error)
|
|||
sysInitPath: sysInitPath,
|
||||
execDriver: ed,
|
||||
eng: eng,
|
||||
trustStore: t,
|
||||
}
|
||||
if err := daemon.checkLocaldns(); err != nil {
|
||||
return nil, err
|
||||
|
|
244
graph/pull.go
244
graph/pull.go
|
@ -1,10 +1,14 @@
|
|||
package graph
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"net/url"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
|
@ -13,8 +17,60 @@ import (
|
|||
"github.com/docker/docker/pkg/log"
|
||||
"github.com/docker/docker/registry"
|
||||
"github.com/docker/docker/utils"
|
||||
"github.com/docker/libtrust"
|
||||
)
|
||||
|
||||
func (s *TagStore) verifyManifest(eng *engine.Engine, manifestBytes []byte) (*registry.ManifestData, bool, error) {
|
||||
sig, err := libtrust.ParsePrettySignature(manifestBytes, "signatures")
|
||||
if err != nil {
|
||||
return nil, false, fmt.Errorf("error parsing payload: %s", err)
|
||||
}
|
||||
keys, err := sig.Verify()
|
||||
if err != nil {
|
||||
return nil, false, fmt.Errorf("error verifying payload: %s", err)
|
||||
}
|
||||
|
||||
payload, err := sig.Payload()
|
||||
if err != nil {
|
||||
return nil, false, fmt.Errorf("error retrieving payload: %s", err)
|
||||
}
|
||||
|
||||
var manifest registry.ManifestData
|
||||
if err := json.Unmarshal(payload, &manifest); err != nil {
|
||||
return nil, false, fmt.Errorf("error unmarshalling manifest: %s", err)
|
||||
}
|
||||
|
||||
var verified bool
|
||||
for _, key := range keys {
|
||||
job := eng.Job("trust_key_check")
|
||||
b, err := key.MarshalJSON()
|
||||
if err != nil {
|
||||
return nil, false, fmt.Errorf("error marshalling public key: %s", err)
|
||||
}
|
||||
namespace := manifest.Name
|
||||
if namespace[0] != '/' {
|
||||
namespace = "/" + namespace
|
||||
}
|
||||
stdoutBuffer := bytes.NewBuffer(nil)
|
||||
|
||||
job.Args = append(job.Args, namespace)
|
||||
job.Setenv("PublicKey", string(b))
|
||||
// Check key has read/write permission (0x03)
|
||||
job.SetenvInt("Permission", 0x03)
|
||||
job.Stdout.Add(stdoutBuffer)
|
||||
if err = job.Run(); err != nil {
|
||||
return nil, false, fmt.Errorf("error running key check: %s", err)
|
||||
}
|
||||
result := engine.Tail(stdoutBuffer, 1)
|
||||
log.Debugf("Key check result: %q", result)
|
||||
if result == "verified" {
|
||||
verified = true
|
||||
}
|
||||
}
|
||||
|
||||
return &manifest, verified, nil
|
||||
}
|
||||
|
||||
func (s *TagStore) CmdPull(job *engine.Job) engine.Status {
|
||||
if n := len(job.Args); n != 1 && n != 2 {
|
||||
return job.Errorf("Usage: %s IMAGE [TAG]", job.Name)
|
||||
|
@ -52,7 +108,7 @@ func (s *TagStore) CmdPull(job *engine.Job) engine.Status {
|
|||
return job.Error(err)
|
||||
}
|
||||
|
||||
endpoint, err := registry.ExpandAndVerifyRegistryUrl(hostname)
|
||||
endpoint, err := registry.NewEndpoint(hostname)
|
||||
if err != nil {
|
||||
return job.Error(err)
|
||||
}
|
||||
|
@ -62,14 +118,32 @@ func (s *TagStore) CmdPull(job *engine.Job) engine.Status {
|
|||
return job.Error(err)
|
||||
}
|
||||
|
||||
if endpoint == registry.IndexServerAddress() {
|
||||
var isOfficial bool
|
||||
if endpoint.VersionString(1) == registry.IndexServerAddress() {
|
||||
// If pull "index.docker.io/foo/bar", it's stored locally under "foo/bar"
|
||||
localName = remoteName
|
||||
|
||||
isOfficial = isOfficialName(remoteName)
|
||||
if isOfficial && strings.IndexRune(remoteName, '/') == -1 {
|
||||
remoteName = "library/" + remoteName
|
||||
}
|
||||
|
||||
// Use provided mirrors, if any
|
||||
mirrors = s.mirrors
|
||||
}
|
||||
|
||||
if isOfficial || endpoint.Version == registry.APIVersion2 {
|
||||
j := job.Eng.Job("trust_update_base")
|
||||
if err = j.Run(); err != nil {
|
||||
return job.Errorf("error updating trust base graph: %s", err)
|
||||
}
|
||||
|
||||
if err := s.pullV2Repository(job.Eng, r, job.Stdout, localName, remoteName, tag, sf, job.GetenvBool("parallel")); err == nil {
|
||||
return engine.StatusOK
|
||||
} else if err != registry.ErrDoesNotExist {
|
||||
log.Errorf("Error from V2 registry: %s", err)
|
||||
}
|
||||
}
|
||||
if err = s.pullRepository(r, job.Stdout, localName, remoteName, tag, sf, job.GetenvBool("parallel"), mirrors); err != nil {
|
||||
return job.Error(err)
|
||||
}
|
||||
|
@ -337,3 +411,169 @@ func WriteStatus(requestedTag string, out io.Writer, sf *utils.StreamFormatter,
|
|||
out.Write(sf.FormatStatus("", "Status: Image is up to date for %s", requestedTag))
|
||||
}
|
||||
}
|
||||
|
||||
// downloadInfo is used to pass information from download to extractor
|
||||
type downloadInfo struct {
|
||||
imgJSON []byte
|
||||
img *image.Image
|
||||
tmpFile *os.File
|
||||
length int64
|
||||
downloaded bool
|
||||
err chan error
|
||||
}
|
||||
|
||||
func (s *TagStore) pullV2Repository(eng *engine.Engine, r *registry.Session, out io.Writer, localName, remoteName, tag string, sf *utils.StreamFormatter, parallel bool) error {
|
||||
if tag == "" {
|
||||
log.Debugf("Pulling tag list from V2 registry for %s", remoteName)
|
||||
tags, err := r.GetV2RemoteTags(remoteName, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, t := range tags {
|
||||
if err := s.pullV2Tag(eng, r, out, localName, remoteName, t, sf, parallel); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if err := s.pullV2Tag(eng, r, out, localName, remoteName, tag, sf, parallel); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *TagStore) pullV2Tag(eng *engine.Engine, r *registry.Session, out io.Writer, localName, remoteName, tag string, sf *utils.StreamFormatter, parallel bool) error {
|
||||
log.Debugf("Pulling tag from V2 registry: %q", tag)
|
||||
manifestBytes, err := r.GetV2ImageManifest(remoteName, tag, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
manifest, verified, err := s.verifyManifest(eng, manifestBytes)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error verifying manifest: %s", err)
|
||||
}
|
||||
|
||||
if len(manifest.BlobSums) != len(manifest.History) {
|
||||
return fmt.Errorf("length of history not equal to number of layers")
|
||||
}
|
||||
|
||||
if verified {
|
||||
out.Write(sf.FormatStatus("", "The image you are pulling has been digitally signed by Docker, Inc."))
|
||||
}
|
||||
out.Write(sf.FormatStatus(tag, "Pulling from %s", localName))
|
||||
|
||||
downloads := make([]downloadInfo, len(manifest.BlobSums))
|
||||
|
||||
for i := len(manifest.BlobSums) - 1; i >= 0; i-- {
|
||||
var (
|
||||
sumStr = manifest.BlobSums[i]
|
||||
imgJSON = []byte(manifest.History[i])
|
||||
)
|
||||
|
||||
img, err := image.NewImgJSON(imgJSON)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to parse json: %s", err)
|
||||
}
|
||||
downloads[i].img = img
|
||||
|
||||
// Check if exists
|
||||
if s.graph.Exists(img.ID) {
|
||||
log.Debugf("Image already exists: %s", img.ID)
|
||||
continue
|
||||
}
|
||||
|
||||
chunks := strings.SplitN(sumStr, ":", 2)
|
||||
if len(chunks) < 2 {
|
||||
return fmt.Errorf("expected 2 parts in the sumStr, got %#v", chunks)
|
||||
}
|
||||
sumType, checksum := chunks[0], chunks[1]
|
||||
out.Write(sf.FormatProgress(utils.TruncateID(img.ID), "Pulling fs layer", nil))
|
||||
|
||||
downloadFunc := func(di *downloadInfo) error {
|
||||
log.Infof("pulling blob %q to V1 img %s", sumStr, img.ID)
|
||||
|
||||
if c, err := s.poolAdd("pull", "img:"+img.ID); err != nil {
|
||||
if c != nil {
|
||||
out.Write(sf.FormatProgress(utils.TruncateID(img.ID), "Layer already being pulled by another client. Waiting.", nil))
|
||||
<-c
|
||||
out.Write(sf.FormatProgress(utils.TruncateID(img.ID), "Download complete", nil))
|
||||
} else {
|
||||
log.Debugf("Image (id: %s) pull is already running, skipping: %v", img.ID, err)
|
||||
}
|
||||
} else {
|
||||
tmpFile, err := ioutil.TempFile("", "GetV2ImageBlob")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
r, l, err := r.GetV2ImageBlobReader(remoteName, sumType, checksum, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer r.Close()
|
||||
io.Copy(tmpFile, utils.ProgressReader(r, int(l), out, sf, false, utils.TruncateID(img.ID), "Downloading"))
|
||||
|
||||
out.Write(sf.FormatProgress(utils.TruncateID(img.ID), "Download complete", nil))
|
||||
|
||||
log.Debugf("Downloaded %s to tempfile %s", img.ID, tmpFile.Name())
|
||||
di.tmpFile = tmpFile
|
||||
di.length = l
|
||||
di.downloaded = true
|
||||
}
|
||||
di.imgJSON = imgJSON
|
||||
defer s.poolRemove("pull", "img:"+img.ID)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
if parallel {
|
||||
downloads[i].err = make(chan error)
|
||||
go func(di *downloadInfo) {
|
||||
di.err <- downloadFunc(di)
|
||||
}(&downloads[i])
|
||||
} else {
|
||||
err := downloadFunc(&downloads[i])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for i := len(downloads) - 1; i >= 0; i-- {
|
||||
d := &downloads[i]
|
||||
if d.err != nil {
|
||||
err := <-d.err
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if d.downloaded {
|
||||
// if tmpFile is empty assume download and extracted elsewhere
|
||||
defer os.Remove(d.tmpFile.Name())
|
||||
defer d.tmpFile.Close()
|
||||
d.tmpFile.Seek(0, 0)
|
||||
if d.tmpFile != nil {
|
||||
err = s.graph.Register(d.img, d.imgJSON,
|
||||
utils.ProgressReader(d.tmpFile, int(d.length), out, sf, false, utils.TruncateID(d.img.ID), "Extracting"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// FIXME: Pool release here for parallel tag pull (ensures any downloads block until fully extracted)
|
||||
}
|
||||
out.Write(sf.FormatProgress(utils.TruncateID(d.img.ID), "Pull complete", nil))
|
||||
|
||||
} else {
|
||||
out.Write(sf.FormatProgress(utils.TruncateID(d.img.ID), "Already exists", nil))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if err = s.Set(localName, tag, downloads[0].img.ID, true); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -214,7 +214,7 @@ func (s *TagStore) CmdPush(job *engine.Job) engine.Status {
|
|||
return job.Error(err)
|
||||
}
|
||||
|
||||
endpoint, err := registry.ExpandAndVerifyRegistryUrl(hostname)
|
||||
endpoint, err := registry.NewEndpoint(hostname)
|
||||
if err != nil {
|
||||
return job.Error(err)
|
||||
}
|
||||
|
@ -243,7 +243,7 @@ func (s *TagStore) CmdPush(job *engine.Job) engine.Status {
|
|||
|
||||
var token []string
|
||||
job.Stdout.Write(sf.FormatStatus("", "The push refers to an image: [%s]", localName))
|
||||
if _, err := s.pushImage(r, job.Stdout, remoteName, img.ID, endpoint, token, sf); err != nil {
|
||||
if _, err := s.pushImage(r, job.Stdout, remoteName, img.ID, endpoint.String(), token, sf); err != nil {
|
||||
return job.Error(err)
|
||||
}
|
||||
return engine.StatusOK
|
||||
|
|
|
@ -276,6 +276,20 @@ func (store *TagStore) GetRepoRefs() map[string][]string {
|
|||
return reporefs
|
||||
}
|
||||
|
||||
// isOfficialName returns whether a repo name is considered an official
|
||||
// repository. Official repositories are repos with names within
|
||||
// the library namespace or which default to the library namespace
|
||||
// by not providing one.
|
||||
func isOfficialName(name string) bool {
|
||||
if strings.HasPrefix(name, "library/") {
|
||||
return true
|
||||
}
|
||||
if strings.IndexRune(name, '/') == -1 {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Validate the name of a repository
|
||||
func validateRepoName(name string) error {
|
||||
if name == "" {
|
||||
|
|
|
@ -2,15 +2,16 @@ package graph
|
|||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"os"
|
||||
"path"
|
||||
"testing"
|
||||
|
||||
"github.com/docker/docker/daemon/graphdriver"
|
||||
_ "github.com/docker/docker/daemon/graphdriver/vfs" // import the vfs driver so it is used in the tests
|
||||
"github.com/docker/docker/image"
|
||||
"github.com/docker/docker/utils"
|
||||
"github.com/docker/docker/vendor/src/code.google.com/p/go/src/pkg/archive/tar"
|
||||
"io"
|
||||
"os"
|
||||
"path"
|
||||
"testing"
|
||||
)
|
||||
|
||||
const (
|
||||
|
@ -132,3 +133,18 @@ func TestInvalidTagName(t *testing.T) {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestOfficialName(t *testing.T) {
|
||||
names := map[string]bool{
|
||||
"library/ubuntu": true,
|
||||
"nonlibrary/ubuntu": false,
|
||||
"ubuntu": true,
|
||||
"other/library": false,
|
||||
}
|
||||
for name, isOfficial := range names {
|
||||
result := isOfficialName(name)
|
||||
if result != isOfficial {
|
||||
t.Errorf("Unexpected result for %s\n\tExpecting: %v\n\tActual: %v", name, isOfficial, result)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -51,7 +51,7 @@ clone hg code.google.com/p/go.net 84a4013f96e0
|
|||
|
||||
clone hg code.google.com/p/gosqlite 74691fb6f837
|
||||
|
||||
clone git github.com/docker/libtrust 136d534cc940
|
||||
clone git github.com/docker/libtrust d273ef2565ca
|
||||
|
||||
# get Go tip's archive/tar, for xattr support and improved performance
|
||||
# TODO after Go 1.4 drops, bump our minimum supported version and drop this vendored dep
|
||||
|
|
129
registry/endpoint.go
Normal file
129
registry/endpoint.go
Normal file
|
@ -0,0 +1,129 @@
|
|||
package registry
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"github.com/docker/docker/pkg/log"
|
||||
)
|
||||
|
||||
// scans string for api version in the URL path. returns the trimmed hostname, if version found, string and API version.
|
||||
func scanForApiVersion(hostname string) (string, APIVersion) {
|
||||
var (
|
||||
chunks []string
|
||||
apiVersionStr string
|
||||
)
|
||||
if strings.HasSuffix(hostname, "/") {
|
||||
chunks = strings.Split(hostname[:len(hostname)-1], "/")
|
||||
apiVersionStr = chunks[len(chunks)-1]
|
||||
} else {
|
||||
chunks = strings.Split(hostname, "/")
|
||||
apiVersionStr = chunks[len(chunks)-1]
|
||||
}
|
||||
for k, v := range apiVersions {
|
||||
if apiVersionStr == v {
|
||||
hostname = strings.Join(chunks[:len(chunks)-1], "/")
|
||||
return hostname, k
|
||||
}
|
||||
}
|
||||
return hostname, DefaultAPIVersion
|
||||
}
|
||||
|
||||
func NewEndpoint(hostname string) (*Endpoint, error) {
|
||||
var (
|
||||
endpoint Endpoint
|
||||
trimmedHostname string
|
||||
err error
|
||||
)
|
||||
if !strings.HasPrefix(hostname, "http") {
|
||||
hostname = "https://" + hostname
|
||||
}
|
||||
trimmedHostname, endpoint.Version = scanForApiVersion(hostname)
|
||||
endpoint.URL, err = url.Parse(trimmedHostname)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
endpoint.URL.Scheme = "https"
|
||||
if _, err := endpoint.Ping(); err != nil {
|
||||
log.Debugf("Registry %s does not work (%s), falling back to http", endpoint, err)
|
||||
// TODO: Check if http fallback is enabled
|
||||
endpoint.URL.Scheme = "http"
|
||||
if _, err = endpoint.Ping(); err != nil {
|
||||
return nil, errors.New("Invalid Registry endpoint: " + err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
return &endpoint, nil
|
||||
}
|
||||
|
||||
type Endpoint struct {
|
||||
URL *url.URL
|
||||
Version APIVersion
|
||||
}
|
||||
|
||||
// Get the formated URL for the root of this registry Endpoint
|
||||
func (e Endpoint) String() string {
|
||||
return fmt.Sprintf("%s/v%d/", e.URL.String(), e.Version)
|
||||
}
|
||||
|
||||
func (e Endpoint) VersionString(version APIVersion) string {
|
||||
return fmt.Sprintf("%s/v%d/", e.URL.String(), version)
|
||||
}
|
||||
|
||||
func (e Endpoint) Ping() (RegistryInfo, error) {
|
||||
if e.String() == IndexServerAddress() {
|
||||
// Skip the check, we now this one is valid
|
||||
// (and we never want to fallback to http in case of error)
|
||||
return RegistryInfo{Standalone: false}, nil
|
||||
}
|
||||
|
||||
req, err := http.NewRequest("GET", e.String()+"_ping", nil)
|
||||
if err != nil {
|
||||
return RegistryInfo{Standalone: false}, err
|
||||
}
|
||||
|
||||
resp, _, err := doRequest(req, nil, ConnectTimeout)
|
||||
if err != nil {
|
||||
return RegistryInfo{Standalone: false}, err
|
||||
}
|
||||
|
||||
defer resp.Body.Close()
|
||||
|
||||
jsonString, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return RegistryInfo{Standalone: false}, fmt.Errorf("Error while reading the http response: %s", err)
|
||||
}
|
||||
|
||||
// If the header is absent, we assume true for compatibility with earlier
|
||||
// versions of the registry. default to true
|
||||
info := RegistryInfo{
|
||||
Standalone: true,
|
||||
}
|
||||
if err := json.Unmarshal(jsonString, &info); err != nil {
|
||||
log.Debugf("Error unmarshalling the _ping RegistryInfo: %s", err)
|
||||
// don't stop here. Just assume sane defaults
|
||||
}
|
||||
if hdr := resp.Header.Get("X-Docker-Registry-Version"); hdr != "" {
|
||||
log.Debugf("Registry version header: '%s'", hdr)
|
||||
info.Version = hdr
|
||||
}
|
||||
log.Debugf("RegistryInfo.Version: %q", info.Version)
|
||||
|
||||
standalone := resp.Header.Get("X-Docker-Registry-Standalone")
|
||||
log.Debugf("Registry standalone header: '%s'", standalone)
|
||||
// Accepted values are "true" (case-insensitive) and "1".
|
||||
if strings.EqualFold(standalone, "true") || standalone == "1" {
|
||||
info.Standalone = true
|
||||
} else if len(standalone) > 0 {
|
||||
// there is a header set, and it is not "true" or "1", so assume fails
|
||||
info.Standalone = false
|
||||
}
|
||||
log.Debugf("RegistryInfo.Standalone: %t", info.Standalone)
|
||||
return info, nil
|
||||
}
|
|
@ -3,7 +3,6 @@ package registry
|
|||
import (
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
|
@ -15,13 +14,13 @@ import (
|
|||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/docker/docker/pkg/log"
|
||||
"github.com/docker/docker/utils"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrAlreadyExists = errors.New("Image already exists")
|
||||
ErrInvalidRepositoryName = errors.New("Invalid repository name (ex: \"registry.domain.tld/myrepos\")")
|
||||
ErrDoesNotExist = errors.New("Image does not exist")
|
||||
errLoginRequired = errors.New("Authentication is required.")
|
||||
validHex = regexp.MustCompile(`^([a-f0-9]{64})$`)
|
||||
validNamespace = regexp.MustCompile(`^([a-z0-9_]{4,30})$`)
|
||||
|
@ -152,55 +151,6 @@ func doRequest(req *http.Request, jar http.CookieJar, timeout TimeoutType) (*htt
|
|||
return nil, nil, nil
|
||||
}
|
||||
|
||||
func pingRegistryEndpoint(endpoint string) (RegistryInfo, error) {
|
||||
if endpoint == IndexServerAddress() {
|
||||
// Skip the check, we now this one is valid
|
||||
// (and we never want to fallback to http in case of error)
|
||||
return RegistryInfo{Standalone: false}, nil
|
||||
}
|
||||
|
||||
req, err := http.NewRequest("GET", endpoint+"_ping", nil)
|
||||
if err != nil {
|
||||
return RegistryInfo{Standalone: false}, err
|
||||
}
|
||||
|
||||
resp, _, err := doRequest(req, nil, ConnectTimeout)
|
||||
if err != nil {
|
||||
return RegistryInfo{Standalone: false}, err
|
||||
}
|
||||
|
||||
defer resp.Body.Close()
|
||||
|
||||
jsonString, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return RegistryInfo{Standalone: false}, fmt.Errorf("Error while reading the http response: %s", err)
|
||||
}
|
||||
|
||||
// If the header is absent, we assume true for compatibility with earlier
|
||||
// versions of the registry. default to true
|
||||
info := RegistryInfo{
|
||||
Standalone: true,
|
||||
}
|
||||
if err := json.Unmarshal(jsonString, &info); err != nil {
|
||||
log.Debugf("Error unmarshalling the _ping RegistryInfo: %s", err)
|
||||
// don't stop here. Just assume sane defaults
|
||||
}
|
||||
if hdr := resp.Header.Get("X-Docker-Registry-Version"); hdr != "" {
|
||||
log.Debugf("Registry version header: '%s'", hdr)
|
||||
info.Version = hdr
|
||||
}
|
||||
log.Debugf("RegistryInfo.Version: %q", info.Version)
|
||||
|
||||
standalone := resp.Header.Get("X-Docker-Registry-Standalone")
|
||||
log.Debugf("Registry standalone header: '%s'", standalone)
|
||||
if !strings.EqualFold(standalone, "true") && standalone != "1" && len(standalone) > 0 {
|
||||
// there is a header set, and it is not "true" or "1", so assume fails
|
||||
info.Standalone = false
|
||||
}
|
||||
log.Debugf("RegistryInfo.Standalone: %q", info.Standalone)
|
||||
return info, nil
|
||||
}
|
||||
|
||||
func validateRepositoryName(repositoryName string) error {
|
||||
var (
|
||||
namespace string
|
||||
|
@ -252,33 +202,6 @@ func ResolveRepositoryName(reposName string) (string, string, error) {
|
|||
return hostname, reposName, nil
|
||||
}
|
||||
|
||||
// this method expands the registry name as used in the prefix of a repo
|
||||
// to a full url. if it already is a url, there will be no change.
|
||||
// The registry is pinged to test if it http or https
|
||||
func ExpandAndVerifyRegistryUrl(hostname string) (string, error) {
|
||||
if strings.HasPrefix(hostname, "http:") || strings.HasPrefix(hostname, "https:") {
|
||||
// if there is no slash after https:// (8 characters) then we have no path in the url
|
||||
if strings.LastIndex(hostname, "/") < 9 {
|
||||
// there is no path given. Expand with default path
|
||||
hostname = hostname + "/v1/"
|
||||
}
|
||||
if _, err := pingRegistryEndpoint(hostname); err != nil {
|
||||
return "", errors.New("Invalid Registry endpoint: " + err.Error())
|
||||
}
|
||||
return hostname, nil
|
||||
}
|
||||
endpoint := fmt.Sprintf("https://%s/v1/", hostname)
|
||||
if _, err := pingRegistryEndpoint(endpoint); err != nil {
|
||||
log.Debugf("Registry %s does not work (%s), falling back to http", endpoint, err)
|
||||
endpoint = fmt.Sprintf("http://%s/v1/", hostname)
|
||||
if _, err = pingRegistryEndpoint(endpoint); err != nil {
|
||||
//TODO: triggering highland build can be done there without "failing"
|
||||
return "", errors.New("Invalid Registry endpoint: " + err.Error())
|
||||
}
|
||||
}
|
||||
return endpoint, nil
|
||||
}
|
||||
|
||||
func trustedLocation(req *http.Request) bool {
|
||||
var (
|
||||
trusteds = []string{"docker.com", "docker.io"}
|
||||
|
|
|
@ -83,6 +83,8 @@ var (
|
|||
|
||||
func init() {
|
||||
r := mux.NewRouter()
|
||||
|
||||
// /v1/
|
||||
r.HandleFunc("/v1/_ping", handlerGetPing).Methods("GET")
|
||||
r.HandleFunc("/v1/images/{image_id:[^/]+}/{action:json|layer|ancestry}", handlerGetImage).Methods("GET")
|
||||
r.HandleFunc("/v1/images/{image_id:[^/]+}/{action:json|layer|checksum}", handlerPutImage).Methods("PUT")
|
||||
|
@ -93,6 +95,10 @@ func init() {
|
|||
r.HandleFunc("/v1/repositories/{repository:.+}{action:/images|/}", handlerImages).Methods("GET", "PUT", "DELETE")
|
||||
r.HandleFunc("/v1/repositories/{repository:.+}/auth", handlerAuth).Methods("PUT")
|
||||
r.HandleFunc("/v1/search", handlerSearch).Methods("GET")
|
||||
|
||||
// /v2/
|
||||
r.HandleFunc("/v2/version", handlerGetPing).Methods("GET")
|
||||
|
||||
testHttpServer = httptest.NewServer(handlerAccessLog(r))
|
||||
}
|
||||
|
||||
|
|
|
@ -18,7 +18,11 @@ var (
|
|||
|
||||
func spawnTestRegistrySession(t *testing.T) *Session {
|
||||
authConfig := &AuthConfig{}
|
||||
r, err := NewSession(authConfig, utils.NewHTTPRequestFactory(), makeURL("/v1/"), true)
|
||||
endpoint, err := NewEndpoint(makeURL("/v1/"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
r, err := NewSession(authConfig, utils.NewHTTPRequestFactory(), endpoint, true)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
@ -26,7 +30,11 @@ func spawnTestRegistrySession(t *testing.T) *Session {
|
|||
}
|
||||
|
||||
func TestPingRegistryEndpoint(t *testing.T) {
|
||||
regInfo, err := pingRegistryEndpoint(makeURL("/v1/"))
|
||||
ep, err := NewEndpoint(makeURL("/v1/"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
regInfo, err := ep.Ping()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
@ -197,7 +205,7 @@ func TestPushImageJSONIndex(t *testing.T) {
|
|||
if repoData == nil {
|
||||
t.Fatal("Expected RepositoryData object")
|
||||
}
|
||||
repoData, err = r.PushImageJSONIndex("foo42/bar", imgData, true, []string{r.indexEndpoint})
|
||||
repoData, err = r.PushImageJSONIndex("foo42/bar", imgData, true, []string{r.indexEndpoint.String()})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
|
|
@ -40,11 +40,14 @@ func (s *Service) Auth(job *engine.Job) engine.Status {
|
|||
job.GetenvJson("authConfig", authConfig)
|
||||
// TODO: this is only done here because auth and registry need to be merged into one pkg
|
||||
if addr := authConfig.ServerAddress; addr != "" && addr != IndexServerAddress() {
|
||||
addr, err = ExpandAndVerifyRegistryUrl(addr)
|
||||
endpoint, err := NewEndpoint(addr)
|
||||
if err != nil {
|
||||
return job.Error(err)
|
||||
}
|
||||
authConfig.ServerAddress = addr
|
||||
if _, err := endpoint.Ping(); err != nil {
|
||||
return job.Error(err)
|
||||
}
|
||||
authConfig.ServerAddress = endpoint.String()
|
||||
}
|
||||
status, err := Login(authConfig, HTTPRequestFactory(nil))
|
||||
if err != nil {
|
||||
|
@ -86,11 +89,11 @@ func (s *Service) Search(job *engine.Job) engine.Status {
|
|||
if err != nil {
|
||||
return job.Error(err)
|
||||
}
|
||||
hostname, err = ExpandAndVerifyRegistryUrl(hostname)
|
||||
endpoint, err := NewEndpoint(hostname)
|
||||
if err != nil {
|
||||
return job.Error(err)
|
||||
}
|
||||
r, err := NewSession(authConfig, HTTPRequestFactory(metaHeaders), hostname, true)
|
||||
r, err := NewSession(authConfig, HTTPRequestFactory(metaHeaders), endpoint, true)
|
||||
if err != nil {
|
||||
return job.Error(err)
|
||||
}
|
||||
|
|
|
@ -25,15 +25,15 @@ import (
|
|||
type Session struct {
|
||||
authConfig *AuthConfig
|
||||
reqFactory *utils.HTTPRequestFactory
|
||||
indexEndpoint string
|
||||
indexEndpoint *Endpoint
|
||||
jar *cookiejar.Jar
|
||||
timeout TimeoutType
|
||||
}
|
||||
|
||||
func NewSession(authConfig *AuthConfig, factory *utils.HTTPRequestFactory, indexEndpoint string, timeout bool) (r *Session, err error) {
|
||||
func NewSession(authConfig *AuthConfig, factory *utils.HTTPRequestFactory, endpoint *Endpoint, timeout bool) (r *Session, err error) {
|
||||
r = &Session{
|
||||
authConfig: authConfig,
|
||||
indexEndpoint: indexEndpoint,
|
||||
indexEndpoint: endpoint,
|
||||
}
|
||||
|
||||
if timeout {
|
||||
|
@ -47,13 +47,13 @@ func NewSession(authConfig *AuthConfig, factory *utils.HTTPRequestFactory, index
|
|||
|
||||
// If we're working with a standalone private registry over HTTPS, send Basic Auth headers
|
||||
// alongside our requests.
|
||||
if indexEndpoint != IndexServerAddress() && strings.HasPrefix(indexEndpoint, "https://") {
|
||||
info, err := pingRegistryEndpoint(indexEndpoint)
|
||||
if r.indexEndpoint.VersionString(1) != IndexServerAddress() && r.indexEndpoint.URL.Scheme == "https" {
|
||||
info, err := r.indexEndpoint.Ping()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if info.Standalone {
|
||||
log.Debugf("Endpoint %s is eligible for private registry registry. Enabling decorator.", indexEndpoint)
|
||||
log.Debugf("Endpoint %s is eligible for private registry registry. Enabling decorator.", r.indexEndpoint.String())
|
||||
dec := utils.NewHTTPAuthDecorator(authConfig.Username, authConfig.Password)
|
||||
factory.AddDecorator(dec)
|
||||
}
|
||||
|
@ -261,8 +261,7 @@ func buildEndpointsList(headers []string, indexEp string) ([]string, error) {
|
|||
}
|
||||
|
||||
func (r *Session) GetRepositoryData(remote string) (*RepositoryData, error) {
|
||||
indexEp := r.indexEndpoint
|
||||
repositoryTarget := fmt.Sprintf("%srepositories/%s/images", indexEp, remote)
|
||||
repositoryTarget := fmt.Sprintf("%srepositories/%s/images", r.indexEndpoint.VersionString(1), remote)
|
||||
|
||||
log.Debugf("[registry] Calling GET %s", repositoryTarget)
|
||||
|
||||
|
@ -296,17 +295,13 @@ func (r *Session) GetRepositoryData(remote string) (*RepositoryData, error) {
|
|||
|
||||
var endpoints []string
|
||||
if res.Header.Get("X-Docker-Endpoints") != "" {
|
||||
endpoints, err = buildEndpointsList(res.Header["X-Docker-Endpoints"], indexEp)
|
||||
endpoints, err = buildEndpointsList(res.Header["X-Docker-Endpoints"], r.indexEndpoint.VersionString(1))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else {
|
||||
// Assume the endpoint is on the same host
|
||||
u, err := url.Parse(indexEp)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
endpoints = append(endpoints, fmt.Sprintf("%s://%s/v1/", u.Scheme, req.URL.Host))
|
||||
endpoints = append(endpoints, fmt.Sprintf("%s://%s/v1/", r.indexEndpoint.URL.Scheme, req.URL.Host))
|
||||
}
|
||||
|
||||
checksumsJSON, err := ioutil.ReadAll(res.Body)
|
||||
|
@ -474,7 +469,6 @@ func (r *Session) PushRegistryTag(remote, revision, tag, registry string, token
|
|||
|
||||
func (r *Session) PushImageJSONIndex(remote string, imgList []*ImgData, validate bool, regs []string) (*RepositoryData, error) {
|
||||
cleanImgList := []*ImgData{}
|
||||
indexEp := r.indexEndpoint
|
||||
|
||||
if validate {
|
||||
for _, elem := range imgList {
|
||||
|
@ -494,7 +488,7 @@ func (r *Session) PushImageJSONIndex(remote string, imgList []*ImgData, validate
|
|||
if validate {
|
||||
suffix = "images"
|
||||
}
|
||||
u := fmt.Sprintf("%srepositories/%s/%s", indexEp, remote, suffix)
|
||||
u := fmt.Sprintf("%srepositories/%s/%s", r.indexEndpoint.VersionString(1), remote, suffix)
|
||||
log.Debugf("[registry] PUT %s", u)
|
||||
log.Debugf("Image list pushed to index:\n%s", imgListJSON)
|
||||
req, err := r.reqFactory.NewRequest("PUT", u, bytes.NewReader(imgListJSON))
|
||||
|
@ -552,7 +546,7 @@ func (r *Session) PushImageJSONIndex(remote string, imgList []*ImgData, validate
|
|||
}
|
||||
|
||||
if res.Header.Get("X-Docker-Endpoints") != "" {
|
||||
endpoints, err = buildEndpointsList(res.Header["X-Docker-Endpoints"], indexEp)
|
||||
endpoints, err = buildEndpointsList(res.Header["X-Docker-Endpoints"], r.indexEndpoint.VersionString(1))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -578,7 +572,7 @@ func (r *Session) PushImageJSONIndex(remote string, imgList []*ImgData, validate
|
|||
|
||||
func (r *Session) SearchRepositories(term string) (*SearchResults, error) {
|
||||
log.Debugf("Index server: %s", r.indexEndpoint)
|
||||
u := r.indexEndpoint + "search?q=" + url.QueryEscape(term)
|
||||
u := r.indexEndpoint.VersionString(1) + "search?q=" + url.QueryEscape(term)
|
||||
req, err := r.reqFactory.NewRequest("GET", u, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
|
386
registry/session_v2.go
Normal file
386
registry/session_v2.go
Normal file
|
@ -0,0 +1,386 @@
|
|||
package registry
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/url"
|
||||
"strconv"
|
||||
|
||||
"github.com/docker/docker/pkg/log"
|
||||
"github.com/docker/docker/utils"
|
||||
"github.com/gorilla/mux"
|
||||
)
|
||||
|
||||
func newV2RegistryRouter() *mux.Router {
|
||||
router := mux.NewRouter()
|
||||
|
||||
v2Router := router.PathPrefix("/v2/").Subrouter()
|
||||
|
||||
// Version Info
|
||||
v2Router.Path("/version").Name("version")
|
||||
|
||||
// Image Manifests
|
||||
v2Router.Path("/manifest/{imagename:[a-z0-9-._/]+}/{tagname:[a-zA-Z0-9-._]+}").Name("manifests")
|
||||
|
||||
// List Image Tags
|
||||
v2Router.Path("/tags/{imagename:[a-z0-9-._/]+}").Name("tags")
|
||||
|
||||
// Download a blob
|
||||
v2Router.Path("/blob/{imagename:[a-z0-9-._/]+}/{sumtype:[a-z0-9_+-]+}/{sum:[a-fA-F0-9]{4,}}").Name("downloadBlob")
|
||||
|
||||
// Upload a blob
|
||||
v2Router.Path("/blob/{imagename:[a-z0-9-._/]+}/{sumtype:[a-z0-9_+-]+}").Name("uploadBlob")
|
||||
|
||||
// Mounting a blob in an image
|
||||
v2Router.Path("/mountblob/{imagename:[a-z0-9-._/]+}/{sumtype:[a-z0-9_+-]+}/{sum:[a-fA-F0-9]{4,}}").Name("mountBlob")
|
||||
|
||||
return router
|
||||
}
|
||||
|
||||
// APIVersion2 /v2/
|
||||
var v2HTTPRoutes = newV2RegistryRouter()
|
||||
|
||||
func getV2URL(e *Endpoint, routeName string, vars map[string]string) (*url.URL, error) {
|
||||
route := v2HTTPRoutes.Get(routeName)
|
||||
if route == nil {
|
||||
return nil, fmt.Errorf("unknown regisry v2 route name: %q", routeName)
|
||||
}
|
||||
|
||||
varReplace := make([]string, 0, len(vars)*2)
|
||||
for key, val := range vars {
|
||||
varReplace = append(varReplace, key, val)
|
||||
}
|
||||
|
||||
routePath, err := route.URLPath(varReplace...)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to make registry route %q with vars %v: %s", routeName, vars, err)
|
||||
}
|
||||
|
||||
return &url.URL{
|
||||
Scheme: e.URL.Scheme,
|
||||
Host: e.URL.Host,
|
||||
Path: routePath.Path,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// V2 Provenance POC
|
||||
|
||||
func (r *Session) GetV2Version(token []string) (*RegistryInfo, error) {
|
||||
routeURL, err := getV2URL(r.indexEndpoint, "version", nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
method := "GET"
|
||||
log.Debugf("[registry] Calling %q %s", method, routeURL.String())
|
||||
|
||||
req, err := r.reqFactory.NewRequest(method, routeURL.String(), nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
setTokenAuth(req, token)
|
||||
res, _, err := r.doRequest(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer res.Body.Close()
|
||||
if res.StatusCode != 200 {
|
||||
return nil, utils.NewHTTPRequestError(fmt.Sprintf("Server error: %d fetching Version", res.StatusCode), res)
|
||||
}
|
||||
|
||||
decoder := json.NewDecoder(res.Body)
|
||||
versionInfo := new(RegistryInfo)
|
||||
|
||||
err = decoder.Decode(versionInfo)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to decode GetV2Version JSON response: %s", err)
|
||||
}
|
||||
|
||||
return versionInfo, nil
|
||||
}
|
||||
|
||||
//
|
||||
// 1) Check if TarSum of each layer exists /v2/
|
||||
// 1.a) if 200, continue
|
||||
// 1.b) if 300, then push the
|
||||
// 1.c) if anything else, err
|
||||
// 2) PUT the created/signed manifest
|
||||
//
|
||||
func (r *Session) GetV2ImageManifest(imageName, tagName string, token []string) ([]byte, error) {
|
||||
vars := map[string]string{
|
||||
"imagename": imageName,
|
||||
"tagname": tagName,
|
||||
}
|
||||
|
||||
routeURL, err := getV2URL(r.indexEndpoint, "manifests", vars)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
method := "GET"
|
||||
log.Debugf("[registry] Calling %q %s", method, routeURL.String())
|
||||
|
||||
req, err := r.reqFactory.NewRequest(method, routeURL.String(), nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
setTokenAuth(req, token)
|
||||
res, _, err := r.doRequest(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer res.Body.Close()
|
||||
if res.StatusCode != 200 {
|
||||
if res.StatusCode == 401 {
|
||||
return nil, errLoginRequired
|
||||
} else if res.StatusCode == 404 {
|
||||
return nil, ErrDoesNotExist
|
||||
}
|
||||
return nil, utils.NewHTTPRequestError(fmt.Sprintf("Server error: %d trying to fetch for %s:%s", res.StatusCode, imageName, tagName), res)
|
||||
}
|
||||
|
||||
buf, err := ioutil.ReadAll(res.Body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Error while reading the http response: %s", err)
|
||||
}
|
||||
return buf, nil
|
||||
}
|
||||
|
||||
// - Succeeded to mount for this image scope
|
||||
// - Failed with no error (So continue to Push the Blob)
|
||||
// - Failed with error
|
||||
func (r *Session) PostV2ImageMountBlob(imageName, sumType, sum string, token []string) (bool, error) {
|
||||
vars := map[string]string{
|
||||
"imagename": imageName,
|
||||
"sumtype": sumType,
|
||||
"sum": sum,
|
||||
}
|
||||
|
||||
routeURL, err := getV2URL(r.indexEndpoint, "mountBlob", vars)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
method := "POST"
|
||||
log.Debugf("[registry] Calling %q %s", method, routeURL.String())
|
||||
|
||||
req, err := r.reqFactory.NewRequest(method, routeURL.String(), nil)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
setTokenAuth(req, token)
|
||||
res, _, err := r.doRequest(req)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
res.Body.Close() // close early, since we're not needing a body on this call .. yet?
|
||||
switch res.StatusCode {
|
||||
case 200:
|
||||
// return something indicating no push needed
|
||||
return true, nil
|
||||
case 300:
|
||||
// return something indicating blob push needed
|
||||
return false, nil
|
||||
}
|
||||
return false, fmt.Errorf("Failed to mount %q - %s:%s : %d", imageName, sumType, sum, res.StatusCode)
|
||||
}
|
||||
|
||||
func (r *Session) GetV2ImageBlob(imageName, sumType, sum string, blobWrtr io.Writer, token []string) error {
|
||||
vars := map[string]string{
|
||||
"imagename": imageName,
|
||||
"sumtype": sumType,
|
||||
"sum": sum,
|
||||
}
|
||||
|
||||
routeURL, err := getV2URL(r.indexEndpoint, "downloadBlob", vars)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
method := "GET"
|
||||
log.Debugf("[registry] Calling %q %s", method, routeURL.String())
|
||||
req, err := r.reqFactory.NewRequest(method, routeURL.String(), nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
setTokenAuth(req, token)
|
||||
res, _, err := r.doRequest(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer res.Body.Close()
|
||||
if res.StatusCode != 200 {
|
||||
if res.StatusCode == 401 {
|
||||
return errLoginRequired
|
||||
}
|
||||
return utils.NewHTTPRequestError(fmt.Sprintf("Server error: %d trying to pull %s blob", res.StatusCode, imageName), res)
|
||||
}
|
||||
|
||||
_, err = io.Copy(blobWrtr, res.Body)
|
||||
return err
|
||||
}
|
||||
|
||||
func (r *Session) GetV2ImageBlobReader(imageName, sumType, sum string, token []string) (io.ReadCloser, int64, error) {
|
||||
vars := map[string]string{
|
||||
"imagename": imageName,
|
||||
"sumtype": sumType,
|
||||
"sum": sum,
|
||||
}
|
||||
|
||||
routeURL, err := getV2URL(r.indexEndpoint, "downloadBlob", vars)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
method := "GET"
|
||||
log.Debugf("[registry] Calling %q %s", method, routeURL.String())
|
||||
req, err := r.reqFactory.NewRequest(method, routeURL.String(), nil)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
setTokenAuth(req, token)
|
||||
res, _, err := r.doRequest(req)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
if res.StatusCode != 200 {
|
||||
if res.StatusCode == 401 {
|
||||
return nil, 0, errLoginRequired
|
||||
}
|
||||
return nil, 0, utils.NewHTTPRequestError(fmt.Sprintf("Server error: %d trying to pull %s blob", res.StatusCode, imageName), res)
|
||||
}
|
||||
lenStr := res.Header.Get("Content-Length")
|
||||
l, err := strconv.ParseInt(lenStr, 10, 64)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
return res.Body, l, err
|
||||
}
|
||||
|
||||
// Push the image to the server for storage.
|
||||
// 'layer' is an uncompressed reader of the blob to be pushed.
|
||||
// The server will generate it's own checksum calculation.
|
||||
func (r *Session) PutV2ImageBlob(imageName, sumType string, blobRdr io.Reader, token []string) (serverChecksum string, err error) {
|
||||
vars := map[string]string{
|
||||
"imagename": imageName,
|
||||
"sumtype": sumType,
|
||||
}
|
||||
|
||||
routeURL, err := getV2URL(r.indexEndpoint, "uploadBlob", vars)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
method := "PUT"
|
||||
log.Debugf("[registry] Calling %q %s", method, routeURL.String())
|
||||
req, err := r.reqFactory.NewRequest(method, routeURL.String(), blobRdr)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
setTokenAuth(req, token)
|
||||
res, _, err := r.doRequest(req)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer res.Body.Close()
|
||||
if res.StatusCode != 201 {
|
||||
if res.StatusCode == 401 {
|
||||
return "", errLoginRequired
|
||||
}
|
||||
return "", utils.NewHTTPRequestError(fmt.Sprintf("Server error: %d trying to push %s blob", res.StatusCode, imageName), res)
|
||||
}
|
||||
|
||||
type sumReturn struct {
|
||||
Checksum string `json:"checksum"`
|
||||
}
|
||||
|
||||
decoder := json.NewDecoder(res.Body)
|
||||
var sumInfo sumReturn
|
||||
|
||||
err = decoder.Decode(&sumInfo)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("unable to decode PutV2ImageBlob JSON response: %s", err)
|
||||
}
|
||||
|
||||
// XXX this is a json struct from the registry, with its checksum
|
||||
return sumInfo.Checksum, nil
|
||||
}
|
||||
|
||||
// Finally Push the (signed) manifest of the blobs we've just pushed
|
||||
func (r *Session) PutV2ImageManifest(imageName, tagName string, manifestRdr io.Reader, token []string) error {
|
||||
vars := map[string]string{
|
||||
"imagename": imageName,
|
||||
"tagname": tagName,
|
||||
}
|
||||
|
||||
routeURL, err := getV2URL(r.indexEndpoint, "manifests", vars)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
method := "PUT"
|
||||
log.Debugf("[registry] Calling %q %s", method, routeURL.String())
|
||||
req, err := r.reqFactory.NewRequest(method, routeURL.String(), manifestRdr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
setTokenAuth(req, token)
|
||||
res, _, err := r.doRequest(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
res.Body.Close()
|
||||
if res.StatusCode != 201 {
|
||||
if res.StatusCode == 401 {
|
||||
return errLoginRequired
|
||||
}
|
||||
return utils.NewHTTPRequestError(fmt.Sprintf("Server error: %d trying to push %s:%s manifest", res.StatusCode, imageName, tagName), res)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Given a repository name, returns a json array of string tags
|
||||
func (r *Session) GetV2RemoteTags(imageName string, token []string) ([]string, error) {
|
||||
vars := map[string]string{
|
||||
"imagename": imageName,
|
||||
}
|
||||
|
||||
routeURL, err := getV2URL(r.indexEndpoint, "tags", vars)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
method := "GET"
|
||||
log.Debugf("[registry] Calling %q %s", method, routeURL.String())
|
||||
|
||||
req, err := r.reqFactory.NewRequest(method, routeURL.String(), nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
setTokenAuth(req, token)
|
||||
res, _, err := r.doRequest(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer res.Body.Close()
|
||||
if res.StatusCode != 200 {
|
||||
if res.StatusCode == 401 {
|
||||
return nil, errLoginRequired
|
||||
} else if res.StatusCode == 404 {
|
||||
return nil, ErrDoesNotExist
|
||||
}
|
||||
return nil, utils.NewHTTPRequestError(fmt.Sprintf("Server error: %d trying to fetch for %s", res.StatusCode, imageName), res)
|
||||
}
|
||||
|
||||
decoder := json.NewDecoder(res.Body)
|
||||
var tags []string
|
||||
err = decoder.Decode(&tags)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Error while decoding the http response: %s", err)
|
||||
}
|
||||
return tags, nil
|
||||
}
|
|
@ -31,3 +31,29 @@ type RegistryInfo struct {
|
|||
Version string `json:"version"`
|
||||
Standalone bool `json:"standalone"`
|
||||
}
|
||||
|
||||
type ManifestData struct {
|
||||
Name string `json:"name"`
|
||||
Tag string `json:"tag"`
|
||||
Architecture string `json:"architecture"`
|
||||
BlobSums []string `json:"blobSums"`
|
||||
History []string `json:"history"`
|
||||
SchemaVersion int `json:"schemaVersion"`
|
||||
}
|
||||
|
||||
type APIVersion int
|
||||
|
||||
func (av APIVersion) String() string {
|
||||
return apiVersions[av]
|
||||
}
|
||||
|
||||
var DefaultAPIVersion APIVersion = APIVersion1
|
||||
var apiVersions = map[APIVersion]string{
|
||||
1: "v1",
|
||||
2: "v2",
|
||||
}
|
||||
|
||||
const (
|
||||
APIVersion1 = iota + 1
|
||||
APIVersion2
|
||||
)
|
||||
|
|
74
trust/service.go
Normal file
74
trust/service.go
Normal file
|
@ -0,0 +1,74 @@
|
|||
package trust
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/docker/docker/engine"
|
||||
"github.com/docker/docker/pkg/log"
|
||||
"github.com/docker/libtrust"
|
||||
)
|
||||
|
||||
func (t *TrustStore) Install(eng *engine.Engine) error {
|
||||
for name, handler := range map[string]engine.Handler{
|
||||
"trust_key_check": t.CmdCheckKey,
|
||||
"trust_update_base": t.CmdUpdateBase,
|
||||
} {
|
||||
if err := eng.Register(name, handler); err != nil {
|
||||
return fmt.Errorf("Could not register %q: %v", name, err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *TrustStore) CmdCheckKey(job *engine.Job) engine.Status {
|
||||
if n := len(job.Args); n != 1 {
|
||||
return job.Errorf("Usage: %s NAMESPACE", job.Name)
|
||||
}
|
||||
var (
|
||||
namespace = job.Args[0]
|
||||
keyBytes = job.Getenv("PublicKey")
|
||||
)
|
||||
|
||||
if keyBytes == "" {
|
||||
return job.Errorf("Missing PublicKey")
|
||||
}
|
||||
pk, err := libtrust.UnmarshalPublicKeyJWK([]byte(keyBytes))
|
||||
if err != nil {
|
||||
return job.Errorf("Error unmarshalling public key: %s", err)
|
||||
}
|
||||
|
||||
permission := uint16(job.GetenvInt("Permission"))
|
||||
if permission == 0 {
|
||||
permission = 0x03
|
||||
}
|
||||
|
||||
t.RLock()
|
||||
defer t.RUnlock()
|
||||
if t.graph == nil {
|
||||
job.Stdout.Write([]byte("no graph"))
|
||||
return engine.StatusOK
|
||||
}
|
||||
|
||||
// Check if any expired grants
|
||||
verified, err := t.graph.Verify(pk, namespace, permission)
|
||||
if err != nil {
|
||||
return job.Errorf("Error verifying key to namespace: %s", namespace)
|
||||
}
|
||||
if !verified {
|
||||
log.Debugf("Verification failed for %s using key %s", namespace, pk.KeyID())
|
||||
job.Stdout.Write([]byte("not verified"))
|
||||
} else if t.expiration.Before(time.Now()) {
|
||||
job.Stdout.Write([]byte("expired"))
|
||||
} else {
|
||||
job.Stdout.Write([]byte("verified"))
|
||||
}
|
||||
|
||||
return engine.StatusOK
|
||||
}
|
||||
|
||||
func (t *TrustStore) CmdUpdateBase(job *engine.Job) engine.Status {
|
||||
t.fetch()
|
||||
|
||||
return engine.StatusOK
|
||||
}
|
199
trust/trusts.go
Normal file
199
trust/trusts.go
Normal file
|
@ -0,0 +1,199 @@
|
|||
package trust
|
||||
|
||||
import (
|
||||
"crypto/x509"
|
||||
"errors"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/docker/docker/pkg/log"
|
||||
"github.com/docker/libtrust/trustgraph"
|
||||
)
|
||||
|
||||
type TrustStore struct {
|
||||
path string
|
||||
caPool *x509.CertPool
|
||||
graph trustgraph.TrustGraph
|
||||
expiration time.Time
|
||||
fetcher *time.Timer
|
||||
fetchTime time.Duration
|
||||
autofetch bool
|
||||
httpClient *http.Client
|
||||
baseEndpoints map[string]*url.URL
|
||||
|
||||
sync.RWMutex
|
||||
}
|
||||
|
||||
// defaultFetchtime represents the starting duration to wait between
|
||||
// fetching sections of the graph. Unsuccessful fetches should
|
||||
// increase time between fetching.
|
||||
const defaultFetchtime = 45 * time.Second
|
||||
|
||||
var baseEndpoints = map[string]string{"official": "https://dvjy3tqbc323p.cloudfront.net/trust/official.json"}
|
||||
|
||||
func NewTrustStore(path string) (*TrustStore, error) {
|
||||
abspath, err := filepath.Abs(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Create base graph url map
|
||||
endpoints := map[string]*url.URL{}
|
||||
for name, endpoint := range baseEndpoints {
|
||||
u, err := url.Parse(endpoint)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
endpoints[name] = u
|
||||
}
|
||||
|
||||
// Load grant files
|
||||
t := &TrustStore{
|
||||
path: abspath,
|
||||
caPool: nil,
|
||||
httpClient: &http.Client{},
|
||||
fetchTime: time.Millisecond,
|
||||
baseEndpoints: endpoints,
|
||||
}
|
||||
|
||||
err = t.reload()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return t, nil
|
||||
}
|
||||
|
||||
func (t *TrustStore) reload() error {
|
||||
t.Lock()
|
||||
defer t.Unlock()
|
||||
|
||||
matches, err := filepath.Glob(filepath.Join(t.path, "*.json"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
statements := make([]*trustgraph.Statement, len(matches))
|
||||
for i, match := range matches {
|
||||
f, err := os.Open(match)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
statements[i], err = trustgraph.LoadStatement(f, nil)
|
||||
if err != nil {
|
||||
f.Close()
|
||||
return err
|
||||
}
|
||||
f.Close()
|
||||
}
|
||||
if len(statements) == 0 {
|
||||
if t.autofetch {
|
||||
log.Debugf("No grants, fetching")
|
||||
t.fetcher = time.AfterFunc(t.fetchTime, t.fetch)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
grants, expiration, err := trustgraph.CollapseStatements(statements, true)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
t.expiration = expiration
|
||||
t.graph = trustgraph.NewMemoryGraph(grants)
|
||||
log.Debugf("Reloaded graph with %d grants expiring at %s", len(grants), expiration)
|
||||
|
||||
if t.autofetch {
|
||||
nextFetch := expiration.Sub(time.Now())
|
||||
if nextFetch < 0 {
|
||||
nextFetch = defaultFetchtime
|
||||
} else {
|
||||
nextFetch = time.Duration(0.8 * (float64)(nextFetch))
|
||||
}
|
||||
t.fetcher = time.AfterFunc(nextFetch, t.fetch)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *TrustStore) fetchBaseGraph(u *url.URL) (*trustgraph.Statement, error) {
|
||||
req := &http.Request{
|
||||
Method: "GET",
|
||||
URL: u,
|
||||
Proto: "HTTP/1.1",
|
||||
ProtoMajor: 1,
|
||||
ProtoMinor: 1,
|
||||
Header: make(http.Header),
|
||||
Body: nil,
|
||||
Host: u.Host,
|
||||
}
|
||||
|
||||
resp, err := t.httpClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if resp.StatusCode == 404 {
|
||||
return nil, errors.New("base graph does not exist")
|
||||
}
|
||||
|
||||
defer resp.Body.Close()
|
||||
|
||||
return trustgraph.LoadStatement(resp.Body, t.caPool)
|
||||
}
|
||||
|
||||
// fetch retrieves updated base graphs. This function cannot error, it
|
||||
// should only log errors
|
||||
func (t *TrustStore) fetch() {
|
||||
t.Lock()
|
||||
defer t.Unlock()
|
||||
|
||||
if t.autofetch && t.fetcher == nil {
|
||||
// Do nothing ??
|
||||
return
|
||||
}
|
||||
|
||||
fetchCount := 0
|
||||
for bg, ep := range t.baseEndpoints {
|
||||
statement, err := t.fetchBaseGraph(ep)
|
||||
if err != nil {
|
||||
log.Infof("Trust graph fetch failed: %s", err)
|
||||
continue
|
||||
}
|
||||
b, err := statement.Bytes()
|
||||
if err != nil {
|
||||
log.Infof("Bad trust graph statement: %s", err)
|
||||
continue
|
||||
}
|
||||
// TODO check if value differs
|
||||
err = ioutil.WriteFile(path.Join(t.path, bg+".json"), b, 0600)
|
||||
if err != nil {
|
||||
log.Infof("Error writing trust graph statement: %s", err)
|
||||
}
|
||||
fetchCount++
|
||||
}
|
||||
log.Debugf("Fetched %d base graphs at %s", fetchCount, time.Now())
|
||||
|
||||
if fetchCount > 0 {
|
||||
go func() {
|
||||
err := t.reload()
|
||||
if err != nil {
|
||||
// TODO log
|
||||
log.Infof("Reload of trust graph failed: %s", err)
|
||||
}
|
||||
}()
|
||||
t.fetchTime = defaultFetchtime
|
||||
t.fetcher = nil
|
||||
} else if t.autofetch {
|
||||
maxTime := 10 * defaultFetchtime
|
||||
t.fetchTime = time.Duration(1.5 * (float64)(t.fetchTime+time.Second))
|
||||
if t.fetchTime > maxTime {
|
||||
t.fetchTime = maxTime
|
||||
}
|
||||
t.fetcher = time.AfterFunc(t.fetchTime, t.fetch)
|
||||
}
|
||||
}
|
50
vendor/src/github.com/docker/libtrust/trustgraph/graph.go
vendored
Normal file
50
vendor/src/github.com/docker/libtrust/trustgraph/graph.go
vendored
Normal file
|
@ -0,0 +1,50 @@
|
|||
package trustgraph
|
||||
|
||||
import "github.com/docker/libtrust"
|
||||
|
||||
// TrustGraph represents a graph of authorization mapping
|
||||
// public keys to nodes and grants between nodes.
|
||||
type TrustGraph interface {
|
||||
// Verifies that the given public key is allowed to perform
|
||||
// the given action on the given node according to the trust
|
||||
// graph.
|
||||
Verify(libtrust.PublicKey, string, uint16) (bool, error)
|
||||
|
||||
// GetGrants returns an array of all grant chains which are used to
|
||||
// allow the requested permission.
|
||||
GetGrants(libtrust.PublicKey, string, uint16) ([][]*Grant, error)
|
||||
}
|
||||
|
||||
// Grant represents a transfer of permission from one part of the
|
||||
// trust graph to another. This is the only way to delegate
|
||||
// permission between two different sub trees in the graph.
|
||||
type Grant struct {
|
||||
// Subject is the namespace being granted
|
||||
Subject string
|
||||
|
||||
// Permissions is a bit map of permissions
|
||||
Permission uint16
|
||||
|
||||
// Grantee represents the node being granted
|
||||
// a permission scope. The grantee can be
|
||||
// either a namespace item or a key id where namespace
|
||||
// items will always start with a '/'.
|
||||
Grantee string
|
||||
|
||||
// statement represents the statement used to create
|
||||
// this object.
|
||||
statement *Statement
|
||||
}
|
||||
|
||||
// Permissions
|
||||
// Read node 0x01 (can read node, no sub nodes)
|
||||
// Write node 0x02 (can write to node object, cannot create subnodes)
|
||||
// Read subtree 0x04 (delegates read to each sub node)
|
||||
// Write subtree 0x08 (delegates write to each sub node, included create on the subject)
|
||||
//
|
||||
// Permission shortcuts
|
||||
// ReadItem = 0x01
|
||||
// WriteItem = 0x03
|
||||
// ReadAccess = 0x07
|
||||
// WriteAccess = 0x0F
|
||||
// Delegate = 0x0F
|
133
vendor/src/github.com/docker/libtrust/trustgraph/memory_graph.go
vendored
Normal file
133
vendor/src/github.com/docker/libtrust/trustgraph/memory_graph.go
vendored
Normal file
|
@ -0,0 +1,133 @@
|
|||
package trustgraph
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/docker/libtrust"
|
||||
)
|
||||
|
||||
type grantNode struct {
|
||||
grants []*Grant
|
||||
children map[string]*grantNode
|
||||
}
|
||||
|
||||
type memoryGraph struct {
|
||||
roots map[string]*grantNode
|
||||
}
|
||||
|
||||
func newGrantNode() *grantNode {
|
||||
return &grantNode{
|
||||
grants: []*Grant{},
|
||||
children: map[string]*grantNode{},
|
||||
}
|
||||
}
|
||||
|
||||
// NewMemoryGraph returns a new in memory trust graph created from
|
||||
// a static list of grants. This graph is immutable after creation
|
||||
// and any alterations should create a new instance.
|
||||
func NewMemoryGraph(grants []*Grant) TrustGraph {
|
||||
roots := map[string]*grantNode{}
|
||||
for _, grant := range grants {
|
||||
parts := strings.Split(grant.Grantee, "/")
|
||||
nodes := roots
|
||||
var node *grantNode
|
||||
var nodeOk bool
|
||||
for _, part := range parts {
|
||||
node, nodeOk = nodes[part]
|
||||
if !nodeOk {
|
||||
node = newGrantNode()
|
||||
nodes[part] = node
|
||||
}
|
||||
if part != "" {
|
||||
node.grants = append(node.grants, grant)
|
||||
}
|
||||
nodes = node.children
|
||||
}
|
||||
}
|
||||
return &memoryGraph{roots}
|
||||
}
|
||||
|
||||
func (g *memoryGraph) getGrants(name string) []*Grant {
|
||||
nameParts := strings.Split(name, "/")
|
||||
nodes := g.roots
|
||||
var node *grantNode
|
||||
var nodeOk bool
|
||||
for _, part := range nameParts {
|
||||
node, nodeOk = nodes[part]
|
||||
if !nodeOk {
|
||||
return nil
|
||||
}
|
||||
nodes = node.children
|
||||
}
|
||||
return node.grants
|
||||
}
|
||||
|
||||
func isSubName(name, sub string) bool {
|
||||
if strings.HasPrefix(name, sub) {
|
||||
if len(name) == len(sub) || name[len(sub)] == '/' {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
type walkFunc func(*Grant, []*Grant) bool
|
||||
|
||||
func foundWalkFunc(*Grant, []*Grant) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (g *memoryGraph) walkGrants(start, target string, permission uint16, f walkFunc, chain []*Grant, visited map[*Grant]bool, collect bool) bool {
|
||||
if visited == nil {
|
||||
visited = map[*Grant]bool{}
|
||||
}
|
||||
grants := g.getGrants(start)
|
||||
subGrants := make([]*Grant, 0, len(grants))
|
||||
for _, grant := range grants {
|
||||
if visited[grant] {
|
||||
continue
|
||||
}
|
||||
visited[grant] = true
|
||||
if grant.Permission&permission == permission {
|
||||
if isSubName(target, grant.Subject) {
|
||||
if f(grant, chain) {
|
||||
return true
|
||||
}
|
||||
} else {
|
||||
subGrants = append(subGrants, grant)
|
||||
}
|
||||
}
|
||||
}
|
||||
for _, grant := range subGrants {
|
||||
var chainCopy []*Grant
|
||||
if collect {
|
||||
chainCopy = make([]*Grant, len(chain)+1)
|
||||
copy(chainCopy, chain)
|
||||
chainCopy[len(chainCopy)-1] = grant
|
||||
} else {
|
||||
chainCopy = nil
|
||||
}
|
||||
|
||||
if g.walkGrants(grant.Subject, target, permission, f, chainCopy, visited, collect) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (g *memoryGraph) Verify(key libtrust.PublicKey, node string, permission uint16) (bool, error) {
|
||||
return g.walkGrants(key.KeyID(), node, permission, foundWalkFunc, nil, nil, false), nil
|
||||
}
|
||||
|
||||
func (g *memoryGraph) GetGrants(key libtrust.PublicKey, node string, permission uint16) ([][]*Grant, error) {
|
||||
grants := [][]*Grant{}
|
||||
collect := func(grant *Grant, chain []*Grant) bool {
|
||||
grantChain := make([]*Grant, len(chain)+1)
|
||||
copy(grantChain, chain)
|
||||
grantChain[len(grantChain)-1] = grant
|
||||
grants = append(grants, grantChain)
|
||||
return false
|
||||
}
|
||||
g.walkGrants(key.KeyID(), node, permission, collect, nil, nil, true)
|
||||
return grants, nil
|
||||
}
|
174
vendor/src/github.com/docker/libtrust/trustgraph/memory_graph_test.go
vendored
Normal file
174
vendor/src/github.com/docker/libtrust/trustgraph/memory_graph_test.go
vendored
Normal file
|
@ -0,0 +1,174 @@
|
|||
package trustgraph
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/docker/libtrust"
|
||||
)
|
||||
|
||||
func createTestKeysAndGrants(count int) ([]*Grant, []libtrust.PrivateKey) {
|
||||
grants := make([]*Grant, count)
|
||||
keys := make([]libtrust.PrivateKey, count)
|
||||
for i := 0; i < count; i++ {
|
||||
pk, err := libtrust.GenerateECP256PrivateKey()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
grant := &Grant{
|
||||
Subject: fmt.Sprintf("/user-%d", i+1),
|
||||
Permission: 0x0f,
|
||||
Grantee: pk.KeyID(),
|
||||
}
|
||||
keys[i] = pk
|
||||
grants[i] = grant
|
||||
}
|
||||
return grants, keys
|
||||
}
|
||||
|
||||
func testVerified(t *testing.T, g TrustGraph, k libtrust.PublicKey, keyName, target string, permission uint16) {
|
||||
if ok, err := g.Verify(k, target, permission); err != nil {
|
||||
t.Fatalf("Unexpected error during verification: %s", err)
|
||||
} else if !ok {
|
||||
t.Errorf("key failed verification\n\tKey: %s(%s)\n\tNamespace: %s", keyName, k.KeyID(), target)
|
||||
}
|
||||
}
|
||||
|
||||
func testNotVerified(t *testing.T, g TrustGraph, k libtrust.PublicKey, keyName, target string, permission uint16) {
|
||||
if ok, err := g.Verify(k, target, permission); err != nil {
|
||||
t.Fatalf("Unexpected error during verification: %s", err)
|
||||
} else if ok {
|
||||
t.Errorf("key should have failed verification\n\tKey: %s(%s)\n\tNamespace: %s", keyName, k.KeyID(), target)
|
||||
}
|
||||
}
|
||||
|
||||
func TestVerify(t *testing.T) {
|
||||
grants, keys := createTestKeysAndGrants(4)
|
||||
extraGrants := make([]*Grant, 3)
|
||||
extraGrants[0] = &Grant{
|
||||
Subject: "/user-3",
|
||||
Permission: 0x0f,
|
||||
Grantee: "/user-2",
|
||||
}
|
||||
extraGrants[1] = &Grant{
|
||||
Subject: "/user-3/sub-project",
|
||||
Permission: 0x0f,
|
||||
Grantee: "/user-4",
|
||||
}
|
||||
extraGrants[2] = &Grant{
|
||||
Subject: "/user-4",
|
||||
Permission: 0x07,
|
||||
Grantee: "/user-1",
|
||||
}
|
||||
grants = append(grants, extraGrants...)
|
||||
|
||||
g := NewMemoryGraph(grants)
|
||||
|
||||
testVerified(t, g, keys[0].PublicKey(), "user-key-1", "/user-1", 0x0f)
|
||||
testVerified(t, g, keys[0].PublicKey(), "user-key-1", "/user-1/some-project/sub-value", 0x0f)
|
||||
testVerified(t, g, keys[0].PublicKey(), "user-key-1", "/user-4", 0x07)
|
||||
testVerified(t, g, keys[1].PublicKey(), "user-key-2", "/user-2/", 0x0f)
|
||||
testVerified(t, g, keys[2].PublicKey(), "user-key-3", "/user-3/sub-value", 0x0f)
|
||||
testVerified(t, g, keys[1].PublicKey(), "user-key-2", "/user-3/sub-value", 0x0f)
|
||||
testVerified(t, g, keys[1].PublicKey(), "user-key-2", "/user-3", 0x0f)
|
||||
testVerified(t, g, keys[1].PublicKey(), "user-key-2", "/user-3/", 0x0f)
|
||||
testVerified(t, g, keys[3].PublicKey(), "user-key-4", "/user-3/sub-project", 0x0f)
|
||||
testVerified(t, g, keys[3].PublicKey(), "user-key-4", "/user-3/sub-project/app", 0x0f)
|
||||
testVerified(t, g, keys[3].PublicKey(), "user-key-4", "/user-4", 0x0f)
|
||||
|
||||
testNotVerified(t, g, keys[0].PublicKey(), "user-key-1", "/user-2", 0x0f)
|
||||
testNotVerified(t, g, keys[0].PublicKey(), "user-key-1", "/user-3/sub-value", 0x0f)
|
||||
testNotVerified(t, g, keys[0].PublicKey(), "user-key-1", "/user-4", 0x0f)
|
||||
testNotVerified(t, g, keys[1].PublicKey(), "user-key-2", "/user-1/", 0x0f)
|
||||
testNotVerified(t, g, keys[2].PublicKey(), "user-key-3", "/user-2", 0x0f)
|
||||
testNotVerified(t, g, keys[1].PublicKey(), "user-key-2", "/user-4", 0x0f)
|
||||
testNotVerified(t, g, keys[3].PublicKey(), "user-key-4", "/user-3", 0x0f)
|
||||
}
|
||||
|
||||
func TestCircularWalk(t *testing.T) {
|
||||
grants, keys := createTestKeysAndGrants(3)
|
||||
user1Grant := &Grant{
|
||||
Subject: "/user-2",
|
||||
Permission: 0x0f,
|
||||
Grantee: "/user-1",
|
||||
}
|
||||
user2Grant := &Grant{
|
||||
Subject: "/user-1",
|
||||
Permission: 0x0f,
|
||||
Grantee: "/user-2",
|
||||
}
|
||||
grants = append(grants, user1Grant, user2Grant)
|
||||
|
||||
g := NewMemoryGraph(grants)
|
||||
|
||||
testVerified(t, g, keys[0].PublicKey(), "user-key-1", "/user-1", 0x0f)
|
||||
testVerified(t, g, keys[0].PublicKey(), "user-key-1", "/user-2", 0x0f)
|
||||
testVerified(t, g, keys[1].PublicKey(), "user-key-2", "/user-2", 0x0f)
|
||||
testVerified(t, g, keys[1].PublicKey(), "user-key-2", "/user-1", 0x0f)
|
||||
testVerified(t, g, keys[2].PublicKey(), "user-key-3", "/user-3", 0x0f)
|
||||
|
||||
testNotVerified(t, g, keys[0].PublicKey(), "user-key-1", "/user-3", 0x0f)
|
||||
testNotVerified(t, g, keys[1].PublicKey(), "user-key-2", "/user-3", 0x0f)
|
||||
}
|
||||
|
||||
func assertGrantSame(t *testing.T, actual, expected *Grant) {
|
||||
if actual != expected {
|
||||
t.Fatalf("Unexpected grant retrieved\n\tExpected: %v\n\tActual: %v", expected, actual)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetGrants(t *testing.T) {
|
||||
grants, keys := createTestKeysAndGrants(5)
|
||||
extraGrants := make([]*Grant, 4)
|
||||
extraGrants[0] = &Grant{
|
||||
Subject: "/user-3/friend-project",
|
||||
Permission: 0x0f,
|
||||
Grantee: "/user-2/friends",
|
||||
}
|
||||
extraGrants[1] = &Grant{
|
||||
Subject: "/user-3/sub-project",
|
||||
Permission: 0x0f,
|
||||
Grantee: "/user-4",
|
||||
}
|
||||
extraGrants[2] = &Grant{
|
||||
Subject: "/user-2/friends",
|
||||
Permission: 0x0f,
|
||||
Grantee: "/user-5/fun-project",
|
||||
}
|
||||
extraGrants[3] = &Grant{
|
||||
Subject: "/user-5/fun-project",
|
||||
Permission: 0x0f,
|
||||
Grantee: "/user-1",
|
||||
}
|
||||
grants = append(grants, extraGrants...)
|
||||
|
||||
g := NewMemoryGraph(grants)
|
||||
|
||||
grantChains, err := g.GetGrants(keys[3], "/user-3/sub-project/specific-app", 0x0f)
|
||||
if err != nil {
|
||||
t.Fatalf("Error getting grants: %s", err)
|
||||
}
|
||||
if len(grantChains) != 1 {
|
||||
t.Fatalf("Expected number of grant chains returned, expected %d, received %d", 1, len(grantChains))
|
||||
}
|
||||
if len(grantChains[0]) != 2 {
|
||||
t.Fatalf("Unexpected number of grants retrieved\n\tExpected: %d\n\tActual: %d", 2, len(grantChains[0]))
|
||||
}
|
||||
assertGrantSame(t, grantChains[0][0], grants[3])
|
||||
assertGrantSame(t, grantChains[0][1], extraGrants[1])
|
||||
|
||||
grantChains, err = g.GetGrants(keys[0], "/user-3/friend-project/fun-app", 0x0f)
|
||||
if err != nil {
|
||||
t.Fatalf("Error getting grants: %s", err)
|
||||
}
|
||||
if len(grantChains) != 1 {
|
||||
t.Fatalf("Expected number of grant chains returned, expected %d, received %d", 1, len(grantChains))
|
||||
}
|
||||
if len(grantChains[0]) != 4 {
|
||||
t.Fatalf("Unexpected number of grants retrieved\n\tExpected: %d\n\tActual: %d", 2, len(grantChains[0]))
|
||||
}
|
||||
assertGrantSame(t, grantChains[0][0], grants[0])
|
||||
assertGrantSame(t, grantChains[0][1], extraGrants[3])
|
||||
assertGrantSame(t, grantChains[0][2], extraGrants[2])
|
||||
assertGrantSame(t, grantChains[0][3], extraGrants[0])
|
||||
}
|
227
vendor/src/github.com/docker/libtrust/trustgraph/statement.go
vendored
Normal file
227
vendor/src/github.com/docker/libtrust/trustgraph/statement.go
vendored
Normal file
|
@ -0,0 +1,227 @@
|
|||
package trustgraph
|
||||
|
||||
import (
|
||||
"crypto/x509"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/docker/libtrust"
|
||||
)
|
||||
|
||||
type jsonGrant struct {
|
||||
Subject string `json:"subject"`
|
||||
Permission uint16 `json:"permission"`
|
||||
Grantee string `json:"grantee"`
|
||||
}
|
||||
|
||||
type jsonRevocation struct {
|
||||
Subject string `json:"subject"`
|
||||
Revocation uint16 `json:"revocation"`
|
||||
Grantee string `json:"grantee"`
|
||||
}
|
||||
|
||||
type jsonStatement struct {
|
||||
Revocations []*jsonRevocation `json:"revocations"`
|
||||
Grants []*jsonGrant `json:"grants"`
|
||||
Expiration time.Time `json:"expiration"`
|
||||
IssuedAt time.Time `json:"issuedAt"`
|
||||
}
|
||||
|
||||
func (g *jsonGrant) Grant(statement *Statement) *Grant {
|
||||
return &Grant{
|
||||
Subject: g.Subject,
|
||||
Permission: g.Permission,
|
||||
Grantee: g.Grantee,
|
||||
statement: statement,
|
||||
}
|
||||
}
|
||||
|
||||
// Statement represents a set of grants made from a verifiable
|
||||
// authority. A statement has an expiration associated with it
|
||||
// set by the authority.
|
||||
type Statement struct {
|
||||
jsonStatement
|
||||
|
||||
signature *libtrust.JSONSignature
|
||||
}
|
||||
|
||||
// IsExpired returns whether the statement has expired
|
||||
func (s *Statement) IsExpired() bool {
|
||||
return s.Expiration.Before(time.Now().Add(-10 * time.Second))
|
||||
}
|
||||
|
||||
// Bytes returns an indented json representation of the statement
|
||||
// in a byte array. This value can be written to a file or stream
|
||||
// without alteration.
|
||||
func (s *Statement) Bytes() ([]byte, error) {
|
||||
return s.signature.PrettySignature("signatures")
|
||||
}
|
||||
|
||||
// LoadStatement loads and verifies a statement from an input stream.
|
||||
func LoadStatement(r io.Reader, authority *x509.CertPool) (*Statement, error) {
|
||||
b, err := ioutil.ReadAll(r)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
js, err := libtrust.ParsePrettySignature(b, "signatures")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
payload, err := js.Payload()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var statement Statement
|
||||
err = json.Unmarshal(payload, &statement.jsonStatement)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if authority == nil {
|
||||
_, err = js.Verify()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else {
|
||||
_, err = js.VerifyChains(authority)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
statement.signature = js
|
||||
|
||||
return &statement, nil
|
||||
}
|
||||
|
||||
// CreateStatements creates and signs a statement from a stream of grants
|
||||
// and revocations in a JSON array.
|
||||
func CreateStatement(grants, revocations io.Reader, expiration time.Duration, key libtrust.PrivateKey, chain []*x509.Certificate) (*Statement, error) {
|
||||
var statement Statement
|
||||
err := json.NewDecoder(grants).Decode(&statement.jsonStatement.Grants)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = json.NewDecoder(revocations).Decode(&statement.jsonStatement.Revocations)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
statement.jsonStatement.Expiration = time.Now().UTC().Add(expiration)
|
||||
statement.jsonStatement.IssuedAt = time.Now().UTC()
|
||||
|
||||
b, err := json.MarshalIndent(&statement.jsonStatement, "", " ")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
statement.signature, err = libtrust.NewJSONSignature(b)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = statement.signature.SignWithChain(key, chain)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &statement, nil
|
||||
}
|
||||
|
||||
type statementList []*Statement
|
||||
|
||||
func (s statementList) Len() int {
|
||||
return len(s)
|
||||
}
|
||||
|
||||
func (s statementList) Less(i, j int) bool {
|
||||
return s[i].IssuedAt.Before(s[j].IssuedAt)
|
||||
}
|
||||
|
||||
func (s statementList) Swap(i, j int) {
|
||||
s[i], s[j] = s[j], s[i]
|
||||
}
|
||||
|
||||
// CollapseStatements returns a single list of the valid statements as well as the
|
||||
// time when the next grant will expire.
|
||||
func CollapseStatements(statements []*Statement, useExpired bool) ([]*Grant, time.Time, error) {
|
||||
sorted := make(statementList, 0, len(statements))
|
||||
for _, statement := range statements {
|
||||
if useExpired || !statement.IsExpired() {
|
||||
sorted = append(sorted, statement)
|
||||
}
|
||||
}
|
||||
sort.Sort(sorted)
|
||||
|
||||
var minExpired time.Time
|
||||
var grantCount int
|
||||
roots := map[string]*grantNode{}
|
||||
for i, statement := range sorted {
|
||||
if statement.Expiration.Before(minExpired) || i == 0 {
|
||||
minExpired = statement.Expiration
|
||||
}
|
||||
for _, grant := range statement.Grants {
|
||||
parts := strings.Split(grant.Grantee, "/")
|
||||
nodes := roots
|
||||
g := grant.Grant(statement)
|
||||
grantCount = grantCount + 1
|
||||
|
||||
for _, part := range parts {
|
||||
node, nodeOk := nodes[part]
|
||||
if !nodeOk {
|
||||
node = newGrantNode()
|
||||
nodes[part] = node
|
||||
}
|
||||
node.grants = append(node.grants, g)
|
||||
nodes = node.children
|
||||
}
|
||||
}
|
||||
|
||||
for _, revocation := range statement.Revocations {
|
||||
parts := strings.Split(revocation.Grantee, "/")
|
||||
nodes := roots
|
||||
|
||||
var node *grantNode
|
||||
var nodeOk bool
|
||||
for _, part := range parts {
|
||||
node, nodeOk = nodes[part]
|
||||
if !nodeOk {
|
||||
break
|
||||
}
|
||||
nodes = node.children
|
||||
}
|
||||
if node != nil {
|
||||
for _, grant := range node.grants {
|
||||
if isSubName(grant.Subject, revocation.Subject) {
|
||||
grant.Permission = grant.Permission &^ revocation.Revocation
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
retGrants := make([]*Grant, 0, grantCount)
|
||||
for _, rootNodes := range roots {
|
||||
retGrants = append(retGrants, rootNodes.grants...)
|
||||
}
|
||||
|
||||
return retGrants, minExpired, nil
|
||||
}
|
||||
|
||||
// FilterStatements filters the statements to statements including the given grants.
|
||||
func FilterStatements(grants []*Grant) ([]*Statement, error) {
|
||||
statements := map[*Statement]bool{}
|
||||
for _, grant := range grants {
|
||||
if grant.statement != nil {
|
||||
statements[grant.statement] = true
|
||||
}
|
||||
}
|
||||
retStatements := make([]*Statement, len(statements))
|
||||
var i int
|
||||
for statement := range statements {
|
||||
retStatements[i] = statement
|
||||
i++
|
||||
}
|
||||
return retStatements, nil
|
||||
}
|
417
vendor/src/github.com/docker/libtrust/trustgraph/statement_test.go
vendored
Normal file
417
vendor/src/github.com/docker/libtrust/trustgraph/statement_test.go
vendored
Normal file
|
@ -0,0 +1,417 @@
|
|||
package trustgraph
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/x509"
|
||||
"encoding/json"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/docker/libtrust"
|
||||
"github.com/docker/libtrust/testutil"
|
||||
)
|
||||
|
||||
const testStatementExpiration = time.Hour * 5
|
||||
|
||||
func generateStatement(grants []*Grant, key libtrust.PrivateKey, chain []*x509.Certificate) (*Statement, error) {
|
||||
var statement Statement
|
||||
|
||||
statement.Grants = make([]*jsonGrant, len(grants))
|
||||
for i, grant := range grants {
|
||||
statement.Grants[i] = &jsonGrant{
|
||||
Subject: grant.Subject,
|
||||
Permission: grant.Permission,
|
||||
Grantee: grant.Grantee,
|
||||
}
|
||||
}
|
||||
statement.IssuedAt = time.Now()
|
||||
statement.Expiration = time.Now().Add(testStatementExpiration)
|
||||
statement.Revocations = make([]*jsonRevocation, 0)
|
||||
|
||||
marshalled, err := json.MarshalIndent(statement.jsonStatement, "", " ")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
sig, err := libtrust.NewJSONSignature(marshalled)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = sig.SignWithChain(key, chain)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
statement.signature = sig
|
||||
|
||||
return &statement, nil
|
||||
}
|
||||
|
||||
func generateTrustChain(t *testing.T, chainLen int) (libtrust.PrivateKey, *x509.CertPool, []*x509.Certificate) {
|
||||
caKey, err := libtrust.GenerateECP256PrivateKey()
|
||||
if err != nil {
|
||||
t.Fatalf("Error generating key: %s", err)
|
||||
}
|
||||
ca, err := testutil.GenerateTrustCA(caKey.CryptoPublicKey(), caKey.CryptoPrivateKey())
|
||||
if err != nil {
|
||||
t.Fatalf("Error generating ca: %s", err)
|
||||
}
|
||||
|
||||
parent := ca
|
||||
parentKey := caKey
|
||||
chain := make([]*x509.Certificate, chainLen)
|
||||
for i := chainLen - 1; i > 0; i-- {
|
||||
intermediatekey, err := libtrust.GenerateECP256PrivateKey()
|
||||
if err != nil {
|
||||
t.Fatalf("Error generate key: %s", err)
|
||||
}
|
||||
chain[i], err = testutil.GenerateIntermediate(intermediatekey.CryptoPublicKey(), parentKey.CryptoPrivateKey(), parent)
|
||||
if err != nil {
|
||||
t.Fatalf("Error generating intermdiate certificate: %s", err)
|
||||
}
|
||||
parent = chain[i]
|
||||
parentKey = intermediatekey
|
||||
}
|
||||
trustKey, err := libtrust.GenerateECP256PrivateKey()
|
||||
if err != nil {
|
||||
t.Fatalf("Error generate key: %s", err)
|
||||
}
|
||||
chain[0], err = testutil.GenerateTrustCert(trustKey.CryptoPublicKey(), parentKey.CryptoPrivateKey(), parent)
|
||||
if err != nil {
|
||||
t.Fatalf("Error generate trust cert: %s", err)
|
||||
}
|
||||
|
||||
caPool := x509.NewCertPool()
|
||||
caPool.AddCert(ca)
|
||||
|
||||
return trustKey, caPool, chain
|
||||
}
|
||||
|
||||
func TestLoadStatement(t *testing.T) {
|
||||
grantCount := 4
|
||||
grants, _ := createTestKeysAndGrants(grantCount)
|
||||
|
||||
trustKey, caPool, chain := generateTrustChain(t, 6)
|
||||
|
||||
statement, err := generateStatement(grants, trustKey, chain)
|
||||
if err != nil {
|
||||
t.Fatalf("Error generating statement: %s", err)
|
||||
}
|
||||
|
||||
statementBytes, err := statement.Bytes()
|
||||
if err != nil {
|
||||
t.Fatalf("Error getting statement bytes: %s", err)
|
||||
}
|
||||
|
||||
s2, err := LoadStatement(bytes.NewReader(statementBytes), caPool)
|
||||
if err != nil {
|
||||
t.Fatalf("Error loading statement: %s", err)
|
||||
}
|
||||
if len(s2.Grants) != grantCount {
|
||||
t.Fatalf("Unexpected grant length\n\tExpected: %d\n\tActual: %d", grantCount, len(s2.Grants))
|
||||
}
|
||||
|
||||
pool := x509.NewCertPool()
|
||||
_, err = LoadStatement(bytes.NewReader(statementBytes), pool)
|
||||
if err == nil {
|
||||
t.Fatalf("No error thrown verifying without an authority")
|
||||
} else if _, ok := err.(x509.UnknownAuthorityError); !ok {
|
||||
t.Fatalf("Unexpected error verifying without authority: %s", err)
|
||||
}
|
||||
|
||||
s2, err = LoadStatement(bytes.NewReader(statementBytes), nil)
|
||||
if err != nil {
|
||||
t.Fatalf("Error loading statement: %s", err)
|
||||
}
|
||||
if len(s2.Grants) != grantCount {
|
||||
t.Fatalf("Unexpected grant length\n\tExpected: %d\n\tActual: %d", grantCount, len(s2.Grants))
|
||||
}
|
||||
|
||||
badData := make([]byte, len(statementBytes))
|
||||
copy(badData, statementBytes)
|
||||
badData[0] = '['
|
||||
_, err = LoadStatement(bytes.NewReader(badData), nil)
|
||||
if err == nil {
|
||||
t.Fatalf("No error thrown parsing bad json")
|
||||
}
|
||||
|
||||
alteredData := make([]byte, len(statementBytes))
|
||||
copy(alteredData, statementBytes)
|
||||
alteredData[30] = '0'
|
||||
_, err = LoadStatement(bytes.NewReader(alteredData), nil)
|
||||
if err == nil {
|
||||
t.Fatalf("No error thrown from bad data")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCollapseGrants(t *testing.T) {
|
||||
grantCount := 8
|
||||
grants, keys := createTestKeysAndGrants(grantCount)
|
||||
linkGrants := make([]*Grant, 4)
|
||||
linkGrants[0] = &Grant{
|
||||
Subject: "/user-3",
|
||||
Permission: 0x0f,
|
||||
Grantee: "/user-2",
|
||||
}
|
||||
linkGrants[1] = &Grant{
|
||||
Subject: "/user-3/sub-project",
|
||||
Permission: 0x0f,
|
||||
Grantee: "/user-4",
|
||||
}
|
||||
linkGrants[2] = &Grant{
|
||||
Subject: "/user-6",
|
||||
Permission: 0x0f,
|
||||
Grantee: "/user-7",
|
||||
}
|
||||
linkGrants[3] = &Grant{
|
||||
Subject: "/user-6/sub-project/specific-app",
|
||||
Permission: 0x0f,
|
||||
Grantee: "/user-5",
|
||||
}
|
||||
trustKey, pool, chain := generateTrustChain(t, 3)
|
||||
|
||||
statements := make([]*Statement, 3)
|
||||
var err error
|
||||
statements[0], err = generateStatement(grants[0:4], trustKey, chain)
|
||||
if err != nil {
|
||||
t.Fatalf("Error generating statement: %s", err)
|
||||
}
|
||||
statements[1], err = generateStatement(grants[4:], trustKey, chain)
|
||||
if err != nil {
|
||||
t.Fatalf("Error generating statement: %s", err)
|
||||
}
|
||||
statements[2], err = generateStatement(linkGrants, trustKey, chain)
|
||||
if err != nil {
|
||||
t.Fatalf("Error generating statement: %s", err)
|
||||
}
|
||||
|
||||
statementsCopy := make([]*Statement, len(statements))
|
||||
for i, statement := range statements {
|
||||
b, err := statement.Bytes()
|
||||
if err != nil {
|
||||
t.Fatalf("Error getting statement bytes: %s", err)
|
||||
}
|
||||
verifiedStatement, err := LoadStatement(bytes.NewReader(b), pool)
|
||||
if err != nil {
|
||||
t.Fatalf("Error loading statement: %s", err)
|
||||
}
|
||||
// Force sort by reversing order
|
||||
statementsCopy[len(statementsCopy)-i-1] = verifiedStatement
|
||||
}
|
||||
statements = statementsCopy
|
||||
|
||||
collapsedGrants, expiration, err := CollapseStatements(statements, false)
|
||||
if len(collapsedGrants) != 12 {
|
||||
t.Fatalf("Unexpected number of grants\n\tExpected: %d\n\tActual: %s", 12, len(collapsedGrants))
|
||||
}
|
||||
if expiration.After(time.Now().Add(time.Hour*5)) || expiration.Before(time.Now()) {
|
||||
t.Fatalf("Unexpected expiration time: %s", expiration.String())
|
||||
}
|
||||
g := NewMemoryGraph(collapsedGrants)
|
||||
|
||||
testVerified(t, g, keys[0].PublicKey(), "user-key-1", "/user-1", 0x0f)
|
||||
testVerified(t, g, keys[1].PublicKey(), "user-key-2", "/user-2", 0x0f)
|
||||
testVerified(t, g, keys[2].PublicKey(), "user-key-3", "/user-3", 0x0f)
|
||||
testVerified(t, g, keys[3].PublicKey(), "user-key-4", "/user-4", 0x0f)
|
||||
testVerified(t, g, keys[4].PublicKey(), "user-key-5", "/user-5", 0x0f)
|
||||
testVerified(t, g, keys[5].PublicKey(), "user-key-6", "/user-6", 0x0f)
|
||||
testVerified(t, g, keys[6].PublicKey(), "user-key-7", "/user-7", 0x0f)
|
||||
testVerified(t, g, keys[7].PublicKey(), "user-key-8", "/user-8", 0x0f)
|
||||
testVerified(t, g, keys[1].PublicKey(), "user-key-2", "/user-3", 0x0f)
|
||||
testVerified(t, g, keys[1].PublicKey(), "user-key-2", "/user-3/sub-project/specific-app", 0x0f)
|
||||
testVerified(t, g, keys[3].PublicKey(), "user-key-4", "/user-3/sub-project", 0x0f)
|
||||
testVerified(t, g, keys[6].PublicKey(), "user-key-7", "/user-6", 0x0f)
|
||||
testVerified(t, g, keys[6].PublicKey(), "user-key-7", "/user-6/sub-project/specific-app", 0x0f)
|
||||
testVerified(t, g, keys[4].PublicKey(), "user-key-5", "/user-6/sub-project/specific-app", 0x0f)
|
||||
|
||||
testNotVerified(t, g, keys[3].PublicKey(), "user-key-4", "/user-3", 0x0f)
|
||||
testNotVerified(t, g, keys[3].PublicKey(), "user-key-4", "/user-6/sub-project", 0x0f)
|
||||
testNotVerified(t, g, keys[4].PublicKey(), "user-key-5", "/user-6/sub-project", 0x0f)
|
||||
|
||||
// Add revocation grant
|
||||
statements = append(statements, &Statement{
|
||||
jsonStatement{
|
||||
IssuedAt: time.Now(),
|
||||
Expiration: time.Now().Add(testStatementExpiration),
|
||||
Grants: []*jsonGrant{},
|
||||
Revocations: []*jsonRevocation{
|
||||
&jsonRevocation{
|
||||
Subject: "/user-1",
|
||||
Revocation: 0x0f,
|
||||
Grantee: keys[0].KeyID(),
|
||||
},
|
||||
&jsonRevocation{
|
||||
Subject: "/user-2",
|
||||
Revocation: 0x08,
|
||||
Grantee: keys[1].KeyID(),
|
||||
},
|
||||
&jsonRevocation{
|
||||
Subject: "/user-6",
|
||||
Revocation: 0x0f,
|
||||
Grantee: "/user-7",
|
||||
},
|
||||
&jsonRevocation{
|
||||
Subject: "/user-9",
|
||||
Revocation: 0x0f,
|
||||
Grantee: "/user-10",
|
||||
},
|
||||
},
|
||||
},
|
||||
nil,
|
||||
})
|
||||
|
||||
collapsedGrants, expiration, err = CollapseStatements(statements, false)
|
||||
if len(collapsedGrants) != 12 {
|
||||
t.Fatalf("Unexpected number of grants\n\tExpected: %d\n\tActual: %s", 12, len(collapsedGrants))
|
||||
}
|
||||
if expiration.After(time.Now().Add(time.Hour*5)) || expiration.Before(time.Now()) {
|
||||
t.Fatalf("Unexpected expiration time: %s", expiration.String())
|
||||
}
|
||||
g = NewMemoryGraph(collapsedGrants)
|
||||
|
||||
testNotVerified(t, g, keys[0].PublicKey(), "user-key-1", "/user-1", 0x0f)
|
||||
testNotVerified(t, g, keys[1].PublicKey(), "user-key-2", "/user-2", 0x0f)
|
||||
testNotVerified(t, g, keys[6].PublicKey(), "user-key-7", "/user-6/sub-project/specific-app", 0x0f)
|
||||
|
||||
testVerified(t, g, keys[1].PublicKey(), "user-key-2", "/user-2", 0x07)
|
||||
}
|
||||
|
||||
func TestFilterStatements(t *testing.T) {
|
||||
grantCount := 8
|
||||
grants, keys := createTestKeysAndGrants(grantCount)
|
||||
linkGrants := make([]*Grant, 3)
|
||||
linkGrants[0] = &Grant{
|
||||
Subject: "/user-3",
|
||||
Permission: 0x0f,
|
||||
Grantee: "/user-2",
|
||||
}
|
||||
linkGrants[1] = &Grant{
|
||||
Subject: "/user-5",
|
||||
Permission: 0x0f,
|
||||
Grantee: "/user-4",
|
||||
}
|
||||
linkGrants[2] = &Grant{
|
||||
Subject: "/user-7",
|
||||
Permission: 0x0f,
|
||||
Grantee: "/user-6",
|
||||
}
|
||||
|
||||
trustKey, _, chain := generateTrustChain(t, 3)
|
||||
|
||||
statements := make([]*Statement, 5)
|
||||
var err error
|
||||
statements[0], err = generateStatement(grants[0:2], trustKey, chain)
|
||||
if err != nil {
|
||||
t.Fatalf("Error generating statement: %s", err)
|
||||
}
|
||||
statements[1], err = generateStatement(grants[2:4], trustKey, chain)
|
||||
if err != nil {
|
||||
t.Fatalf("Error generating statement: %s", err)
|
||||
}
|
||||
statements[2], err = generateStatement(grants[4:6], trustKey, chain)
|
||||
if err != nil {
|
||||
t.Fatalf("Error generating statement: %s", err)
|
||||
}
|
||||
statements[3], err = generateStatement(grants[6:], trustKey, chain)
|
||||
if err != nil {
|
||||
t.Fatalf("Error generating statement: %s", err)
|
||||
}
|
||||
statements[4], err = generateStatement(linkGrants, trustKey, chain)
|
||||
if err != nil {
|
||||
t.Fatalf("Error generating statement: %s", err)
|
||||
}
|
||||
collapsed, _, err := CollapseStatements(statements, false)
|
||||
if err != nil {
|
||||
t.Fatalf("Error collapsing grants: %s", err)
|
||||
}
|
||||
|
||||
// Filter 1, all 5 statements
|
||||
filter1, err := FilterStatements(collapsed)
|
||||
if err != nil {
|
||||
t.Fatalf("Error filtering statements: %s", err)
|
||||
}
|
||||
if len(filter1) != 5 {
|
||||
t.Fatalf("Wrong number of statements, expected %d, received %d", 5, len(filter1))
|
||||
}
|
||||
|
||||
// Filter 2, one statement
|
||||
filter2, err := FilterStatements([]*Grant{collapsed[0]})
|
||||
if err != nil {
|
||||
t.Fatalf("Error filtering statements: %s", err)
|
||||
}
|
||||
if len(filter2) != 1 {
|
||||
t.Fatalf("Wrong number of statements, expected %d, received %d", 1, len(filter2))
|
||||
}
|
||||
|
||||
// Filter 3, 2 statements, from graph lookup
|
||||
g := NewMemoryGraph(collapsed)
|
||||
lookupGrants, err := g.GetGrants(keys[1], "/user-3", 0x0f)
|
||||
if err != nil {
|
||||
t.Fatalf("Error looking up grants: %s", err)
|
||||
}
|
||||
if len(lookupGrants) != 1 {
|
||||
t.Fatalf("Wrong numberof grant chains returned from lookup, expected %d, received %d", 1, len(lookupGrants))
|
||||
}
|
||||
if len(lookupGrants[0]) != 2 {
|
||||
t.Fatalf("Wrong number of grants looked up, expected %d, received %d", 2, len(lookupGrants))
|
||||
}
|
||||
filter3, err := FilterStatements(lookupGrants[0])
|
||||
if err != nil {
|
||||
t.Fatalf("Error filtering statements: %s", err)
|
||||
}
|
||||
if len(filter3) != 2 {
|
||||
t.Fatalf("Wrong number of statements, expected %d, received %d", 2, len(filter3))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestCreateStatement(t *testing.T) {
|
||||
grantJSON := bytes.NewReader([]byte(`[
|
||||
{
|
||||
"subject": "/user-2",
|
||||
"permission": 15,
|
||||
"grantee": "/user-1"
|
||||
},
|
||||
{
|
||||
"subject": "/user-7",
|
||||
"permission": 1,
|
||||
"grantee": "/user-9"
|
||||
},
|
||||
{
|
||||
"subject": "/user-3",
|
||||
"permission": 15,
|
||||
"grantee": "/user-2"
|
||||
}
|
||||
]`))
|
||||
revocationJSON := bytes.NewReader([]byte(`[
|
||||
{
|
||||
"subject": "user-8",
|
||||
"revocation": 12,
|
||||
"grantee": "user-9"
|
||||
}
|
||||
]`))
|
||||
|
||||
trustKey, pool, chain := generateTrustChain(t, 3)
|
||||
|
||||
statement, err := CreateStatement(grantJSON, revocationJSON, testStatementExpiration, trustKey, chain)
|
||||
if err != nil {
|
||||
t.Fatalf("Error creating statement: %s", err)
|
||||
}
|
||||
|
||||
b, err := statement.Bytes()
|
||||
if err != nil {
|
||||
t.Fatalf("Error retrieving bytes: %s", err)
|
||||
}
|
||||
|
||||
verified, err := LoadStatement(bytes.NewReader(b), pool)
|
||||
if err != nil {
|
||||
t.Fatalf("Error loading statement: %s", err)
|
||||
}
|
||||
|
||||
if len(verified.Grants) != 3 {
|
||||
t.Errorf("Unexpected number of grants, expected %d, received %d", 3, len(verified.Grants))
|
||||
}
|
||||
|
||||
if len(verified.Revocations) != 1 {
|
||||
t.Errorf("Unexpected number of revocations, expected %d, received %d", 1, len(verified.Revocations))
|
||||
}
|
||||
}
|
Loading…
Add table
Reference in a new issue