Browse code

Add token pass-thru for AuthConfig

This change allows API clients to retrieve an authentication token from
a registry, and then pass that token directly to the API.

Example usage:

REPO_USER=dhiltgen
read -s PASSWORD
REPO=privateorg/repo
AUTH_URL=https://auth.docker.io/token
TOKEN=$(curl -s -u "${REPO_USER}:${PASSWORD}" "${AUTH_URL}?scope=repository:${REPO}:pull&service=registry.docker.io" |
jq -r ".token")

HEADER=$(echo "{\"registrytoken\":\"${TOKEN}\"}"|base64 -w 0 )
curl -s -D - -H "X-Registry-Auth: ${HEADER}" -X POST "http://localhost:2376/images/create?fromImage=${REPO}"

Signed-off-by: Daniel Hiltgen <daniel.hiltgen@docker.com>

Daniel Hiltgen authored on 2015/11/04 14:03:12
Showing 5 changed files
... ...
@@ -51,6 +51,7 @@ type AuthConfig struct {
51 51
 	Auth          string `json:"auth"`
52 52
 	Email         string `json:"email"`
53 53
 	ServerAddress string `json:"serveraddress,omitempty"`
54
+	RegistryToken string `json:"registrytoken,omitempty"`
54 55
 }
55 56
 
56 57
 // ConfigFile ~/.docker/config.json file info
... ...
@@ -2,6 +2,7 @@ package distribution
2 2
 
3 3
 import (
4 4
 	"errors"
5
+	"fmt"
5 6
 	"net"
6 7
 	"net/http"
7 8
 	"net/url"
... ...
@@ -91,10 +92,15 @@ func NewV2Repository(repoInfo *registry.RepositoryInfo, endpoint registry.APIEnd
91 91
 		return nil, err
92 92
 	}
93 93
 
94
-	creds := dumbCredentialStore{auth: authConfig}
95
-	tokenHandler := auth.NewTokenHandler(authTransport, creds, repoName.Name(), actions...)
96
-	basicHandler := auth.NewBasicHandler(creds)
97
-	modifiers = append(modifiers, auth.NewAuthorizer(challengeManager, tokenHandler, basicHandler))
94
+	if authConfig.RegistryToken != "" {
95
+		passThruTokenHandler := &existingTokenHandler{token: authConfig.RegistryToken}
96
+		modifiers = append(modifiers, auth.NewAuthorizer(challengeManager, passThruTokenHandler))
97
+	} else {
98
+		creds := dumbCredentialStore{auth: authConfig}
99
+		tokenHandler := auth.NewTokenHandler(authTransport, creds, repoName.Name(), actions...)
100
+		basicHandler := auth.NewBasicHandler(creds)
101
+		modifiers = append(modifiers, auth.NewAuthorizer(challengeManager, tokenHandler, basicHandler))
102
+	}
98 103
 	tr := transport.NewTransport(base, modifiers...)
99 104
 
100 105
 	return client.NewRepository(ctx, repoName.Name(), endpoint.URL, tr)
... ...
@@ -113,3 +119,16 @@ func digestFromManifest(m *schema1.SignedManifest, localName string) (digest.Dig
113 113
 	}
114 114
 	return manifestDigest, len(payload), nil
115 115
 }
