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.regionserver; 019 020import java.io.IOException; 021import java.math.BigInteger; 022import java.security.PrivilegedAction; 023import java.security.SecureRandom; 024import java.util.ArrayList; 025import java.util.HashMap; 026import java.util.List; 027import java.util.Map; 028 029import org.apache.hadoop.conf.Configuration; 030import org.apache.hadoop.fs.FileStatus; 031import org.apache.hadoop.fs.FileSystem; 032import org.apache.hadoop.fs.FileUtil; 033import org.apache.hadoop.fs.Path; 034import org.apache.hadoop.fs.permission.FsPermission; 035import org.apache.hadoop.hbase.DoNotRetryIOException; 036import org.apache.hadoop.hbase.HConstants; 037import org.apache.hadoop.hbase.TableName; 038import org.apache.hadoop.hbase.client.Connection; 039import org.apache.hadoop.hbase.ipc.RpcServer; 040import org.apache.hadoop.hbase.regionserver.HRegion.BulkLoadListener; 041import org.apache.hadoop.hbase.security.User; 042import org.apache.hadoop.hbase.security.UserProvider; 043import org.apache.hadoop.hbase.security.token.FsDelegationToken; 044import org.apache.hadoop.hbase.security.token.TokenUtil; 045import org.apache.hadoop.hbase.util.Bytes; 046import org.apache.hadoop.hbase.util.FSHDFSUtils; 047import org.apache.hadoop.hbase.util.FSUtils; 048import org.apache.hadoop.hbase.util.Methods; 049import org.apache.hadoop.hbase.util.Pair; 050import org.apache.hadoop.io.Text; 051import org.apache.hadoop.security.UserGroupInformation; 052import org.apache.hadoop.security.token.Token; 053import org.apache.yetus.audience.InterfaceAudience; 054import org.slf4j.Logger; 055import org.slf4j.LoggerFactory; 056import org.apache.hadoop.hbase.shaded.protobuf.generated.ClientProtos; 057import org.apache.hadoop.hbase.shaded.protobuf.generated.ClientProtos.BulkLoadHFileRequest; 058import org.apache.hadoop.hbase.shaded.protobuf.generated.ClientProtos.CleanupBulkLoadRequest; 059import org.apache.hadoop.hbase.shaded.protobuf.generated.ClientProtos.PrepareBulkLoadRequest; 060 061/** 062 * Bulk loads in secure mode. 063 * 064 * This service addresses two issues: 065 * <ol> 066 * <li>Moving files in a secure filesystem wherein the HBase Client 067 * and HBase Server are different filesystem users.</li> 068 * <li>Does moving in a secure manner. Assuming that the filesystem 069 * is POSIX compliant.</li> 070 * </ol> 071 * 072 * The algorithm is as follows: 073 * <ol> 074 * <li>Create an hbase owned staging directory which is 075 * world traversable (711): {@code /hbase/staging}</li> 076 * <li>A user writes out data to his secure output directory: {@code /user/foo/data}</li> 077 * <li>A call is made to hbase to create a secret staging directory 078 * which globally rwx (777): {@code /user/staging/averylongandrandomdirectoryname}</li> 079 * <li>The user moves the data into the random staging directory, 080 * then calls bulkLoadHFiles()</li> 081 * </ol> 082 * 083 * Like delegation tokens the strength of the security lies in the length 084 * and randomness of the secret directory. 085 * 086 */ 087@InterfaceAudience.Private 088public class SecureBulkLoadManager { 089 090 public static final long VERSION = 0L; 091 092 //320/5 = 64 characters 093 private static final int RANDOM_WIDTH = 320; 094 private static final int RANDOM_RADIX = 32; 095 096 private static final Logger LOG = LoggerFactory.getLogger(SecureBulkLoadManager.class); 097 098 private final static FsPermission PERM_ALL_ACCESS = FsPermission.valueOf("-rwxrwxrwx"); 099 private final static FsPermission PERM_HIDDEN = FsPermission.valueOf("-rwx--x--x"); 100 private SecureRandom random; 101 private FileSystem fs; 102 private Configuration conf; 103 104 //two levels so it doesn't get deleted accidentally 105 //no sticky bit in Hadoop 1.0 106 private Path baseStagingDir; 107 108 private UserProvider userProvider; 109 private Connection conn; 110 111 SecureBulkLoadManager(Configuration conf, Connection conn) { 112 this.conf = conf; 113 this.conn = conn; 114 } 115 116 public void start() throws IOException { 117 random = new SecureRandom(); 118 userProvider = UserProvider.instantiate(conf); 119 fs = FileSystem.get(conf); 120 baseStagingDir = new Path(FSUtils.getRootDir(conf), HConstants.BULKLOAD_STAGING_DIR_NAME); 121 122 if (conf.get("hbase.bulkload.staging.dir") != null) { 123 LOG.warn("hbase.bulkload.staging.dir " + " is deprecated. Bulkload staging directory is " 124 + baseStagingDir); 125 } 126 if (!fs.exists(baseStagingDir)) { 127 fs.mkdirs(baseStagingDir, PERM_HIDDEN); 128 } 129 } 130 131 public void stop() throws IOException { 132 } 133 134 public String prepareBulkLoad(final HRegion region, final PrepareBulkLoadRequest request) 135 throws IOException { 136 User user = getActiveUser(); 137 region.getCoprocessorHost().prePrepareBulkLoad(user); 138 139 String bulkToken = 140 createStagingDir(baseStagingDir, user, region.getTableDescriptor().getTableName()) 141 .toString(); 142 143 return bulkToken; 144 } 145 146 public void cleanupBulkLoad(final HRegion region, final CleanupBulkLoadRequest request) 147 throws IOException { 148 try { 149 region.getCoprocessorHost().preCleanupBulkLoad(getActiveUser()); 150 151 Path path = new Path(request.getBulkToken()); 152 if (!fs.delete(path, true)) { 153 if (fs.exists(path)) { 154 throw new IOException("Failed to clean up " + path); 155 } 156 } 157 LOG.info("Cleaned up " + path + " successfully."); 158 } finally { 159 UserGroupInformation ugi = getActiveUser().getUGI(); 160 try { 161 if (!UserGroupInformation.getLoginUser().equals(ugi)) { 162 FileSystem.closeAllForUGI(ugi); 163 } 164 } catch (IOException e) { 165 LOG.error("Failed to close FileSystem for: " + ugi, e); 166 } 167 } 168 } 169 170 public Map<byte[], List<Path>> secureBulkLoadHFiles(final HRegion region, 171 final BulkLoadHFileRequest request) throws IOException { 172 final List<Pair<byte[], String>> familyPaths = new ArrayList<>(request.getFamilyPathCount()); 173 for(ClientProtos.BulkLoadHFileRequest.FamilyPath el : request.getFamilyPathList()) { 174 familyPaths.add(new Pair<>(el.getFamily().toByteArray(), el.getPath())); 175 } 176 177 Token userToken = null; 178 if (userProvider.isHadoopSecurityEnabled()) { 179 userToken = new Token(request.getFsToken().getIdentifier().toByteArray(), request.getFsToken() 180 .getPassword().toByteArray(), new Text(request.getFsToken().getKind()), new Text( 181 request.getFsToken().getService())); 182 } 183 final String bulkToken = request.getBulkToken(); 184 User user = getActiveUser(); 185 final UserGroupInformation ugi = user.getUGI(); 186 if (userProvider.isHadoopSecurityEnabled()) { 187 try { 188 Token tok = TokenUtil.obtainToken(conn); 189 if (tok != null) { 190 boolean b = ugi.addToken(tok); 191 LOG.debug("token added " + tok + " for user " + ugi + " return=" + b); 192 } 193 } catch (IOException ioe) { 194 LOG.warn("unable to add token", ioe); 195 } 196 } 197 if (userToken != null) { 198 ugi.addToken(userToken); 199 } else if (userProvider.isHadoopSecurityEnabled()) { 200 //we allow this to pass through in "simple" security mode 201 //for mini cluster testing 202 throw new DoNotRetryIOException("User token cannot be null"); 203 } 204 205 if (region.getCoprocessorHost() != null) { 206 region.getCoprocessorHost().preBulkLoadHFile(familyPaths); 207 } 208 Map<byte[], List<Path>> map = null; 209 210 try { 211 // Get the target fs (HBase region server fs) delegation token 212 // Since we have checked the permission via 'preBulkLoadHFile', now let's give 213 // the 'request user' necessary token to operate on the target fs. 214 // After this point the 'doAs' user will hold two tokens, one for the source fs 215 // ('request user'), another for the target fs (HBase region server principal). 216 if (userProvider.isHadoopSecurityEnabled()) { 217 FsDelegationToken targetfsDelegationToken = new FsDelegationToken(userProvider,"renewer"); 218 targetfsDelegationToken.acquireDelegationToken(fs); 219 220 Token<?> targetFsToken = targetfsDelegationToken.getUserToken(); 221 if (targetFsToken != null 222 && (userToken == null || !targetFsToken.getService().equals(userToken.getService()))){ 223 ugi.addToken(targetFsToken); 224 } 225 } 226 227 map = ugi.doAs(new PrivilegedAction<Map<byte[], List<Path>>>() { 228 @Override 229 public Map<byte[], List<Path>> run() { 230 FileSystem fs = null; 231 try { 232 fs = FileSystem.get(conf); 233 for(Pair<byte[], String> el: familyPaths) { 234 Path stageFamily = new Path(bulkToken, Bytes.toString(el.getFirst())); 235 if(!fs.exists(stageFamily)) { 236 fs.mkdirs(stageFamily); 237 fs.setPermission(stageFamily, PERM_ALL_ACCESS); 238 } 239 } 240 //We call bulkLoadHFiles as requesting user 241 //To enable access prior to staging 242 return region.bulkLoadHFiles(familyPaths, true, 243 new SecureBulkLoadListener(fs, bulkToken, conf), request.getCopyFile()); 244 } catch (Exception e) { 245 LOG.error("Failed to complete bulk load", e); 246 } 247 return null; 248 } 249 }); 250 } finally { 251 if (region.getCoprocessorHost() != null) { 252 region.getCoprocessorHost().postBulkLoadHFile(familyPaths, map); 253 } 254 } 255 return map; 256 } 257 258 private Path createStagingDir(Path baseDir, 259 User user, 260 TableName tableName) throws IOException { 261 String tblName = tableName.getNameAsString().replace(":", "_"); 262 String randomDir = user.getShortName()+"__"+ tblName +"__"+ 263 (new BigInteger(RANDOM_WIDTH, random).toString(RANDOM_RADIX)); 264 return createStagingDir(baseDir, user, randomDir); 265 } 266 267 private Path createStagingDir(Path baseDir, 268 User user, 269 String randomDir) throws IOException { 270 Path p = new Path(baseDir, randomDir); 271 fs.mkdirs(p, PERM_ALL_ACCESS); 272 fs.setPermission(p, PERM_ALL_ACCESS); 273 return p; 274 } 275 276 private User getActiveUser() throws IOException { 277 // for non-rpc handling, fallback to system user 278 User user = RpcServer.getRequestUser().orElse(userProvider.getCurrent()); 279 // this is for testing 280 if (userProvider.isHadoopSecurityEnabled() && 281 "simple".equalsIgnoreCase(conf.get(User.HBASE_SECURITY_CONF_KEY))) { 282 return User.createUserForTesting(conf, user.getShortName(), new String[] {}); 283 } 284 285 return user; 286 } 287 288 private static class SecureBulkLoadListener implements BulkLoadListener { 289 // Target filesystem 290 private final FileSystem fs; 291 private final String stagingDir; 292 private final Configuration conf; 293 // Source filesystem 294 private FileSystem srcFs = null; 295 private Map<String, FsPermission> origPermissions = null; 296 297 public SecureBulkLoadListener(FileSystem fs, String stagingDir, Configuration conf) { 298 this.fs = fs; 299 this.stagingDir = stagingDir; 300 this.conf = conf; 301 this.origPermissions = new HashMap<>(); 302 } 303 304 @Override 305 public String prepareBulkLoad(final byte[] family, final String srcPath, boolean copyFile) 306 throws IOException { 307 Path p = new Path(srcPath); 308 Path stageP = new Path(stagingDir, new Path(Bytes.toString(family), p.getName())); 309 310 // In case of Replication for bulk load files, hfiles are already copied in staging directory 311 if (p.equals(stageP)) { 312 LOG.debug(p.getName() 313 + " is already available in staging directory. Skipping copy or rename."); 314 return stageP.toString(); 315 } 316 317 if (srcFs == null) { 318 srcFs = FileSystem.newInstance(p.toUri(), conf); 319 } 320 321 if(!isFile(p)) { 322 throw new IOException("Path does not reference a file: " + p); 323 } 324 325 // Check to see if the source and target filesystems are the same 326 if (!FSHDFSUtils.isSameHdfs(conf, srcFs, fs)) { 327 LOG.debug("Bulk-load file " + srcPath + " is on different filesystem than " + 328 "the destination filesystem. Copying file over to destination staging dir."); 329 FileUtil.copy(srcFs, p, fs, stageP, false, conf); 330 } else if (copyFile) { 331 LOG.debug("Bulk-load file " + srcPath + " is copied to destination staging dir."); 332 FileUtil.copy(srcFs, p, fs, stageP, false, conf); 333 } else { 334 LOG.debug("Moving " + p + " to " + stageP); 335 FileStatus origFileStatus = fs.getFileStatus(p); 336 origPermissions.put(srcPath, origFileStatus.getPermission()); 337 if(!fs.rename(p, stageP)) { 338 throw new IOException("Failed to move HFile: " + p + " to " + stageP); 339 } 340 } 341 fs.setPermission(stageP, PERM_ALL_ACCESS); 342 return stageP.toString(); 343 } 344 345 @Override 346 public void doneBulkLoad(byte[] family, String srcPath) throws IOException { 347 LOG.debug("Bulk Load done for: " + srcPath); 348 closeSrcFs(); 349 } 350 351 private void closeSrcFs() throws IOException { 352 if (srcFs != null) { 353 srcFs.close(); 354 srcFs = null; 355 } 356 } 357 358 @Override 359 public void failedBulkLoad(final byte[] family, final String srcPath) throws IOException { 360 try { 361 Path p = new Path(srcPath); 362 if (srcFs == null) { 363 srcFs = FileSystem.newInstance(p.toUri(), conf); 364 } 365 if (!FSHDFSUtils.isSameHdfs(conf, srcFs, fs)) { 366 // files are copied so no need to move them back 367 return; 368 } 369 Path stageP = new Path(stagingDir, new Path(Bytes.toString(family), p.getName())); 370 371 // In case of Replication for bulk load files, hfiles are not renamed by end point during 372 // prepare stage, so no need of rename here again 373 if (p.equals(stageP)) { 374 LOG.debug(p.getName() + " is already available in source directory. Skipping rename."); 375 return; 376 } 377 378 LOG.debug("Moving " + stageP + " back to " + p); 379 if (!fs.rename(stageP, p)) { 380 throw new IOException("Failed to move HFile: " + stageP + " to " + p); 381 } 382 383 // restore original permission 384 if (origPermissions.containsKey(srcPath)) { 385 fs.setPermission(p, origPermissions.get(srcPath)); 386 } else { 387 LOG.warn("Can't find previous permission for path=" + srcPath); 388 } 389 } finally { 390 closeSrcFs(); 391 } 392 } 393 394 /** 395 * Check if the path is referencing a file. 396 * This is mainly needed to avoid symlinks. 397 * @param p 398 * @return true if the p is a file 399 * @throws IOException 400 */ 401 private boolean isFile(Path p) throws IOException { 402 FileStatus status = srcFs.getFileStatus(p); 403 boolean isFile = !status.isDirectory(); 404 try { 405 isFile = isFile && !(Boolean)Methods.call(FileStatus.class, status, "isSymlink", null, null); 406 } catch (Exception e) { 407 } 408 return isFile; 409 } 410 } 411}