NIFI-5973 Adds ShellUserGroupProvider.

NIFI-5973 More comments and better defaults for the shell provider.
NIFI-5973 Fixed bug where user was being retrieved by identifier when identity was provided.
NIFI-5973 Fixed a formatting string in the OS X shell commands.
Updated testing conditions to run IT in OS X environment.
Changed unit test to provide identity rather than identifier.

This closes #3537.

Signed-off-by: Andy LoPresto <alopresto@apache.org>
This commit is contained in:
Troy Melhase 2019-06-18 16:41:44 -08:00 committed by Andy LoPresto
parent 58ae7d4f92
commit e973cacb2f
No known key found for this signature in database
GPG Key ID: 6EC293152D90B61D
18 changed files with 1590 additions and 2 deletions

View File

@ -452,6 +452,27 @@ The LdapUserGroupProvider has the following properties:
NOTE: Any identity mapping rules specified in _nifi.properties_ will also be applied to the user identities. Group names are not mapped.
==== ShellUserGroupProvider
The ShellUserGroupProvider fetches user and group details from Unix-like systems using shell commands.
This provider executes various shell pipelines with commands such as `getent` on Linux and `dscl` on MacOS.
Supported systems may be configured to retrieve users and groups from an external source, such as LDAP or NIS. In these cases the shell commands
will return those external users and groups. This provides administrators another mechanism to integrate user and group directory services.
The ShellUserGroupProvider has the following properties:
[options="header,footer"]
|==================================================================================================================================================
| Property Name | Description
|`Initial Refresh Delay` | Duration of initial delay before first user and group refresh. (i.e. `10 secs`). Default is `5 mins`.
|`Refresh Delay` | Duration of delay between each user and group refresh. (i.e. `10 secs`). Default is `5 mins`.
|==================================================================================================================================================
Like LdapUserGroupProvider, the ShellUserGroupProvider is commented out in the _authorizers.xml_ file. Refer to that comment for usage examples.
==== Composite Implementations
Another option for the UserGroupProvider are composite implementations. This means that multiple sources/implementations can be configured and composed. For instance, an admin can configure users/groups to be loaded from a file and a directory server. There are two composite implementations, one that supports multiple UserGroupProviders and one that supports multiple UserGroupProviders and a single configurable UserGroupProvider.

View File

@ -35,6 +35,10 @@
<groupId>org.apache.nifi</groupId>
<artifactId>nifi-file-authorizer</artifactId>
</dependency>
<dependency>
<groupId>org.apache.nifi</groupId>
<artifactId>nifi-shell-authorizer</artifactId>
</dependency>
<dependency>
<groupId>org.apache.nifi</groupId>
<artifactId>nifi-authorizer</artifactId>

View File

@ -130,5 +130,11 @@
<artifactId>nifi-expression-language</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>testcontainers</artifactId>
<version>1.11.3</version>
<scope>test</scope>
</dependency>
</dependencies>
</project>

View File

