Merge pull request #252 from scottyaslan/NIFIREG-352
[NIFIREG-352] update frontend deps
This closes #252.
Signed-off-by: Andy LoPresto <alopresto@apache.org>
diff --git a/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/flow/VersionedFlowSnapshot.java b/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/flow/VersionedFlowSnapshot.java
index 4611b9e..bf6ad98 100644
--- a/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/flow/VersionedFlowSnapshot.java
+++ b/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/flow/VersionedFlowSnapshot.java
@@ -35,6 +35,11 @@
* is saved as a snapshot, representing information such as the name of the flow, the
* version of the flow, the timestamp when it was saved, the contents of the flow, etc.
* </p>
+ * <p>
+ * With the advent of download and upload flow in NiFi, this class is now used outside of
+ * NiFi Registry to represent a snapshot of an unversioned flow, which is purely the
+ * flow contents without any versioning or snapshot metadata.
+ * </p>
*/
@ApiModel
@XmlRootElement
@@ -52,7 +57,7 @@
private Map<String,ExternalControllerServiceReference> externalControllerServices;
// optional parameter contexts mapped by their name
- private Map<String,VersionedParameterContext> parameterContexts;;
+ private Map<String,VersionedParameterContext> parameterContexts;
// optional encoding version that clients may specify to track how the flow contents are encoded
private String flowEncodingVersion;
@@ -159,8 +164,14 @@
@Override
public String toString() {
- final String flowName = (flow == null ? "null" : flow.getName());
- return "VersionedFlowSnapshot[flowId=" + snapshotMetadata.getFlowIdentifier() + ", flowName=" + flowName
- + ", version=" + snapshotMetadata.getVersion() + ", comments=" + snapshotMetadata.getComments() + "]";
+ // snapshotMetadata and flow will be null when this is used to represent an unversioned flow
+ if (snapshotMetadata == null) {
+ return "VersionedFlowSnapshot[flowContentsId=" + flowContents.getIdentifier() + ", flowContentsName="
+ + flowContents.getName() + ", NoMetadataAvailable]";
+ } else {
+ final String flowName = (flow == null ? "null" : flow.getName());
+ return "VersionedFlowSnapshot[flowId=" + snapshotMetadata.getFlowIdentifier() + ", flowName=" + flowName
+ + ", version=" + snapshotMetadata.getVersion() + ", comments=" + snapshotMetadata.getComments() + "]";
+ }
}
-}
+}
\ No newline at end of file
diff --git a/nifi-registry-core/nifi-registry-framework/pom.xml b/nifi-registry-core/nifi-registry-framework/pom.xml
index 67f77c4..e8ef77b 100644
--- a/nifi-registry-core/nifi-registry-framework/pom.xml
+++ b/nifi-registry-core/nifi-registry-framework/pom.xml
@@ -300,7 +300,7 @@
<dependency>
<groupId>org.eclipse.jgit</groupId>
<artifactId>org.eclipse.jgit</artifactId>
- <version>4.11.8.201904181247-r</version>
+ <version>4.11.9.201909030838-r</version>
</dependency>
<dependency>
<groupId>commons-codec</groupId>
diff --git a/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/extension/ExtensionClassLoader.java b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/extension/ExtensionClassLoader.java
new file mode 100644
index 0000000..1411f29
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/extension/ExtensionClassLoader.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.nifi.registry.extension;
+
+import java.net.URL;
+import java.net.URLClassLoader;
+
+/**
+ * Extend URLClassLoader to keep track of the root directory.
+ */
+public class ExtensionClassLoader extends URLClassLoader {
+
+ private final String rootDir;
+
+ public ExtensionClassLoader(final String rootDir, final URL[] urls, final ClassLoader parent) {
+ super(urls, parent);
+ this.rootDir = rootDir;
+ }
+
+ public String getRootDir() {
+ return rootDir;
+ }
+}
diff --git a/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/extension/ExtensionCloseable.java b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/extension/ExtensionCloseable.java
index b24f950..0735f2c 100644
--- a/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/extension/ExtensionCloseable.java
+++ b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/extension/ExtensionCloseable.java
@@ -40,6 +40,13 @@
return closeable;
}
+ public static ExtensionCloseable withClassLoader(final ClassLoader componentClassLoader) {
+ final ClassLoader current = Thread.currentThread().getContextClassLoader();
+ final ExtensionCloseable closeable = new ExtensionCloseable(current);
+ Thread.currentThread().setContextClassLoader(componentClassLoader);
+ return closeable;
+ }
+
@Override
public void close() throws IOException {
if (toSet != null) {
diff --git a/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/extension/ExtensionManager.java b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/extension/ExtensionManager.java
index 455c833..edb3350 100644
--- a/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/extension/ExtensionManager.java
+++ b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/extension/ExtensionManager.java
@@ -33,7 +33,6 @@
import java.io.File;
import java.net.MalformedURLException;
import java.net.URL;
-import java.net.URLClassLoader;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
@@ -88,7 +87,7 @@
}
}
- public ClassLoader getExtensionClassLoader(final String canonicalClassName) {
+ public ExtensionClassLoader getExtensionClassLoader(final String canonicalClassName) {
if (StringUtils.isBlank(canonicalClassName)) {
throw new IllegalArgumentException("Class name can not be null");
}
@@ -199,20 +198,4 @@
return new ExtensionClassLoader(dir, urls, parentClassLoader);
}
- /**
- * Extend URLClassLoader to keep track of the root directory.
- */
- private static class ExtensionClassLoader extends URLClassLoader {
-
- private final String rootDir;
-
- public ExtensionClassLoader(final String rootDir, final URL[] urls, final ClassLoader parent) {
- super(urls, parent);
- this.rootDir = rootDir;
- }
-
- public String getRootDir() {
- return rootDir;
- }
- }
}
diff --git a/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/AuthorizerFactory.java b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/AuthorizerFactory.java
index 916892b..959e29e 100644
--- a/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/AuthorizerFactory.java
+++ b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/AuthorizerFactory.java
@@ -17,6 +17,7 @@
package org.apache.nifi.registry.security.authorization;
import org.apache.commons.lang3.StringUtils;
+import org.apache.nifi.registry.extension.ExtensionClassLoader;
import org.apache.nifi.registry.extension.ExtensionCloseable;
import org.apache.nifi.registry.extension.ExtensionManager;
import org.apache.nifi.registry.properties.NiFiRegistryProperties;
@@ -30,6 +31,7 @@
import org.apache.nifi.registry.security.authorization.generated.Prop;
import org.apache.nifi.registry.security.exception.SecurityProviderCreationException;
import org.apache.nifi.registry.security.exception.SecurityProviderDestructionException;
+import org.apache.nifi.registry.security.util.ClassLoaderUtils;
import org.apache.nifi.registry.security.util.XmlUtils;
import org.apache.nifi.registry.service.RegistryService;
import org.slf4j.Logger;
@@ -55,7 +57,11 @@
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
+import java.net.URL;
+import java.net.URLClassLoader;
+import java.util.Arrays;
import java.util.HashMap;
+import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
@@ -201,11 +207,12 @@
// configure each authorizer
for (final org.apache.nifi.registry.security.authorization.generated.Authorizer provider : authorizerConfiguration.getAuthorizer()) {
+ if (provider.getIdentifier().equals(authorizerIdentifier)) {
+ continue;
+ }
final Authorizer instance = authorizers.get(provider.getIdentifier());
- final Class authorizerClass = instance instanceof WrappedAuthorizer
- ? ((WrappedAuthorizer) instance).getBaseAuthorizer().getClass()
- : instance.getClass();
- try (ExtensionCloseable extCloseable = ExtensionCloseable.withComponentClassLoader(extensionManager, authorizerClass)) {
+ final ClassLoader instanceClassLoader = instance.getClass().getClassLoader();
+ try (final ExtensionCloseable extClosable = ExtensionCloseable.withClassLoader(instanceClassLoader)) {
instance.onConfigured(loadAuthorizerConfiguration(provider.getIdentifier(), provider.getProperty()));
}
}
@@ -216,7 +223,37 @@
// ensure it was found
if (authorizer == null) {
throw new AuthorizerFactoryException(String.format("The specified authorizer '%s' could not be found.", authorizerIdentifier));
+ } else {
+ // get the ClassLoader of the Authorizer before wrapping it with anything
+ final ClassLoader authorizerClassLoader = authorizer.getClass().getClassLoader();
+
+ // install integrity checks
+ authorizer = AuthorizerFactory.installIntegrityChecks(authorizer);
+
+ // load the configuration context for the selected authorizer
+ AuthorizerConfigurationContext authorizerConfigurationContext = null;
+ for (final org.apache.nifi.registry.security.authorization.generated.Authorizer provider : authorizerConfiguration.getAuthorizer()) {
+ if (provider.getIdentifier().equals(authorizerIdentifier)) {
+ authorizerConfigurationContext = loadAuthorizerConfiguration(provider.getIdentifier(), provider.getProperty());
+ break;
+ }
+ }
+
+ if (authorizerConfigurationContext == null) {
+ throw new IllegalStateException("Unable to load configuration for authorizer with id: " + authorizerIdentifier);
+ }
+
+ // configure the authorizer that is wrapped with integrity checks
+ // set the context ClassLoader the wrapped authorizer's ClassLoader
+ try (final ExtensionCloseable extClosable = ExtensionCloseable.withClassLoader(authorizerClassLoader)) {
+ authorizer.onConfigured(authorizerConfigurationContext);
+ }
+
+ // wrap the integrity checked Authorizer with the FrameworkAuthorizer
+ authorizer = createFrameworkAuthorizer(authorizer);
}
+
+
} catch (AuthorizerFactoryException e) {
throw e;
} catch (Exception e) {
@@ -281,7 +318,6 @@
}
private UserGroupProvider createUserGroupProvider(final String identifier, final String userGroupProviderClassName) throws Exception {
-
final UserGroupProvider instance;
final ClassLoader classLoader = extensionManager.getExtensionClassLoader(userGroupProviderClassName);
@@ -289,22 +325,24 @@
throw new IllegalStateException("Extension not found in any of the configured class loaders: " + userGroupProviderClassName);
}
- // attempt to load the class
- Class<?> rawUserGroupProviderClass = Class.forName(userGroupProviderClassName, true, classLoader);
- Class<? extends UserGroupProvider> userGroupProviderClass = rawUserGroupProviderClass.asSubclass(UserGroupProvider.class);
+ try (final ExtensionCloseable closeable = ExtensionCloseable.withClassLoader(classLoader)) {
+ // attempt to load the class
+ Class<?> rawUserGroupProviderClass = Class.forName(userGroupProviderClassName, true, classLoader);
+ Class<? extends UserGroupProvider> userGroupProviderClass = rawUserGroupProviderClass.asSubclass(UserGroupProvider.class);
- // otherwise create a new instance
- Constructor constructor = userGroupProviderClass.getConstructor();
- instance = (UserGroupProvider) constructor.newInstance();
+ // otherwise create a new instance
+ Constructor constructor = userGroupProviderClass.getConstructor();
+ instance = (UserGroupProvider) constructor.newInstance();
- // method injection
- performMethodInjection(instance, userGroupProviderClass);
+ // method injection
+ performMethodInjection(instance, userGroupProviderClass);
- // field injection
- performFieldInjection(instance, userGroupProviderClass);
+ // field injection
+ performFieldInjection(instance, userGroupProviderClass);
- // call post construction lifecycle event
- instance.initialize(new StandardAuthorizerInitializationContext(identifier, this, this, this));
+ // call post construction lifecycle event
+ instance.initialize(new StandardAuthorizerInitializationContext(identifier, this, this, this));
+ }
return instance;
}
@@ -317,22 +355,24 @@
throw new IllegalStateException("Extension not found in any of the configured class loaders: " + accessPolicyProviderClassName);
}
- // attempt to load the class
- Class<?> rawAccessPolicyProviderClass = Class.forName(accessPolicyProviderClassName, true, classLoader);
- Class<? extends AccessPolicyProvider> accessPolicyClass = rawAccessPolicyProviderClass.asSubclass(AccessPolicyProvider.class);
+ try (final ExtensionCloseable closeable = ExtensionCloseable.withClassLoader(classLoader)) {
+ // attempt to load the class
+ Class<?> rawAccessPolicyProviderClass = Class.forName(accessPolicyProviderClassName, true, classLoader);
+ Class<? extends AccessPolicyProvider> accessPolicyClass = rawAccessPolicyProviderClass.asSubclass(AccessPolicyProvider.class);
- // otherwise create a new instance
- Constructor constructor = accessPolicyClass.getConstructor();
- instance = (AccessPolicyProvider) constructor.newInstance();
+ // otherwise create a new instance
+ Constructor constructor = accessPolicyClass.getConstructor();
+ instance = (AccessPolicyProvider) constructor.newInstance();
- // method injection
- performMethodInjection(instance, accessPolicyClass);
+ // method injection
+ performMethodInjection(instance, accessPolicyClass);
- // field injection
- performFieldInjection(instance, accessPolicyClass);
+ // field injection
+ performFieldInjection(instance, accessPolicyClass);
- // call post construction lifecycle event
- instance.initialize(new StandardAuthorizerInitializationContext(identifier, this, this, this));
+ // call post construction lifecycle event
+ instance.initialize(new StandardAuthorizerInitializationContext(identifier, this, this, this));
+ }
return instance;
}
@@ -340,33 +380,51 @@
private Authorizer createAuthorizer(final String identifier, final String authorizerClassName, final String classpathResources) throws Exception {
final Authorizer instance;
- final ClassLoader classLoader = extensionManager.getExtensionClassLoader(authorizerClassName);
- if (classLoader == null) {
+ ClassLoader classLoader;
+
+ final ExtensionClassLoader extensionClassLoader = extensionManager.getExtensionClassLoader(authorizerClassName);
+ if (extensionClassLoader == null) {
throw new IllegalStateException("Extension not found in any of the configured class loaders: " + authorizerClassName);
}
- // attempt to load the class
- Class<?> rawAuthorizerClass = Class.forName(authorizerClassName, true, classLoader);
- Class<? extends Authorizer> authorizerClass = rawAuthorizerClass.asSubclass(Authorizer.class);
+ // if additional classpath resources were specified, replace with a new ClassLoader that contains
+ // the combined resources of the original ClassLoader + the additional resources
+ if (StringUtils.isNotEmpty(classpathResources)) {
+ logger.info(String.format("Replacing Authorizer ClassLoader for '%s' to include additional resources: %s", identifier, classpathResources));
+ final URL[] originalUrls = extensionClassLoader.getURLs();
+ final URL[] additionalUrls = ClassLoaderUtils.getURLsForClasspath(classpathResources, null, true);
- // otherwise create a new instance
- Constructor constructor = authorizerClass.getConstructor();
- instance = (Authorizer) constructor.newInstance();
+ final Set<URL> combinedUrls = new HashSet<>();
+ combinedUrls.addAll(Arrays.asList(originalUrls));
+ combinedUrls.addAll(Arrays.asList(additionalUrls));
- // method injection
- performMethodInjection(instance, authorizerClass);
+ final URL[] urls = combinedUrls.toArray(new URL[combinedUrls.size()]);
+ classLoader = new URLClassLoader(urls, extensionClassLoader.getParent());
+ } else {
+ // no additional resources so just use the ExtensionClassLoader
+ classLoader = extensionClassLoader;
+ }
- // field injection
- performFieldInjection(instance, authorizerClass);
+ try (final ExtensionCloseable closeable = ExtensionCloseable.withClassLoader(classLoader)) {
+ // attempt to load the class
+ Class<?> rawAuthorizerClass = Class.forName(authorizerClassName, true, classLoader);
+ Class<? extends Authorizer> authorizerClass = rawAuthorizerClass.asSubclass(Authorizer.class);
- // call post construction lifecycle event
- instance.initialize(new StandardAuthorizerInitializationContext(identifier, this, this, this));
+ // otherwise create a new instance
+ Constructor constructor = authorizerClass.getConstructor();
+ instance = (Authorizer) constructor.newInstance();
- // wrap the instance Authorizer with checks to ensure integrity of data
- final Authorizer integrityCheckAuthorizer = installIntegrityChecks(instance);
+ // method injection
+ performMethodInjection(instance, authorizerClass);
- // wrap the integrity checked Authorizer with the FrameworkAuthorizer
- return createFrameworkAuthorizer(integrityCheckAuthorizer);
+ // field injection
+ performFieldInjection(instance, authorizerClass);
+
+ // call post construction lifecycle event
+ instance.initialize(new StandardAuthorizerInitializationContext(identifier, this, this, this));
+ }
+
+ return instance;
}
private Authorizer createFrameworkAuthorizer(final Authorizer baseAuthorizer) {
@@ -611,7 +669,7 @@
@Override
public User addUser(User user) throws AuthorizationAccessException {
- if (tenantExists(baseConfigurableUserGroupProvider, user.getIdentifier(), user.getIdentity())) {
+ if (userExists(baseConfigurableUserGroupProvider, user.getIdentifier(), user.getIdentity())) {
throw new IllegalStateException(String.format("User/user group already exists with the identity '%s'.", user.getIdentity()));
}
return baseConfigurableUserGroupProvider.addUser(user);
@@ -624,7 +682,7 @@
@Override
public User updateUser(User user) throws AuthorizationAccessException {
- if (tenantExists(baseConfigurableUserGroupProvider, user.getIdentifier(), user.getIdentity())) {
+ if (userExists(baseConfigurableUserGroupProvider, user.getIdentifier(), user.getIdentity())) {
throw new IllegalStateException(String.format("User/user group already exists with the identity '%s'.", user.getIdentity()));
}
if (!baseConfigurableUserGroupProvider.isConfigurable(user)) {
@@ -651,7 +709,7 @@
@Override
public Group addGroup(Group group) throws AuthorizationAccessException {
- if (tenantExists(baseConfigurableUserGroupProvider, group.getIdentifier(), group.getName())) {
+ if (groupExists(baseConfigurableUserGroupProvider, group.getIdentifier(), group.getName())) {
throw new IllegalStateException(String.format("User/user group already exists with the identity '%s'.", group.getName()));
}
if (!allGroupUsersExist(baseUserGroupProvider, group)) {
@@ -667,7 +725,7 @@
@Override
public Group updateGroup(Group group) throws AuthorizationAccessException {
- if (tenantExists(baseConfigurableUserGroupProvider, group.getIdentifier(), group.getName())) {
+ if (groupExists(baseConfigurableUserGroupProvider, group.getIdentifier(), group.getName())) {
throw new IllegalStateException(String.format("User/user group already exists with the identity '%s'.", group.getName()));
}
if (!baseConfigurableUserGroupProvider.isConfigurable(group)) {
@@ -796,14 +854,14 @@
// ensure that only one group exists per identity
for (User user : userGroupProvider.getUsers()) {
- if (tenantExists(userGroupProvider, user.getIdentifier(), user.getIdentity())) {
+ if (userExists(userGroupProvider, user.getIdentifier(), user.getIdentity())) {
throw new SecurityProviderCreationException(String.format("Found multiple users/user groups with identity '%s'.", user.getIdentity()));
}
}
// ensure that only one group exists per identity
for (Group group : userGroupProvider.getGroups()) {
- if (tenantExists(userGroupProvider, group.getIdentifier(), group.getName())) {
+ if (groupExists(userGroupProvider, group.getIdentifier(), group.getName())) {
throw new SecurityProviderCreationException(String.format("Found multiple users/user groups with name '%s'.", group.getName()));
}
}
@@ -896,7 +954,7 @@
* @param identity identity of the tenant
* @return true if another user exists with the same identity, false otherwise
*/
- private static boolean tenantExists(final UserGroupProvider userGroupProvider, final String identifier, final String identity) {
+ private static boolean userExists(final UserGroupProvider userGroupProvider, final String identifier, final String identity) {
for (User user : userGroupProvider.getUsers()) {
if (!user.getIdentifier().equals(identifier)
&& user.getIdentity().equals(identity)) {
@@ -904,6 +962,10 @@
}
}
+ return false;
+ }
+
+ private static boolean groupExists(final UserGroupProvider userGroupProvider, final String identifier, final String identity) {
for (Group group : userGroupProvider.getGroups()) {
if (!group.getIdentifier().equals(identifier)
&& group.getName().equals(identity)) {
diff --git a/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/shell/NssShellCommands.java b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/shell/NssShellCommands.java
new file mode 100644
index 0000000..eef58b0
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/shell/NssShellCommands.java
@@ -0,0 +1,89 @@
+/*
+ * 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.registry.security.authorization.shell;
+
+/**
+ * Provides shell commands to read users and groups on NSS-enabled systems.
+ *
+ * See `man 5 nsswitch.conf` for more info.
+ */
+class NssShellCommands implements ShellCommandsProvider {
+ /**
+ * @return Shell command string that will return a list of users.
+ */
+ public String getUsersList() {
+ return "getent passwd | cut -f 1,3,4 -d ':'";
+ }
+
+ /**
+ * @return Shell command string that will return a list of groups.
+ */
+ public String getGroupsList() {
+ return "getent group | cut -f 1,3 -d ':'";
+ }
+
+ /**
+ * @param groupName name of group.
+ * @return Shell command string that will return a list of users for a group.
+ */
+ public String getGroupMembers(String groupName) {
+ return String.format("getent group %s | cut -f 4 -d ':'", groupName);
+ }
+
+ /**
+ * Gets the command for reading a single user by id.
+ *
+ * When executed, this command should output a single line, in the format used by `getUsersList`.
+ *
+ * @param userId name of user.
+ * @return Shell command string that will read a single user.
+ */
+ @Override
+ public String getUserById(String userId) {
+ return String.format("getent passwd %s | cut -f 1,3,4 -d ':'", userId);
+ }
+
+ /**
+ * This method reuses `getUserById` because the getent command is the same for
+ * both uid and username.
+ *
+ * @param userName name of user.
+ * @return Shell command string that will read a single user.
+ */
+ public String getUserByName(String userName) {
+ return getUserById(userName);
+ }
+
+ /**
+ * This method supports gid or group name because getent does.
+ *
+ * @param groupId name of group.
+ * @return Shell command string that will read a single group.
+ */
+ public String getGroupById(String groupId) {
+ return String.format("getent group %s | cut -f 1,3,4 -d ':'", groupId);
+ }
+
+ /**
+ * This gives exit code 0 on all tested distributions.
+ *
+ * @return Shell command string that will exit normally (0) on a suitable system.
+ */
+ public String getSystemCheck() {
+ return "getent --version";
+ }
+}
\ No newline at end of file
diff --git a/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/shell/OsxShellCommands.java b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/shell/OsxShellCommands.java
new file mode 100644
index 0000000..0591662
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/shell/OsxShellCommands.java
@@ -0,0 +1,81 @@
+/*
+ * 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.registry.security.authorization.shell;
+
+/**
+ * Provides shell commands to read users and groups on Mac OSX systems.
+ *
+ * See `man dscl` for more info.
+ */
+class OsxShellCommands implements ShellCommandsProvider {
+ /**
+ * @return Shell command string that will return a list of users.
+ */
+ public String getUsersList() {
+ return "dscl . -readall /Users UniqueID PrimaryGroupID | awk 'BEGIN { OFS = \":\"; ORS=\"\\n\"; i=0;} /RecordName: / {name = $2;i = 0;}" +
+ "/PrimaryGroupID: / {gid = $2;} /^ / {if (i == 0) { i++; name = $1;}} /UniqueID: / {uid = $2;print name, uid, gid;}' | grep -v ^_";
+ }
+
+ /**
+ * @return Shell command string that will return a list of groups.
+ */
+ public String getGroupsList() {
+ return "dscl . -list /Groups PrimaryGroupID | grep -v '^_' | sed 's/ \\{1,\\}/:/g'";
+ }
+
+ /**
+ *
+ * @param groupName name of group.
+ * @return Shell command string that will return a list of users for a group.
+ */
+ public String getGroupMembers(String groupName) {
+ return String.format("dscl . -read /Groups/%s GroupMembership | cut -f 2- -d ' ' | sed 's/\\ /,/g'", groupName);
+ }
+
+ /**
+ * @param userId name of user.
+ * @return Shell command string that will read a single user.
+ */
+ @Override
+ public String getUserById(String userId) {
+ return String.format("id -P %s | cut -f 1,3,4 -d ':'", userId);
+ }
+
+ /**
+ * @param userName name of user.
+ * @return Shell command string that will read a single user.
+ */
+ public String getUserByName(String userName) {
+ return getUserById(userName); // 'id' command works for both uid/username
+ }
+
+ /**
+ * @param groupId name of group.
+ * @return Shell command string that will read a single group.
+ */
+ public String getGroupById(String groupId) {
+ return String.format(" dscl . -read /Groups/`dscl . -search /Groups gid %s | head -n 1 | cut -f 1` RecordName PrimaryGroupID | awk 'BEGIN { OFS = \":\"; ORS=\"\\n\"; i=0;} " +
+ "/RecordName: / {name = $2;i = 1;}/PrimaryGroupID: / {gid = $2;}; {if (i==1) {print name,gid,\"\"}}'", groupId);
+ }
+
+ /**
+ * @return Shell command string that will exit normally (0) on a suitable system.
+ */
+ public String getSystemCheck() {
+ return "which dscl";
+ }
+}
diff --git a/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/shell/RemoteShellCommands.java b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/shell/RemoteShellCommands.java
new file mode 100644
index 0000000..f622409
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/shell/RemoteShellCommands.java
@@ -0,0 +1,73 @@
+/*
+ * 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.registry.security.authorization.shell;
+
+class RemoteShellCommands implements ShellCommandsProvider {
+ // Carefully crafted command replacement string:
+ private final static String remoteCommand = "ssh " +
+ "-o 'StrictHostKeyChecking no' " +
+ "-o 'PasswordAuthentication no' " +
+ "-o \"RemoteCommand %s\" " +
+ "-i %s -p %s -l root %s";
+
+ private ShellCommandsProvider innerProvider;
+ private String privateKeyPath;
+ private String remoteHost;
+ private Integer remotePort;
+
+ private RemoteShellCommands() {
+ }
+
+ public static ShellCommandsProvider wrapOtherProvider(ShellCommandsProvider otherProvider, String keyPath, String host, Integer port) {
+ RemoteShellCommands remote = new RemoteShellCommands();
+
+ remote.innerProvider = otherProvider;
+ remote.privateKeyPath = keyPath;
+ remote.remoteHost = host;
+ remote.remotePort = port;
+
+ return remote;
+ }
+
+ public String getUsersList() {
+ return String.format(remoteCommand, innerProvider.getUsersList(), privateKeyPath, remotePort, remoteHost);
+ }
+
+ public String getGroupsList() {
+ return String.format(remoteCommand, innerProvider.getGroupsList(), privateKeyPath, remotePort, remoteHost);
+ }
+
+ public String getGroupMembers(String groupName) {
+ return String.format(remoteCommand, innerProvider.getGroupMembers(groupName), privateKeyPath, remotePort, remoteHost);
+ }
+
+ public String getUserById(String userId) {
+ return String.format(remoteCommand, innerProvider.getUserById(userId), privateKeyPath, remotePort, remoteHost);
+ }
+
+ public String getUserByName(String userName) {
+ return String.format(remoteCommand, innerProvider.getUserByName(userName), privateKeyPath, remotePort, remoteHost);
+ }
+
+ public String getGroupById(String groupId) {
+ return String.format(remoteCommand, innerProvider.getGroupById(groupId), privateKeyPath, remotePort, remoteHost);
+ }
+
+ public String getSystemCheck() {
+ return String.format(remoteCommand, innerProvider.getSystemCheck(), privateKeyPath, remotePort, remoteHost);
+ }
+}
diff --git a/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/shell/ShellCommandsProvider.java b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/shell/ShellCommandsProvider.java
new file mode 100644
index 0000000..ce3e6a4
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/shell/ShellCommandsProvider.java
@@ -0,0 +1,100 @@
+/*
+ * 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.registry.security.authorization.shell;
+
+/**
+ * Common interface for shell command strings to read users and groups.
+ *
+ */
+interface ShellCommandsProvider {
+ /**
+ * Gets the command for listing users.
+ *
+ * When executed, this command should output one record per line in this format:
+ *
+ * `username:user-id:primary-group-id`
+ *
+ * @return Shell command string that will return a list of users.
+ */
+ String getUsersList();
+
+ /**
+ * Gets the command for listing groups.
+ *
+ * When executed, this command should output one record per line in this format:
+ *
+ * `group-name:group-id`
+ *
+ * @return Shell command string that will return a list of groups.
+ */
+ String getGroupsList();
+
+ /**
+ * Gets the command for listing the members of a group.
+ *
+ * When executed, this command should output one line in this format:
+ *
+ * `user-name-1,user-name-2,user-name-n`
+ *
+ * @param groupName name of group.
+ * @return Shell command string that will return a list of users for a group.
+ */
+ String getGroupMembers(String groupName);
+
+ /**
+ * Gets the command for reading a single user by id. Implementations may return null if reading a single
+ * user by id is not supported.
+ *
+ * When executed, this command should output a single line, in the format used by `getUsersList`.
+ *
+ * @param userId name of user.
+ * @return Shell command string that will read a single user.
+ */
+ String getUserById(String userId);
+
+ /**
+ * Gets the command for reading a single user. Implementations may return null if reading a single user by
+ * username is not supported.
+ *
+ * When executed, this command should output a single line, in the format used by `getUsersList`.
+ *
+ * @param userName name of user.
+ * @return Shell command string that will read a single user.
+ */
+ String getUserByName(String userName);
+
+ /**
+ * Gets the command for reading a single group. Implementations may return null if reading a single group
+ * by name is not supported.
+ *
+ * When executed, this command should output a single line, in the format used by `getGroupsList`.
+ *
+ * @param groupId name of group.
+ * @return Shell command string that will read a single group.
+ */
+ String getGroupById(String groupId);
+
+ /**
+ * Gets the command for checking the suitability of the host system.
+ *
+ * The command is expected to exit with status 0 (zero) to indicate success, and any other status
+ * to indicate failure.
+ *
+ * @return Shell command string that will exit normally (0) on a suitable system.
+ */
+ String getSystemCheck();
+}
diff --git a/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/shell/ShellRunner.java b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/shell/ShellRunner.java
new file mode 100644
index 0000000..ee7ef41
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/shell/ShellRunner.java
@@ -0,0 +1,81 @@
+/*
+ * 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.registry.security.authorization.shell;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.io.Reader;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+
+public class ShellRunner {
+ private final static Logger logger = LoggerFactory.getLogger(ShellRunner.class);
+
+ static String SHELL = "sh";
+ static String OPTS = "-c";
+ static Integer TIMEOUT = 60;
+
+ public static List<String> runShell(String command) throws IOException {
+ return runShell(command, "<unknown>");
+ }
+
+ public static List<String> runShell(String command, String description) throws IOException {
+ final ProcessBuilder builder = new ProcessBuilder(SHELL, OPTS, command);
+ final List<String> builderCommand = builder.command();
+
+ logger.debug("Run Command '" + description + "': " + builderCommand);
+ final Process proc = builder.start();
+
+ boolean completed;
+ try {
+ completed = proc.waitFor(TIMEOUT, TimeUnit.SECONDS);
+ } catch (InterruptedException irexc) {
+ throw new IOException(irexc.getMessage(), irexc.getCause());
+ }
+
+ if (!completed) {
+ throw new IllegalStateException("Shell command '" + command + "' did not complete during the allotted time period");
+ }
+
+ if (proc.exitValue() != 0) {
+ try (final Reader stderr = new InputStreamReader(proc.getErrorStream());
+ final BufferedReader reader = new BufferedReader(stderr)) {
+ String line;
+ while ((line = reader.readLine()) != null) {
+ logger.warn(line.trim());
+ }
+ }
+ throw new IOException("Command exit non-zero: " + proc.exitValue());
+ }
+
+ final List<String> lines = new ArrayList<>();
+ try (final Reader stdin = new InputStreamReader(proc.getInputStream());
+ final BufferedReader reader = new BufferedReader(stdin)) {
+ String line;
+ while ((line = reader.readLine()) != null) {
+ lines.add(line.trim());
+ }
+ }
+
+ return lines;
+ }
+}
diff --git a/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/shell/ShellUserGroupProvider.java b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/shell/ShellUserGroupProvider.java
new file mode 100644
index 0000000..1d709ac
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/shell/ShellUserGroupProvider.java
@@ -0,0 +1,678 @@
+/*
+ * 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.registry.security.authorization.shell;
+
+import org.apache.commons.lang3.StringUtils;
+import org.apache.nifi.registry.security.authorization.AuthorizerConfigurationContext;
+import org.apache.nifi.registry.security.authorization.Group;
+import org.apache.nifi.registry.security.authorization.User;
+import org.apache.nifi.registry.security.authorization.UserAndGroups;
+import org.apache.nifi.registry.security.authorization.UserGroupProvider;
+import org.apache.nifi.registry.security.authorization.UserGroupProviderInitializationContext;
+import org.apache.nifi.registry.security.authorization.exception.AuthorizationAccessException;
+import org.apache.nifi.registry.security.exception.SecurityProviderCreationException;
+import org.apache.nifi.registry.security.exception.SecurityProviderDestructionException;
+import org.apache.nifi.registry.util.FormatUtils;
+import org.apache.nifi.registry.util.PropertyValue;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.TreeSet;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+import java.util.regex.Pattern;
+import java.util.regex.PatternSyntaxException;
+
+/*
+ * ShellUserGroupProvider implements UserGroupProvider by way of shell commands.
+ */
+public class ShellUserGroupProvider implements UserGroupProvider {
+ private final static Logger logger = LoggerFactory.getLogger(ShellUserGroupProvider.class);
+
+ private final static String OS_TYPE_ERROR = "Unsupported operating system.";
+ private final static String SYS_CHECK_ERROR = "System check failed - cannot provide users and groups.";
+ private final static Map<String, User> usersById = new HashMap<>(); // id == identifier
+ private final static Map<String, User> usersByName = new HashMap<>(); // name == identity
+ private final static Map<String, Group> groupsById = new HashMap<>();
+
+ public static final String REFRESH_DELAY_PROPERTY = "Refresh Delay";
+ private static final long MINIMUM_SYNC_INTERVAL_MILLISECONDS = 10_000;
+
+ public static final String EXCLUDE_USER_PROPERTY = "Exclude Users";
+ public static final String EXCLUDE_GROUP_PROPERTY = "Exclude Groups";
+
+ private long fixedDelay;
+ private Pattern excludeUsers;
+ private Pattern excludeGroups;
+
+ // Our scheduler has one thread for users, one for groups:
+ private final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(2);
+
+ // Our shell timeout, in seconds:
+ @SuppressWarnings("FieldCanBeLocal")
+ private final Integer shellTimeout = 10;
+
+ // Commands selected during initialization:
+ private ShellCommandsProvider selectedShellCommands;
+
+ // Start of the UserGroupProvider implementation. Javadoc strings
+ // copied from the interface definition for reference.
+
+ /**
+ * Retrieves all users. Must be non null
+ *
+ * @return a list of users
+ * @throws AuthorizationAccessException if there was an unexpected error performing the operation
+ */
+ @Override
+ public Set<User> getUsers() throws AuthorizationAccessException {
+ synchronized (usersById) {
+ logger.debug("getUsers has user set of size: " + usersById.size());
+ return new HashSet<>(usersById.values());
+ }
+ }
+
+ /**
+ * Retrieves the user with the given identifier.
+ *
+ * @param identifier the id of the user to retrieve
+ * @return the user with the given id, or null if no matching user was found
+ * @throws AuthorizationAccessException if there was an unexpected error performing the operation
+ */
+ @Override
+ public User getUser(String identifier) throws AuthorizationAccessException {
+ User user;
+
+ synchronized (usersById) {
+ user = usersById.get(identifier);
+ }
+
+ if (user == null) {
+ logger.debug("getUser (by id) user not found: " + identifier);
+ } else {
+ logger.debug("getUser (by id) found user: " + user + " for id: " + identifier);
+ }
+ return user;
+ }
+
+ /**
+ * Retrieves the user with the given identity.
+ *
+ * @param identity the identity of the user to retrieve
+ * @return the user with the given identity, or null if no matching user was found
+ * @throws AuthorizationAccessException if there was an unexpected error performing the operation
+ */
+ @Override
+ public User getUserByIdentity(String identity) throws AuthorizationAccessException {
+ User user;
+
+ synchronized (usersByName) {
+ user = usersByName.get(identity);
+ }
+
+ if (user == null) {
+ refreshOneUser(selectedShellCommands.getUserByName(identity), "Get Single User by Name");
+ user = usersByName.get(identity);
+ }
+
+ if (user == null) {
+ logger.debug("getUser (by name) user not found: " + identity);
+ } else {
+ logger.debug("getUser (by name) found user: " + user.getIdentity() + " for name: " + identity);
+ }
+ return user;
+ }
+
+ /**
+ * Retrieves all groups. Must be non null
+ *
+ * @return a list of groups
+ * @throws AuthorizationAccessException if there was an unexpected error performing the operation
+ */
+ @Override
+ public Set<Group> getGroups() throws AuthorizationAccessException {
+ synchronized (groupsById) {
+ logger.debug("getGroups has group set of size: " + groupsById.size());
+ return new HashSet<>(groupsById.values());
+ }
+ }
+
+ /**
+ * Retrieves a Group by Id.
+ *
+ * @param identifier the identifier of the Group to retrieve
+ * @return the Group with the given identifier, or null if no matching group was found
+ * @throws AuthorizationAccessException if there was an unexpected error performing the operation
+ */
+ @Override
+ public Group getGroup(String identifier) throws AuthorizationAccessException {
+ Group group;
+
+ synchronized (groupsById) {
+ group = groupsById.get(identifier);
+ }
+
+ if (group == null) {
+ refreshOneGroup(selectedShellCommands.getGroupById(identifier), "Get Single Group by Id");
+ group = groupsById.get(identifier);
+ }
+
+ if (group == null) {
+ logger.debug("getGroup (by id) group not found: " + identifier);
+ } else {
+ logger.debug("getGroup (by id) found group: " + group.getName() + " for id: " + identifier);
+ }
+ return group;
+
+ }
+
+ /**
+ * Gets a user and their groups.
+ *
+ * @return the UserAndGroups for the specified identity
+ * @throws AuthorizationAccessException if there was an unexpected error performing the operation
+ */
+ @Override
+ public UserAndGroups getUserAndGroups(String identity) throws AuthorizationAccessException {
+ User user = getUserByIdentity(identity);
+ logger.debug("Retrieved user {} for identity {}", new Object[]{user, identity});
+
+ Set<Group> groups = new HashSet<>();
+ if (user != null) {
+ for (Group g : getGroups()) {
+ if (g.getUsers().contains(user.getIdentifier())) {
+ logger.debug("User {} belongs to group {}", new Object[]{user.getIdentity(), g.getName()});
+ groups.add(g);
+ }
+ }
+ }
+
+ if (groups.isEmpty()) {
+ logger.debug("User {} belongs to no groups", user);
+ }
+
+ return new UserAndGroups() {
+ @Override
+ public User getUser() {
+ return user;
+ }
+
+ @Override
+ public Set<Group> getGroups() {
+ return groups;
+ }
+ };
+ }
+
+ /**
+ * Called immediately after instance creation for implementers to perform additional setup
+ *
+ * @param initializationContext in which to initialize
+ */
+ @Override
+ public void initialize(UserGroupProviderInitializationContext initializationContext) throws SecurityProviderCreationException {
+ }
+
+ /**
+ * Called to configure the Authorizer.
+ *
+ * @param configurationContext at the time of configuration
+ * @throws SecurityProviderCreationException for any issues configuring the provider
+ */
+ @Override
+ public void onConfigured(AuthorizerConfigurationContext configurationContext) throws SecurityProviderCreationException {
+ fixedDelay = getDelayProperty(configurationContext, REFRESH_DELAY_PROPERTY, "5 mins");
+
+ // Our next init step is to select the command set based on the operating system name:
+ ShellCommandsProvider commands = getCommandsProvider();
+
+ if (commands == null) {
+ commands = getCommandsProviderFromName(null);
+ setCommandsProvider(commands);
+ }
+
+ // Our next init step is to run the system check from that command set to determine if the other commands
+ // will work on this host or not.
+ try {
+ ShellRunner.runShell(commands.getSystemCheck());
+ } catch (final Exception e) {
+ logger.error("initialize exception: " + e + " system check command: " + commands.getSystemCheck());
+ throw new SecurityProviderCreationException(SYS_CHECK_ERROR, e);
+ }
+
+ // The next step is to add the user and group exclude regexes:
+ try {
+ excludeGroups = Pattern.compile(getProperty(configurationContext, EXCLUDE_GROUP_PROPERTY, ""));
+ excludeUsers = Pattern.compile(getProperty(configurationContext, EXCLUDE_USER_PROPERTY, ""));
+ } catch (final PatternSyntaxException e) {
+ throw new SecurityProviderCreationException(e);
+ }
+
+ // With our command set selected, and our system check passed, we can pull in the users and groups:
+ refreshUsersAndGroups();
+
+ // And finally, our last init step is to fire off the refresh thread:
+ scheduler.scheduleWithFixedDelay(() -> {
+ try {
+ refreshUsersAndGroups();
+ }catch (final Throwable t) {
+ logger.error("", t);
+ }
+ }, fixedDelay, fixedDelay, TimeUnit.MILLISECONDS);
+
+ }
+
+ private static ShellCommandsProvider getCommandsProviderFromName(String osName) {
+ if (osName == null) {
+ osName = System.getProperty("os.name");
+ }
+
+ ShellCommandsProvider commands;
+ if (osName.startsWith("Linux")) {
+ logger.debug("Selected Linux command set.");
+ commands = new NssShellCommands();
+ } else if (osName.startsWith("Mac OS X")) {
+ logger.debug("Selected OSX command set.");
+ commands = new OsxShellCommands();
+ } else {
+ throw new SecurityProviderCreationException(OS_TYPE_ERROR);
+ }
+ return commands;
+ }
+
+ private String getProperty(AuthorizerConfigurationContext authContext, String propertyName, String defaultValue) {
+ final PropertyValue property = authContext.getProperty(propertyName);
+ final String value;
+
+ if (property != null && property.isSet()) {
+ value = property.getValue();
+ } else {
+ value = defaultValue;
+ }
+ return value;
+
+ }
+
+ private long getDelayProperty(AuthorizerConfigurationContext authContext, String propertyName, String defaultValue) {
+ final PropertyValue intervalProperty = authContext.getProperty(propertyName);
+ final String propertyValue;
+ final long syncInterval;
+
+ if (intervalProperty.isSet()) {
+ propertyValue = intervalProperty.getValue();
+ } else {
+ propertyValue = defaultValue;
+ }
+
+ try {
+ syncInterval = Math.round(FormatUtils.getPreciseTimeDuration(propertyValue, TimeUnit.MILLISECONDS));
+ } catch (final IllegalArgumentException ignored) {
+ throw new SecurityProviderCreationException(String.format("The %s '%s' is not a valid time interval.", propertyName, propertyValue));
+ }
+
+ if (syncInterval < MINIMUM_SYNC_INTERVAL_MILLISECONDS) {
+ throw new SecurityProviderCreationException(String.format("The %s '%s' is below the minimum value of '%d ms'", propertyName, propertyValue, MINIMUM_SYNC_INTERVAL_MILLISECONDS));
+ }
+ return syncInterval;
+ }
+
+ /**
+ * Called immediately before instance destruction for implementers to release resources.
+ *
+ * @throws SecurityProviderDestructionException If pre-destruction fails.
+ */
+ @Override
+ public void preDestruction() throws SecurityProviderDestructionException {
+ try {
+ scheduler.shutdownNow();
+ } catch (final Exception ignored) {
+ }
+ }
+
+ public ShellCommandsProvider getCommandsProvider() {
+ return selectedShellCommands;
+ }
+
+ public void setCommandsProvider(ShellCommandsProvider commandsProvider) {
+ selectedShellCommands = commandsProvider;
+ }
+
+ /**
+ * Refresh a single user.
+ *
+ * @param command Shell command to read a single user. Pre-formatted by caller.
+ * @param description Shell command description.
+ */
+ private void refreshOneUser(String command, String description) {
+ if (command != null) {
+ Map<String, User> idToUser = new HashMap<>();
+ Map<String, User> usernameToUser = new HashMap<>();
+ Map<String, User> gidToUser = new HashMap<>();
+ List<String> userLines;
+
+ try {
+ userLines = ShellRunner.runShell(command, description);
+ rebuildUsers(userLines, idToUser, usernameToUser, gidToUser);
+ } catch (final IOException ioexc) {
+ logger.error("refreshOneUser shell exception: " + ioexc);
+ }
+
+ if (idToUser.size() > 0) {
+ synchronized (usersById) {
+ usersById.putAll(idToUser);
+ }
+ }
+
+ if (usernameToUser.size() > 0) {
+ synchronized (usersByName) {
+ usersByName.putAll(usernameToUser);
+ }
+ }
+ } else {
+ logger.info("Get Single User not supported on this system.");
+ }
+ }
+
+ /**
+ * Refresh a single group.
+ *
+ * @param command Shell command to read a single group. Pre-formatted by caller.
+ * @param description Shell command description.
+ */
+ private void refreshOneGroup(String command, String description) {
+ if (command != null) {
+ Map<String, Group> gidToGroup = new HashMap<>();
+ List<String> groupLines;
+
+ try {
+ groupLines = ShellRunner.runShell(command, description);
+ rebuildGroups(groupLines, gidToGroup);
+ } catch (final IOException ioexc) {
+ logger.error("refreshOneGroup shell exception: " + ioexc);
+ }
+
+ if (gidToGroup.size() > 0) {
+ synchronized (groupsById) {
+ groupsById.putAll(gidToGroup);
+ }
+ }
+ } else {
+ logger.info("Get Single Group not supported on this system.");
+ }
+ }
+
+ /**
+ * This is our entry point for user and group refresh. This method runs the top-level
+ * `getUserList()` and `getGroupsList()` shell commands, then passes those results to the
+ * other methods for record parse, extract, and object construction.
+ */
+ private void refreshUsersAndGroups() {
+ Map<String, User> uidToUser = new HashMap<>();
+ Map<String, User> usernameToUser = new HashMap<>();
+ Map<String, User> gidToUser = new HashMap<>();
+ Map<String, Group> gidToGroup = new HashMap<>();
+
+ List<String> userLines;
+ List<String> groupLines;
+
+ try {
+ userLines = ShellRunner.runShell(selectedShellCommands.getUsersList(), "Get Users List");
+ groupLines = ShellRunner.runShell(selectedShellCommands.getGroupsList(), "Get Groups List");
+ } catch (final IOException ioexc) {
+ logger.error("refreshUsersAndGroups shell exception: " + ioexc);
+ return;
+ }
+
+ rebuildUsers(userLines, uidToUser, usernameToUser, gidToUser);
+ rebuildGroups(groupLines, gidToGroup);
+ reconcilePrimaryGroups(gidToUser, gidToGroup);
+
+ synchronized (usersById) {
+ usersById.clear();
+ usersById.putAll(uidToUser);
+
+ if (logger.isTraceEnabled()) {
+ logger.trace("=== Users by id...");
+ Set<User> sortedUsers = new TreeSet<>(Comparator.comparing(User::getIdentity));
+ sortedUsers.addAll(usersById.values());
+ sortedUsers.forEach(u -> logger.trace("=== " + u.toString()));
+ }
+ }
+
+ synchronized (usersByName) {
+ usersByName.clear();
+ usersByName.putAll(usernameToUser);
+ logger.debug("users now size: " + usersByName.size());
+ }
+
+ synchronized (groupsById) {
+ groupsById.clear();
+ groupsById.putAll(gidToGroup);
+ logger.debug("groups now size: " + groupsById.size());
+
+ if (logger.isTraceEnabled()) {
+ logger.trace("=== Groups by id...");
+ Set<Group> sortedGroups = new TreeSet<>(Comparator.comparing(Group::getName));
+ sortedGroups.addAll(groupsById.values());
+ sortedGroups.forEach(g -> logger.trace("=== " + g.toString()));
+ }
+ }
+ }
+
+ /**
+ * This method parses the output of the `getUsersList()` shell command, where we expect the output
+ * to look like `user-name:user-id:primary-group-id`.
+ * <p>
+ * This method splits each output line on the ":" and attempts to build a User object
+ * from the resulting name, uid, and primary gid. Unusable records are logged.
+ */
+ private void rebuildUsers(List<String> userLines, Map<String, User> idToUser, Map<String, User> usernameToUser, Map<String, User> gidToUser) {
+ userLines.forEach(line -> {
+ logger.trace("Processing user: {}", new Object[]{line});
+
+ String[] record = line.split(":");
+ if (record.length > 2) {
+ String userIdentity = record[0], userIdentifier = record[1], primaryGroupIdentifier = record[2];
+
+ if (!StringUtils.isBlank(userIdentifier) && !StringUtils.isBlank(userIdentity) && !excludeUsers.matcher(userIdentity).matches()) {
+ User user = new User.Builder()
+ .identity(userIdentity)
+ .identifierGenerateFromSeed(getUserIdentifierSeed(userIdentity))
+ .build();
+ idToUser.put(user.getIdentifier(), user);
+ usernameToUser.put(userIdentity, user);
+ logger.debug("Refreshed user {}", new Object[]{user});
+
+ if (!StringUtils.isBlank(primaryGroupIdentifier)) {
+ // create a temporary group to deterministically generate the group id and associate this user
+ Group group = new Group.Builder()
+ .name(primaryGroupIdentifier)
+ .identifierGenerateFromSeed(getGroupIdentifierSeed(primaryGroupIdentifier))
+ .build();
+ gidToUser.put(group.getIdentifier(), user);
+ logger.debug("Associated primary group {} with user {}", new Object[]{group.getIdentifier(), userIdentity});
+ } else {
+ logger.warn("Null or empty primary group id for: " + userIdentity);
+ }
+
+ } else {
+ logger.warn("Null, empty, or skipped user name: " + userIdentity + " or id: " + userIdentifier);
+ }
+ } else {
+ logger.warn("Unexpected record format. Expected 3 or more colon separated values per line.");
+ }
+ });
+ }
+
+ /**
+ * This method parses the output of the `getGroupsList()` shell command, where we expect the output
+ * to look like `group-name:group-id`.
+ * <p>
+ * This method splits each output line on the ":" and attempts to build a Group object
+ * from the resulting name and gid. Unusable records are logged.
+ * <p>
+ * This command also runs the `getGroupMembers(username)` command once per group. The expected output
+ * of that command should look like `group-name-1,group-name-2`.
+ */
+ private void rebuildGroups(List<String> groupLines, Map<String, Group> groupsById) {
+ groupLines.forEach(line -> {
+ logger.trace("Processing group: {}", new Object[]{line});
+
+ String[] record = line.split(":");
+ if (record.length > 1) {
+ Set<String> users = new HashSet<>();
+ String groupName = record[0], groupIdentifier = record[1];
+
+ try {
+ String groupMembersCommand = selectedShellCommands.getGroupMembers(groupName);
+ List<String> memberLines = ShellRunner.runShell(groupMembersCommand);
+ // Use the first line only, and log if the line count isn't exactly one:
+ if (!memberLines.isEmpty()) {
+ String memberLine = memberLines.get(0);
+ if (!StringUtils.isBlank(memberLine)) {
+ String[] members = memberLine.split(",");
+ for (String userIdentity : members) {
+ if (!StringUtils.isBlank(userIdentity)) {
+ User tempUser = new User.Builder()
+ .identity(userIdentity)
+ .identifierGenerateFromSeed(getUserIdentifierSeed(userIdentity))
+ .build();
+ users.add(tempUser.getIdentifier());
+ logger.debug("Added temp user {} for group {}", new Object[]{tempUser, groupName});
+ }
+ }
+ } else {
+ logger.debug("list membership returned no members");
+ }
+ } else {
+ logger.debug("list membership returned zero lines.");
+ }
+ if (memberLines.size() > 1) {
+ logger.error("list membership returned too many lines, only used the first.");
+ }
+
+ } catch (final IOException ioexc) {
+ logger.error("list membership shell exception: " + ioexc);
+ }
+
+ if (!StringUtils.isBlank(groupIdentifier) && !StringUtils.isBlank(groupName) && !excludeGroups.matcher(groupName).matches()) {
+ Group group = new Group.Builder()
+ .name(groupName)
+ .identifierGenerateFromSeed(getGroupIdentifierSeed(groupIdentifier))
+ .addUsers(users)
+ .build();
+ groupsById.put(group.getIdentifier(), group);
+ logger.debug("Refreshed group {}", new Object[] {group});
+ } else {
+ logger.warn("Null, empty, or skipped group name: " + groupName + " or id: " + groupIdentifier);
+ }
+ } else {
+ logger.warn("Unexpected record format. Expected 1 or more comma separated values.");
+ }
+ });
+ }
+
+ /**
+ * This method parses the output of the `getGroupsList()` shell command, where we expect the output
+ * to look like `group-name:group-id`.
+ * <p>
+ * This method splits each output line on the ":" and attempts to build a Group object
+ * from the resulting name and gid.
+ */
+ private void reconcilePrimaryGroups(Map<String, User> uidToUser, Map<String, Group> gidToGroup) {
+ uidToUser.forEach((primaryGid, primaryUser) -> {
+ Group primaryGroup = gidToGroup.get(primaryGid);
+
+ if (primaryGroup == null) {
+ logger.warn("Primary group {} not found for {}", new Object[]{primaryGid, primaryUser.getIdentity()});
+ } else if (!excludeGroups.matcher(primaryGroup.getName()).matches()) {
+ Set<String> groupUsers = primaryGroup.getUsers();
+ if (!groupUsers.contains(primaryUser.getIdentifier())) {
+ Set<String> updatedUserIdentifiers = new HashSet<>(groupUsers);
+ updatedUserIdentifiers.add(primaryUser.getIdentifier());
+
+ Group updatedGroup = new Group.Builder()
+ .identifier(primaryGroup.getIdentifier())
+ .name(primaryGroup.getName())
+ .addUsers(updatedUserIdentifiers)
+ .build();
+ gidToGroup.put(updatedGroup.getIdentifier(), updatedGroup);
+ logger.debug("Added user {} to primary group {}", new Object[]{primaryUser, updatedGroup});
+ } else {
+ logger.debug("Primary group {} already contains user {}", new Object[]{primaryGroup, primaryUser});
+ }
+ } else {
+ logger.debug("Primary group {} excluded from matcher for {}", new Object[]{primaryGroup.getName(), primaryUser.getIdentity()});
+ }
+ });
+ }
+
+ private String getUserIdentifierSeed(final String userIdentifier) {
+ return userIdentifier + "-user";
+ }
+
+ private String getGroupIdentifierSeed(final String groupIdentifier) {
+ return groupIdentifier + "-group";
+ }
+
+
+ /**
+ * @return The fixed refresh delay.
+ */
+ public long getRefreshDelay() {
+ return fixedDelay;
+ }
+
+ /**
+ * Testing concession for clearing the internal caches.
+ */
+ void clearCaches() {
+ synchronized (usersById) {
+ usersById.clear();
+ }
+
+ synchronized (usersByName) {
+ usersByName.clear();
+ }
+
+ synchronized (groupsById) {
+ groupsById.clear();
+ }
+ }
+
+ /**
+ * @return The size of the internal user cache.
+ */
+ public int userCacheSize() {
+ return usersById.size();
+ }
+
+ /**
+ * @return The size of the internal group cache.
+ */
+ public int groupCacheSize() {
+ return groupsById.size();
+ }
+}
diff --git a/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/util/ClassLoaderUtils.java b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/util/ClassLoaderUtils.java
new file mode 100644
index 0000000..1f05cd3
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/util/ClassLoaderUtils.java
@@ -0,0 +1,138 @@
+/*
+ * 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.registry.security.util;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.File;
+import java.io.FilenameFilter;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.net.URLClassLoader;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.LinkedHashSet;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Set;
+
+public class ClassLoaderUtils {
+
+ static final Logger LOGGER = LoggerFactory.getLogger(ClassLoaderUtils.class);
+
+ public static ClassLoader getCustomClassLoader(String modulePath, ClassLoader parentClassLoader, FilenameFilter filenameFilter) throws MalformedURLException {
+ URL[] classpaths = getURLsForClasspath(modulePath, filenameFilter, false);
+ return createModuleClassLoader(classpaths, parentClassLoader);
+ }
+
+ /**
+ *
+ * @param modulePath a module path to get URLs from, the module path may be
+ * a comma-separated list of paths
+ * @param filenameFilter a filter to apply when a module path is a directory
+ * and performs a listing, a null filter will return all matches
+ * @param suppressExceptions indicates whether to suppress exceptions
+ * @return an array of URL instances representing all of the modules
+ * resolved from processing modulePath
+ * @throws MalformedURLException if a module path does not exist
+ */
+ public static URL[] getURLsForClasspath(String modulePath, FilenameFilter filenameFilter, boolean suppressExceptions) throws MalformedURLException {
+ return getURLsForClasspath(modulePath == null ? Collections.emptySet() : Collections.singleton(modulePath), filenameFilter, suppressExceptions);
+ }
+
+ /**
+ *
+ * @param modulePaths one or modules paths to get URLs from, each module
+ * path may be a comma-separated list of paths
+ * @param filenameFilter a filter to apply when a module path is a directory
+ * and performs a listing, a null filter will return all matches
+ * @param suppressExceptions if true then all modules will attempt to be
+ * resolved even if some throw an exception, if false the first exception
+ * will be thrown
+ * @return an array of URL instances representing all of the modules
+ * resolved from processing modulePaths
+ * @throws MalformedURLException if a module path does not exist
+ */
+ public static URL[] getURLsForClasspath(Set<String> modulePaths, FilenameFilter filenameFilter, boolean suppressExceptions) throws MalformedURLException {
+ // use LinkedHashSet to maintain the ordering that the incoming paths are processed
+ Set<String> modules = new LinkedHashSet<>();
+ if (modulePaths != null) {
+ modulePaths.stream()
+ .flatMap(path -> Arrays.stream(path.split(",")))
+ .filter(path -> isNotBlank(path))
+ .map(String::trim)
+ .forEach(m -> modules.add(m));
+ }
+ return toURLs(modules, filenameFilter, suppressExceptions);
+ }
+
+ private static boolean isNotBlank(final String value) {
+ return value != null && !value.trim().isEmpty();
+ }
+
+ protected static URL[] toURLs(Set<String> modulePaths, FilenameFilter filenameFilter, boolean suppressExceptions) throws MalformedURLException {
+ List<URL> additionalClasspath = new LinkedList<>();
+ if (modulePaths != null) {
+ for (String modulePathString : modulePaths) {
+ // If the path is already a URL, just add it (but don't check if it exists, too expensive and subject to network availability)
+ boolean isUrl = true;
+ try {
+ additionalClasspath.add(new URL(modulePathString));
+ } catch (MalformedURLException mue) {
+ isUrl = false;
+ }
+ if (!isUrl) {
+ try {
+ File modulePath = new File(modulePathString);
+
+ if (modulePath.exists()) {
+
+ additionalClasspath.add(modulePath.toURI().toURL());
+
+ if (modulePath.isDirectory()) {
+ File[] files = modulePath.listFiles(filenameFilter);
+
+ if (files != null) {
+ for (File classpathResource : files) {
+ if (classpathResource.isDirectory()) {
+ LOGGER.warn("Recursive directories are not supported, skipping " + classpathResource.getAbsolutePath());
+ } else {
+ additionalClasspath.add(classpathResource.toURI().toURL());
+ }
+ }
+ }
+ }
+ } else {
+ throw new MalformedURLException("Path specified does not exist");
+ }
+ } catch (MalformedURLException e) {
+ if (!suppressExceptions) {
+ throw e;
+ }
+ }
+ }
+ }
+ }
+ return additionalClasspath.toArray(new URL[additionalClasspath.size()]);
+ }
+
+ protected static ClassLoader createModuleClassLoader(URL[] modules, ClassLoader parentClassLoader) {
+ return new URLClassLoader(modules, parentClassLoader);
+ }
+
+}
diff --git a/nifi-registry-core/nifi-registry-framework/src/main/resources/META-INF/services/org.apache.nifi.registry.security.authorization.UserGroupProvider b/nifi-registry-core/nifi-registry-framework/src/main/resources/META-INF/services/org.apache.nifi.registry.security.authorization.UserGroupProvider
index ee28c07..a4ac129 100644
--- a/nifi-registry-core/nifi-registry-framework/src/main/resources/META-INF/services/org.apache.nifi.registry.security.authorization.UserGroupProvider
+++ b/nifi-registry-core/nifi-registry-framework/src/main/resources/META-INF/services/org.apache.nifi.registry.security.authorization.UserGroupProvider
@@ -15,4 +15,5 @@
org.apache.nifi.registry.security.authorization.CompositeUserGroupProvider
org.apache.nifi.registry.security.authorization.CompositeConfigurableUserGroupProvider
org.apache.nifi.registry.security.authorization.file.FileUserGroupProvider
-org.apache.nifi.registry.security.ldap.tenants.LdapUserGroupProvider
\ No newline at end of file
+org.apache.nifi.registry.security.ldap.tenants.LdapUserGroupProvider
+org.apache.nifi.registry.security.authorization.shell.ShellUserGroupProvider
\ No newline at end of file
diff --git a/nifi-registry-core/nifi-registry-framework/src/test/groovy/org/apache/nifi/registry/security/authorization/AuthorizerFactorySpec.groovy b/nifi-registry-core/nifi-registry-framework/src/test/groovy/org/apache/nifi/registry/security/authorization/AuthorizerFactorySpec.groovy
index 34dfe6b..bd79e55 100644
--- a/nifi-registry-core/nifi-registry-framework/src/test/groovy/org/apache/nifi/registry/security/authorization/AuthorizerFactorySpec.groovy
+++ b/nifi-registry-core/nifi-registry-framework/src/test/groovy/org/apache/nifi/registry/security/authorization/AuthorizerFactorySpec.groovy
@@ -16,6 +16,7 @@
*/
package org.apache.nifi.registry.security.authorization
+import org.apache.nifi.registry.extension.ExtensionClassLoader
import org.apache.nifi.registry.extension.ExtensionManager
import org.apache.nifi.registry.properties.NiFiRegistryProperties
import org.apache.nifi.registry.security.authorization.resource.ResourceFactory
@@ -32,7 +33,7 @@
// runs before every feature method
def setup() {
- mockExtensionManager.getExtensionClassLoader(_) >> this.getClass().getClassLoader()
+ mockExtensionManager.getExtensionClassLoader(_) >> new ExtensionClassLoader("/tmp", new URL[0],this.getClass().getClassLoader())
mockProperties.getPropertyKeys() >> new HashSet<String>() // Called by IdentityMappingUtil.getIdentityMappings()
authorizerFactory = new AuthorizerFactory(mockProperties, mockExtensionManager, null, mockRegistryService)
diff --git a/nifi-registry-core/nifi-registry-framework/src/test/java/org/apache/nifi/registry/provider/TestStandardProviderFactory.java b/nifi-registry-core/nifi-registry-framework/src/test/java/org/apache/nifi/registry/provider/TestStandardProviderFactory.java
index 0d23110..7fb8dee 100644
--- a/nifi-registry-core/nifi-registry-framework/src/test/java/org/apache/nifi/registry/provider/TestStandardProviderFactory.java
+++ b/nifi-registry-core/nifi-registry-framework/src/test/java/org/apache/nifi/registry/provider/TestStandardProviderFactory.java
@@ -17,6 +17,7 @@
package org.apache.nifi.registry.provider;
import org.apache.nifi.registry.extension.BundlePersistenceProvider;
+import org.apache.nifi.registry.extension.ExtensionClassLoader;
import org.apache.nifi.registry.extension.ExtensionManager;
import org.apache.nifi.registry.flow.FlowPersistenceProvider;
import org.apache.nifi.registry.properties.NiFiRegistryProperties;
@@ -24,6 +25,7 @@
import org.mockito.Mockito;
import javax.sql.DataSource;
+import java.net.URL;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
@@ -38,7 +40,8 @@
props.setProperty(NiFiRegistryProperties.PROVIDERS_CONFIGURATION_FILE, "src/test/resources/provider/providers-good.xml");
final ExtensionManager extensionManager = Mockito.mock(ExtensionManager.class);
- when(extensionManager.getExtensionClassLoader(any(String.class))).thenReturn(this.getClass().getClassLoader());
+ when(extensionManager.getExtensionClassLoader(any(String.class)))
+ .thenReturn(new ExtensionClassLoader("/tmp", new URL[0],this.getClass().getClassLoader()));
final DataSource dataSource = Mockito.mock(DataSource.class);
@@ -68,7 +71,8 @@
props.setProperty(NiFiRegistryProperties.PROVIDERS_CONFIGURATION_FILE, "src/test/resources/provider/providers-good.xml");
final ExtensionManager extensionManager = Mockito.mock(ExtensionManager.class);
- when(extensionManager.getExtensionClassLoader(any(String.class))).thenReturn(this.getClass().getClassLoader());
+ when(extensionManager.getExtensionClassLoader(any(String.class)))
+ .thenReturn(new ExtensionClassLoader("/tmp", new URL[0],this.getClass().getClassLoader()));
final DataSource dataSource = Mockito.mock(DataSource.class);
@@ -82,7 +86,8 @@
props.setProperty(NiFiRegistryProperties.PROVIDERS_CONFIGURATION_FILE, "src/test/resources/provider/providers-does-not-exist.xml");
final ExtensionManager extensionManager = Mockito.mock(ExtensionManager.class);
- when(extensionManager.getExtensionClassLoader(any(String.class))).thenReturn(this.getClass().getClassLoader());
+ when(extensionManager.getExtensionClassLoader(any(String.class)))
+ .thenReturn(new ExtensionClassLoader("/tmp", new URL[0],this.getClass().getClassLoader()));
final DataSource dataSource = Mockito.mock(DataSource.class);
@@ -96,7 +101,8 @@
props.setProperty(NiFiRegistryProperties.PROVIDERS_CONFIGURATION_FILE, "src/test/resources/provider/providers-class-not-found.xml");
final ExtensionManager extensionManager = Mockito.mock(ExtensionManager.class);
- when(extensionManager.getExtensionClassLoader(any(String.class))).thenReturn(this.getClass().getClassLoader());
+ when(extensionManager.getExtensionClassLoader(any(String.class)))
+ .thenReturn(new ExtensionClassLoader("/tmp", new URL[0],this.getClass().getClassLoader()));
final DataSource dataSource = Mockito.mock(DataSource.class);
diff --git a/nifi-registry-core/nifi-registry-framework/src/test/java/org/apache/nifi/registry/provider/hook/TestScriptEventHookProvider.java b/nifi-registry-core/nifi-registry-framework/src/test/java/org/apache/nifi/registry/provider/hook/TestScriptEventHookProvider.java
index b7d241b..39d45ae 100644
--- a/nifi-registry-core/nifi-registry-framework/src/test/java/org/apache/nifi/registry/provider/hook/TestScriptEventHookProvider.java
+++ b/nifi-registry-core/nifi-registry-framework/src/test/java/org/apache/nifi/registry/provider/hook/TestScriptEventHookProvider.java
@@ -16,6 +16,7 @@
*/
package org.apache.nifi.registry.provider.hook;
+import org.apache.nifi.registry.extension.ExtensionClassLoader;
import org.apache.nifi.registry.extension.ExtensionManager;
import org.apache.nifi.registry.properties.NiFiRegistryProperties;
import org.apache.nifi.registry.provider.ProviderCreationException;
@@ -26,6 +27,8 @@
import javax.sql.DataSource;
+import java.net.URL;
+
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.when;
@@ -37,7 +40,8 @@
props.setProperty(NiFiRegistryProperties.PROVIDERS_CONFIGURATION_FILE, "src/test/resources/provider/hook/bad-script-provider.xml");
final ExtensionManager extensionManager = Mockito.mock(ExtensionManager.class);
- when(extensionManager.getExtensionClassLoader(any(String.class))).thenReturn(this.getClass().getClassLoader());
+ when(extensionManager.getExtensionClassLoader(any(String.class)))
+ .thenReturn(new ExtensionClassLoader("/tmp", new URL[0],this.getClass().getClassLoader()));
final DataSource dataSource = Mockito.mock(DataSource.class);
diff --git a/nifi-registry-core/nifi-registry-resources/src/main/resources/conf/authorizers.xml b/nifi-registry-core/nifi-registry-resources/src/main/resources/conf/authorizers.xml
index 9f63754..38a6ee8 100644
--- a/nifi-registry-core/nifi-registry-resources/src/main/resources/conf/authorizers.xml
+++ b/nifi-registry-core/nifi-registry-resources/src/main/resources/conf/authorizers.xml
@@ -166,6 +166,25 @@
To enable the ldap-user-group-provider remove 2 lines. This is 2 of 2. -->
<!--
+ The ShellUserGroupProvider provides support for retrieving users and groups by way of shell commands
+ on systems that support `sh`. Implementations available for Linux and Mac OS, and are selected by the
+ provider based on the system property `os.name`.
+
+ 'Refresh Delay' - duration to wait between subsequent refreshes. Default is '5 mins'.
+ 'Exclude Groups' - regular expression used to exclude groups. Default is '', which means no groups are excluded.
+ 'Exclude Users' - regular expression used to exclude users. Default is '', which means no users are excluded.
+ -->
+ <!-- To enable the shell-user-group-provider remove 2 lines. This is 1 of 2.
+ <userGroupProvider>
+ <identifier>shell-user-group-provider</identifier>
+ <class>org.apache.nifi.registry.security.authorization.shell.ShellUserGroupProvider</class>
+ <property name="Refresh Delay">5 mins</property>
+ <property name="Exclude Groups"></property>
+ <property name="Exclude Users"></property>
+ </userGroupProvider>
+ To enable the shell-user-group-provider remove 2 lines. This is 2 of 2. -->
+
+ <!--
The CompositeUserGroupProvider will provide support for retrieving users and groups from multiple sources.
- User Group Provider [unique key] - The identifier of user group providers to load from. The name of
diff --git a/nifi-registry-core/nifi-registry-utils/src/main/java/org/apache/nifi/registry/util/FormatUtils.java b/nifi-registry-core/nifi-registry-utils/src/main/java/org/apache/nifi/registry/util/FormatUtils.java
index c1e353d..aa207c5 100644
--- a/nifi-registry-core/nifi-registry-utils/src/main/java/org/apache/nifi/registry/util/FormatUtils.java
+++ b/nifi-registry-core/nifi-registry-utils/src/main/java/org/apache/nifi/registry/util/FormatUtils.java
@@ -17,12 +17,13 @@
package org.apache.nifi.registry.util;
import java.text.NumberFormat;
+import java.util.Arrays;
+import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class FormatUtils {
-
private static final String UNION = "|";
// for Data Sizes
@@ -41,8 +42,9 @@
private static final String WEEKS = join(UNION, "w", "wk", "wks", "week", "weeks");
private static final String VALID_TIME_UNITS = join(UNION, NANOS, MILLIS, SECS, MINS, HOURS, DAYS, WEEKS);
- public static final String TIME_DURATION_REGEX = "(\\d+)\\s*(" + VALID_TIME_UNITS + ")";
+ public static final String TIME_DURATION_REGEX = "([\\d.]+)\\s*(" + VALID_TIME_UNITS + ")";
public static final Pattern TIME_DURATION_PATTERN = Pattern.compile(TIME_DURATION_REGEX);
+ private static final List<Long> TIME_UNIT_MULTIPLIERS = Arrays.asList(1000L, 1000L, 1000L, 60L, 60L, 24L);
/**
* Formats the specified count by adding commas.
@@ -58,7 +60,7 @@
* Formats the specified duration in 'mm:ss.SSS' format.
*
* @param sourceDuration the duration to format
- * @param sourceUnit the unit to interpret the duration
+ * @param sourceUnit the unit to interpret the duration
* @return representation of the given time data in minutes/seconds
*/
public static String formatMinutesSeconds(final long sourceDuration, final TimeUnit sourceUnit) {
@@ -79,7 +81,7 @@
* Formats the specified duration in 'HH:mm:ss.SSS' format.
*
* @param sourceDuration the duration to format
- * @param sourceUnit the unit to interpret the duration
+ * @param sourceUnit the unit to interpret the duration
* @return representation of the given time data in hours/minutes/seconds
*/
public static String formatHoursMinutesSeconds(final long sourceDuration, final TimeUnit sourceUnit) {
@@ -139,65 +141,230 @@
return format.format(dataSize) + " bytes";
}
+ /**
+ * Returns a time duration in the requested {@link TimeUnit} after parsing the {@code String}
+ * input. If the resulting value is a decimal (i.e.
+ * {@code 25 hours -> TimeUnit.DAYS = 1.04}), the value is rounded.
+ *
+ * @param value the raw String input (i.e. "28 minutes")
+ * @param desiredUnit the requested output {@link TimeUnit}
+ * @return the whole number value of this duration in the requested units
+ * @deprecated As of Apache NiFi 1.9.0, because this method only returns whole numbers, use {@link #getPreciseTimeDuration(String, TimeUnit)} when possible.
+ */
+ @Deprecated
public static long getTimeDuration(final String value, final TimeUnit desiredUnit) {
+ return Math.round(getPreciseTimeDuration(value, desiredUnit));
+ }
+
+ /**
+ * Returns the parsed and converted input in the requested units.
+ * <p>
+ * If the value is {@code 0 <= x < 1} in the provided units, the units will first be converted to a smaller unit to get a value >= 1 (i.e. 0.5 seconds -> 500 milliseconds).
+ * This is because the underlying unit conversion cannot handle decimal values.
+ * <p>
+ * If the value is {@code x >= 1} but x is not a whole number, the units will first be converted to a smaller unit to attempt to get a whole number value (i.e. 1.5 seconds -> 1500 milliseconds).
+ * <p>
+ * If the value is {@code x < 1000} and the units are {@code TimeUnit.NANOSECONDS}, the result will be a whole number of nanoseconds, rounded (i.e. 123.4 ns -> 123 ns).
+ * <p>
+ * This method handles decimal values over {@code 1 ns}, but {@code < 1 ns} will return {@code 0} in any other unit.
+ * <p>
+ * Examples:
+ * <p>
+ * "10 seconds", {@code TimeUnit.MILLISECONDS} -> 10_000.0
+ * "0.010 s", {@code TimeUnit.MILLISECONDS} -> 10.0
+ * "0.010 s", {@code TimeUnit.SECONDS} -> 0.010
+ * "0.010 ns", {@code TimeUnit.NANOSECONDS} -> 1
+ * "0.010 ns", {@code TimeUnit.MICROSECONDS} -> 0
+ *
+ * @param value the {@code String} input
+ * @param desiredUnit the desired output {@link TimeUnit}
+ * @return the parsed and converted amount (without a unit)
+ */
+ public static double getPreciseTimeDuration(final String value, final TimeUnit desiredUnit) {
final Matcher matcher = TIME_DURATION_PATTERN.matcher(value.toLowerCase());
if (!matcher.matches()) {
- throw new IllegalArgumentException("Value '" + value + "' is not a valid Time Duration");
+ throw new IllegalArgumentException("Value '" + value + "' is not a valid time duration");
}
final String duration = matcher.group(1);
final String units = matcher.group(2);
- TimeUnit specifiedTimeUnit = null;
- switch (units.toLowerCase()) {
- case "ns":
- case "nano":
- case "nanos":
- case "nanoseconds":
- specifiedTimeUnit = TimeUnit.NANOSECONDS;
- break;
- case "ms":
- case "milli":
- case "millis":
- case "milliseconds":
- specifiedTimeUnit = TimeUnit.MILLISECONDS;
- break;
- case "s":
- case "sec":
- case "secs":
- case "second":
- case "seconds":
- specifiedTimeUnit = TimeUnit.SECONDS;
- break;
- case "m":
- case "min":
- case "mins":
- case "minute":
- case "minutes":
- specifiedTimeUnit = TimeUnit.MINUTES;
- break;
- case "h":
- case "hr":
- case "hrs":
- case "hour":
- case "hours":
- specifiedTimeUnit = TimeUnit.HOURS;
- break;
- case "d":
- case "day":
- case "days":
- specifiedTimeUnit = TimeUnit.DAYS;
- break;
+
+ double durationVal = Double.parseDouble(duration);
+ TimeUnit specifiedTimeUnit;
+
+ // The TimeUnit enum doesn't have a value for WEEKS, so handle this case independently
+ if (isWeek(units)) {
+ specifiedTimeUnit = TimeUnit.DAYS;
+ durationVal *= 7;
+ } else {
+ specifiedTimeUnit = determineTimeUnit(units);
+ }
+
+ // The units are now guaranteed to be in DAYS or smaller
+ long durationLong;
+ if (durationVal == Math.rint(durationVal)) {
+ durationLong = Math.round(durationVal);
+ } else {
+ // Try reducing the size of the units to make the input a long
+ List wholeResults = makeWholeNumberTime(durationVal, specifiedTimeUnit);
+ durationLong = (long) wholeResults.get(0);
+ specifiedTimeUnit = (TimeUnit) wholeResults.get(1);
+ }
+
+ return desiredUnit.convert(durationLong, specifiedTimeUnit);
+ }
+
+ /**
+ * Converts the provided time duration value to one that can be represented as a whole number.
+ * Returns a {@code List} containing the new value as a {@code long} at index 0 and the
+ * {@link TimeUnit} at index 1. If the incoming value is already whole, it is returned as is.
+ * If the incoming value cannot be made whole, a whole approximation is returned. For values
+ * {@code >= 1 TimeUnit.NANOSECONDS}, the value is rounded (i.e. 123.4 ns -> 123 ns).
+ * For values {@code < 1 TimeUnit.NANOSECONDS}, the constant [1L, {@code TimeUnit.NANOSECONDS}] is returned as the smallest measurable unit of time.
+ * <p>
+ * Examples:
+ * <p>
+ * 1, {@code TimeUnit.SECONDS} -> [1, {@code TimeUnit.SECONDS}]
+ * 1.1, {@code TimeUnit.SECONDS} -> [1100, {@code TimeUnit.MILLISECONDS}]
+ * 0.1, {@code TimeUnit.SECONDS} -> [100, {@code TimeUnit.MILLISECONDS}]
+ * 0.1, {@code TimeUnit.NANOSECONDS} -> [1, {@code TimeUnit.NANOSECONDS}]
+ *
+ * @param decimal the time duration as a decimal
+ * @param timeUnit the current time unit
+ * @return the time duration as a whole number ({@code long}) and the smaller time unit used
+ */
+ protected static List<Object> makeWholeNumberTime(double decimal, TimeUnit timeUnit) {
+ // If the value is already a whole number, return it and the current time unit
+ if (decimal == Math.rint(decimal)) {
+ return Arrays.asList(new Object[]{(long) decimal, timeUnit});
+ } else if (TimeUnit.NANOSECONDS == timeUnit) {
+ // The time unit is as small as possible
+ if (decimal < 1.0) {
+ decimal = 1;
+ } else {
+ decimal = Math.rint(decimal);
+ }
+ return Arrays.asList(new Object[]{(long) decimal, timeUnit});
+ } else {
+ // Determine the next time unit and the respective multiplier
+ TimeUnit smallerTimeUnit = getSmallerTimeUnit(timeUnit);
+ long multiplier = calculateMultiplier(timeUnit, smallerTimeUnit);
+
+ // Recurse with the original number converted to the smaller unit
+ return makeWholeNumberTime(decimal * multiplier, smallerTimeUnit);
+ }
+ }
+
+ /**
+ * Returns the numerical multiplier to convert a value from {@code originalTimeUnit} to
+ * {@code newTimeUnit} (i.e. for {@code TimeUnit.DAYS -> TimeUnit.MINUTES} would return
+ * 24 * 60 = 1440). If the original and new units are the same, returns 1. If the new unit
+ * is larger than the original (i.e. the result would be less than 1), throws an
+ * {@link IllegalArgumentException}.
+ *
+ * @param originalTimeUnit the source time unit
+ * @param newTimeUnit the destination time unit
+ * @return the numerical multiplier between the units
+ */
+ protected static long calculateMultiplier(TimeUnit originalTimeUnit, TimeUnit newTimeUnit) {
+ if (originalTimeUnit == newTimeUnit) {
+ return 1;
+ } else if (originalTimeUnit.ordinal() < newTimeUnit.ordinal()) {
+ throw new IllegalArgumentException("The original time unit '" + originalTimeUnit + "' must be larger than the new time unit '" + newTimeUnit + "'");
+ } else {
+ int originalOrd = originalTimeUnit.ordinal();
+ int newOrd = newTimeUnit.ordinal();
+
+ List<Long> unitMultipliers = TIME_UNIT_MULTIPLIERS.subList(newOrd, originalOrd);
+ return unitMultipliers.stream().reduce(1L, (a, b) -> (long) a * b);
+ }
+ }
+
+ /**
+ * Returns the next smallest {@link TimeUnit} (i.e. {@code TimeUnit.DAYS -> TimeUnit.HOURS}).
+ * If the parameter is {@code null} or {@code TimeUnit.NANOSECONDS}, an
+ * {@link IllegalArgumentException} is thrown because there is no valid smaller TimeUnit.
+ *
+ * @param originalUnit the TimeUnit
+ * @return the next smaller TimeUnit
+ */
+ protected static TimeUnit getSmallerTimeUnit(TimeUnit originalUnit) {
+ if (originalUnit == null || TimeUnit.NANOSECONDS == originalUnit) {
+ throw new IllegalArgumentException("Cannot determine a smaller time unit than '" + originalUnit + "'");
+ } else {
+ return TimeUnit.values()[originalUnit.ordinal() - 1];
+ }
+ }
+
+ /**
+ * Returns {@code true} if this raw unit {@code String} is parsed as representing "weeks", which does not have a value in the {@link TimeUnit} enum.
+ *
+ * @param rawUnit the String containing the desired unit
+ * @return true if the unit is "weeks"; false otherwise
+ */
+ protected static boolean isWeek(final String rawUnit) {
+ switch (rawUnit) {
case "w":
case "wk":
case "wks":
case "week":
case "weeks":
- final long durationVal = Long.parseLong(duration);
- return desiredUnit.convert(durationVal, TimeUnit.DAYS)*7;
+ return true;
+ default:
+ return false;
}
+ }
- final long durationVal = Long.parseLong(duration);
- return desiredUnit.convert(durationVal, specifiedTimeUnit);
+ /**
+ * Returns the {@link TimeUnit} enum that maps to the provided raw {@code String} input. The
+ * highest time unit is {@code TimeUnit.DAYS}. Any input that cannot be parsed will result in
+ * an {@link IllegalArgumentException}.
+ *
+ * @param rawUnit the String to parse
+ * @return the TimeUnit
+ */
+ protected static TimeUnit determineTimeUnit(String rawUnit) {
+ switch (rawUnit.toLowerCase()) {
+ case "ns":
+ case "nano":
+ case "nanos":
+ case "nanoseconds":
+ return TimeUnit.NANOSECONDS;
+ case "µs":
+ case "micro":
+ case "micros":
+ case "microseconds":
+ return TimeUnit.MICROSECONDS;
+ case "ms":
+ case "milli":
+ case "millis":
+ case "milliseconds":
+ return TimeUnit.MILLISECONDS;
+ case "s":
+ case "sec":
+ case "secs":
+ case "second":
+ case "seconds":
+ return TimeUnit.SECONDS;
+ case "m":
+ case "min":
+ case "mins":
+ case "minute":
+ case "minutes":
+ return TimeUnit.MINUTES;
+ case "h":
+ case "hr":
+ case "hrs":
+ case "hour":
+ case "hours":
+ return TimeUnit.HOURS;
+ case "d":
+ case "day":
+ case "days":
+ return TimeUnit.DAYS;
+ default:
+ throw new IllegalArgumentException("Could not parse '" + rawUnit + "' to TimeUnit");
+ }
}
public static String formatUtilization(final double utilization) {
@@ -225,15 +392,15 @@
* 3 seconds, 8 millis, 3 nanos - if includeTotalNanos = false,
* 3 seconds, 8 millis, 3 nanos (3008000003 nanos) - if includeTotalNanos = true
*
- * @param nanos the number of nanoseconds to format
+ * @param nanos the number of nanoseconds to format
* @param includeTotalNanos whether or not to include the total number of nanoseconds in parentheses in the returned value
* @return a human-readable String that is a formatted representation of the given number of nanoseconds.
*/
public static String formatNanos(final long nanos, final boolean includeTotalNanos) {
final StringBuilder sb = new StringBuilder();
- final long seconds = nanos > 1000000000L ? nanos / 1000000000L : 0L;
- long millis = nanos > 1000000L ? nanos / 1000000L : 0L;
+ final long seconds = nanos >= 1000000000L ? nanos / 1000000000L : 0L;
+ long millis = nanos >= 1000000L ? nanos / 1000000L : 0L;
final long nanosLeft = nanos % 1000000L;
if (seconds > 0) {
@@ -258,4 +425,5 @@
return sb.toString();
}
+
}
diff --git a/nifi-registry-core/nifi-registry-web-api/src/test/java/org/apache/nifi/registry/web/TestRestAPI.java b/nifi-registry-core/nifi-registry-web-api/src/test/java/org/apache/nifi/registry/web/TestRestAPI.java
index 3cbc892..038b299 100644
--- a/nifi-registry-core/nifi-registry-web-api/src/test/java/org/apache/nifi/registry/web/TestRestAPI.java
+++ b/nifi-registry-core/nifi-registry-web-api/src/test/java/org/apache/nifi/registry/web/TestRestAPI.java
@@ -21,6 +21,7 @@
import org.apache.nifi.registry.flow.VersionedFlowSnapshot;
import org.apache.nifi.registry.flow.VersionedFlowSnapshotMetadata;
import org.apache.nifi.registry.flow.VersionedProcessGroup;
+import org.apache.nifi.registry.revision.entity.RevisionInfo;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -92,6 +93,7 @@
final Bucket bucket = new Bucket();
bucket.setName("Bucket #" + num);
bucket.setDescription("This is bucket #" + num);
+ bucket.setRevision(new RevisionInfo("clientId", 0L));
final Bucket createdBucket = client.target(REGISTRY_API_BUCKETS_URL)
.request()
@@ -107,6 +109,7 @@
final VersionedFlow versionedFlow = new VersionedFlow();
versionedFlow.setName(bucket.getName() + " Flow #" + num);
versionedFlow.setDescription("This is " + bucket.getName() + " flow #" + num);
+ versionedFlow.setRevision(new RevisionInfo("clientId", 0L));
final VersionedFlow createdFlow = client.target(REGISTRY_API_BUCKETS_URL)
.path("/{bucketId}/flows")
diff --git a/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/components/login/dialogs/nf-registry-user-login.html b/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/components/login/dialogs/nf-registry-user-login.html
index eee4614..e873e47 100644
--- a/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/components/login/dialogs/nf-registry-user-login.html
+++ b/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/components/login/dialogs/nf-registry-user-login.html
@@ -32,7 +32,7 @@
</div>
<div class="fill-available-width">
<mat-form-field floatLabel="always" fxFlex>
- <input #passwordInput type="password" matInput floatPlaceholder="always" placeholder="Password">
+ <input #passwordInput type="password" matInput floatPlaceholder="always" placeholder="Password" autocomplete="off">
</mat-form-field>
</div>
</div>
diff --git a/nifi-registry-extensions/nifi-registry-ranger/nifi-registry-ranger-plugin/pom.xml b/nifi-registry-extensions/nifi-registry-ranger/nifi-registry-ranger-plugin/pom.xml
index fa291d5..df21832 100644
--- a/nifi-registry-extensions/nifi-registry-ranger/nifi-registry-ranger-plugin/pom.xml
+++ b/nifi-registry-extensions/nifi-registry-ranger/nifi-registry-ranger-plugin/pom.xml
@@ -212,4 +212,70 @@
</dependencies>
+ <profiles>
+ <!-- Includes hadoop-aws for accessing HDFS with an s3a:// filesystem -->
+ <profile>
+ <id>include-hadoop-aws</id>
+ <activation>
+ <activeByDefault>false</activeByDefault>
+ </activation>
+ <dependencies>
+ <dependency>
+ <groupId>org.apache.hadoop</groupId>
+ <artifactId>hadoop-aws</artifactId>
+ <version>${ranger.hadoop.version}</version>
+ </dependency>
+ </dependencies>
+ </profile>
+ <!-- Includes hadoop-azure and hadoop-azure-datalake for accessing HDFS with wasb://, abfs://, and adl:// filesystems -->
+ <profile>
+ <id>include-hadoop-azure</id>
+ <activation>
+ <activeByDefault>false</activeByDefault>
+ </activation>
+ <dependencies>
+ <dependency>
+ <groupId>org.apache.hadoop</groupId>
+ <artifactId>hadoop-azure</artifactId>
+ <version>${ranger.hadoop.version}</version>
+ <exclusions>
+ <exclusion>
+ <groupId>com.google.guava</groupId>
+ <artifactId>guava</artifactId>
+ </exclusion>
+ <exclusion>
+ <groupId>com.fasterxml.jackson.core</groupId>
+ <artifactId>jackson-core</artifactId>
+ </exclusion>
+ </exclusions>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.hadoop</groupId>
+ <artifactId>hadoop-azure-datalake</artifactId>
+ <version>${ranger.hadoop.version}</version>
+ <exclusions>
+ <exclusion>
+ <groupId>com.fasterxml.jackson.core</groupId>
+ <artifactId>jackson-core</artifactId>
+ </exclusion>
+ </exclusions>
+ </dependency>
+ </dependencies>
+ </profile>
+ <!-- Includes hadoop-cloud-storage -->
+ <profile>
+ <id>include-hadoop-cloud-storage</id>
+ <activation>
+ <activeByDefault>false</activeByDefault>
+ </activation>
+ <dependencies>
+ <dependency>
+ <groupId>org.apache.hadoop</groupId>
+ <artifactId>hadoop-cloud-storage</artifactId>
+ <version>${ranger.hadoop.version}</version>
+ </dependency>
+ </dependencies>
+ </profile>
+ </profiles>
+
</project>
\ No newline at end of file
diff --git a/pom.xml b/pom.xml
index 2ac512a..5e6108b 100644
--- a/pom.xml
+++ b/pom.xml
@@ -96,14 +96,14 @@
<jax.rs.api.version>2.1</jax.rs.api.version>
<jersey.version>2.27</jersey.version>
<jackson.version>2.9.9</jackson.version>
- <jackson.databind.version>2.9.9.1</jackson.databind.version>
- <spring.boot.version>2.1.6.RELEASE</spring.boot.version>
- <spring.security.version>5.1.5.RELEASE</spring.security.version>
+ <jackson.databind.version>2.9.10.2</jackson.databind.version>
+ <spring.boot.version>2.1.12.RELEASE</spring.boot.version>
+ <spring.security.version>5.1.8.RELEASE</spring.security.version>
<flyway.version>5.2.4</flyway.version>
<flyway.tests.version>5.1.0</flyway.tests.version>
<swagger.ui.version>3.12.0</swagger.ui.version>
<testcontainers.version>1.11.2</testcontainers.version>
- <h2.version>1.4.197</h2.version>
+ <h2.version>1.4.200</h2.version>
<groovy.version>2.5.4</groovy.version>
<groovy.eclipse.compiler.version>3.4.0-01</groovy.eclipse.compiler.version>
<jaxb.version>2.3.2</jaxb.version>