mirror of https://github.com/apache/nifi.git
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:
parent
58ae7d4f92
commit
e973cacb2f
|
@ -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.
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
|
@ -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";
|
||||
}
|
||||
}
|
|
@ -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";
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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
|
|
@ -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());
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -1342,7 +1342,7 @@
|
|||
});
|
||||
|
||||
// set the rows
|
||||
usersData.setItems(users);
|
||||
usersData.setItems(users, 'uri');
|
||||
|
||||
// end the update
|
||||
usersData.endUpdate();
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
7
pom.xml
7
pom.xml
|
@ -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>
|
||||
|
|
Loading…
Reference in New Issue