001/*
002 * Licensed to the Apache Software Foundation (ASF) under one
003 * or more contributor license agreements.  See the NOTICE file
004 * distributed with this work for additional information
005 * regarding copyright ownership.  The ASF licenses this file
006 * to you under the Apache License, Version 2.0 (the
007 * "License"); you may not use this file except in compliance
008 * with the License.  You may obtain a copy of the License at
009 *
010 *     http://www.apache.org/licenses/LICENSE-2.0
011 *
012 * Unless required by applicable law or agreed to in writing, software
013 * distributed under the License is distributed on an "AS IS" BASIS,
014 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
015 * See the License for the specific language governing permissions and
016 * limitations under the License.
017 */
018package org.apache.hadoop.hbase.security;
019
020import java.io.IOException;
021import java.security.PrivilegedAction;
022import java.security.PrivilegedExceptionAction;
023import java.util.Arrays;
024import java.util.Collection;
025import java.util.HashMap;
026import java.util.List;
027import java.util.Map;
028import java.util.Optional;
029import java.util.concurrent.ExecutionException;
030import org.apache.hadoop.conf.Configuration;
031import org.apache.hadoop.hbase.AuthUtil;
032import org.apache.hadoop.hbase.util.Methods;
033import org.apache.hadoop.security.Groups;
034import org.apache.hadoop.security.SecurityUtil;
035import org.apache.hadoop.security.UserGroupInformation;
036import org.apache.hadoop.security.token.Token;
037import org.apache.hadoop.security.token.TokenIdentifier;
038import org.apache.yetus.audience.InterfaceAudience;
039
040import org.apache.hbase.thirdparty.com.google.common.cache.LoadingCache;
041
042/**
043 * Wrapper to abstract out usage of user and group information in HBase.
044 * <p>
045 * This class provides a common interface for interacting with user and group information across
046 * changing APIs in different versions of Hadoop. It only provides access to the common set of
047 * functionality in {@link org.apache.hadoop.security.UserGroupInformation} currently needed by
048 * HBase, but can be extended as needs change.
049 * </p>
050 */
051@InterfaceAudience.Public
052public abstract class User {
053  public static final String HBASE_SECURITY_CONF_KEY = "hbase.security.authentication";
054  public static final String HBASE_SECURITY_AUTHORIZATION_CONF_KEY = "hbase.security.authorization";
055
056  protected UserGroupInformation ugi;
057
058  public UserGroupInformation getUGI() {
059    return ugi;
060  }
061
062  /**
063   * Returns the full user name. For Kerberos principals this will include the host and realm
064   * portions of the principal name.
065   * @return User full name.
066   */
067  public String getName() {
068    return ugi.getUserName();
069  }
070
071  /**
072   * Returns the list of groups of which this user is a member. On secure Hadoop this returns the
073   * group information for the user as resolved on the server. For 0.20 based Hadoop, the group
074   * names are passed from the client.
075   */
076  public String[] getGroupNames() {
077    return ugi.getGroupNames();
078  }
079
080  /**
081   * Returns the shortened version of the user name -- the portion that maps to an operating system
082   * user name.
083   * @return Short name
084   */
085  public abstract String getShortName();
086
087  /**
088   * Executes the given action within the context of this user.
089   */
090  public abstract <T> T runAs(PrivilegedAction<T> action);
091
092  /**
093   * Executes the given action within the context of this user.
094   */
095  public abstract <T> T runAs(PrivilegedExceptionAction<T> action)
096    throws IOException, InterruptedException;
097
098  /**
099   * Returns the Token of the specified kind associated with this user, or null if the Token is not
100   * present.
101   * @param kind    the kind of token
102   * @param service service on which the token is supposed to be used
103   * @return the token of the specified kind.
104   */
105  public Token<?> getToken(String kind, String service) throws IOException {
106    for (Token<?> token : ugi.getTokens()) {
107      if (
108        token.getKind().toString().equals(kind)
109          && (service != null && token.getService().toString().equals(service))
110      ) {
111        return token;
112      }
113    }
114    return null;
115  }
116
117  /**
118   * Returns all the tokens stored in the user's credentials.
119   */
120  public Collection<Token<? extends TokenIdentifier>> getTokens() {
121    return ugi.getTokens();
122  }
123
124  /**
125   * Adds the given Token to the user's credentials.
126   * @param token the token to add
127   */
128  public void addToken(Token<? extends TokenIdentifier> token) {
129    ugi.addToken(token);
130  }
131
132  /** Returns true if user credentials are obtained from keytab. */
133  public boolean isLoginFromKeytab() {
134    return ugi.isFromKeytab();
135  }
136
137  @Override
138  public boolean equals(Object o) {
139    if (this == o) {
140      return true;
141    }
142    if (o == null || getClass() != o.getClass()) {
143      return false;
144    }
145    return ugi.equals(((User) o).ugi);
146  }
147
148  @Override
149  public int hashCode() {
150    return ugi.hashCode();
151  }
152
153  @Override
154  public String toString() {
155    return ugi.toString();
156  }
157
158  /**
159   * Returns the {@code User} instance within current execution context.
160   */
161  public static User getCurrent() throws IOException {
162    User user = new SecureHadoopUser();
163    if (user.getUGI() == null) {
164      return null;
165    }
166    return user;
167  }
168
169  /**
170   * Executes the given action as the login user
171   * @return the result of the action
172   */
173  @SuppressWarnings({ "rawtypes", "unchecked" })
174  public static <T> T runAsLoginUser(PrivilegedExceptionAction<T> action) throws IOException {
175    try {
176      Class c = Class.forName("org.apache.hadoop.security.SecurityUtil");
177      Class[] types = new Class[] { PrivilegedExceptionAction.class };
178      Object[] args = new Object[] { action };
179      return (T) Methods.call(c, null, "doAsLoginUser", types, args);
180    } catch (Throwable e) {
181      throw new IOException(e);
182    }
183  }
184
185  /**
186   * Wraps an underlying {@code UserGroupInformation} instance.
187   * @param ugi The base Hadoop user
188   */
189  public static User create(UserGroupInformation ugi) {
190    if (ugi == null) {
191      return null;
192    }
193    return new SecureHadoopUser(ugi);
194  }
195
196  /**
197   * Generates a new {@code User} instance specifically for use in test code.
198   * @param name   the full username
199   * @param groups the group names to which the test user will belong
200   * @return a new <code>User</code> instance
201   */
202  public static User createUserForTesting(Configuration conf, String name, String[] groups) {
203    User userForTesting = SecureHadoopUser.createUserForTesting(conf, name, groups);
204    return userForTesting;
205  }
206
207  /**
208   * Log in the current process using the given configuration keys for the credential file and login
209   * principal.
210   * <p>
211   * <strong>This is only applicable when running on secure Hadoop</strong> -- see
212   * org.apache.hadoop.security.SecurityUtil#login(Configuration,String,String,String). On regular
213   * Hadoop (without security features), this will safely be ignored.
214   * </p>
215   * @param conf             The configuration data to use
216   * @param fileConfKey      Property key used to configure path to the credential file
217   * @param principalConfKey Property key used to configure login principal
218   * @param localhost        Current hostname to use in any credentials
219   * @throws IOException underlying exception from SecurityUtil.login() call
220   */
221  public static void login(Configuration conf, String fileConfKey, String principalConfKey,
222    String localhost) throws IOException {
223    SecureHadoopUser.login(conf, fileConfKey, principalConfKey, localhost);
224  }
225
226  /**
227   * Login with the given keytab and principal.
228   * @param keytabLocation path of keytab
229   * @param pricipalName   login principal
230   * @throws IOException underlying exception from UserGroupInformation.loginUserFromKeytab
231   */
232  public static void login(String keytabLocation, String pricipalName) throws IOException {
233    SecureHadoopUser.login(keytabLocation, pricipalName);
234  }
235
236  /**
237   * Returns whether or not Kerberos authentication is configured for Hadoop. For non-secure Hadoop,
238   * this always returns <code>false</code>. For secure Hadoop, it will return the value from
239   * {@code UserGroupInformation.isSecurityEnabled()}.
240   */
241  public static boolean isSecurityEnabled() {
242    return SecureHadoopUser.isSecurityEnabled();
243  }
244
245  /**
246   * Returns whether or not secure authentication is enabled for HBase. Note that HBase security
247   * requires HDFS security to provide any guarantees, so it is recommended that secure HBase should
248   * run on secure HDFS.
249   */
250  public static boolean isHBaseSecurityEnabled(Configuration conf) {
251    return "kerberos".equalsIgnoreCase(conf.get(HBASE_SECURITY_CONF_KEY));
252  }
253
254  /**
255   * In secure environment, if a user specified his keytab and principal, a hbase client will try to
256   * login with them. Otherwise, hbase client will try to obtain ticket(through kinit) from system.
257   * @param conf configuration file
258   * @return true if keytab and principal are configured
259   */
260  public static boolean shouldLoginFromKeytab(Configuration conf) {
261    Optional<String> keytab = Optional.ofNullable(conf.get(AuthUtil.HBASE_CLIENT_KEYTAB_FILE));
262    Optional<String> principal =
263      Optional.ofNullable(conf.get(AuthUtil.HBASE_CLIENT_KERBEROS_PRINCIPAL));
264    return keytab.isPresent() && principal.isPresent();
265  }
266
267  /* Concrete implementations */
268
269  /**
270   * Bridges {@code User} invocations to underlying calls to
271   * {@link org.apache.hadoop.security.UserGroupInformation} for secure Hadoop 0.20 and versions
272   * 0.21 and above.
273   */
274  @InterfaceAudience.Private
275  public static final class SecureHadoopUser extends User {
276    private String shortName;
277    private LoadingCache<String, String[]> cache;
278
279    public SecureHadoopUser() throws IOException {
280      ugi = UserGroupInformation.getCurrentUser();
281      this.cache = null;
282    }
283
284    public SecureHadoopUser(UserGroupInformation ugi) {
285      this.ugi = ugi;
286      this.cache = null;
287    }
288
289    public SecureHadoopUser(UserGroupInformation ugi, LoadingCache<String, String[]> cache) {
290      this.ugi = ugi;
291      this.cache = cache;
292    }
293
294    @Override
295    public String getShortName() {
296      if (shortName != null) return shortName;
297      try {
298        shortName = ugi.getShortUserName();
299        return shortName;
300      } catch (Exception e) {
301        throw new RuntimeException("Unexpected error getting user short name", e);
302      }
303    }
304
305    @Override
306    public String[] getGroupNames() {
307      if (cache != null) {
308        try {
309          return this.cache.get(getShortName());
310        } catch (ExecutionException e) {
311          return new String[0];
312        }
313      }
314      return ugi.getGroupNames();
315    }
316
317    @Override
318    public <T> T runAs(PrivilegedAction<T> action) {
319      return ugi.doAs(action);
320    }
321
322    @Override
323    public <T> T runAs(PrivilegedExceptionAction<T> action)
324      throws IOException, InterruptedException {
325      return ugi.doAs(action);
326    }
327
328    /** @see User#createUserForTesting(org.apache.hadoop.conf.Configuration, String, String[]) */
329    public static User createUserForTesting(Configuration conf, String name, String[] groups) {
330      synchronized (UserProvider.class) {
331        if (!(UserProvider.groups instanceof TestingGroups)) {
332          UserProvider.groups = new TestingGroups(UserProvider.groups);
333        }
334      }
335
336      ((TestingGroups) UserProvider.groups).setUserGroups(name, groups);
337      return new SecureHadoopUser(UserGroupInformation.createUserForTesting(name, groups));
338    }
339
340    /**
341     * Obtain credentials for the current process using the configured Kerberos keytab file and
342     * principal.
343     * @see User#login(org.apache.hadoop.conf.Configuration, String, String, String)
344     * @param conf             the Configuration to use
345     * @param fileConfKey      Configuration property key used to store the path to the keytab file
346     * @param principalConfKey Configuration property key used to store the principal name to login
347     *                         as
348     * @param localhost        the local hostname
349     */
350    public static void login(Configuration conf, String fileConfKey, String principalConfKey,
351      String localhost) throws IOException {
352      if (isSecurityEnabled()) {
353        SecurityUtil.login(conf, fileConfKey, principalConfKey, localhost);
354      }
355    }
356
357    /**
358     * Login through configured keytab and pricipal.
359     * @param keytabLocation location of keytab
360     * @param principalName  principal in keytab
361     * @throws IOException exception from UserGroupInformation.loginUserFromKeytab
362     */
363    public static void login(String keytabLocation, String principalName) throws IOException {
364      if (isSecurityEnabled()) {
365        UserGroupInformation.loginUserFromKeytab(principalName, keytabLocation);
366      }
367    }
368
369    /**
370     * Returns the result of {@code UserGroupInformation.isSecurityEnabled()}.
371     */
372    public static boolean isSecurityEnabled() {
373      return UserGroupInformation.isSecurityEnabled();
374    }
375  }
376
377  public static class TestingGroups extends Groups {
378    public static final String TEST_CONF = "hbase.group.service.for.test.only";
379
380    private final Map<String, List<String>> userToGroupsMapping = new HashMap<>();
381    private Groups underlyingImplementation;
382
383    public TestingGroups(Groups underlyingImplementation) {
384      super(new Configuration());
385      this.underlyingImplementation = underlyingImplementation;
386    }
387
388    @Override
389    public List<String> getGroups(String user) throws IOException {
390      List<String> result = userToGroupsMapping.get(user);
391
392      if (result == null) {
393        result = underlyingImplementation.getGroups(user);
394      }
395
396      return result;
397    }
398
399    private void setUserGroups(String user, String[] groups) {
400      userToGroupsMapping.put(user, Arrays.asList(groups));
401    }
402  }
403}