116
+
117
+type existingTokenHandler struct {
118
+	token string
119
+}
120
+
121
+func (th *existingTokenHandler) Scheme() string {
122
+	return "bearer"
123
+}
124
+
125
+func (th *existingTokenHandler) AuthorizeRequest(req *http.Request, params map[string]string) error {
126
+	req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", th.token))
127
+	return nil
128
+}
116 129
new file mode 100644
... ...
@@ -0,0 +1,95 @@
0
+package distribution
1
+
2
+import (
3
+	"net/http"
4
+	"net/http/httptest"
5
+	"os"
6
+	"strings"
7
+	"testing"
8
+
9
+	"github.com/Sirupsen/logrus"
10
+	"github.com/docker/distribution/reference"
11
+	"github.com/docker/distribution/registry/client/auth"
12
+	"github.com/docker/docker/cliconfig"
13
+	"github.com/docker/docker/pkg/streamformatter"
14
+	"github.com/docker/docker/registry"
15
+	"github.com/docker/docker/utils"
16
+)
17
+
18
+func TestTokenPassThru(t *testing.T) {
19
+	authConfig := &cliconfig.AuthConfig{
20
+		RegistryToken: "mysecrettoken",
21
+	}
22
+	gotToken := false
23
+	handler := func(w http.ResponseWriter, r *http.Request) {
24
+		if strings.Contains(r.Header.Get("Authorization"), authConfig.RegistryToken) {
25
+			logrus.Debug("Detected registry token in auth header")
26
+			gotToken = true
27
+		}
28
+		if r.RequestURI == "/v2/" {
29
+			w.Header().Set("WWW-Authenticate", `Bearer realm="foorealm"`)
30
+			w.WriteHeader(401)
31
+		}
32
+	}
33
+	ts := httptest.NewServer(http.HandlerFunc(handler))
34
+	defer ts.Close()
35
+
36
+	tmp, err := utils.TestDirectory("")
37
+	if err != nil {
38
+		t.Fatal(err)
39
+	}
40
+	defer os.RemoveAll(tmp)
41
+
42
+	endpoint := registry.APIEndpoint{
43
+		Mirror:       false,
44
+		URL:          ts.URL,
45
+		Version:      2,
46
+		Official:     false,
47
+		TrimHostname: false,
48
+		TLSConfig:    nil,
49
+		//VersionHeader: "verheader",
50
+		Versions: []auth.APIVersion{
51
+			{
52
+				Type:    "registry",
53
+				Version: "2",
54
+			},
55
+		},
56
+	}
57
+	n, _ := reference.ParseNamed("testremotename")
58
+	repoInfo := &registry.RepositoryInfo{
59
+		Index: &registry.IndexInfo{
60
+			Name:     "testrepo",
61
+			Mirrors:  nil,
62
+			Secure:   false,
63
+			Official: false,
64
+		},
65
+		RemoteName:    n,
66
+		LocalName:     n,
67
+		CanonicalName: n,
68
+		Official:      false,
69
+	}
70
+	imagePullConfig := &ImagePullConfig{
71
+		MetaHeaders: http.Header{},
72
+		AuthConfig:  authConfig,
73
+	}
74
+	sf := streamformatter.NewJSONStreamFormatter()
75
+	puller, err := newPuller(endpoint, repoInfo, imagePullConfig, sf)
76
+	if err != nil {
77
+		t.Fatal(err)
78
+	}
79
+	p := puller.(*v2Puller)
80
+	p.repo, err = NewV2Repository(p.repoInfo, p.endpoint, p.config.MetaHeaders, p.config.AuthConfig, "pull")
81
+	if err != nil {
82
+		t.Fatal(err)
83
+	}
84
+
85
+	logrus.Debug("About to pull")
86
+	// We expect it to fail, since we haven't mock'd the full registry exchange in our handler above
87
+	tag, _ := reference.WithTag(n, "tag_goes_here")
88
+	_ = p.pullV2Repository(tag)
89
+
90
+	if !gotToken {
91
+		t.Fatal("Failed to receive registry token")
92
+	}
93
+
94
+}
... ...
@@ -101,6 +101,7 @@ This section lists each version from latest to oldest.  Each listing includes a
101 101
 * `GET /networks/(name)` now returns a `Name` field for each container attached to the network.
102 102
 * `GET /version` now returns the `BuildTime` field in RFC3339Nano format to make it 
103 103
   consistent with other date/time values returned by the API.
104
+* `AuthConfig` now supports a `registrytoken` for token based authentication
104 105
 
105 106
 ### v1.21 API changes
106 107
 
... ...
@@ -1532,7 +1532,24 @@ Query Parameters:
1532 1532
 
1533 1533
     Request Headers:
1534 1534
 
1535
--   **X-Registry-Auth** – base64-encoded AuthConfig object
1535
+-   **X-Registry-Auth** – base64-encoded AuthConfig object, containing either login information, or a token
1536
+    - Credential based login:
1537
+
1538
+        ```
1539
+    {
1540
+            "username": "jdoe",
1541
+            "password": "secret",
1542
+            "email": "jdoe@acme.com",
1543
+    }
1544
+        ```
1545
+
1546
+    - Token based login:
1547
+
1548
+        ```
1549
+    {
1550
+            "registrytoken": "9cbaf023786cd7..."
1551
+    }
1552
+        ```
1536 1553
 
1537 1554
 Status Codes:
1538 1555
 
... ...
@@ -1741,8 +1758,24 @@ Query Parameters:
1741 1741
 
1742 1742
 Request Headers:
1743 1743
 
1744
--   **X-Registry-Auth** – Include a base64-encoded AuthConfig.
1745
-        object.
1744
+-   **X-Registry-Auth** – base64-encoded AuthConfig object, containing either login information, or a token
1745
+    - Credential based login:
1746
+
1747
+        ```
1748
+    {
1749
+            "username": "jdoe",
1750
+            "password": "secret",
1751
+            "email": "jdoe@acme.com",
1752
+    }
1753
+        ```
1754
+
1755
+    - Token based login:
1756
+
1757
+        ```
1758
+    {
1759
+            "registrytoken": "9cbaf023786cd7..."
1760
+    }
1761
+        ```
1746 1762
 
1747 1763
 Status Codes:
1748 1764