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   @Override
131   public boolean equals(Object o) {
132     if (this == o) {
133       return true;
134     }
135     if (o == null || getClass() != o.getClass()) {
136       return false;
137     }
138     return ugi.equals(((User) o).ugi);
139   }
140   
141   @Override
142   public int hashCode() {
143     return ugi.hashCode();
144   }
145   
146   @Override
147   public String toString() {
148     return ugi.toString();
149   }
150 
151   /**
152    * Returns the {@code User} instance within current execution context.
153    */
154   public static User getCurrent() throws IOException {
155     User user;
156     if (IS_SECURE_HADOOP) {
157       user = new SecureHadoopUser();
158     } else {
159       user = new HadoopUser();
160     }
161     if (user.getUGI() == null) {
162       return null;
163     }
164     return user;
165   }
166 
167   /**
168    * Wraps an underlying {@code UserGroupInformation} instance.
169    * @param ugi The base Hadoop user
170    * @return User
171    */
172   public static User create(UserGroupInformation ugi) {
173     if (ugi == null) {
174       return null;
175     }
176 
177     if (IS_SECURE_HADOOP) {
178       return new SecureHadoopUser(ugi);
179     }
180     return new HadoopUser(ugi);
181   }
182 
183   /**
184    * Generates a new {@code User} instance specifically for use in test code.
185    * @param name the full username
186    * @param groups the group names to which the test user will belong
187    * @return a new <code>User</code> instance
188    */
189   public static User createUserForTesting(Configuration conf,
190       String name, String[] groups) {
191     if (IS_SECURE_HADOOP) {
192       return SecureHadoopUser.createUserForTesting(conf, name, groups);
193     }
194     return HadoopUser.createUserForTesting(conf, name, groups);
195   }
196 
197   /**
198    * Log in the current process using the given configuration keys for the
199    * credential file and login principal.
200    *
201    * <p><strong>This is only applicable when
202    * running on secure Hadoop</strong> -- see
203    * org.apache.hadoop.security.SecurityUtil#login(Configuration,String,String,String).
204    * On regular Hadoop (without security features), this will safely be ignored.
205    * </p>
206    *
207    * @param conf The configuration data to use
208    * @param fileConfKey Property key used to configure path to the credential file
209    * @param principalConfKey Property key used to configure login principal
210    * @param localhost Current hostname to use in any credentials
211    * @throws IOException underlying exception from SecurityUtil.login() call
212    */
213   public static void login(Configuration conf, String fileConfKey,
214       String principalConfKey, String localhost) throws IOException {
215     if (IS_SECURE_HADOOP) {
216       SecureHadoopUser.login(conf, fileConfKey, principalConfKey, localhost);
217     } else {
218       HadoopUser.login(conf, fileConfKey, principalConfKey, localhost);
219     }
220   }
221 
222   /**
223    * Returns whether or not Kerberos authentication is configured for Hadoop.
224    * For non-secure Hadoop, this always returns <code>false</code>.
225    * For secure Hadoop, it will return the value from
226    * {@code UserGroupInformation.isSecurityEnabled()}.
227    */
228   public static boolean isSecurityEnabled() {
229     if (IS_SECURE_HADOOP) {
230       return SecureHadoopUser.isSecurityEnabled();
231     } else {
232       return HadoopUser.isSecurityEnabled();
233     }
234   }
235 
236   /**
237    * Returns whether or not secure authentication is enabled for HBase.  Note that
238    * HBase security requires HDFS security to provide any guarantees, so this requires that
239    * both <code>hbase.security.authentication</code> and <code>hadoop.security.authentication</code>
240    * are set to <code>kerberos</code>.
241    */
242   public static boolean isHBaseSecurityEnabled(Configuration conf) {
243     return "kerberos".equalsIgnoreCase(conf.get(HBASE_SECURITY_CONF_KEY)) &&
244         "kerberos".equalsIgnoreCase(
245             conf.get(CommonConfigurationKeys.HADOOP_SECURITY_AUTHENTICATION));
246   }
247 
248   /* Concrete implementations */
249 
250   /**
251    * Bridges {@link User} calls to invocations of the appropriate methods
252    * in {@link org.apache.hadoop.security.UserGroupInformation} in regular
253    * Hadoop 0.20 (ASF Hadoop and other versions without the backported security
254    * features).
255    */
256   private static class HadoopUser extends User {
257 
258     private HadoopUser() {
259       try {
260         ugi = (UserGroupInformation) callStatic("getCurrentUGI");
261         if (ugi == null) {
262           // Secure Hadoop UGI will perform an implicit login if the current
263           // user is null.  Emulate the same behavior here for consistency
264           Configuration conf = HBaseConfiguration.create();
265           ugi = (UserGroupInformation) callStatic("login",
266               new Class[]{ Configuration.class }, new Object[]{ conf });
267           if (ugi != null) {
268             callStatic("setCurrentUser",
269                 new Class[]{ UserGroupInformation.class }, new Object[]{ ugi });
270           }
271         }
272       } catch (RuntimeException re) {
273         throw re;
274       } catch (Exception e) {
275         throw new UndeclaredThrowableException(e,
276             "Unexpected exception HadoopUser<init>");
277       }
278     }
279 
280     private HadoopUser(UserGroupInformation ugi) {
281       this.ugi = ugi;
282     }
283 
284     @Override
285     public String getShortName() {
286       return ugi != null ? ugi.getUserName() : null;
287     }
288 
289     @Override
290     public <T> T runAs(PrivilegedAction<T> action) {
291       T result = null;
292       UserGroupInformation previous = null;
293       try {
294         previous = (UserGroupInformation) callStatic("getCurrentUGI");
295         try {
296           if (ugi != null) {
297             callStatic("setCurrentUser", new Class[]{UserGroupInformation.class},
298                 new Object[]{ugi});
299           }
300           result = action.run();
301         } finally {
302           callStatic("setCurrentUser", new Class[]{UserGroupInformation.class},
303               new Object[]{previous});
304         }
305       } catch (RuntimeException re) {
306         throw re;
307       } catch (Exception e) {
308         throw new UndeclaredThrowableException(e,
309             "Unexpected exception in runAs()");
310       }
311       return result;
312     }
313 
314     @Override
315     public <T> T runAs(PrivilegedExceptionAction<T> action)
316         throws IOException, InterruptedException {
317       T result = null;
318       try {
319         UserGroupInformation previous =
320             (UserGroupInformation) callStatic("getCurrentUGI");
321         try {
322           if (ugi != null) {
323             callStatic("setCurrentUGI", new Class[]{UserGroupInformation.class},
324                 new Object[]{ugi});
325           }
326           result = action.run();
327         } finally {
328           callStatic("setCurrentUGI", new Class[]{UserGroupInformation.class},
329               new Object[]{previous});
330         }
331       } catch (Exception e) {
332         if (e instanceof IOException) {
333           throw (IOException)e;
334         } else if (e instanceof InterruptedException) {
335           throw (InterruptedException)e;
336         } else if (e instanceof RuntimeException) {
337           throw (RuntimeException)e;
338         } else {
339           throw new UndeclaredThrowableException(e, "Unknown exception in runAs()");
340         }
341       }
342       return result;
343     }
344 
345     @Override
346     public void obtainAuthTokenForJob(Configuration conf, Job job)
347         throws IOException, InterruptedException {
348       // this is a no-op.  token creation is only supported for kerberos
349       // authenticated clients
350     }
351 
352     @Override
353     public void obtainAuthTokenForJob(JobConf job)
354         throws IOException, InterruptedException {
355       // this is a no-op.  token creation is only supported for kerberos
356       // authenticated clients
357     }
358 
359     /** @see User#createUserForTesting(org.apache.hadoop.conf.Configuration, String, String[]) */
360     public static User createUserForTesting(Configuration conf,
361         String name, String[] groups) {
362       try {
363         Class c = Class.forName("org.apache.hadoop.security.UnixUserGroupInformation");
364         Constructor constructor = c.getConstructor(String.class, String[].class);
365         if (constructor == null) {
366           throw new NullPointerException(
367              );
368         }
369         UserGroupInformation newUser =
370             (UserGroupInformation)constructor.newInstance(name, groups);
371         // set user in configuration -- hack for regular hadoop
372         conf.set("hadoop.job.ugi", newUser.toString());
373         return new HadoopUser(newUser);
374       } catch (ClassNotFoundException cnfe) {
375         throw new RuntimeException(
376             "UnixUserGroupInformation not found, is this secure Hadoop?", cnfe);
377       } catch (NoSuchMethodException nsme) {
378         throw new RuntimeException(
379             "No valid constructor found for UnixUserGroupInformation!", nsme);
380       } catch (RuntimeException re) {
381         throw re;
382       } catch (Exception e) {
383         throw new UndeclaredThrowableException(e,
384             "Unexpected exception instantiating new UnixUserGroupInformation");
385       }
386     }
387 
388     /**
389      * No-op since we're running on a version of Hadoop that doesn't support
390      * logins.
391      * @see User#login(org.apache.hadoop.conf.Configuration, String, String, String)
392      */
393     public static void login(Configuration conf, String fileConfKey,
394         String principalConfKey, String localhost) throws IOException {
395       LOG.info("Skipping login, not running on secure Hadoop");
396     }
397 
398     /** Always returns {@code false}. */
399     public static boolean isSecurityEnabled() {
400       return false;
401     }
402   }
403 
404   /**
405    * Bridges {@code User} invocations to underlying calls to
406    * {@link org.apache.hadoop.security.UserGroupInformation} for secure Hadoop
407    * 0.20 and versions 0.21 and above.
408    */
409   private static class SecureHadoopUser extends User {
410     private String shortName;
411 
412     private SecureHadoopUser() throws IOException {
413       try {
414         ugi = (UserGroupInformation) callStatic("getCurrentUser");
415       } catch (IOException ioe) {
416         throw ioe;
417       } catch (RuntimeException re) {
418         throw re;
419       } catch (Exception e) {
420         throw new UndeclaredThrowableException(e,
421             "Unexpected exception getting current secure user");
422       }
423     }
424 
425     private SecureHadoopUser(UserGroupInformation ugi) {
426       this.ugi = ugi;
427     }
428 
429     @Override
430     public String getShortName() {
431       if (shortName != null) return shortName;
432 
433       try {
434         shortName = (String)call(ugi, "getShortUserName", null, null);
435         return shortName;
436       } catch (RuntimeException re) {
437         throw re;
438       } catch (Exception e) {
439         throw new UndeclaredThrowableException(e,
440             "Unexpected error getting user short name");
441       }
442     }
443 
444     @Override
445     public <T> T runAs(PrivilegedAction<T> action) {
446       try {
447         return (T) call(ugi, "doAs", new Class[]{PrivilegedAction.class},
448             new Object[]{action});
449       } catch (RuntimeException re) {
450         throw re;
451       } catch (Exception e) {
452         throw new UndeclaredThrowableException(e,
453             "Unexpected exception in runAs()");
454       }
455     }
456 
457     @Override
458     public <T> T runAs(PrivilegedExceptionAction<T> action)
459         throws IOException, InterruptedException {
460       try {
461         return (T) call(ugi, "doAs",
462             new Class[]{PrivilegedExceptionAction.class},
463             new Object[]{action});
464       } catch (IOException ioe) {
465         throw ioe;
466       } catch (InterruptedException ie) {
467         throw ie;
468       } catch (RuntimeException re) {
469         throw re;
470       } catch (Exception e) {
471         throw new UndeclaredThrowableException(e,
472             "Unexpected exception in runAs(PrivilegedExceptionAction)");
473       }
474     }
475 
476     @Override
477     public void obtainAuthTokenForJob(Configuration conf, Job job)
478         throws IOException, InterruptedException {
479       try {
480         Class c = Class.forName(
481             "org.apache.hadoop.hbase.security.token.TokenUtil");
482         Methods.call(c, null, "obtainTokenForJob",
483             new Class[]{Configuration.class, UserGroupInformation.class,
484                 Job.class},
485             new Object[]{conf, ugi, job});
486       } catch (ClassNotFoundException cnfe) {
487         throw new RuntimeException("Failure loading TokenUtil class, "
488             +"is secure RPC available?", cnfe);
489       } catch (IOException ioe) {
490         throw ioe;
491       } catch (InterruptedException ie) {
492         throw ie;
493       } catch (RuntimeException re) {
494         throw re;
495       } catch (Exception e) {
496         throw new UndeclaredThrowableException(e,
497             "Unexpected error calling TokenUtil.obtainAndCacheToken()");
498       }
499     }
500 
501     @Override
502     public void obtainAuthTokenForJob(JobConf job)
503         throws IOException, InterruptedException {
504       try {
505         Class c = Class.forName(
506             "org.apache.hadoop.hbase.security.token.TokenUtil");
507         Methods.call(c, null, "obtainTokenForJob",
508             new Class[]{JobConf.class, UserGroupInformation.class},
509             new Object[]{job, ugi});
510       } catch (ClassNotFoundException cnfe) {
511         throw new RuntimeException("Failure loading TokenUtil class, "
512             +"is secure RPC available?", cnfe);
513       } catch (IOException ioe) {
514         throw ioe;
515       } catch (InterruptedException ie) {
516         throw ie;
517       } catch (RuntimeException re) {
518         throw re;
519       } catch (Exception e) {
520         throw new UndeclaredThrowableException(e,
521             "Unexpected error calling TokenUtil.obtainAndCacheToken()");
522       }
523     }
524 
525     /** @see User#createUserForTesting(org.apache.hadoop.conf.Configuration, String, String[]) */
526     public static User createUserForTesting(Configuration conf,
527         String name, String[] groups) {
528       try {
529         return new SecureHadoopUser(
530             (UserGroupInformation)callStatic("createUserForTesting",
531                 new Class[]{String.class, String[].class},
532                 new Object[]{name, groups})
533         );
534       } catch (RuntimeException re) {
535         throw re;
536       } catch (Exception e) {
537         throw new UndeclaredThrowableException(e,
538             "Error creating secure test user");
539       }
540     }
541 
542     /**
543      * Obtain credentials for the current process using the configured
544      * Kerberos keytab file and principal.
545      * @see User#login(org.apache.hadoop.conf.Configuration, String, String, String)
546      *
547      * @param conf the Configuration to use
548      * @param fileConfKey Configuration property key used to store the path
549      * to the keytab file
550      * @param principalConfKey Configuration property key used to store the
551      * principal name to login as
552      * @param localhost the local hostname
553      */
554     public static void login(Configuration conf, String fileConfKey,
555         String principalConfKey, String localhost) throws IOException {
556       if (isSecurityEnabled()) {
557         // check for SecurityUtil class
558         try {
559           Class c = Class.forName("org.apache.hadoop.security.SecurityUtil");
560           Class[] types = new Class[]{
561               Configuration.class, String.class, String.class, String.class };
562           Object[] args = new Object[]{
563               conf, fileConfKey, principalConfKey, localhost };
564           Methods.call(c, null, "login", types, args);
565         } catch (ClassNotFoundException cnfe) {
566           throw new RuntimeException("Unable to login using " +
567               "org.apache.hadoop.security.SecurityUtil.login(). SecurityUtil class " +
568               "was not found!  Is this a version of secure Hadoop?", cnfe);
569         } catch (IOException ioe) {
570           throw ioe;
571         } catch (RuntimeException re) {
572           throw re;
573         } catch (Exception e) {
574           throw new UndeclaredThrowableException(e,
575               "Unhandled exception in User.login()");
576         }
577       }
578     }
579 
580     /**
581      * Returns the result of {@code UserGroupInformation.isSecurityEnabled()}.
582      */
583     public static boolean isSecurityEnabled() {
584       try {
585         return (Boolean)callStatic("isSecurityEnabled");
586       } catch (RuntimeException re) {
587         throw re;
588       } catch (Exception e) {
589         throw new UndeclaredThrowableException(e,
590             "Unexpected exception calling UserGroupInformation.isSecurityEnabled()");
591       }
592     }
593   }
594 
595   /* Reflection helper methods */
596   private static Object callStatic(String methodName) throws Exception {
597     return call(null, methodName, null, null);
598   }
599 
600   private static Object callStatic(String methodName, Class[] types,
601       Object[] args) throws Exception {
602     return call(null, methodName, types, args);
603   }
604 
605   private static Object call(UserGroupInformation instance, String methodName,
606       Class[] types, Object[] args) throws Exception {
607     return Methods.call(UserGroupInformation.class, instance, methodName, types,
608         args);
609   }
610 }