001/* 002 * Licensed to the Apache Software Foundation (ASF) under one 003 * or more contributor license agreements. See the NOTICE file 004 * distributed with this work for additional information 005 * regarding copyright ownership. The ASF licenses this file 006 * to you under the Apache License, Version 2.0 (the 007 * "License"); you may not use this file except in compliance 008 * with the License. You may obtain a copy of the License at 009 * 010 * http://www.apache.org/licenses/LICENSE-2.0 011 * 012 * Unless required by applicable law or agreed to in writing, software 013 * distributed under the License is distributed on an "AS IS" BASIS, 014 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 015 * See the License for the specific language governing permissions and 016 * limitations under the License. 017 */ 018package org.apache.hadoop.hbase.security; 019 020import java.io.IOException; 021import java.security.PrivilegedAction; 022import java.security.PrivilegedExceptionAction; 023import java.util.Arrays; 024import java.util.Collection; 025import java.util.HashMap; 026import java.util.List; 027import java.util.Map; 028import java.util.Optional; 029import java.util.concurrent.ExecutionException; 030import org.apache.hadoop.conf.Configuration; 031import org.apache.hadoop.hbase.AuthUtil; 032import org.apache.hadoop.hbase.util.Methods; 033import org.apache.hadoop.security.Groups; 034import org.apache.hadoop.security.SecurityUtil; 035import org.apache.hadoop.security.UserGroupInformation; 036import org.apache.hadoop.security.token.Token; 037import org.apache.hadoop.security.token.TokenIdentifier; 038import org.apache.yetus.audience.InterfaceAudience; 039 040import org.apache.hbase.thirdparty.com.google.common.cache.LoadingCache; 041 042/** 043 * Wrapper to abstract out usage of user and group information in HBase. 044 * <p> 045 * This class provides a common interface for interacting with user and group information across 046 * changing APIs in different versions of Hadoop. It only provides access to the common set of 047 * functionality in {@link org.apache.hadoop.security.UserGroupInformation} currently needed by 048 * HBase, but can be extended as needs change. 049 * </p> 050 */ 051@InterfaceAudience.Public 052public abstract class User { 053 public static final String HBASE_SECURITY_CONF_KEY = "hbase.security.authentication"; 054 public static final String HBASE_SECURITY_AUTHORIZATION_CONF_KEY = "hbase.security.authorization"; 055 056 protected UserGroupInformation ugi; 057 058 public UserGroupInformation getUGI() { 059 return ugi; 060 } 061 062 /** 063 * Returns the full user name. For Kerberos principals this will include the host and realm 064 * portions of the principal name. 065 * @return User full name. 066 */ 067 public String getName() { 068 return ugi.getUserName(); 069 } 070 071 /** 072 * Returns the list of groups of which this user is a member. On secure Hadoop this returns the 073 * group information for the user as resolved on the server. For 0.20 based Hadoop, the group 074 * names are passed from the client. 075 */ 076 public String[] getGroupNames() { 077 return ugi.getGroupNames(); 078 } 079 080 /** 081 * Returns the shortened version of the user name -- the portion that maps to an operating system 082 * user name. 083 * @return Short name 084 */ 085 public abstract String getShortName(); 086 087 /** 088 * Executes the given action within the context of this user. 089 */ 090 public abstract <T> T runAs(PrivilegedAction<T> action); 091 092 /** 093 * Executes the given action within the context of this user. 094 */ 095 public abstract <T> T runAs(PrivilegedExceptionAction<T> action) 096 throws IOException, InterruptedException; 097 098 /** 099 * Returns the Token of the specified kind associated with this user, or null if the Token is not 100 * present. 101 * @param kind the kind of token 102 * @param service service on which the token is supposed to be used 103 * @return the token of the specified kind. 104 */ 105 public Token<?> getToken(String kind, String service) throws IOException { 106 for (Token<?> token : ugi.getTokens()) { 107 if ( 108 token.getKind().toString().equals(kind) 109 && (service != null && token.getService().toString().equals(service)) 110 ) { 111 return token; 112 } 113 } 114 return null; 115 } 116 117 /** 118 * Returns all the tokens stored in the user's credentials. 119 */ 120 public Collection<Token<? extends TokenIdentifier>> getTokens() { 121 return ugi.getTokens(); 122 } 123 124 /** 125 * Adds the given Token to the user's credentials. 126 * @param token the token to add 127 */ 128 public void addToken(Token<? extends TokenIdentifier> token) { 129 ugi.addToken(token); 130 } 131 132 /** Returns true if user credentials are obtained from keytab. */ 133 public boolean isLoginFromKeytab() { 134 return ugi.isFromKeytab(); 135 } 136 137 @Override 138 public boolean equals(Object o) { 139 if (this == o) { 140 return true; 141 } 142 if (!(o instanceof User)) { 143 return false; 144 } 145 return ugi.equals(((User) o).ugi); 146 } 147 148 @Override 149 public int hashCode() { 150 return ugi.hashCode(); 151 } 152 153 @Override 154 public String toString() { 155 return ugi.toString(); 156 } 157 158 /** Returns the {@code User} instance within current execution context. */ 159 public static User getCurrent() throws IOException { 160 User user = new SecureHadoopUser(); 161 if (user.getUGI() == null) { 162 return null; 163 } 164 return user; 165 } 166 167 /** Executes the given action as the login user */ 168 @SuppressWarnings({ "rawtypes", "unchecked" }) 169 public static <T> T runAsLoginUser(PrivilegedExceptionAction<T> action) throws IOException { 170 try { 171 Class c = Class.forName("org.apache.hadoop.security.SecurityUtil"); 172 Class[] types = new Class[] { PrivilegedExceptionAction.class }; 173 Object[] args = new Object[] { action }; 174 return (T) Methods.call(c, null, "doAsLoginUser", types, args); 175 } catch (Throwable e) { 176 throw new IOException(e); 177 } 178 } 179 180 /** 181 * Wraps an underlying {@code UserGroupInformation} instance. 182 * @param ugi The base Hadoop user 183 */ 184 public static User create(UserGroupInformation ugi) { 185 if (ugi == null) { 186 return null; 187 } 188 return new SecureHadoopUser(ugi); 189 } 190 191 /** 192 * Generates a new {@code User} instance specifically for use in test code. 193 * @param name the full username 194 * @param groups the group names to which the test user will belong 195 * @return a new <code>User</code> instance 196 */ 197 public static User createUserForTesting(Configuration conf, String name, String[] groups) { 198 User userForTesting = SecureHadoopUser.createUserForTesting(conf, name, groups); 199 return userForTesting; 200 } 201 202 /** 203 * Log in the current process using the given configuration keys for the credential file and login 204 * principal. 205 * <p> 206 * <strong>This is only applicable when running on secure Hadoop</strong> -- see 207 * org.apache.hadoop.security.SecurityUtil#login(Configuration,String,String,String). On regular 208 * Hadoop (without security features), this will safely be ignored. 209 * </p> 210 * @param conf The configuration data to use 211 * @param fileConfKey Property key used to configure path to the credential file 212 * @param principalConfKey Property key used to configure login principal 213 * @param localhost Current hostname to use in any credentials 214 * @throws IOException underlying exception from SecurityUtil.login() call 215 */ 216 public static void login(Configuration conf, String fileConfKey, String principalConfKey, 217 String localhost) throws IOException { 218 SecureHadoopUser.login(conf, fileConfKey, principalConfKey, localhost); 219 } 220 221 /** 222 * Login with the given keytab and principal. 223 * @param keytabLocation path of keytab 224 * @param pricipalName login principal 225 * @throws IOException underlying exception from UserGroupInformation.loginUserFromKeytab 226 */ 227 public static void login(String keytabLocation, String pricipalName) throws IOException { 228 SecureHadoopUser.login(keytabLocation, pricipalName); 229 } 230 231 /** 232 * Returns whether or not Kerberos authentication is configured for Hadoop. For non-secure Hadoop, 233 * this always returns <code>false</code>. For secure Hadoop, it will return the value from 234 * {@code UserGroupInformation.isSecurityEnabled()}. 235 */ 236 public static boolean isSecurityEnabled() { 237 return SecureHadoopUser.isSecurityEnabled(); 238 } 239 240 /** 241 * Returns whether or not secure authentication is enabled for HBase. Note that HBase security 242 * requires HDFS security to provide any guarantees, so it is recommended that secure HBase should 243 * run on secure HDFS. 244 */ 245 public static boolean isHBaseSecurityEnabled(Configuration conf) { 246 return "kerberos".equalsIgnoreCase(conf.get(HBASE_SECURITY_CONF_KEY)); 247 } 248 249 /** 250 * In secure environment, if a user specified his keytab and principal, a hbase client will try to 251 * login with them. Otherwise, hbase client will try to obtain ticket(through kinit) from system. 252 * @param conf configuration file 253 * @return true if keytab and principal are configured 254 */ 255 public static boolean shouldLoginFromKeytab(Configuration conf) { 256 Optional<String> keytab = Optional.ofNullable(conf.get(AuthUtil.HBASE_CLIENT_KEYTAB_FILE)); 257 Optional<String> principal = 258 Optional.ofNullable(conf.get(AuthUtil.HBASE_CLIENT_KERBEROS_PRINCIPAL)); 259 return keytab.isPresent() && principal.isPresent(); 260 } 261 262 /* Concrete implementations */ 263 264 /** 265 * Bridges {@code User} invocations to underlying calls to 266 * {@link org.apache.hadoop.security.UserGroupInformation} for secure Hadoop 0.20 and versions 267 * 0.21 and above. 268 */ 269 @InterfaceAudience.Private 270 public static final class SecureHadoopUser extends User { 271 private String shortName; 272 private LoadingCache<String, String[]> cache; 273 /** 274 * Cache value of this instance's {@link #toString()} value. Computing this value is expensive. 275 * Assumes the UGI is never updated. See HBASE-27708. 276 */ 277 private final String toString; 278 279 public SecureHadoopUser() throws IOException { 280 ugi = UserGroupInformation.getCurrentUser(); 281 this.cache = null; 282 this.toString = ugi.toString(); 283 } 284 285 public SecureHadoopUser(UserGroupInformation ugi) { 286 this.ugi = ugi; 287 this.cache = null; 288 this.toString = ugi.toString(); 289 } 290 291 public SecureHadoopUser(UserGroupInformation ugi, LoadingCache<String, String[]> cache) { 292 this.ugi = ugi; 293 this.cache = cache; 294 this.toString = ugi.toString(); 295 } 296 297 @Override 298 public String getShortName() { 299 if (shortName != null) return shortName; 300 try { 301 shortName = ugi.getShortUserName(); 302 return shortName; 303 } catch (Exception e) { 304 throw new RuntimeException("Unexpected error getting user short name", e); 305 } 306 } 307 308 @Override 309 public String[] getGroupNames() { 310 if (cache != null) { 311 try { 312 return this.cache.get(getShortName()); 313 } catch (ExecutionException e) { 314 return new String[0]; 315 } 316 } 317 return ugi.getGroupNames(); 318 } 319 320 @Override 321 public <T> T runAs(PrivilegedAction<T> action) { 322 return ugi.doAs(action); 323 } 324 325 @Override 326 public <T> T runAs(PrivilegedExceptionAction<T> action) 327 throws IOException, InterruptedException { 328 return ugi.doAs(action); 329 } 330 331 @Override 332 public String toString() { 333 return toString; 334 } 335 336 /** 337 * Create a user for testing. 338 * @see User#createUserForTesting(org.apache.hadoop.conf.Configuration, String, String[]) 339 */ 340 public static User createUserForTesting(Configuration conf, String name, String[] groups) { 341 synchronized (UserProvider.class) { 342 if (!(UserProvider.groups instanceof TestingGroups)) { 343 UserProvider.groups = new TestingGroups(UserProvider.groups); 344 } 345 } 346 347 ((TestingGroups) UserProvider.groups).setUserGroups(name, groups); 348 return new SecureHadoopUser(UserGroupInformation.createUserForTesting(name, groups)); 349 } 350 351 /** 352 * Obtain credentials for the current process using the configured Kerberos keytab file and 353 * principal. 354 * @see User#login(org.apache.hadoop.conf.Configuration, String, String, String) 355 * @param conf the Configuration to use 356 * @param fileConfKey Configuration property key used to store the path to the keytab file 357 * @param principalConfKey Configuration property key used to store the principal name to login 358 * as 359 * @param localhost the local hostname 360 */ 361 public static void login(Configuration conf, String fileConfKey, String principalConfKey, 362 String localhost) throws IOException { 363 if (isSecurityEnabled()) { 364 SecurityUtil.login(conf, fileConfKey, principalConfKey, localhost); 365 } 366 } 367 368 /** 369 * Login through configured keytab and pricipal. 370 * @param keytabLocation location of keytab 371 * @param principalName principal in keytab 372 * @throws IOException exception from UserGroupInformation.loginUserFromKeytab 373 */ 374 public static void login(String keytabLocation, String principalName) throws IOException { 375 if (isSecurityEnabled()) { 376 UserGroupInformation.loginUserFromKeytab(principalName, keytabLocation); 377 } 378 } 379 380 /** Returns the result of {@code UserGroupInformation.isSecurityEnabled()}. */ 381 public static boolean isSecurityEnabled() { 382 return UserGroupInformation.isSecurityEnabled(); 383 } 384 } 385 386 public static class TestingGroups extends Groups { 387 public static final String TEST_CONF = "hbase.group.service.for.test.only"; 388 389 private final Map<String, List<String>> userToGroupsMapping = new HashMap<>(); 390 private Groups underlyingImplementation; 391 392 public TestingGroups(Groups underlyingImplementation) { 393 super(new Configuration()); 394 this.underlyingImplementation = underlyingImplementation; 395 } 396 397 @Override 398 public List<String> getGroups(String user) throws IOException { 399 List<String> result = userToGroupsMapping.get(user); 400 401 if (result == null) { 402 result = underlyingImplementation.getGroups(user); 403 } 404 405 return result; 406 } 407 408 private void setUserGroups(String user, String[] groups) { 409 userToGroupsMapping.put(user, Arrays.asList(groups)); 410 } 411 } 412}