Repository: incubator-slider Updated Branches: refs/heads/develop 80e8df0c6 -> 2359c6ddb
SLIDER-474 enable keytab-based security for AM Project: http://git-wip-us.apache.org/repos/asf/incubator-slider/repo Commit: http://git-wip-us.apache.org/repos/asf/incubator-slider/commit/2359c6dd Tree: http://git-wip-us.apache.org/repos/asf/incubator-slider/tree/2359c6dd Diff: http://git-wip-us.apache.org/repos/asf/incubator-slider/diff/2359c6dd Branch: refs/heads/develop Commit: 2359c6ddbaab56c84a4c901b09c025593eee08ef Parents: 80e8df0 Author: Jon Maron <jma...@hortonworks.com> Authored: Mon Oct 6 15:50:36 2014 -0400 Committer: Jon Maron <jma...@hortonworks.com> Committed: Mon Oct 6 15:50:36 2014 -0400 ---------------------------------------------------------------------- .../org/apache/slider/common/SliderKeys.java | 4 + .../slider/common/tools/CoreFileSystem.java | 15 ++ .../providers/agent/AgentProviderService.java | 27 +++ .../server/appmaster/SliderAppMaster.java | 161 ++++++++++----- .../security/SecurityConfiguration.java | 201 +++++++++++++++++++ .../security/SecurityConfigurationTest.groovy | 159 +++++++++++++++ 6 files changed, 520 insertions(+), 47 deletions(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/incubator-slider/blob/2359c6dd/slider-core/src/main/java/org/apache/slider/common/SliderKeys.java ---------------------------------------------------------------------- diff --git a/slider-core/src/main/java/org/apache/slider/common/SliderKeys.java b/slider-core/src/main/java/org/apache/slider/common/SliderKeys.java index ddb9ee0..4348fb0 100644 --- a/slider-core/src/main/java/org/apache/slider/common/SliderKeys.java +++ b/slider-core/src/main/java/org/apache/slider/common/SliderKeys.java @@ -86,6 +86,7 @@ public interface SliderKeys extends SliderXmlConfKeys { String HISTORY_DIR_NAME = "history"; String HISTORY_FILENAME_SUFFIX = "json"; String HISTORY_FILENAME_PREFIX = "rolehistory-"; + String KEYTAB_DIR = "keytabs"; /** * Filename pattern is required to save in strict temporal order. @@ -170,6 +171,9 @@ public interface SliderKeys extends SliderXmlConfKeys { String PASSPHRASE = "DEV"; String PASS_LEN = "50"; String KEYSTORE_LOCATION = "ssl.server.keystore.location"; + String AM_LOGIN_KEYTAB_NAME = "slider.am.login.keytab.name"; + String AM_KEYTAB_LOCAL_PATH = "slider.am.keytab.local.path"; + String KEYTAB_PRINCIPAL = "slider.keytab.principal.name"; /** * Python specific http://git-wip-us.apache.org/repos/asf/incubator-slider/blob/2359c6dd/slider-core/src/main/java/org/apache/slider/common/tools/CoreFileSystem.java ---------------------------------------------------------------------- diff --git a/slider-core/src/main/java/org/apache/slider/common/tools/CoreFileSystem.java b/slider-core/src/main/java/org/apache/slider/common/tools/CoreFileSystem.java index 955d991..b6e6ecf 100644 --- a/slider-core/src/main/java/org/apache/slider/common/tools/CoreFileSystem.java +++ b/slider-core/src/main/java/org/apache/slider/common/tools/CoreFileSystem.java @@ -134,6 +134,21 @@ public class CoreFileSystem { } /** + * Build up the path string for keytab install location -no attempt to + * create the directory is made + * + * @return the path for keytab installation location + */ + public Path buildKeytabPath(String keytabName, String applicationName) { + Preconditions.checkNotNull(applicationName); + Path basePath = getBaseApplicationPath(); + Path baseKeytabDir = new Path(basePath, SliderKeys.KEYTAB_DIR); + Path appKeytabDir = new Path(baseKeytabDir, applicationName); + return keytabName == null ? appKeytabDir : + new Path(appKeytabDir, keytabName); + } + + /** * Create the Slider cluster path for a named cluster and all its subdirs * This is a directory; a mkdirs() operation is executed * to ensure that it is there. http://git-wip-us.apache.org/repos/asf/incubator-slider/blob/2359c6dd/slider-core/src/main/java/org/apache/slider/providers/agent/AgentProviderService.java ---------------------------------------------------------------------- diff --git a/slider-core/src/main/java/org/apache/slider/providers/agent/AgentProviderService.java b/slider-core/src/main/java/org/apache/slider/providers/agent/AgentProviderService.java index 67a268e..88c8709 100644 --- a/slider-core/src/main/java/org/apache/slider/providers/agent/AgentProviderService.java +++ b/slider-core/src/main/java/org/apache/slider/providers/agent/AgentProviderService.java @@ -21,6 +21,7 @@ package org.apache.slider.providers.agent; import com.google.common.annotations.VisibleForTesting; import org.apache.curator.utils.ZKPaths; import org.apache.hadoop.conf.Configuration; +import org.apache.hadoop.fs.FileStatus; import org.apache.hadoop.fs.Path; import org.apache.hadoop.util.StringUtils; import org.apache.hadoop.yarn.api.ApplicationConstants; @@ -325,6 +326,32 @@ public class AgentProviderService extends AbstractProviderService implements launcher.addLocalResource(AgentKeys.AGENT_VERSION_FILE, agentVerRes); } + if (SliderUtils.isHadoopClusterSecure(getConfig())) { + String keytabFullPath = instanceDefinition.getAppConfOperations() + .getComponent(SliderKeys.COMPONENT_AM).get( + SliderKeys.AM_KEYTAB_LOCAL_PATH); + String amKeytabName = instanceDefinition.getAppConfOperations() + .getComponent(SliderKeys.COMPONENT_AM).get( + SliderKeys.AM_LOGIN_KEYTAB_NAME); + if (SliderUtils.isUnset(keytabFullPath)) { + // we need to localize the keytab files in the directory + Path keytabDir = fileSystem.buildKeytabPath(null, + getAmState().getApplicationName()); + FileStatus[] keytabs = fileSystem.getFileSystem().listStatus(keytabDir); + LocalResource keytabRes; + for (FileStatus keytab : keytabs) { + if (!amKeytabName.equals(keytab.getPath().getName())) { + log.info("Localizing keytab {}", keytab.getPath().getName()); + keytabRes = fileSystem.createAmResource(keytab.getPath(), + LocalResourceType.FILE); + launcher.addLocalResource(SliderKeys.KEYTAB_DIR + "/" + + keytab.getPath().getName(), + keytabRes); + } + } + } + } + //add the configuration resources launcher.addLocalResources(fileSystem.submitDirectory( generatedConfPath, http://git-wip-us.apache.org/repos/asf/incubator-slider/blob/2359c6dd/slider-core/src/main/java/org/apache/slider/server/appmaster/SliderAppMaster.java ---------------------------------------------------------------------- diff --git a/slider-core/src/main/java/org/apache/slider/server/appmaster/SliderAppMaster.java b/slider-core/src/main/java/org/apache/slider/server/appmaster/SliderAppMaster.java index 5676f3f..50fc265 100644 --- a/slider-core/src/main/java/org/apache/slider/server/appmaster/SliderAppMaster.java +++ b/slider-core/src/main/java/org/apache/slider/server/appmaster/SliderAppMaster.java @@ -26,6 +26,7 @@ import org.apache.hadoop.conf.Configuration; import org.apache.hadoop.fs.CommonConfigurationKeysPublic; import org.apache.hadoop.fs.Path; import org.apache.hadoop.hdfs.DFSConfigKeys; +import org.apache.hadoop.hdfs.security.token.delegation.DelegationTokenIdentifier; import org.apache.hadoop.io.DataOutputBuffer; import org.apache.hadoop.ipc.ProtocolSignature; import org.apache.hadoop.security.Credentials; @@ -121,6 +122,7 @@ import org.apache.slider.server.appmaster.rpc.RpcBinder; import org.apache.slider.server.appmaster.rpc.SliderAMPolicyProvider; import org.apache.slider.server.appmaster.rpc.SliderClusterProtocolPBImpl; import org.apache.slider.server.appmaster.operations.AbstractRMOperation; +import org.apache.slider.server.appmaster.security.SecurityConfiguration; import org.apache.slider.server.appmaster.state.AppState; import org.apache.slider.server.appmaster.state.ContainerAssignment; import org.apache.slider.server.appmaster.state.ProviderAppState; @@ -226,7 +228,7 @@ public class SliderAppMaster extends AbstractSliderLaunchedService /** * token blob */ - private ByteBuffer allTokens; + private Credentials containerTokens; private WorkflowRpcService rpcService; @@ -430,19 +432,6 @@ public class SliderAppMaster extends AbstractSliderLaunchedService super.serviceStart(); } - @Override - protected void serviceStop() throws Exception { - super.serviceStop(); - - if (fsDelegationTokenManager != null) { - try { - fsDelegationTokenManager.cancelDelegationToken(getConfig()); - } catch (Exception e) { - log.info("Error cancelling HDFS delegation token", e); - } - } - } - /** * Start the queue processing */ @@ -559,6 +548,9 @@ public class SliderAppMaster extends AbstractSliderLaunchedService // triggers resolution and snapshotting in agent appState.updateInstanceDefinition(instanceDefinition); + + Configuration serviceConf = getConfig(); + File confDir = getLocalConfDir(); if (!confDir.exists() || !confDir.isDirectory()) { log.info("Conf dir {} does not exist.", confDir); @@ -566,8 +558,7 @@ public class SliderAppMaster extends AbstractSliderLaunchedService log.info("Parent dir {}:\n{}", parentFile, SliderUtils.listDir(parentFile)); } - Configuration serviceConf = getConfig(); - // IP filtering + // IP filtering serviceConf.set(HADOOP_HTTP_FILTER_INITIALIZERS, AM_FILTER_NAME); //get our provider @@ -706,16 +697,49 @@ public class SliderAppMaster extends AbstractSliderLaunchedService // set the RM-defined maximum cluster values appInformation.put(ResourceKeys.YARN_CORES, Integer.toString(containerMaxCores)); appInformation.put(ResourceKeys.YARN_MEMORY, Integer.toString(containerMaxMemory)); - - boolean securityEnabled = UserGroupInformation.isSecurityEnabled(); + + // process the initial user to obtain the set of user + // supplied credentials (tokens were passed in by client). Remove AMRM + // token and HDFS delegation token, the latter because we will provide an + // up to date token for container launches (getContainerTokens()). + UserGroupInformation currentUser = UserGroupInformation.getCurrentUser(); + Credentials credentials = currentUser.getCredentials(); + Iterator<Token<?>> iter = credentials.getAllTokens().iterator(); + while (iter.hasNext()) { + Token<?> token = iter.next(); + log.info("Token {}", token.getKind()); + if (token.getKind().equals(AMRMTokenIdentifier.KIND_NAME) || + token.getKind().equals(DelegationTokenIdentifier.HDFS_DELEGATION_KIND)) { + iter.remove(); + } + } + // at this point this credentials map is probably clear, but leaving this + // code to allow for future tokens... + containerTokens = credentials; + + SecurityConfiguration securityConfiguration = new SecurityConfiguration( + serviceConf, instanceDefinition, clustername); + // obtain security state + boolean securityEnabled = securityConfiguration.isSecurityEnabled(); if (securityEnabled) { secretManager.setMasterKey( amRegistrationData.getClientToAMTokenMasterKey().array()); applicationACLs = amRegistrationData.getApplicationACLs(); - //tell the server what the ACLs are + //tell the server what the ACLs are rpcService.getServer().refreshServiceAcl(serviceConf, new SliderAMPolicyProvider()); + // perform keytab based login to establish kerberos authenticated + // principal. Can do so now since AM registration with RM above required + // tokens associated to principal + String principal = securityConfiguration.getPrincipal(); + File localKeytabFile = securityConfiguration.getKeytabFile( + fs, instanceDefinition, principal); + // Now log in... + login(principal, localKeytabFile); + // obtain new FS reference that should be kerberos based and different + // than the previously cached reference + fs = getClusterFS(); } // extract container list @@ -795,27 +819,10 @@ public class SliderAppMaster extends AbstractSliderLaunchedService maybeStartMonkey(); // setup token renewal and expiry handling for long lived apps - if (SliderUtils.isHadoopClusterSecure(getConfig())) { - fsDelegationTokenManager = new FsDelegationTokenManager(actionQueues); - fsDelegationTokenManager.acquireDelegationToken(getConfig()); - } - - UserGroupInformation currentUser = UserGroupInformation.getCurrentUser(); - Credentials credentials = - currentUser.getCredentials(); - DataOutputBuffer dob = new DataOutputBuffer(); - credentials.writeTokenStorageToStream(dob); - dob.close(); - // Now remove the AM->RM token so that containers cannot access it. - Iterator<Token<?>> iter = credentials.getAllTokens().iterator(); - while (iter.hasNext()) { - Token<?> token = iter.next(); - log.info("Token {}", token.getKind()); - if (token.getKind().equals(AMRMTokenIdentifier.KIND_NAME)) { - iter.remove(); - } - } - allTokens = ByteBuffer.wrap(dob.getData(), 0, dob.getLength()); +// if (SliderUtils.isHadoopClusterSecure(getConfig())) { +// fsDelegationTokenManager = new FsDelegationTokenManager(actionQueues); +// fsDelegationTokenManager.acquireDelegationToken(getConfig()); +// } // if not a secure cluster, extract the username -it will be // propagated to workers @@ -861,6 +868,40 @@ public class SliderAppMaster extends AbstractSliderLaunchedService return finish(); } + protected void login(String principal, File localKeytabFile) + throws IOException, SliderException { + UserGroupInformation.loginUserFromKeytab(principal, + localKeytabFile.getAbsolutePath()); + validateLoginUser(UserGroupInformation.getLoginUser()); + } + + /** + * Ensure that the user is generated from a keytab and has no HDFS delegation + * tokens. + * + * @param user + * @throws SliderException + */ + protected void validateLoginUser(UserGroupInformation user) + throws SliderException { + if (!user.isFromKeytab()) { + throw new SliderException(SliderExitCodes.EXIT_BAD_STATE, "User is " + + "not based on a keytab in a secure deployment."); + } + Credentials credentials = + user.getCredentials(); + Iterator<Token<?>> iter = credentials.getAllTokens().iterator(); + while (iter.hasNext()) { + Token<?> token = iter.next(); + log.info("Token {}", token.getKind()); + if (token.getKind().equals( + DelegationTokenIdentifier.HDFS_DELEGATION_KIND)) { + log.info("Unexpected HDFS delegation token. Removing..."); + iter.remove(); + } + } + } + private void startAgentWebApp(MapOperations appInformation, Configuration serviceConf) throws IOException { URL[] urls = ((URLClassLoader) AgentWebApp.class.getClassLoader() ).getURLs(); @@ -1387,9 +1428,9 @@ public class SliderAppMaster extends AbstractSliderLaunchedService public void onShutdownRequest() { LOG_YARN.info("Shutdown Request received"); signalAMComplete(new ActionStopSlider("stop", - EXIT_SUCCESS, - FinalApplicationStatus.SUCCEEDED, - "Shutdown requested from RM")); + EXIT_SUCCESS, + FinalApplicationStatus.SUCCEEDED, + "Shutdown requested from RM")); } /** @@ -1558,7 +1599,7 @@ public class SliderAppMaster extends AbstractSliderLaunchedService YarnException { onRpcCall("getNode()"); RoleInstance instance = appState.getLiveInstanceByContainerID( - request.getUuid()); + request.getUuid()); return Messages.GetNodeResponseProto.newBuilder() .setClusterNode(instance.toProtobuf()) .build(); @@ -1571,7 +1612,7 @@ public class SliderAppMaster extends AbstractSliderLaunchedService onRpcCall("getClusterNodes()"); List<RoleInstance> clusterNodes = appState.getLiveInstancesByContainerIDs( - request.getUuidList()); + request.getUuidList()); Messages.GetClusterNodesResponseProto.Builder builder = Messages.GetClusterNodesResponseProto.newBuilder(); @@ -1783,18 +1824,44 @@ public class SliderAppMaster extends AbstractSliderLaunchedService */ public void startContainer(Container container, ContainerLaunchContext ctx, - RoleInstance instance) { + RoleInstance instance) throws IOException { // Set up tokens for the container too. Today, for normal shell commands, // the container in distribute-shell doesn't need any tokens. We are // populating them mainly for NodeManagers to be able to download any // files in the distributed file-system. The tokens are otherwise also // useful in cases, for e.g., when one is running a "hadoop dfs" command // inside the distributed shell. - ctx.setTokens(allTokens.duplicate()); + + // add current HDFS delegation token with an up to date token + ByteBuffer tokens = getContainerTokens(); + + if (tokens != null) { + ctx.setTokens(tokens); + } else { + log.warn("No delegation tokens obtained and set for launch context"); + } appState.containerStartSubmitted(container, instance); nmClientAsync.startContainerAsync(container, ctx); } + private ByteBuffer getContainerTokens() throws IOException { + // a delegation token can be retrieved from filesystem since + // the login is via a keytab (see above) + ByteBuffer tokens = null; + Token hdfsToken = getClusterFS().getFileSystem().getDelegationToken + (UserGroupInformation.getLoginUser().getShortUserName()); + if (hdfsToken != null) { + Credentials credentials = new Credentials(containerTokens); + credentials.addToken(hdfsToken.getKind(), hdfsToken); + DataOutputBuffer dob = new DataOutputBuffer(); + credentials.writeTokenStorageToStream(dob); + dob.close(); + tokens = ByteBuffer.wrap(dob.getData(), 0, dob.getLength()); + } + + return tokens; + } + @Override // NMClientAsync.CallbackHandler public void onContainerStopped(ContainerId containerId) { // do nothing but log: container events from the AM http://git-wip-us.apache.org/repos/asf/incubator-slider/blob/2359c6dd/slider-core/src/main/java/org/apache/slider/server/appmaster/security/SecurityConfiguration.java ---------------------------------------------------------------------- diff --git a/slider-core/src/main/java/org/apache/slider/server/appmaster/security/SecurityConfiguration.java b/slider-core/src/main/java/org/apache/slider/server/appmaster/security/SecurityConfiguration.java new file mode 100644 index 0000000..448d02f --- /dev/null +++ b/slider-core/src/main/java/org/apache/slider/server/appmaster/security/SecurityConfiguration.java @@ -0,0 +1,201 @@ +/* + * 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.slider.server.appmaster.security; + +import com.google.common.base.Preconditions; +import org.apache.commons.io.FileUtils; +import org.apache.hadoop.conf.Configuration; +import org.apache.hadoop.fs.FileUtil; +import org.apache.hadoop.fs.Path; +import org.apache.hadoop.fs.RawLocalFileSystem; +import org.apache.hadoop.fs.permission.FsAction; +import org.apache.hadoop.fs.permission.FsPermission; +import org.apache.hadoop.security.UserGroupInformation; +import org.apache.slider.common.SliderExitCodes; +import org.apache.slider.common.SliderKeys; +import org.apache.slider.common.tools.SliderFileSystem; +import org.apache.slider.common.tools.SliderUtils; +import org.apache.slider.core.conf.AggregateConf; +import org.apache.slider.core.exceptions.SliderException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.File; +import java.io.IOException; + +/** + * + */ +public class SecurityConfiguration { + + protected static final Logger log = + LoggerFactory.getLogger(SecurityConfiguration.class); + private final Configuration configuration; + private final AggregateConf instanceDefinition; + private String clusterName; + + public SecurityConfiguration(Configuration configuration, + AggregateConf instanceDefinition, + String clusterName) throws SliderException { + Preconditions.checkNotNull(configuration); + Preconditions.checkNotNull(instanceDefinition); + Preconditions.checkNotNull(clusterName); + this.configuration = configuration; + this.instanceDefinition = instanceDefinition; + this.clusterName = clusterName; + validate(); + } + + private void validate() throws SliderException { + if (isSecurityEnabled()) { + String principal = instanceDefinition.getAppConfOperations() + .getComponent(SliderKeys.COMPONENT_AM).get(SliderKeys.KEYTAB_PRINCIPAL); + if(SliderUtils.isUnset(principal)) { + // if no login identity is available, fail + UserGroupInformation loginUser = null; + try { + loginUser = getLoginUser(); + } catch (IOException e) { + throw new SliderException(SliderExitCodes.EXIT_BAD_STATE, e, + "No principal configured for the application and" + + "exception raised during retrieval of login user. " + + "Unable to proceed with application " + + "initialization. Please ensure a value " + + "for %s exists in the application " + + "configuration or the login issue is addressed", + SliderKeys.KEYTAB_PRINCIPAL); + } + if (loginUser == null) { + throw new SliderException(SliderExitCodes.EXIT_BAD_CONFIGURATION, + "No principal configured for the application " + + "and no login user found. " + + "Unable to proceed with application " + + "initialization. Please ensure a value " + + "for %s exists in the application " + + "configuration or the login issue is addressed", + SliderKeys.KEYTAB_PRINCIPAL); + } + } + // ensure that either local or distributed keytab mechanism is enabled, + // but not both + String keytabFullPath = instanceDefinition.getAppConfOperations() + .getComponent(SliderKeys.COMPONENT_AM) + .get(SliderKeys.AM_KEYTAB_LOCAL_PATH); + String keytabName = instanceDefinition.getAppConfOperations() + .getComponent(SliderKeys.COMPONENT_AM) + .get(SliderKeys.AM_LOGIN_KEYTAB_NAME); + if (SliderUtils.isUnset(keytabFullPath) && SliderUtils.isUnset(keytabName)) { + throw new SliderException(SliderExitCodes.EXIT_BAD_CONFIGURATION, + "Either a keytab path on the cluster host (%s) or a" + + " keytab to be retrieved from HDFS (%s) are" + + " required. Please configure one of the keytab" + + " retrieval mechanisms.", + SliderKeys.AM_KEYTAB_LOCAL_PATH, + SliderKeys.AM_LOGIN_KEYTAB_NAME); + } + if (SliderUtils.isSet(keytabFullPath) && SliderUtils.isSet(keytabName)) { + throw new SliderException(SliderExitCodes.EXIT_BAD_CONFIGURATION, + "Both a keytab on the cluster host (%s) and a" + + " keytab to be retrieved from HDFS (%s) are" + + " specified. Please configure only one keytab" + + " retrieval mechanism.", + SliderKeys.AM_KEYTAB_LOCAL_PATH, + SliderKeys.AM_LOGIN_KEYTAB_NAME); + + } + } + } + + protected UserGroupInformation getLoginUser() throws IOException { + return UserGroupInformation.getLoginUser(); + } + + public boolean isSecurityEnabled () { + return SliderUtils.isHadoopClusterSecure(configuration); + } + + public String getPrincipal () throws IOException { + String principal = instanceDefinition.getAppConfOperations() + .getComponent(SliderKeys.COMPONENT_AM).get(SliderKeys.KEYTAB_PRINCIPAL); + if (SliderUtils.isUnset(principal)) { + principal = UserGroupInformation.getLoginUser().getShortUserName(); + log.info("No principal set in the slider configuration. Will use AM login" + + " identity {} to attempt keytab-based login", principal); + } + + return principal; + } + + public File getKeytabFile(SliderFileSystem fs, + AggregateConf instanceDefinition, String principal) + throws SliderException, IOException { + String keytabFullPath = instanceDefinition.getAppConfOperations() + .getComponent(SliderKeys.COMPONENT_AM) + .get(SliderKeys.AM_KEYTAB_LOCAL_PATH); + File localKeytabFile; + if (SliderUtils.isUnset(keytabFullPath)) { + // get the keytab + String keytabName = instanceDefinition.getAppConfOperations() + .getComponent(SliderKeys.COMPONENT_AM).get(SliderKeys.AM_LOGIN_KEYTAB_NAME); + log.info("No host keytab file path specified. Downloading keytab {}" + + " from HDFS to perform login of using principal {}", + keytabName, principal); + // download keytab to local, protected directory + localKeytabFile = getFileFromFileSystem(fs, keytabName); + } else { + log.info("Leveraging host keytab file {} to login principal {}", + keytabFullPath, principal); + localKeytabFile = new File(keytabFullPath); + } + return localKeytabFile; + } + + /** + * Download the keytab file from FileSystem to local file. + * @param fs + * @param keytabName + * @return + * @throws SliderException + * @throws IOException + */ + protected File getFileFromFileSystem(SliderFileSystem fs, String keytabName) + throws SliderException, IOException { + File keytabDestinationDir = new File( + FileUtils.getTempDirectory().getAbsolutePath() + + "/keytab" + System.currentTimeMillis()); + if (!keytabDestinationDir.mkdirs()) { + throw new SliderException("Unable to create local keytab directory"); + } + RawLocalFileSystem fileSystem = new RawLocalFileSystem(); + // allow app user to access local keytab dir + FsPermission permissions = new FsPermission(FsAction.ALL, FsAction.NONE, + FsAction.NONE); + fileSystem.setPermission(new Path(keytabDestinationDir.getAbsolutePath()), + permissions); + + Path keytabPath = fs.buildKeytabPath(keytabName, clusterName); + File localKeytabFile = new File(keytabDestinationDir, keytabName); + FileUtil.copy(fs.getFileSystem(), keytabPath, + localKeytabFile, + false, configuration); + // set permissions on actual keytab file to be read-only for user + permissions = new FsPermission(FsAction.READ, FsAction.NONE, FsAction.NONE); + fileSystem.setPermission(new Path(localKeytabFile.getAbsolutePath()), + permissions); + return localKeytabFile; + } +} http://git-wip-us.apache.org/repos/asf/incubator-slider/blob/2359c6dd/slider-core/src/test/groovy/org/apache/slider/server/appmaster/security/SecurityConfigurationTest.groovy ---------------------------------------------------------------------- diff --git a/slider-core/src/test/groovy/org/apache/slider/server/appmaster/security/SecurityConfigurationTest.groovy b/slider-core/src/test/groovy/org/apache/slider/server/appmaster/security/SecurityConfigurationTest.groovy new file mode 100644 index 0000000..1dcbd9c --- /dev/null +++ b/slider-core/src/test/groovy/org/apache/slider/server/appmaster/security/SecurityConfigurationTest.groovy @@ -0,0 +1,159 @@ +/* + * 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.slider.server.appmaster.security + +import org.apache.hadoop.conf.Configuration +import org.apache.hadoop.fs.CommonConfigurationKeysPublic +import org.apache.hadoop.security.UserGroupInformation +import org.apache.slider.common.SliderKeys +import org.apache.slider.core.conf.AggregateConf +import org.apache.slider.core.conf.MapOperations +import org.apache.slider.core.exceptions.SliderException; +import org.junit.Test; + +/** + * + */ +public class SecurityConfigurationTest { + final shouldFail = new GroovyTestCase().&shouldFail + + @Test + public void testValidLocalConfiguration() throws Throwable { + Configuration config = new Configuration() + config.set(CommonConfigurationKeysPublic.HADOOP_SECURITY_AUTHENTICATION, "kerberos") + AggregateConf aggregateConf = new AggregateConf(); + MapOperations compOps = + aggregateConf.appConfOperations.getOrAddComponent(SliderKeys.COMPONENT_AM) + compOps.put(SliderKeys.KEYTAB_PRINCIPAL, "test") + compOps.put(SliderKeys.AM_KEYTAB_LOCAL_PATH, "/some/local/path") + + SecurityConfiguration securityConfiguration = + new SecurityConfiguration(config, aggregateConf, "testCluster") + } + + @Test + public void testValidDistributedConfiguration() throws Throwable { + Configuration config = new Configuration() + config.set(CommonConfigurationKeysPublic.HADOOP_SECURITY_AUTHENTICATION, "kerberos") + AggregateConf aggregateConf = new AggregateConf(); + MapOperations compOps = + aggregateConf.appConfOperations.getOrAddComponent(SliderKeys.COMPONENT_AM) + compOps.put(SliderKeys.KEYTAB_PRINCIPAL, "test") + compOps.put(SliderKeys.AM_LOGIN_KEYTAB_NAME, "some.keytab") + + SecurityConfiguration securityConfiguration = + new SecurityConfiguration(config, aggregateConf, "testCluster") + } + + @Test + public void testMissingPrincipalNoLoginWithDistributedConfig() throws Throwable { + Configuration config = new Configuration() + config.set(CommonConfigurationKeysPublic.HADOOP_SECURITY_AUTHENTICATION, "kerberos") + AggregateConf aggregateConf = new AggregateConf(); + MapOperations compOps = + aggregateConf.appConfOperations.getOrAddComponent(SliderKeys.COMPONENT_AM) + compOps.put(SliderKeys.AM_LOGIN_KEYTAB_NAME, "some.keytab") + + shouldFail(SliderException) { + SecurityConfiguration securityConfiguration = + new SecurityConfiguration(config, aggregateConf, "testCluster") { + @Override + protected UserGroupInformation getLoginUser() throws IOException { + return null + } + } + } + } + + @Test + public void testMissingPrincipalNoLoginWithLocalConfig() throws Throwable { + Configuration config = new Configuration() + config.set(CommonConfigurationKeysPublic.HADOOP_SECURITY_AUTHENTICATION, "kerberos") + AggregateConf aggregateConf = new AggregateConf(); + MapOperations compOps = + aggregateConf.appConfOperations.getOrAddComponent(SliderKeys.COMPONENT_AM) + compOps.put(SliderKeys.AM_KEYTAB_LOCAL_PATH, "/some/local/path") + + shouldFail(SliderException) { + SecurityConfiguration securityConfiguration = + new SecurityConfiguration(config, aggregateConf, "testCluster") { + @Override + protected UserGroupInformation getLoginUser() throws IOException { + return null + } + } + } + } + + @Test + public void testBothKeytabMechanismsConfigured() throws Throwable { + Configuration config = new Configuration() + config.set(CommonConfigurationKeysPublic.HADOOP_SECURITY_AUTHENTICATION, "kerberos") + AggregateConf aggregateConf = new AggregateConf(); + MapOperations compOps = + aggregateConf.appConfOperations.getOrAddComponent(SliderKeys.COMPONENT_AM) + compOps.put(SliderKeys.KEYTAB_PRINCIPAL, "test") + compOps.put(SliderKeys.AM_KEYTAB_LOCAL_PATH, "/some/local/path") + compOps.put(SliderKeys.AM_LOGIN_KEYTAB_NAME, "some.keytab") + + shouldFail(SliderException) { + SecurityConfiguration securityConfiguration = + new SecurityConfiguration(config, aggregateConf, "testCluster") + } + } + + @Test + public void testNoKeytabMechanismConfigured() throws Throwable { + Configuration config = new Configuration() + config.set(CommonConfigurationKeysPublic.HADOOP_SECURITY_AUTHENTICATION, "kerberos") + AggregateConf aggregateConf = new AggregateConf(); + MapOperations compOps = + aggregateConf.appConfOperations.getOrAddComponent(SliderKeys.COMPONENT_AM) + compOps.put(SliderKeys.KEYTAB_PRINCIPAL, "test") + + shouldFail(SliderException) { + SecurityConfiguration securityConfiguration = + new SecurityConfiguration(config, aggregateConf, "testCluster") + } + } + + @Test + public void testMissingPrincipalButLoginWithDistributedConfig() throws Throwable { + Configuration config = new Configuration() + config.set(CommonConfigurationKeysPublic.HADOOP_SECURITY_AUTHENTICATION, "kerberos") + AggregateConf aggregateConf = new AggregateConf(); + MapOperations compOps = + aggregateConf.appConfOperations.getOrAddComponent(SliderKeys.COMPONENT_AM) + compOps.put(SliderKeys.AM_LOGIN_KEYTAB_NAME, "some.keytab") + + SecurityConfiguration securityConfiguration = + new SecurityConfiguration(config, aggregateConf, "testCluster") + } + + @Test + public void testMissingPrincipalButLoginWithLocalConfig() throws Throwable { + Configuration config = new Configuration() + config.set(CommonConfigurationKeysPublic.HADOOP_SECURITY_AUTHENTICATION, "kerberos") + AggregateConf aggregateConf = new AggregateConf(); + MapOperations compOps = + aggregateConf.appConfOperations.getOrAddComponent(SliderKeys.COMPONENT_AM) + compOps.put(SliderKeys.AM_KEYTAB_LOCAL_PATH, "/some/local/path") + + SecurityConfiguration securityConfiguration = + new SecurityConfiguration(config, aggregateConf, "testCluster") + } +}