This is an automated email from the ASF dual-hosted git repository.
cgivre pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/drill.git
The following commit(s) were added to refs/heads/master by this push:
new 2b47cec DRILL-8056: Add OAuth2 Support for HTTP Rest Plugin (#2401)
2b47cec is described below
commit 2b47cec3a95966e119dac643050544c93b45be15
Author: Charles S. Givre <[email protected]>
AuthorDate: Mon Jan 31 16:48:42 2022 -0500
DRILL-8056: Add OAuth2 Support for HTTP Rest Plugin (#2401)
* DRILL-8056: Add OAuth2 Support for HTTP Rest Plugin
* Fixed one unit test and removed code from other branch
* Fix UT
* WIP
* Moving to Cred provider
* WIP
* UT working with Cred provider
* Successfully optained tokens
* Ready for review
* Fixed Checkstyle
* Code cleanup
* Updated docs and minor code cleanup
* Moved okhttp3 to test scope
* Fixed pom... again
* Pom fixes
* Added redirect URI to access token request
* Added token type to config
* Doc update
* Updated docs and addressed review comments
* Initial work with persistent store
* 2/3 Unit tests passing
* All unit tests passing
* Code cleanup
* Passing manual tests with Clickup and Salesforce
* Final revisions
* Fix resource leak
* Added Success FTL
* Fix resource leak
* UTs passing
* Addressed Review Comments
* Removed unused import
---
contrib/storage-http/OAuth.md | 127 +++++++++++
contrib/storage-http/README.md | 9 +-
contrib/storage-http/images/access_token.png | Bin 0 -> 92037 bytes
contrib/storage-http/images/get_access_token.png | Bin 0 -> 19115 bytes
.../exec/store/http/HttpAPIConnectionSchema.java | 5 +-
.../drill/exec/store/http/HttpJsonOptions.java | 9 -
.../drill/exec/store/http/HttpOAuthConfig.java | 198 ++++++++++++++++
.../exec/store/http/HttpScanBatchCreator.java | 5 +-
.../apache/drill/exec/store/http/HttpScanSpec.java | 21 +-
.../drill/exec/store/http/HttpSchemaFactory.java | 2 +-
.../drill/exec/store/http/HttpStoragePlugin.java | 23 +-
.../exec/store/http/HttpStoragePluginConfig.java | 64 +++++-
.../store/http/oauth/AccessTokenAuthenticator.java | 83 +++++++
.../store/http/oauth/AccessTokenInterceptor.java | 92 ++++++++
.../store/http/oauth/AccessTokenRepository.java | 154 +++++++++++++
.../drill/exec/store/http/util/SimpleHttp.java | 62 ++++-
.../drill/exec/store/http/TestHttpPlugin.java | 6 +-
.../drill/exec/store/http/TestOAuthProcess.java | 254 +++++++++++++++++++++
.../drill/exec/store/http/TestPagination.java | 4 +-
.../src/test/resources/data/oauth-1.json | 3 +
.../src/test/resources/data/oauth-2.json | 3 +
.../data/oauth_access_token_response.json | 7 +
.../src/test/resources/data/token_refresh.json | 6 +
exec/java-exec/pom.xml | 13 +-
.../drill/exec/oauth/OAuthTokenProvider.java | 55 +++++
.../drill/exec/oauth/PersistentTokenRegistry.java | 113 +++++++++
.../drill/exec/oauth/PersistentTokenTable.java | 119 ++++++++++
.../org/apache/drill/exec/oauth/TokenRegistry.java | 34 +--
.../java/org/apache/drill/exec/oauth/Tokens.java | 70 ++++++
.../apache/drill/exec/server/DrillbitContext.java | 8 +-
.../exec/server/rest/PluginConfigWrapper.java | 27 +++
.../drill/exec/server/rest/StorageResources.java | 97 ++++++++
.../drill/exec/store/http/oauth/OAuthUtils.java | 176 ++++++++++++++
.../store/security/CredentialProviderUtils.java | 46 ++++
.../security/UsernamePasswordCredentials.java | 12 +-
.../security/oauth/OAuthTokenCredentials.java | 87 +++++++
.../src/main/resources/rest/storage/success.html | 35 +++
.../src/main/resources/rest/storage/update.ftl | 79 ++++++-
exec/jdbc-all/pom.xml | 2 +-
.../logical/security/CredentialsProvider.java | 4 +
.../logical/security/PlainCredentialsProvider.java | 3 +
41 files changed, 2047 insertions(+), 70 deletions(-)
diff --git a/contrib/storage-http/OAuth.md b/contrib/storage-http/OAuth.md
new file mode 100644
index 0000000..6c95c00
--- /dev/null
+++ b/contrib/storage-http/OAuth.md
@@ -0,0 +1,127 @@
+# OAuth2.0 Authentication in Drill
+Many APIs use OAuth2.0 as a means of authentication. Drill can connect to APIs
that use OAuth2 for authentication but OAuth2 is significantly more complex
than simple
+username/password authentication.
+
+The good news, and bottom line here is that you can configure Drill to handle
all this automagically, but you do have to understand how to configure it so
that it works. First,
+let's get a high level understanding of how OAuth works. Click here to [skip
to the section on configuring Drill](#configure-drill).
+
+### Understanding the OAuth2 Process
+There are many tutorials as to how OAuth works which we will not repeat here.
There are some slight variations but this is a good enough high level overview
so you will understand the process works.
+Thus, we will summarize the process as four steps:
+
+#### Step 1: Obtain an Authorization Code
+For the first step, a user will have to log into the API's front end, and
authorize the application to access the API. The API will issue you a
`clientID` and a
+`client_secret`. We will use these tokens in later stages.
+
+You will also need to provide the API with a `callbackURL`. This URL is how
the API sends your application the `authorizationCode` which we will use in
step 2.
+Once you have the `clientID` and the `callbackURL`, your application will make
a `GET` request to the API to obtain the `authorizationCode`.
+
+#### Step 2: Swap the Authorization Code for an Access Token
+At this point, we need to obtain the `accessToken`. We do so by sending a
`POST` request to the API with the `clientID`, the `clientSecret` and the
`authorizationCode` we
+obtained in step 1. Note that the `authorizationCode` is a short lived token,
but the `accessToken` lasts for a longer period. When the access token
expires, you may need to
+either re-authorize the application or use a refresh token to obtain a new one.
+
+#### Step 3: Call the Protected Resource with the Access Token
+Once you have the `accessToken` you are ready to make authenticated API calls.
All you have to do here is add the `accessToken` to the API header and you can
make API calls
+just like any other.
+
+#### Step 4: (Optional) Obtain a new Access Token using the Refresh Token
+Sometimes, the `accessToken` will expire. When this happens, the API will
respond with a `401` not authorized response. When this happens, the
application will make a `POST`
+request containing the `clientSecret`, the `clientID` and the `refreshToken`
and will obtain new tokens.
+
+## The Artifacts
+Unlike simple username/password authentication, there are about 5 artifacts
that you will need to authenticate using OAuth and it is also helpful to
understand where they come
+from and to whom they "belong". Let's start with the artifacts that you will
need to manually obtain from the API when you register your application:
(These are not the Drill
+config variables, but the names are similar. More on that later.)
+* `clientID`: A token to uniquely identify your application with the API.
+* `clientSecret`: A sort of password token which will be used to obtain
additional tokens.
+* `callbackURL`: The URL to which access and refresh tokens will be sent. You
have to provide this URL when you register your application with the API. If
this does not match
+ what you provide the API, the calls will fail.
+* `scope`: An optional parameter which defines the scope of access request
for the given access token. The API will provide examples, but you have to pick
what accesses you
+ are requesting.
+
+You will need to find two URLs in the API documentation:
+
+* `authorizationURL`: This is the URL from which you will request the
`authorizationCode`. You should find this in the API documentation.
+* `tokenURL`: The URL from which you can request the `accessToken`.
+
+There are two other artifacts that you will need, but these artifacts are
generated by the API. One thing to note is that while all the other artifacts
are owned by the
+application, these two are unique (and "owned by") the user. These artifacts
are:
+* `accessToken`: The token which is used to grant access to a protected
resource
+* `refreshToken`: The token used to obtain a new `accessToken` without having
to re-authorize the application.
+
+Currently, Drill does not allow per-user credentials. However, future work
may permit this.
+
+<h1 id="configure-drill">Configuring Drill for OAuth</h1>
+Configuring Drill to connect to OAuth2.0 enabled APIs is a little complicated
as part of the configuration parameters are stored in the REST plugin and
others are stored in the
+credentialProvider.
+
+To use OAuth2.0, you will have to create an `oAuthConfig` in the plugin
configuration. Within the `oAuthConfig`, define the `callbackURL` and
`authorizationURL` parameters:
+* The `authorizationURL` is provided by the API and is the URL where the
authorization code is obtained.
+* The `callbackURL` parameter is the URL where the API will send the access
token. You must provide this when you register and obtain your client ID and
client secret. This
+ will be in the format: `http(s)://<your drill host>/storage/<storage plugin
name>update_oauth2_authtoken`
+* (Optional)`scope`: The scope parameter limits the scope of your access.
This is something which can be found in the remote API documentation.
+
+### The Credential Provider
+The actual tokens are stored using Drill's Credential Provider. In addition
to the `oAuthConfig` section of the plugin configuration, you must also add a
section for the
+`credentialProvider` as shown in the example below. At a minimum, that
section must contain:
+* `clientID`
+* `clientSecret`
+* `tokenURI`
+
+All those parameters are provided by the API once you register. Currently,
only the `PlainCredentialProvider` is supported for this, but future work will
likely include the
+`VaultCredentialProvider`.
+
+## Obtaining the Access Token
+Once you have set up the configuration, you will see an additional button
labeled `Get Access Token` at the bottom of the configuration screen.
+<img src="images/get_access_token.png" />
+
+If everything is configured correctly, you will be taken to your API's
authentication page, where the user will be asked to authenticate with the API
and grant your application
+permission to access protected resources. Once the user does this, you will
see the window below:
+<img src="images/access_token.png" />
+
+When you close that window, you will see an updated configuration which
includes the access and refresh tokens. At this point you are ready to query!
+
+## Example Configuration
+The example configuration below demonstrates how to connect Drill to the API
available at clickup.com.
+```json
+{
+ "type": "http",
+ "connections": {
+ "team": {
+ "url": "https://api.clickup.com/api/v2/team",
+ "requireTail": false,
+ "method": "GET",
+ "headers": {
+ "Content-Type": "application/json"
+ },
+ "dataPath": "teams",
+ "authType": "none",
+ "inputType": "json",
+ "xmlDataLevel": 1,
+ "verifySSLCert": true
+ }
+ },
+ "proxyType": "direct",
+ "oAuthConfig": {
+ "callbackURL":
"http://localhost:8047/storage/clickup/update_oath2_authtoken",
+ "authorizationURL": "https://app.clickup.com/api"
+ },
+ "credentialsProvider": {
+ "credentialsProviderType": "PlainCredentialsProvider",
+ "credentials": {
+ "clientID": "<your client ID>",
+ "clientSecret": "<your client secret>",
+ "tokenURI": "https://app.clickup.com/api/v2/oauth/token"
+ }
+ },
+ "enabled": true
+}
+
+```
+
+## Optional Parameters
+There are a few optional parameters in the OAuth config which you may need to
set in order for Drill to successfully. These parameters are completely
optional.
+
+* `tokenType`: Some OAuth enabled APIs provide a `Bearer` token. If that is
the case, this should be set to `Bearer`.
+* `authorizationParams`: A key value parameters which are sent during the
authentication process.
diff --git a/contrib/storage-http/README.md b/contrib/storage-http/README.md
index ab4058d..79c76f6 100644
--- a/contrib/storage-http/README.md
+++ b/contrib/storage-http/README.md
@@ -270,8 +270,7 @@ All of these can be set by adding the `jsonOptions` to your
connection configura
#### Authorization
`authType`: If your API requires authentication, specify the authentication
-type. At the time of implementation, the plugin only supports basic
authentication, however, the
-plugin will likely support OAUTH2 in the future. Defaults to `none`.
+type. Defaults to `none`.
If the `authType` is set to `basic`, `username` and `password` must be set in
the configuration as well.
`username`: The username for basic authentication.
@@ -291,6 +290,12 @@ Drill will send the following request to your API:
https://<api>?maxRecords=10
```
+### OAuth2.0
+If the API which you are querying requires OAuth2.0 for authentication [read
the documentation for configuring Drill to use OAuth2.0](OAuth.md).
+
+### Pagination
+If you want to use automatic pagination in Drill, [click here to read the
documentation for pagination](Pagination.md).
+
#### errorOn400
When a user makes HTTP calls, the response code will be from 100-599. 400
series error codes can contain useful information and in some cases you would
not want Drill to throw
errors on 400 series errors. This option allows you to define Drill's
behavior on 400 series error codes. When set to `true`, Drill will throw an
exception and halt execution
diff --git a/contrib/storage-http/images/access_token.png
b/contrib/storage-http/images/access_token.png
new file mode 100644
index 0000000..e2d81b5
Binary files /dev/null and b/contrib/storage-http/images/access_token.png differ
diff --git a/contrib/storage-http/images/get_access_token.png
b/contrib/storage-http/images/get_access_token.png
new file mode 100644
index 0000000..55a72a0
Binary files /dev/null and b/contrib/storage-http/images/get_access_token.png
differ
diff --git
a/contrib/storage-http/src/main/java/org/apache/drill/exec/store/http/HttpAPIConnectionSchema.java
b/contrib/storage-http/src/main/java/org/apache/drill/exec/store/http/HttpAPIConnectionSchema.java
index 81efda1..c9f7e31 100644
---
a/contrib/storage-http/src/main/java/org/apache/drill/exec/store/http/HttpAPIConnectionSchema.java
+++
b/contrib/storage-http/src/main/java/org/apache/drill/exec/store/http/HttpAPIConnectionSchema.java
@@ -63,10 +63,11 @@ public class HttpAPIConnectionSchema extends AbstractSchema
{
// Return the found table
return table;
} else {
+
// Register a new table
return registerTable(tableName, new DynamicDrillTable(plugin,
plugin.getName(),
- new HttpScanSpec(plugin.getName(), name, tableName,
- plugin.getConfig().copyForPlan(name))));
+ new HttpScanSpec(plugin.getName(), name, tableName,
+ plugin.getConfig().copyForPlan(name), plugin.getTokenTable(),
plugin.getRegistry())));
}
}
diff --git
a/contrib/storage-http/src/main/java/org/apache/drill/exec/store/http/HttpJsonOptions.java
b/contrib/storage-http/src/main/java/org/apache/drill/exec/store/http/HttpJsonOptions.java
index 4928c66..0ca2b76 100644
---
a/contrib/storage-http/src/main/java/org/apache/drill/exec/store/http/HttpJsonOptions.java
+++
b/contrib/storage-http/src/main/java/org/apache/drill/exec/store/http/HttpJsonOptions.java
@@ -20,7 +20,6 @@ package org.apache.drill.exec.store.http;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonInclude;
-import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import lombok.Builder;
import lombok.EqualsAndHashCode;
import lombok.Getter;
@@ -37,7 +36,6 @@ import
org.apache.drill.exec.store.easy.json.loader.JsonLoaderOptions;
@EqualsAndHashCode
@ToString
@JsonInclude(JsonInclude.Include.NON_DEFAULT)
-@JsonDeserialize(builder = HttpJsonOptions.HttpJsonOptionsBuilder.class)
public class HttpJsonOptions {
@JsonInclude
@@ -54,26 +52,19 @@ public class HttpJsonOptions {
@JsonIgnore
public JsonLoaderOptions getJsonOptions(OptionSet optionSet) {
-
JsonLoaderOptions options = new JsonLoaderOptions(optionSet);
-
if (allowNanInf != null) {
options.allowNanInf = allowNanInf;
}
-
if (allTextMode != null) {
options.allTextMode = allTextMode;
}
-
if (readNumbersAsDouble != null) {
options.readNumbersAsDouble = readNumbersAsDouble;
}
-
if (enableEscapeAnyChar != null) {
options.enableEscapeAnyChar = enableEscapeAnyChar;
}
-
return options;
}
-
}
diff --git
a/contrib/storage-http/src/main/java/org/apache/drill/exec/store/http/HttpOAuthConfig.java
b/contrib/storage-http/src/main/java/org/apache/drill/exec/store/http/HttpOAuthConfig.java
new file mode 100644
index 0000000..cdac404
--- /dev/null
+++
b/contrib/storage-http/src/main/java/org/apache/drill/exec/store/http/HttpOAuthConfig.java
@@ -0,0 +1,198 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.drill.exec.store.http;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
+import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder;
+import org.apache.drill.common.PlanStringBuilder;
+
+import java.util.Map;
+import java.util.Objects;
+
+@JsonInclude(JsonInclude.Include.NON_DEFAULT)
+@JsonDeserialize(builder = HttpOAuthConfig.HttpOAuthConfigBuilder.class)
+public class HttpOAuthConfig {
+
+ private final String callbackURL;
+ private final String authorizationURL;
+ private final Map<String, String> authorizationParams;
+ private final String tokenType;
+ private final boolean generateCSRFToken;
+ private final String scope;
+ private final boolean accessTokenInHeader;
+
+ @JsonCreator
+ public HttpOAuthConfig(@JsonProperty("callbackURL") String callbackURL,
+ @JsonProperty("authorizationURL") String
authorizationURL,
+ @JsonProperty("authorizationParams") Map<String,
String> authorizationParams,
+ @JsonProperty("tokenType") String tokenType,
+ @JsonProperty("generateCSRFToken") boolean
generateCSRFToken,
+ @JsonProperty("scope") String scope,
+ @JsonProperty("accessTokenInHeader") boolean
accessTokenInHeader) {
+ this.callbackURL = callbackURL;
+ this.authorizationURL = authorizationURL;
+ this.authorizationParams = authorizationParams;
+ this.tokenType = tokenType;
+ this.generateCSRFToken = generateCSRFToken;
+ this.accessTokenInHeader = accessTokenInHeader;
+ this.scope = scope;
+ }
+
+ public HttpOAuthConfig(HttpOAuthConfig.HttpOAuthConfigBuilder builder) {
+ this.callbackURL = builder.callbackURL;
+ this.authorizationURL = builder.authorizationURL;
+ this.authorizationParams = builder.authorizationParams;
+ this.generateCSRFToken = builder.generateCSRFToken;
+ this.tokenType = builder.tokenType;
+ this.accessTokenInHeader = builder.accessTokenInHeader;
+ this.scope = builder.scope;
+ }
+
+ public static HttpOAuthConfigBuilder builder() {
+ return new HttpOAuthConfigBuilder();
+ }
+
+ public String getCallbackURL() {
+ return this.callbackURL;
+ }
+
+ public String getAuthorizationURL() {
+ return this.authorizationURL;
+ }
+
+ public Map<String, String> getAuthorizationParams() {
+ return this.authorizationParams;
+ }
+
+ public String getTokenType() {
+ return this.tokenType;
+ }
+
+ public boolean isGenerateCSRFToken() {
+ return this.generateCSRFToken;
+ }
+
+ public String getScope() {
+ return this.scope;
+ }
+
+ public boolean isAccessTokenInHeader() {
+ return this.accessTokenInHeader;
+ }
+
+ @Override
+ public String toString() {
+ return new PlanStringBuilder(this)
+ .field("callbackURL", callbackURL)
+ .field("authorizationURL", authorizationURL)
+ .field("authorizationParams", authorizationParams)
+ .field("tokenType", tokenType)
+ .field("generateCSRFToken", generateCSRFToken)
+ .field("scope", scope)
+ .field("accessTokenInHeader", accessTokenInHeader)
+ .toString();
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(callbackURL, authorizationURL, authorizationParams,
+ tokenType, generateCSRFToken, scope, accessTokenInHeader);
+ }
+
+ @Override
+ public boolean equals(Object that) {
+ if (this == that) {
+ return true;
+ } else if (that == null || getClass() != that.getClass()) {
+ return false;
+ }
+ HttpOAuthConfig thatConfig = (HttpOAuthConfig) that;
+ return Objects.equals(callbackURL, thatConfig.callbackURL) &&
+ Objects.equals(authorizationURL, thatConfig.authorizationURL) &&
+ Objects.equals(authorizationParams, thatConfig.authorizationParams) &&
+ Objects.equals(tokenType, thatConfig.tokenType) &&
+ Objects.equals(generateCSRFToken, thatConfig.generateCSRFToken) &&
+ Objects.equals(scope, thatConfig.scope) &&
+ Objects.equals(accessTokenInHeader, thatConfig.accessTokenInHeader);
+ }
+
+ @JsonPOJOBuilder(withPrefix = "")
+ public static class HttpOAuthConfigBuilder {
+ private String callbackURL;
+
+ private String authorizationURL;
+
+ private Map<String, String> authorizationParams;
+
+ private String tokenType;
+
+ private boolean generateCSRFToken;
+
+ private String scope;
+
+ private boolean accessTokenInHeader;
+
+ private Map<String, String> tokens;
+
+ HttpOAuthConfigBuilder() {
+ }
+
+ public HttpOAuthConfig build() {
+ return new HttpOAuthConfig(this);
+ }
+
+ public HttpOAuthConfigBuilder callbackURL(String callbackURL) {
+ this.callbackURL = callbackURL;
+ return this;
+ }
+
+ public HttpOAuthConfigBuilder authorizationURL(String authorizationURL) {
+ this.authorizationURL = authorizationURL;
+ return this;
+ }
+
+ public HttpOAuthConfigBuilder authorizationParams(Map<String, String>
authorizationParams) {
+ this.authorizationParams = authorizationParams;
+ return this;
+ }
+
+ public HttpOAuthConfigBuilder tokenType(String tokenType) {
+ this.tokenType = tokenType;
+ return this;
+ }
+
+ public HttpOAuthConfigBuilder generateCSRFToken(boolean generateCSRFToken)
{
+ this.generateCSRFToken = generateCSRFToken;
+ return this;
+ }
+
+ public HttpOAuthConfigBuilder scope(String scope) {
+ this.scope = scope;
+ return this;
+ }
+
+ public HttpOAuthConfigBuilder accessTokenInHeader(boolean
accessTokenInHeader) {
+ this.accessTokenInHeader = accessTokenInHeader;
+ return this;
+ }
+ }
+}
diff --git
a/contrib/storage-http/src/main/java/org/apache/drill/exec/store/http/HttpScanBatchCreator.java
b/contrib/storage-http/src/main/java/org/apache/drill/exec/store/http/HttpScanBatchCreator.java
index a50fdc8..fda295c 100644
---
a/contrib/storage-http/src/main/java/org/apache/drill/exec/store/http/HttpScanBatchCreator.java
+++
b/contrib/storage-http/src/main/java/org/apache/drill/exec/store/http/HttpScanBatchCreator.java
@@ -48,9 +48,10 @@ public class HttpScanBatchCreator implements
BatchCreator<HttpSubScan> {
private static final Logger logger =
LoggerFactory.getLogger(HttpScanBatchCreator.class);
@Override
- public CloseableRecordBatch getBatch(ExecutorFragmentContext context,
HttpSubScan subScan, List<RecordBatch> children) throws ExecutionSetupException
{
+ public CloseableRecordBatch getBatch(ExecutorFragmentContext context,
+ HttpSubScan subScan,
+ List<RecordBatch> children) throws
ExecutionSetupException {
Preconditions.checkArgument(children.isEmpty());
-
try {
ScanFrameworkBuilder builder = createBuilder(context.getOptions(),
subScan);
return builder.buildScanOperator(context, subScan);
diff --git
a/contrib/storage-http/src/main/java/org/apache/drill/exec/store/http/HttpScanSpec.java
b/contrib/storage-http/src/main/java/org/apache/drill/exec/store/http/HttpScanSpec.java
index f2cab11..e43490c 100644
---
a/contrib/storage-http/src/main/java/org/apache/drill/exec/store/http/HttpScanSpec.java
+++
b/contrib/storage-http/src/main/java/org/apache/drill/exec/store/http/HttpScanSpec.java
@@ -17,11 +17,14 @@
*/
package org.apache.drill.exec.store.http;
+import com.fasterxml.jackson.annotation.JacksonInject;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonTypeName;
import org.apache.drill.common.PlanStringBuilder;
+import org.apache.drill.exec.oauth.PersistentTokenTable;
+import org.apache.drill.exec.store.StoragePluginRegistry;
@JsonTypeName("http-scan-spec")
public class HttpScanSpec {
@@ -30,16 +33,22 @@ public class HttpScanSpec {
private final String connectionName;
private final String tableName;
private final HttpStoragePluginConfig config;
+ private final StoragePluginRegistry registry;
+ private final PersistentTokenTable tokenTable;
@JsonCreator
public HttpScanSpec(@JsonProperty("pluginName") String pluginName,
@JsonProperty("connection") String connectionName,
@JsonProperty("tableName") String tableName,
- @JsonProperty("config") HttpStoragePluginConfig config) {
+ @JsonProperty("config") HttpStoragePluginConfig config,
+ @JsonProperty("tokenTable") PersistentTokenTable
tokenTable,
+ @JacksonInject StoragePluginRegistry engineRegistry) {
this.pluginName = pluginName;
this.connectionName = connectionName;
this.tableName = tableName;
this.config = config;
+ this.registry = engineRegistry;
+ this.tokenTable = tokenTable;
}
@JsonProperty("pluginName")
@@ -63,11 +72,21 @@ public class HttpScanSpec {
}
@JsonIgnore
+ public PersistentTokenTable getTokenTable() {
+ return tokenTable;
+ }
+
+ @JsonIgnore
public String getURL() {
return connectionName;
}
@JsonIgnore
+ public StoragePluginRegistry getRegistry() {
+ return registry;
+ }
+
+ @JsonIgnore
public HttpApiConfig connectionConfig() {
return config.getConnection(connectionName);
}
diff --git
a/contrib/storage-http/src/main/java/org/apache/drill/exec/store/http/HttpSchemaFactory.java
b/contrib/storage-http/src/main/java/org/apache/drill/exec/store/http/HttpSchemaFactory.java
index 7610806..661cbef 100644
---
a/contrib/storage-http/src/main/java/org/apache/drill/exec/store/http/HttpSchemaFactory.java
+++
b/contrib/storage-http/src/main/java/org/apache/drill/exec/store/http/HttpSchemaFactory.java
@@ -104,7 +104,7 @@ public class HttpSchemaFactory extends
AbstractSchemaFactory {
// Register a new table
return registerTable(name, new DynamicDrillTable(plugin,
plugin.getName(),
new HttpScanSpec(plugin.getName(), name, null,
- plugin.getConfig().copyForPlan(name))));
+ plugin.getConfig().copyForPlan(name), plugin.getTokenTable(),
plugin.getRegistry())));
} else {
return null; // Unknown table
}
diff --git
a/contrib/storage-http/src/main/java/org/apache/drill/exec/store/http/HttpStoragePlugin.java
b/contrib/storage-http/src/main/java/org/apache/drill/exec/store/http/HttpStoragePlugin.java
index ab55f21..181c92e 100644
---
a/contrib/storage-http/src/main/java/org/apache/drill/exec/store/http/HttpStoragePlugin.java
+++
b/contrib/storage-http/src/main/java/org/apache/drill/exec/store/http/HttpStoragePlugin.java
@@ -20,12 +20,16 @@ package org.apache.drill.exec.store.http;
import org.apache.calcite.plan.RelOptRule;
import org.apache.calcite.schema.SchemaPlus;
import org.apache.drill.common.JSONOptions;
+import org.apache.drill.exec.oauth.OAuthTokenProvider;
+import org.apache.drill.exec.oauth.PersistentTokenTable;
+import org.apache.drill.exec.oauth.TokenRegistry;
import org.apache.drill.exec.ops.OptimizerRulesContext;
import org.apache.drill.exec.physical.base.AbstractGroupScan;
import org.apache.drill.exec.planner.PlannerPhase;
import org.apache.drill.exec.server.DrillbitContext;
import org.apache.drill.exec.store.AbstractStoragePlugin;
import org.apache.drill.exec.store.SchemaConfig;
+import org.apache.drill.exec.store.StoragePluginRegistry;
import org.apache.drill.exec.store.base.filter.FilterPushDownUtils;
import org.apache.drill.shaded.guava.com.google.common.collect.ImmutableSet;
@@ -36,13 +40,20 @@ import java.util.Set;
public class HttpStoragePlugin extends AbstractStoragePlugin {
private final HttpStoragePluginConfig config;
-
private final HttpSchemaFactory schemaFactory;
+ private final StoragePluginRegistry registry;
+ private final TokenRegistry tokenRegistry;
public HttpStoragePlugin(HttpStoragePluginConfig configuration,
DrillbitContext context, String name) {
super(context, name);
this.config = configuration;
+ this.registry = context.getStorage();
this.schemaFactory = new HttpSchemaFactory(this);
+
+ // Get OAuth Token Provider if needed
+ OAuthTokenProvider tokenProvider = context.getoAuthTokenProvider();
+ tokenRegistry = tokenProvider.getOauthTokenRegistry();
+ tokenRegistry.createTokenTable(getName());
}
@Override
@@ -55,6 +66,16 @@ public class HttpStoragePlugin extends AbstractStoragePlugin
{
return config;
}
+ public StoragePluginRegistry getRegistry() {
+ return registry;
+ }
+
+ public TokenRegistry getTokenRegistry() {
+ return tokenRegistry;
+ }
+
+ public PersistentTokenTable getTokenTable() { return
tokenRegistry.getTokenTable(getName()); }
+
@Override
public boolean supportsRead() {
return true;
diff --git
a/contrib/storage-http/src/main/java/org/apache/drill/exec/store/http/HttpStoragePluginConfig.java
b/contrib/storage-http/src/main/java/org/apache/drill/exec/store/http/HttpStoragePluginConfig.java
index 119d4b0..ba32c82 100644
---
a/contrib/storage-http/src/main/java/org/apache/drill/exec/store/http/HttpStoragePluginConfig.java
+++
b/contrib/storage-http/src/main/java/org/apache/drill/exec/store/http/HttpStoragePluginConfig.java
@@ -28,6 +28,7 @@ import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonTypeName;
import org.apache.drill.exec.store.security.CredentialProviderUtils;
import org.apache.drill.common.logical.security.CredentialsProvider;
+import org.apache.drill.exec.store.security.oauth.OAuthTokenCredentials;
import org.apache.drill.exec.store.security.UsernamePasswordCredentials;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -42,11 +43,14 @@ import java.util.concurrent.TimeUnit;
public class HttpStoragePluginConfig extends
AbstractSecuredStoragePluginConfig {
private static final Logger logger =
LoggerFactory.getLogger(HttpStoragePluginConfig.class);
public static final String NAME = "http";
+
public final Map<String, HttpApiConfig> connections;
public final boolean cacheResults;
public final String proxyHost;
public final int proxyPort;
public final String proxyType;
+ public final HttpOAuthConfig oAuthConfig;
+
/**
* Timeout in {@link TimeUnit#SECONDS}.
*/
@@ -61,9 +65,16 @@ public class HttpStoragePluginConfig extends
AbstractSecuredStoragePluginConfig
@JsonProperty("proxyType") String proxyType,
@JsonProperty("proxyUsername") String
proxyUsername,
@JsonProperty("proxyPassword") String
proxyPassword,
+ @JsonProperty("oAuthConfig") HttpOAuthConfig
oAuthConfig,
@JsonProperty("credentialsProvider")
CredentialsProvider credentialsProvider
) {
-
super(CredentialProviderUtils.getCredentialsProvider(normalize(proxyUsername),
normalize(proxyPassword), credentialsProvider),
+ super(CredentialProviderUtils.getCredentialsProvider(
+ getClientID(new OAuthTokenCredentials(credentialsProvider)),
+ getClientSecret(new OAuthTokenCredentials(credentialsProvider)),
+ getTokenURL(new OAuthTokenCredentials(credentialsProvider)),
+ normalize(proxyUsername),
+ normalize(proxyPassword),
+ credentialsProvider),
credentialsProvider == null);
this.cacheResults = cacheResults != null && cacheResults;
@@ -75,6 +86,8 @@ public class HttpStoragePluginConfig extends
AbstractSecuredStoragePluginConfig
this.timeout = timeout == null ? 0 : timeout;
this.proxyHost = normalize(proxyHost);
this.proxyPort = proxyPort == null ? 0 : proxyPort;
+ this.oAuthConfig = oAuthConfig;
+
proxyType = normalize(proxyType);
this.proxyType = proxyType == null
? "direct" : proxyType.trim().toLowerCase();
@@ -93,6 +106,24 @@ public class HttpStoragePluginConfig extends
AbstractSecuredStoragePluginConfig
}
}
+ /**
+ * Clone constructor used for updating OAuth tokens
+ * @param that The current HTTP Plugin Config
+ * @param oAuthConfig The updated OAuth config
+ */
+ public HttpStoragePluginConfig(HttpStoragePluginConfig that, HttpOAuthConfig
oAuthConfig) {
+ super(CredentialProviderUtils.getCredentialsProvider(that.proxyUsername(),
that.proxyPassword(), that.credentialsProvider),
+ that.credentialsProvider == null);
+
+ this.cacheResults = that.cacheResults;
+ this.connections = that.connections;
+ this.timeout = that.timeout;
+ this.proxyHost = that.proxyHost;
+ this.proxyPort = that.proxyPort;
+ this.proxyType = that.proxyType;
+ this.oAuthConfig = oAuthConfig;
+ }
+
private static String normalize(String value) {
if (value == null) {
return value;
@@ -108,7 +139,7 @@ public class HttpStoragePluginConfig extends
AbstractSecuredStoragePluginConfig
public HttpStoragePluginConfig copyForPlan(String connectionName) {
return new HttpStoragePluginConfig(
cacheResults, configFor(connectionName), timeout,
- proxyHost, proxyPort, proxyType, null, null, credentialsProvider);
+ proxyHost, proxyPort, proxyType, null, null, oAuthConfig,
credentialsProvider);
}
private Map<String, HttpApiConfig> configFor(String connectionName) {
@@ -130,6 +161,7 @@ public class HttpStoragePluginConfig extends
AbstractSecuredStoragePluginConfig
Objects.equals(proxyHost, thatConfig.proxyHost) &&
Objects.equals(proxyPort, thatConfig.proxyPort) &&
Objects.equals(proxyType, thatConfig.proxyType) &&
+ Objects.equals(oAuthConfig, thatConfig.oAuthConfig) &&
Objects.equals(credentialsProvider, thatConfig.credentialsProvider);
}
@@ -142,6 +174,7 @@ public class HttpStoragePluginConfig extends
AbstractSecuredStoragePluginConfig
.field("proxyHost", proxyHost)
.field("proxyPort", proxyPort)
.field("credentialsProvider", credentialsProvider)
+ .field("oauthConfig", oAuthConfig)
.field("proxyType", proxyType)
.toString();
}
@@ -149,7 +182,7 @@ public class HttpStoragePluginConfig extends
AbstractSecuredStoragePluginConfig
@Override
public int hashCode() {
return Objects.hash(connections, cacheResults, timeout,
- proxyHost, proxyPort, proxyType, credentialsProvider);
+ proxyHost, proxyPort, proxyType, oAuthConfig, credentialsProvider);
}
@JsonProperty("cacheResults")
@@ -167,6 +200,11 @@ public class HttpStoragePluginConfig extends
AbstractSecuredStoragePluginConfig
@JsonProperty("proxyPort")
public int proxyPort() { return proxyPort; }
+ @JsonProperty("oAuthConfig")
+ public HttpOAuthConfig oAuthConfig() {
+ return oAuthConfig;
+ }
+
@JsonProperty("proxyUsername")
public String proxyUsername() {
if (directCredentials) {
@@ -183,6 +221,21 @@ public class HttpStoragePluginConfig extends
AbstractSecuredStoragePluginConfig
return null;
}
+ @JsonIgnore
+ private static String getClientID(OAuthTokenCredentials credentials) {
+ return credentials.getClientID();
+ }
+
+ @JsonIgnore
+ private static String getClientSecret(OAuthTokenCredentials credentials) {
+ return credentials.getClientSecret();
+ }
+
+ @JsonIgnore
+ private static String getTokenURL(OAuthTokenCredentials credentials) {
+ return credentials.getTokenUri();
+ }
+
@JsonProperty("proxyType")
public String proxyType() { return proxyType; }
@@ -195,4 +248,9 @@ public class HttpStoragePluginConfig extends
AbstractSecuredStoragePluginConfig
public UsernamePasswordCredentials getUsernamePasswordCredentials() {
return new UsernamePasswordCredentials(credentialsProvider);
}
+
+ @JsonIgnore
+ public static OAuthTokenCredentials getOAuthCredentials(CredentialsProvider
credentialsProvider) {
+ return new OAuthTokenCredentials(credentialsProvider);
+ }
}
diff --git
a/contrib/storage-http/src/main/java/org/apache/drill/exec/store/http/oauth/AccessTokenAuthenticator.java
b/contrib/storage-http/src/main/java/org/apache/drill/exec/store/http/oauth/AccessTokenAuthenticator.java
new file mode 100644
index 0000000..9cca12b
--- /dev/null
+++
b/contrib/storage-http/src/main/java/org/apache/drill/exec/store/http/oauth/AccessTokenAuthenticator.java
@@ -0,0 +1,83 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.drill.exec.store.http.oauth;
+
+import lombok.NonNull;
+import okhttp3.Authenticator;
+import okhttp3.HttpUrl;
+import okhttp3.Request;
+import okhttp3.Response;
+import okhttp3.Route;
+import org.apache.commons.lang3.StringUtils;
+import org.jetbrains.annotations.NotNull;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class AccessTokenAuthenticator implements Authenticator {
+ private final static Logger logger =
LoggerFactory.getLogger(AccessTokenAuthenticator.class);
+
+ private final AccessTokenRepository accessTokenRepository;
+
+ public AccessTokenAuthenticator(AccessTokenRepository accessTokenRepository)
{
+ this.accessTokenRepository = accessTokenRepository;
+ }
+
+ @Override
+ public Request authenticate(Route route, @NotNull Response response) {
+ logger.debug("Authenticating {}", response.headers());
+ final String accessToken = accessTokenRepository.getAccessToken();
+ if (!isRequestWithAccessToken(response) || accessToken == null) {
+ return null;
+ }
+ synchronized (this) {
+ final String newAccessToken = accessTokenRepository.getAccessToken();
+ // Access token is refreshed in another thread.
+ if (!accessToken.equals(newAccessToken)) {
+ return newRequestWithAccessToken(response.request(), newAccessToken);
+ }
+
+ // Need to refresh an access token
+ final String updatedAccessToken;
+ updatedAccessToken = accessTokenRepository.refreshAccessToken();
+ return newRequestWithAccessToken(response.request(), updatedAccessToken);
+ }
+ }
+
+ private boolean isRequestWithAccessToken(@NonNull Response response) {
+ String header = response.request().header("Authorization");
+ return header != null && header.startsWith("Bearer");
+ }
+
+ @NonNull
+ private Request newRequestWithAccessToken(@NonNull Request request, @NonNull
String accessToken) {
+ logger.debug("Creating a new request with access token.");
+ String tokenType = accessTokenRepository.getTokenType();
+ if (StringUtils.isNotEmpty(tokenType)) {
+ accessToken = tokenType + " " + accessToken;
+ }
+
+ if (accessTokenRepository.getOAuthConfig().isAccessTokenInHeader()) {
+ HttpUrl rawUrl = HttpUrl.parse(request.url().toString());
+ rawUrl.newBuilder().addQueryParameter("access_token", accessToken);
+ return request.newBuilder().url(rawUrl.url()).build();
+ } else {
+ return request.newBuilder().header("Authorization", accessToken).build();
+ }
+ }
+}
diff --git
a/contrib/storage-http/src/main/java/org/apache/drill/exec/store/http/oauth/AccessTokenInterceptor.java
b/contrib/storage-http/src/main/java/org/apache/drill/exec/store/http/oauth/AccessTokenInterceptor.java
new file mode 100644
index 0000000..cf9bb11
--- /dev/null
+++
b/contrib/storage-http/src/main/java/org/apache/drill/exec/store/http/oauth/AccessTokenInterceptor.java
@@ -0,0 +1,92 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.drill.exec.store.http.oauth;
+
+import lombok.NonNull;
+import okhttp3.HttpUrl;
+import okhttp3.Interceptor;
+import okhttp3.Request;
+import okhttp3.Response;
+import org.apache.commons.lang3.StringUtils;
+import org.jetbrains.annotations.NotNull;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.net.HttpURLConnection;
+
+/**
+ * This class intercepts HTTP requests without the requisite OAuth credentials
and
+ * adds them to the request.
+ */
+public class AccessTokenInterceptor implements Interceptor {
+
+ private static final Logger logger =
LoggerFactory.getLogger(AccessTokenInterceptor.class);
+
+ private final AccessTokenRepository accessTokenRepository;
+
+ public AccessTokenInterceptor(AccessTokenRepository accessTokenRepository) {
+ this.accessTokenRepository = accessTokenRepository;
+ }
+
+ @NotNull
+ @Override
+ public Response intercept(Chain chain) throws IOException {
+ logger.debug("Intercepting call {}", chain.toString());
+ String accessToken = accessTokenRepository.getAccessToken();
+ Request request = newRequestWithAccessToken(chain.request(), accessToken);
+ Response response = chain.proceed(request);
+
+ if (response.code() == HttpURLConnection.HTTP_UNAUTHORIZED) {
+ logger.debug("Unauthorized request.");
+ response.close();
+ synchronized (this) {
+ final String newAccessToken = accessTokenRepository.getAccessToken();
+ // Access token is refreshed in another thread
+ if (!accessToken.equals(newAccessToken)) {
+ return chain.proceed(newRequestWithAccessToken(request,
newAccessToken));
+ }
+
+ // Need to refresh access token
+ final String updatedAccessToken;
+ updatedAccessToken = accessTokenRepository.refreshAccessToken();
+ // Retry the request
+ return chain.proceed(newRequestWithAccessToken(request,
updatedAccessToken));
+ }
+ }
+ return response;
+ }
+
+ @NonNull
+ private Request newRequestWithAccessToken(@NonNull Request request, @NonNull
String accessToken) {
+ logger.debug("Interceptor making new request with access token: {}",
request.url());
+ String tokenType = accessTokenRepository.getTokenType();
+ if (StringUtils.isNotEmpty(tokenType)) {
+ accessToken = tokenType + " " + accessToken;
+ }
+
+ if (accessTokenRepository.getOAuthConfig().isAccessTokenInHeader()) {
+ HttpUrl rawUrl = HttpUrl.parse(request.url().toString());
+ rawUrl.newBuilder().addQueryParameter("access_token", accessToken);
+ return request.newBuilder().url(rawUrl.url()).build();
+ } else {
+ return request.newBuilder().header("Authorization", accessToken).build();
+ }
+ }
+}
diff --git
a/contrib/storage-http/src/main/java/org/apache/drill/exec/store/http/oauth/AccessTokenRepository.java
b/contrib/storage-http/src/main/java/org/apache/drill/exec/store/http/oauth/AccessTokenRepository.java
new file mode 100644
index 0000000..3c7b06c
--- /dev/null
+++
b/contrib/storage-http/src/main/java/org/apache/drill/exec/store/http/oauth/AccessTokenRepository.java
@@ -0,0 +1,154 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.drill.exec.store.http.oauth;
+
+import okhttp3.OkHttpClient.Builder;
+import okhttp3.OkHttpClient;
+import okhttp3.Request;
+
+import org.apache.drill.common.exceptions.UserException;
+import org.apache.drill.common.logical.security.CredentialsProvider;
+import org.apache.drill.exec.oauth.PersistentTokenTable;
+import org.apache.drill.exec.store.http.HttpOAuthConfig;
+import org.apache.drill.exec.store.http.HttpStoragePluginConfig;
+import org.apache.drill.exec.store.http.util.HttpProxyConfig;
+import org.apache.drill.exec.store.http.util.SimpleHttp;
+import org.apache.drill.exec.store.security.oauth.OAuthTokenCredentials;
+import org.apache.parquet.Strings;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.Map;
+
+
+public class AccessTokenRepository {
+
+ private static final Logger logger =
LoggerFactory.getLogger(AccessTokenRepository.class);
+
+ private final OkHttpClient client;
+ private final OAuthTokenCredentials credentials;
+ private final CredentialsProvider credentialsProvider;
+ private HttpStoragePluginConfig pluginConfig;
+ private PersistentTokenTable tokenTable;
+ private String accessToken;
+ private String refreshToken;
+
+ public AccessTokenRepository(HttpProxyConfig proxyConfig,
+ HttpStoragePluginConfig pluginConfig,
+ PersistentTokenTable tokenTable) {
+ Builder builder = new OkHttpClient.Builder();
+ this.tokenTable = tokenTable;
+ this.pluginConfig = pluginConfig;
+ this.credentialsProvider = pluginConfig.getCredentialsProvider();
+ accessToken = tokenTable.getAccessToken();
+ refreshToken = tokenTable.getRefreshToken();
+
+ this.credentials = new OAuthTokenCredentials(credentialsProvider,
tokenTable);
+
+ // Add proxy info
+ SimpleHttp.addProxyInfo(builder, proxyConfig);
+ client = builder.build();
+ }
+
+ public HttpOAuthConfig getOAuthConfig() {
+ return pluginConfig.oAuthConfig();
+ }
+
+ public String getTokenType() {
+ return pluginConfig.oAuthConfig().getTokenType();
+ }
+
+ /**
+ * Returns the current access token. Does not perform an HTTP request.
+ * @return The current access token.
+ */
+ public String getAccessToken() {
+ logger.debug("Getting Access token");
+ if (accessToken == null) {
+ return refreshAccessToken();
+ }
+ return accessToken;
+ }
+
+ /**
+ * Refreshes the access token using the code and other information from the
HTTP OAuthConfig.
+ * This executes a POST request. This method will throw exceptions if any
of the required fields
+ * are empty. This plugin also updates the configuration in the storage
plugin registry.
+ *
+ * In the event that a user submits a request and the access token is
expired, the API will
+ * return a 401 non-authorized response. In the event of a 401 response,
the AccessTokenAuthenticator will
+ * create additional calls to obtain an updated token. This process should
be transparent to the user.
+ *
+ * @return String of the new access token.
+ */
+ public String refreshAccessToken() {
+ Request request;
+ logger.debug("Refreshing Access Token.");
+ validateKeys();
+
+ // If the refresh token is present process with that
+ if (! Strings.isNullOrEmpty(refreshToken)) {
+ request =
OAuthUtils.getAccessTokenRequestFromRefreshToken(pluginConfig.getCredentialsProvider(),
refreshToken);
+ } else {
+ throw UserException.connectionError()
+ .message("Your connection expired. Please refresh your access token in
the Drill configuration.")
+ .build(logger);
+ }
+
+ // Update/Refresh the tokens
+ Map<String, String> updatedTokens = OAuthUtils.getOAuthTokens(client,
request);
+
tokenTable.setAccessToken(updatedTokens.get(OAuthTokenCredentials.ACCESS_TOKEN));
+
+ // If we get a new refresh token, update it as well
+ if (updatedTokens.containsKey(OAuthTokenCredentials.REFRESH_TOKEN)) {
+
tokenTable.setRefreshToken(updatedTokens.get(OAuthTokenCredentials.REFRESH_TOKEN));
+ refreshToken = updatedTokens.get(OAuthTokenCredentials.REFRESH_TOKEN);
+ }
+
+ if (updatedTokens.containsKey("accessToken")) {
+ accessToken = updatedTokens.get("accessToken");
+ }
+
+ return accessToken;
+ }
+
+ /**
+ * Validate the key parts of the OAuth request and throw helpful error
messages
+ * if anything is missing.
+ */
+ private void validateKeys() {
+ if (Strings.isNullOrEmpty(credentials.getClientID())) {
+ throw UserException.validationError()
+ .message("The client ID field is missing in your OAuth configuration.")
+ .build(logger);
+ }
+
+ if (Strings.isNullOrEmpty(credentials.getClientSecret())) {
+ throw UserException.validationError()
+ .message("The client secret field is missing in your OAuth
configuration.")
+ .build(logger);
+ }
+
+ if (Strings.isNullOrEmpty(credentials.getTokenUri())) {
+ throw UserException.validationError()
+ .message("The access token path field is missing in your OAuth
configuration.")
+ .build(logger);
+ }
+ }
+}
diff --git
a/contrib/storage-http/src/main/java/org/apache/drill/exec/store/http/util/SimpleHttp.java
b/contrib/storage-http/src/main/java/org/apache/drill/exec/store/http/util/SimpleHttp.java
index 2f9f8b8..b27a3c5 100644
---
a/contrib/storage-http/src/main/java/org/apache/drill/exec/store/http/util/SimpleHttp.java
+++
b/contrib/storage-http/src/main/java/org/apache/drill/exec/store/http/util/SimpleHttp.java
@@ -32,11 +32,17 @@ import org.apache.commons.lang3.StringUtils;
import org.apache.drill.common.map.CaseInsensitiveMap;
import org.apache.drill.common.exceptions.CustomErrorContext;
import org.apache.drill.common.exceptions.UserException;
+import org.apache.drill.exec.oauth.PersistentTokenTable;
+import org.apache.drill.exec.store.StoragePluginRegistry;
import org.apache.drill.exec.store.http.HttpApiConfig;
import org.apache.drill.exec.store.http.HttpApiConfig.HttpMethod;
+import org.apache.drill.exec.store.http.HttpOAuthConfig;
import org.apache.drill.exec.store.http.HttpStoragePluginConfig;
import org.apache.drill.exec.store.http.HttpSubScan;
import org.apache.drill.exec.store.http.paginator.Paginator;
+import org.apache.drill.exec.store.http.oauth.AccessTokenAuthenticator;
+import org.apache.drill.exec.store.http.oauth.AccessTokenInterceptor;
+import org.apache.drill.exec.store.http.oauth.AccessTokenRepository;
import org.apache.drill.exec.store.security.UsernamePasswordCredentials;
import org.jetbrains.annotations.NotNull;
@@ -80,6 +86,8 @@ public class SimpleHttp {
private final CustomErrorContext errorContext;
private final Paginator paginator;
private final HttpUrl url;
+ private final StoragePluginRegistry registry;
+ private final PersistentTokenTable tokenTable;
private String responseMessage;
private int responseCode;
private String responseProtocol;
@@ -94,8 +102,10 @@ public class SimpleHttp {
this.tempDir = tempDir;
this.proxyConfig = proxyConfig;
this.errorContext = errorContext;
- this.client = setupHttpClient();
+ this.registry = scanDefn.tableSpec().getRegistry();
+ this.tokenTable = scanDefn.tableSpec().getTokenTable();
this.paginator = paginator;
+ this.client = setupHttpClient();
}
/**
@@ -113,10 +123,19 @@ public class SimpleHttp {
if (config.cacheResults()) {
setupCache(builder);
}
-
- // If the API uses basic authentication add the authentication code.
HttpApiConfig apiConfig = scanDefn.tableSpec().connectionConfig();
- if (apiConfig.authType().equalsIgnoreCase("basic")) {
+ // If OAuth information is provided, we will assume that the user does not
want to use
+ // basic authentication
+ HttpOAuthConfig oAuthConfig = scanDefn.tableSpec().config().oAuthConfig();
+ if (oAuthConfig != null) {
+ // Add interceptors for OAuth2
+ logger.debug("Adding OAuth2 Interceptor");
+ AccessTokenRepository repository = new
AccessTokenRepository(proxyConfig, config, tokenTable);
+
+ builder.authenticator(new AccessTokenAuthenticator(repository));
+ builder.addInterceptor(new AccessTokenInterceptor(repository));
+ } else if (apiConfig.authType().equalsIgnoreCase("basic")) {
+ // If the API uses basic authentication add the authentication code.
logger.debug("Adding Interceptor");
UsernamePasswordCredentials credentials =
apiConfig.getUsernamePasswordCredentials();
builder.addInterceptor(new
BasicAuthInterceptor(credentials.getUsername(), credentials.getPassword()));
@@ -130,7 +149,7 @@ public class SimpleHttp {
// Code to skip SSL Certificate validation
// Sourced from
https://stackoverflow.com/questions/60110848/how-to-disable-ssl-verification
- if (! scanDefn.tableSpec().connectionConfig().verifySSLCert()) {
+ if (! apiConfig.verifySSLCert()) {
try {
TrustManager[] trustAllCerts = getAllTrustingTrustManager();
SSLContext sslContext = SSLContext.getInstance("SSL");
@@ -148,6 +167,25 @@ public class SimpleHttp {
}
// Set the proxy configuration
+ addProxyInfo(builder, proxyConfig);
+
+ return builder.build();
+ }
+
+ public String url() {
+ return url.toString();
+ }
+
+ /**
+ * Applies the proxy configuration to the OkHttp3 builder. This ensures
that proxy configurations
+ * will be consistent across HTTP REST connections.
+ * @param builder The input OkHttp3 builder
+ * @param proxyConfig The proxy configuration
+ */
+ public static void addProxyInfo(Builder builder, HttpProxyConfig
proxyConfig) {
+ if (proxyConfig == null) {
+ return;
+ }
Proxy.Type proxyType;
switch (proxyConfig.type) {
@@ -172,12 +210,6 @@ public class SimpleHttp {
});
}
}
-
- return builder.build();
- }
-
- public String url() {
- return url.toString();
}
private TrustManager[] getAllTrustingTrustManager() {
@@ -200,6 +232,11 @@ public class SimpleHttp {
}
+ /**
+ * Returns an InputStream based on the URL and config in the scanSpec. If
anything goes wrong
+ * the method throws a UserException.
+ * @return An Inputstream of the data from the URL call.
+ */
public InputStream getInputStream() {
Request.Builder requestBuilder = new Request.Builder()
@@ -229,6 +266,9 @@ public class SimpleHttp {
Request request = requestBuilder.build();
try {
+ logger.debug("Executing request: {}", request);
+ logger.debug("Headers: {}", request.headers());
+
// Execute the request
Response response = client
.newCall(request)
diff --git
a/contrib/storage-http/src/test/java/org/apache/drill/exec/store/http/TestHttpPlugin.java
b/contrib/storage-http/src/test/java/org/apache/drill/exec/store/http/TestHttpPlugin.java
index 64305f2..bde9cca 100644
---
a/contrib/storage-http/src/test/java/org/apache/drill/exec/store/http/TestHttpPlugin.java
+++
b/contrib/storage-http/src/test/java/org/apache/drill/exec/store/http/TestHttpPlugin.java
@@ -128,7 +128,7 @@ public class TestHttpPlugin extends ClusterTest {
configs.put("pokemon", pokemonConfig);
HttpStoragePluginConfig mockStorageConfigWithWorkspace =
- new HttpStoragePluginConfig(false, configs, 10, "", 80, "", "", "",
PlainCredentialsProvider.EMPTY_CREDENTIALS_PROVIDER);
+ new HttpStoragePluginConfig(false, configs, 10, "", 80, "", "", "",
null, PlainCredentialsProvider.EMPTY_CREDENTIALS_PROVIDER);
mockStorageConfigWithWorkspace.setEnabled(true);
cluster.defineStoragePlugin("live", mockStorageConfigWithWorkspace);
}
@@ -290,7 +290,7 @@ public class TestHttpPlugin extends ClusterTest {
configs.put("mockJsonAllText", mockTableWithJsonOptions);
HttpStoragePluginConfig mockStorageConfigWithWorkspace =
- new HttpStoragePluginConfig(false, configs, 2, "", 80, "", "", "",
PlainCredentialsProvider.EMPTY_CREDENTIALS_PROVIDER);
+ new HttpStoragePluginConfig(false, configs, 2, "", 80, "", "", "",
null, PlainCredentialsProvider.EMPTY_CREDENTIALS_PROVIDER);
mockStorageConfigWithWorkspace.setEnabled(true);
cluster.defineStoragePlugin("local", mockStorageConfigWithWorkspace);
}
@@ -1223,7 +1223,7 @@ public class TestHttpPlugin extends ClusterTest {
* @return Started Mock server
* @throws IOException If the server cannot start, throws IOException
*/
- private MockWebServer startServer() throws IOException {
+ public static MockWebServer startServer() throws IOException {
MockWebServer server = new MockWebServer();
server.start(MOCK_SERVER_PORT);
return server;
diff --git
a/contrib/storage-http/src/test/java/org/apache/drill/exec/store/http/TestOAuthProcess.java
b/contrib/storage-http/src/test/java/org/apache/drill/exec/store/http/TestOAuthProcess.java
new file mode 100644
index 0000000..ce1edec
--- /dev/null
+++
b/contrib/storage-http/src/test/java/org/apache/drill/exec/store/http/TestOAuthProcess.java
@@ -0,0 +1,254 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.drill.exec.store.http;
+
+import okhttp3.OkHttpClient;
+import okhttp3.Request;
+import okhttp3.Response;
+import okhttp3.mockwebserver.MockResponse;
+import okhttp3.mockwebserver.MockWebServer;
+import org.apache.drill.common.logical.security.CredentialsProvider;
+import org.apache.drill.common.logical.security.PlainCredentialsProvider;
+import org.apache.drill.common.types.TypeProtos.DataMode;
+import org.apache.drill.common.types.TypeProtos.MinorType;
+import org.apache.drill.common.util.DrillFileUtils;
+import org.apache.drill.exec.ExecConstants;
+import org.apache.drill.exec.oauth.PersistentTokenTable;
+import org.apache.drill.exec.physical.rowSet.DirectRowSet;
+import org.apache.drill.exec.physical.rowSet.RowSet;
+import org.apache.drill.exec.physical.rowSet.RowSetBuilder;
+import org.apache.drill.exec.record.metadata.SchemaBuilder;
+import org.apache.drill.exec.record.metadata.TupleMetadata;
+import org.apache.drill.exec.store.security.oauth.OAuthTokenCredentials;
+import org.apache.drill.shaded.guava.com.google.common.base.Charsets;
+import org.apache.drill.shaded.guava.com.google.common.io.Files;
+import org.apache.drill.test.ClusterFixtureBuilder;
+import org.apache.drill.test.ClusterTest;
+import org.apache.drill.test.rowSet.RowSetUtilities;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.concurrent.TimeUnit;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.fail;
+
+public class TestOAuthProcess extends ClusterTest {
+
+ private static final Logger logger =
LoggerFactory.getLogger(TestOAuthProcess.class);
+ private static final int MOCK_SERVER_PORT = 47770;
+
+ private static final int TIMEOUT = 30;
+ private static final String CONNECTION_NAME = "localOauth";
+ private final OkHttpClient httpClient = new OkHttpClient.Builder()
+ .connectTimeout(TIMEOUT, TimeUnit.SECONDS)
+ .writeTimeout(TIMEOUT, TimeUnit.SECONDS)
+ .readTimeout(TIMEOUT, TimeUnit.SECONDS).build();
+
+ private static String ACCESS_TOKEN_RESPONSE;
+ private static String REFRESH_TOKEN_RESPONSE;
+ private static String TEST_JSON_RESPONSE_WITH_DATATYPES;
+ private static String hostname;
+
+ @BeforeClass
+ public static void setup() throws Exception {
+ ACCESS_TOKEN_RESPONSE =
Files.asCharSource(DrillFileUtils.getResourceAsFile("/data/oauth_access_token_response.json"),
Charsets.UTF_8).read();
+ REFRESH_TOKEN_RESPONSE =
Files.asCharSource(DrillFileUtils.getResourceAsFile("/data/token_refresh.json"),
Charsets.UTF_8).read();
+ TEST_JSON_RESPONSE_WITH_DATATYPES =
Files.asCharSource(DrillFileUtils.getResourceAsFile("/data/response2.json"),
Charsets.UTF_8).read();
+
+ ClusterFixtureBuilder builder = new ClusterFixtureBuilder(dirTestWatcher)
+ .configProperty(ExecConstants.HTTP_ENABLE, true)
+ .configProperty(ExecConstants.HTTP_PORT_HUNT, true);
+ startCluster(builder);
+ int portNumber = cluster.drillbit().getWebServerPort();
+ hostname = "http://localhost:" + portNumber + "/storage/" +
CONNECTION_NAME;
+
+ Map<String, String> creds = new HashMap<>();
+ creds.put("clientID", "12345");
+ creds.put("clientSecret", "54321");
+ creds.put("accessToken", null);
+ creds.put("refreshToken", null);
+ creds.put(OAuthTokenCredentials.TOKEN_URI, "http://localhost:" +
MOCK_SERVER_PORT + "/get_access_token");
+
+ CredentialsProvider credentialsProvider = new
PlainCredentialsProvider(creds);
+
+ HttpApiConfig connectionConfig = HttpApiConfig.builder()
+ .url("http://localhost:" + MOCK_SERVER_PORT + "/getdata")
+ .method("get")
+ .requireTail(false)
+ .inputType("json")
+ .build();
+
+ HttpOAuthConfig oAuthConfig = HttpOAuthConfig.builder()
+ .callbackURL(hostname + "/update_oath2_authtoken")
+ .build();
+
+ Map<String, HttpApiConfig> configs = new HashMap<>();
+ configs.put("test", connectionConfig);
+
+ // Add storage plugin for test OAuth
+ HttpStoragePluginConfig mockStorageConfigWithWorkspace =
+ new HttpStoragePluginConfig(false, configs, TIMEOUT, "", 80, "", "", "",
+ oAuthConfig, credentialsProvider);
+ mockStorageConfigWithWorkspace.setEnabled(true);
+ cluster.defineStoragePlugin("localOauth", mockStorageConfigWithWorkspace);
+ }
+
+ @Test
+ public void testAccessToken() {
+ String url = hostname + "/update_oath2_authtoken?code=ABCDEF";
+ Request request = new Request.Builder().url(url).build();
+
+ try (MockWebServer server = startServer()) {
+ server.enqueue(new
MockResponse().setResponseCode(200).setBody(ACCESS_TOKEN_RESPONSE));
+ Response response = httpClient.newCall(request).execute();
+
+ // Verify that the request succeeded w/o error
+ assertEquals(200, response.code());
+
+ // Verify that the access and refresh tokens were saved
+ PersistentTokenTable tokenTable = ((HttpStoragePlugin) cluster
+ .storageRegistry()
+ .getPlugin("localOauth"))
+ .getTokenTable();
+
+ assertEquals("you_have_access", tokenTable.getAccessToken());
+ assertEquals("refresh_me", tokenTable.getRefreshToken());
+
+ } catch (Exception e) {
+ logger.debug(e.getMessage());
+ fail();
+ }
+ }
+
+ @Test
+ public void testGetDataWithAuthentication() {
+ String url = hostname + "/update_oath2_authtoken?code=ABCDEF";
+ Request request = new Request.Builder().url(url).build();
+ try (MockWebServer server = startServer()) {
+ server.enqueue(new
MockResponse().setResponseCode(200).setBody(ACCESS_TOKEN_RESPONSE));
+ Response response = httpClient.newCall(request).execute();
+
+ // Verify that the request succeeded w/o error
+ assertEquals(200, response.code());
+
+ // Verify that the access and refresh tokens were saved
+ PersistentTokenTable tokenTable = ((HttpStoragePlugin)
cluster.storageRegistry()
+ .getPlugin("localOauth"))
+ .getTokenRegistry()
+ .getTokenTable("localOauth");
+
+ assertEquals("you_have_access", tokenTable.getAccessToken());
+ assertEquals("refresh_me", tokenTable.getRefreshToken());
+ // Now execute a query and get query results.
+ server.enqueue(new MockResponse()
+ .setResponseCode(200)
+ .setBody(TEST_JSON_RESPONSE_WITH_DATATYPES));
+
+ String sql = "SELECT * FROM localOauth.test";
+ DirectRowSet results = queryBuilder().sql(sql).rowSet();
+
+ TupleMetadata expectedSchema = new SchemaBuilder()
+ .add("col_1", MinorType.FLOAT8, DataMode.OPTIONAL)
+ .add("col_2", MinorType.BIGINT, DataMode.OPTIONAL)
+ .add("col_3", MinorType.VARCHAR, DataMode.OPTIONAL)
+ .build();
+
+ RowSet expected = new RowSetBuilder(client.allocator(), expectedSchema)
+ .addRow(1.0, 2, "3.0")
+ .addRow(4.0, 5, "6.0")
+ .build();
+
+ RowSetUtilities.verify(expected, results);
+
+ } catch (Exception e) {
+ logger.debug(e.getMessage());
+ fail();
+ }
+ }
+
+ @Test
+ public void testGetDataWithTokenRefresh() {
+ String url = hostname + "/update_oath2_authtoken?code=ABCDEF";
+ Request request = new Request.Builder().url(url).build();
+ try (MockWebServer server = startServer()) {
+ server.enqueue(new
MockResponse().setResponseCode(200).setBody(ACCESS_TOKEN_RESPONSE));
+ Response response = httpClient.newCall(request).execute();
+
+ // Verify that the request succeeded w/o error
+ assertEquals(200, response.code());
+
+ // Verify that the access and refresh tokens were saved
+ PersistentTokenTable tokenTable = ((HttpStoragePlugin)
cluster.storageRegistry().getPlugin("localOauth")).getTokenRegistry().getTokenTable("localOauth");
+
+ assertEquals("you_have_access", tokenTable.getAccessToken());
+ assertEquals("refresh_me", tokenTable.getRefreshToken());
+
+ // Now execute a query and get a refresh token
+ // The API should return a 401 error. This should trigger Drill to
automatically
+ // fire off a second call with the refresh token and then a third
request with the
+ // new access token to obtain the actual data.
+ server.enqueue(new MockResponse().setResponseCode(401).setBody("Access
Denied"));
+ server.enqueue(new
MockResponse().setResponseCode(200).setBody(REFRESH_TOKEN_RESPONSE));
+ server.enqueue(new MockResponse()
+ .setResponseCode(200)
+ .setBody(TEST_JSON_RESPONSE_WITH_DATATYPES));
+
+ String sql = "SELECT * FROM localOauth.test";
+ DirectRowSet results = queryBuilder().sql(sql).rowSet();
+
+ // Verify that the access and refresh tokens were saved
+ assertEquals("token 2.0", tokenTable.getAccessToken());
+ assertEquals("refresh 2.0", tokenTable.getRefreshToken());
+
+ TupleMetadata expectedSchema = new SchemaBuilder()
+ .add("col_1", MinorType.FLOAT8, DataMode.OPTIONAL)
+ .add("col_2", MinorType.BIGINT, DataMode.OPTIONAL)
+ .add("col_3", MinorType.VARCHAR, DataMode.OPTIONAL)
+ .build();
+
+ RowSet expected = new RowSetBuilder(client.allocator(), expectedSchema)
+ .addRow(1.0, 2, "3.0")
+ .addRow(4.0, 5, "6.0")
+ .build();
+
+ RowSetUtilities.verify(expected, results);
+
+ } catch (Exception e) {
+ logger.debug(e.getMessage());
+ fail();
+ }
+ }
+
+ /**
+ * Helper function to start the MockHTTPServer
+ * @return Started Mock server
+ * @throws IOException If the server cannot start, throws IOException
+ */
+ public static MockWebServer startServer () throws IOException {
+ MockWebServer server = new MockWebServer();
+ server.start(MOCK_SERVER_PORT);
+ return server;
+ }
+}
diff --git
a/contrib/storage-http/src/test/java/org/apache/drill/exec/store/http/TestPagination.java
b/contrib/storage-http/src/test/java/org/apache/drill/exec/store/http/TestPagination.java
index 2ee6ba5..d5ad618 100644
---
a/contrib/storage-http/src/test/java/org/apache/drill/exec/store/http/TestPagination.java
+++
b/contrib/storage-http/src/test/java/org/apache/drill/exec/store/http/TestPagination.java
@@ -113,7 +113,7 @@ public class TestPagination extends ClusterTest {
configs.put("github", githubConfig);
HttpStoragePluginConfig mockStorageConfigWithWorkspace =
- new HttpStoragePluginConfig(false, configs, 10, "", 80, "", "", "",
PlainCredentialsProvider.EMPTY_CREDENTIALS_PROVIDER);
+ new HttpStoragePluginConfig(false, configs, 10, "", 80, "", "", "",
null, PlainCredentialsProvider.EMPTY_CREDENTIALS_PROVIDER);
mockStorageConfigWithWorkspace.setEnabled(true);
cluster.defineStoragePlugin("live", mockStorageConfigWithWorkspace);
}
@@ -193,7 +193,7 @@ public class TestPagination extends ClusterTest {
configs.put("xml_paginator_url_params",
mockXmlConfigWithPaginatorAndUrlParams);
HttpStoragePluginConfig mockStorageConfigWithWorkspace =
- new HttpStoragePluginConfig(false, configs, 2, "", 80, "", "", "",
PlainCredentialsProvider.EMPTY_CREDENTIALS_PROVIDER);
+ new HttpStoragePluginConfig(false, configs, 2, "", 80, "", "", "", null,
PlainCredentialsProvider.EMPTY_CREDENTIALS_PROVIDER);
mockStorageConfigWithWorkspace.setEnabled(true);
cluster.defineStoragePlugin("local", mockStorageConfigWithWorkspace);
}
diff --git a/contrib/storage-http/src/test/resources/data/oauth-1.json
b/contrib/storage-http/src/test/resources/data/oauth-1.json
new file mode 100644
index 0000000..336311a
--- /dev/null
+++ b/contrib/storage-http/src/test/resources/data/oauth-1.json
@@ -0,0 +1,3 @@
+{
+ "access_token": "123456789"
+}
diff --git a/contrib/storage-http/src/test/resources/data/oauth-2.json
b/contrib/storage-http/src/test/resources/data/oauth-2.json
new file mode 100644
index 0000000..4a29cd6
--- /dev/null
+++ b/contrib/storage-http/src/test/resources/data/oauth-2.json
@@ -0,0 +1,3 @@
+{
+ "access_token": "987654321"
+}
diff --git
a/contrib/storage-http/src/test/resources/data/oauth_access_token_response.json
b/contrib/storage-http/src/test/resources/data/oauth_access_token_response.json
new file mode 100644
index 0000000..43e5616
--- /dev/null
+++
b/contrib/storage-http/src/test/resources/data/oauth_access_token_response.json
@@ -0,0 +1,7 @@
+{
+ "access_token":"you_have_access",
+ "token_type":"Bearer",
+ "expires_in":3600,
+ "refresh_token":"refresh_me",
+ "scope":"create"
+}
diff --git a/contrib/storage-http/src/test/resources/data/token_refresh.json
b/contrib/storage-http/src/test/resources/data/token_refresh.json
new file mode 100644
index 0000000..78bb84d
--- /dev/null
+++ b/contrib/storage-http/src/test/resources/data/token_refresh.json
@@ -0,0 +1,6 @@
+{
+ "access_token": "token 2.0",
+ "refresh_token": "refresh 2.0",
+ "token_type": "Bearer",
+ "expires": 3600
+}
diff --git a/exec/java-exec/pom.xml b/exec/java-exec/pom.xml
index 852e362..bea536c 100644
--- a/exec/java-exec/pom.xml
+++ b/exec/java-exec/pom.xml
@@ -32,7 +32,7 @@
<libpam4j.version>1.8-rev2</libpam4j.version>
<aether.version>1.1.0</aether.version>
<wagon.version>3.3.4</wagon.version>
- <okhttp.version>4.5.0</okhttp.version>
+ <okhttp.version>4.9.3</okhttp.version>
</properties>
<dependencies>
@@ -383,6 +383,11 @@
<version>3.2</version>
</dependency>
<dependency>
+ <groupId>com.squareup.okhttp3</groupId>
+ <artifactId>okhttp</artifactId>
+ <version>${okhttp.version}</version>
+ </dependency>
+ <dependency>
<groupId>org.apache.hadoop</groupId>
<artifactId>hadoop-common</artifactId>
<exclusions>
@@ -642,12 +647,6 @@
<scope>test</scope>
</dependency>
<dependency>
- <groupId>com.squareup.okhttp3</groupId>
- <artifactId>okhttp</artifactId>
- <version>${okhttp.version}</version>
- <scope>test</scope>
- </dependency>
- <dependency>
<groupId>org.testcontainers</groupId>
<artifactId>vault</artifactId>
<version>${testcontainers.version}</version>
diff --git
a/exec/java-exec/src/main/java/org/apache/drill/exec/oauth/OAuthTokenProvider.java
b/exec/java-exec/src/main/java/org/apache/drill/exec/oauth/OAuthTokenProvider.java
new file mode 100644
index 0000000..c6a2917
--- /dev/null
+++
b/exec/java-exec/src/main/java/org/apache/drill/exec/oauth/OAuthTokenProvider.java
@@ -0,0 +1,55 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.drill.exec.oauth;
+
+import org.apache.drill.common.AutoCloseables;
+import org.apache.drill.exec.server.DrillbitContext;
+
+/**
+ * Class for managing oauth tokens. Storage plugins will have to manage
obtaining the plugins, but
+ * these classes handle the storage of access and refresh tokens.
+ */
+public class OAuthTokenProvider implements AutoCloseable {
+ private static final String STORAGE_REGISTRY_PATH = "oauth_tokens";
+
+ private final DrillbitContext context;
+
+ private PersistentTokenRegistry oauthTokenRegistry;
+
+ public OAuthTokenProvider(DrillbitContext context) {
+ this.context = context;
+ }
+
+ public TokenRegistry getOauthTokenRegistry() {
+ if (oauthTokenRegistry == null) {
+ initRemoteRegistries();
+ }
+ return oauthTokenRegistry;
+ }
+
+ private synchronized void initRemoteRegistries() {
+ if (oauthTokenRegistry == null) {
+ oauthTokenRegistry = new PersistentTokenRegistry(context,
STORAGE_REGISTRY_PATH);
+ }
+ }
+
+ @Override
+ public void close() throws Exception {
+ AutoCloseables.closeSilently(oauthTokenRegistry);
+ }
+}
diff --git
a/exec/java-exec/src/main/java/org/apache/drill/exec/oauth/PersistentTokenRegistry.java
b/exec/java-exec/src/main/java/org/apache/drill/exec/oauth/PersistentTokenRegistry.java
new file mode 100644
index 0000000..f1d2bb7
--- /dev/null
+++
b/exec/java-exec/src/main/java/org/apache/drill/exec/oauth/PersistentTokenRegistry.java
@@ -0,0 +1,113 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.drill.exec.oauth;
+
+import com.fasterxml.jackson.databind.InjectableValues;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.apache.drill.common.exceptions.DrillRuntimeException;
+import org.apache.drill.exec.exception.StoreException;
+import org.apache.drill.exec.server.DrillbitContext;
+import org.apache.drill.exec.store.sys.PersistentStore;
+import org.apache.drill.exec.store.sys.PersistentStoreConfig;
+
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.function.Supplier;
+
+/**
+ * Implementation of {@link TokenRegistry} that persists token tables
+ * to the pre-configured persistent store.
+ */
+public class PersistentTokenRegistry implements TokenRegistry {
+ private final PersistentStore<PersistentTokenTable> store;
+
+ public PersistentTokenRegistry(DrillbitContext context, String registryPath)
{
+ try {
+ ObjectMapper mapper = context.getLpPersistence().getMapper().copy();
+ InjectableValues injectables = new InjectableValues.Std()
+ .addValue(StoreProvider.class, new StoreProvider(this::getStore));
+
+ mapper.setInjectableValues(injectables);
+ this.store = context
+ .getStoreProvider()
+ .getOrCreateStore(PersistentStoreConfig
+ .newJacksonBuilder(mapper, PersistentTokenTable.class)
+ .name(registryPath)
+ .build());
+ } catch (StoreException e) {
+ throw new DrillRuntimeException(
+ "Failure while reading and loading token table.");
+ }
+ }
+
+ public PersistentStore<PersistentTokenTable> getStore() {
+ return store;
+ }
+
+ @Override
+ public PersistentTokenTable getTokenTable(String name) {
+ name = name.toLowerCase();
+ if (!store.contains(name)) {
+ createTokenTable(name);
+ }
+ return store.get(name);
+ }
+
+ @SuppressWarnings({"unchecked", "rawtypes"})
+ public Iterator<Map.Entry<String, Tokens>> getAllTokens() {
+ return (Iterator) store.getAll();
+ }
+
+ @Override
+ public void createTokenTable(String pluginName) {
+ // In Drill, Storage plugin names are stored in lower case. These checks
make sure
+ // that the tokens are associated with the correct plugin
+ pluginName = pluginName.toLowerCase();
+ if (!store.contains(pluginName)) {
+ PersistentTokenTable tokenTable =
+ new PersistentTokenTable(new HashMap<>(), pluginName, new
StoreProvider(this::getStore));
+ store.put(pluginName, tokenTable);
+ }
+ }
+
+ @Override
+ public void deleteTokenTable(String pluginName) {
+ pluginName = pluginName.toLowerCase();
+ if (store.contains(pluginName)) {
+ store.delete(pluginName);
+ }
+ }
+
+ @Override
+ public void close() throws Exception {
+ store.close();
+ }
+
+ public static class StoreProvider {
+ private final Supplier<PersistentStore<PersistentTokenTable>> supplier;
+
+ public StoreProvider(Supplier<PersistentStore<PersistentTokenTable>>
supplier) {
+ this.supplier = supplier;
+ }
+
+ public PersistentStore<PersistentTokenTable> getStore() {
+ return supplier.get();
+ }
+ }
+}
diff --git
a/exec/java-exec/src/main/java/org/apache/drill/exec/oauth/PersistentTokenTable.java
b/exec/java-exec/src/main/java/org/apache/drill/exec/oauth/PersistentTokenTable.java
new file mode 100644
index 0000000..a586ff0
--- /dev/null
+++
b/exec/java-exec/src/main/java/org/apache/drill/exec/oauth/PersistentTokenTable.java
@@ -0,0 +1,119 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.drill.exec.oauth;
+
+import com.fasterxml.jackson.annotation.JacksonInject;
+import com.fasterxml.jackson.annotation.JsonCreator;
+
+import com.fasterxml.jackson.annotation.JsonIgnore;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import org.apache.drill.exec.store.sys.PersistentStore;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Implementation of tokens table that updates its version in persistent store
after modifications.
+ * For OAuth tokens, the only possible tokens are the access_token, the
refresh_token and authorization_code.
+ */
+public class PersistentTokenTable implements Tokens {
+ public final String ACCESS_TOKEN_KEY = "access_token";
+ public final String REFRESH_TOKEN_KEY = "refresh_token";
+
+ private final Map<String, String> tokens;
+
+ private final String key;
+
+ private final PersistentStore<PersistentTokenTable> store;
+
+ @JsonCreator
+ public PersistentTokenTable(
+ @JsonProperty("tokens") Map<String, String> tokens,
+ @JsonProperty("key") String key,
+ @JacksonInject PersistentTokenRegistry.StoreProvider storeProvider) {
+ this.tokens = tokens != null ? tokens : new HashMap<>();
+ this.key = key;
+ this.store = storeProvider.getStore();
+ }
+
+ @Override
+ @JsonProperty("key")
+ public String getKey() {
+ return key;
+ }
+
+ @Override
+ @JsonIgnore
+ public String get(String token) {
+ return tokens.get(token);
+ }
+
+ @Override
+ @JsonIgnore
+ public boolean put(String token, String value, boolean replace) {
+ if (replace || ! tokens.containsKey(token)) {
+ tokens.put(token, value);
+ store.put(key, this);
+ return true;
+ }
+ return false;
+ }
+
+ @Override
+ @JsonIgnore
+ public String getAccessToken() {
+ return get(ACCESS_TOKEN_KEY);
+ }
+
+ @Override
+ @JsonIgnore
+ public String getRefreshToken() {
+ return get(REFRESH_TOKEN_KEY);
+ }
+
+ @Override
+ @JsonIgnore
+ public void setAccessToken(String token) {
+ // Only update the access token if it is not the same as the previous token
+ if (!tokens.containsKey(ACCESS_TOKEN_KEY) ||
!token.equals(getAccessToken())) {
+ put(ACCESS_TOKEN_KEY, token, true);
+ }
+ }
+
+ @Override
+ @JsonIgnore
+ public void setRefreshToken(String token) {
+ // Only update the access token if it is not the same as the previous token
+ if (!tokens.containsKey(REFRESH_TOKEN_KEY) ||
!getAccessToken().equals(token)) {
+ put(REFRESH_TOKEN_KEY, token,true);
+ }
+ }
+
+ @Override
+ @JsonIgnore
+ public boolean remove(String token) {
+ boolean isRemoved = tokens.remove(token) != null;
+ store.put(key, this);
+ return isRemoved;
+ }
+
+ @JsonProperty("tokens")
+ public Map<String, String> getTokens() {
+ return tokens;
+ }
+}
diff --git
a/logical/src/main/java/org/apache/drill/common/logical/security/CredentialsProvider.java
b/exec/java-exec/src/main/java/org/apache/drill/exec/oauth/TokenRegistry.java
similarity index 55%
copy from
logical/src/main/java/org/apache/drill/common/logical/security/CredentialsProvider.java
copy to
exec/java-exec/src/main/java/org/apache/drill/exec/oauth/TokenRegistry.java
index 59b6ce0..a56392f 100644
---
a/logical/src/main/java/org/apache/drill/common/logical/security/CredentialsProvider.java
+++
b/exec/java-exec/src/main/java/org/apache/drill/exec/oauth/TokenRegistry.java
@@ -15,24 +15,32 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package org.apache.drill.common.logical.security;
-
-import com.fasterxml.jackson.annotation.JsonIgnore;
-import com.fasterxml.jackson.annotation.JsonTypeInfo;
+package org.apache.drill.exec.oauth;
+import java.util.Iterator;
import java.util.Map;
/**
- * Provider of authentication credentials.
+ * Persistent Registry for OAuth Tokens
*/
-@JsonTypeInfo(use = JsonTypeInfo.Id.NAME,
- property = "credentialsProviderType",
- defaultImpl = PlainCredentialsProvider.class)
-public interface CredentialsProvider {
+public interface TokenRegistry extends AutoCloseable {
+
+ /**
+ * Creates a token table for specified {@code pluginName}.
+ * @param pluginName The name of the plugin instance.
+ */
+ void createTokenTable(String pluginName);
+
+ PersistentTokenTable getTokenTable(String name);
+
+ /**
+ * Deletes aliases table for specified {@code userName}.
+ * @param pluginName name of the user whose aliases table should be removed
+ */
+ void deleteTokenTable(String pluginName);
+
/**
- * Returns map with authentication credentials. Key is the credential name,
for example {@code "username"}
- * and map value is corresponding credential value.
+ * Returns iterator for aliases table entries.
*/
- @JsonIgnore
- Map<String, String> getCredentials();
+ Iterator<Map.Entry<String, Tokens>> getAllTokens();
}
diff --git
a/exec/java-exec/src/main/java/org/apache/drill/exec/oauth/Tokens.java
b/exec/java-exec/src/main/java/org/apache/drill/exec/oauth/Tokens.java
new file mode 100644
index 0000000..574eb8a
--- /dev/null
+++ b/exec/java-exec/src/main/java/org/apache/drill/exec/oauth/Tokens.java
@@ -0,0 +1,70 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.drill.exec.oauth;
+
+public interface Tokens {
+ /**
+ * Key of {@link this} tokens table.
+ */
+ String getKey();
+
+ /**
+ * Gets the current access token.
+ *
+ * @return The current access token
+ */
+ String getAccessToken();
+
+ /**
+ * Sets the access token.
+ *
+ * @param accessToken Sets the access token.
+ */
+ void setAccessToken(String accessToken);
+
+ String getRefreshToken();
+
+ void setRefreshToken(String refreshToken);
+
+ /**
+ * Returns value from tokens table that corresponds to provided plugin.
+ *
+ * @param token token of the value to obtain
+ * @return value from token table that corresponds to provided plugin
+ */
+ String get(String token);
+
+ /**
+ * Associates provided token with provided plugin in token table.
+ *
+ * @param token Token of the value to associate with
+ * @param value Value that will be associated with provided alias
+ * @param replace Whether existing value for the same token should be
replaced
+ * @return {@code true} if provided token was associated with
+ * the provided value in tokens table
+ */
+ boolean put(String token, String value, boolean replace);
+
+ /**
+ * Removes value for specified token from tokens table.
+ * @param token token of the value to remove
+ * @return {@code true} if the value associated with
+ * provided token was removed from the tokens table.
+ */
+ boolean remove(String token);
+}
diff --git
a/exec/java-exec/src/main/java/org/apache/drill/exec/server/DrillbitContext.java
b/exec/java-exec/src/main/java/org/apache/drill/exec/server/DrillbitContext.java
index fc54511..bcc446b 100644
---
a/exec/java-exec/src/main/java/org/apache/drill/exec/server/DrillbitContext.java
+++
b/exec/java-exec/src/main/java/org/apache/drill/exec/server/DrillbitContext.java
@@ -30,6 +30,7 @@ import
org.apache.drill.exec.expr.fn.FunctionImplementationRegistry;
import org.apache.drill.exec.expr.fn.registry.RemoteFunctionRegistry;
import org.apache.drill.exec.memory.BufferAllocator;
import org.apache.drill.exec.metrics.DrillCounters;
+import org.apache.drill.exec.oauth.OAuthTokenProvider;
import org.apache.drill.exec.physical.impl.OperatorCreatorRegistry;
import org.apache.drill.exec.planner.PhysicalPlanReader;
import org.apache.drill.exec.planner.sql.DrillOperatorTable;
@@ -65,6 +66,7 @@ public class DrillbitContext implements AutoCloseable {
private final DrillbitEndpoint endpoint;
private final StoragePluginRegistry storagePlugins;
private final AliasRegistryProvider aliasRegistryProvider;
+ private final OAuthTokenProvider oAuthTokenProvider;
private final OperatorCreatorRegistry operatorCreatorRegistry;
private final Controller controller;
private final WorkEventBus workBus;
@@ -129,6 +131,7 @@ public class DrillbitContext implements AutoCloseable {
profileStoreContext = new QueryProfileStoreContext(config,
profileStoreProvider, coord);
this.metastoreRegistry = new MetastoreRegistry(config);
this.aliasRegistryProvider = new AliasRegistryProvider(this);
+ this.oAuthTokenProvider = new OAuthTokenProvider(this);
this.counters = DrillCounters.getInstance();
}
@@ -186,7 +189,7 @@ public class DrillbitContext implements AutoCloseable {
public boolean isForeman(DrillbitEndpoint endpoint) {
DrillbitEndpoint foreman = getEndpoint();
- if(endpoint.getAddress().equals(foreman.getAddress()) &&
+ if (endpoint.getAddress().equals(foreman.getAddress()) &&
endpoint.getUserPort() == foreman.getUserPort()) {
return true;
}
@@ -221,6 +224,8 @@ public class DrillbitContext implements AutoCloseable {
return aliasRegistryProvider;
}
+ public OAuthTokenProvider getoAuthTokenProvider() { return
oAuthTokenProvider; }
+
public EventLoopGroup getBitLoopGroup() {
return context.getBitLoopGroup();
}
@@ -312,6 +317,7 @@ public class DrillbitContext implements AutoCloseable {
getCompiler().close();
getMetastoreRegistry().close();
getAliasRegistryProvider().close();
+ getoAuthTokenProvider().close();
}
public ResourceManager getResourceManager() {
diff --git
a/exec/java-exec/src/main/java/org/apache/drill/exec/server/rest/PluginConfigWrapper.java
b/exec/java-exec/src/main/java/org/apache/drill/exec/server/rest/PluginConfigWrapper.java
index 30c46b1..397b264 100644
---
a/exec/java-exec/src/main/java/org/apache/drill/exec/server/rest/PluginConfigWrapper.java
+++
b/exec/java-exec/src/main/java/org/apache/drill/exec/server/rest/PluginConfigWrapper.java
@@ -19,12 +19,16 @@ package org.apache.drill.exec.server.rest;
import javax.xml.bind.annotation.XmlRootElement;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.drill.common.logical.AbstractSecuredStoragePluginConfig;
import org.apache.drill.common.logical.StoragePluginConfig;
+import org.apache.drill.common.logical.security.CredentialsProvider;
import org.apache.drill.exec.store.StoragePluginRegistry;
import org.apache.drill.exec.store.StoragePluginRegistry.PluginException;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
+import org.apache.drill.exec.store.security.oauth.OAuthTokenCredentials;
@XmlRootElement
public class PluginConfigWrapper {
@@ -50,4 +54,27 @@ public class PluginConfigWrapper {
public void createOrUpdateInStorage(StoragePluginRegistry storage) throws
PluginException {
storage.validatedPut(name, config);
}
+
+ /**
+ * Determines whether the storage plugin in question needs the OAuth button
in the UI. In
+ * order to be considered an OAuth plugin, the plugin must:
+ * 1. Use AbstractSecuredStoragePluginConfig
+ * 2. The credential provider must not be null
+ * 3. The credentialsProvider must contain a client_id and client_secret
+ * @return true if the plugin uses OAuth, false if not.
+ */
+ public boolean isOauth() {
+ if (! (config instanceof AbstractSecuredStoragePluginConfig)) {
+ return false;
+ }
+ AbstractSecuredStoragePluginConfig securedStoragePluginConfig =
(AbstractSecuredStoragePluginConfig) config;
+ CredentialsProvider credentialsProvider =
securedStoragePluginConfig.getCredentialsProvider();
+ if (credentialsProvider == null) {
+ return false;
+ }
+ OAuthTokenCredentials tokenCredentials = new
OAuthTokenCredentials(credentialsProvider);
+
+ return !StringUtils.isEmpty(tokenCredentials.getClientID()) ||
+ !StringUtils.isEmpty(tokenCredentials.getClientSecret());
+ }
}
diff --git
a/exec/java-exec/src/main/java/org/apache/drill/exec/server/rest/StorageResources.java
b/exec/java-exec/src/main/java/org/apache/drill/exec/server/rest/StorageResources.java
index dcba516..1dfdcfb 100644
---
a/exec/java-exec/src/main/java/org/apache/drill/exec/server/rest/StorageResources.java
+++
b/exec/java-exec/src/main/java/org/apache/drill/exec/server/rest/StorageResources.java
@@ -17,9 +17,15 @@
*/
package org.apache.drill.exec.server.rest;
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.nio.charset.StandardCharsets;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
+import java.util.Map;
import java.util.Spliterator;
import java.util.Spliterators;
import java.util.stream.Collectors;
@@ -36,19 +42,32 @@ import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
+import javax.ws.rs.QueryParam;
import javax.ws.rs.core.HttpHeaders;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
+import javax.ws.rs.core.Response.Status;
import javax.ws.rs.core.SecurityContext;
import javax.xml.bind.annotation.XmlRootElement;
+import okhttp3.OkHttpClient;
+import okhttp3.OkHttpClient.Builder;
+import okhttp3.Request;
import org.apache.commons.lang3.StringUtils;
+import org.apache.drill.common.logical.AbstractSecuredStoragePluginConfig;
+import org.apache.drill.common.logical.security.CredentialsProvider;
+import org.apache.drill.exec.oauth.PersistentTokenTable;
+import org.apache.drill.exec.oauth.TokenRegistry;
import org.apache.drill.exec.server.rest.DrillRestServer.UserAuthEnabled;
+import org.apache.drill.exec.store.AbstractStoragePlugin;
import org.apache.drill.exec.store.StoragePluginRegistry;
import
org.apache.drill.exec.store.StoragePluginRegistry.PluginEncodingException;
import org.apache.drill.exec.store.StoragePluginRegistry.PluginException;
import org.apache.drill.exec.store.StoragePluginRegistry.PluginFilter;
import
org.apache.drill.exec.store.StoragePluginRegistry.PluginNotFoundException;
+import org.apache.drill.exec.store.http.oauth.OAuthUtils;
+import org.apache.drill.exec.store.security.oauth.OAuthTokenCredentials;
+import org.eclipse.jetty.util.resource.Resource;
import org.glassfish.jersey.server.mvc.Viewable;
import org.slf4j.Logger;
@@ -80,6 +99,7 @@ public class StorageResources {
private static final String ALL_PLUGINS = "all";
private static final String ENABLED_PLUGINS = "enabled";
private static final String DISABLED_PLUGINS = "disabled";
+ private static final String OAUTH_SUCCESS_PAGE =
"/rest/storage/success.html";
private static final Comparator<PluginConfigWrapper> PLUGIN_COMPARATOR =
Comparator.comparing(PluginConfigWrapper::getName);
@@ -180,6 +200,64 @@ public class StorageResources {
}
}
+ @GET
+ @Path("/storage/{name}/update_oath2_authtoken")
+ @Produces(MediaType.TEXT_HTML)
+ public Response updateAuthToken(@PathParam("name") String name,
@QueryParam("code") String code) {
+ try {
+ if (storage.getPlugin(name).getConfig() instanceof
AbstractSecuredStoragePluginConfig) {
+ AbstractSecuredStoragePluginConfig securedStoragePluginConfig =
(AbstractSecuredStoragePluginConfig) storage.getPlugin(name).getConfig();
+ CredentialsProvider credentialsProvider =
securedStoragePluginConfig.getCredentialsProvider();
+ String callbackURL = this.request.getRequestURL().toString();
+
+ // Now exchange the authorization token for an access token
+ Builder builder = new OkHttpClient.Builder();
+ OkHttpClient client = builder.build();
+ Request accessTokenRequest =
OAuthUtils.getAccessTokenRequest(credentialsProvider, code, callbackURL);
+ Map<String, String> updatedTokens = OAuthUtils.getOAuthTokens(client,
accessTokenRequest);
+
+ // Add to token registry
+ TokenRegistry tokenRegistry = ((AbstractStoragePlugin)
storage.getPlugin(name))
+ .getContext()
+ .getoAuthTokenProvider()
+ .getOauthTokenRegistry();
+
+ // Add a token registry table if none exists
+ tokenRegistry.createTokenTable(name);
+ PersistentTokenTable tokenTable = tokenRegistry.getTokenTable(name);
+
+ // Add tokens to persistent storage
+
tokenTable.setAccessToken(updatedTokens.get(OAuthTokenCredentials.ACCESS_TOKEN));
+
tokenTable.setRefreshToken(updatedTokens.get(OAuthTokenCredentials.REFRESH_TOKEN));
+
+ // Get success page
+ String successPage = null;
+ try (InputStream inputStream =
Resource.newClassPathResource(OAUTH_SUCCESS_PAGE).getInputStream()) {
+ InputStreamReader reader = new InputStreamReader(inputStream,
StandardCharsets.UTF_8);
+ BufferedReader bufferedReader = new BufferedReader(reader);
+ successPage = bufferedReader.lines()
+ .collect(Collectors.joining("\n"));
+ bufferedReader.close();
+ reader.close();
+ } catch (IOException e) {
+ Response.status(Status.OK).entity("You may close this
window.").build();
+ }
+
+ return Response.status(Status.OK).entity(successPage).build();
+ } else {
+ logger.error("{} is not a HTTP plugin. You can only add auth code to
HTTP plugins.", name);
+ return Response.status(Status.INTERNAL_SERVER_ERROR)
+ .entity(message("Unable to add authorization code: %s", name))
+ .build();
+ }
+ } catch (PluginException e) {
+ logger.error("Error when adding auth token to {}", name);
+ return Response.status(Status.INTERNAL_SERVER_ERROR)
+ .entity(message("Unable to add authorization code: %s",
e.getMessage()))
+ .build();
+ }
+ }
+
/**
* @deprecated use the method with POST request {@link #enablePlugin} instead
*/
@@ -218,6 +296,14 @@ public class StorageResources {
@Produces(MediaType.APPLICATION_JSON)
public Response deletePlugin(@PathParam("name") String name) {
try {
+ TokenRegistry tokenRegistry = ((AbstractStoragePlugin)
storage.getPlugin(name))
+ .getContext()
+ .getoAuthTokenProvider()
+ .getOauthTokenRegistry();
+
+ // Delete a token registry table if it exists
+ tokenRegistry.deleteTokenTable(name);
+
storage.remove(name);
return Response.ok().entity(message("Success")).build();
} catch (PluginException e) {
@@ -368,13 +454,24 @@ public class StorageResources {
*/
public static class StoragePluginModel {
private final PluginConfigWrapper plugin;
+ private final String type;
private final String csrfToken;
public StoragePluginModel(PluginConfigWrapper plugin, HttpServletRequest
request) {
this.plugin = plugin;
+
+ if (plugin != null) {
+ this.type = plugin.getConfig().getClass().getSimpleName();
+ } else {
+ this.type = "Unknown";
+ }
csrfToken = WebUtils.getCsrfTokenFromHttpRequest(request);
}
+ public String getType() {
+ return type;
+ }
+
public PluginConfigWrapper getPlugin() {
return plugin;
}
diff --git
a/exec/java-exec/src/main/java/org/apache/drill/exec/store/http/oauth/OAuthUtils.java
b/exec/java-exec/src/main/java/org/apache/drill/exec/store/http/oauth/OAuthUtils.java
new file mode 100644
index 0000000..573df78
--- /dev/null
+++
b/exec/java-exec/src/main/java/org/apache/drill/exec/store/http/oauth/OAuthUtils.java
@@ -0,0 +1,176 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.drill.exec.store.http.oauth;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import okhttp3.FormBody;
+import okhttp3.OkHttpClient;
+import okhttp3.Request;
+import okhttp3.RequestBody;
+import okhttp3.Response;
+import org.apache.drill.common.exceptions.UserException;
+import org.apache.drill.common.logical.security.CredentialsProvider;
+import org.apache.drill.exec.store.security.oauth.OAuthTokenCredentials;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.Map;
+
+public class OAuthUtils {
+ private static final Logger logger =
LoggerFactory.getLogger(OAuthUtils.class);
+
+ /**
+ * Craft a GET request to obtain an access token.
+ * @param credentialsProvider A credential provider containing the clientID,
clientSecret and authorizationCode
+ * @param authorizationCode The authorization code from the OAuth2.0 enabled
API
+ * @param callbackURL The callback URL. For our purposes this is obtained
from the incoming Drill request as it all goes to the same place.
+ * @return A Request Body to obtain an access token
+ */
+ public static RequestBody getPostResponse(CredentialsProvider
credentialsProvider, String authorizationCode, String callbackURL) {
+ return new FormBody.Builder()
+ .add("grant_type", "authorization_code")
+ .add("client_id",
credentialsProvider.getCredentials().get(OAuthTokenCredentials.CLIENT_ID))
+ .add("client_secret",
credentialsProvider.getCredentials().get(OAuthTokenCredentials.CLIENT_SECRET))
+ .add("redirect_uri", callbackURL)
+ .add("code", authorizationCode)
+ .build();
+ }
+
+ /**
+ * Crafts a POST response for refreshing an access token when a refresh
token is present.
+ * @param credentialsProvider A credential provider containing the clientID,
clientSecret and refreshToken
+ * @param refreshToken The refresh token
+ * @return A Request Body with the correct parameters for obtaining an
access token
+ */
+ public static RequestBody getPostResponseForTokenRefresh(CredentialsProvider
credentialsProvider, String refreshToken) {
+ return new FormBody.Builder()
+ .add("grant_type", "refresh_token")
+ .add("client_id",
credentialsProvider.getCredentials().get(OAuthTokenCredentials.CLIENT_ID))
+ .add("client_secret",
credentialsProvider.getCredentials().get(OAuthTokenCredentials.CLIENT_SECRET))
+ .add("refresh_token", refreshToken)
+ .build();
+ }
+
+ /**
+ * Helper method for building the access token URL.
+ * @param credentialsProvider The credentialsProvider containing all the
OAuth pieces.
+ * @return The URL string for obtaining an Auth Code.
+ */
+ public static String buildAccessTokenURL(CredentialsProvider
credentialsProvider) {
+ return
credentialsProvider.getCredentials().get(OAuthTokenCredentials.TOKEN_URI);
+ }
+
+ /**
+ * Crafts a POST request to obtain an access token. This method should be
used for the initial call
+ * to the OAuth API when you are exchanging the authorization code for an
access token.
+ * @param credentialsProvider The credentialsProvider containing the
client_id, client_secret, and auth_code.
+ * @param authenticationCode The authentication code from the API.
+ * @return A request to obtain the access token.
+ */
+ public static Request getAccessTokenRequest(CredentialsProvider
credentialsProvider, String authenticationCode, String callbackURL) {
+ return new Request.Builder()
+ .url(buildAccessTokenURL(credentialsProvider))
+ .header("Content-Type", "application/json")
+ .addHeader("Accept", "application/json")
+ .post(getPostResponse(credentialsProvider, authenticationCode,
callbackURL))
+ .build();
+ }
+
+
+ /**
+ * Crafts a POST request to obtain an access token. This method should be
used for the additional calls
+ * to the OAuth API when you are refreshing the access token. The refresh
token must be populated for this
+ * to be successful.
+ * @param credentialsProvider The credential provider containing the
client_id, client_secret, and refresh token.
+ * @param refreshToken The OAuth2.0 refresh token
+ * @return A request to obtain the access token.
+ */
+ public static Request
getAccessTokenRequestFromRefreshToken(CredentialsProvider credentialsProvider,
String refreshToken) {
+ String tokenURI =
credentialsProvider.getCredentials().get(OAuthTokenCredentials.TOKEN_URI);
+ logger.debug("Requesting new access token with refresh token from {}",
tokenURI);
+ return new Request.Builder()
+ .url(tokenURI)
+ .header("Content-Type", "application/json")
+ .addHeader("Accept", "application/json")
+ .post(getPostResponseForTokenRefresh(credentialsProvider, refreshToken))
+ .build();
+ }
+
+ /**
+ * This function is called in after the user has obtained an OAuth
Authorization Code.
+ * It returns a map of any tokens returned which should be an access_token
and an optional
+ * refresh_token.
+ * @param client The OkHTTP3 client.
+ * @param request The finalized Request to obtain the tokens. This request
should be a POST request
+ * containing a client_id, client_secret, authorization code,
and grant type.
+ * @return a Map of any tokens returned.
+ */
+ public static Map<String, String> getOAuthTokens(OkHttpClient client,
Request request) {
+ String accessToken;
+ String refreshToken;
+ Map<String, String> tokens = new HashMap<>();
+
+ try {
+ Response response = client.newCall(request).execute();
+ String responseBody = response.body().string();
+
+ if (!response.isSuccessful()) {
+ throw UserException.connectionError()
+ .message("Error obtaining access tokens: ")
+ .addContext(response.message())
+ .addContext("Response code: " + response.code())
+ .addContext(response.body().string())
+ .build(logger);
+ }
+
+ logger.debug("Response: {}", responseBody);
+ ObjectMapper mapper = new ObjectMapper();
+ Map<String, Object> parsedJson = mapper.readValue(responseBody,
Map.class);
+
+ if (parsedJson.containsKey("access_token")) {
+ accessToken = (String) parsedJson.get("access_token");
+ tokens.put(OAuthTokenCredentials.ACCESS_TOKEN, accessToken);
+ logger.debug("Successfully added access token");
+ } else {
+ // Something went wrong here.
+ throw UserException.connectionError()
+ .message("Error obtaining access token.")
+ .addContext(parsedJson.toString())
+ .build(logger);
+ }
+
+ // Some APIs will return an access token AND a refresh token at the same
time. In that case,
+ // we will get both tokens and store them in a HashMap. The refresh
token is used when the
+ // access token expires.
+ if (parsedJson.containsKey("refresh_token")) {
+ refreshToken = (String) parsedJson.get("refresh_token");
+ tokens.put(OAuthTokenCredentials.REFRESH_TOKEN, refreshToken);
+ }
+ response.close();
+ return tokens;
+
+ } catch (NullPointerException | IOException e) {
+ throw UserException.connectionError()
+ .message("Error refreshing access OAuth2 access token. " +
e.getMessage())
+ .build(logger);
+ }
+ }
+}
diff --git
a/exec/java-exec/src/main/java/org/apache/drill/exec/store/security/CredentialProviderUtils.java
b/exec/java-exec/src/main/java/org/apache/drill/exec/store/security/CredentialProviderUtils.java
index e8ecf7c..4ad2463 100644
---
a/exec/java-exec/src/main/java/org/apache/drill/exec/store/security/CredentialProviderUtils.java
+++
b/exec/java-exec/src/main/java/org/apache/drill/exec/store/security/CredentialProviderUtils.java
@@ -19,6 +19,7 @@ package org.apache.drill.exec.store.security;
import org.apache.drill.common.logical.security.CredentialsProvider;
import org.apache.drill.common.logical.security.PlainCredentialsProvider;
+import org.apache.drill.exec.store.security.oauth.OAuthTokenCredentials;
import org.apache.drill.shaded.guava.com.google.common.collect.ImmutableMap;
public class CredentialProviderUtils {
@@ -43,4 +44,49 @@ public class CredentialProviderUtils {
}
return new PlainCredentialsProvider(mapBuilder.build());
}
+
+ /**
+ * Constructor for OAuth based authentication. Allows for tokens to be
stored in whatever vault
+ * mechanism the user chooses.
+ * Returns specified {@code CredentialsProvider credentialsProvider}
+ * if it is not null or builds and returns {@link PlainCredentialsProvider}
+ * with specified {@code CLIENT_ID}, {@code CLIENT_SECRET}, {@code
ACCESS_TOKEN}, {@code REFRESH_TOKEN}.
+ * @param clientID The OAuth Client ID. This is provided by the application
during signup.
+ * @param clientSecret The OAUth Client Secret. This is provided by the
application during signup.
+ * @param tokenURI The URI from which you swap the auth code for access and
refresh tokens.
+ * @param username Optional username for proxy or other services
+ * @param password Optional password for proxy or other services
+ * @param credentialsProvider The credential store which retains the
credentials.
+ * @return A credential provider with the access tokens
+ */
+ public static CredentialsProvider getCredentialsProvider(
+ String clientID,
+ String clientSecret,
+ String tokenURI,
+ String username,
+ String password,
+ CredentialsProvider credentialsProvider) {
+
+ if (credentialsProvider != null) {
+ return credentialsProvider;
+ }
+ ImmutableMap.Builder<String, String> mapBuilder = ImmutableMap.builder();
+ if (clientID != null) {
+ mapBuilder.put(OAuthTokenCredentials.CLIENT_ID, clientID);
+ }
+ if (clientSecret != null) {
+ mapBuilder.put(OAuthTokenCredentials.CLIENT_SECRET, clientSecret);
+ }
+ if (username != null) {
+ mapBuilder.put(OAuthTokenCredentials.USERNAME, username);
+ }
+ if (password != null) {
+ mapBuilder.put(OAuthTokenCredentials.PASSWORD, password);
+ }
+ if (tokenURI != null) {
+ mapBuilder.put(OAuthTokenCredentials.TOKEN_URI, tokenURI);
+ }
+
+ return new PlainCredentialsProvider(mapBuilder.build());
+ }
}
diff --git
a/exec/java-exec/src/main/java/org/apache/drill/exec/store/security/UsernamePasswordCredentials.java
b/exec/java-exec/src/main/java/org/apache/drill/exec/store/security/UsernamePasswordCredentials.java
index 2dbc8b1..c7f3d02 100644
---
a/exec/java-exec/src/main/java/org/apache/drill/exec/store/security/UsernamePasswordCredentials.java
+++
b/exec/java-exec/src/main/java/org/apache/drill/exec/store/security/UsernamePasswordCredentials.java
@@ -19,6 +19,7 @@ package org.apache.drill.exec.store.security;
import org.apache.drill.common.logical.security.CredentialsProvider;
+import java.util.HashMap;
import java.util.Map;
public class UsernamePasswordCredentials {
@@ -29,9 +30,14 @@ public class UsernamePasswordCredentials {
private final String password;
public UsernamePasswordCredentials(CredentialsProvider credentialsProvider) {
- Map<String, String> credentials = credentialsProvider.getCredentials();
- this.username = credentials.get(USERNAME);
- this.password = credentials.get(PASSWORD);
+ if (credentialsProvider == null) {
+ this.username = null;
+ this.password = null;
+ } else {
+ Map<String, String> credentials = credentialsProvider.getCredentials()
== null ? new HashMap<>() : credentialsProvider.getCredentials();
+ this.username = credentials.get(USERNAME);
+ this.password = credentials.get(PASSWORD);
+ }
}
public String getUsername() {
diff --git
a/exec/java-exec/src/main/java/org/apache/drill/exec/store/security/oauth/OAuthTokenCredentials.java
b/exec/java-exec/src/main/java/org/apache/drill/exec/store/security/oauth/OAuthTokenCredentials.java
new file mode 100644
index 0000000..2e39ee3
--- /dev/null
+++
b/exec/java-exec/src/main/java/org/apache/drill/exec/store/security/oauth/OAuthTokenCredentials.java
@@ -0,0 +1,87 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.drill.exec.store.security.oauth;
+
+import org.apache.drill.common.logical.security.CredentialsProvider;
+import org.apache.drill.exec.oauth.PersistentTokenTable;
+import org.apache.drill.exec.store.security.UsernamePasswordCredentials;
+
+import java.util.HashMap;
+import java.util.Map;
+
+public class OAuthTokenCredentials extends UsernamePasswordCredentials {
+
+ public static final String CLIENT_ID = "clientID";
+ public static final String CLIENT_SECRET = "clientSecret";
+ public static final String ACCESS_TOKEN = "accessToken";
+ public static final String REFRESH_TOKEN = "refreshToken";
+ public static final String TOKEN_URI = "tokenURI";
+
+ private final String clientID;
+ private final String clientSecret;
+ private final String tokenURI;
+ private PersistentTokenTable tokenTable;
+
+ public OAuthTokenCredentials(CredentialsProvider credentialsProvider) {
+ super(credentialsProvider);
+ if (credentialsProvider == null) {
+ this.clientID = null;
+ this.clientSecret = null;
+ this.tokenURI = null;
+ } else {
+ Map<String, String> credentials = credentialsProvider.getCredentials() ==
null
+ ? new HashMap<>() : credentialsProvider.getCredentials();
+
+ this.clientID = credentials.getOrDefault(CLIENT_ID, null);
+ this.clientSecret = credentials.getOrDefault(CLIENT_SECRET, null);
+ this.tokenURI = credentials.getOrDefault(TOKEN_URI, null);
+ }
+ }
+
+ public OAuthTokenCredentials(CredentialsProvider credentialsProvider,
PersistentTokenTable tokenTable) {
+ this(credentialsProvider);
+ this.tokenTable = tokenTable;
+ }
+
+ public String getClientID() {
+ return clientID;
+ }
+
+ public String getClientSecret() {
+ return clientSecret;
+ }
+
+ public String getAccessToken() {
+ if (tokenTable == null) {
+ return null;
+ }
+ return tokenTable.getAccessToken();
+ }
+
+ public String getRefreshToken() {
+ if (tokenTable == null) {
+ return null;
+ }
+ return tokenTable.getRefreshToken();
+ }
+
+ public String getTokenUri() {
+ return tokenURI;
+ }
+}
diff --git a/exec/java-exec/src/main/resources/rest/storage/success.html
b/exec/java-exec/src/main/resources/rest/storage/success.html
new file mode 100644
index 0000000..d78b083
--- /dev/null
+++ b/exec/java-exec/src/main/resources/rest/storage/success.html
@@ -0,0 +1,35 @@
+<!--
+
+ Licensed to the Apache Software Foundation (ASF) under one
+ or more contributor license agreements. See the NOTICE file
+ distributed with this work for additional information
+ regarding copyright ownership. The ASF licenses this file
+ to you under the Apache License, Version 2.0 (the
+ "License"); you may not use this file except in compliance
+ with the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+-->
+
+<html>
+ <head>
+ <title>Success!</title>
+ <link rel="shortcut icon" href="/static/img/drill.ico">
+ <link href="/static/css/bootstrap.min.css" rel="stylesheet">
+ <link href="/static/css/drillStyle.css" rel="stylesheet">
+ <link href="https://fonts.googleapis.com/icon?family=Material+Icons"
rel="stylesheet">
+ </head>
+ <body>
+ <h3>Success</h3>
+ You have successfully obtained an OAuth2 authorization code.
+ You may now close this window. <br/>
+ <button class="btn btn-primary" type="submit"
onclick="self.close();">Close Window</button>
+ </body>
+</html>
diff --git a/exec/java-exec/src/main/resources/rest/storage/update.ftl
b/exec/java-exec/src/main/resources/rest/storage/update.ftl
index 285771f..972890d 100644
--- a/exec/java-exec/src/main/resources/rest/storage/update.ftl
+++ b/exec/java-exec/src/main/resources/rest/storage/update.ftl
@@ -31,6 +31,7 @@
<h3>Configuration</h3>
<form id="updateForm" role="form" action="/storage/create_update"
method="POST">
<input type="hidden" name="name" value="${model.getPlugin().getName()}" />
+ <input type="hidden" name="pluginType" value="${model.getType()}" />
<div class="form-group">
<div id="editor" class="form-control"></div>
<textarea class="form-control" id="config" name="config"
data-editor="json" style="display: none;" >
@@ -38,14 +39,17 @@
</div>
<a class="btn btn-secondary" href="/storage">Back</a>
<button class="btn btn-primary" type="submit"
onclick="doUpdate();">Update</button>
- <#if model.getPlugin().enabled()>
+ <#if model.getPlugin().enabled()>
<a id="enabled" class="btn btn-warning">Disable</a>
- <#else>
+ <#else>
<a id="enabled" class="btn btn-success text-white">Enable</a>
- </#if>
+ </#if>
+ <#if model.getType() == "HttpStoragePluginConfig" &&
model.getPlugin().isOauth() >
+ <a id="getOauth" class="btn btn-success text-white">Authorize</a>
+ </#if>
<button type="button" class="btn btn-secondary export"
name="${model.getPlugin().getName()}" data-toggle="modal"
- data-target="#pluginsModal">
- Export
+ data-target="#pluginsModal">
+ Export
</button>
<a id="del" class="btn btn-danger text-white"
onclick="deleteFunction()">Delete</a>
<input type="hidden" name="csrfToken" value="${model.getCsrfToken()}">
@@ -56,7 +60,7 @@
<#include "*/confirmationModals.ftl">
- <#-- Modal window-->
+<#-- Modal window-->
<div class="modal fade" id="pluginsModal" tabindex="-1" role="dialog"
aria-labelledby="exportPlugin" aria-hidden="true">
<div class="modal-dialog modal-sm" role="document">
<div class="modal-content">
@@ -70,13 +74,13 @@
<div class="radio">
<label>
<input type="radio" name="format" id="json" value="json"
checked="checked">
- JSON
+ JSON
</label>
</div>
<div class="radio">
<label>
<input type="radio" name="format" id="hocon" value="conf">
- HOCON
+ HOCON
</label>
</div>
</div>
@@ -133,6 +137,65 @@
}
});
+ <#if model.getType() == "HttpStoragePluginConfig" >
+ $("#getOauth").click(function() {
+ var field = document.getElementById("config");
+ try {
+ var storage_config = JSON.parse(config.value);
+
+ // Construct the Callback URL
+ var clientID = storage_config.credentialsProvider.credentials.clientID;
+ if (clientID.length == 0) {
+ window.alert("Invalid client ID.");
+ return false;
+ }
+
+ var callbackURL = storage_config.oAuthConfig.callbackURL;
+ if (callbackURL.length == 0) {
+ window.alert("Invalid callback URL.");
+ return false;
+ }
+
+ var authorizationURL = storage_config.oAuthConfig.authorizationURL;
+ if (authorizationURL) {
+ finalURL = authorizationURL + "?client_id=" + clientID +
"&redirect_uri=" + callbackURL;
+ } else {
+ window.alert("Invalid authorization URL.")
+ return false;
+ }
+
+ // Add scope(s) if populated
+ var scope = storage_config.oAuthConfig.scope;
+ if (scope) {
+ var encodedScope = encodeURIComponent(scope);
+ finalURL = finalURL + "&scope=" + encodedScope;
+ }
+
+ // Add any additional parameters to the oAuth configuration string
+ var params = storage_config.oAuthConfig.authorizationParams
+ if (params) {
+ var param;
+ for (var key in storage_config.oAuthConfig.authorizationParams) {
+ param = params[key];
+ finalURL = finalURL + "&" + key + "=" + encodeURIComponent(param);
+ }
+ }
+ console.log(finalURL);
+ var tokenGetterWindow = window.open(finalURL, 'Authorize Drill',
"toolbar=no,menubar=no,scrollbars=yes,resizable=yes,top=500,left=500,width=450,height=600");
+
+ var timer = setInterval(function () {
+ if (tokenGetterWindow.closed) {
+ clearInterval(timer);
+ window.location.reload(); // Refresh the parent page
+ }
+ }, 1000);
+ } catch (error) {
+ console.error(error);
+ window.alert("Cannot parse JSON.");
+ }
+ });
+ </#if>
+
function doUpdate() {
$("#updateForm").ajaxForm({
dataType: 'json',
diff --git a/exec/jdbc-all/pom.xml b/exec/jdbc-all/pom.xml
index 4d72572..984aa1e 100644
--- a/exec/jdbc-all/pom.xml
+++ b/exec/jdbc-all/pom.xml
@@ -33,7 +33,7 @@
"package.namespace.prefix" equals to "oadd.". It can be overridden if
necessary within any profile -->
<properties>
<package.namespace.prefix>oadd.</package.namespace.prefix>
- <jdbc-all-jar.maxsize>46700000</jdbc-all-jar.maxsize>
+ <jdbc-all-jar.maxsize>50000000</jdbc-all-jar.maxsize>
</properties>
<dependencies>
diff --git
a/logical/src/main/java/org/apache/drill/common/logical/security/CredentialsProvider.java
b/logical/src/main/java/org/apache/drill/common/logical/security/CredentialsProvider.java
index 59b6ce0..5c82edf 100644
---
a/logical/src/main/java/org/apache/drill/common/logical/security/CredentialsProvider.java
+++
b/logical/src/main/java/org/apache/drill/common/logical/security/CredentialsProvider.java
@@ -19,6 +19,8 @@ package org.apache.drill.common.logical.security;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
import java.util.Map;
@@ -33,6 +35,8 @@ public interface CredentialsProvider {
* Returns map with authentication credentials. Key is the credential name,
for example {@code "username"}
* and map value is corresponding credential value.
*/
+ Logger logger = LoggerFactory.getLogger(CredentialsProvider.class);
+
@JsonIgnore
Map<String, String> getCredentials();
}
diff --git
a/logical/src/main/java/org/apache/drill/common/logical/security/PlainCredentialsProvider.java
b/logical/src/main/java/org/apache/drill/common/logical/security/PlainCredentialsProvider.java
index d157d66..8427b63 100644
---
a/logical/src/main/java/org/apache/drill/common/logical/security/PlainCredentialsProvider.java
+++
b/logical/src/main/java/org/apache/drill/common/logical/security/PlainCredentialsProvider.java
@@ -20,6 +20,8 @@ package org.apache.drill.common.logical.security;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
import java.util.Collections;
import java.util.Map;
@@ -31,6 +33,7 @@ import java.util.Objects;
* Its constructor accepts a map with credential names as keys and values as
corresponding credential values.
*/
public class PlainCredentialsProvider implements CredentialsProvider {
+ private static final Logger logger =
LoggerFactory.getLogger(PlainCredentialsProvider.class);
public static final CredentialsProvider EMPTY_CREDENTIALS_PROVIDER =
new PlainCredentialsProvider(Collections.emptyMap());