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  
27  import org.apache.commons.logging.Log;
28  import org.apache.commons.logging.LogFactory;
29  import org.apache.hadoop.classification.InterfaceAudience;
30  import org.apache.hadoop.conf.Configuration;
31  import org.apache.hadoop.hbase.util.Methods;
32  import org.apache.hadoop.mapred.JobConf;
33  import org.apache.hadoop.mapreduce.Job;
34  import org.apache.hadoop.security.UserGroupInformation;
35  import org.apache.hadoop.security.token.Token;
36  
37  /**
38   * Wrapper to abstract out usage of user and group information in HBase.
39   *
40   * <p>
41   * This class provides a common interface for interacting with user and group
42   * information across changing APIs in different versions of Hadoop.  It only
43   * provides access to the common set of functionality in
44   * {@link org.apache.hadoop.security.UserGroupInformation} currently needed by
45   * HBase, but can be extended as needs change.
46   * </p>
47   */
48  @InterfaceAudience.Private
49  public abstract class User {
50    public static final String HBASE_SECURITY_CONF_KEY =
51        "hbase.security.authentication";
52  
53    private static Log LOG = LogFactory.getLog(User.class);
54  
55    protected UserGroupInformation ugi;
56  
57    public UserGroupInformation getUGI() {
58      return ugi;
59    }
60  
61    /**
62     * Returns the full user name.  For Kerberos principals this will include
63     * the host and realm portions of the principal name.
64     * @return User full name.
65     */
66    public String getName() {
67      return ugi.getUserName();
68    }
69  
70    /**
71     * Returns the list of groups of which this user is a member.  On secure
72     * Hadoop this returns the group information for the user as resolved on the
73     * server.  For 0.20 based Hadoop, the group names are passed from the client.
74     */
75    public String[] getGroupNames() {
76      return ugi.getGroupNames();
77    }
78  
79    /**
80     * Returns the shortened version of the user name -- the portion that maps
81     * to an operating system user name.
82     * @return Short name
83     */
84    public abstract String getShortName();
85  
86    /**
87     * Executes the given action within the context of this user.
88     */
89    public abstract <T> T runAs(PrivilegedAction<T> action);
90  
91    /**
92     * Executes the given action within the context of this user.
93     */
94    public abstract <T> T runAs(PrivilegedExceptionAction<T> action)
95        throws IOException, InterruptedException;
96  
97    /**
98     * Requests an authentication token for this user and stores it in the
99     * user's credentials.
100    *
101    * @throws IOException
102    */
103   public abstract void obtainAuthTokenForJob(Configuration conf, Job job)
104       throws IOException, InterruptedException;
105 
106   /**
107    * Requests an authentication token for this user and stores it in the
108    * user's credentials.
109    *
110    * @throws IOException
111    */
112   public abstract void obtainAuthTokenForJob(JobConf job)
113       throws IOException, InterruptedException;
114 
115   /**
116    * Returns the Token of the specified kind associated with this user,
117    * or null if the Token is not present.
118    *
119    * @param kind the kind of token
120    * @param service service on which the token is supposed to be used
121    * @return the token of the specified kind.
122    */
123   public Token<?> getToken(String kind, String service) throws IOException {
124     for (Token<?> token: ugi.getTokens()) {
125       if (token.getKind().toString().equals(kind) &&
126           (service != null && token.getService().toString().equals(service)))
127       {
128         return token;
129       }
130     }
131     return null;
132   }
133 
134   @Override
135   public boolean equals(Object o) {
136     if (this == o) {
137       return true;
138     }
139     if (o == null || getClass() != o.getClass()) {
140       return false;
141     }
142     return ugi.equals(((User) o).ugi);
143   }
144 
145   @Override
146   public int hashCode() {
147     return ugi.hashCode();
148   }
149 
150   @Override
151   public String toString() {
152     return ugi.toString();
153   }
154 
155   /**
156    * Returns the {@code User} instance within current execution context.
157    */
158   public static User getCurrent() throws IOException {
159     User user = new SecureHadoopUser();
160     if (user.getUGI() == null) {
161       return null;
162     }
163     return user;
164   }
165 
166   /**
167    * Wraps an underlying {@code UserGroupInformation} instance.
168    * @param ugi The base Hadoop user
169    * @return User
170    */
171   public static User create(UserGroupInformation ugi) {
172     if (ugi == null) {
173       return null;
174     }
175     return new SecureHadoopUser(ugi);
176   }
177 
178   /**
179    * Generates a new {@code User} instance specifically for use in test code.
180    * @param name the full username
181    * @param groups the group names to which the test user will belong
182    * @return a new <code>User</code> instance
183    */
184   public static User createUserForTesting(Configuration conf,
185       String name, String[] groups) {
186     return SecureHadoopUser.createUserForTesting(conf, name, groups);
187   }
188 
189   /**
190    * Log in the current process using the given configuration keys for the
191    * credential file and login principal.
192    *
193    * <p><strong>This is only applicable when
194    * running on secure Hadoop</strong> -- see
195    * org.apache.hadoop.security.SecurityUtil#login(Configuration,String,String,String).
196    * On regular Hadoop (without security features), this will safely be ignored.
197    * </p>
198    *
199    * @param conf The configuration data to use
200    * @param fileConfKey Property key used to configure path to the credential file
201    * @param principalConfKey Property key used to configure login principal
202    * @param localhost Current hostname to use in any credentials
203    * @throws IOException underlying exception from SecurityUtil.login() call
204    */
205   public static void login(Configuration conf, String fileConfKey,
206       String principalConfKey, String localhost) throws IOException {
207     SecureHadoopUser.login(conf, fileConfKey, principalConfKey, localhost);
208   }
209 
210   /**
211    * Returns whether or not Kerberos authentication is configured for Hadoop.
212    * For non-secure Hadoop, this always returns <code>false</code>.
213    * For secure Hadoop, it will return the value from
214    * {@code UserGroupInformation.isSecurityEnabled()}.
215    */
216   public static boolean isSecurityEnabled() {
217     return SecureHadoopUser.isSecurityEnabled();
218   }
219 
220   /**
221    * Returns whether or not secure authentication is enabled for HBase. Note that
222    * HBase security requires HDFS security to provide any guarantees, so it is
223    * recommended that secure HBase should run on secure HDFS.
224    */
225   public static boolean isHBaseSecurityEnabled(Configuration conf) {
226     return "kerberos".equalsIgnoreCase(conf.get(HBASE_SECURITY_CONF_KEY));
227   }
228 
229   /* Concrete implementations */
230 
231   /**
232    * Bridges {@code User} invocations to underlying calls to
233    * {@link org.apache.hadoop.security.UserGroupInformation} for secure Hadoop
234    * 0.20 and versions 0.21 and above.
235    */
236   private static class SecureHadoopUser extends User {
237     private String shortName;
238 
239     private SecureHadoopUser() throws IOException {
240       try {
241         ugi = (UserGroupInformation) callStatic("getCurrentUser");
242       } catch (IOException ioe) {
243         throw ioe;
244       } catch (RuntimeException re) {
245         throw re;
246       } catch (Exception e) {
247         throw new UndeclaredThrowableException(e,
248             "Unexpected exception getting current secure user");
249       }
250     }
251 
252     private SecureHadoopUser(UserGroupInformation ugi) {
253       this.ugi = ugi;
254     }
255 
256     @Override
257     public String getShortName() {
258       if (shortName != null) return shortName;
259 
260       try {
261         shortName = (String)call(ugi, "getShortUserName", null, null);
262         return shortName;
263       } catch (RuntimeException re) {
264         throw re;
265       } catch (Exception e) {
266         throw new UndeclaredThrowableException(e,
267             "Unexpected error getting user short name");
268       }
269     }
270 
271     @Override
272     public <T> T runAs(PrivilegedAction<T> action) {
273       try {
274         return (T) call(ugi, "doAs", new Class[]{PrivilegedAction.class},
275             new Object[]{action});
276       } catch (RuntimeException re) {
277         throw re;
278       } catch (Exception e) {
279         throw new UndeclaredThrowableException(e,
280             "Unexpected exception in runAs()");
281       }
282     }
283 
284     @Override
285     public <T> T runAs(PrivilegedExceptionAction<T> action)
286         throws IOException, InterruptedException {
287       try {
288         return (T) call(ugi, "doAs",
289             new Class[]{PrivilegedExceptionAction.class},
290             new Object[]{action});
291       } catch (IOException ioe) {
292         throw ioe;
293       } catch (InterruptedException ie) {
294         throw ie;
295       } catch (RuntimeException re) {
296         throw re;
297       } catch (Exception e) {
298         throw new UndeclaredThrowableException(e,
299             "Unexpected exception in runAs(PrivilegedExceptionAction)");
300       }
301     }
302 
303     @Override
304     public void obtainAuthTokenForJob(Configuration conf, Job job)
305         throws IOException, InterruptedException {
306       try {
307         Class c = Class.forName(
308             "org.apache.hadoop.hbase.security.token.TokenUtil");
309         Methods.call(c, null, "obtainTokenForJob",
310             new Class[]{Configuration.class, UserGroupInformation.class,
311                 Job.class},
312             new Object[]{conf, ugi, job});
313       } catch (ClassNotFoundException cnfe) {
314         throw new RuntimeException("Failure loading TokenUtil class, "
315             +"is secure RPC available?", cnfe);
316       } catch (IOException ioe) {
317         throw ioe;
318       } catch (InterruptedException ie) {
319         throw ie;
320       } catch (RuntimeException re) {
321         throw re;
322       } catch (Exception e) {
323         throw new UndeclaredThrowableException(e,
324             "Unexpected error calling TokenUtil.obtainAndCacheToken()");
325       }
326     }
327 
328     @Override
329     public void obtainAuthTokenForJob(JobConf job)
330         throws IOException, InterruptedException {
331       try {
332         Class c = Class.forName(
333             "org.apache.hadoop.hbase.security.token.TokenUtil");
334         Methods.call(c, null, "obtainTokenForJob",
335             new Class[]{JobConf.class, UserGroupInformation.class},
336             new Object[]{job, ugi});
337       } catch (ClassNotFoundException cnfe) {
338         throw new RuntimeException("Failure loading TokenUtil class, "
339             +"is secure RPC available?", cnfe);
340       } catch (IOException ioe) {
341         throw ioe;
342       } catch (InterruptedException ie) {
343         throw ie;
344       } catch (RuntimeException re) {
345         throw re;
346       } catch (Exception e) {
347         throw new UndeclaredThrowableException(e,
348             "Unexpected error calling TokenUtil.obtainAndCacheToken()");
349       }
350     }
351 
352     /** @see User#createUserForTesting(org.apache.hadoop.conf.Configuration, String, String[]) */
353     public static User createUserForTesting(Configuration conf,
354         String name, String[] groups) {
355       try {
356         return new SecureHadoopUser(
357             (UserGroupInformation)callStatic("createUserForTesting",
358                 new Class[]{String.class, String[].class},
359                 new Object[]{name, groups})
360         );
361       } catch (RuntimeException re) {
362         throw re;
363       } catch (Exception e) {
364         throw new UndeclaredThrowableException(e,
365             "Error creating secure test user");
366       }
367     }
368 
369     /**
370      * Obtain credentials for the current process using the configured
371      * Kerberos keytab file and principal.
372      * @see User#login(org.apache.hadoop.conf.Configuration, String, String, String)
373      *
374      * @param conf the Configuration to use
375      * @param fileConfKey Configuration property key used to store the path
376      * to the keytab file
377      * @param principalConfKey Configuration property key used to store the
378      * principal name to login as
379      * @param localhost the local hostname
380      */
381     public static void login(Configuration conf, String fileConfKey,
382         String principalConfKey, String localhost) throws IOException {
383       if (isSecurityEnabled()) {
384         // check for SecurityUtil class
385         try {
386           Class c = Class.forName("org.apache.hadoop.security.SecurityUtil");
387           Class[] types = new Class[]{
388               Configuration.class, String.class, String.class, String.class };
389           Object[] args = new Object[]{
390               conf, fileConfKey, principalConfKey, localhost };
391           Methods.call(c, null, "login", types, args);
392         } catch (ClassNotFoundException cnfe) {
393           throw new RuntimeException("Unable to login using " +
394               "org.apache.hadoop.security.SecurityUtil.login(). SecurityUtil class " +
395               "was not found!  Is this a version of secure Hadoop?", cnfe);
396         } catch (IOException ioe) {
397           throw ioe;
398         } catch (RuntimeException re) {
399           throw re;
400         } catch (Exception e) {
401           throw new UndeclaredThrowableException(e,
402               "Unhandled exception in User.login()");
403         }
404       }
405     }
406 
407     /**
408      * Returns the result of {@code UserGroupInformation.isSecurityEnabled()}.
409      */
410     public static boolean isSecurityEnabled() {
411       try {
412         return (Boolean)callStatic("isSecurityEnabled");
413       } catch (RuntimeException re) {
414         throw re;
415       } catch (Exception e) {
416         throw new UndeclaredThrowableException(e,
417             "Unexpected exception calling UserGroupInformation.isSecurityEnabled()");
418       }
419     }
420   }
421 
422   /* Reflection helper methods */
423   private static Object callStatic(String methodName) throws Exception {
424     return call(null, methodName, null, null);
425   }
426 
427   private static Object callStatic(String methodName, Class[] types,
428       Object[] args) throws Exception {
429     return call(null, methodName, types, args);
430   }
431 
432   private static Object call(UserGroupInformation instance, String methodName,
433       Class[] types, Object[] args) throws Exception {
434     return Methods.call(UserGroupInformation.class, instance, methodName, types,
435         args);
436   }
437 }