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