View Javadoc

1   /*
2    * Copyright 2010 The Apache Software Foundation
3    *
4    * Licensed to the Apache Software Foundation (ASF) under one
5    * or more contributor license agreements.  See the NOTICE file
6    * distributed with this work for additional information
7    * regarding copyright ownership.  The ASF licenses this file
8    * to you under the Apache License, Version 2.0 (the
9    * "License"); you may not use this file except in compliance
10   * with the License.  You may obtain a copy of the License at
11   *
12   *     http://www.apache.org/licenses/LICENSE-2.0
13   *
14   * Unless required by applicable law or agreed to in writing, software
15   * distributed under the License is distributed on an "AS IS" BASIS,
16   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17   * See the License for the specific language governing permissions and
18   * limitations under the License.
19   */
20  
21  package org.apache.hadoop.hbase.security;
22  
23  import org.apache.commons.logging.LogFactory;
24  import org.apache.hadoop.conf.Configuration;
25  import org.apache.hadoop.fs.CommonConfigurationKeys;
26  import org.apache.hadoop.hbase.HBaseConfiguration;
27  import org.apache.hadoop.hbase.util.Methods;
28  import org.apache.hadoop.mapred.JobConf;
29  import org.apache.hadoop.mapreduce.Job;
30  import org.apache.hadoop.security.UserGroupInformation;
31  
32  import java.io.IOException;
33  import java.lang.reflect.Constructor;
34  import java.lang.reflect.UndeclaredThrowableException;
35  import java.security.PrivilegedAction;
36  import java.security.PrivilegedExceptionAction;
37  
38  import org.apache.commons.logging.Log;
39  
40  /**
41   * Wrapper to abstract out usage of user and group information in HBase.
42   *
43   * <p>
44   * This class provides a common interface for interacting with user and group
45   * information across changing APIs in different versions of Hadoop.  It only
46   * provides access to the common set of functionality in
47   * {@link org.apache.hadoop.security.UserGroupInformation} currently needed by
48   * HBase, but can be extended as needs change.
49   * </p>
50   */
51  public abstract class User {
52    public static final String HBASE_SECURITY_CONF_KEY =
53        "hbase.security.authentication";
54  
55    /**
56     * Flag to differentiate between API-incompatible changes to
57     * {@link org.apache.hadoop.security.UserGroupInformation} between vanilla
58     * Hadoop 0.20.x and secure Hadoop 0.20+.
59     */
60    private static boolean IS_SECURE_HADOOP = true;
61    static {
62      try {
63        UserGroupInformation.class.getMethod("isSecurityEnabled");
64      } catch (NoSuchMethodException nsme) {
65        IS_SECURE_HADOOP = false;
66      }
67    }
68    private static Log LOG = LogFactory.getLog(User.class);
69  
70    protected UserGroupInformation ugi;
71  
72    public UserGroupInformation getUGI() {
73      return ugi;
74    }
75  
76    /**
77     * Returns the full user name.  For Kerberos principals this will include
78     * the host and realm portions of the principal name.
79     * @return User full name.
80     */
81    public String getName() {
82      return ugi.getUserName();
83    }
84  
85    /**
86     * Returns the list of groups of which this user is a member.  On secure
87     * Hadoop this returns the group information for the user as resolved on the
88     * server.  For 0.20 based Hadoop, the group names are passed from the client.
89     */
90    public String[] getGroupNames() {
91      return ugi.getGroupNames();
92    }
93  
94    /**
95     * Returns the shortened version of the user name -- the portion that maps
96     * to an operating system user name.
97     * @return Short name
98     */
99    public abstract String getShortName();
100 
101   /**
102    * Executes the given action within the context of this user.
103    */
104   public abstract <T> T runAs(PrivilegedAction<T> action);
105 
106   /**
107    * Executes the given action within the context of this user.
108    */
109   public abstract <T> T runAs(PrivilegedExceptionAction<T> action)
110       throws IOException, InterruptedException;
111 
112   /**
113    * Requests an authentication token for this user and stores it in the
114    * user's credentials.
115    *
116    * @throws IOException
117    */
118   public abstract void obtainAuthTokenForJob(Configuration conf, Job job)
119       throws IOException, InterruptedException;
120 
121   /**
122    * Requests an authentication token for this user and stores it in the
123    * user's credentials.
124    *
125    * @throws IOException
126    */
127   public abstract void obtainAuthTokenForJob(JobConf job)
128       throws IOException, InterruptedException;
129 
130   public String toString() {
131     return ugi.toString();
132   }
133 
134   /**
135    * Returns the {@code User} instance within current execution context.
136    */
137   public static User getCurrent() throws IOException {
138     User user;
139     if (IS_SECURE_HADOOP) {
140       user = new SecureHadoopUser();
141     } else {
142       user = new HadoopUser();
143     }
144     if (user.getUGI() == null) {
145       return null;
146     }
147     return user;
148   }
149 
150   /**
151    * Wraps an underlying {@code UserGroupInformation} instance.
152    * @param ugi The base Hadoop user
153    * @return User
154    */
155   public static User create(UserGroupInformation ugi) {
156     if (ugi == null) {
157       return null;
158     }
159 
160     if (IS_SECURE_HADOOP) {
161       return new SecureHadoopUser(ugi);
162     }
163     return new HadoopUser(ugi);
164   }
165 
166   /**
167    * Generates a new {@code User} instance specifically for use in test code.
168    * @param name the full username
169    * @param groups the group names to which the test user will belong
170    * @return a new <code>User</code> instance
171    */
172   public static User createUserForTesting(Configuration conf,
173       String name, String[] groups) {
174     if (IS_SECURE_HADOOP) {
175       return SecureHadoopUser.createUserForTesting(conf, name, groups);
176     }
177     return HadoopUser.createUserForTesting(conf, name, groups);
178   }
179 
180   /**
181    * Log in the current process using the given configuration keys for the
182    * credential file and login principal.
183    *
184    * <p><strong>This is only applicable when
185    * running on secure Hadoop</strong> -- see
186    * org.apache.hadoop.security.SecurityUtil#login(Configuration,String,String,String).
187    * On regular Hadoop (without security features), this will safely be ignored.
188    * </p>
189    *
190    * @param conf The configuration data to use
191    * @param fileConfKey Property key used to configure path to the credential file
192    * @param principalConfKey Property key used to configure login principal
193    * @param localhost Current hostname to use in any credentials
194    * @throws IOException underlying exception from SecurityUtil.login() call
195    */
196   public static void login(Configuration conf, String fileConfKey,
197       String principalConfKey, String localhost) throws IOException {
198     if (IS_SECURE_HADOOP) {
199       SecureHadoopUser.login(conf, fileConfKey, principalConfKey, localhost);
200     } else {
201       HadoopUser.login(conf, fileConfKey, principalConfKey, localhost);
202     }
203   }
204 
205   /**
206    * Returns whether or not Kerberos authentication is configured for Hadoop.
207    * For non-secure Hadoop, this always returns <code>false</code>.
208    * For secure Hadoop, it will return the value from
209    * {@code UserGroupInformation.isSecurityEnabled()}.
210    */
211   public static boolean isSecurityEnabled() {
212     if (IS_SECURE_HADOOP) {
213       return SecureHadoopUser.isSecurityEnabled();
214     } else {
215       return HadoopUser.isSecurityEnabled();
216     }
217   }
218 
219   /**
220    * Returns whether or not secure authentication is enabled for HBase.  Note that
221    * HBase security requires HDFS security to provide any guarantees, so this requires that
222    * both <code>hbase.security.authentication</code> and <code>hadoop.security.authentication</code>
223    * are set to <code>kerberos</code>.
224    */
225   public static boolean isHBaseSecurityEnabled(Configuration conf) {
226     return "kerberos".equalsIgnoreCase(conf.get(HBASE_SECURITY_CONF_KEY)) &&
227         "kerberos".equalsIgnoreCase(
228             conf.get(CommonConfigurationKeys.HADOOP_SECURITY_AUTHENTICATION));
229   }
230 
231   /* Concrete implementations */
232 
233   /**
234    * Bridges {@link User} calls to invocations of the appropriate methods
235    * in {@link org.apache.hadoop.security.UserGroupInformation} in regular
236    * Hadoop 0.20 (ASF Hadoop and other versions without the backported security
237    * features).
238    */
239   private static class HadoopUser extends User {
240 
241     private HadoopUser() {
242       try {
243         ugi = (UserGroupInformation) callStatic("getCurrentUGI");
244         if (ugi == null) {
245           // Secure Hadoop UGI will perform an implicit login if the current
246           // user is null.  Emulate the same behavior here for consistency
247           Configuration conf = HBaseConfiguration.create();
248           ugi = (UserGroupInformation) callStatic("login",
249               new Class[]{ Configuration.class }, new Object[]{ conf });
250           if (ugi != null) {
251             callStatic("setCurrentUser",
252                 new Class[]{ UserGroupInformation.class }, new Object[]{ ugi });
253           }
254         }
255       } catch (RuntimeException re) {
256         throw re;
257       } catch (Exception e) {
258         throw new UndeclaredThrowableException(e,
259             "Unexpected exception HadoopUser<init>");
260       }
261     }
262 
263     private HadoopUser(UserGroupInformation ugi) {
264       this.ugi = ugi;
265     }
266 
267     @Override
268     public String getShortName() {
269       return ugi != null ? ugi.getUserName() : null;
270     }
271 
272     @Override
273     public <T> T runAs(PrivilegedAction<T> action) {
274       T result = null;
275       UserGroupInformation previous = null;
276       try {
277         previous = (UserGroupInformation) callStatic("getCurrentUGI");
278         try {
279           if (ugi != null) {
280             callStatic("setCurrentUser", new Class[]{UserGroupInformation.class},
281                 new Object[]{ugi});
282           }
283           result = action.run();
284         } finally {
285           callStatic("setCurrentUser", new Class[]{UserGroupInformation.class},
286               new Object[]{previous});
287         }
288       } catch (RuntimeException re) {
289         throw re;
290       } catch (Exception e) {
291         throw new UndeclaredThrowableException(e,
292             "Unexpected exception in runAs()");
293       }
294       return result;
295     }
296 
297     @Override
298     public <T> T runAs(PrivilegedExceptionAction<T> action)
299         throws IOException, InterruptedException {
300       T result = null;
301       try {
302         UserGroupInformation previous =
303             (UserGroupInformation) callStatic("getCurrentUGI");
304         try {
305           if (ugi != null) {
306             callStatic("setCurrentUGI", new Class[]{UserGroupInformation.class},
307                 new Object[]{ugi});
308           }
309           result = action.run();
310         } finally {
311           callStatic("setCurrentUGI", new Class[]{UserGroupInformation.class},
312               new Object[]{previous});
313         }
314       } catch (Exception e) {
315         if (e instanceof IOException) {
316           throw (IOException)e;
317         } else if (e instanceof InterruptedException) {
318           throw (InterruptedException)e;
319         } else if (e instanceof RuntimeException) {
320           throw (RuntimeException)e;
321         } else {
322           throw new UndeclaredThrowableException(e, "Unknown exception in runAs()");
323         }
324       }
325       return result;
326     }
327 
328     @Override
329     public void obtainAuthTokenForJob(Configuration conf, Job job)
330         throws IOException, InterruptedException {
331       // this is a no-op.  token creation is only supported for kerberos
332       // authenticated clients
333     }
334 
335     @Override
336     public void obtainAuthTokenForJob(JobConf job)
337         throws IOException, InterruptedException {
338       // this is a no-op.  token creation is only supported for kerberos
339       // authenticated clients
340     }
341 
342     /** @see User#createUserForTesting(org.apache.hadoop.conf.Configuration, String, String[]) */
343     public static User createUserForTesting(Configuration conf,
344         String name, String[] groups) {
345       try {
346         Class c = Class.forName("org.apache.hadoop.security.UnixUserGroupInformation");
347         Constructor constructor = c.getConstructor(String.class, String[].class);
348         if (constructor == null) {
349           throw new NullPointerException(
350              );
351         }
352         UserGroupInformation newUser =
353             (UserGroupInformation)constructor.newInstance(name, groups);
354         // set user in configuration -- hack for regular hadoop
355         conf.set("hadoop.job.ugi", newUser.toString());
356         return new HadoopUser(newUser);
357       } catch (ClassNotFoundException cnfe) {
358         throw new RuntimeException(
359             "UnixUserGroupInformation not found, is this secure Hadoop?", cnfe);
360       } catch (NoSuchMethodException nsme) {
361         throw new RuntimeException(
362             "No valid constructor found for UnixUserGroupInformation!", nsme);
363       } catch (RuntimeException re) {
364         throw re;
365       } catch (Exception e) {
366         throw new UndeclaredThrowableException(e,
367             "Unexpected exception instantiating new UnixUserGroupInformation");
368       }
369     }
370 
371     /**
372      * No-op since we're running on a version of Hadoop that doesn't support
373      * logins.
374      * @see User#login(org.apache.hadoop.conf.Configuration, String, String, String)
375      */
376     public static void login(Configuration conf, String fileConfKey,
377         String principalConfKey, String localhost) throws IOException {
378       LOG.info("Skipping login, not running on secure Hadoop");
379     }
380 
381     /** Always returns {@code false}. */
382     public static boolean isSecurityEnabled() {
383       return false;
384     }
385   }
386 
387   /**
388    * Bridges {@code User} invocations to underlying calls to
389    * {@link org.apache.hadoop.security.UserGroupInformation} for secure Hadoop
390    * 0.20 and versions 0.21 and above.
391    */
392   private static class SecureHadoopUser extends User {
393     private String shortName;
394 
395     private SecureHadoopUser() throws IOException {
396       try {
397         ugi = (UserGroupInformation) callStatic("getCurrentUser");
398       } catch (IOException ioe) {
399         throw ioe;
400       } catch (RuntimeException re) {
401         throw re;
402       } catch (Exception e) {
403         throw new UndeclaredThrowableException(e,
404             "Unexpected exception getting current secure user");
405       }
406     }
407 
408     private SecureHadoopUser(UserGroupInformation ugi) {
409       this.ugi = ugi;
410     }
411 
412     @Override
413     public String getShortName() {
414       if (shortName != null) return shortName;
415 
416       try {
417         shortName = (String)call(ugi, "getShortUserName", null, null);
418         return shortName;
419       } catch (RuntimeException re) {
420         throw re;
421       } catch (Exception e) {
422         throw new UndeclaredThrowableException(e,
423             "Unexpected error getting user short name");
424       }
425     }
426 
427     @Override
428     public <T> T runAs(PrivilegedAction<T> action) {
429       try {
430         return (T) call(ugi, "doAs", new Class[]{PrivilegedAction.class},
431             new Object[]{action});
432       } catch (RuntimeException re) {
433         throw re;
434       } catch (Exception e) {
435         throw new UndeclaredThrowableException(e,
436             "Unexpected exception in runAs()");
437       }
438     }
439 
440     @Override
441     public <T> T runAs(PrivilegedExceptionAction<T> action)
442         throws IOException, InterruptedException {
443       try {
444         return (T) call(ugi, "doAs",
445             new Class[]{PrivilegedExceptionAction.class},
446             new Object[]{action});
447       } catch (IOException ioe) {
448         throw ioe;
449       } catch (InterruptedException ie) {
450         throw ie;
451       } catch (RuntimeException re) {
452         throw re;
453       } catch (Exception e) {
454         throw new UndeclaredThrowableException(e,
455             "Unexpected exception in runAs(PrivilegedExceptionAction)");
456       }
457     }
458 
459     @Override
460     public void obtainAuthTokenForJob(Configuration conf, Job job)
461         throws IOException, InterruptedException {
462       try {
463         Class c = Class.forName(
464             "org.apache.hadoop.hbase.security.token.TokenUtil");
465         Methods.call(c, null, "obtainTokenForJob",
466             new Class[]{Configuration.class, UserGroupInformation.class,
467                 Job.class},
468             new Object[]{conf, ugi, job});
469       } catch (ClassNotFoundException cnfe) {
470         throw new RuntimeException("Failure loading TokenUtil class, "
471             +"is secure RPC available?", cnfe);
472       } catch (IOException ioe) {
473         throw ioe;
474       } catch (InterruptedException ie) {
475         throw ie;
476       } catch (RuntimeException re) {
477         throw re;
478       } catch (Exception e) {
479         throw new UndeclaredThrowableException(e,
480             "Unexpected error calling TokenUtil.obtainAndCacheToken()");
481       }
482     }
483 
484     @Override
485     public void obtainAuthTokenForJob(JobConf job)
486         throws IOException, InterruptedException {
487       try {
488         Class c = Class.forName(
489             "org.apache.hadoop.hbase.security.token.TokenUtil");
490         Methods.call(c, null, "obtainTokenForJob",
491             new Class[]{JobConf.class, UserGroupInformation.class},
492             new Object[]{job, ugi});
493       } catch (ClassNotFoundException cnfe) {
494         throw new RuntimeException("Failure loading TokenUtil class, "
495             +"is secure RPC available?", cnfe);
496       } catch (IOException ioe) {
497         throw ioe;
498       } catch (InterruptedException ie) {
499         throw ie;
500       } catch (RuntimeException re) {
501         throw re;
502       } catch (Exception e) {
503         throw new UndeclaredThrowableException(e,
504             "Unexpected error calling TokenUtil.obtainAndCacheToken()");
505       }
506     }
507 
508     /** @see User#createUserForTesting(org.apache.hadoop.conf.Configuration, String, String[]) */
509     public static User createUserForTesting(Configuration conf,
510         String name, String[] groups) {
511       try {
512         return new SecureHadoopUser(
513             (UserGroupInformation)callStatic("createUserForTesting",
514                 new Class[]{String.class, String[].class},
515                 new Object[]{name, groups})
516         );
517       } catch (RuntimeException re) {
518         throw re;
519       } catch (Exception e) {
520         throw new UndeclaredThrowableException(e,
521             "Error creating secure test user");
522       }
523     }
524 
525     /**
526      * Obtain credentials for the current process using the configured
527      * Kerberos keytab file and principal.
528      * @see User#login(org.apache.hadoop.conf.Configuration, String, String, String)
529      *
530      * @param conf the Configuration to use
531      * @param fileConfKey Configuration property key used to store the path
532      * to the keytab file
533      * @param principalConfKey Configuration property key used to store the
534      * principal name to login as
535      * @param localhost the local hostname
536      */
537     public static void login(Configuration conf, String fileConfKey,
538         String principalConfKey, String localhost) throws IOException {
539       if (isSecurityEnabled()) {
540         // check for SecurityUtil class
541         try {
542           Class c = Class.forName("org.apache.hadoop.security.SecurityUtil");
543           Class[] types = new Class[]{
544               Configuration.class, String.class, String.class, String.class };
545           Object[] args = new Object[]{
546               conf, fileConfKey, principalConfKey, localhost };
547           Methods.call(c, null, "login", types, args);
548         } catch (ClassNotFoundException cnfe) {
549           throw new RuntimeException("Unable to login using " +
550               "org.apache.hadoop.security.SecurityUtil.login(). SecurityUtil class " +
551               "was not found!  Is this a version of secure Hadoop?", cnfe);
552         } catch (IOException ioe) {
553           throw ioe;
554         } catch (RuntimeException re) {
555           throw re;
556         } catch (Exception e) {
557           throw new UndeclaredThrowableException(e,
558               "Unhandled exception in User.login()");
559         }
560       }
561     }
562 
563     /**
564      * Returns the result of {@code UserGroupInformation.isSecurityEnabled()}.
565      */
566     public static boolean isSecurityEnabled() {
567       try {
568         return (Boolean)callStatic("isSecurityEnabled");
569       } catch (RuntimeException re) {
570         throw re;
571       } catch (Exception e) {
572         throw new UndeclaredThrowableException(e,
573             "Unexpected exception calling UserGroupInformation.isSecurityEnabled()");
574       }
575     }
576   }
577 
578   /* Reflection helper methods */
579   private static Object callStatic(String methodName) throws Exception {
580     return call(null, methodName, null, null);
581   }
582 
583   private static Object callStatic(String methodName, Class[] types,
584       Object[] args) throws Exception {
585     return call(null, methodName, types, args);
586   }
587 
588   private static Object call(UserGroupInformation instance, String methodName,
589       Class[] types, Object[] args) throws Exception {
590     return Methods.call(UserGroupInformation.class, instance, methodName, types,
591         args);
592   }
593 }