Repository: incubator-ranger Updated Branches: refs/heads/master 608776712 -> b64218a34
RANGER-938 Adding NiFi service-definition and the NiFiClient for resource lookups Signed-off-by: Madhan Neethiraj <[email protected]> Project: http://git-wip-us.apache.org/repos/asf/incubator-ranger/repo Commit: http://git-wip-us.apache.org/repos/asf/incubator-ranger/commit/b64218a3 Tree: http://git-wip-us.apache.org/repos/asf/incubator-ranger/tree/b64218a3 Diff: http://git-wip-us.apache.org/repos/asf/incubator-ranger/diff/b64218a3 Branch: refs/heads/master Commit: b64218a347e54aff132f821946de63133258d407 Parents: 6087767 Author: Bryan Bende <[email protected]> Authored: Thu Mar 31 10:59:17 2016 -0400 Committer: Madhan Neethiraj <[email protected]> Committed: Fri May 13 11:36:55 2016 -0700 ---------------------------------------------------------------------- .../plugin/store/EmbeddedServiceDefsUtil.java | 9 + .../service-defs/ranger-servicedef-nifi.json | 162 ++++++++++++++ plugin-nifi/.gitignore | 1 + plugin-nifi/pom.xml | 62 ++++++ .../ranger/services/nifi/RangerServiceNiFi.java | 71 ++++++ .../services/nifi/client/NiFiAuthType.java | 29 +++ .../ranger/services/nifi/client/NiFiClient.java | 217 +++++++++++++++++++ .../services/nifi/client/NiFiConfigs.java | 37 ++++ .../services/nifi/client/NiFiConnectionMgr.java | 157 ++++++++++++++ .../services/nifi/client/TestNiFiClient.java | 196 +++++++++++++++++ .../nifi/client/TestNiFiConnectionMgr.java | 124 +++++++++++ pom.xml | 1 + src/main/assembly/admin-web.xml | 13 ++ 13 files changed, 1079 insertions(+) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/incubator-ranger/blob/b64218a3/agents-common/src/main/java/org/apache/ranger/plugin/store/EmbeddedServiceDefsUtil.java ---------------------------------------------------------------------- diff --git a/agents-common/src/main/java/org/apache/ranger/plugin/store/EmbeddedServiceDefsUtil.java b/agents-common/src/main/java/org/apache/ranger/plugin/store/EmbeddedServiceDefsUtil.java index 7ec8d98..bf29ee6 100755 --- a/agents-common/src/main/java/org/apache/ranger/plugin/store/EmbeddedServiceDefsUtil.java +++ b/agents-common/src/main/java/org/apache/ranger/plugin/store/EmbeddedServiceDefsUtil.java @@ -60,6 +60,8 @@ public class EmbeddedServiceDefsUtil { public static final String EMBEDDED_SERVICEDEF_YARN_NAME = "yarn"; public static final String EMBEDDED_SERVICEDEF_KAFKA_NAME = "kafka"; public static final String EMBEDDED_SERVICEDEF_SOLR_NAME = "solr"; + public static final String EMBEDDED_SERVICEDEF_NIFI_NAME = "nifi"; + public static final String PROPERTY_CREATE_EMBEDDED_SERVICE_DEFS = "ranger.service.store.create.embedded.service-defs"; public static final String HDFS_IMPL_CLASS_NAME = "org.apache.ranger.services.hdfs.RangerServiceHdfs"; @@ -71,6 +73,7 @@ public class EmbeddedServiceDefsUtil { public static final String YARN_IMPL_CLASS_NAME = "org.apache.ranger.services.yarn.RangerServiceYarn"; public static final String KAFKA_IMPL_CLASS_NAME = "org.apache.ranger.services.kafka.RangerServiceKafka"; public static final String SOLR_IMPL_CLASS_NAME = "org.apache.ranger.services.solr.RangerServiceSolr"; + public static final String NIFI_IMPL_CLASS_NAME = "org.apache.ranger.services.nifi.RangerServiceNiFi"; private static EmbeddedServiceDefsUtil instance = new EmbeddedServiceDefsUtil(); @@ -84,6 +87,7 @@ public class EmbeddedServiceDefsUtil { private RangerServiceDef yarnServiceDef = null; private RangerServiceDef kafkaServiceDef = null; private RangerServiceDef solrServiceDef = null; + private RangerServiceDef nifiServiceDef = null; private RangerServiceDef tagServiceDef = null; @@ -120,6 +124,7 @@ public class EmbeddedServiceDefsUtil { yarnServiceDef = getOrCreateServiceDef(store, EMBEDDED_SERVICEDEF_YARN_NAME); kafkaServiceDef = getOrCreateServiceDef(store, EMBEDDED_SERVICEDEF_KAFKA_NAME); solrServiceDef = getOrCreateServiceDef(store, EMBEDDED_SERVICEDEF_SOLR_NAME); + nifiServiceDef = getOrCreateServiceDef(store, EMBEDDED_SERVICEDEF_NIFI_NAME); tagServiceDef = getOrCreateServiceDef(store, EMBEDDED_SERVICEDEF_TAG_NAME); @@ -168,6 +173,10 @@ public class EmbeddedServiceDefsUtil { return getId(solrServiceDef); } + public long getNiFiServiceDefId() { + return getId(nifiServiceDef); + } + public long getTagServiceDefId() { return getId(tagServiceDef); } private long getId(RangerServiceDef serviceDef) { http://git-wip-us.apache.org/repos/asf/incubator-ranger/blob/b64218a3/agents-common/src/main/resources/service-defs/ranger-servicedef-nifi.json ---------------------------------------------------------------------- diff --git a/agents-common/src/main/resources/service-defs/ranger-servicedef-nifi.json b/agents-common/src/main/resources/service-defs/ranger-servicedef-nifi.json new file mode 100644 index 0000000..b81785d --- /dev/null +++ b/agents-common/src/main/resources/service-defs/ranger-servicedef-nifi.json @@ -0,0 +1,162 @@ +{ + "id":10, + "name":"nifi", + "implClass":"org.apache.ranger.services.nifi.RangerServiceNiFi", + "label":"NIFI", + "description":"NiFi", + "resources":[ + { + "itemId":100, + "name":"nifi-resource", + "type":"string", + "level":10, + "parent":"", + "mandatory":true, + "lookupSupported":true, + "recursiveSupported":false, + "excludesSupported":true, + "matcher":"org.apache.ranger.plugin.resourcematcher.RangerDefaultResourceMatcher", + "matcherOptions":{ + "wildCard":true, + "ignoreCase":true + }, + "validationRegEx":"", + "validationMessage":"", + "uiHint":"", + "label":"NiFi Resource Identifier", + "description":"NiFi Resource" + } + + ], + "accessTypes":[ + { + "itemId":100, + "name":"READ", + "label":"Read" + }, + { + "itemId":200, + "name":"WRITE", + "label":"Write" + } + ], + "configs":[ + { + "itemId":400, + "name":"nifi.url", + "type":"string", + "mandatory":true, + "defaultValue":"http://localhost:8080/nifi-api/resources", + "validationRegEx":"", + "validationMessage":"", + "uiHint":"The URL of the NiFi REST API that provides the available resources.", + "label":"NiFi URL" + }, + { + "itemId": 410, + "name": "nifi.authentication", + "type": "enum", + "subType": "authType", + "mandatory": true, + "validationRegEx":"", + "validationMessage": "", + "uiHint":"", + "label": "Authentication Type", + "defaultValue": "NONE" + }, + { + "itemId":500, + "name":"nifi.ssl.keystore", + "type":"string", + "mandatory":false, + "defaultValue":"", + "validationRegEx":"", + "validationMessage":"", + "uiHint":"", + "label":"Keystore" + }, + { + "itemId":510, + "name":"nifi.ssl.keystoreType", + "type":"string", + "mandatory":false, + "defaultValue":"", + "validationRegEx":"", + "validationMessage":"", + "uiHint":"", + "label":"Keystore Type" + }, + { + "itemId":520, + "name":"nifi.ssl.keystorePassword", + "type":"password", + "mandatory":false, + "defaultValue":"", + "validationRegEx":"", + "validationMessage":"", + "uiHint":"", + "label":"Keystore Password" + }, + { + "itemId":530, + "name":"nifi.ssl.truststore", + "type":"string", + "mandatory":false, + "defaultValue":"", + "validationRegEx":"", + "validationMessage":"", + "uiHint":"", + "label":"Truststore" + }, + { + "itemId":540, + "name":"nifi.ssl.truststoreType", + "type":"string", + "mandatory":false, + "defaultValue":"", + "validationRegEx":"", + "validationMessage":"", + "uiHint":"", + "label":"Truststore Type" + }, + { + "itemId":550, + "name":"nifi.ssl.truststorePassword", + "type":"password", + "mandatory":false, + "defaultValue":"", + "validationRegEx":"", + "validationMessage":"", + "uiHint":"", + "label":"Truststore Password" + } + ], + "enums": + [ + { + "itemId": 1, + "name": "authType", + "elements": + [ + { + "itemId": 1, + "name": "NONE", + "label": "None" + }, + { + "itemId": 2, + "name": "SSL", + "label": "SSL" + } + ], + + "defaultIndex": 0 + } + ], + "contextEnrichers":[ + + ], + "policyConditions":[ + + ] +} \ No newline at end of file http://git-wip-us.apache.org/repos/asf/incubator-ranger/blob/b64218a3/plugin-nifi/.gitignore ---------------------------------------------------------------------- diff --git a/plugin-nifi/.gitignore b/plugin-nifi/.gitignore new file mode 100644 index 0000000..ea8c4bf --- /dev/null +++ b/plugin-nifi/.gitignore @@ -0,0 +1 @@ +/target http://git-wip-us.apache.org/repos/asf/incubator-ranger/blob/b64218a3/plugin-nifi/pom.xml ---------------------------------------------------------------------- diff --git a/plugin-nifi/pom.xml b/plugin-nifi/pom.xml new file mode 100644 index 0000000..740b7f9 --- /dev/null +++ b/plugin-nifi/pom.xml @@ -0,0 +1,62 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + 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. +--> +<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> + <modelVersion>4.0.0</modelVersion> + <artifactId>ranger-nifi-plugin</artifactId> + <name>NiFi Security Plugin</name> + <description>NiFi Security Plugin</description> + <packaging>jar</packaging> + <properties> + <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> + </properties> + <parent> + <groupId>org.apache.ranger</groupId> + <artifactId>ranger</artifactId> + <version>0.6.0-SNAPSHOT</version> + <relativePath>..</relativePath> + </parent> + <dependencies> + <dependency> + <groupId>org.apache.ranger</groupId> + <artifactId>ranger-plugins-common</artifactId> + <version>${project.version}</version> + </dependency> + <dependency> + <groupId>org.apache.ranger</groupId> + <artifactId>ranger-plugins-audit</artifactId> + <version>${project.version}</version> + </dependency> + <dependency> + <groupId>org.apache.ranger</groupId> + <artifactId>credentialbuilder</artifactId> + <version>${project.version}</version> + </dependency> + + <dependency> + <groupId>junit</groupId> + <artifactId>junit</artifactId> + <scope>test</scope> + </dependency> + <dependency> + <groupId>org.mockito</groupId> + <artifactId>mockito-all</artifactId> + <version>${mockito.version}</version> + <scope>test</scope> + </dependency> + </dependencies> +</project> http://git-wip-us.apache.org/repos/asf/incubator-ranger/blob/b64218a3/plugin-nifi/src/main/java/org/apache/ranger/services/nifi/RangerServiceNiFi.java ---------------------------------------------------------------------- diff --git a/plugin-nifi/src/main/java/org/apache/ranger/services/nifi/RangerServiceNiFi.java b/plugin-nifi/src/main/java/org/apache/ranger/services/nifi/RangerServiceNiFi.java new file mode 100644 index 0000000..4f38f42 --- /dev/null +++ b/plugin-nifi/src/main/java/org/apache/ranger/services/nifi/RangerServiceNiFi.java @@ -0,0 +1,71 @@ +/* + * 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.ranger.services.nifi; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.apache.ranger.plugin.service.RangerBaseService; +import org.apache.ranger.plugin.service.ResourceLookupContext; +import org.apache.ranger.services.nifi.client.NiFiClient; +import org.apache.ranger.services.nifi.client.NiFiConnectionMgr; + +import java.util.HashMap; +import java.util.List; + +/** + * RangerService for Apache NiFi. + */ +public class RangerServiceNiFi extends RangerBaseService { + + private static final Log LOG = LogFactory.getLog(RangerServiceNiFi.class); + + @Override + public HashMap<String, Object> validateConfig() throws Exception { + HashMap<String, Object> ret = new HashMap<>(); + String serviceName = getServiceName(); + + if (LOG.isDebugEnabled()) { + LOG.debug("==> RangerServiceNiFi.validateConfig Service: (" + serviceName + " )"); + } + + if (configs != null) { + try { + ret = NiFiConnectionMgr.connectionTest(serviceName, configs); + } catch (Exception e) { + LOG.error("<== RangerServiceNiFi.validateConfig Error:", e); + throw e; + } + } else { + throw new IllegalStateException("No Configuration found"); + } + + if (LOG.isDebugEnabled()) { + LOG.debug("<== RangerServiceNiFi.validateConfig Response : (" + ret + " )"); + } + + return ret; + } + + @Override + public List<String> lookupResource(ResourceLookupContext context) throws Exception { + final NiFiClient client = NiFiConnectionMgr.getNiFiClient(serviceName, configs); + return client.getResources(context); + } + +} http://git-wip-us.apache.org/repos/asf/incubator-ranger/blob/b64218a3/plugin-nifi/src/main/java/org/apache/ranger/services/nifi/client/NiFiAuthType.java ---------------------------------------------------------------------- diff --git a/plugin-nifi/src/main/java/org/apache/ranger/services/nifi/client/NiFiAuthType.java b/plugin-nifi/src/main/java/org/apache/ranger/services/nifi/client/NiFiAuthType.java new file mode 100644 index 0000000..47267a4 --- /dev/null +++ b/plugin-nifi/src/main/java/org/apache/ranger/services/nifi/client/NiFiAuthType.java @@ -0,0 +1,29 @@ +/* + * 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.ranger.services.nifi.client; + +/** + * Possible authentication types for NiFi. + */ +public enum NiFiAuthType { + + NONE, + SSL + +} http://git-wip-us.apache.org/repos/asf/incubator-ranger/blob/b64218a3/plugin-nifi/src/main/java/org/apache/ranger/services/nifi/client/NiFiClient.java ---------------------------------------------------------------------- diff --git a/plugin-nifi/src/main/java/org/apache/ranger/services/nifi/client/NiFiClient.java b/plugin-nifi/src/main/java/org/apache/ranger/services/nifi/client/NiFiClient.java new file mode 100644 index 0000000..1c21c0e --- /dev/null +++ b/plugin-nifi/src/main/java/org/apache/ranger/services/nifi/client/NiFiClient.java @@ -0,0 +1,217 @@ +/* + * 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.ranger.services.nifi.client; + +import com.sun.jersey.api.client.Client; +import com.sun.jersey.api.client.ClientResponse; +import com.sun.jersey.api.client.WebResource; +import com.sun.jersey.api.client.config.ClientConfig; +import com.sun.jersey.api.client.config.DefaultClientConfig; +import com.sun.jersey.client.urlconnection.HTTPSProperties; +import org.apache.commons.io.IOUtils; +import org.apache.commons.lang.StringUtils; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.apache.ranger.plugin.client.BaseClient; +import org.apache.ranger.plugin.service.ResourceLookupContext; +import org.codehaus.jackson.JsonNode; +import org.codehaus.jackson.map.ObjectMapper; + +import javax.net.ssl.HostnameVerifier; +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLPeerUnverifiedException; +import javax.net.ssl.SSLSession; +import javax.ws.rs.core.Response; +import java.security.cert.Certificate; +import java.security.cert.CertificateParsingException; +import java.security.cert.X509Certificate; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; + +/** + * Client to communicate with NiFi and retrieve available resources. + */ +public class NiFiClient { + + private static final Log LOG = LogFactory.getLog(NiFiClient.class) ; + + static final String SUCCESS_MSG = "ConnectionTest Successful"; + static final String FAILURE_MSG = "Unable to retrieve any resources using given parameters. "; + + private final String url; + private final SSLContext sslContext; + private final HostnameVerifier hostnameVerifier; + private final ObjectMapper mapper = new ObjectMapper(); + + public NiFiClient(final String url, final SSLContext sslContext) { + this.url = url; + this.sslContext = sslContext; + this.hostnameVerifier = new NiFiHostnameVerifier(); + } + + public HashMap<String, Object> connectionTest() { + String errMsg = ""; + boolean connectivityStatus; + HashMap<String, Object> responseData = new HashMap<>(); + + try { + final WebResource resource = getWebResource(); + final ClientResponse response = getResponse(resource, "application/json"); + + if (LOG.isDebugEnabled()) { + LOG.debug("Got response from NiFi with status code " + response.getStatus()); + } + + if (Response.Status.OK.getStatusCode() == response.getStatus()) { + connectivityStatus = true; + } else { + connectivityStatus = false; + errMsg = "Status Code = " + response.getStatus(); + } + + } catch (Exception e) { + LOG.error("Connection to NiFi failed due to " + e.getMessage(), e); + connectivityStatus = false; + errMsg = e.getMessage(); + } + + if (connectivityStatus) { + BaseClient.generateResponseDataMap(connectivityStatus, SUCCESS_MSG, SUCCESS_MSG, null, null, responseData); + } else { + BaseClient.generateResponseDataMap(connectivityStatus, FAILURE_MSG, FAILURE_MSG + errMsg, null, null, responseData); + } + + if (LOG.isDebugEnabled()) { + LOG.debug("Response Data - " + responseData); + } + + return responseData; + } + + public List<String> getResources(ResourceLookupContext context) throws Exception { + final WebResource resource = getWebResource(); + final ClientResponse response = getResponse(resource, "application/json"); + + if (Response.Status.OK.getStatusCode() != response.getStatus()) { + String errorMsg = IOUtils.toString(response.getEntityInputStream()); + throw new Exception("Unable to retrieve resources from NiFi due to: " + errorMsg); + } + + JsonNode rootNode = mapper.readTree(response.getEntityInputStream()); + if (rootNode == null) { + throw new Exception("Unable to retrieve resources from NiFi"); + } + + JsonNode resourcesNode = rootNode.findValue("resources"); + List<String> identifiers = resourcesNode.findValuesAsText("identifier"); + + final String userInput = context.getUserInput(); + if (StringUtils.isBlank(userInput)) { + return identifiers; + } else { + List<String> filteredIdentifiers = new ArrayList<>(); + + for (String identifier : identifiers) { + if (identifier.contains(userInput)) { + filteredIdentifiers.add(identifier); + } + } + + return filteredIdentifiers; + } + } + + protected WebResource getWebResource() { + final ClientConfig config = new DefaultClientConfig(); + if (sslContext != null) { + config.getProperties().put(HTTPSProperties.PROPERTY_HTTPS_PROPERTIES, + new HTTPSProperties(hostnameVerifier, sslContext)); + } + + final Client client = Client.create(config); + return client.resource(url); + } + + protected ClientResponse getResponse(WebResource resource, String accept) { + return resource.accept(accept).get(ClientResponse.class); + } + + public String getUrl() { + return url; + } + + public SSLContext getSslContext() { + return sslContext; + } + + public HostnameVerifier getHostnameVerifier() { + return hostnameVerifier; + } + + /** + * Custom hostname verifier that checks subject alternative names against the hostname of the URI. + */ + private static class NiFiHostnameVerifier implements HostnameVerifier { + + @Override + public boolean verify(final String hostname, final SSLSession ssls) { + try { + for (final Certificate peerCertificate : ssls.getPeerCertificates()) { + if (peerCertificate instanceof X509Certificate) { + final X509Certificate x509Cert = (X509Certificate) peerCertificate; + final List<String> subjectAltNames = getSubjectAlternativeNames(x509Cert); + if (subjectAltNames.contains(hostname.toLowerCase())) { + return true; + } + } + } + } catch (final SSLPeerUnverifiedException | CertificateParsingException ex) { + LOG.warn("Hostname Verification encountered exception verifying hostname due to: " + ex, ex); + } + + return false; + } + + private List<String> getSubjectAlternativeNames(final X509Certificate certificate) throws CertificateParsingException { + final Collection<List<?>> altNames = certificate.getSubjectAlternativeNames(); + if (altNames == null) { + return new ArrayList<>(); + } + + final List<String> result = new ArrayList<>(); + for (final List<?> generalName : altNames) { + /** + * generalName has the name type as the first element a String or byte array for the second element. We return any general names that are String types. + * + * We don't inspect the numeric name type because some certificates incorrectly put IPs and DNS names under the wrong name types. + */ + final Object value = generalName.get(1); + if (value instanceof String) { + result.add(((String) value).toLowerCase()); + } + + } + + return result; + } + } + +} http://git-wip-us.apache.org/repos/asf/incubator-ranger/blob/b64218a3/plugin-nifi/src/main/java/org/apache/ranger/services/nifi/client/NiFiConfigs.java ---------------------------------------------------------------------- diff --git a/plugin-nifi/src/main/java/org/apache/ranger/services/nifi/client/NiFiConfigs.java b/plugin-nifi/src/main/java/org/apache/ranger/services/nifi/client/NiFiConfigs.java new file mode 100644 index 0000000..cc68710 --- /dev/null +++ b/plugin-nifi/src/main/java/org/apache/ranger/services/nifi/client/NiFiConfigs.java @@ -0,0 +1,37 @@ +/* + * 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.ranger.services.nifi.client; + +/** + * Config property names from the NiFi service definition. + */ +public interface NiFiConfigs { + + String NIFI_URL = "nifi.url"; + String NIFI_AUTHENTICATION_TYPE = "nifi.authentication"; + + String NIFI_SSL_KEYSTORE = "nifi.ssl.keystore"; + String NIFI_SSL_KEYSTORE_TYPE = "nifi.ssl.keystoreType"; + String NIFI_SSL_KEYSTORE_PASSWORD = "nifi.ssl.keystorePassword"; + + String NIFI_SSL_TRUSTSTORE = "nifi.ssl.truststore"; + String NIFI_SSL_TRUSTSTORE_TYPE = "nifi.ssl.truststoreType"; + String NIFI_SSL_TRUSTSTORE_PASSWORD = "nifi.ssl.truststorePassword"; + +} http://git-wip-us.apache.org/repos/asf/incubator-ranger/blob/b64218a3/plugin-nifi/src/main/java/org/apache/ranger/services/nifi/client/NiFiConnectionMgr.java ---------------------------------------------------------------------- diff --git a/plugin-nifi/src/main/java/org/apache/ranger/services/nifi/client/NiFiConnectionMgr.java b/plugin-nifi/src/main/java/org/apache/ranger/services/nifi/client/NiFiConnectionMgr.java new file mode 100644 index 0000000..739bef6 --- /dev/null +++ b/plugin-nifi/src/main/java/org/apache/ranger/services/nifi/client/NiFiConnectionMgr.java @@ -0,0 +1,157 @@ +/* + * 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.ranger.services.nifi.client; + + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import javax.net.ssl.KeyManagerFactory; +import javax.net.ssl.SSLContext; +import javax.net.ssl.TrustManagerFactory; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.URL; +import java.security.KeyManagementException; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; +import java.security.UnrecoverableKeyException; +import java.security.cert.CertificateException; +import java.util.HashMap; +import java.util.Map; + +/** + * Creates a NiFiClient and provides method to test a connection to NiFi. + */ +public class NiFiConnectionMgr { + + private static final Log LOG = LogFactory.getLog(NiFiConnectionMgr.class); + + static final String INVALID_URL_MSG = "NiFi URL must be a valid URL of the form " + + "http(s)://<hostname>(:<port>)/nifi-api/resources"; + + + static public NiFiClient getNiFiClient(String serviceName, Map<String, String> configs) throws Exception { + final String url = configs.get(NiFiConfigs.NIFI_URL); + validateNotBlank(url, "NiFi URL is required for " + serviceName); + validateUrl(url); + + final String authTypeStr = configs.get(NiFiConfigs.NIFI_AUTHENTICATION_TYPE); + validateNotBlank(authTypeStr, "Authentication Type is required for " + serviceName); + + final NiFiAuthType authType = NiFiAuthType.valueOf(authTypeStr); + LOG.debug("NiFiAuthType is " + authType.name()); + + SSLContext sslContext = null; + + if (authType == NiFiAuthType.SSL) { + + if (!url.startsWith("https")) { + throw new IllegalArgumentException("Authentication Type of SSL requires an https URL"); + } + + final String keystore = configs.get(NiFiConfigs.NIFI_SSL_KEYSTORE); + final String keystoreType = configs.get(NiFiConfigs.NIFI_SSL_KEYSTORE_TYPE); + final String keystorePassword = configs.get(NiFiConfigs.NIFI_SSL_KEYSTORE_PASSWORD); + + validateNotBlank(keystore, "Keystore is required for " + serviceName + " with Authentication Type of SSL"); + validateNotBlank(keystoreType, "Keystore Type is required for " + serviceName + " with Authentication Type of SSL"); + validateNotBlank(keystorePassword, "Keystore Password is required for " + serviceName + " with Authentication Type of SSL"); + + final String truststore = configs.get(NiFiConfigs.NIFI_SSL_TRUSTSTORE); + final String truststoreType = configs.get(NiFiConfigs.NIFI_SSL_TRUSTSTORE_TYPE); + final String truststorePassword = configs.get(NiFiConfigs.NIFI_SSL_TRUSTSTORE_PASSWORD); + + validateNotBlank(truststore, "Truststore is required for " + serviceName + " with Authentication Type of SSL"); + validateNotBlank(truststoreType, "Truststore Type is required for " + serviceName + " with Authentication Type of SSL"); + validateNotBlank(truststorePassword, "Truststore Password is required for " + serviceName + " with Authentication Type of SSL"); + + LOG.debug("Creating SSLContext for NiFi connection"); + + sslContext = createSslContext( + keystore.trim(), + keystorePassword.trim().toCharArray(), + keystoreType.trim(), + truststore.trim(), + truststorePassword.trim().toCharArray(), + truststoreType.trim(), + "TLS"); + } + + return new NiFiClient(url.trim(), sslContext); + } + + public static HashMap<String, Object> connectionTest(String serviceName, Map<String, String> configs) throws Exception { + NiFiClient client = getNiFiClient(serviceName, configs); + return client.connectionTest(); + } + + private static void validateNotBlank(final String input, final String message) { + if (input == null || input.trim().isEmpty()) { + throw new IllegalArgumentException(message); + } + } + + private static void validateUrl(String url) { + URI nifiUri; + try { + nifiUri = new URI(url); + if (!nifiUri.getPath().endsWith("nifi-api/resources")) { + throw new IllegalArgumentException(INVALID_URL_MSG); + } + } catch (URISyntaxException urie) { + throw new IllegalArgumentException(INVALID_URL_MSG); + } + } + + private static SSLContext createSslContext( + final String keystore, final char[] keystorePasswd, final String keystoreType, + final String truststore, final char[] truststorePasswd, final String truststoreType, + final String protocol) + throws KeyStoreException, IOException, NoSuchAlgorithmException, CertificateException, + UnrecoverableKeyException, KeyManagementException { + + // prepare the keystore + final KeyStore keyStore = KeyStore.getInstance(keystoreType); + try (final InputStream keyStoreStream = new FileInputStream(keystore)) { + keyStore.load(keyStoreStream, keystorePasswd); + } + final KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm()); + keyManagerFactory.init(keyStore, keystorePasswd); + + // prepare the truststore + final KeyStore trustStore = KeyStore.getInstance(truststoreType); + try (final InputStream trustStoreStream = new FileInputStream(truststore)) { + trustStore.load(trustStoreStream, truststorePasswd); + } + final TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); + trustManagerFactory.init(trustStore); + + // initialize the ssl context + final SSLContext sslContext = SSLContext.getInstance(protocol); + sslContext.init(keyManagerFactory.getKeyManagers(), trustManagerFactory.getTrustManagers(), new SecureRandom()); + return sslContext; + } + +} http://git-wip-us.apache.org/repos/asf/incubator-ranger/blob/b64218a3/plugin-nifi/src/test/java/org/apache/ranger/services/nifi/client/TestNiFiClient.java ---------------------------------------------------------------------- diff --git a/plugin-nifi/src/test/java/org/apache/ranger/services/nifi/client/TestNiFiClient.java b/plugin-nifi/src/test/java/org/apache/ranger/services/nifi/client/TestNiFiClient.java new file mode 100644 index 0000000..051c940 --- /dev/null +++ b/plugin-nifi/src/test/java/org/apache/ranger/services/nifi/client/TestNiFiClient.java @@ -0,0 +1,196 @@ +/* + * 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.ranger.services.nifi.client; + +import com.sun.jersey.api.client.ClientResponse; +import com.sun.jersey.api.client.WebResource; +import org.apache.ranger.plugin.service.ResourceLookupContext; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mockito; + +import javax.ws.rs.core.Response; +import java.io.ByteArrayInputStream; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; + +import static org.mockito.Mockito.when; + +public class TestNiFiClient { + + private static final String RESOURCES_RESPONSE = "{\n" + + " \"revision\": {\n" + + " \"clientId\": \"0daac173-025c-4aa7-b644-97f7b10435d2\"\n" + + " },\n" + + " \"resources\": [\n" + + " {\n" + + " \"identifier\": \"/system\",\n" + + " \"name\": \"System\"\n" + + " },\n" + + " {\n" + + " \"identifier\": \"/controller\",\n" + + " \"name\": \"Controller\"\n" + + " },\n" + + " {\n" + + " \"identifier\": \"/flow\",\n" + + " \"name\": \"NiFi Flow\"\n" + + " },\n" + + " {\n" + + " \"identifier\": \"/provenance\",\n" + + " \"name\": \"Provenance\"\n" + + " },\n" + + " {\n" + + " \"identifier\": \"/proxy\",\n" + + " \"name\": \"Proxy User Requests\"\n" + + " },\n" + + " {\n" + + " \"identifier\": \"/resources\",\n" + + " \"name\": \"NiFi Resources\"\n" + + " }\n" + + " ]\n" + + "}"; + + private NiFiClient niFiClient; + + @Before + public void setup() { + niFiClient = new MockNiFiClient(RESOURCES_RESPONSE, 200); + } + + @Test + public void testGetResourcesNoUserInput() throws Exception { + ResourceLookupContext resourceLookupContext = Mockito.mock(ResourceLookupContext.class); + when(resourceLookupContext.getUserInput()).thenReturn(""); + + final List<String> expectedResources = new ArrayList<>(); + expectedResources.add("/system"); + expectedResources.add("/controller"); + expectedResources.add("/flow"); + expectedResources.add("/provenance"); + expectedResources.add("/proxy"); + expectedResources.add("/resources"); + + List<String> resources = niFiClient.getResources(resourceLookupContext); + Assert.assertNotNull(resources); + Assert.assertEquals(expectedResources.size(), resources.size()); + + resources.removeAll(expectedResources); + Assert.assertEquals(0, resources.size()); + } + + @Test + public void testGetResourcesWithUserInputBeginning() throws Exception { + ResourceLookupContext resourceLookupContext = Mockito.mock(ResourceLookupContext.class); + when(resourceLookupContext.getUserInput()).thenReturn("/pr"); + + final List<String> expectedResources = new ArrayList<>(); + expectedResources.add("/provenance"); + expectedResources.add("/proxy"); + + List<String> resources = niFiClient.getResources(resourceLookupContext); + Assert.assertNotNull(resources); + Assert.assertEquals(expectedResources.size(), resources.size()); + + resources.removeAll(expectedResources); + Assert.assertEquals(0, resources.size()); + } + + @Test + public void testGetResourcesWithUserInputAnywhere() throws Exception { + ResourceLookupContext resourceLookupContext = Mockito.mock(ResourceLookupContext.class); + when(resourceLookupContext.getUserInput()).thenReturn("trol"); + + final List<String> expectedResources = new ArrayList<>(); + expectedResources.add("/controller"); + + List<String> resources = niFiClient.getResources(resourceLookupContext); + Assert.assertNotNull(resources); + Assert.assertEquals(expectedResources.size(), resources.size()); + + resources.removeAll(expectedResources); + Assert.assertEquals(0, resources.size()); + } + + @Test + public void testGetResourcesErrorResponse() throws Exception { + final String errorMsg = "unknown error"; + niFiClient = new MockNiFiClient(errorMsg, Response.Status.BAD_REQUEST.getStatusCode()); + + ResourceLookupContext resourceLookupContext = Mockito.mock(ResourceLookupContext.class); + when(resourceLookupContext.getUserInput()).thenReturn(""); + + try { + niFiClient.getResources(resourceLookupContext); + Assert.fail("should have thrown exception"); + } catch (Exception e) { + Assert.assertTrue(e.getMessage().contains(errorMsg)); + } + } + + @Test + public void testConnectionTestSuccess() { + HashMap<String, Object> ret = niFiClient.connectionTest(); + Assert.assertNotNull(ret); + Assert.assertEquals(NiFiClient.SUCCESS_MSG, ret.get("message")); + } + + @Test + public void testConnectionTestFailure() { + final String errorMsg = "unknown error"; + niFiClient = new MockNiFiClient(errorMsg, Response.Status.BAD_REQUEST.getStatusCode()); + + HashMap<String, Object> ret = niFiClient.connectionTest(); + Assert.assertNotNull(ret); + Assert.assertEquals(NiFiClient.FAILURE_MSG, ret.get("message")); + } + + + /** + * Extend NiFiClient to return mock responses. + */ + private static final class MockNiFiClient extends NiFiClient { + + private int statusCode; + private String responseEntity; + + public MockNiFiClient(String responseEntity, int statusCode) { + super("http://localhost:8080/nifi-api/resources", null); + this.statusCode = statusCode; + this.responseEntity = responseEntity; + } + + @Override + protected WebResource getWebResource() { + return Mockito.mock(WebResource.class); + } + + @Override + protected ClientResponse getResponse(WebResource resource, String accept) { + ClientResponse response = Mockito.mock(ClientResponse.class); + when(response.getStatus()).thenReturn(statusCode); + when(response.getEntityInputStream()).thenReturn(new ByteArrayInputStream( + responseEntity.getBytes(StandardCharsets.UTF_8) + )); + return response; + } + } +} http://git-wip-us.apache.org/repos/asf/incubator-ranger/blob/b64218a3/plugin-nifi/src/test/java/org/apache/ranger/services/nifi/client/TestNiFiConnectionMgr.java ---------------------------------------------------------------------- diff --git a/plugin-nifi/src/test/java/org/apache/ranger/services/nifi/client/TestNiFiConnectionMgr.java b/plugin-nifi/src/test/java/org/apache/ranger/services/nifi/client/TestNiFiConnectionMgr.java new file mode 100644 index 0000000..1726854 --- /dev/null +++ b/plugin-nifi/src/test/java/org/apache/ranger/services/nifi/client/TestNiFiConnectionMgr.java @@ -0,0 +1,124 @@ +/* + * 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.ranger.services.nifi.client; + +import org.junit.Assert; +import org.junit.Test; + +import java.io.FileNotFoundException; +import java.util.HashMap; +import java.util.Map; + +public class TestNiFiConnectionMgr { + + @Test (expected = IllegalArgumentException.class) + public void testValidURLWithWrongEndPoint() throws Exception { + final String nifiUrl = "http://localhost:8080/nifi"; + + Map<String,String> configs = new HashMap<>(); + configs.put(NiFiConfigs.NIFI_URL, nifiUrl); + configs.put(NiFiConfigs.NIFI_AUTHENTICATION_TYPE, NiFiAuthType.NONE.name()); + + NiFiConnectionMgr.getNiFiClient("nifi", configs); + } + + @Test (expected = IllegalArgumentException.class) + public void testInvalidURL() throws Exception { + final String nifiUrl = "not a url"; + + Map<String,String> configs = new HashMap<>(); + configs.put(NiFiConfigs.NIFI_URL, nifiUrl); + configs.put(NiFiConfigs.NIFI_AUTHENTICATION_TYPE, NiFiAuthType.NONE.name()); + + NiFiConnectionMgr.getNiFiClient("nifi", configs); + } + + @Test + public void testAuthTypeNone() throws Exception { + final String nifiUrl = "http://localhost:8080/nifi-api/resources"; + + Map<String,String> configs = new HashMap<>(); + configs.put(NiFiConfigs.NIFI_URL, nifiUrl); + configs.put(NiFiConfigs.NIFI_AUTHENTICATION_TYPE, NiFiAuthType.NONE.name()); + + NiFiClient client = NiFiConnectionMgr.getNiFiClient("nifi", configs); + Assert.assertNotNull(client); + Assert.assertEquals(nifiUrl, client.getUrl()); + Assert.assertNull(client.getSslContext()); + } + + @Test(expected = IllegalArgumentException.class) + public void testAuthTypeNoneMissingURL() throws Exception { + Map<String,String> configs = new HashMap<>(); + configs.put(NiFiConfigs.NIFI_URL, null); + configs.put(NiFiConfigs.NIFI_AUTHENTICATION_TYPE, NiFiAuthType.NONE.name()); + + NiFiConnectionMgr.getNiFiClient("nifi", configs); + } + + @Test(expected = FileNotFoundException.class) + public void testAuthTypeSSL() throws Exception { + final String nifiUrl = "https://localhost:8080/nifi-api/resources"; + + Map<String,String> configs = new HashMap<>(); + configs.put(NiFiConfigs.NIFI_URL, nifiUrl); + configs.put(NiFiConfigs.NIFI_AUTHENTICATION_TYPE, NiFiAuthType.SSL.name()); + + configs.put(NiFiConfigs.NIFI_SSL_KEYSTORE, "src/test/resources/missing.jks"); + configs.put(NiFiConfigs.NIFI_SSL_KEYSTORE_PASSWORD, "password"); + configs.put(NiFiConfigs.NIFI_SSL_KEYSTORE_TYPE, "JKS"); + + configs.put(NiFiConfigs.NIFI_SSL_TRUSTSTORE, "src/test/resources/missing.jks"); + configs.put(NiFiConfigs.NIFI_SSL_TRUSTSTORE_PASSWORD, "password"); + configs.put(NiFiConfigs.NIFI_SSL_TRUSTSTORE_TYPE, "JKS"); + + NiFiConnectionMgr.getNiFiClient("nifi", configs); + } + + @Test(expected = IllegalArgumentException.class) + public void testAuthTypeSSLWithNonHttpsUrl() throws Exception { + final String nifiUrl = "http://localhost:8080/nifi-api/resources"; + + Map<String,String> configs = new HashMap<>(); + configs.put(NiFiConfigs.NIFI_URL, nifiUrl); + configs.put(NiFiConfigs.NIFI_AUTHENTICATION_TYPE, NiFiAuthType.SSL.name()); + + configs.put(NiFiConfigs.NIFI_SSL_KEYSTORE, "src/test/resources/missing.jks"); + configs.put(NiFiConfigs.NIFI_SSL_KEYSTORE_PASSWORD, "password"); + configs.put(NiFiConfigs.NIFI_SSL_KEYSTORE_TYPE, "JKS"); + + configs.put(NiFiConfigs.NIFI_SSL_TRUSTSTORE, "src/test/resources/missing.jks"); + configs.put(NiFiConfigs.NIFI_SSL_TRUSTSTORE_PASSWORD, "password"); + configs.put(NiFiConfigs.NIFI_SSL_TRUSTSTORE_TYPE, "JKS"); + + NiFiConnectionMgr.getNiFiClient("nifi", configs); + } + + @Test(expected = IllegalArgumentException.class) + public void testAuthTypeSSLMissingConfigs() throws Exception { + final String nifiUrl = "http://localhost:8080/nifi"; + + Map<String,String> configs = new HashMap<>(); + configs.put(NiFiConfigs.NIFI_URL, nifiUrl); + configs.put(NiFiConfigs.NIFI_AUTHENTICATION_TYPE, NiFiAuthType.SSL.name()); + + NiFiConnectionMgr.getNiFiClient("nifi", configs); + } + +} http://git-wip-us.apache.org/repos/asf/incubator-ranger/blob/b64218a3/pom.xml ---------------------------------------------------------------------- diff --git a/pom.xml b/pom.xml index eda7d7d..d6aa833 100644 --- a/pom.xml +++ b/pom.xml @@ -87,6 +87,7 @@ <module>security-admin</module> <module>plugin-kafka</module> <module>plugin-solr</module> + <module>plugin-nifi</module> <module>ugsync</module> <module>ugsync/ldapconfigchecktool/ldapconfigcheck</module> <module>unixauthclient</module> http://git-wip-us.apache.org/repos/asf/incubator-ranger/blob/b64218a3/src/main/assembly/admin-web.xml ---------------------------------------------------------------------- diff --git a/src/main/assembly/admin-web.xml b/src/main/assembly/admin-web.xml index 6aeb397..7fd2abf 100644 --- a/src/main/assembly/admin-web.xml +++ b/src/main/assembly/admin-web.xml @@ -296,6 +296,19 @@ <include>org.apache.ranger:ranger-solr-plugin</include> </includes> </moduleSet> + + <moduleSet> + <binaries> + <includeDependencies>true</includeDependencies> + <outputDirectory>/ews/webapp/WEB-INF/classes/ranger-plugins/nifi</outputDirectory> + <unpack>false</unpack> + <directoryMode>755</directoryMode> + <fileMode>644</fileMode> + </binaries> + <includes> + <include>org.apache.ranger:ranger-nifi-plugin</include> + </includes> + </moduleSet> </moduleSets> <fileSets>
