Signed-off-by: Austin Vazquez <austin.vazquez@docker.com>
| 1 | 1 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,92 @@ |
| 0 |
+package authconfig |
|
| 1 |
+ |
|
| 2 |
+import ( |
|
| 3 |
+ "bytes" |
|
| 4 |
+ "encoding/base64" |
|
| 5 |
+ "encoding/json" |
|
| 6 |
+ "errors" |
|
| 7 |
+ "fmt" |
|
| 8 |
+ "io" |
|
| 9 |
+ |
|
| 10 |
+ "github.com/moby/moby/api/types/registry" |
|
| 11 |
+) |
|
| 12 |
+ |
|
| 13 |
+// Encode serializes the auth configuration as a base64url encoded |
|
| 14 |
+// ([RFC4648, section 5]) JSON string for sending through the X-Registry-Auth header. |
|
| 15 |
+// |
|
| 16 |
+// [RFC4648, section 5]: https://tools.ietf.org/html/rfc4648#section-5 |
|
| 17 |
+func Encode(authConfig registry.AuthConfig) (string, error) {
|
|
| 18 |
+ // Older daemons (or registries) may not handle an empty string, |
|
| 19 |
+ // which resulted in an "io.EOF" when unmarshaling or decoding. |
|
| 20 |
+ // |
|
| 21 |
+ // FIXME(thaJeztah): find exactly what code-paths are impacted by this. |
|
| 22 |
+ // if authConfig == (AuthConfig{}) { return "", nil }
|
|
| 23 |
+ buf, err := json.Marshal(authConfig) |
|
| 24 |
+ if err != nil {
|
|
| 25 |
+ return "", errInvalidParameter{err}
|
|
| 26 |
+ } |
|
| 27 |
+ return base64.URLEncoding.EncodeToString(buf), nil |
|
| 28 |
+} |
|
| 29 |
+ |
|
| 30 |
+// Decode decodes base64url encoded ([RFC4648, section 5]) JSON |
|
| 31 |
+// authentication information as sent through the X-Registry-Auth header. |
|
| 32 |
+// |
|
| 33 |
+// This function always returns an [AuthConfig], even if an error occurs. It is up |
|
| 34 |
+// to the caller to decide if authentication is required, and if the error can |
|
| 35 |
+// be ignored. |
|
| 36 |
+// |
|
| 37 |
+// [RFC4648, section 5]: https://tools.ietf.org/html/rfc4648#section-5 |
|
| 38 |
+func Decode(authEncoded string) (*registry.AuthConfig, error) {
|
|
| 39 |
+ if authEncoded == "" {
|
|
| 40 |
+ return ®istry.AuthConfig{}, nil
|
|
| 41 |
+ } |
|
| 42 |
+ |
|
| 43 |
+ decoded, err := base64.URLEncoding.DecodeString(authEncoded) |
|
| 44 |
+ if err != nil {
|
|
| 45 |
+ var e base64.CorruptInputError |
|
| 46 |
+ if errors.As(err, &e) {
|
|
| 47 |
+ return ®istry.AuthConfig{}, invalid(errors.New("must be a valid base64url-encoded string"))
|
|
| 48 |
+ } |
|
| 49 |
+ return ®istry.AuthConfig{}, invalid(err)
|
|
| 50 |
+ } |
|
| 51 |
+ |
|
| 52 |
+ if bytes.Equal(decoded, []byte("{}")) {
|
|
| 53 |
+ return ®istry.AuthConfig{}, nil
|
|
| 54 |
+ } |
|
| 55 |
+ |
|
| 56 |
+ return decode(bytes.NewReader(decoded)) |
|
| 57 |
+} |
|
| 58 |
+ |
|
| 59 |
+// DecodeRequestBody decodes authentication information as sent as JSON in the |
|
| 60 |
+// body of a request. This function is to provide backward compatibility with old |
|
| 61 |
+// clients and API versions. Current clients and API versions expect authentication |
|
| 62 |
+// to be provided through the X-Registry-Auth header. |
|
| 63 |
+// |
|
| 64 |
+// Like [Decode], this function always returns an [AuthConfig], even if an |
|
| 65 |
+// error occurs. It is up to the caller to decide if authentication is required, |
|
| 66 |
+// and if the error can be ignored. |
|
| 67 |
+func DecodeRequestBody(r io.ReadCloser) (*registry.AuthConfig, error) {
|
|
| 68 |
+ return decode(r) |
|
| 69 |
+} |
|
| 70 |
+ |
|
| 71 |
+func decode(r io.Reader) (*registry.AuthConfig, error) {
|
|
| 72 |
+ authConfig := ®istry.AuthConfig{}
|
|
| 73 |
+ if err := json.NewDecoder(r).Decode(authConfig); err != nil {
|
|
| 74 |
+ // always return an (empty) AuthConfig to increase compatibility with |
|
| 75 |
+ // the existing API. |
|
| 76 |
+ return ®istry.AuthConfig{}, invalid(fmt.Errorf("invalid JSON: %w", err))
|
|
| 77 |
+ } |
|
| 78 |
+ return authConfig, nil |
|
| 79 |
+} |
|
| 80 |
+ |
|
| 81 |
+func invalid(err error) error {
|
|
| 82 |
+ return errInvalidParameter{fmt.Errorf("invalid X-Registry-Auth header: %w", err)}
|
|
| 83 |
+} |
|
| 84 |
+ |
|
| 85 |
+type errInvalidParameter struct{ error }
|
|
| 86 |
+ |
|
| 87 |
+func (errInvalidParameter) InvalidParameter() {}
|
|
| 88 |
+ |
|
| 89 |
+func (e errInvalidParameter) Cause() error { return e.error }
|
|
| 90 |
+ |
|
| 91 |
+func (e errInvalidParameter) Unwrap() error { return e.error }
|
| 0 | 92 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,191 @@ |
| 0 |
+package authconfig |
|
| 1 |
+ |
|
| 2 |
+import ( |
|
| 3 |
+ "encoding/base64" |
|
| 4 |
+ "strings" |
|
| 5 |
+ "testing" |
|
| 6 |
+ |
|
| 7 |
+ "github.com/moby/moby/api/types/registry" |
|
| 8 |
+ "gotest.tools/v3/assert" |
|
| 9 |
+ is "gotest.tools/v3/assert/cmp" |
|
| 10 |
+) |
|
| 11 |
+ |
|
| 12 |
+func TestDecodeAuthConfig(t *testing.T) {
|
|
| 13 |
+ tests := []struct {
|
|
| 14 |
+ doc string |
|
| 15 |
+ input string |
|
| 16 |
+ inputBase64 string |
|
| 17 |
+ expected registry.AuthConfig |
|
| 18 |
+ expectedErr string |
|
| 19 |
+ }{
|
|
| 20 |
+ {
|
|
| 21 |
+ doc: "empty", |
|
| 22 |
+ input: ``, |
|
| 23 |
+ inputBase64: ``, |
|
| 24 |
+ expected: registry.AuthConfig{},
|
|
| 25 |
+ }, |
|
| 26 |
+ {
|
|
| 27 |
+ doc: "empty JSON", |
|
| 28 |
+ input: `{}`,
|
|
| 29 |
+ inputBase64: `e30=`, |
|
| 30 |
+ expected: registry.AuthConfig{},
|
|
| 31 |
+ }, |
|
| 32 |
+ {
|
|
| 33 |
+ doc: "malformed JSON", |
|
| 34 |
+ input: `{`,
|
|
| 35 |
+ inputBase64: `ew==`, |
|
| 36 |
+ expected: registry.AuthConfig{},
|
|
| 37 |
+ expectedErr: `invalid X-Registry-Auth header: invalid JSON: unexpected EOF`, |
|
| 38 |
+ }, |
|
| 39 |
+ {
|
|
| 40 |
+ doc: "test authConfig", |
|
| 41 |
+ input: `{"username":"testuser","password":"testpassword","serveraddress":"example.com"}`,
|
|
| 42 |
+ inputBase64: `eyJ1c2VybmFtZSI6InRlc3R1c2VyIiwicGFzc3dvcmQiOiJ0ZXN0cGFzc3dvcmQiLCJzZXJ2ZXJhZGRyZXNzIjoiZXhhbXBsZS5jb20ifQ==`, |
|
| 43 |
+ expected: registry.AuthConfig{
|
|
| 44 |
+ Username: "testuser", |
|
| 45 |
+ Password: "testpassword", |
|
| 46 |
+ ServerAddress: "example.com", |
|
| 47 |
+ }, |
|
| 48 |
+ }, |
|
| 49 |
+ {
|
|
| 50 |
+ // FIXME(thaJeztah): we should not accept multiple JSON documents. |
|
| 51 |
+ doc: "multiple authConfig", |
|
| 52 |
+ input: `{"username":"testuser","password":"testpassword","serveraddress":"example.com"}{"username":"testuser2","password":"testpassword2","serveraddress":"example.org"}`,
|
|
| 53 |
+ inputBase64: `eyJ1c2VybmFtZSI6InRlc3R1c2VyIiwicGFzc3dvcmQiOiJ0ZXN0cGFzc3dvcmQiLCJzZXJ2ZXJhZGRyZXNzIjoiZXhhbXBsZS5jb20ifXsidXNlcm5hbWUiOiJ0ZXN0dXNlcjIiLCJwYXNzd29yZCI6InRlc3RwYXNzd29yZDIiLCJzZXJ2ZXJhZGRyZXNzIjoiZXhhbXBsZS5vcmcifQ==`, |
|
| 54 |
+ expected: registry.AuthConfig{
|
|
| 55 |
+ Username: "testuser", |
|
| 56 |
+ Password: "testpassword", |
|
| 57 |
+ ServerAddress: "example.com", |
|
| 58 |
+ }, |
|
| 59 |
+ }, |
|
| 60 |
+ // We currently only support base64url encoding with padding, so |
|
| 61 |
+ // un-padded should produce an error. |
|
| 62 |
+ // |
|
| 63 |
+ // RFC4648, section 5: https://tools.ietf.org/html/rfc4648#section-5 |
|
| 64 |
+ // RFC4648, section 3.2: https://tools.ietf.org/html/rfc4648#section-3.2 |
|
| 65 |
+ {
|
|
| 66 |
+ doc: "empty JSON no padding", |
|
| 67 |
+ input: `{}`,
|
|
| 68 |
+ inputBase64: `e30`, |
|
| 69 |
+ expected: registry.AuthConfig{},
|
|
| 70 |
+ expectedErr: `invalid X-Registry-Auth header: must be a valid base64url-encoded string`, |
|
| 71 |
+ }, |
|
| 72 |
+ {
|
|
| 73 |
+ doc: "test authConfig", |
|
| 74 |
+ input: `{"username":"testuser","password":"testpassword","serveraddress":"example.com"}`,
|
|
| 75 |
+ inputBase64: `eyJ1c2VybmFtZSI6InRlc3R1c2VyIiwicGFzc3dvcmQiOiJ0ZXN0cGFzc3dvcmQiLCJzZXJ2ZXJhZGRyZXNzIjoiZXhhbXBsZS5jb20ifQ`, |
|
| 76 |
+ expected: registry.AuthConfig{},
|
|
| 77 |
+ expectedErr: `invalid X-Registry-Auth header: must be a valid base64url-encoded string`, |
|
| 78 |
+ }, |
|
| 79 |
+ } |
|
| 80 |
+ |
|
| 81 |
+ for _, tc := range tests {
|
|
| 82 |
+ t.Run(tc.doc, func(t *testing.T) {
|
|
| 83 |
+ if tc.inputBase64 != "" {
|
|
| 84 |
+ // Sanity check to make sure our fixtures are correct. |
|
| 85 |
+ b64 := base64.URLEncoding.EncodeToString([]byte(tc.input)) |
|
| 86 |
+ if !strings.HasSuffix(tc.inputBase64, "=") {
|
|
| 87 |
+ b64 = strings.TrimRight(b64, "=") |
|
| 88 |
+ } |
|
| 89 |
+ assert.Check(t, is.Equal(b64, tc.inputBase64)) |
|
| 90 |
+ } |
|
| 91 |
+ |
|
| 92 |
+ out, err := Decode(tc.inputBase64) |
|
| 93 |
+ if tc.expectedErr != "" {
|
|
| 94 |
+ assert.Check(t, is.ErrorType(err, errInvalidParameter{}))
|
|
| 95 |
+ assert.Check(t, is.Error(err, tc.expectedErr)) |
|
| 96 |
+ } else {
|
|
| 97 |
+ assert.NilError(t, err) |
|
| 98 |
+ assert.Equal(t, *out, tc.expected) |
|
| 99 |
+ } |
|
| 100 |
+ }) |
|
| 101 |
+ } |
|
| 102 |
+} |
|
| 103 |
+ |
|
| 104 |
+func TestEncodeAuthConfig(t *testing.T) {
|
|
| 105 |
+ tests := []struct {
|
|
| 106 |
+ doc string |
|
| 107 |
+ input registry.AuthConfig |
|
| 108 |
+ outBase64 string |
|
| 109 |
+ outPlain string |
|
| 110 |
+ }{
|
|
| 111 |
+ {
|
|
| 112 |
+ // Older daemons (or registries) may not handle an empty string, |
|
| 113 |
+ // which resulted in an "io.EOF" when unmarshaling or decoding. |
|
| 114 |
+ // |
|
| 115 |
+ // FIXME(thaJeztah): find exactly what code-paths are impacted by this. |
|
| 116 |
+ doc: "empty", |
|
| 117 |
+ input: registry.AuthConfig{},
|
|
| 118 |
+ outBase64: `e30=`, |
|
| 119 |
+ outPlain: `{}`,
|
|
| 120 |
+ }, |
|
| 121 |
+ {
|
|
| 122 |
+ doc: "test authConfig", |
|
| 123 |
+ input: registry.AuthConfig{
|
|
| 124 |
+ Username: "testuser", |
|
| 125 |
+ Password: "testpassword", |
|
| 126 |
+ ServerAddress: "example.com", |
|
| 127 |
+ }, |
|
| 128 |
+ outBase64: `eyJ1c2VybmFtZSI6InRlc3R1c2VyIiwicGFzc3dvcmQiOiJ0ZXN0cGFzc3dvcmQiLCJzZXJ2ZXJhZGRyZXNzIjoiZXhhbXBsZS5jb20ifQ==`, |
|
| 129 |
+ outPlain: `{"username":"testuser","password":"testpassword","serveraddress":"example.com"}`,
|
|
| 130 |
+ }, |
|
| 131 |
+ } |
|
| 132 |
+ for _, tc := range tests {
|
|
| 133 |
+ // Sanity check to make sure our fixtures are correct. |
|
| 134 |
+ b64 := base64.URLEncoding.EncodeToString([]byte(tc.outPlain)) |
|
| 135 |
+ assert.Check(t, is.Equal(b64, tc.outBase64)) |
|
| 136 |
+ |
|
| 137 |
+ t.Run(tc.doc, func(t *testing.T) {
|
|
| 138 |
+ out, err := Encode(tc.input) |
|
| 139 |
+ assert.NilError(t, err) |
|
| 140 |
+ assert.Equal(t, out, tc.outBase64) |
|
| 141 |
+ |
|
| 142 |
+ authJSON, err := base64.URLEncoding.DecodeString(out) |
|
| 143 |
+ assert.NilError(t, err) |
|
| 144 |
+ assert.Equal(t, string(authJSON), tc.outPlain) |
|
| 145 |
+ }) |
|
| 146 |
+ } |
|
| 147 |
+} |
|
| 148 |
+ |
|
| 149 |
+func BenchmarkDecodeAuthConfig(b *testing.B) {
|
|
| 150 |
+ cases := []struct {
|
|
| 151 |
+ doc string |
|
| 152 |
+ inputBase64 string |
|
| 153 |
+ invalid bool |
|
| 154 |
+ }{
|
|
| 155 |
+ {
|
|
| 156 |
+ doc: "empty", |
|
| 157 |
+ inputBase64: ``, |
|
| 158 |
+ }, |
|
| 159 |
+ {
|
|
| 160 |
+ doc: "empty JSON", |
|
| 161 |
+ inputBase64: `e30=`, |
|
| 162 |
+ }, |
|
| 163 |
+ {
|
|
| 164 |
+ doc: "valid", |
|
| 165 |
+ inputBase64: base64.URLEncoding.EncodeToString([]byte(`{"username":"testuser","password":"testpassword","serveraddress":"example.com"}`)),
|
|
| 166 |
+ }, |
|
| 167 |
+ {
|
|
| 168 |
+ doc: "invalid base64", |
|
| 169 |
+ inputBase64: "not-base64", |
|
| 170 |
+ invalid: true, |
|
| 171 |
+ }, |
|
| 172 |
+ {
|
|
| 173 |
+ doc: "malformed JSON", |
|
| 174 |
+ inputBase64: `ew==`, |
|
| 175 |
+ invalid: true, |
|
| 176 |
+ }, |
|
| 177 |
+ } |
|
| 178 |
+ |
|
| 179 |
+ for _, tc := range cases {
|
|
| 180 |
+ b.Run(tc.doc, func(b *testing.B) {
|
|
| 181 |
+ b.ReportAllocs() |
|
| 182 |
+ for i := 0; i < b.N; i++ {
|
|
| 183 |
+ _, err := Decode(tc.inputBase64) |
|
| 184 |
+ if !tc.invalid && err != nil {
|
|
| 185 |
+ b.Fatal(err) |
|
| 186 |
+ } |
|
| 187 |
+ } |
|
| 188 |
+ }) |
|
| 189 |
+ } |
|
| 190 |
+} |
| ... | ... |
@@ -1,14 +1,6 @@ |
| 1 | 1 |
package registry |
| 2 | 2 |
|
| 3 |
-import ( |
|
| 4 |
- "bytes" |
|
| 5 |
- "context" |
|
| 6 |
- "encoding/base64" |
|
| 7 |
- "encoding/json" |
|
| 8 |
- "errors" |
|
| 9 |
- "fmt" |
|
| 10 |
- "io" |
|
| 11 |
-) |
|
| 3 |
+import "context" |
|
| 12 | 4 |
|
| 13 | 5 |
// AuthHeader is the name of the header used to send encoded registry |
| 14 | 6 |
// authorization credentials for registry operations (push/pull). |
| ... | ... |
@@ -46,85 +38,3 @@ type AuthConfig struct {
|
| 46 | 46 |
// RegistryToken is a bearer token to be sent to a registry |
| 47 | 47 |
RegistryToken string `json:"registrytoken,omitempty"` |
| 48 | 48 |
} |
| 49 |
- |
|
| 50 |
-// EncodeAuthConfig serializes the auth configuration as a base64url encoded |
|
| 51 |
-// ([RFC4648, section 5]) JSON string for sending through the X-Registry-Auth header. |
|
| 52 |
-// |
|
| 53 |
-// [RFC4648, section 5]: https://tools.ietf.org/html/rfc4648#section-5 |
|
| 54 |
-func EncodeAuthConfig(authConfig AuthConfig) (string, error) {
|
|
| 55 |
- // Older daemons (or registries) may not handle an empty string, |
|
| 56 |
- // which resulted in an "io.EOF" when unmarshaling or decoding. |
|
| 57 |
- // |
|
| 58 |
- // FIXME(thaJeztah): find exactly what code-paths are impacted by this. |
|
| 59 |
- // if authConfig == (AuthConfig{}) { return "", nil }
|
|
| 60 |
- buf, err := json.Marshal(authConfig) |
|
| 61 |
- if err != nil {
|
|
| 62 |
- return "", errInvalidParameter{err}
|
|
| 63 |
- } |
|
| 64 |
- return base64.URLEncoding.EncodeToString(buf), nil |
|
| 65 |
-} |
|
| 66 |
- |
|
| 67 |
-// DecodeAuthConfig decodes base64url encoded ([RFC4648, section 5]) JSON |
|
| 68 |
-// authentication information as sent through the X-Registry-Auth header. |
|
| 69 |
-// |
|
| 70 |
-// This function always returns an [AuthConfig], even if an error occurs. It is up |
|
| 71 |
-// to the caller to decide if authentication is required, and if the error can |
|
| 72 |
-// be ignored. |
|
| 73 |
-// |
|
| 74 |
-// [RFC4648, section 5]: https://tools.ietf.org/html/rfc4648#section-5 |
|
| 75 |
-func DecodeAuthConfig(authEncoded string) (*AuthConfig, error) {
|
|
| 76 |
- if authEncoded == "" {
|
|
| 77 |
- return &AuthConfig{}, nil
|
|
| 78 |
- } |
|
| 79 |
- |
|
| 80 |
- decoded, err := base64.URLEncoding.DecodeString(authEncoded) |
|
| 81 |
- if err != nil {
|
|
| 82 |
- var e base64.CorruptInputError |
|
| 83 |
- if errors.As(err, &e) {
|
|
| 84 |
- return &AuthConfig{}, invalid(errors.New("must be a valid base64url-encoded string"))
|
|
| 85 |
- } |
|
| 86 |
- return &AuthConfig{}, invalid(err)
|
|
| 87 |
- } |
|
| 88 |
- |
|
| 89 |
- if bytes.Equal(decoded, []byte("{}")) {
|
|
| 90 |
- return &AuthConfig{}, nil
|
|
| 91 |
- } |
|
| 92 |
- |
|
| 93 |
- return decodeAuthConfigFromReader(bytes.NewReader(decoded)) |
|
| 94 |
-} |
|
| 95 |
- |
|
| 96 |
-// DecodeAuthConfigBody decodes authentication information as sent as JSON in the |
|
| 97 |
-// body of a request. This function is to provide backward compatibility with old |
|
| 98 |
-// clients and API versions. Current clients and API versions expect authentication |
|
| 99 |
-// to be provided through the X-Registry-Auth header. |
|
| 100 |
-// |
|
| 101 |
-// Like [DecodeAuthConfig], this function always returns an [AuthConfig], even if an |
|
| 102 |
-// error occurs. It is up to the caller to decide if authentication is required, |
|
| 103 |
-// and if the error can be ignored. |
|
| 104 |
-// |
|
| 105 |
-// Deprecated: this function is no longer used and will be removed in the next release. |
|
| 106 |
-func DecodeAuthConfigBody(rdr io.ReadCloser) (*AuthConfig, error) {
|
|
| 107 |
- return decodeAuthConfigFromReader(rdr) |
|
| 108 |
-} |
|
| 109 |
- |
|
| 110 |
-func decodeAuthConfigFromReader(rdr io.Reader) (*AuthConfig, error) {
|
|
| 111 |
- authConfig := &AuthConfig{}
|
|
| 112 |
- if err := json.NewDecoder(rdr).Decode(authConfig); err != nil {
|
|
| 113 |
- // always return an (empty) AuthConfig to increase compatibility with |
|
| 114 |
- // the existing API. |
|
| 115 |
- return &AuthConfig{}, invalid(fmt.Errorf("invalid JSON: %w", err))
|
|
| 116 |
- } |
|
| 117 |
- return authConfig, nil |
|
| 118 |
-} |
|
| 119 |
- |
|
| 120 |
-func invalid(err error) error {
|
|
| 121 |
- return errInvalidParameter{fmt.Errorf("invalid X-Registry-Auth header: %w", err)}
|
|
| 122 |
-} |
|
| 123 |
- |
|
| 124 |
-type errInvalidParameter struct{ error }
|
|
| 125 |
- |
|
| 126 |
-func (errInvalidParameter) InvalidParameter() {}
|
|
| 127 |
- |
|
| 128 |
-func (e errInvalidParameter) Cause() error { return e.error }
|
|
| 129 |
- |
|
| 130 |
-func (e errInvalidParameter) Unwrap() error { return e.error }
|
| 131 | 49 |
deleted file mode 100644 |
| ... | ... |
@@ -1,190 +0,0 @@ |
| 1 |
-package registry |
|
| 2 |
- |
|
| 3 |
-import ( |
|
| 4 |
- "encoding/base64" |
|
| 5 |
- "strings" |
|
| 6 |
- "testing" |
|
| 7 |
- |
|
| 8 |
- "gotest.tools/v3/assert" |
|
| 9 |
- is "gotest.tools/v3/assert/cmp" |
|
| 10 |
-) |
|
| 11 |
- |
|
| 12 |
-func TestDecodeAuthConfig(t *testing.T) {
|
|
| 13 |
- tests := []struct {
|
|
| 14 |
- doc string |
|
| 15 |
- input string |
|
| 16 |
- inputBase64 string |
|
| 17 |
- expected AuthConfig |
|
| 18 |
- expectedErr string |
|
| 19 |
- }{
|
|
| 20 |
- {
|
|
| 21 |
- doc: "empty", |
|
| 22 |
- input: ``, |
|
| 23 |
- inputBase64: ``, |
|
| 24 |
- expected: AuthConfig{},
|
|
| 25 |
- }, |
|
| 26 |
- {
|
|
| 27 |
- doc: "empty JSON", |
|
| 28 |
- input: `{}`,
|
|
| 29 |
- inputBase64: `e30=`, |
|
| 30 |
- expected: AuthConfig{},
|
|
| 31 |
- }, |
|
| 32 |
- {
|
|
| 33 |
- doc: "malformed JSON", |
|
| 34 |
- input: `{`,
|
|
| 35 |
- inputBase64: `ew==`, |
|
| 36 |
- expected: AuthConfig{},
|
|
| 37 |
- expectedErr: `invalid X-Registry-Auth header: invalid JSON: unexpected EOF`, |
|
| 38 |
- }, |
|
| 39 |
- {
|
|
| 40 |
- doc: "test authConfig", |
|
| 41 |
- input: `{"username":"testuser","password":"testpassword","serveraddress":"example.com"}`,
|
|
| 42 |
- inputBase64: `eyJ1c2VybmFtZSI6InRlc3R1c2VyIiwicGFzc3dvcmQiOiJ0ZXN0cGFzc3dvcmQiLCJzZXJ2ZXJhZGRyZXNzIjoiZXhhbXBsZS5jb20ifQ==`, |
|
| 43 |
- expected: AuthConfig{
|
|
| 44 |
- Username: "testuser", |
|
| 45 |
- Password: "testpassword", |
|
| 46 |
- ServerAddress: "example.com", |
|
| 47 |
- }, |
|
| 48 |
- }, |
|
| 49 |
- {
|
|
| 50 |
- // FIXME(thaJeztah): we should not accept multiple JSON documents. |
|
| 51 |
- doc: "multiple authConfig", |
|
| 52 |
- input: `{"username":"testuser","password":"testpassword","serveraddress":"example.com"}{"username":"testuser2","password":"testpassword2","serveraddress":"example.org"}`,
|
|
| 53 |
- inputBase64: `eyJ1c2VybmFtZSI6InRlc3R1c2VyIiwicGFzc3dvcmQiOiJ0ZXN0cGFzc3dvcmQiLCJzZXJ2ZXJhZGRyZXNzIjoiZXhhbXBsZS5jb20ifXsidXNlcm5hbWUiOiJ0ZXN0dXNlcjIiLCJwYXNzd29yZCI6InRlc3RwYXNzd29yZDIiLCJzZXJ2ZXJhZGRyZXNzIjoiZXhhbXBsZS5vcmcifQ==`, |
|
| 54 |
- expected: AuthConfig{
|
|
| 55 |
- Username: "testuser", |
|
| 56 |
- Password: "testpassword", |
|
| 57 |
- ServerAddress: "example.com", |
|
| 58 |
- }, |
|
| 59 |
- }, |
|
| 60 |
- // We currently only support base64url encoding with padding, so |
|
| 61 |
- // un-padded should produce an error. |
|
| 62 |
- // |
|
| 63 |
- // RFC4648, section 5: https://tools.ietf.org/html/rfc4648#section-5 |
|
| 64 |
- // RFC4648, section 3.2: https://tools.ietf.org/html/rfc4648#section-3.2 |
|
| 65 |
- {
|
|
| 66 |
- doc: "empty JSON no padding", |
|
| 67 |
- input: `{}`,
|
|
| 68 |
- inputBase64: `e30`, |
|
| 69 |
- expected: AuthConfig{},
|
|
| 70 |
- expectedErr: `invalid X-Registry-Auth header: must be a valid base64url-encoded string`, |
|
| 71 |
- }, |
|
| 72 |
- {
|
|
| 73 |
- doc: "test authConfig", |
|
| 74 |
- input: `{"username":"testuser","password":"testpassword","serveraddress":"example.com"}`,
|
|
| 75 |
- inputBase64: `eyJ1c2VybmFtZSI6InRlc3R1c2VyIiwicGFzc3dvcmQiOiJ0ZXN0cGFzc3dvcmQiLCJzZXJ2ZXJhZGRyZXNzIjoiZXhhbXBsZS5jb20ifQ`, |
|
| 76 |
- expected: AuthConfig{},
|
|
| 77 |
- expectedErr: `invalid X-Registry-Auth header: must be a valid base64url-encoded string`, |
|
| 78 |
- }, |
|
| 79 |
- } |
|
| 80 |
- |
|
| 81 |
- for _, tc := range tests {
|
|
| 82 |
- t.Run(tc.doc, func(t *testing.T) {
|
|
| 83 |
- if tc.inputBase64 != "" {
|
|
| 84 |
- // Sanity check to make sure our fixtures are correct. |
|
| 85 |
- b64 := base64.URLEncoding.EncodeToString([]byte(tc.input)) |
|
| 86 |
- if !strings.HasSuffix(tc.inputBase64, "=") {
|
|
| 87 |
- b64 = strings.TrimRight(b64, "=") |
|
| 88 |
- } |
|
| 89 |
- assert.Check(t, is.Equal(b64, tc.inputBase64)) |
|
| 90 |
- } |
|
| 91 |
- |
|
| 92 |
- out, err := DecodeAuthConfig(tc.inputBase64) |
|
| 93 |
- if tc.expectedErr != "" {
|
|
| 94 |
- assert.Check(t, is.ErrorType(err, errInvalidParameter{}))
|
|
| 95 |
- assert.Check(t, is.Error(err, tc.expectedErr)) |
|
| 96 |
- } else {
|
|
| 97 |
- assert.NilError(t, err) |
|
| 98 |
- assert.Equal(t, *out, tc.expected) |
|
| 99 |
- } |
|
| 100 |
- }) |
|
| 101 |
- } |
|
| 102 |
-} |
|
| 103 |
- |
|
| 104 |
-func TestEncodeAuthConfig(t *testing.T) {
|
|
| 105 |
- tests := []struct {
|
|
| 106 |
- doc string |
|
| 107 |
- input AuthConfig |
|
| 108 |
- outBase64 string |
|
| 109 |
- outPlain string |
|
| 110 |
- }{
|
|
| 111 |
- {
|
|
| 112 |
- // Older daemons (or registries) may not handle an empty string, |
|
| 113 |
- // which resulted in an "io.EOF" when unmarshaling or decoding. |
|
| 114 |
- // |
|
| 115 |
- // FIXME(thaJeztah): find exactly what code-paths are impacted by this. |
|
| 116 |
- doc: "empty", |
|
| 117 |
- input: AuthConfig{},
|
|
| 118 |
- outBase64: `e30=`, |
|
| 119 |
- outPlain: `{}`,
|
|
| 120 |
- }, |
|
| 121 |
- {
|
|
| 122 |
- doc: "test authConfig", |
|
| 123 |
- input: AuthConfig{
|
|
| 124 |
- Username: "testuser", |
|
| 125 |
- Password: "testpassword", |
|
| 126 |
- ServerAddress: "example.com", |
|
| 127 |
- }, |
|
| 128 |
- outBase64: `eyJ1c2VybmFtZSI6InRlc3R1c2VyIiwicGFzc3dvcmQiOiJ0ZXN0cGFzc3dvcmQiLCJzZXJ2ZXJhZGRyZXNzIjoiZXhhbXBsZS5jb20ifQ==`, |
|
| 129 |
- outPlain: `{"username":"testuser","password":"testpassword","serveraddress":"example.com"}`,
|
|
| 130 |
- }, |
|
| 131 |
- } |
|
| 132 |
- for _, tc := range tests {
|
|
| 133 |
- // Sanity check to make sure our fixtures are correct. |
|
| 134 |
- b64 := base64.URLEncoding.EncodeToString([]byte(tc.outPlain)) |
|
| 135 |
- assert.Check(t, is.Equal(b64, tc.outBase64)) |
|
| 136 |
- |
|
| 137 |
- t.Run(tc.doc, func(t *testing.T) {
|
|
| 138 |
- out, err := EncodeAuthConfig(tc.input) |
|
| 139 |
- assert.NilError(t, err) |
|
| 140 |
- assert.Equal(t, out, tc.outBase64) |
|
| 141 |
- |
|
| 142 |
- authJSON, err := base64.URLEncoding.DecodeString(out) |
|
| 143 |
- assert.NilError(t, err) |
|
| 144 |
- assert.Equal(t, string(authJSON), tc.outPlain) |
|
| 145 |
- }) |
|
| 146 |
- } |
|
| 147 |
-} |
|
| 148 |
- |
|
| 149 |
-func BenchmarkDecodeAuthConfig(b *testing.B) {
|
|
| 150 |
- cases := []struct {
|
|
| 151 |
- doc string |
|
| 152 |
- inputBase64 string |
|
| 153 |
- invalid bool |
|
| 154 |
- }{
|
|
| 155 |
- {
|
|
| 156 |
- doc: "empty", |
|
| 157 |
- inputBase64: ``, |
|
| 158 |
- }, |
|
| 159 |
- {
|
|
| 160 |
- doc: "empty JSON", |
|
| 161 |
- inputBase64: `e30=`, |
|
| 162 |
- }, |
|
| 163 |
- {
|
|
| 164 |
- doc: "valid", |
|
| 165 |
- inputBase64: base64.URLEncoding.EncodeToString([]byte(`{"username":"testuser","password":"testpassword","serveraddress":"example.com"}`)),
|
|
| 166 |
- }, |
|
| 167 |
- {
|
|
| 168 |
- doc: "invalid base64", |
|
| 169 |
- inputBase64: "not-base64", |
|
| 170 |
- invalid: true, |
|
| 171 |
- }, |
|
| 172 |
- {
|
|
| 173 |
- doc: "malformed JSON", |
|
| 174 |
- inputBase64: `ew==`, |
|
| 175 |
- invalid: true, |
|
| 176 |
- }, |
|
| 177 |
- } |
|
| 178 |
- |
|
| 179 |
- for _, tc := range cases {
|
|
| 180 |
- b.Run(tc.doc, func(b *testing.B) {
|
|
| 181 |
- b.ReportAllocs() |
|
| 182 |
- for i := 0; i < b.N; i++ {
|
|
| 183 |
- _, err := DecodeAuthConfig(tc.inputBase64) |
|
| 184 |
- if !tc.invalid && err != nil {
|
|
| 185 |
- b.Fatal(err) |
|
| 186 |
- } |
|
| 187 |
- } |
|
| 188 |
- }) |
|
| 189 |
- } |
|
| 190 |
-} |
| ... | ... |
@@ -11,6 +11,7 @@ import ( |
| 11 | 11 |
"github.com/containerd/log" |
| 12 | 12 |
"github.com/distribution/reference" |
| 13 | 13 |
gogotypes "github.com/gogo/protobuf/types" |
| 14 |
+ "github.com/moby/moby/api/pkg/authconfig" |
|
| 14 | 15 |
"github.com/moby/moby/api/types/container" |
| 15 | 16 |
"github.com/moby/moby/api/types/registry" |
| 16 | 17 |
"github.com/moby/moby/api/types/swarm" |
| ... | ... |
@@ -230,7 +231,7 @@ func (c *Cluster) CreateService(s swarm.ServiceSpec, encodedAuth string, queryRe |
| 230 | 230 |
authConfig := ®istry.AuthConfig{}
|
| 231 | 231 |
if encodedAuth != "" {
|
| 232 | 232 |
var err error |
| 233 |
- authConfig, err = registry.DecodeAuthConfig(encodedAuth) |
|
| 233 |
+ authConfig, err = authconfig.Decode(encodedAuth) |
|
| 234 | 234 |
if err != nil {
|
| 235 | 235 |
log.G(ctx).Warnf("invalid authconfig: %v", err)
|
| 236 | 236 |
} |
| ... | ... |
@@ -348,7 +349,7 @@ func (c *Cluster) UpdateService(serviceIDOrName string, version uint64, spec swa |
| 348 | 348 |
authConfig := ®istry.AuthConfig{}
|
| 349 | 349 |
if encodedAuth != "" {
|
| 350 | 350 |
var err error |
| 351 |
- authConfig, err = registry.DecodeAuthConfig(encodedAuth) |
|
| 351 |
+ authConfig, err = authconfig.Decode(encodedAuth) |
|
| 352 | 352 |
if err != nil {
|
| 353 | 353 |
log.G(ctx).Warnf("invalid authconfig: %v", err)
|
| 354 | 354 |
} |
| ... | ... |
@@ -9,6 +9,7 @@ import ( |
| 9 | 9 |
"github.com/docker/distribution" |
| 10 | 10 |
"github.com/docker/distribution/manifest/manifestlist" |
| 11 | 11 |
"github.com/docker/distribution/manifest/schema2" |
| 12 |
+ "github.com/moby/moby/api/pkg/authconfig" |
|
| 12 | 13 |
"github.com/moby/moby/api/types/registry" |
| 13 | 14 |
distributionpkg "github.com/moby/moby/v2/daemon/internal/distribution" |
| 14 | 15 |
"github.com/moby/moby/v2/daemon/server/httputils" |
| ... | ... |
@@ -42,7 +43,7 @@ func (dr *distributionRouter) getDistributionInfo(ctx context.Context, w http.Re |
| 42 | 42 |
|
| 43 | 43 |
// For a search it is not an error if no auth was given. Ignore invalid |
| 44 | 44 |
// AuthConfig to increase compatibility with the existing API. |
| 45 |
- authConfig, _ := registry.DecodeAuthConfig(r.Header.Get(registry.AuthHeader)) |
|
| 45 |
+ authConfig, _ := authconfig.Decode(r.Header.Get(registry.AuthHeader)) |
|
| 46 | 46 |
repos, err := dr.backend.GetRepositories(ctx, namedRef, authConfig) |
| 47 | 47 |
if err != nil {
|
| 48 | 48 |
return err |
| ... | ... |
@@ -12,6 +12,7 @@ import ( |
| 12 | 12 |
|
| 13 | 13 |
"github.com/containerd/platforms" |
| 14 | 14 |
"github.com/distribution/reference" |
| 15 |
+ "github.com/moby/moby/api/pkg/authconfig" |
|
| 15 | 16 |
"github.com/moby/moby/api/pkg/progress" |
| 16 | 17 |
"github.com/moby/moby/api/pkg/streamformatter" |
| 17 | 18 |
"github.com/moby/moby/api/types/filters" |
| ... | ... |
@@ -101,7 +102,7 @@ func (ir *imageRouter) postImagesCreate(ctx context.Context, w http.ResponseWrit |
| 101 | 101 |
// AuthConfig to increase compatibility with the existing API. |
| 102 | 102 |
// |
| 103 | 103 |
// TODO(thaJeztah): accept empty values but return an error when failing to decode. |
| 104 |
- authConfig, _ := registry.DecodeAuthConfig(r.Header.Get(registry.AuthHeader)) |
|
| 104 |
+ authConfig, _ := authconfig.Decode(r.Header.Get(registry.AuthHeader)) |
|
| 105 | 105 |
progressErr = ir.backend.PullImage(ctx, ref, platform, metaHeaders, authConfig, output) |
| 106 | 106 |
} else { // import
|
| 107 | 107 |
src := r.Form.Get("fromSrc")
|
| ... | ... |
@@ -170,7 +171,7 @@ func (ir *imageRouter) postImagesPush(ctx context.Context, w http.ResponseWriter |
| 170 | 170 |
// to increase compatibility with the existing API. |
| 171 | 171 |
// |
| 172 | 172 |
// TODO(thaJeztah): accept empty values but return an error when failing to decode. |
| 173 |
- authConfig, _ := registry.DecodeAuthConfig(r.Header.Get(registry.AuthHeader)) |
|
| 173 |
+ authConfig, _ := authconfig.Decode(r.Header.Get(registry.AuthHeader)) |
|
| 174 | 174 |
|
| 175 | 175 |
output := ioutils.NewWriteFlusher(w) |
| 176 | 176 |
defer output.Close() |
| ... | ... |
@@ -554,7 +555,7 @@ func (ir *imageRouter) getImagesSearch(ctx context.Context, w http.ResponseWrite |
| 554 | 554 |
|
| 555 | 555 |
// For a search it is not an error if no auth was given. Ignore invalid |
| 556 | 556 |
// AuthConfig to increase compatibility with the existing API. |
| 557 |
- authConfig, _ := registry.DecodeAuthConfig(r.Header.Get(registry.AuthHeader)) |
|
| 557 |
+ authConfig, _ := authconfig.Decode(r.Header.Get(registry.AuthHeader)) |
|
| 558 | 558 |
|
| 559 | 559 |
headers := http.Header{}
|
| 560 | 560 |
for k, v := range r.Header {
|
| ... | ... |
@@ -7,6 +7,7 @@ import ( |
| 7 | 7 |
"strings" |
| 8 | 8 |
|
| 9 | 9 |
"github.com/distribution/reference" |
| 10 |
+ "github.com/moby/moby/api/pkg/authconfig" |
|
| 10 | 11 |
"github.com/moby/moby/api/pkg/streamformatter" |
| 11 | 12 |
"github.com/moby/moby/api/types/filters" |
| 12 | 13 |
"github.com/moby/moby/api/types/plugin" |
| ... | ... |
@@ -26,7 +27,7 @@ func parseHeaders(headers http.Header) (map[string][]string, *registry.AuthConfi |
| 26 | 26 |
} |
| 27 | 27 |
|
| 28 | 28 |
// Ignore invalid AuthConfig to increase compatibility with the existing API. |
| 29 |
- authConfig, _ := registry.DecodeAuthConfig(headers.Get(registry.AuthHeader)) |
|
| 29 |
+ authConfig, _ := authconfig.Decode(headers.Get(registry.AuthHeader)) |
|
| 30 | 30 |
return metaHeaders, authConfig |
| 31 | 31 |
} |
| 32 | 32 |
|
| ... | ... |
@@ -8,6 +8,7 @@ import ( |
| 8 | 8 |
"time" |
| 9 | 9 |
|
| 10 | 10 |
"github.com/containerd/log" |
| 11 |
+ "github.com/moby/moby/api/pkg/authconfig" |
|
| 11 | 12 |
buildtypes "github.com/moby/moby/api/types/build" |
| 12 | 13 |
"github.com/moby/moby/api/types/events" |
| 13 | 14 |
"github.com/moby/moby/api/types/filters" |
| ... | ... |
@@ -373,9 +374,7 @@ func (s *systemRouter) getEvents(ctx context.Context, w http.ResponseWriter, r * |
| 373 | 373 |
} |
| 374 | 374 |
|
| 375 | 375 |
func (s *systemRouter) postAuth(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
|
| 376 |
- var config *registry.AuthConfig |
|
| 377 |
- err := json.NewDecoder(r.Body).Decode(&config) |
|
| 378 |
- r.Body.Close() |
|
| 376 |
+ config, err := authconfig.DecodeRequestBody(r.Body) |
|
| 379 | 377 |
if err != nil {
|
| 380 | 378 |
return err |
| 381 | 379 |
} |
| 382 | 380 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,92 @@ |
| 0 |
+package authconfig |
|
| 1 |
+ |
|
| 2 |
+import ( |
|
| 3 |
+ "bytes" |
|
| 4 |
+ "encoding/base64" |
|
| 5 |
+ "encoding/json" |
|
| 6 |
+ "errors" |
|
| 7 |
+ "fmt" |
|
| 8 |
+ "io" |
|
| 9 |
+ |
|
| 10 |
+ "github.com/moby/moby/api/types/registry" |
|
| 11 |
+) |
|
| 12 |
+ |
|
| 13 |
+// Encode serializes the auth configuration as a base64url encoded |
|
| 14 |
+// ([RFC4648, section 5]) JSON string for sending through the X-Registry-Auth header. |
|
| 15 |
+// |
|
| 16 |
+// [RFC4648, section 5]: https://tools.ietf.org/html/rfc4648#section-5 |
|
| 17 |
+func Encode(authConfig registry.AuthConfig) (string, error) {
|
|
| 18 |
+ // Older daemons (or registries) may not handle an empty string, |
|
| 19 |
+ // which resulted in an "io.EOF" when unmarshaling or decoding. |
|
| 20 |
+ // |
|
| 21 |
+ // FIXME(thaJeztah): find exactly what code-paths are impacted by this. |
|
| 22 |
+ // if authConfig == (AuthConfig{}) { return "", nil }
|
|
| 23 |
+ buf, err := json.Marshal(authConfig) |
|
| 24 |
+ if err != nil {
|
|
| 25 |
+ return "", errInvalidParameter{err}
|
|
| 26 |
+ } |
|
| 27 |
+ return base64.URLEncoding.EncodeToString(buf), nil |
|
| 28 |
+} |
|
| 29 |
+ |
|
| 30 |
+// Decode decodes base64url encoded ([RFC4648, section 5]) JSON |
|
| 31 |
+// authentication information as sent through the X-Registry-Auth header. |
|
| 32 |
+// |
|
| 33 |
+// This function always returns an [AuthConfig], even if an error occurs. It is up |
|
| 34 |
+// to the caller to decide if authentication is required, and if the error can |
|
| 35 |
+// be ignored. |
|
| 36 |
+// |
|
| 37 |
+// [RFC4648, section 5]: https://tools.ietf.org/html/rfc4648#section-5 |
|
| 38 |
+func Decode(authEncoded string) (*registry.AuthConfig, error) {
|
|
| 39 |
+ if authEncoded == "" {
|
|
| 40 |
+ return ®istry.AuthConfig{}, nil
|
|
| 41 |
+ } |
|
| 42 |
+ |
|
| 43 |
+ decoded, err := base64.URLEncoding.DecodeString(authEncoded) |
|
| 44 |
+ if err != nil {
|
|
| 45 |
+ var e base64.CorruptInputError |
|
| 46 |
+ if errors.As(err, &e) {
|
|
| 47 |
+ return ®istry.AuthConfig{}, invalid(errors.New("must be a valid base64url-encoded string"))
|
|
| 48 |
+ } |
|
| 49 |
+ return ®istry.AuthConfig{}, invalid(err)
|
|
| 50 |
+ } |
|
| 51 |
+ |
|
| 52 |
+ if bytes.Equal(decoded, []byte("{}")) {
|
|
| 53 |
+ return ®istry.AuthConfig{}, nil
|
|
| 54 |
+ } |
|
| 55 |
+ |
|
| 56 |
+ return decode(bytes.NewReader(decoded)) |
|
| 57 |
+} |
|
| 58 |
+ |
|
| 59 |
+// DecodeRequestBody decodes authentication information as sent as JSON in the |
|
| 60 |
+// body of a request. This function is to provide backward compatibility with old |
|
| 61 |
+// clients and API versions. Current clients and API versions expect authentication |
|
| 62 |
+// to be provided through the X-Registry-Auth header. |
|
| 63 |
+// |
|
| 64 |
+// Like [Decode], this function always returns an [AuthConfig], even if an |
|
| 65 |
+// error occurs. It is up to the caller to decide if authentication is required, |
|
| 66 |
+// and if the error can be ignored. |
|
| 67 |
+func DecodeRequestBody(r io.ReadCloser) (*registry.AuthConfig, error) {
|
|
| 68 |
+ return decode(r) |
|
| 69 |
+} |
|
| 70 |
+ |
|
| 71 |
+func decode(r io.Reader) (*registry.AuthConfig, error) {
|
|
| 72 |
+ authConfig := ®istry.AuthConfig{}
|
|
| 73 |
+ if err := json.NewDecoder(r).Decode(authConfig); err != nil {
|
|
| 74 |
+ // always return an (empty) AuthConfig to increase compatibility with |
|
| 75 |
+ // the existing API. |
|
| 76 |
+ return ®istry.AuthConfig{}, invalid(fmt.Errorf("invalid JSON: %w", err))
|
|
| 77 |
+ } |
|
| 78 |
+ return authConfig, nil |
|
| 79 |
+} |
|
| 80 |
+ |
|
| 81 |
+func invalid(err error) error {
|
|
| 82 |
+ return errInvalidParameter{fmt.Errorf("invalid X-Registry-Auth header: %w", err)}
|
|
| 83 |
+} |
|
| 84 |
+ |
|
| 85 |
+type errInvalidParameter struct{ error }
|
|
| 86 |
+ |
|
| 87 |
+func (errInvalidParameter) InvalidParameter() {}
|
|
| 88 |
+ |
|
| 89 |
+func (e errInvalidParameter) Cause() error { return e.error }
|
|
| 90 |
+ |
|
| 91 |
+func (e errInvalidParameter) Unwrap() error { return e.error }
|
| ... | ... |
@@ -1,14 +1,6 @@ |
| 1 | 1 |
package registry |
| 2 | 2 |
|
| 3 |
-import ( |
|
| 4 |
- "bytes" |
|
| 5 |
- "context" |
|
| 6 |
- "encoding/base64" |
|
| 7 |
- "encoding/json" |
|
| 8 |
- "errors" |
|
| 9 |
- "fmt" |
|
| 10 |
- "io" |
|
| 11 |
-) |
|
| 3 |
+import "context" |
|
| 12 | 4 |
|
| 13 | 5 |
// AuthHeader is the name of the header used to send encoded registry |
| 14 | 6 |
// authorization credentials for registry operations (push/pull). |
| ... | ... |
@@ -46,85 +38,3 @@ type AuthConfig struct {
|
| 46 | 46 |
// RegistryToken is a bearer token to be sent to a registry |
| 47 | 47 |
RegistryToken string `json:"registrytoken,omitempty"` |
| 48 | 48 |
} |
| 49 |
- |
|
| 50 |
-// EncodeAuthConfig serializes the auth configuration as a base64url encoded |
|
| 51 |
-// ([RFC4648, section 5]) JSON string for sending through the X-Registry-Auth header. |
|
| 52 |
-// |
|
| 53 |
-// [RFC4648, section 5]: https://tools.ietf.org/html/rfc4648#section-5 |
|
| 54 |
-func EncodeAuthConfig(authConfig AuthConfig) (string, error) {
|
|
| 55 |
- // Older daemons (or registries) may not handle an empty string, |
|
| 56 |
- // which resulted in an "io.EOF" when unmarshaling or decoding. |
|
| 57 |
- // |
|
| 58 |
- // FIXME(thaJeztah): find exactly what code-paths are impacted by this. |
|
| 59 |
- // if authConfig == (AuthConfig{}) { return "", nil }
|
|
| 60 |
- buf, err := json.Marshal(authConfig) |
|
| 61 |
- if err != nil {
|
|
| 62 |
- return "", errInvalidParameter{err}
|
|
| 63 |
- } |
|
| 64 |
- return base64.URLEncoding.EncodeToString(buf), nil |
|
| 65 |
-} |
|
| 66 |
- |
|
| 67 |
-// DecodeAuthConfig decodes base64url encoded ([RFC4648, section 5]) JSON |
|
| 68 |
-// authentication information as sent through the X-Registry-Auth header. |
|
| 69 |
-// |
|
| 70 |
-// This function always returns an [AuthConfig], even if an error occurs. It is up |
|
| 71 |
-// to the caller to decide if authentication is required, and if the error can |
|
| 72 |
-// be ignored. |
|
| 73 |
-// |
|
| 74 |
-// [RFC4648, section 5]: https://tools.ietf.org/html/rfc4648#section-5 |
|
| 75 |
-func DecodeAuthConfig(authEncoded string) (*AuthConfig, error) {
|
|
| 76 |
- if authEncoded == "" {
|
|
| 77 |
- return &AuthConfig{}, nil
|
|
| 78 |
- } |
|
| 79 |
- |
|
| 80 |
- decoded, err := base64.URLEncoding.DecodeString(authEncoded) |
|
| 81 |
- if err != nil {
|
|
| 82 |
- var e base64.CorruptInputError |
|
| 83 |
- if errors.As(err, &e) {
|
|
| 84 |
- return &AuthConfig{}, invalid(errors.New("must be a valid base64url-encoded string"))
|
|
| 85 |
- } |
|
| 86 |
- return &AuthConfig{}, invalid(err)
|
|
| 87 |
- } |
|
| 88 |
- |
|
| 89 |
- if bytes.Equal(decoded, []byte("{}")) {
|
|
| 90 |
- return &AuthConfig{}, nil
|
|
| 91 |
- } |
|
| 92 |
- |
|
| 93 |
- return decodeAuthConfigFromReader(bytes.NewReader(decoded)) |
|
| 94 |
-} |
|
| 95 |
- |
|
| 96 |
-// DecodeAuthConfigBody decodes authentication information as sent as JSON in the |
|
| 97 |
-// body of a request. This function is to provide backward compatibility with old |
|
| 98 |
-// clients and API versions. Current clients and API versions expect authentication |
|
| 99 |
-// to be provided through the X-Registry-Auth header. |
|
| 100 |
-// |
|
| 101 |
-// Like [DecodeAuthConfig], this function always returns an [AuthConfig], even if an |
|
| 102 |
-// error occurs. It is up to the caller to decide if authentication is required, |
|
| 103 |
-// and if the error can be ignored. |
|
| 104 |
-// |
|
| 105 |
-// Deprecated: this function is no longer used and will be removed in the next release. |
|
| 106 |
-func DecodeAuthConfigBody(rdr io.ReadCloser) (*AuthConfig, error) {
|
|
| 107 |
- return decodeAuthConfigFromReader(rdr) |
|
| 108 |
-} |
|
| 109 |
- |
|
| 110 |
-func decodeAuthConfigFromReader(rdr io.Reader) (*AuthConfig, error) {
|
|
| 111 |
- authConfig := &AuthConfig{}
|
|
| 112 |
- if err := json.NewDecoder(rdr).Decode(authConfig); err != nil {
|
|
| 113 |
- // always return an (empty) AuthConfig to increase compatibility with |
|
| 114 |
- // the existing API. |
|
| 115 |
- return &AuthConfig{}, invalid(fmt.Errorf("invalid JSON: %w", err))
|
|
| 116 |
- } |
|
| 117 |
- return authConfig, nil |
|
| 118 |
-} |
|
| 119 |
- |
|
| 120 |
-func invalid(err error) error {
|
|
| 121 |
- return errInvalidParameter{fmt.Errorf("invalid X-Registry-Auth header: %w", err)}
|
|
| 122 |
-} |
|
| 123 |
- |
|
| 124 |
-type errInvalidParameter struct{ error }
|
|
| 125 |
- |
|
| 126 |
-func (errInvalidParameter) InvalidParameter() {}
|
|
| 127 |
- |
|
| 128 |
-func (e errInvalidParameter) Cause() error { return e.error }
|
|
| 129 |
- |
|
| 130 |
-func (e errInvalidParameter) Unwrap() error { return e.error }
|
| ... | ... |
@@ -944,6 +944,7 @@ github.com/moby/locker |
| 944 | 944 |
# github.com/moby/moby/api v1.52.0-alpha.1 => ./api |
| 945 | 945 |
## explicit; go 1.23.0 |
| 946 | 946 |
github.com/moby/moby/api |
| 947 |
+github.com/moby/moby/api/pkg/authconfig |
|
| 947 | 948 |
github.com/moby/moby/api/pkg/progress |
| 948 | 949 |
github.com/moby/moby/api/pkg/stdcopy |
| 949 | 950 |
github.com/moby/moby/api/pkg/streamformatter |