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