-
Notifications
You must be signed in to change notification settings - Fork 9.2k
HADOOP-13817. Add a finite shell command timeout to ShellBasedUnixGroupsMapping. #161
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 3 commits
6707438
87e53b0
28a6bd4
a594e6a
ccddde5
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -18,17 +18,24 @@ | |
| package org.apache.hadoop.security; | ||
|
|
||
| import java.io.IOException; | ||
| import java.util.Arrays; | ||
| import java.util.LinkedList; | ||
| import java.util.List; | ||
| import java.util.StringTokenizer; | ||
| import java.util.concurrent.TimeUnit; | ||
|
|
||
| import com.google.common.annotations.VisibleForTesting; | ||
| import org.apache.commons.lang.StringUtils; | ||
| import org.apache.commons.logging.Log; | ||
| import org.apache.commons.logging.LogFactory; | ||
| import org.apache.hadoop.classification.InterfaceAudience; | ||
| import org.apache.hadoop.classification.InterfaceStability; | ||
| import org.apache.hadoop.conf.Configuration; | ||
| import org.apache.hadoop.conf.Configured; | ||
| import org.apache.hadoop.fs.CommonConfigurationKeys; | ||
| import org.apache.hadoop.util.Shell; | ||
| import org.apache.hadoop.util.Shell.ExitCodeException; | ||
| import org.apache.hadoop.util.Shell.ShellCommandExecutor; | ||
| import org.slf4j.Logger; | ||
| import org.slf4j.LoggerFactory; | ||
|
|
||
| /** | ||
| * A simple shell-based implementation of {@link GroupMappingServiceProvider} | ||
|
|
@@ -37,11 +44,28 @@ | |
| */ | ||
| @InterfaceAudience.LimitedPrivate({"HDFS", "MapReduce"}) | ||
| @InterfaceStability.Evolving | ||
| public class ShellBasedUnixGroupsMapping | ||
| public class ShellBasedUnixGroupsMapping extends Configured | ||
| implements GroupMappingServiceProvider { | ||
|
|
||
| private static final Log LOG = | ||
| LogFactory.getLog(ShellBasedUnixGroupsMapping.class); | ||
|
|
||
| @VisibleForTesting | ||
| protected static final Logger LOG = | ||
| LoggerFactory.getLogger(ShellBasedUnixGroupsMapping.class); | ||
|
|
||
| private long timeout = 0L; | ||
| private static final List<String> EMPTY_GROUPS = new LinkedList<>(); | ||
|
|
||
| @Override | ||
| public void setConf(Configuration conf) { | ||
| super.setConf(conf); | ||
| if (conf != null) { | ||
| timeout = conf.getTimeDuration( | ||
| CommonConfigurationKeys. | ||
| HADOOP_SECURITY_GROUP_SHELL_COMMAND_TIMEOUT_SECS, | ||
| CommonConfigurationKeys. | ||
| HADOOP_SECURITY_GROUP_SHELL_COMMAND_TIMEOUT_SECS_DEFAULT, | ||
| TimeUnit.SECONDS); | ||
| } | ||
| } | ||
|
|
||
| @SuppressWarnings("serial") | ||
| private static class PartialGroupNameException extends IOException { | ||
|
|
@@ -98,7 +122,17 @@ public void cacheGroupsAdd(List<String> groups) throws IOException { | |
| */ | ||
| protected ShellCommandExecutor createGroupExecutor(String userName) { | ||
| return new ShellCommandExecutor( | ||
| Shell.getGroupsForUserCommand(userName), null, null, 0L); | ||
| getGroupsForUserCommand(userName), null, null, timeout); | ||
| } | ||
|
|
||
| /** | ||
| * Returns just the shell command to be used to fetch a user's groups list. | ||
| * This is mainly separate to make some tests easier. | ||
| * @param userName The username that needs to be passed into the command built | ||
| * @return An appropriate shell command with arguments | ||
| */ | ||
| protected String[] getGroupsForUserCommand(String userName) { | ||
| return Shell.getGroupsForUserCommand(userName); | ||
| } | ||
|
|
||
| /** | ||
|
|
@@ -109,7 +143,17 @@ protected ShellCommandExecutor createGroupExecutor(String userName) { | |
| */ | ||
| protected ShellCommandExecutor createGroupIDExecutor(String userName) { | ||
| return new ShellCommandExecutor( | ||
| Shell.getGroupsIDForUserCommand(userName), null, null, 0L); | ||
| getGroupsIDForUserCommand(userName), null, null, timeout); | ||
| } | ||
|
|
||
| /** | ||
| * Returns just the shell command to be used to fetch a user's group IDs list. | ||
| * This is mainly separate to make some tests easier. | ||
| * @param userName The username that needs to be passed into the command built | ||
| * @return An appropriate shell command with arguments | ||
| */ | ||
| protected String[] getGroupsIDForUserCommand(String userName) { | ||
| return Shell.getGroupsIDForUserCommand(userName); | ||
| } | ||
|
|
||
| /** | ||
|
|
@@ -133,8 +177,26 @@ private List<String> getUnixGroups(String user) throws IOException { | |
| groups = resolvePartialGroupNames(user, e.getMessage(), | ||
| executor.getOutput()); | ||
| } catch (PartialGroupNameException pge) { | ||
| LOG.warn("unable to return groups for user " + user, pge); | ||
| return new LinkedList<>(); | ||
| LOG.warn("unable to return groups for user {}", user, pge); | ||
| return EMPTY_GROUPS; | ||
| } | ||
| } catch (IOException ioe) { | ||
| // If its a shell executor timeout, indicate so in the message | ||
| // but treat the result as empty instead of throwing it up, | ||
| // similar to how partial resolution failures are handled above | ||
| if (executor.isTimedOut()) { | ||
| LOG.warn( | ||
| "Unable to return groups for user '{}' as shell group lookup " + | ||
| "command '{}' ran longer than the configured timeout limit of " + | ||
| "{} seconds.", | ||
| user, | ||
| Arrays.asList(executor.getExecString()), | ||
| timeout | ||
| ); | ||
| return EMPTY_GROUPS; | ||
| } else { | ||
| // If its not an executor timeout, we should let the caller handle it | ||
| throw ioe; | ||
| } | ||
| } | ||
|
|
||
|
|
@@ -212,11 +274,11 @@ private List<String> resolvePartialGroupNames(String userName, | |
| throw new PartialGroupNameException("The user name '" + userName | ||
| + "' is not found. " + errMessage); | ||
| } else { | ||
| LOG.warn("Some group names for '" + userName + "' are not resolvable. " | ||
| + errMessage); | ||
| LOG.warn("Some group names for '{}' are not resolvable. {}", | ||
| userName, errMessage); | ||
| // attempt to partially resolve group names | ||
| ShellCommandExecutor exec2 = createGroupIDExecutor(userName); | ||
| try { | ||
| ShellCommandExecutor exec2 = createGroupIDExecutor(userName); | ||
| exec2.execute(); | ||
| return parsePartialGroupNames(groupNames, exec2.getOutput()); | ||
| } catch (ExitCodeException ece) { | ||
|
|
@@ -225,8 +287,16 @@ private List<String> resolvePartialGroupNames(String userName, | |
| throw new PartialGroupNameException("failed to get group id list for " + | ||
| "user '" + userName + "'", ece); | ||
| } catch (IOException ioe) { | ||
| throw new PartialGroupNameException("can't execute the shell command to" | ||
| + " get the list of group id for user '" + userName + "'", ioe); | ||
| String message = | ||
| "Can't execute the shell command to " + | ||
| "get the list of group id for user '" + userName + "'"; | ||
| if (exec2.isTimedOut()) { | ||
| message += | ||
| " because of the command taking longer than " + | ||
| "the configured timeout: " + timeout + " seconds"; | ||
| throw new PartialGroupNameException(message); | ||
|
||
| } | ||
| throw new PartialGroupNameException(message, ioe); | ||
| } | ||
| } | ||
| } | ||
|
|
@@ -237,7 +307,8 @@ private List<String> resolvePartialGroupNames(String userName, | |
| * @param groupNames a string representing the user's group names | ||
| * @return a linked list of group names | ||
| */ | ||
| private List<String> resolveFullGroupNames(String groupNames) { | ||
| @VisibleForTesting | ||
| protected List<String> resolveFullGroupNames(String groupNames) { | ||
| StringTokenizer tokenizer = | ||
| new StringTokenizer(groupNames, Shell.TOKEN_SEPARATOR_REGEX); | ||
| List<String> groups = new LinkedList<String>(); | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Reviewed the patch again, and I think it's almost ready.
Here's one nit regarding the warning message here: it would print something like "Unable to return groups for user 'foobarnonexistinguser' as shell group lookup command '[sleep, 2]'". But this can be confusing as the command is not run with brackets and comas.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am +1 pending this and Jenkins precommit build. Somehow Jenkins is never run for your patches.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thank you again @jojochuang. I've added a new change here addressing your comments. I've also uploaded the full patch directly to JIRA to trigger a build test.