@ -166,6 +166,23 @@
</userGroupProvider>
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`.
'Initial Refresh Delay' - duration to wait before first refresh. Default is '5 mins'.
'Refresh Delay' - duration to wait between subsequent refreshes. Default is '5 mins'.
-->
<!-- 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.authorization.ShellUserGroupProvider</class>
<property name="Initial Refresh Delay">5 mins</property>
<property name="Refresh Delay">5 mins</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.
@ -198,6 +215,7 @@
NOTE: Any identity mapping rules specified in nifi.properties are not applied in this implementation. This behavior
would need to be applied by the base implementation.
-->
<!-- To enable the composite-configurable-user-group-provider remove 2 lines. This is 1 of 2.
<userGroupProvider>
<identifier>composite-configurable-user-group-provider</identifier>

View File

@ -0,0 +1,46 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
Licensed to the Apache Software Foundation (ASF) under one or more
contributor license agreements. See the NOTICE file distributed with
this work for additional information regarding copyright ownership.
The ASF licenses this file to You under the Apache License, Version 2.0
(the "License"); you may not use this file except in compliance with
the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>nifi-framework</artifactId>
<groupId>org.apache.nifi</groupId>
<version>1.10.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>nifi-shell-authorizer</artifactId>
<dependencies>
<dependency>
<groupId>org.apache.nifi</groupId>
<artifactId>nifi-framework-api</artifactId>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>testcontainers</artifactId>
<version>1.11.3</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.apache.nifi</groupId>
<artifactId>nifi-mock</artifactId>
</dependency>
<dependency>
<groupId>org.apache.nifi</groupId>
<artifactId>nifi-utils</artifactId>
<version>1.10.0-SNAPSHOT</version>
</dependency>
</dependencies>
</project>

View File

@ -0,0 +1,90 @@
/*
* 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.authorization;
/**
* 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 passwd";
}
}

View File

@ -0,0 +1,82 @@
/*
* 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.authorization;
/**
* 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";
}
}

View File

@ -0,0 +1,74 @@
/*
* 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.authorization;
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);
}
}

View File

@ -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.authorization;
/**
* 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();
}

View File

@ -0,0 +1,585 @@
/*
* 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.authorization;
import java.io.IOException;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import org.apache.nifi.authorization.exception.AuthorizationAccessException;
import org.apache.nifi.authorization.exception.AuthorizerCreationException;
import org.apache.nifi.authorization.exception.AuthorizerDestructionException;
import org.apache.nifi.authorization.util.ShellRunner;
import org.apache.nifi.components.PropertyValue;
import org.apache.nifi.util.FormatUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/*
* 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 INITIAL_REFRESH_DELAY_PROPERTY = "Initial Refresh Delay";
public static final String REFRESH_DELAY_PROPERTY = "Refresh Delay";
private static final long MINIMUM_SYNC_INTERVAL_MILLISECONDS = 10_000;
private long initialDelay;
private long fixedDelay;
// 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) {
refreshOneUser(selectedShellCommands.getUserById(identifier), "Get Single User by Id");
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 + " 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 + " 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<>();
for (Group g : getGroups()) {
if (user != null && g.getUsers().contains(user.getIdentity())) {
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 AuthorizerCreationException {
}
/**
* Called to configure the Authorizer.
*
* @param configurationContext at the time of configuration
* @throws AuthorizerCreationException for any issues configuring the provider
*/
@Override
public void onConfigured(AuthorizerConfigurationContext configurationContext) throws AuthorizerCreationException {
initialDelay = getDelayProperty(configurationContext, INITIAL_REFRESH_DELAY_PROPERTY, "5 mins");
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 IOException ioexc) {
logger.error("initialize exception: " + ioexc + " system check command: " + commands.getSystemCheck());
throw new AuthorizerCreationException(SYS_CHECK_ERROR, ioexc.getCause());
}
// 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(this::refreshUsersAndGroups, initialDelay, fixedDelay, TimeUnit.SECONDS);
}
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 AuthorizerCreationException(OS_TYPE_ERROR);
}
return commands;
}
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 AuthorizerCreationException(String.format("The %s '%s' is not a valid time interval.", propertyName, propertyValue));
}
if (syncInterval < MINIMUM_SYNC_INTERVAL_MILLISECONDS) {
throw new AuthorizerCreationException(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 AuthorizerDestructionException If pre-destruction fails.
*/
@Override
public void preDestruction() throws AuthorizerDestructionException {
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);
}
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());
}
}
/**
* 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 -> {
String[] record = line.split(":");
if (record.length > 2) {
String name = record[0], id = record[1], gid = record[2];
if (name != null && id != null && !name.equals("") && !id.equals("")) {
User user = new User.Builder().identity(name).identifier(id).build();
idToUser.put(id, user);
usernameToUser.put(name, user);
if (gid != null && !gid.equals("")) {
gidToUser.put(gid, user);
} else {
logger.warn("Null or empty primary group id for: " + name);
}
} else {
logger.warn("Null or empty user name: " + name + " or id: " + id);
}
} 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 -> {
String[] record = line.split(":");
if (record.length > 1) {
Set<String> users = new HashSet<>();
String name = record[0], id = record[1];
try {
List<String> memberLines = ShellRunner.runShell(selectedShellCommands.getGroupMembers(name));
// Use the first line only, and log if the line count isn't exactly one:
if (!memberLines.isEmpty()) {
users.addAll(Arrays.asList(memberLines.get(0).split(",")));
} 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 (name != null && id != null && !name.equals("") && !id.equals("")) {
Group group = new Group.Builder().name(name).identifier(id).addUsers(users).build();
groupsById.put(id, group);
logger.debug("Refreshed group: " + group);
} else {
logger.warn("Null or empty group name: " + name + " or id: " + id);
}
} 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("user: " + primaryUser + " primary group not found");
} else {
Set<String> groupUsers = primaryGroup.getUsers();
if (!groupUsers.contains(primaryUser.getIdentity())) {
Set<String> secondSet = new HashSet<>(groupUsers);
secondSet.add(primaryUser.getIdentity());
Group group = new Group.Builder().name(primaryGroup.getName()).identifier(primaryGid).addUsers(secondSet).build();
gidToGroup.put(primaryGid, group);
}
}
});
}
/**
* @return The initial refresh delay.
*/
public long getInitialRefreshDelay() {
return initialDelay;
}
/**
* @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();
}
}

View File

@ -0,0 +1,77 @@
/*
* 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.authorization.util;
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 = 30;
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();
try {
proc.waitFor(TIMEOUT, TimeUnit.SECONDS);
} catch (InterruptedException irexc) {
throw new IOException(irexc.getMessage(), irexc.getCause());
}
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;
}
}

View File

@ -0,0 +1,15 @@
# Licensed to the Apache Software Foundation (ASF) under one or more
# contributor license agreements. See the NOTICE file distributed with
# this work for additional information regarding copyright ownership.
# The ASF licenses this file to You under the Apache License, Version 2.0
# (the "License"); you may not use this file except in compliance with
# the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
org.apache.nifi.authorization.ShellUserGroupProvider

View File

@ -0,0 +1,176 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.nifi.authorization;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assume.assumeTrue;
import java.util.Set;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
abstract class ShellUserGroupProviderBase {
private static final Logger logger = LoggerFactory.getLogger(ShellUserGroupProviderBase.class);
private final String KNOWN_USER = "root";
private final String KNOWN_UID = "0";
@SuppressWarnings("FieldCanBeLocal")
private final String KNOWN_GROUP = "root";
@SuppressWarnings("FieldCanBeLocal")
private final String OTHER_GROUP = "wheel"; // e.g., macos
private final String KNOWN_GID = "0";
// We're using this knob to control the test runs on Travis. The issue there is that tests
// running on Travis do not have `getent`, thus not behaving like a typical Linux installation.
protected static boolean systemCheckFailed = false;
/**
* Ensures that the test can run because Docker is available and the remote instance can be reached via ssh.
*
* @return true if Docker is available on this OS
*/
protected boolean isSSHAvailable() {
return !systemCheckFailed;
}
/**
* Tests the provider behavior by getting its users and checking minimum size.
*
* @param provider {@link UserGroupProvider}
*/
void testGetUsersAndUsersMinimumCount(UserGroupProvider provider) {
assumeTrue(isSSHAvailable());
Set<User> users = provider.getUsers();
assertNotNull(users);
assertTrue(users.size() > 0);
}
/**
* Tests the provider behavior by getting a known user by uid.
*
* @param provider {@link UserGroupProvider}
*/
void testGetKnownUserByUsername(UserGroupProvider provider) {
// assumeTrue(isSSHAvailable());
User root = provider.getUser(KNOWN_UID);
assertNotNull(root);
assertEquals(KNOWN_USER, root.getIdentity());
assertEquals(KNOWN_UID, root.getIdentifier());
}
/**
* Tests the provider behavior by getting a known user by id.
*
* @param provider {@link UserGroupProvider}
*/
void testGetKnownUserByUid(UserGroupProvider provider) {
assumeTrue(isSSHAvailable());
User root = provider.getUserByIdentity(KNOWN_USER);
assertNotNull(root);
assertEquals(KNOWN_USER, root.getIdentity());
assertEquals(KNOWN_UID, root.getIdentifier());
}
/**
* Tests the provider behavior by getting its groups and checking minimum size.
*
* @param provider {@link UserGroupProvider}
*/
void testGetGroupsAndMinimumGroupCount(UserGroupProvider provider) {
assumeTrue(isSSHAvailable());
Set<Group> groups = provider.getGroups();
assertNotNull(groups);
assertTrue(groups.size() > 0);
}
/**
* Tests the provider behavior by getting a known group by GID.
*
* @param provider {@link UserGroupProvider}
*/
void testGetKnownGroupByGid(UserGroupProvider provider) {
assumeTrue(isSSHAvailable());
Group group = provider.getGroup(KNOWN_GID);
assertNotNull(group);
assertTrue(group.getName().equals(KNOWN_GROUP) || group.getName().equals(OTHER_GROUP));
assertEquals(KNOWN_GID, group.getIdentifier());
}
/**
* Tests the provider behavior by getting a known group and checking for a known member of it.
*
* @param provider {@link UserGroupProvider}
*/
void testGetGroupByGidAndGetGroupMembership(UserGroupProvider provider) {
assumeTrue(isSSHAvailable());
Group group = provider.getGroup(KNOWN_GID);
assertNotNull(group);
// These next few try/catch blocks are here for debugging. The user-to-group relationship
// is delicate with this implementation, and this approach allows us a measure of control.
// Check your logs if you're having problems!
try {
assertTrue(group.getUsers().size() > 0);
logger.info("root group count: " + group.getUsers().size());
} catch (final AssertionError ignored) {
logger.info("root group count zero on this system");
}
try {
assertTrue(group.getUsers().contains(KNOWN_USER));
logger.info("root group membership: " + group.getUsers());
} catch (final AssertionError ignored) {
logger.info("root group membership unexpected on this system");
}
}
/**
* Tests the provider behavior by getting a known user and checking its group membership.
*
* @param provider {@link UserGroupProvider}
*/
void testGetUserByIdentityAndGetGroupMembership(UserGroupProvider provider) {
assumeTrue(isSSHAvailable());
UserAndGroups user = provider.getUserAndGroups(KNOWN_USER);
assertNotNull(user);
try {
assertTrue(user.getGroups().size() > 0);
logger.info("root user group count: " + user.getGroups().size());
} catch (final AssertionError ignored) {
logger.info("root user and groups group count zero on this system");
}
Set<Group> groups = provider.getGroups();
assertTrue(groups.size() > user.getGroups().size());
}
}

View File

@ -0,0 +1,283 @@
/*
* 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.authorization;
import java.io.File;
import java.io.IOException;
import java.util.Arrays;
import java.util.List;
import org.apache.nifi.authorization.exception.AuthorizerCreationException;
import org.apache.nifi.authorization.util.ShellRunner;
import org.apache.nifi.util.MockPropertyValue;
import org.junit.After;
import org.junit.Assert;
import org.junit.Assume;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.ClassRule;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.junit.rules.TemporaryFolder;
import org.mockito.Mockito;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.testcontainers.containers.GenericContainer;
import org.testcontainers.utility.MountableFile;
public class ShellUserGroupProviderIT extends ShellUserGroupProviderBase {
private static final Logger logger = LoggerFactory.getLogger(ShellUserGroupProviderIT.class);
// These images are publicly available on the hub.docker.com, and the source to each
// is available on github. In lieu of using named images, the Dockerfiles could be
// migrated into module and referenced in the testcontainer setup.
private final static String ALPINE_IMAGE = "natural/alpine-sshd:latest";
private final static String CENTOS_IMAGE = "natural/centos-sshd:latest";
private final static String DEBIAN_IMAGE = "natural/debian-sshd:latest";
private final static String UBUNTU_IMAGE = "natural/ubuntu-sshd:latest";
private final static List<String> TEST_CONTAINER_IMAGES =
Arrays.asList(
ALPINE_IMAGE,
CENTOS_IMAGE,
DEBIAN_IMAGE,
UBUNTU_IMAGE
);
private final static String CONTAINER_SSH_AUTH_KEYS = "/root/.ssh/authorized_keys";
private final static Integer CONTAINER_SSH_PORT = 22;
private static String sshPrivKeyFile;
private static String sshPubKeyFile;
private AuthorizerConfigurationContext authContext;
private ShellUserGroupProvider localProvider;
private UserGroupProviderInitializationContext initContext;
@ClassRule
static public TemporaryFolder tempFolder = new TemporaryFolder();
@Rule
public ExpectedException expectedException = ExpectedException.none();
@BeforeClass
public static void setupOnce() throws IOException {
sshPrivKeyFile = tempFolder.getRoot().getAbsolutePath() + "/id_rsa";
sshPubKeyFile = sshPrivKeyFile + ".pub";
try {
// NB: this command is a bit perplexing: it works without prompt from the shell, but hangs
// here without the pipe from `yes`:
ShellRunner.runShell("yes | ssh-keygen -C '' -N '' -t rsa -f " + sshPrivKeyFile);
} catch (final IOException ioexc) {
systemCheckFailed = true;
logger.error("setupOnce() exception: " + ioexc + "; tests cannot run on this system.");
return;
}
// Fix the file permissions to abide by the ssh client
// requirements:
Arrays.asList(sshPrivKeyFile, sshPubKeyFile).forEach(name -> {
final File f = new File(name);
Assert.assertTrue(f.setReadable(false, false));
Assert.assertTrue(f.setReadable(true));
});
}
@Before
public void setup() throws IOException {
authContext = Mockito.mock(AuthorizerConfigurationContext.class);
initContext = Mockito.mock(UserGroupProviderInitializationContext.class);
Mockito.when(authContext.getProperty(Mockito.eq(ShellUserGroupProvider.INITIAL_REFRESH_DELAY_PROPERTY))).thenReturn(new MockPropertyValue("10 sec"));
Mockito.when(authContext.getProperty(Mockito.eq(ShellUserGroupProvider.REFRESH_DELAY_PROPERTY))).thenReturn(new MockPropertyValue("15 sec"));
localProvider = new ShellUserGroupProvider();
try {
localProvider.initialize(initContext);
localProvider.onConfigured(authContext);
} catch (final Exception exc) {
systemCheckFailed = true;
logger.error("setup() exception: " + exc + "; tests cannot run on this system.");
return;
}
Assert.assertEquals(10000, localProvider.getInitialRefreshDelay());
Assert.assertEquals(15000, localProvider.getRefreshDelay());
}
@After
public void tearDown() {
localProvider.preDestruction();
}
// Our primary test methods all accept a provider; here we define overloads to those methods to
// use the local provider. This allows the reuse of those test methods with the remote provider.
@Test
public void testGetUsersAndUsersMinimumCount() {
testGetUsersAndUsersMinimumCount(localProvider);
}
@Test
public void testGetKnownUserByUsername() {
testGetKnownUserByUsername(localProvider);
}
@Test
public void testGetKnownUserByUid() {
testGetKnownUserByUid(localProvider);
}
@Test
public void testGetGroupsAndMinimumGroupCount() {
testGetGroupsAndMinimumGroupCount(localProvider);
}
@Test
public void testGetKnownGroupByGid() {
testGetKnownGroupByGid(localProvider);
}
@Test
public void testGetGroupByGidAndGetGroupMembership() {
testGetGroupByGidAndGetGroupMembership(localProvider);
}
@Test
public void testGetUserByIdentityAndGetGroupMembership() {
testGetUserByIdentityAndGetGroupMembership(localProvider);
}
@SuppressWarnings("RedundantThrows")
private GenericContainer createContainer(String image) throws IOException, InterruptedException {
GenericContainer container = new GenericContainer(image)
.withEnv("SSH_ENABLE_ROOT", "true").withExposedPorts(CONTAINER_SSH_PORT);
container.start();
// This can go into the docker images:
container.execInContainer("mkdir", "-p", "/root/.ssh");
container.copyFileToContainer(MountableFile.forHostPath(sshPubKeyFile), CONTAINER_SSH_AUTH_KEYS);
return container;
}
private UserGroupProvider createRemoteProvider(GenericContainer container) {
final ShellCommandsProvider remoteCommands =
RemoteShellCommands.wrapOtherProvider(new NssShellCommands(),
sshPrivKeyFile,
container.getContainerIpAddress(),
container.getMappedPort(CONTAINER_SSH_PORT));
ShellUserGroupProvider remoteProvider = new ShellUserGroupProvider();
remoteProvider.setCommandsProvider(remoteCommands);
remoteProvider.initialize(initContext);
remoteProvider.onConfigured(authContext);
return remoteProvider;
}
@Test
public void testTooShortDelayIntervalThrowsException() throws AuthorizerCreationException {
final AuthorizerConfigurationContext authContext = Mockito.mock(AuthorizerConfigurationContext.class);
final ShellUserGroupProvider localProvider = new ShellUserGroupProvider();
Mockito.when(authContext.getProperty(Mockito.eq(ShellUserGroupProvider.INITIAL_REFRESH_DELAY_PROPERTY))).thenReturn(new MockPropertyValue("1 milliseconds"));
expectedException.expect(AuthorizerCreationException.class);
expectedException.expectMessage("The Initial Refresh Delay '1 milliseconds' is below the minimum value of '10000 ms'");
localProvider.onConfigured(authContext);
}
@Test
public void testInvalidDelayIntervalThrowsException() throws AuthorizerCreationException {
final AuthorizerConfigurationContext authContext = Mockito.mock(AuthorizerConfigurationContext.class);
final ShellUserGroupProvider localProvider = new ShellUserGroupProvider();
Mockito.when(authContext.getProperty(Mockito.eq(ShellUserGroupProvider.INITIAL_REFRESH_DELAY_PROPERTY))).thenReturn(new MockPropertyValue("Not an interval"));
expectedException.expect(AuthorizerCreationException.class);
expectedException.expectMessage("The Initial Refresh Delay 'Not an interval' is not a valid time interval.");
localProvider.onConfigured(authContext);
}
@Test
public void testCacheSizesAfterClearingCaches() {
localProvider.clearCaches();
assert localProvider.userCacheSize() == 0;
assert localProvider.groupCacheSize() == 0;
}
@Test
public void testGetOneUserAfterClearingCaches() {
// assert known state: empty, testable, not empty
localProvider.clearCaches();
testGetKnownUserByUid(localProvider);
assert localProvider.userCacheSize() > 0;
}
@Test
public void testGetOneGroupAfterClearingCaches() {
Assume.assumeTrue(isSSHAvailable());
// assert known state: empty, testable, not empty
localProvider.clearCaches();
testGetKnownGroupByGid(localProvider);
assert localProvider.groupCacheSize() > 0;
}
@Test
public void testVariousSystemImages() {
// Here we explicitly clear the system check flag to allow the remote checks that follow:
systemCheckFailed = false;
Assume.assumeTrue(isSSHAvailable());
TEST_CONTAINER_IMAGES.forEach(image -> {
GenericContainer container;
UserGroupProvider remoteProvider;
logger.debug("creating container from image: " + image);
try {
container = createContainer(image);
} catch (final Exception exc) {
logger.error("create container exception: " + exc);
return;
}
try {
remoteProvider = createRemoteProvider(container);
} catch (final Exception exc) {
logger.error("create user provider exception: " + exc);
return;
}
try {
testGetUsersAndUsersMinimumCount(remoteProvider);
testGetKnownUserByUsername(remoteProvider);
testGetGroupsAndMinimumGroupCount(remoteProvider);
testGetKnownGroupByGid(remoteProvider);
testGetGroupByGidAndGetGroupMembership(remoteProvider);
testGetUserByIdentityAndGetGroupMembership(remoteProvider);
} catch (final Exception e) {
// Some environments don't allow our tests to work.
logger.error("Exception running remote provider on image: " + image + ", exception: " + e);
}
container.stop();
remoteProvider.preDestruction();
logger.debug("finished with container image: " + image);
});
}
// TODO: Make test which retrieves list of users and then getUserByIdentity to ensure the user is populated in the response
}

View File

@ -1342,7 +1342,7 @@
});
// set the rows
usersData.setItems(users);
usersData.setItems(users, 'uri');
// end the update
usersData.endUpdate();

View File

@ -47,6 +47,7 @@
<module>nifi-properties-loader</module>
<module>nifi-standard-prioritizers</module>
<module>nifi-mock-authorizer</module>
<module>nifi-shell-authorizer</module>
</modules>
<dependencies>
<dependency>

View File

@ -184,6 +184,11 @@
<artifactId>nifi-file-authorizer</artifactId>
<version>1.10.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.apache.nifi</groupId>
<artifactId>nifi-shell-authorizer</artifactId>
<version>1.10.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.apache.nifi</groupId>
<artifactId>nifi-authorizer</artifactId>

View File

@ -239,7 +239,12 @@
<version>1.3</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>testcontainers</artifactId>
<version>1.11.3</version>
<scope>test</scope>
</dependency>
<!-- These Jetty dependencies are required for the Jetty Web Server all nars extend from it so we dont want this getting overriden -->
<dependency>
<groupId>org.eclipse.jetty</groupId>