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 instanceof User)) {
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  /** Returns the {@code User} instance within current execution context. */
159  public static User getCurrent() throws IOException {
160    User user = new SecureHadoopUser();
161    if (user.getUGI() == null) {
162      return null;
163    }
164    return user;
165  }
166
167  /** Executes the given action as the login user */
168  @SuppressWarnings({ "rawtypes", "unchecked" })
169  public static <T> T runAsLoginUser(PrivilegedExceptionAction<T> action) throws IOException {
170    try {
171      Class c = Class.forName("org.apache.hadoop.security.SecurityUtil");
172      Class[] types = new Class[] { PrivilegedExceptionAction.class };
173      Object[] args = new Object[] { action };
174      return (T) Methods.call(c, null, "doAsLoginUser", types, args);
175    } catch (Throwable e) {
176      throw new IOException(e);
177    }
178  }
179
180  /**
181   * Wraps an underlying {@code UserGroupInformation} instance.
182   * @param ugi The base Hadoop user
183   */
184  public static User create(UserGroupInformation ugi) {
185    if (ugi == null) {
186      return null;
187    }
188    return new SecureHadoopUser(ugi);
189  }
190
191  /**
192   * Generates a new {@code User} instance specifically for use in test code.
193   * @param name   the full username
194   * @param groups the group names to which the test user will belong
195   * @return a new <code>User</code> instance
196   */
197  public static User createUserForTesting(Configuration conf, String name, String[] groups) {
198    User userForTesting = SecureHadoopUser.createUserForTesting(conf, name, groups);
199    return userForTesting;
200  }
201
202  /**
203   * Log in the current process using the given configuration keys for the credential file and login
204   * principal.
205   * <p>
206   * <strong>This is only applicable when running on secure Hadoop</strong> -- see
207   * org.apache.hadoop.security.SecurityUtil#login(Configuration,String,String,String). On regular
208   * Hadoop (without security features), this will safely be ignored.
209   * </p>
210   * @param conf             The configuration data to use
211   * @param fileConfKey      Property key used to configure path to the credential file
212   * @param principalConfKey Property key used to configure login principal
213   * @param localhost        Current hostname to use in any credentials
214   * @throws IOException underlying exception from SecurityUtil.login() call
215   */
216  public static void login(Configuration conf, String fileConfKey, String principalConfKey,
217    String localhost) throws IOException {
218    SecureHadoopUser.login(conf, fileConfKey, principalConfKey, localhost);
219  }
220
221  /**
222   * Login with the given keytab and principal.
223   * @param keytabLocation path of keytab
224   * @param pricipalName   login principal
225   * @throws IOException underlying exception from UserGroupInformation.loginUserFromKeytab
226   */
227  public static void login(String keytabLocation, String pricipalName) throws IOException {
228    SecureHadoopUser.login(keytabLocation, pricipalName);
229  }
230
231  /**
232   * Returns whether or not Kerberos authentication is configured for Hadoop. For non-secure Hadoop,
233   * this always returns <code>false</code>. For secure Hadoop, it will return the value from
234   * {@code UserGroupInformation.isSecurityEnabled()}.
235   */
236  public static boolean isSecurityEnabled() {
237    return SecureHadoopUser.isSecurityEnabled();
238  }
239
240  /**
241   * Returns whether or not secure authentication is enabled for HBase. Note that HBase security
242   * requires HDFS security to provide any guarantees, so it is recommended that secure HBase should
243   * run on secure HDFS.
244   */
245  public static boolean isHBaseSecurityEnabled(Configuration conf) {
246    return "kerberos".equalsIgnoreCase(conf.get(HBASE_SECURITY_CONF_KEY));
247  }
248
249  /**
250   * In secure environment, if a user specified his keytab and principal, a hbase client will try to
251   * login with them. Otherwise, hbase client will try to obtain ticket(through kinit) from system.
252   * @param conf configuration file
253   * @return true if keytab and principal are configured
254   */
255  public static boolean shouldLoginFromKeytab(Configuration conf) {
256    Optional<String> keytab = Optional.ofNullable(conf.get(AuthUtil.HBASE_CLIENT_KEYTAB_FILE));
257    Optional<String> principal =
258      Optional.ofNullable(conf.get(AuthUtil.HBASE_CLIENT_KERBEROS_PRINCIPAL));
259    return keytab.isPresent() && principal.isPresent();
260  }
261
262  /* Concrete implementations */
263
264  /**
265   * Bridges {@code User} invocations to underlying calls to
266   * {@link org.apache.hadoop.security.UserGroupInformation} for secure Hadoop 0.20 and versions
267   * 0.21 and above.
268   */
269  @InterfaceAudience.Private
270  public static final class SecureHadoopUser extends User {
271    private String shortName;
272    private LoadingCache<String, String[]> cache;
273
274    public SecureHadoopUser() throws IOException {
275      ugi = UserGroupInformation.getCurrentUser();
276      this.cache = null;
277    }
278
279    public SecureHadoopUser(UserGroupInformation ugi) {
280      this.ugi = ugi;
281      this.cache = null;
282    }
283
284    public SecureHadoopUser(UserGroupInformation ugi, LoadingCache<String, String[]> cache) {
285      this.ugi = ugi;
286      this.cache = cache;
287    }
288
289    @Override
290    public String getShortName() {
291      if (shortName != null) return shortName;
292      try {
293        shortName = ugi.getShortUserName();
294        return shortName;
295      } catch (Exception e) {
296        throw new RuntimeException("Unexpected error getting user short name", e);
297      }
298    }
299
300    @Override
301    public String[] getGroupNames() {
302      if (cache != null) {
303        try {
304          return this.cache.get(getShortName());
305        } catch (ExecutionException e) {
306          return new String[0];
307        }
308      }
309      return ugi.getGroupNames();
310    }
311
312    @Override
313    public <T> T runAs(PrivilegedAction<T> action) {
314      return ugi.doAs(action);
315    }
316
317    @Override
318    public <T> T runAs(PrivilegedExceptionAction<T> action)
319      throws IOException, InterruptedException {
320      return ugi.doAs(action);
321    }
322
323    /**
324     * Create a user for testing.
325     * @see User#createUserForTesting(org.apache.hadoop.conf.Configuration, String, String[])
326     */
327    public static User createUserForTesting(Configuration conf, String name, String[] groups) {
328      synchronized (UserProvider.class) {
329        if (!(UserProvider.groups instanceof TestingGroups)) {
330          UserProvider.groups = new TestingGroups(UserProvider.groups);
331        }
332      }
333
334      ((TestingGroups) UserProvider.groups).setUserGroups(name, groups);
335      return new SecureHadoopUser(UserGroupInformation.createUserForTesting(name, groups));
336    }
337
338    /**
339     * Obtain credentials for the current process using the configured Kerberos keytab file and
340     * principal.
341     * @see User#login(org.apache.hadoop.conf.Configuration, String, String, String)
342     * @param conf             the Configuration to use
343     * @param fileConfKey      Configuration property key used to store the path to the keytab file
344     * @param principalConfKey Configuration property key used to store the principal name to login
345     *                         as
346     * @param localhost        the local hostname
347     */
348    public static void login(Configuration conf, String fileConfKey, String principalConfKey,
349      String localhost) throws IOException {
350      if (isSecurityEnabled()) {
351        SecurityUtil.login(conf, fileConfKey, principalConfKey, localhost);
352      }
353    }
354
355    /**
356     * Login through configured keytab and pricipal.
357     * @param keytabLocation location of keytab
358     * @param principalName  principal in keytab
359     * @throws IOException exception from UserGroupInformation.loginUserFromKeytab
360     */
361    public static void login(String keytabLocation, String principalName) throws IOException {
362      if (isSecurityEnabled()) {
363        UserGroupInformation.loginUserFromKeytab(principalName, keytabLocation);
364      }
365    }
366
367    /** Returns the result of {@code UserGroupInformation.isSecurityEnabled()}. */
368    public static boolean isSecurityEnabled() {
369      return UserGroupInformation.isSecurityEnabled();
370    }
371  }
372
373  public static class TestingGroups extends Groups {
374    public static final String TEST_CONF = "hbase.group.service.for.test.only";
375
376    private final Map<String, List<String>> userToGroupsMapping = new HashMap<>();
377    private Groups underlyingImplementation;
378
379    public TestingGroups(Groups underlyingImplementation) {
380      super(new Configuration());
381      this.underlyingImplementation = underlyingImplementation;
382    }
383
384    @Override
385    public List<String> getGroups(String user) throws IOException {
386      List<String> result = userToGroupsMapping.get(user);
387
388      if (result == null) {
389        result = underlyingImplementation.getGroups(user);
390      }
391
392      return result;
393    }
394
395    private void setUserGroups(String user, String[] groups) {
396      userToGroupsMapping.put(user, Arrays.asList(groups));
397    }
398  }
399}