diff --git a/nifi-assembly/pom.xml b/nifi-assembly/pom.xml index 7f96c8758d..4912c869f4 100644 --- a/nifi-assembly/pom.xml +++ b/nifi-assembly/pom.xml @@ -427,6 +427,12 @@ language governing permissions and limitations under the License. --> 1.12.0-SNAPSHOT nar + + org.apache.nifi + nifi-oauth2-provider-nar + 1.12.0-SNAPSHOT + nar + org.apache.nifi nifi-azure-nar diff --git a/nifi-nar-bundles/nifi-standard-services/nifi-oauth2-provider-api/pom.xml b/nifi-nar-bundles/nifi-standard-services/nifi-oauth2-provider-api/pom.xml new file mode 100644 index 0000000000..d7efc35ea4 --- /dev/null +++ b/nifi-nar-bundles/nifi-standard-services/nifi-oauth2-provider-api/pom.xml @@ -0,0 +1,40 @@ + + + + 4.0.0 + + + org.apache.nifi + nifi-standard-services + 1.12.0-SNAPSHOT + + + nifi-oauth2-provider-api + jar + + + + org.apache.nifi + nifi-api + provided + + + org.apache.nifi + nifi-ssl-context-service-api + compile + + + diff --git a/nifi-nar-bundles/nifi-standard-services/nifi-oauth2-provider-api/src/main/java/org/apache/nifi/oauth2/AccessToken.java b/nifi-nar-bundles/nifi-standard-services/nifi-oauth2-provider-api/src/main/java/org/apache/nifi/oauth2/AccessToken.java new file mode 100644 index 0000000000..2e261ddce0 --- /dev/null +++ b/nifi-nar-bundles/nifi-standard-services/nifi-oauth2-provider-api/src/main/java/org/apache/nifi/oauth2/AccessToken.java @@ -0,0 +1,69 @@ +/* + * 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.nifi.oauth2; + +public class AccessToken { + private String accessToken; + private String refreshToken; + private String tokenType; + private Integer expires; + private String scope; + + private Long fetchTime; + + public AccessToken(String accessToken, + String refreshToken, + String tokenType, + Integer expires, + String scope) { + this.accessToken = accessToken; + this.tokenType = tokenType; + this.refreshToken = refreshToken; + this.expires = expires; + this.scope = scope; + this.fetchTime = System.currentTimeMillis(); + } + + public String getAccessToken() { + return accessToken; + } + + public String getRefreshToken() { + return refreshToken; + } + + public String getTokenType() { + return tokenType; + } + + public Integer getExpires() { + return expires; + } + + public String getScope() { + return scope; + } + + public Long getFetchTime() { + return fetchTime; + } + + public boolean isExpired() { + return System.currentTimeMillis() >= ( fetchTime + (expires * 1000) ); + } +} diff --git a/nifi-nar-bundles/nifi-standard-services/nifi-oauth2-provider-api/src/main/java/org/apache/nifi/oauth2/AccessTokenAcquisitionException.java b/nifi-nar-bundles/nifi-standard-services/nifi-oauth2-provider-api/src/main/java/org/apache/nifi/oauth2/AccessTokenAcquisitionException.java new file mode 100644 index 0000000000..dc85a3ad7a --- /dev/null +++ b/nifi-nar-bundles/nifi-standard-services/nifi-oauth2-provider-api/src/main/java/org/apache/nifi/oauth2/AccessTokenAcquisitionException.java @@ -0,0 +1,28 @@ +/* + * 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.nifi.oauth2; + +public class AccessTokenAcquisitionException extends Exception { + public AccessTokenAcquisitionException(String message) { + super(message); + } + + public AccessTokenAcquisitionException(Throwable t) { + super(t); + } +} diff --git a/nifi-nar-bundles/nifi-standard-services/nifi-oauth2-provider-api/src/main/java/org/apache/nifi/oauth2/OAuth2TokenProvider.java b/nifi-nar-bundles/nifi-standard-services/nifi-oauth2-provider-api/src/main/java/org/apache/nifi/oauth2/OAuth2TokenProvider.java new file mode 100644 index 0000000000..964ae439d5 --- /dev/null +++ b/nifi-nar-bundles/nifi-standard-services/nifi-oauth2-provider-api/src/main/java/org/apache/nifi/oauth2/OAuth2TokenProvider.java @@ -0,0 +1,58 @@ +/* + * 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.nifi.oauth2; + +import org.apache.nifi.components.PropertyDescriptor; +import org.apache.nifi.components.Validator; +import org.apache.nifi.controller.ControllerService; +import org.apache.nifi.expression.ExpressionLanguageScope; +import org.apache.nifi.ssl.SSLContextService; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +/** + * Interface for defining a credential-providing controller service for oauth2 processes. + */ +public interface OAuth2TokenProvider extends ControllerService { + PropertyDescriptor SSL_CONTEXT = new PropertyDescriptor.Builder() + .name("oauth2-ssl-context") + .displayName("SSL Context") + .addValidator(Validator.VALID) + .identifiesControllerService(SSLContextService.class) + .required(false) + .build(); + PropertyDescriptor ACCESS_TOKEN_URL = new PropertyDescriptor.Builder() + .name("oauth2-access-token-url") + .displayName("Access Token Url") + .description("The full endpoint of the URL where access tokens are handled.") + .required(true) + .defaultValue("") + .addValidator(Validator.VALID) + .expressionLanguageSupported(ExpressionLanguageScope.VARIABLE_REGISTRY) + .build(); + + List PROPERTIES = Collections.unmodifiableList(Arrays.asList( + SSL_CONTEXT, ACCESS_TOKEN_URL + )); + + AccessToken getAccessTokenByPassword(String clientId, String clientSecret, String username, String password) throws AccessTokenAcquisitionException; + AccessToken getAccessTokenByClientCredentials(String clientId, String clientSecret) throws AccessTokenAcquisitionException; + AccessToken refreshToken(AccessToken refreshThis) throws AccessTokenAcquisitionException; +} diff --git a/nifi-nar-bundles/nifi-standard-services/nifi-oauth2-provider-bundle/nifi-oauth2-provider-nar/pom.xml b/nifi-nar-bundles/nifi-standard-services/nifi-oauth2-provider-bundle/nifi-oauth2-provider-nar/pom.xml new file mode 100644 index 0000000000..6e93992e3a --- /dev/null +++ b/nifi-nar-bundles/nifi-standard-services/nifi-oauth2-provider-bundle/nifi-oauth2-provider-nar/pom.xml @@ -0,0 +1,47 @@ + + + + 4.0.0 + + + org.apache.nifi + nifi-oauth2-provider-bundle + 1.12.0-SNAPSHOT + + + nifi-oauth2-provider-nar + 1.12.0-SNAPSHOT + nar + + true + true + + + + + org.apache.nifi + nifi-standard-services-api-nar + 1.12.0-SNAPSHOT + nar + + + org.apache.nifi + nifi-oauth2-provider-service + 1.12.0-SNAPSHOT + + + + diff --git a/nifi-nar-bundles/nifi-standard-services/nifi-oauth2-provider-bundle/nifi-oauth2-provider-nar/src/main/resources/META-INF/LICENSE b/nifi-nar-bundles/nifi-standard-services/nifi-oauth2-provider-bundle/nifi-oauth2-provider-nar/src/main/resources/META-INF/LICENSE new file mode 100644 index 0000000000..d645695673 --- /dev/null +++ b/nifi-nar-bundles/nifi-standard-services/nifi-oauth2-provider-bundle/nifi-oauth2-provider-nar/src/main/resources/META-INF/LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed 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. diff --git a/nifi-nar-bundles/nifi-standard-services/nifi-oauth2-provider-bundle/nifi-oauth2-provider-nar/src/main/resources/META-INF/NOTICE b/nifi-nar-bundles/nifi-standard-services/nifi-oauth2-provider-bundle/nifi-oauth2-provider-nar/src/main/resources/META-INF/NOTICE new file mode 100644 index 0000000000..9f75a9d438 --- /dev/null +++ b/nifi-nar-bundles/nifi-standard-services/nifi-oauth2-provider-bundle/nifi-oauth2-provider-nar/src/main/resources/META-INF/NOTICE @@ -0,0 +1,42 @@ +nifi-oauth2-provider-nar +Copyright 2014-2020 The Apache Software Foundation + +This product includes software developed at +The Apache Software Foundation (http://www.apache.org/). + +****************** +Apache Software License v2 +****************** + + + (ASLv2) Apache Commons Lang + The following NOTICE information applies: + Apache Commons Lang + Copyright 2001-2011 The Apache Software Foundation + + + (ASLv2) Jackson Core ASL + The following NOTICE information applies: + This product currently only contains code developed by authors + of specific components, as identified by the source code files; + if such notes are missing files have been created by + Tatu Saloranta. + + For additional credits (generally to people who reported problems) + see CREDITS file. + + (ASLv2) Jackson Mapper ASL + The following NOTICE information applies: + This product currently only contains code developed by authors + of specific components, as identified by the source code files; + if such notes are missing files have been created by + Tatu Saloranta. + + For additional credits (generally to people who reported problems) + see CREDITS file. + + (ASLv2) OkHttp Client + * LICENSE: + * okhttp/third_party/okhttp/LICENSE (Apache License 2.0) + * HOMEPAGE: + * https://github.com/square/okhttp diff --git a/nifi-nar-bundles/nifi-standard-services/nifi-oauth2-provider-bundle/nifi-oauth2-provider-service/pom.xml b/nifi-nar-bundles/nifi-standard-services/nifi-oauth2-provider-bundle/nifi-oauth2-provider-service/pom.xml new file mode 100644 index 0000000000..14dadf95d9 --- /dev/null +++ b/nifi-nar-bundles/nifi-standard-services/nifi-oauth2-provider-bundle/nifi-oauth2-provider-service/pom.xml @@ -0,0 +1,98 @@ + + + + 4.0.0 + + + org.apache.nifi + nifi-oauth2-provider-bundle + 1.12.0-SNAPSHOT + + + nifi-oauth2-provider-service + jar + + + + org.apache.nifi + nifi-oauth2-provider-api + 1.12.0-SNAPSHOT + provided + + + org.apache.nifi + nifi-api + provided + + + org.apache.nifi + nifi-utils + 1.12.0-SNAPSHOT + + + org.apache.nifi + nifi-record + provided + + + + org.apache.nifi + nifi-ssl-context-service-api + 1.12.0-SNAPSHOT + provided + + + + org.apache.commons + commons-lang3 + 3.9 + + + org.slf4j + log4j-over-slf4j + + + + org.apache.nifi + nifi-mock + 1.12.0-SNAPSHOT + test + + + com.squareup.okhttp3 + okhttp + 3.14.4 + compile + + + com.fasterxml.jackson.core + jackson-databind + 2.10.0 + compile + + + org.apache.nifi + nifi-standard-web-test-utils + 1.12.0-SNAPSHOT + test + + + org.eclipse.jetty + jetty-server + test + + + diff --git a/nifi-nar-bundles/nifi-standard-services/nifi-oauth2-provider-bundle/nifi-oauth2-provider-service/src/main/java/org/apache/nifi/oauth2/OAuth2TokenProviderImpl.java b/nifi-nar-bundles/nifi-standard-services/nifi-oauth2-provider-bundle/nifi-oauth2-provider-service/src/main/java/org/apache/nifi/oauth2/OAuth2TokenProviderImpl.java new file mode 100644 index 0000000000..dfae289d6f --- /dev/null +++ b/nifi-nar-bundles/nifi-standard-services/nifi-oauth2-provider-bundle/nifi-oauth2-provider-service/src/main/java/org/apache/nifi/oauth2/OAuth2TokenProviderImpl.java @@ -0,0 +1,155 @@ +/* + * 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.nifi.oauth2; + +import okhttp3.FormBody; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.RequestBody; +import okhttp3.Response; +import org.apache.nifi.annotation.documentation.CapabilityDescription; +import org.apache.nifi.annotation.documentation.Tags; +import org.apache.nifi.annotation.lifecycle.OnEnabled; +import org.apache.nifi.components.PropertyDescriptor; +import org.apache.nifi.controller.AbstractControllerService; +import org.apache.nifi.controller.ConfigurationContext; +import org.apache.nifi.processor.exception.ProcessException; +import org.apache.nifi.ssl.SSLContextService; +import org.apache.nifi.security.util.SslContextFactory; +import org.apache.nifi.util.StringUtils; + +import javax.net.ssl.SSLContext; +import java.io.IOException; +import java.util.List; + +import static org.apache.nifi.oauth2.Util.parseTokenResponse; + +@Tags({"oauth2", "provider", "authorization" }) +@CapabilityDescription("This controller service provides a way of working with access and refresh tokens via the " + + "password and client_credential grant flows in the OAuth2 specification. It is meant to provide a way for components " + + "to get a token from an oauth2 provider and pass that token as a part of a header to another service.") +public class OAuth2TokenProviderImpl extends AbstractControllerService implements OAuth2TokenProvider { + @Override + public List getSupportedPropertyDescriptors() { + return PROPERTIES; + } + + private String resourceServerUrl; + private SSLContext sslContext; + private SSLContextService sslContextService; + + @OnEnabled + public void onEnabled(ConfigurationContext context) { + resourceServerUrl = context.getProperty(ACCESS_TOKEN_URL).evaluateAttributeExpressions().getValue(); + + sslContextService = context.getProperty(SSL_CONTEXT).asControllerService(SSLContextService.class); + + sslContext = sslContextService == null ? null : sslContextService.createSSLContext(SslContextFactory.ClientAuth.NONE); + } + + + @Override + public AccessToken getAccessTokenByPassword(String clientId, String clientSecret, + String username, String password) throws AccessTokenAcquisitionException { + OkHttpClient.Builder builder = getClientBuilder(); + OkHttpClient client = builder.build(); + + RequestBody body = new FormBody.Builder() + .add("username", username) + .add("password", password) + .add("client_id", clientId) + .add("client_secret", clientSecret) + .add("grant_type", "password") + .build(); + + Request newRequest = new Request.Builder() + .url(resourceServerUrl) + .post(body) + .build(); + + return executePost(client, newRequest); + } + + private OkHttpClient.Builder getClientBuilder() { + OkHttpClient.Builder clientBuilder = new OkHttpClient.Builder(); + + if (sslContext != null) { + try { + Util.setSslSocketFactory(clientBuilder, sslContextService, sslContext, false); + } catch (Exception e) { + throw new ProcessException(e); + } + } + + return clientBuilder; + } + + private AccessToken executePost(OkHttpClient httpClient, Request newRequest) throws AccessTokenAcquisitionException { + try { + Response response = httpClient.newCall(newRequest).execute(); + String body = response.body().string(); + if (response.code() >= 300) { + getLogger().error(String.format("Bad response from the server during oauth2 request:\n%s", body)); + throw new AccessTokenAcquisitionException(String.format("Got HTTP %d during oauth2 request.", response.code())); + } + + return parseTokenResponse(body); + } catch (IOException e) { + throw new AccessTokenAcquisitionException(e); + } + } + + @Override + public AccessToken getAccessTokenByClientCredentials(String clientId, String clientSecret) throws AccessTokenAcquisitionException { + OkHttpClient.Builder builder = getClientBuilder(); + OkHttpClient client = builder.build(); + + RequestBody body = new FormBody.Builder() + .add("grant_type", "client_credentials") + .add("client_id", clientId) + .add("client_secret", clientSecret) + .build(); + + Request newRequest = new Request.Builder() + .url(resourceServerUrl) + .post(body) + .build(); + + return executePost(client, newRequest); + } + + @Override + public AccessToken refreshToken(AccessToken refreshThis) throws AccessTokenAcquisitionException { + if (StringUtils.isEmpty(refreshThis.getRefreshToken())) { + throw new ProcessException("Missing refresh token. Refresh cannot happen."); + } + OkHttpClient.Builder builder = getClientBuilder(); + OkHttpClient client = builder.build(); + RequestBody body = new FormBody.Builder() + .add("grant_type", "refresh_token") + .add("refresh_token", refreshThis.getRefreshToken()) + .build(); + + Request newRequest = new Request.Builder() + .url(resourceServerUrl) + .post(body) + .build(); + + return executePost(client, newRequest); + } +} diff --git a/nifi-nar-bundles/nifi-standard-services/nifi-oauth2-provider-bundle/nifi-oauth2-provider-service/src/main/java/org/apache/nifi/oauth2/Util.java b/nifi-nar-bundles/nifi-standard-services/nifi-oauth2-provider-bundle/nifi-oauth2-provider-service/src/main/java/org/apache/nifi/oauth2/Util.java new file mode 100644 index 0000000000..9ef003822b --- /dev/null +++ b/nifi-nar-bundles/nifi-standard-services/nifi-oauth2-provider-bundle/nifi-oauth2-provider-service/src/main/java/org/apache/nifi/oauth2/Util.java @@ -0,0 +1,139 @@ +/* + * 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.nifi.oauth2; + +import com.fasterxml.jackson.databind.ObjectMapper; +import okhttp3.OkHttpClient; +import org.apache.nifi.processor.exception.ProcessException; +import org.apache.nifi.ssl.SSLContextService; +import org.apache.nifi.util.StringUtils; + +import javax.net.ssl.KeyManager; +import javax.net.ssl.KeyManagerFactory; +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLSocketFactory; +import javax.net.ssl.TrustManager; +import javax.net.ssl.TrustManagerFactory; +import javax.net.ssl.X509TrustManager; +import java.io.FileInputStream; +import java.io.IOException; +import java.security.KeyManagementException; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.UnrecoverableKeyException; +import java.security.cert.CertificateException; +import java.util.Map; + +public class Util { + private static final ObjectMapper MAPPER = new ObjectMapper(); + + /** + * This code as taken from the InvokeHttp processor from Apache NiFi 1.10-SNAPSHOT found here: + * + * https://github.com/apache/nifi/blob/1cadc722229ad50cf569ee107eaeeb95dc216ea2/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/main/java/org/apache/nifi/processors/standard/InvokeHTTP.java + */ + public static void setSslSocketFactory(OkHttpClient.Builder okHttpClientBuilder, SSLContextService sslService, SSLContext sslContext, boolean setAsSocketFactory) + throws IOException, KeyStoreException, CertificateException, NoSuchAlgorithmException, UnrecoverableKeyException, KeyManagementException { + + final KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm()); + final TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance("X509"); + // initialize the KeyManager array to null and we will overwrite later if a keystore is loaded + KeyManager[] keyManagers = null; + + // we will only initialize the keystore if properties have been supplied by the SSLContextService + if (sslService.isKeyStoreConfigured()) { + final String keystoreLocation = sslService.getKeyStoreFile(); + final String keystorePass = sslService.getKeyStorePassword(); + final String keystoreType = sslService.getKeyStoreType(); + + // prepare the keystore + final KeyStore keyStore = KeyStore.getInstance(keystoreType); + + try (FileInputStream keyStoreStream = new FileInputStream(keystoreLocation)) { + keyStore.load(keyStoreStream, keystorePass.toCharArray()); + } + + keyManagerFactory.init(keyStore, keystorePass.toCharArray()); + keyManagers = keyManagerFactory.getKeyManagers(); + } + + // we will only initialize the truststure if properties have been supplied by the SSLContextService + if (sslService.isTrustStoreConfigured()) { + // load truststore + final String truststoreLocation = sslService.getTrustStoreFile(); + final String truststorePass = sslService.getTrustStorePassword(); + final String truststoreType = sslService.getTrustStoreType(); + + KeyStore truststore = KeyStore.getInstance(truststoreType); + truststore.load(new FileInputStream(truststoreLocation), truststorePass.toCharArray()); + trustManagerFactory.init(truststore); + } + + /* + TrustManagerFactory.getTrustManagers returns a trust manager for each type of trust material. Since we are getting a trust manager factory that uses "X509" + as it's trust management algorithm, we are able to grab the first (and thus the most preferred) and use it as our x509 Trust Manager + https://docs.oracle.com/javase/8/docs/api/javax/net/ssl/TrustManagerFactory.html#getTrustManagers-- + */ + final X509TrustManager x509TrustManager; + TrustManager[] trustManagers = trustManagerFactory.getTrustManagers(); + if (trustManagers[0] != null) { + x509TrustManager = (X509TrustManager) trustManagers[0]; + } else { + throw new IllegalStateException("List of trust managers is null"); + } + + // if keystore properties were not supplied, the keyManagers array will be null + sslContext.init(keyManagers, trustManagerFactory.getTrustManagers(), null); + + final SSLSocketFactory sslSocketFactory = sslContext.getSocketFactory(); + okHttpClientBuilder.sslSocketFactory(sslSocketFactory, x509TrustManager); + if (setAsSocketFactory) { + okHttpClientBuilder.socketFactory(sslSocketFactory); + } + } + + public static final String KEY_ACCESS_TOKEN = "access_token"; + public static final String KEY_REFRESH_TOKEN = "refresh_token"; + public static final String KEY_EXPIRES = "expires_in"; + public static final String KEY_TOKEN_TYPE = "token_type"; + public static final String KEY_SCOPE = "scope"; + + public static AccessToken parseTokenResponse(String rawResponse) { + try { + Map parsed = MAPPER.readValue(rawResponse, Map.class); + String accessToken = (String)parsed.get(KEY_ACCESS_TOKEN); + String refreshToken = (String)parsed.get(KEY_REFRESH_TOKEN); + Integer expires = (Integer)parsed.get(KEY_EXPIRES); + String tokenType = (String)parsed.get(KEY_TOKEN_TYPE); + String scope = (String)parsed.get(KEY_SCOPE); + + if (StringUtils.isEmpty(accessToken)) { + throw new Exception(String.format("Missing value for %s", KEY_ACCESS_TOKEN)); + } + + if (StringUtils.isEmpty(tokenType)) { + throw new Exception(String.format("Missing value for %s", KEY_TOKEN_TYPE)); + } + + return new AccessToken(accessToken, refreshToken, tokenType, expires, scope); + } catch (Exception ex) { + throw new ProcessException(ex); + } + } +} diff --git a/nifi-nar-bundles/nifi-standard-services/nifi-oauth2-provider-bundle/nifi-oauth2-provider-service/src/main/resources/META-INF/services/org.apache.nifi.controller.ControllerService b/nifi-nar-bundles/nifi-standard-services/nifi-oauth2-provider-bundle/nifi-oauth2-provider-service/src/main/resources/META-INF/services/org.apache.nifi.controller.ControllerService new file mode 100644 index 0000000000..75e29d02a5 --- /dev/null +++ b/nifi-nar-bundles/nifi-standard-services/nifi-oauth2-provider-bundle/nifi-oauth2-provider-service/src/main/resources/META-INF/services/org.apache.nifi.controller.ControllerService @@ -0,0 +1,15 @@ +# 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. +org.apache.nifi.oauth2.OAuth2TokenProviderImpl diff --git a/nifi-nar-bundles/nifi-standard-services/nifi-oauth2-provider-bundle/nifi-oauth2-provider-service/src/test/java/org/apache/nifi/oauth2/OAuth2TokenProviderImplTest.java b/nifi-nar-bundles/nifi-standard-services/nifi-oauth2-provider-bundle/nifi-oauth2-provider-service/src/test/java/org/apache/nifi/oauth2/OAuth2TokenProviderImplTest.java new file mode 100644 index 0000000000..83d56d3589 --- /dev/null +++ b/nifi-nar-bundles/nifi-standard-services/nifi-oauth2-provider-bundle/nifi-oauth2-provider-service/src/test/java/org/apache/nifi/oauth2/OAuth2TokenProviderImplTest.java @@ -0,0 +1,194 @@ +/* + * 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.nifi.oauth2; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.apache.commons.lang3.SystemUtils; +import org.apache.nifi.processor.AbstractProcessor; +import org.apache.nifi.processor.ProcessContext; +import org.apache.nifi.processor.ProcessSession; +import org.apache.nifi.processor.exception.ProcessException; +import org.apache.nifi.util.TestRunner; +import org.apache.nifi.util.TestRunners; +import org.apache.nifi.web.util.TestServer; +import org.eclipse.jetty.server.Request; +import org.junit.Assume; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Test; +import org.eclipse.jetty.server.handler.AbstractHandler; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + +public class OAuth2TokenProviderImplTest { + private TestRunner runner; + private static TestServer server; + private static String url; + private OAuth2TokenProvider oAuth2TokenProvider; + + private static FakeOAuth2Server handler; + + @BeforeClass + public static void beforeClass() throws Exception { + Assume.assumeTrue("Test only runs on *nix", !SystemUtils.IS_OS_WINDOWS); + // useful for verbose logging output + // don't commit this with this property enabled, or any 'mvn test' will be really verbose + // System.setProperty("org.slf4j.simpleLogger.log.nifi.processors.standard", "debug"); + + // create a Jetty server on a random port + server = new TestServer(); + server.startServer(); + + // this is the base url with the random port + url = server.getUrl(); + + handler = new FakeOAuth2Server(); + + server.addHandler(handler); + } + + @Before + public void setup() throws Exception { + runner = TestRunners.newTestRunner(new AbstractProcessor() { + @Override + public void onTrigger(ProcessContext context, ProcessSession session) throws ProcessException { + + } + }); + oAuth2TokenProvider = new OAuth2TokenProviderImpl(); + runner.addControllerService("provider", oAuth2TokenProvider); + runner.setProperty(oAuth2TokenProvider, OAuth2TokenProvider.ACCESS_TOKEN_URL, url); + runner.enableControllerService(oAuth2TokenProvider); + runner.assertValid(); + } + + @Test + public void testClientCredentialGrant() { + Exception ex = null; + AccessToken token = null; + try { + token = oAuth2TokenProvider.getAccessTokenByClientCredentials( + "test-client", + UUID.randomUUID().toString() + ); + } catch (AccessTokenAcquisitionException e) { + ex = e; + } finally { + commonTest(ex, token); + } + } + + @Test + public void testErrorHandler() { + Exception ex = null; + + try { + handler.setThrowException(true); + oAuth2TokenProvider.getAccessTokenByClientCredentials( + "test-client", + UUID.randomUUID().toString() + ); + } catch (AccessTokenAcquisitionException e) { + ex = e; + } finally { + handler.setThrowException(false); + assertTrue(ex instanceof AccessTokenAcquisitionException); + } + } + + @Test + public void testPasswordGrant() { + Exception ex = null; + AccessToken token = null; + try { + token = oAuth2TokenProvider.getAccessTokenByPassword( + "test-client", + UUID.randomUUID().toString(), + "user", + "password" + ); + } catch (AccessTokenAcquisitionException e) { + ex = e; + } finally { + commonTest(ex, token); + } + } + + @Test + public void testRefreshToken() { + Exception ex = null; + AccessToken token = null; + try { + token = oAuth2TokenProvider.refreshToken( + new AccessToken("token", "refresh", "BEARER", 300, "test") + ); + } catch (AccessTokenAcquisitionException e) { + ex = e; + } finally { + commonTest(ex, token); + } + } + + private void commonTest(Exception ex, AccessToken token) { + assertNull(ex); + assertNotNull(token); + assertEquals("access token", token.getAccessToken()); + assertEquals(300, token.getExpires().intValue()); + assertEquals("BEARER", token.getTokenType()); + assertFalse(token.isExpired()); + } + + public static final class FakeOAuth2Server extends AbstractHandler { + boolean throwException = false; + + public void setThrowException(boolean throwException) { + this.throwException = throwException; + } + + @Override + public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException { + baseRequest.setHandled(true); + + if (throwException) { + response.setStatus(500); + } else { + Map token = new HashMap<>(); + token.put("access_token", "access token"); + token.put("refresh_token", "refresh token"); + token.put("token_type", "BEARER"); + token.put("expires_in", 300); + token.put("scope", "test scope"); + + response.setContentType("application/json"); + response.getWriter().write(new ObjectMapper().writeValueAsString(token)); + } + } + } +} \ No newline at end of file diff --git a/nifi-nar-bundles/nifi-standard-services/nifi-oauth2-provider-bundle/pom.xml b/nifi-nar-bundles/nifi-standard-services/nifi-oauth2-provider-bundle/pom.xml new file mode 100644 index 0000000000..e851fa6988 --- /dev/null +++ b/nifi-nar-bundles/nifi-standard-services/nifi-oauth2-provider-bundle/pom.xml @@ -0,0 +1,48 @@ + + + + 4.0.0 + + org.apache.nifi + nifi-standard-services + 1.12.0-SNAPSHOT + + + org.apache.nifi + nifi-oauth2-provider-bundle + 1.12.0-SNAPSHOT + pom + + + nifi-oauth2-provider-service + nifi-oauth2-provider-nar + + + + + + org.apache.rat + apache-rat-plugin + + + src/test/resources/fake.keytab + src/test/resources/krb5.conf + + + + + + diff --git a/nifi-nar-bundles/nifi-standard-services/nifi-standard-services-api-nar/pom.xml b/nifi-nar-bundles/nifi-standard-services/nifi-standard-services-api-nar/pom.xml index acec0ad19e..3e6611808a 100644 --- a/nifi-nar-bundles/nifi-standard-services/nifi-standard-services-api-nar/pom.xml +++ b/nifi-nar-bundles/nifi-standard-services/nifi-standard-services-api-nar/pom.xml @@ -64,6 +64,12 @@ 1.12.0-SNAPSHOT compile + + org.apache.nifi + nifi-oauth2-provider-api + 1.12.0-SNAPSHOT + compile + org.apache.nifi nifi-rules-engine-service-api diff --git a/nifi-nar-bundles/nifi-standard-services/pom.xml b/nifi-nar-bundles/nifi-standard-services/pom.xml index aecf01ef31..b148513146 100644 --- a/nifi-nar-bundles/nifi-standard-services/pom.xml +++ b/nifi-nar-bundles/nifi-standard-services/pom.xml @@ -13,7 +13,8 @@ See the License for the specific language governing permissions and limitations under the License. --> - + 4.0.0 org.apache.nifi @@ -23,6 +24,8 @@ nifi-standard-services pom + nifi-oauth2-provider-api + nifi-oauth2-provider-bundle nifi-distributed-cache-client-service-api nifi-distributed-cache-services-bundle nifi-load-distribution-service-api @@ -37,7 +40,7 @@ nifi-dbcp-service-bundle nifi-hbase-client-service-api nifi-hbase_1_1_2-client-service-bundle - nifi-hbase_2-client-service-bundle + nifi-hbase_2-client-service-bundle nifi-schema-registry-service-api nifi-record-serialization-service-api nifi-record-serialization-services-bundle