This is an automated email from the ASF dual-hosted git repository.

guangning pushed a commit to branch branch-2.8
in repository https://gitbox.apache.org/repos/asf/pulsar.git


The following commit(s) were added to refs/heads/branch-2.8 by this push:
     new 8ef70a8  [C++] Support configuring optional scope field for OAuth2 
authentication (#12305)
8ef70a8 is described below

commit 8ef70a8f009e133830ff6951a97d32941cb049c8
Author: Yunze Xu <[email protected]>
AuthorDate: Sat Oct 9 08:14:40 2021 +0800

    [C++] Support configuring optional scope field for OAuth2 authentication 
(#12305)
    
    It's a C++ client catchup for https://github.com/apache/pulsar/pull/11931.
    
    - Add a `scope_` field to `ClientCredentialFlow` and load it from 
`ParamMap` object whose key is `scope`.
    - Refactor `ClientCredentialFlow` to simplify code and make it testable:
      - Use only one constructor instead of two overloaded constructors that 
might look confused
      - Add a `generateJsonBody` public method for generating JSON body for 
post fields in `authenticate` so that it can be tested.
      - Add a `KeyFile` class like what Java client does to load client id and 
client secret from `ParamMap` or file.
    
    - [x] Make sure that the change passes the CI checks.
    
    This change added test `AuthPluginTest.testOauth2RequestBody` for the cases 
that scope exists or doesn't exist.
    
    (cherry picked from commit 44dcc04d037511984ec383fb3f2c75170e4cfefc)
---
 pulsar-client-cpp/lib/auth/AuthOauth2.cc  | 124 ++++++++++++++----------------
 pulsar-client-cpp/lib/auth/AuthOauth2.h   |  35 +++++++--
 pulsar-client-cpp/tests/AuthPluginTest.cc |  32 ++++++++
 3 files changed, 118 insertions(+), 73 deletions(-)

diff --git a/pulsar-client-cpp/lib/auth/AuthOauth2.cc 
b/pulsar-client-cpp/lib/auth/AuthOauth2.cc
index 33ae450..c7cc2bf 100644
--- a/pulsar-client-cpp/lib/auth/AuthOauth2.cc
+++ b/pulsar-client-cpp/lib/auth/AuthOauth2.cc
@@ -119,57 +119,50 @@ bool Oauth2CachedToken::isExpired() { return expiresAt_ < 
currentTimeMillis(); }
 Oauth2Flow::Oauth2Flow() {}
 Oauth2Flow::~Oauth2Flow() {}
 
-// ClientCredentialFlow
-static std::string readFromFile(const std::string& credentialsFilePath) {
-    std::ifstream input(credentialsFilePath);
-    std::stringstream buffer;
-    buffer << input.rdbuf();
-    return buffer.str();
-}
-
-ClientCredentialFlow::ClientCredentialFlow(const std::string& issuerUrl, const 
std::string& clientId,
-                                           const std::string& clientSecret, 
const std::string& audience) {
-    issuerUrl_ = issuerUrl;
-    clientId_ = clientId;
-    clientSecret_ = clientSecret;
-    audience_ = audience;
-    this->initialize();
+KeyFile KeyFile::fromParamMap(ParamMap& params) {
+    const auto it = params.find("private_key");
+    if (it != params.cend()) {
+        return fromFile(it->second);
+    } else {
+        return {params["client_id"], params["client_secret"]};
+    }
 }
 
 // read clientId/clientSecret from passed in `credentialsFilePath`
-ClientCredentialFlow::ClientCredentialFlow(const std::string& issuerUrl,
-                                           const std::string& 
credentialsFilePath,
-                                           const std::string& audience) {
-    issuerUrl_ = issuerUrl;
-    audience_ = audience;
-
+KeyFile KeyFile::fromFile(const std::string& credentialsFilePath) {
     boost::property_tree::ptree loadPtreeRoot;
     try {
         boost::property_tree::read_json(credentialsFilePath, loadPtreeRoot);
-    } catch (boost::property_tree::json_parser_error& e) {
-        LOG_ERROR("Failed to parse json input file for credentialsFilePath: " 
<< credentialsFilePath
-                                                                              
<< "with error:" << e.what());
-        return;
+    } catch (const boost::property_tree::json_parser_error& e) {
+        LOG_ERROR("Failed to parse json input file for credentialsFilePath: " 
<< credentialsFilePath << ": "
+                                                                              
<< e.what());
+        return {};
     }
 
-    const std::string defaultNotFoundString = "Client Id / Secret Not Found";
-
-    clientId_ = loadPtreeRoot.get<std::string>("client_id", 
defaultNotFoundString);
-    clientSecret_ = loadPtreeRoot.get<std::string>("client_secret", 
defaultNotFoundString);
-
-    if (clientId_ == defaultNotFoundString || clientSecret_ == 
defaultNotFoundString) {
-        LOG_ERROR("Not get valid clientId / clientSecret: " << clientId_ << 
"/" << clientSecret_);
-        return;
+    try {
+        return {loadPtreeRoot.get<std::string>("client_id"), 
loadPtreeRoot.get<std::string>("client_secret")};
+    } catch (const boost::property_tree::ptree_error& e) {
+        LOG_ERROR("Failed to get client_id or client_secret in " << 
credentialsFilePath << ": " << e.what());
+        return {};
     }
-    this->initialize();
 }
 
+ClientCredentialFlow::ClientCredentialFlow(ParamMap& params)
+    : issuerUrl_(params["issuer_url"]),
+      keyFile_(KeyFile::fromParamMap(params)),
+      audience_(params["audience"]),
+      scope_(params["scope"]) {}
+
 static size_t curlWriteCallback(void* contents, size_t size, size_t nmemb, 
void* responseDataPtr) {
     ((std::string*)responseDataPtr)->append((char*)contents, size * nmemb);
     return size * nmemb;
 }
 
 void ClientCredentialFlow::initialize() {
+    if (!keyFile_.isValid()) {
+        return;
+    }
+
     CURL* handle = curl_easy_init();
     CURLcode res;
     std::string responseData;
@@ -181,8 +174,7 @@ void ClientCredentialFlow::initialize() {
     curl_easy_setopt(handle, CURLOPT_CUSTOMREQUEST, "GET");
 
     // set URL: well-know endpoint
-    issuerUrl_.append("/.well-known/openid-configuration");
-    curl_easy_setopt(handle, CURLOPT_URL, issuerUrl_.c_str());
+    curl_easy_setopt(handle, CURLOPT_URL, (issuerUrl_ + 
"/.well-known/openid-configuration").c_str());
 
     // Write callback
     curl_easy_setopt(handle, CURLOPT_WRITEFUNCTION, curlWriteCallback);
@@ -235,8 +227,33 @@ void ClientCredentialFlow::initialize() {
 }
 void ClientCredentialFlow::close() {}
 
+std::string ClientCredentialFlow::generateJsonBody() const {
+    if (!keyFile_.isValid()) {
+        return "";
+    }
+
+    // fill in the request data
+    boost::property_tree::ptree pt;
+    pt.put("grant_type", "client_credentials");
+    pt.put("client_id", keyFile_.getClientId());
+    pt.put("client_secret", keyFile_.getClientSecret());
+    pt.put("audience", audience_);
+    if (!scope_.empty()) {
+        pt.put("scope", scope_);
+    }
+
+    std::ostringstream ss;
+    boost::property_tree::json_parser::write_json(ss, pt);
+    return ss.str();
+}
+
 Oauth2TokenResultPtr ClientCredentialFlow::authenticate() {
     Oauth2TokenResultPtr resultPtr = Oauth2TokenResultPtr(new 
Oauth2TokenResult());
+    const auto jsonBody = generateJsonBody();
+    if (jsonBody.empty() || tokenEndPoint_.empty()) {
+        return resultPtr;
+    }
+    LOG_DEBUG("Generate JSON body for ClientCredentialFlow: " << jsonBody);
 
     CURL* handle = curl_easy_init();
     CURLcode res;
@@ -263,25 +280,11 @@ Oauth2TokenResultPtr ClientCredentialFlow::authenticate() 
{
     curl_easy_setopt(handle, CURLOPT_SSL_VERIFYPEER, 0L);
     curl_easy_setopt(handle, CURLOPT_SSL_VERIFYHOST, 0L);
 
-    // fill in the request data
-    boost::property_tree::ptree pt;
-    pt.put("grant_type", "client_credentials");
-    pt.put("client_id", clientId_);
-    pt.put("client_secret", clientSecret_);
-    pt.put("audience", audience_);
-
-    std::stringstream ss;
-    boost::property_tree::json_parser::write_json(ss, pt);
-    std::string ssString = ss.str();
-
-    curl_easy_setopt(handle, CURLOPT_POSTFIELDS, ssString.c_str());
+    curl_easy_setopt(handle, CURLOPT_POSTFIELDS, jsonBody.c_str());
 
     // Make get call to server
     res = curl_easy_perform(handle);
 
-    LOG_DEBUG("issuerUrl_ " << issuerUrl_ << " clientid: " << clientId_ << " 
client_secret " << clientSecret_
-                            << " audience " << audience_ << " ssstring " << 
ssString);
-
     switch (res) {
         case CURLE_OK:
             long response_code;
@@ -295,7 +298,7 @@ Oauth2TokenResultPtr ClientCredentialFlow::authenticate() {
                     boost::property_tree::read_json(stream, root);
                 } catch (boost::property_tree::json_parser_error& e) {
                     LOG_ERROR("Failed to parse json of Oauth2 response: "
-                              << e.what() << "\nInput Json = " << responseData 
<< " passedin: " << ssString);
+                              << e.what() << "\nInput Json = " << responseData 
<< " passedin: " << jsonBody);
                     break;
                 }
 
@@ -306,12 +309,12 @@ Oauth2TokenResultPtr ClientCredentialFlow::authenticate() 
{
                                            << " expires_in: " << 
resultPtr->getExpiresIn());
             } else {
                 LOG_ERROR("Response failed for issuerurl " << issuerUrl_ << ". 
response Code "
-                                                           << response_code << 
" passedin: " << ssString);
+                                                           << response_code << 
" passedin: " << jsonBody);
             }
             break;
         default:
             LOG_ERROR("Response failed for issuerurl " << issuerUrl_ << ". 
Error Code " << res
-                                                       << " passedin: " << 
ssString);
+                                                       << " passedin: " << 
jsonBody);
             break;
     }
     // Free header list
@@ -323,17 +326,8 @@ Oauth2TokenResultPtr ClientCredentialFlow::authenticate() {
 
 // AuthOauth2
 
-AuthOauth2::AuthOauth2(ParamMap& params) {
-    std::map<std::string, std::string>::iterator it;
-    it = params.find("private_key");
-
-    if (it != params.end()) {
-        flowPtr_ = FlowPtr(
-            new ClientCredentialFlow(params["issuer_url"], 
params["private_key"], params["audience"]));
-    } else {
-        flowPtr_ = FlowPtr(new ClientCredentialFlow(params["issuer_url"], 
params["client_id"],
-                                                    params["client_secret"], 
params["audience"]));
-    }
+AuthOauth2::AuthOauth2(ParamMap& params) : flowPtr_(new 
ClientCredentialFlow(params)) {
+    flowPtr_->initialize();
 }
 
 AuthOauth2::~AuthOauth2() {}
diff --git a/pulsar-client-cpp/lib/auth/AuthOauth2.h 
b/pulsar-client-cpp/lib/auth/AuthOauth2.h
index 4de6f53..43f3743 100644
--- a/pulsar-client-cpp/lib/auth/AuthOauth2.h
+++ b/pulsar-client-cpp/lib/auth/AuthOauth2.h
@@ -28,22 +28,41 @@ const std::string OAUTH2_TOKEN_PLUGIN_NAME = "oauth2token";
 const std::string OAUTH2_TOKEN_JAVA_PLUGIN_NAME =
     "org.apache.pulsar.client.impl.auth.oauth2.AuthenticationOAuth2";
 
+class KeyFile {
+   public:
+    static KeyFile fromParamMap(ParamMap& params);
+
+    const std::string& getClientId() const noexcept { return clientId_; }
+    const std::string& getClientSecret() const noexcept { return 
clientSecret_; }
+    bool isValid() const noexcept { return valid_; }
+
+   private:
+    const std::string clientId_;
+    const std::string clientSecret_;
+    const bool valid_;
+
+    KeyFile(const std::string& clientId, const std::string& clientSecret)
+        : clientId_(clientId), clientSecret_(clientSecret), valid_(true) {}
+    KeyFile() : valid_(false) {}
+
+    static KeyFile fromFile(const std::string& filename);
+};
+
 class ClientCredentialFlow : public Oauth2Flow {
    public:
-    ClientCredentialFlow(const std::string& issuerUrl, const std::string& 
clientId,
-                         const std::string& clientSecret, const std::string& 
audience);
-    ClientCredentialFlow(const std::string& issuerUrl, const std::string& 
credentialsFilePath,
-                         const std::string& audience);
+    ClientCredentialFlow(ParamMap& params);
     void initialize();
     Oauth2TokenResultPtr authenticate();
     void close();
 
+    std::string generateJsonBody() const;
+
    private:
     std::string tokenEndPoint_;
-    std::string issuerUrl_;
-    std::string clientId_;
-    std::string clientSecret_;
-    std::string audience_;
+    const std::string issuerUrl_;
+    const KeyFile keyFile_;
+    const std::string audience_;
+    const std::string scope_;
 };
 
 class Oauth2CachedToken : public CachedToken {
diff --git a/pulsar-client-cpp/tests/AuthPluginTest.cc 
b/pulsar-client-cpp/tests/AuthPluginTest.cc
index 2d7aa1d..d15a383 100644
--- a/pulsar-client-cpp/tests/AuthPluginTest.cc
+++ b/pulsar-client-cpp/tests/AuthPluginTest.cc
@@ -23,6 +23,7 @@
 #include <boost/algorithm/string.hpp>
 #include <thread>
 #include <lib/LogUtils.h>
+#include <lib/auth/AuthOauth2.h>
 
 #include "lib/Future.h"
 #include "lib/Utils.h"
@@ -395,3 +396,34 @@ TEST(AuthPluginTest, testOauth2CredentialFile) {
     ASSERT_EQ(data->hasDataFromCommand(), true);
     ASSERT_EQ(data->getCommandData().length(), expectedTokenLength);
 }
+
+TEST(AuthPluginTest, testOauth2RequestBody) {
+    ParamMap params;
+    params["issuer_url"] = "https://dev-kt-aa9ne.us.auth0.com";;
+    params["client_id"] = "Xd23RHsUnvUlP7wchjNYOaIfazgeHd9x";
+    params["client_secret"] = 
"rT7ps7WY8uhdVuBTKWZkttwLdQotmdEliaM5rLfmgNibvqziZ-g07ZH52N_poGAb";
+    params["audience"] = "https://dev-kt-aa9ne.us.auth0.com/api/v2/";;
+
+    std::string expectedJson = R"({
+    "grant_type": "client_credentials",
+    "client_id": "Xd23RHsUnvUlP7wchjNYOaIfazgeHd9x",
+    "client_secret": 
"rT7ps7WY8uhdVuBTKWZkttwLdQotmdEliaM5rLfmgNibvqziZ-g07ZH52N_poGAb",
+    "audience": "https:\/\/dev-kt-aa9ne.us.auth0.com\/api\/v2\/"
+}
+)";
+
+    ClientCredentialFlow flow1(params);
+    ASSERT_EQ(flow1.generateJsonBody(), expectedJson);
+
+    params["scope"] = "test-scope";
+    expectedJson = R"({
+    "grant_type": "client_credentials",
+    "client_id": "Xd23RHsUnvUlP7wchjNYOaIfazgeHd9x",
+    "client_secret": 
"rT7ps7WY8uhdVuBTKWZkttwLdQotmdEliaM5rLfmgNibvqziZ-g07ZH52N_poGAb",
+    "audience": "https:\/\/dev-kt-aa9ne.us.auth0.com\/api\/v2\/",
+    "scope": "test-scope"
+}
+)";
+    ClientCredentialFlow flow2(params);
+    ASSERT_EQ(flow2.generateJsonBody(), expectedJson);
+}

Reply via email to