/* * 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 gobblin.util; import java.io.IOException; import java.net.URI; import java.security.PrivilegedExceptionAction; import java.util.Properties; import com.google.common.base.Optional; import com.google.common.base.Preconditions; import com.google.common.io.Closer; import lombok.AllArgsConstructor; import lombok.NonNull; import lombok.extern.slf4j.Slf4j; import org.apache.hadoop.conf.Configuration; import org.apache.hadoop.fs.FileSystem; import org.apache.hadoop.fs.Path; import org.apache.hadoop.io.SequenceFile; import org.apache.hadoop.io.Text; import org.apache.hadoop.security.UserGroupInformation; import org.apache.hadoop.security.token.Token; import gobblin.configuration.ConfigurationKeys; import gobblin.configuration.State; /** * Utility class for creating {@link FileSystem} objects while proxied as another user. This class requires access to a * user with secure impersonation priveleges. The {@link FileSystem} objects returned will have full permissions to * access any operations on behalf of the specified user. * * <p> * As a user, use methods in {@link gobblin.util.ProxiedFileSystemCache} to generate the proxied file systems. * </p> * * @see <a href="http://hadoop.apache.org/docs/r1.2.1/Secure_Impersonation.html">Secure Impersonation</a>, * <a href="https://hadoop.apache.org/docs/r1.2.1/api/org/apache/hadoop/security/UserGroupInformation.html">UserGroupInformation</a> * * TODO figure out the proper generic type for the {@link Token} objects. */ @Slf4j public class ProxiedFileSystemUtils { public static final String AUTH_TYPE_KEY = "gobblin.utility.user.proxy.auth.type"; public static final String AUTH_TOKEN_PATH = "gobblin.utility.proxy.auth.token.path"; // Two authentication types for Hadoop Security, through TOKEN or KEYTAB. public enum AuthType { TOKEN, KEYTAB; } /** * Creates a {@link FileSystem} that can perform any operations allowed by the specified userNameToProxyAs. * * @param userNameToProxyAs The name of the user the super user should proxy as * @param properties {@link java.util.Properties} containing initialization properties. * @param fsURI The {@link URI} for the {@link FileSystem} that should be created. * @param conf The {@link Configuration} for the {@link FileSystem} that should be created. * @return a {@link FileSystem} that can execute commands on behalf of the specified userNameToProxyAs * @throws IOException */ static FileSystem createProxiedFileSystem(@NonNull final String userNameToProxyAs, Properties properties, URI fsURI, Configuration conf) throws IOException { Preconditions.checkArgument(properties.containsKey(AUTH_TYPE_KEY)); switch (AuthType.valueOf(properties.getProperty(AUTH_TYPE_KEY))) { case TOKEN: Preconditions.checkArgument(properties.containsKey(AUTH_TOKEN_PATH)); Path tokenPath = new Path(properties.getProperty(AUTH_TOKEN_PATH)); Optional<Token<?>> proxyToken = getTokenFromSeqFile(userNameToProxyAs, tokenPath); if (proxyToken.isPresent()) { try { return createProxiedFileSystemUsingToken(userNameToProxyAs, proxyToken.get(), fsURI, conf); } catch (InterruptedException e) { throw new IOException("Failed to proxy as user " + userNameToProxyAs, e); } } throw new IOException("No delegation token found for proxy user " + userNameToProxyAs); case KEYTAB: Preconditions.checkArgument(properties.containsKey(ConfigurationKeys.SUPER_USER_NAME_TO_PROXY_AS_OTHERS) && properties.containsKey(ConfigurationKeys.SUPER_USER_KEY_TAB_LOCATION)); String superUserName = properties.getProperty(ConfigurationKeys.SUPER_USER_NAME_TO_PROXY_AS_OTHERS); Path keytabPath = new Path(properties.getProperty(ConfigurationKeys.SUPER_USER_KEY_TAB_LOCATION)); try { return createProxiedFileSystemUsingKeytab(userNameToProxyAs, superUserName, keytabPath, fsURI, conf); } catch (InterruptedException e) { throw new IOException("Failed to proxy as user " + userNameToProxyAs, e); } default: throw new IOException("User proxy auth type " + properties.getProperty(AUTH_TYPE_KEY) + " not recognized."); } } /** * Creates a {@link FileSystem} that can perform any operations allowed by the specified userNameToProxyAs. This * method first logs in as the specified super user. If Hadoop security is enabled, then logging in entails * authenticating via Kerberos. So logging in requires contacting the Kerberos infrastructure. A proxy user is then * created on behalf of the logged in user, and a {@link FileSystem} object is created using the proxy user's UGI. * * @param userNameToProxyAs The name of the user the super user should proxy as * @param superUserName The name of the super user with secure impersonation priveleges * @param superUserKeytabLocation The location of the keytab file for the super user * @param fsURI The {@link URI} for the {@link FileSystem} that should be created * @param conf The {@link Configuration} for the {@link FileSystem} that should be created * * @return a {@link FileSystem} that can execute commands on behalf of the specified userNameToProxyAs */ static FileSystem createProxiedFileSystemUsingKeytab(String userNameToProxyAs, String superUserName, Path superUserKeytabLocation, URI fsURI, Configuration conf) throws IOException, InterruptedException { return loginAndProxyAsUser(userNameToProxyAs, superUserName, superUserKeytabLocation) .doAs(new ProxiedFileSystem(fsURI, conf)); } /** * Create a {@link FileSystem} that can perform any operations allowed the by the specified userNameToProxyAs. This * method uses the {@link #createProxiedFileSystemUsingKeytab(String, String, Path, URI, Configuration)} object to perform * all its work. A specific set of configuration keys are required to be set in the given {@link State} object: * * <ul> * <li>{@link ConfigurationKeys#FS_PROXY_AS_USER_NAME} specifies the user name to proxy as</li> * <li>{@link ConfigurationKeys#SUPER_USER_NAME_TO_PROXY_AS_OTHERS} specifies the name of the user with secure * impersonation priveleges</li> * <li>{@link ConfigurationKeys#SUPER_USER_KEY_TAB_LOCATION} specifies the location of the super user's keytab file</li> * <ul> * * @param state The {@link State} object that contains all the necessary key, value pairs for * {@link #createProxiedFileSystemUsingKeytab(String, String, Path, URI, Configuration)} * @param fsURI The {@link URI} for the {@link FileSystem} that should be created * @param conf The {@link Configuration} for the {@link FileSystem} that should be created * * @return a {@link FileSystem} that can execute commands on behalf of the specified userNameToProxyAs */ static FileSystem createProxiedFileSystemUsingKeytab(State state, URI fsURI, Configuration conf) throws IOException, InterruptedException { Preconditions.checkArgument(state.contains(ConfigurationKeys.FS_PROXY_AS_USER_NAME)); Preconditions.checkArgument(state.contains(ConfigurationKeys.SUPER_USER_NAME_TO_PROXY_AS_OTHERS)); Preconditions.checkArgument(state.contains(ConfigurationKeys.SUPER_USER_KEY_TAB_LOCATION)); return createProxiedFileSystemUsingKeytab(state.getProp(ConfigurationKeys.FS_PROXY_AS_USER_NAME), state.getProp(ConfigurationKeys.SUPER_USER_NAME_TO_PROXY_AS_OTHERS), new Path(state.getProp(ConfigurationKeys.SUPER_USER_KEY_TAB_LOCATION)), fsURI, conf); } /** * Create a {@link FileSystem} that can perform any operations allowed the by the specified userNameToProxyAs. The * method first proxies as userNameToProxyAs, and then adds the specified {@link Token} to the given * {@link UserGroupInformation} object. It then uses the {@link UserGroupInformation#doAs(PrivilegedExceptionAction)} * method to create a {@link FileSystem}. * * @param userNameToProxyAs The name of the user the super user should proxy as * @param userNameToken The {@link Token} to add to the proxied user's {@link UserGroupInformation}. * @param fsURI The {@link URI} for the {@link FileSystem} that should be created * @param conf The {@link Configuration} for the {@link FileSystem} that should be created * * @return a {@link FileSystem} that can execute commands on behalf of the specified userNameToProxyAs */ static FileSystem createProxiedFileSystemUsingToken(@NonNull String userNameToProxyAs, @NonNull Token<?> userNameToken, URI fsURI, Configuration conf) throws IOException, InterruptedException { UserGroupInformation ugi = UserGroupInformation.createProxyUser(userNameToProxyAs, UserGroupInformation.getLoginUser()); ugi.addToken(userNameToken); return ugi.doAs(new ProxiedFileSystem(fsURI, conf)); } /** * Returns true if superUserName can proxy as userNameToProxyAs using the specified superUserKeytabLocation, false * otherwise. */ public static boolean canProxyAs(String userNameToProxyAs, String superUserName, Path superUserKeytabLocation) { try { loginAndProxyAsUser(userNameToProxyAs, superUserName, superUserKeytabLocation); } catch (IOException e) { return false; } return true; } /** * Retrives a {@link Token} from a given sequence file for a specified user. The sequence file should contain a list * of key, value pairs where each key corresponds to a user and each value corresponds to a {@link Token} for that * user. * * @param userNameKey The name of the user to retrieve a {@link Token} for * @param tokenFilePath The path to the sequence file containing the {@link Token}s * * @return A {@link Token} for the given user name */ public static Optional<Token<?>> getTokenFromSeqFile(String userNameKey, Path tokenFilePath) throws IOException { log.info("Reading tokens from sequence file " + tokenFilePath); try (Closer closer = Closer.create()) { FileSystem localFs = FileSystem.getLocal(new Configuration()); @SuppressWarnings("deprecation") SequenceFile.Reader tokenReader = closer.register(new SequenceFile.Reader(localFs, tokenFilePath, localFs.getConf())); Text key = new Text(); Token<?> value = new Token<>(); while (tokenReader.next(key, value)) { log.debug("Found token for user: " + key); if (key.toString().equals(userNameKey)) { return Optional.<Token<?>> of(value); } } } log.warn("Did not find any tokens for user " + userNameKey); return Optional.absent(); } private static UserGroupInformation loginAndProxyAsUser(@NonNull String userNameToProxyAs, @NonNull String superUserName, Path superUserKeytabLocation) throws IOException { if (!UserGroupInformation.getLoginUser().getUserName().equals(superUserName)) { Preconditions.checkNotNull(superUserKeytabLocation); UserGroupInformation.loginUserFromKeytab(superUserName, superUserKeytabLocation.toString()); } return UserGroupInformation.createProxyUser(userNameToProxyAs, UserGroupInformation.getLoginUser()); } @AllArgsConstructor private static class ProxiedFileSystem implements PrivilegedExceptionAction<FileSystem> { @NonNull private URI fsURI; @NonNull private Configuration conf; @Override public FileSystem run() throws IOException { log.info("Creating a filesystem for user: " + UserGroupInformation.getCurrentUser()); return FileSystem.get(this.fsURI, this.conf); } } }