github.com/hanks177/podman/v4@v4.1.3-0.20220613032544-16d90015bc83/pkg/auth/auth.go (about) 1 package auth 2 3 import ( 4 "encoding/base64" 5 "encoding/json" 6 "io/ioutil" 7 "net/http" 8 "os" 9 "strings" 10 11 imageAuth "github.com/containers/image/v5/pkg/docker/config" 12 "github.com/containers/image/v5/types" 13 dockerAPITypes "github.com/docker/docker/api/types" 14 "github.com/pkg/errors" 15 "github.com/sirupsen/logrus" 16 ) 17 18 // xRegistryAuthHeader is the key to the encoded registry authentication configuration in an http-request header. 19 // This header supports one registry per header occurrence. To support N registries provide N headers, one per registry. 20 // As of Docker API 1.40 and Libpod API 1.0.0, this header is supported by all endpoints. 21 const xRegistryAuthHeader = "X-Registry-Auth" 22 23 // xRegistryConfigHeader is the key to the encoded registry authentication configuration in an http-request header. 24 // This header supports N registries in one header via a Base64 encoded, JSON map. 25 // As of Docker API 1.40 and Libpod API 2.0.0, this header is supported by build endpoints. 26 const xRegistryConfigHeader = "X-Registry-Config" 27 28 // GetCredentials queries the http.Request for X-Registry-.* headers and extracts 29 // the necessary authentication information for libpod operations, possibly 30 // creating a config file. If that is the case, the caller must call RemoveAuthFile. 31 func GetCredentials(r *http.Request) (*types.DockerAuthConfig, string, error) { 32 nonemptyHeaderValue := func(key string) ([]string, bool) { 33 hdr := r.Header.Values(key) 34 return hdr, len(hdr) > 0 35 } 36 var override *types.DockerAuthConfig 37 var fileContents map[string]types.DockerAuthConfig 38 var headerName string 39 var err error 40 if hdr, ok := nonemptyHeaderValue(xRegistryConfigHeader); ok { 41 headerName = xRegistryConfigHeader 42 override, fileContents, err = getConfigCredentials(r, hdr) 43 } else if hdr, ok := nonemptyHeaderValue(xRegistryAuthHeader); ok { 44 headerName = xRegistryAuthHeader 45 override, fileContents, err = getAuthCredentials(hdr) 46 } else { 47 return nil, "", nil 48 } 49 if err != nil { 50 return nil, "", errors.Wrapf(err, "failed to parse %q header for %s", headerName, r.URL.String()) 51 } 52 53 var authFile string 54 if fileContents == nil { 55 authFile = "" 56 } else { 57 authFile, err = authConfigsToAuthFile(fileContents) 58 if err != nil { 59 return nil, "", errors.Wrapf(err, "failed to parse %q header for %s", headerName, r.URL.String()) 60 } 61 } 62 return override, authFile, nil 63 } 64 65 // getConfigCredentials extracts one or more docker.AuthConfig from a request and its 66 // xRegistryConfigHeader value. An empty key will be used as default while a named registry will be 67 // returned as types.DockerAuthConfig 68 func getConfigCredentials(r *http.Request, headers []string) (*types.DockerAuthConfig, map[string]types.DockerAuthConfig, error) { 69 var auth *types.DockerAuthConfig 70 configs := make(map[string]types.DockerAuthConfig) 71 72 for _, h := range headers { 73 param, err := base64.URLEncoding.DecodeString(h) 74 if err != nil { 75 return nil, nil, errors.Wrapf(err, "failed to decode %q", xRegistryConfigHeader) 76 } 77 78 ac := make(map[string]dockerAPITypes.AuthConfig) 79 err = json.Unmarshal(param, &ac) 80 if err != nil { 81 return nil, nil, errors.Wrapf(err, "failed to unmarshal %q", xRegistryConfigHeader) 82 } 83 84 for k, v := range ac { 85 configs[k] = dockerAuthToImageAuth(v) 86 } 87 } 88 89 // Empty key implies no registry given in API 90 if c, found := configs[""]; found { 91 auth = &c 92 } 93 94 // Override any default given above if specialized credentials provided 95 if registries, found := r.URL.Query()["registry"]; found { 96 for _, r := range registries { 97 for k, v := range configs { 98 if strings.Contains(k, r) { 99 v := v 100 auth = &v 101 break 102 } 103 } 104 if auth != nil { 105 break 106 } 107 } 108 109 if auth == nil { 110 logrus.Debugf("%q header found in request, but \"registry=%v\" query parameter not provided", 111 xRegistryConfigHeader, registries) 112 } else { 113 logrus.Debugf("%q header found in request for username %q", xRegistryConfigHeader, auth.Username) 114 } 115 } 116 117 return auth, configs, nil 118 } 119 120 // getAuthCredentials extracts one or more DockerAuthConfigs from an xRegistryAuthHeader 121 // value. The header could specify a single-auth config in which case the 122 // first return value is set. In case of a multi-auth header, the contents are 123 // returned in the second return value. 124 func getAuthCredentials(headers []string) (*types.DockerAuthConfig, map[string]types.DockerAuthConfig, error) { 125 authHeader := headers[0] 126 127 // First look for a multi-auth header (i.e., a map). 128 authConfigs, err := parseMultiAuthHeader(authHeader) 129 if err == nil { 130 return nil, authConfigs, nil 131 } 132 133 // Fallback to looking for a single-auth header (i.e., one config). 134 authConfig, err := parseSingleAuthHeader(authHeader) 135 if err != nil { 136 return nil, nil, err 137 } 138 return &authConfig, nil, nil 139 } 140 141 // MakeXRegistryConfigHeader returns a map with the "X-Registry-Config" header set, which can 142 // conveniently be used in the http stack. 143 func MakeXRegistryConfigHeader(sys *types.SystemContext, username, password string) (http.Header, error) { 144 if sys == nil { 145 sys = &types.SystemContext{} 146 } 147 authConfigs, err := imageAuth.GetAllCredentials(sys) 148 if err != nil { 149 return nil, err 150 } 151 152 if username != "" { 153 authConfigs[""] = types.DockerAuthConfig{ 154 Username: username, 155 Password: password, 156 } 157 } 158 159 if len(authConfigs) == 0 { 160 return nil, nil 161 } 162 content, err := encodeMultiAuthConfigs(authConfigs) 163 if err != nil { 164 return nil, err 165 } 166 return http.Header{xRegistryConfigHeader: []string{content}}, nil 167 } 168 169 // MakeXRegistryAuthHeader returns a map with the "X-Registry-Auth" header set, which can 170 // conveniently be used in the http stack. 171 func MakeXRegistryAuthHeader(sys *types.SystemContext, username, password string) (http.Header, error) { 172 if username != "" { 173 content, err := encodeSingleAuthConfig(types.DockerAuthConfig{Username: username, Password: password}) 174 if err != nil { 175 return nil, err 176 } 177 return http.Header{xRegistryAuthHeader: []string{content}}, nil 178 } 179 180 if sys == nil { 181 sys = &types.SystemContext{} 182 } 183 authConfigs, err := imageAuth.GetAllCredentials(sys) 184 if err != nil { 185 return nil, err 186 } 187 content, err := encodeMultiAuthConfigs(authConfigs) 188 if err != nil { 189 return nil, err 190 } 191 return http.Header{xRegistryAuthHeader: []string{content}}, nil 192 } 193 194 // RemoveAuthfile is a convenience function that is meant to be called in a 195 // deferred statement. If non-empty, it removes the specified authfile and log 196 // errors. It's meant to reduce boilerplate code at call sites of 197 // `GetCredentials`. 198 func RemoveAuthfile(authfile string) { 199 if authfile == "" { 200 return 201 } 202 if err := os.Remove(authfile); err != nil { 203 logrus.Errorf("Removing temporary auth file %q: %v", authfile, err) 204 } 205 } 206 207 // encodeSingleAuthConfig serializes the auth configuration as a base64 encoded JSON payload. 208 func encodeSingleAuthConfig(authConfig types.DockerAuthConfig) (string, error) { 209 conf := imageAuthToDockerAuth(authConfig) 210 buf, err := json.Marshal(conf) 211 if err != nil { 212 return "", err 213 } 214 return base64.URLEncoding.EncodeToString(buf), nil 215 } 216 217 // encodeMultiAuthConfigs serializes the auth configurations as a base64 encoded JSON payload. 218 func encodeMultiAuthConfigs(authConfigs map[string]types.DockerAuthConfig) (string, error) { 219 confs := make(map[string]dockerAPITypes.AuthConfig) 220 for registry, authConf := range authConfigs { 221 confs[registry] = imageAuthToDockerAuth(authConf) 222 } 223 buf, err := json.Marshal(confs) 224 if err != nil { 225 return "", err 226 } 227 return base64.URLEncoding.EncodeToString(buf), nil 228 } 229 230 // authConfigsToAuthFile stores the specified auth configs in a temporary files 231 // and returns its path. The file can later be used an auth file for contacting 232 // one or more container registries. If tmpDir is empty, the system's default 233 // TMPDIR will be used. 234 func authConfigsToAuthFile(authConfigs map[string]types.DockerAuthConfig) (string, error) { 235 // Initialize an empty temporary JSON file. 236 tmpFile, err := ioutil.TempFile("", "auth.json.") 237 if err != nil { 238 return "", err 239 } 240 if _, err := tmpFile.Write([]byte{'{', '}'}); err != nil { 241 return "", errors.Wrap(err, "error initializing temporary auth file") 242 } 243 if err := tmpFile.Close(); err != nil { 244 return "", errors.Wrap(err, "error closing temporary auth file") 245 } 246 authFilePath := tmpFile.Name() 247 248 // Now use the c/image packages to store the credentials. It's battle 249 // tested, and we make sure to use the same code as the image backend. 250 sys := types.SystemContext{AuthFilePath: authFilePath} 251 for authFileKey, config := range authConfigs { 252 key := normalizeAuthFileKey(authFileKey) 253 254 // Note that we do not validate the credentials here. We assume 255 // that all credentials are valid. They'll be used on demand 256 // later. 257 if err := imageAuth.SetAuthentication(&sys, key, config.Username, config.Password); err != nil { 258 return "", errors.Wrapf(err, "error storing credentials in temporary auth file (key: %q / %q, user: %q)", authFileKey, key, config.Username) 259 } 260 } 261 262 return authFilePath, nil 263 } 264 265 // normalizeAuthFileKey takes an auth file key and converts it into a new-style credential key 266 // in the canonical format, as interpreted by c/image/pkg/docker/config. 267 func normalizeAuthFileKey(authFileKey string) string { 268 stripped := strings.TrimPrefix(authFileKey, "http://") 269 stripped = strings.TrimPrefix(stripped, "https://") 270 271 if stripped != authFileKey { // URLs are interpreted to mean complete registries 272 stripped = strings.SplitN(stripped, "/", 2)[0] 273 } 274 275 // Only non-namespaced registry names (or URLs) need to be normalized; repo namespaces 276 // always use the simple format. 277 switch stripped { 278 case "registry-1.docker.io", "index.docker.io": 279 return "docker.io" 280 default: 281 return stripped 282 } 283 } 284 285 // dockerAuthToImageAuth converts a docker auth config to one we're using 286 // internally from c/image. Note that the Docker types look slightly 287 // different, so we need to convert to be extra sure we're not running into 288 // undesired side-effects when unmarhalling directly to our types. 289 func dockerAuthToImageAuth(authConfig dockerAPITypes.AuthConfig) types.DockerAuthConfig { 290 return types.DockerAuthConfig{ 291 Username: authConfig.Username, 292 Password: authConfig.Password, 293 IdentityToken: authConfig.IdentityToken, 294 } 295 } 296 297 // reverse conversion of `dockerAuthToImageAuth`. 298 func imageAuthToDockerAuth(authConfig types.DockerAuthConfig) dockerAPITypes.AuthConfig { 299 return dockerAPITypes.AuthConfig{ 300 Username: authConfig.Username, 301 Password: authConfig.Password, 302 IdentityToken: authConfig.IdentityToken, 303 } 304 } 305 306 // parseSingleAuthHeader extracts a DockerAuthConfig from an xRegistryAuthHeader value. 307 // The header content is a single DockerAuthConfig. 308 func parseSingleAuthHeader(authHeader string) (types.DockerAuthConfig, error) { 309 // Accept "null" and handle it as empty value for compatibility reason with Docker. 310 // Some java docker clients pass this value, e.g. this one used in Eclipse. 311 if len(authHeader) == 0 || authHeader == "null" { 312 return types.DockerAuthConfig{}, nil 313 } 314 315 authConfig := dockerAPITypes.AuthConfig{} 316 authJSON := base64.NewDecoder(base64.URLEncoding, strings.NewReader(authHeader)) 317 if err := json.NewDecoder(authJSON).Decode(&authConfig); err != nil { 318 return types.DockerAuthConfig{}, err 319 } 320 return dockerAuthToImageAuth(authConfig), nil 321 } 322 323 // parseMultiAuthHeader extracts a DockerAuthConfig from an xRegistryAuthHeader value. 324 // The header content is a map[string]DockerAuthConfigs. 325 func parseMultiAuthHeader(authHeader string) (map[string]types.DockerAuthConfig, error) { 326 // Accept "null" and handle it as empty value for compatibility reason with Docker. 327 // Some java docker clients pass this value, e.g. this one used in Eclipse. 328 if len(authHeader) == 0 || authHeader == "null" { 329 return nil, nil 330 } 331 332 dockerAuthConfigs := make(map[string]dockerAPITypes.AuthConfig) 333 authJSON := base64.NewDecoder(base64.URLEncoding, strings.NewReader(authHeader)) 334 if err := json.NewDecoder(authJSON).Decode(&dockerAuthConfigs); err != nil { 335 return nil, err 336 } 337 338 // Now convert to the internal types. 339 authConfigs := make(map[string]types.DockerAuthConfig) 340 for server := range dockerAuthConfigs { 341 authConfigs[server] = dockerAuthToImageAuth(dockerAuthConfigs[server]) 342 } 343 return authConfigs, nil 344 }