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 it is
239    * recommended that secure HBase should run on secure HDFS.
240    */
241   public static boolean isHBaseSecurityEnabled(Configuration conf) {
242     return "kerberos".equalsIgnoreCase(conf.get(HBASE_SECURITY_CONF_KEY));
243   }
244 
245   /* Concrete implementations */
246 
247   /**
248    * Bridges {@link User} calls to invocations of the appropriate methods
249    * in {@link org.apache.hadoop.security.UserGroupInformation} in regular
250    * Hadoop 0.20 (ASF Hadoop and other versions without the backported security
251    * features).
252    */
253   private static class HadoopUser extends User {
254 
255     private HadoopUser() {
256       try {
257         ugi = (UserGroupInformation) callStatic("getCurrentUGI");
258         if (ugi == null) {
259           // Secure Hadoop UGI will perform an implicit login if the current
260           // user is null.  Emulate the same behavior here for consistency
261           Configuration conf = HBaseConfiguration.create();
262           ugi = (UserGroupInformation) callStatic("login",
263               new Class[]{ Configuration.class }, new Object[]{ conf });
264           if (ugi != null) {
265             callStatic("setCurrentUser",
266                 new Class[]{ UserGroupInformation.class }, new Object[]{ ugi });
267           }
268         }
269       } catch (RuntimeException re) {
270         throw re;
271       } catch (Exception e) {
272         throw new UndeclaredThrowableException(e,
273             "Unexpected exception HadoopUser<init>");
274       }
275     }
276 
277     private HadoopUser(UserGroupInformation ugi) {
278       this.ugi = ugi;
279     }
280 
281     @Override
282     public String getShortName() {
283       return ugi != null ? ugi.getUserName() : null;
284     }
285 
286     @Override
287     public <T> T runAs(PrivilegedAction<T> action) {
288       T result = null;
289       UserGroupInformation previous = null;
290       try {
291         previous = (UserGroupInformation) callStatic("getCurrentUGI");
292         try {
293           if (ugi != null) {
294             callStatic("setCurrentUser", new Class[]{UserGroupInformation.class},
295                 new Object[]{ugi});
296           }
297           result = action.run();
298         } finally {
299           callStatic("setCurrentUser", new Class[]{UserGroupInformation.class},
300               new Object[]{previous});
301         }
302       } catch (RuntimeException re) {
303         throw re;
304       } catch (Exception e) {
305         throw new UndeclaredThrowableException(e,
306             "Unexpected exception in runAs()");
307       }
308       return result;
309     }
310 
311     @Override
312     public <T> T runAs(PrivilegedExceptionAction<T> action)
313         throws IOException, InterruptedException {
314       T result = null;
315       try {
316         UserGroupInformation previous =
317             (UserGroupInformation) callStatic("getCurrentUGI");
318         try {
319           if (ugi != null) {
320             callStatic("setCurrentUGI", new Class[]{UserGroupInformation.class},
321                 new Object[]{ugi});
322           }
323           result = action.run();
324         } finally {
325           callStatic("setCurrentUGI", new Class[]{UserGroupInformation.class},
326               new Object[]{previous});
327         }
328       } catch (Exception e) {
329         if (e instanceof IOException) {
330           throw (IOException)e;
331         } else if (e instanceof InterruptedException) {
332           throw (InterruptedException)e;
333         } else if (e instanceof RuntimeException) {
334           throw (RuntimeException)e;
335         } else {
336           throw new UndeclaredThrowableException(e, "Unknown exception in runAs()");
337         }
338       }
339       return result;
340     }
341 
342     @Override
343     public void obtainAuthTokenForJob(Configuration conf, Job job)
344         throws IOException, InterruptedException {
345       // this is a no-op.  token creation is only supported for kerberos
346       // authenticated clients
347     }
348 
349     @Override
350     public void obtainAuthTokenForJob(JobConf job)
351         throws IOException, InterruptedException {
352       // this is a no-op.  token creation is only supported for kerberos
353       // authenticated clients
354     }
355 
356     /** @see User#createUserForTesting(org.apache.hadoop.conf.Configuration, String, String[]) */
357     public static User createUserForTesting(Configuration conf,
358         String name, String[] groups) {
359       try {
360         Class c = Class.forName("org.apache.hadoop.security.UnixUserGroupInformation");
361         Constructor constructor = c.getConstructor(String.class, String[].class);
362         if (constructor == null) {
363           throw new NullPointerException(
364              );
365         }
366         UserGroupInformation newUser =
367             (UserGroupInformation)constructor.newInstance(name, groups);
368         // set user in configuration -- hack for regular hadoop
369         conf.set("hadoop.job.ugi", newUser.toString());
370         return new HadoopUser(newUser);
371       } catch (ClassNotFoundException cnfe) {
372         throw new RuntimeException(
373             "UnixUserGroupInformation not found, is this secure Hadoop?", cnfe);
374       } catch (NoSuchMethodException nsme) {
375         throw new RuntimeException(
376             "No valid constructor found for UnixUserGroupInformation!", nsme);
377       } catch (RuntimeException re) {
378         throw re;
379       } catch (Exception e) {
380         throw new UndeclaredThrowableException(e,
381             "Unexpected exception instantiating new UnixUserGroupInformation");
382       }
383     }
384 
385     /**
386      * No-op since we're running on a version of Hadoop that doesn't support
387      * logins.
388      * @see User#login(org.apache.hadoop.conf.Configuration, String, String, String)
389      */
390     public static void login(Configuration conf, String fileConfKey,
391         String principalConfKey, String localhost) throws IOException {
392       LOG.info("Skipping login, not running on secure Hadoop");
393     }
394 
395     /** Always returns {@code false}. */
396     public static boolean isSecurityEnabled() {
397       return false;
398     }
399   }
400 
401   /**
402    * Bridges {@code User} invocations to underlying calls to
403    * {@link org.apache.hadoop.security.UserGroupInformation} for secure Hadoop
404    * 0.20 and versions 0.21 and above.
405    */
406   public static class SecureHadoopUser extends User {
407     private String shortName;
408 
409     private SecureHadoopUser() throws IOException {
410       try {
411         ugi = (UserGroupInformation) callStatic("getCurrentUser");
412       } catch (IOException ioe) {
413         throw ioe;
414       } catch (RuntimeException re) {
415         throw re;
416       } catch (Exception e) {
417         throw new UndeclaredThrowableException(e,
418             "Unexpected exception getting current secure user");
419       }
420     }
421 
422     public SecureHadoopUser(UserGroupInformation ugi) {
423       this.ugi = ugi;
424     }
425 
426     @Override
427     public String getShortName() {
428       if (shortName != null) return shortName;
429 
430       try {
431         shortName = (String)call(ugi, "getShortUserName", null, null);
432         return shortName;
433       } catch (RuntimeException re) {
434         throw re;
435       } catch (Exception e) {
436         throw new UndeclaredThrowableException(e,
437             "Unexpected error getting user short name");
438       }
439     }
440 
441     @Override
442     public <T> T runAs(PrivilegedAction<T> action) {
443       try {
444         return (T) call(ugi, "doAs", new Class[]{PrivilegedAction.class},
445             new Object[]{action});
446       } catch (RuntimeException re) {
447         throw re;
448       } catch (Exception e) {
449         throw new UndeclaredThrowableException(e,
450             "Unexpected exception in runAs()");
451       }
452     }
453 
454     @Override
455     public <T> T runAs(PrivilegedExceptionAction<T> action)
456         throws IOException, InterruptedException {
457       try {
458         return (T) call(ugi, "doAs",
459             new Class[]{PrivilegedExceptionAction.class},
460             new Object[]{action});
461       } catch (IOException ioe) {
462         throw ioe;
463       } catch (InterruptedException ie) {
464         throw ie;
465       } catch (RuntimeException re) {
466         throw re;
467       } catch (Exception e) {
468         throw new UndeclaredThrowableException(e,
469             "Unexpected exception in runAs(PrivilegedExceptionAction)");
470       }
471     }
472 
473     @Override
474     public void obtainAuthTokenForJob(Configuration conf, Job job)
475         throws IOException, InterruptedException {
476       try {
477         Class c = Class.forName(
478             "org.apache.hadoop.hbase.security.token.TokenUtil");
479         Methods.call(c, null, "obtainTokenForJob",
480             new Class[]{Configuration.class, UserGroupInformation.class,
481                 Job.class},
482             new Object[]{conf, ugi, job});
483       } catch (ClassNotFoundException cnfe) {
484         throw new RuntimeException("Failure loading TokenUtil class, "
485             +"is secure RPC available?", cnfe);
486       } catch (IOException ioe) {
487         throw ioe;
488       } catch (InterruptedException ie) {
489         throw ie;
490       } catch (RuntimeException re) {
491         throw re;
492       } catch (Exception e) {
493         throw new UndeclaredThrowableException(e,
494             "Unexpected error calling TokenUtil.obtainAndCacheToken()");
495       }
496     }
497 
498     @Override
499     public void obtainAuthTokenForJob(JobConf job)
500         throws IOException, InterruptedException {
501       try {
502         Class c = Class.forName(
503             "org.apache.hadoop.hbase.security.token.TokenUtil");
504         Methods.call(c, null, "obtainTokenForJob",
505             new Class[]{JobConf.class, UserGroupInformation.class},
506             new Object[]{job, ugi});
507       } catch (ClassNotFoundException cnfe) {
508         throw new RuntimeException("Failure loading TokenUtil class, "
509             +"is secure RPC available?", cnfe);
510       } catch (IOException ioe) {
511         throw ioe;
512       } catch (InterruptedException ie) {
513         throw ie;
514       } catch (RuntimeException re) {
515         throw re;
516       } catch (Exception e) {
517         throw new UndeclaredThrowableException(e,
518             "Unexpected error calling TokenUtil.obtainAndCacheToken()");
519       }
520     }
521 
522     /** @see User#createUserForTesting(org.apache.hadoop.conf.Configuration, String, String[]) */
523     public static User createUserForTesting(Configuration conf,
524         String name, String[] groups) {
525       try {
526         return new SecureHadoopUser(
527             (UserGroupInformation)callStatic("createUserForTesting",
528                 new Class[]{String.class, String[].class},
529                 new Object[]{name, groups})
530         );
531       } catch (RuntimeException re) {
532         throw re;
533       } catch (Exception e) {
534         throw new UndeclaredThrowableException(e,
535             "Error creating secure test user");
536       }
537     }
538 
539     /**
540      * Obtain credentials for the current process using the configured
541      * Kerberos keytab file and principal.
542      * @see User#login(org.apache.hadoop.conf.Configuration, String, String, String)
543      *
544      * @param conf the Configuration to use
545      * @param fileConfKey Configuration property key used to store the path
546      * to the keytab file
547      * @param principalConfKey Configuration property key used to store the
548      * principal name to login as
549      * @param localhost the local hostname
550      */
551     public static void login(Configuration conf, String fileConfKey,
552         String principalConfKey, String localhost) throws IOException {
553       if (isSecurityEnabled()) {
554         // check for SecurityUtil class
555         try {
556           Class c = Class.forName("org.apache.hadoop.security.SecurityUtil");
557           Class[] types = new Class[]{
558               Configuration.class, String.class, String.class, String.class };
559           Object[] args = new Object[]{
560               conf, fileConfKey, principalConfKey, localhost };
561           Methods.call(c, null, "login", types, args);
562         } catch (ClassNotFoundException cnfe) {
563           throw new RuntimeException("Unable to login using " +
564               "org.apache.hadoop.security.SecurityUtil.login(). SecurityUtil class " +
565               "was not found!  Is this a version of secure Hadoop?", cnfe);
566         } catch (IOException ioe) {
567           throw ioe;
568         } catch (RuntimeException re) {
569           throw re;
570         } catch (Exception e) {
571           throw new UndeclaredThrowableException(e,
572               "Unhandled exception in User.login()");
573         }
574       }
575     }
576 
577     /**
578      * Returns the result of {@code UserGroupInformation.isSecurityEnabled()}.
579      */
580     public static boolean isSecurityEnabled() {
581       try {
582         return (Boolean)callStatic("isSecurityEnabled");
583       } catch (RuntimeException re) {
584         throw re;
585       } catch (Exception e) {
586         throw new UndeclaredThrowableException(e,
587             "Unexpected exception calling UserGroupInformation.isSecurityEnabled()");
588       }
589     }
590   }
591 
592   /* Reflection helper methods */
593   private static Object callStatic(String methodName) throws Exception {
594     return call(null, methodName, null, null);
595   }
596 
597   private static Object callStatic(String methodName, Class[] types,
598       Object[] args) throws Exception {
599     return call(null, methodName, types, args);
600   }
601 
602   private static Object call(UserGroupInformation instance, String methodName,
603       Class[] types, Object[] args) throws Exception {
604     return Methods.call(UserGroupInformation.class, instance, methodName, types,
605         args);
606   }
607 }