[prev in list] [next in list] [prev in thread] [next in thread]
List: mesos-commits
Subject: [mesos] 05/06: Added workaround for Docker repositories not providing scope/service.
From: asekretenko () apache ! org
Date: 2020-02-26 17:52:21
Message-ID: 20200226175216.558F48DACA () gitbox ! apache ! org
[Download RAW message or body]
This is an automated email from the ASF dual-hosted git repository.
asekretenko pushed a commit to branch 1.7.x
in repository https://gitbox.apache.org/repos/asf/mesos.git
commit e07405e398e9aa932395709a2b4b6b2ab5808eb9
Author: Andrei Sekretenko <asekretenko@mesosphere.com>
AuthorDate: Thu Jan 30 14:26:38 2020 +0100
Added workaround for Docker repositories not providing scope/service.
This patch adds a fallback Docker authorization server URI generation
mechanism (see MESOS-10092) for repository servers that provide no
"scope"/"service" params in the "WWW-Authenticate" header of the initial
"401 Unathorized" response.
Review: https://reviews.apache.org/r/72079
---
src/uri/fetchers/docker.cpp | 163 ++++++++++++++++++++++++++++++++------------
src/uri/fetchers/docker.hpp | 9 ++-
2 files changed, 128 insertions(+), 44 deletions(-)
diff --git a/src/uri/fetchers/docker.cpp b/src/uri/fetchers/docker.cpp
index 9a64021..fbacabf 100644
--- a/src/uri/fetchers/docker.cpp
+++ b/src/uri/fetchers/docker.cpp
@@ -450,6 +450,12 @@ static URI constructRegistryUri(const URI& imageUri, string&& path)
}
+static URI getRegistryRootUri(const URI& imageUri)
+{
+ return constructRegistryUri(imageUri, "/v2");
+}
+
+
static URI getManifestUri(const URI& imageUri)
{
return constructRegistryUri(
@@ -467,56 +473,40 @@ static URI getBlobUri(const URI& imageUri)
}
-// Obtains URI of the V2 authorization service based on the
-// initial "401 unauthorized" response of the Docker V2 registry.
-static Future<string> getAuthServiceUri(const http::Response& initialResponse)
+// Validates that the response contains WWW-Authenticate header with
+// a scheme BEARER and, if so, extracts parameters from the header.
+// Otherwise, returns Error.
+static Try<hashmap<string, string>> getBearerAuthParam(
+ const URI& uri,
+ const http::Response& response)
{
Result<http::header::WWWAuthenticate> header =
- initialResponse.headers.get<http::header::WWWAuthenticate>();
+ response.headers.get<http::header::WWWAuthenticate>();
if (header.isError()) {
- return Failure("Failed to get WWW-Authenticate header: " + header.error());
+ return Error(
+ "Failed to get WWW-Authenticate header from " + stringify(uri) +
+ ": " + header.error());
} else if (header.isNone()) {
- return Failure("Unexpected empty WWW-Authenticate header");
+ return Error(
+ "Got unexpected empty WWW-Authenticate header from " + stringify(uri));
}
// According to RFC, auth scheme should be case insensitive.
const string authScheme = strings::upper(header->authScheme());
-
- if (authScheme == "BASIC") {
- return Failure(
- "Unexpected BASIC Authorization response status: " +
- initialResponse.status);
+ if (authScheme == "BASIC"){
+ return Error(
+ "Got unexpected BASIC Authorization response status: " +
+ response.status + " from " + stringify(uri));
}
if (authScheme != "BEARER") {
- return Failure("Unsupported auth-scheme: " + authScheme);
+ return Error(
+ "Got unsupported auth-scheme: " + authScheme +
+ " from " + stringify(uri));
}
- hashmap<string, string> authParam = header->authParam();
-
- // `authParam` is supposed to contain the 'realm', 'service'
- // and 'scope' information for bearer authentication.
- //
- // TODO(asekretenko): Fall back to querying repository root and
- // constructing scope if one of these is missing (see MESOS-10092).
- if (!authParam.contains("realm")) {
- return Failure("Missing 'realm' in WWW-Authenticate header");
- }
-
- if (!authParam.contains("service")) {
- return Failure("Missing 'service' in WWW-Authenticate header");
- }
-
- if (!authParam.contains("scope")) {
- return Failure("Missing 'scope' in WWW-Authenticate header");
- }
-
- // TODO(jieyu): Currently, we don't expect the auth server to return
- // a service or a scope that needs encoding.
- return authParam.at("realm") + "?" +
- "service=" + authParam.at("service") + "&" +
- "scope=" + authParam.at("scope");
+ return header->authParam();
}
@@ -529,10 +519,12 @@ class DockerFetcherPluginProcess : public Process<DockerFetcherPluginProcess>
public:
DockerFetcherPluginProcess(
const hashmap<string, spec::Config::Auth>& _auths,
- const Option<Duration>& _stallTimeout)
+ const Option<Duration>& _stallTimeout,
+ bool _enableAuthServiceUriFallback)
: ProcessBase(process::ID::generate("docker-fetcher-plugin")),
auths(_auths),
- stallTimeout(_stallTimeout) {}
+ stallTimeout(_stallTimeout),
+ enableAuthServiceUriFallback(_enableAuthServiceUriFallback) {}
Future<Nothing> fetch(
const URI& uri,
@@ -585,9 +577,16 @@ private:
vector<string> urls);
#endif
+ Future<string> getAuthServiceUri(
+ const string& repository,
+ const URI& initialUri,
+ const http::Response& initialResponse,
+ const http::Headers& basicAuthHeaders) const;
+
// Returns a token-based authorization header. Basic authorization
// header may be required to get a proper authorization token.
Future<http::Headers> getAuthHeader(
+ const string& repository,
const URI& uri,
const http::Headers& basicAuthHeaders,
const http::Response& response);
@@ -599,6 +598,10 @@ private:
// Timeout for curl to wait when a net download stalls.
const Option<Duration> stallTimeout;
+
+ // Disables auth server URI generation (see MESOS-10092).
+ // Used for tests only.
+ const bool enableAuthServiceUriFallback;
};
@@ -619,7 +622,9 @@ DockerFetcherPlugin::Flags::Flags()
const char DockerFetcherPlugin::NAME[] = "docker";
-Try<Owned<Fetcher::Plugin>> DockerFetcherPlugin::create(const Flags& flags)
+Try<Owned<Fetcher::Plugin>> DockerFetcherPlugin::create(
+ const Flags& flags,
+ bool enableAuthServiceUriFallback)
{
// TODO(jieyu): Make sure curl is available.
@@ -637,7 +642,8 @@ Try<Owned<Fetcher::Plugin>> DockerFetcherPlugin::create(const Flags& flags)
Owned<DockerFetcherPluginProcess> process(new DockerFetcherPluginProcess(
hashmap<string, spec::Config::Auth>(auths),
- flags.docker_stall_timeout));
+ flags.docker_stall_timeout,
+ enableAuthServiceUriFallback));
return Owned<Fetcher::Plugin>(new DockerFetcherPlugin(process));
}
@@ -801,7 +807,7 @@ Future<Nothing> DockerFetcherPluginProcess::_fetch(
{
if (response.code == http::Status::UNAUTHORIZED) {
// Use the 'Basic' credential to request an auth token by default.
- return getAuthHeader(manifestUri, basicAuthHeaders, response)
+ return getAuthHeader(uri.path(), manifestUri, basicAuthHeaders, response)
.then(defer(self(), [=](
const http::Headers& authHeaders) -> Future<Nothing> {
return curl(manifestUri, manifestHeaders + authHeaders, stallTimeout)
@@ -1020,7 +1026,7 @@ Future<Nothing> DockerFetcherPluginProcess::_fetchBlob(
"but get '" + response.status + "' instead");
}
- return getAuthHeader(blobUri, basicAuthHeaders, response)
+ return getAuthHeader(uri.path(), blobUri, basicAuthHeaders, response)
.then(defer(self(), [=](
const http::Headers& authHeaders) -> Future<Nothing> {
return download(
@@ -1120,6 +1126,76 @@ Future<Nothing> DockerFetcherPluginProcess::_urlFetchBlob(
}
#endif
+// Tries to obtain URI of the V2 authorization service based on the
+// "realm", "scope" and "service" auth params from the
+// initial "401 unauthorized" response of the Docker V2 registry
+// (see details in https://docs.docker.com/registry/spec/auth/token).
+//
+// If any of the params are missing from the "WWW-Authenticate" header, this
+// function falls back to the scheme implemented in Docker image puller (see
+// MESOS-10092): it queries the registry root URI (as opposed to manifest/blob
+// URI) to get the "WWW-Authenticate" header with "realm", and composes the
+// repository scope on its own. Scope grammar and semantics are documented in
+// https://docs.docker.com/registry/spec/auth/scope .
+Future<string> DockerFetcherPluginProcess::getAuthServiceUri(
+ const string& repository,
+ const URI& initialUri,
+ const http::Response& initialResponse,
+ const http::Headers& basicAuthHeaders) const
+{
+ const Try<hashmap<string, string>> authParam =
+ getBearerAuthParam(initialUri, initialResponse);
+
+ if (authParam.isError()) {
+ LOG(WARNING) << authParam.error();
+ return Failure(authParam.error());
+ }
+
+ // `authParam` is supposed to contain the 'realm', 'service'
+ // and 'scope' information for bearer authentication.
+ if (authParam->contains("realm") &&
+ authParam->contains("service") &&
+ authParam->contains("scope")) {
+ // TODO(jieyu): Currently, we don't expect the auth server to return
+ // a service or a scope that needs encoding.
+ return authParam->at("realm") + "?" +
+ "service=" + authParam->at("service") + "&" +
+ "scope=" + authParam->at("scope");
+ }
+
+ const string msg =
+ "Missing 'realm', 'service' or 'scope' in header WWW-Authenticate: " +
+ initialResponse.headers.at("WWW-Authenticate");
+
+ if (!enableAuthServiceUriFallback) {
+ return Failure(msg);
+ }
+
+ LOG(WARNING) << msg;
+
+ const URI registryRootUri = getRegistryRootUri(initialUri);
+ return curl(registryRootUri, basicAuthHeaders, stallTimeout)
+ .then([repository, registryRootUri](const http::Response& rootResponse)
+ -> Future<string> {
+ const Try<hashmap<string, string>> authParam =
+ getBearerAuthParam(registryRootUri, rootResponse);
+
+ if (authParam.isError()) {
+ LOG(WARNING) << authParam.error();
+ return Failure(authParam.error());
+ }
+
+ if (!authParam->contains("realm")) {
+ return Failure(
+ "Missing 'realm' in WWW-Authenticate header obtained from " +
+ stringify(registryRootUri));
+ }
+
+ return authParam->at("realm") + "?scope=repository:" + repository +
+ ":pull";
+ });
+}
+
// If a '401 Unauthorized' response is received and the auth-scheme
// is 'Bearer', we expect a header 'Www-Authenticate' containing the
@@ -1131,13 +1207,14 @@ Future<Nothing> DockerFetcherPluginProcess::_urlFetchBlob(
// See details here:
// https://docs.docker.com/registry/spec/auth/token/
Future<http::Headers> DockerFetcherPluginProcess::getAuthHeader(
+ const string& repository,
const URI& uri,
const http::Headers& basicAuthHeaders,
const http::Response& response)
{
const auto stallTimeout = this->stallTimeout;
- return getAuthServiceUri(response)
+ return getAuthServiceUri(repository, uri, response, basicAuthHeaders)
.then([basicAuthHeaders, stallTimeout](const string& authServiceUri) {
return curl(authServiceUri, basicAuthHeaders, stallTimeout)
.then([authServiceUri](const http::Response& response)
diff --git a/src/uri/fetchers/docker.hpp b/src/uri/fetchers/docker.hpp
index 2abe735..03a91af 100644
--- a/src/uri/fetchers/docker.hpp
+++ b/src/uri/fetchers/docker.hpp
@@ -45,7 +45,14 @@ public:
static const char NAME[];
- static Try<process::Owned<Fetcher::Plugin>> create(const Flags& flags);
+ // `enableAuthServiceUriFallback` switches on the fallback auth service URI
+ // generation scheme for V2 registries that do not provide 'service'/'scope'
+ // parameters in the initial 'WWW-Authenticate' header.
+ //
+ // NOTE: switching the fallback off is required for testing purposes.
+ static Try<process::Owned<Fetcher::Plugin>> create(
+ const Flags& flags,
+ bool enableAuthServiceUriFallback = true);
static std::string getBlobPath(
const std::string& directory,
[prev in list] [next in list] [prev in thread] [next in thread]
Configure |
About |
News |
Add a list |
Sponsored by KoreLogic