View Javadoc

1   /*
2    *
3    * Licensed to the Apache Software Foundation (ASF) under one
4    * or more contributor license agreements.  See the NOTICE file
5    * distributed with this work for additional information
6    * regarding copyright ownership.  The ASF licenses this file
7    * to you under the Apache License, Version 2.0 (the
8    * "License"); you may not use this file except in compliance
9    * with the License.  You may obtain a copy of the License at
10   *
11   *     http://www.apache.org/licenses/LICENSE-2.0
12   *
13   * Unless required by applicable law or agreed to in writing, software
14   * distributed under the License is distributed on an "AS IS" BASIS,
15   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16   * See the License for the specific language governing permissions and
17   * limitations under the License.
18   */
19  
20  package org.apache.hadoop.hbase.security;
21  
22  import java.io.IOException;
23  import java.lang.reflect.UndeclaredThrowableException;
24  import java.security.PrivilegedAction;
25  import java.security.PrivilegedExceptionAction;
26  import java.util.Arrays;
27  import java.util.Collection;
28  import java.util.HashMap;
29  import java.util.List;
30  import java.util.Map;
31  import java.util.concurrent.ExecutionException;
32  
33  import com.google.common.cache.LoadingCache;
34  
35  import org.apache.hadoop.conf.Configuration;
36  import org.apache.hadoop.hbase.classification.InterfaceAudience;
37  import org.apache.hadoop.hbase.classification.InterfaceStability;
38  import org.apache.hadoop.hbase.util.Methods;
39  import org.apache.hadoop.mapred.JobConf;
40  import org.apache.hadoop.mapreduce.Job;
41  import org.apache.hadoop.security.Groups;
42  import org.apache.hadoop.security.SecurityUtil;
43  import org.apache.hadoop.security.UserGroupInformation;
44  import org.apache.hadoop.security.token.Token;
45  import org.apache.hadoop.security.token.TokenIdentifier;
46  
47  /**
48   * Wrapper to abstract out usage of user and group information in HBase.
49   *
50   * <p>
51   * This class provides a common interface for interacting with user and group
52   * information across changing APIs in different versions of Hadoop.  It only
53   * provides access to the common set of functionality in
54   * {@link org.apache.hadoop.security.UserGroupInformation} currently needed by
55   * HBase, but can be extended as needs change.
56   * </p>
57   */
58  @InterfaceAudience.Public
59  @InterfaceStability.Stable
60  public abstract class User {
61    public static final String HBASE_SECURITY_CONF_KEY =
62        "hbase.security.authentication";
63    public static final String HBASE_SECURITY_AUTHORIZATION_CONF_KEY =
64        "hbase.security.authorization";
65  
66    protected UserGroupInformation ugi;
67  
68    public UserGroupInformation getUGI() {
69      return ugi;
70    }
71  
72    /**
73     * Returns the full user name.  For Kerberos principals this will include
74     * the host and realm portions of the principal name.
75     *
76     * @return User full name.
77     */
78    public String getName() {
79      return ugi.getUserName();
80    }
81  
82    /**
83     * Returns the list of groups of which this user is a member.  On secure
84     * Hadoop this returns the group information for the user as resolved on the
85     * server.  For 0.20 based Hadoop, the group names are passed from the client.
86     */
87    public String[] getGroupNames() {
88      return ugi.getGroupNames();
89    }
90  
91    /**
92     * Returns the shortened version of the user name -- the portion that maps
93     * to an operating system user name.
94     *
95     * @return Short name
96     */
97    public abstract String getShortName();
98  
99    /**
100    * Executes the given action within the context of this user.
101    */
102   public abstract <T> T runAs(PrivilegedAction<T> action);
103 
104   /**
105    * Executes the given action within the context of this user.
106    */
107   public abstract <T> T runAs(PrivilegedExceptionAction<T> action)
108       throws IOException, InterruptedException;
109 
110   /**
111    * Requests an authentication token for this user and stores it in the
112    * user's credentials.
113    *
114    * @throws IOException
115    * @deprecated Use {@code TokenUtil.obtainAuthTokenForJob(HConnection,User,Job)}
116    *     instead.
117    */
118   @Deprecated
119   public abstract void obtainAuthTokenForJob(Configuration conf, Job job)
120       throws IOException, InterruptedException;
121 
122   /**
123    * Requests an authentication token for this user and stores it in the
124    * user's credentials.
125    *
126    * @throws IOException
127    * @deprecated Use {@code TokenUtil.obtainAuthTokenForJob(HConnection,JobConf,User)}
128    *     instead.
129    */
130   @Deprecated
131   public abstract void obtainAuthTokenForJob(JobConf job)
132       throws IOException, InterruptedException;
133 
134   /**
135    * Returns the Token of the specified kind associated with this user,
136    * or null if the Token is not present.
137    *
138    * @param kind the kind of token
139    * @param service service on which the token is supposed to be used
140    * @return the token of the specified kind.
141    */
142   public Token<?> getToken(String kind, String service) throws IOException {
143     for (Token<?> token : ugi.getTokens()) {
144       if (token.getKind().toString().equals(kind) &&
145           (service != null && token.getService().toString().equals(service))) {
146         return token;
147       }
148     }
149     return null;
150   }
151 
152   /**
153    * Returns all the tokens stored in the user's credentials.
154    */
155   public Collection<Token<? extends TokenIdentifier>> getTokens() {
156     return ugi.getTokens();
157   }
158 
159   /**
160    * Adds the given Token to the user's credentials.
161    *
162    * @param token the token to add
163    */
164   public void addToken(Token<? extends TokenIdentifier> token) {
165     ugi.addToken(token);
166   }
167 
168   @Override
169   public boolean equals(Object o) {
170     if (this == o) {
171       return true;
172     }
173     if (o == null || getClass() != o.getClass()) {
174       return false;
175     }
176     return ugi.equals(((User) o).ugi);
177   }
178 
179   @Override
180   public int hashCode() {
181     return ugi.hashCode();
182   }
183 
184   @Override
185   public String toString() {
186     return ugi.toString();
187   }
188 
189   /**
190    * Returns the {@code User} instance within current execution context.
191    */
192   public static User getCurrent() throws IOException {
193     User user = new SecureHadoopUser();
194     if (user.getUGI() == null) {
195       return null;
196     }
197     return user;
198   }
199 
200   /**
201    * Executes the given action as the login user
202    * @param action
203    * @return
204    * @throws IOException
205    * @throws InterruptedException
206    */
207   @SuppressWarnings({ "rawtypes", "unchecked" })
208   public static <T> T runAsLoginUser(PrivilegedExceptionAction<T> action) throws IOException {
209     return doAsUser(UserGroupInformation.getLoginUser(), action);
210   }
211 
212   private static <T> T doAsUser(UserGroupInformation ugi,
213       PrivilegedExceptionAction<T> action) throws IOException {
214     try {
215       return ugi.doAs(action);
216     } catch (InterruptedException ie) {
217       throw new IOException(ie);
218     }
219   }
220 
221   /**
222    * Wraps an underlying {@code UserGroupInformation} instance.
223    * @param ugi The base Hadoop user
224    * @return User
225    */
226   public static User create(UserGroupInformation ugi) {
227     if (ugi == null) {
228       return null;
229     }
230     return new SecureHadoopUser(ugi);
231   }
232 
233   /**
234    * Generates a new {@code User} instance specifically for use in test code.
235    * @param name the full username
236    * @param groups the group names to which the test user will belong
237    * @return a new <code>User</code> instance
238    */
239   public static User createUserForTesting(Configuration conf,
240       String name, String[] groups) {
241     User userForTesting = SecureHadoopUser.createUserForTesting(conf, name, groups);
242     return userForTesting;
243   }
244 
245   /**
246    * Log in the current process using the given configuration keys for the
247    * credential file and login principal.
248    *
249    * <p><strong>This is only applicable when
250    * running on secure Hadoop</strong> -- see
251    * org.apache.hadoop.security.SecurityUtil#login(Configuration,String,String,String).
252    * On regular Hadoop (without security features), this will safely be ignored.
253    * </p>
254    *
255    * @param conf The configuration data to use
256    * @param fileConfKey Property key used to configure path to the credential file
257    * @param principalConfKey Property key used to configure login principal
258    * @param localhost Current hostname to use in any credentials
259    * @throws IOException underlying exception from SecurityUtil.login() call
260    */
261   public static void login(Configuration conf, String fileConfKey,
262       String principalConfKey, String localhost) throws IOException {
263     SecureHadoopUser.login(conf, fileConfKey, principalConfKey, localhost);
264   }
265 
266   /**
267    * Returns whether or not Kerberos authentication is configured for Hadoop.
268    * For non-secure Hadoop, this always returns <code>false</code>.
269    * For secure Hadoop, it will return the value from
270    * {@code UserGroupInformation.isSecurityEnabled()}.
271    */
272   public static boolean isSecurityEnabled() {
273     return SecureHadoopUser.isSecurityEnabled();
274   }
275 
276   /**
277    * Returns whether or not secure authentication is enabled for HBase. Note that
278    * HBase security requires HDFS security to provide any guarantees, so it is
279    * recommended that secure HBase should run on secure HDFS.
280    */
281   public static boolean isHBaseSecurityEnabled(Configuration conf) {
282     return "kerberos".equalsIgnoreCase(conf.get(HBASE_SECURITY_CONF_KEY));
283   }
284 
285   /* Concrete implementations */
286 
287   /**
288    * Bridges {@code User} invocations to underlying calls to
289    * {@link org.apache.hadoop.security.UserGroupInformation} for secure Hadoop
290    * 0.20 and versions 0.21 and above.
291    */
292   @InterfaceAudience.Private
293    public static final class SecureHadoopUser extends User {
294     private String shortName;
295     private LoadingCache<String, String[]> cache;
296 
297     public SecureHadoopUser() throws IOException {
298       ugi = UserGroupInformation.getCurrentUser();
299       this.cache = null;
300     }
301 
302     public SecureHadoopUser(UserGroupInformation ugi) {
303       this.ugi = ugi;
304       this.cache = null;
305     }
306 
307     public SecureHadoopUser(UserGroupInformation ugi,
308                             LoadingCache<String, String[]> cache) {
309       this.ugi = ugi;
310       this.cache = cache;
311     }
312 
313     @Override
314     public String getShortName() {
315       if (shortName != null) return shortName;
316       try {
317         shortName = ugi.getShortUserName();
318         return shortName;
319       } catch (Exception e) {
320         throw new RuntimeException("Unexpected error getting user short name",
321           e);
322       }
323     }
324 
325     @Override
326     public String[] getGroupNames() {
327       if (cache != null) {
328         try {
329           return this.cache.get(getShortName());
330         } catch (ExecutionException e) {
331           return new String[0];
332         }
333       }
334       return ugi.getGroupNames();
335     }
336 
337     @Override
338     public <T> T runAs(PrivilegedAction<T> action) {
339       return ugi.doAs(action);
340     }
341 
342     @Override
343     public <T> T runAs(PrivilegedExceptionAction<T> action)
344         throws IOException, InterruptedException {
345       return ugi.doAs(action);
346     }
347 
348     @Override
349     public void obtainAuthTokenForJob(Configuration conf, Job job)
350         throws IOException, InterruptedException {
351       try {
352         Class<?> c = Class.forName(
353             "org.apache.hadoop.hbase.security.token.TokenUtil");
354         Methods.call(c, null, "obtainTokenForJob",
355             new Class[]{Configuration.class, UserGroupInformation.class,
356                 Job.class},
357             new Object[]{conf, ugi, job});
358       } catch (ClassNotFoundException cnfe) {
359         throw new RuntimeException("Failure loading TokenUtil class, "
360             +"is secure RPC available?", cnfe);
361       } catch (IOException ioe) {
362         throw ioe;
363       } catch (InterruptedException ie) {
364         throw ie;
365       } catch (RuntimeException re) {
366         throw re;
367       } catch (Exception e) {
368         throw new UndeclaredThrowableException(e,
369             "Unexpected error calling TokenUtil.obtainAndCacheToken()");
370       }
371     }
372 
373     @Override
374     public void obtainAuthTokenForJob(JobConf job)
375         throws IOException, InterruptedException {
376       try {
377         Class<?> c = Class.forName(
378             "org.apache.hadoop.hbase.security.token.TokenUtil");
379         Methods.call(c, null, "obtainTokenForJob",
380             new Class[]{JobConf.class, UserGroupInformation.class},
381             new Object[]{job, ugi});
382       } catch (ClassNotFoundException cnfe) {
383         throw new RuntimeException("Failure loading TokenUtil class, "
384             +"is secure RPC available?", cnfe);
385       } catch (IOException ioe) {
386         throw ioe;
387       } catch (InterruptedException ie) {
388         throw ie;
389       } catch (RuntimeException re) {
390         throw re;
391       } catch (Exception e) {
392         throw new UndeclaredThrowableException(e,
393             "Unexpected error calling TokenUtil.obtainAndCacheToken()");
394       }
395     }
396 
397     /** @see User#createUserForTesting(org.apache.hadoop.conf.Configuration, String, String[]) */
398     public static User createUserForTesting(Configuration conf,
399         String name, String[] groups) {
400       synchronized (UserProvider.class) {
401         if (!(UserProvider.groups instanceof TestingGroups)) {
402           UserProvider.groups = new TestingGroups(UserProvider.groups);
403         }
404       }
405 
406       ((TestingGroups)UserProvider.groups).setUserGroups(name, groups);
407       return new SecureHadoopUser(UserGroupInformation.createUserForTesting(name, groups));
408     }
409 
410     /**
411      * Obtain credentials for the current process using the configured
412      * Kerberos keytab file and principal.
413      * @see User#login(org.apache.hadoop.conf.Configuration, String, String, String)
414      *
415      * @param conf the Configuration to use
416      * @param fileConfKey Configuration property key used to store the path
417      * to the keytab file
418      * @param principalConfKey Configuration property key used to store the
419      * principal name to login as
420      * @param localhost the local hostname
421      */
422     public static void login(Configuration conf, String fileConfKey,
423         String principalConfKey, String localhost) throws IOException {
424       if (isSecurityEnabled()) {
425         SecurityUtil.login(conf, fileConfKey, principalConfKey, localhost);
426       }
427     }
428 
429     /**
430      * Returns the result of {@code UserGroupInformation.isSecurityEnabled()}.
431      */
432     public static boolean isSecurityEnabled() {
433       return UserGroupInformation.isSecurityEnabled();
434     }
435   }
436 
437   static class TestingGroups extends Groups {
438     private final Map<String, List<String>> userToGroupsMapping =
439         new HashMap<String,List<String>>();
440     private Groups underlyingImplementation;
441 
442     TestingGroups(Groups underlyingImplementation) {
443       super(new Configuration());
444       this.underlyingImplementation = underlyingImplementation;
445     }
446 
447     @Override
448     public List<String> getGroups(String user) throws IOException {
449       List<String> result = userToGroupsMapping.get(user);
450 
451       if (result == null) {
452         result = underlyingImplementation.getGroups(user);
453       }
454 
455       return result;
456     }
457 
458     private void setUserGroups(String user, String[] groups) {
459       userToGroupsMapping.put(user, Arrays.asList(groups));
460     }
461   }
462 }