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.coprocessor; 019 020import static org.junit.Assert.assertEquals; 021 022import com.google.protobuf.ServiceException; 023import java.io.File; 024import java.io.IOException; 025import java.security.PrivilegedExceptionAction; 026import java.util.Arrays; 027import java.util.LinkedList; 028import java.util.List; 029import java.util.Map; 030import org.apache.hadoop.conf.Configuration; 031import org.apache.hadoop.fs.FileStatus; 032import org.apache.hadoop.fs.FileSystem; 033import org.apache.hadoop.fs.Path; 034import org.apache.hadoop.fs.permission.FsAction; 035import org.apache.hadoop.fs.permission.FsPermission; 036import org.apache.hadoop.hbase.HBaseClassTestRule; 037import org.apache.hadoop.hbase.HBaseTestingUtility; 038import org.apache.hadoop.hbase.TableName; 039import org.apache.hadoop.hbase.client.ColumnFamilyDescriptorBuilder; 040import org.apache.hadoop.hbase.client.Connection; 041import org.apache.hadoop.hbase.client.ConnectionFactory; 042import org.apache.hadoop.hbase.client.Put; 043import org.apache.hadoop.hbase.client.Result; 044import org.apache.hadoop.hbase.client.ResultScanner; 045import org.apache.hadoop.hbase.client.Scan; 046import org.apache.hadoop.hbase.client.Table; 047import org.apache.hadoop.hbase.client.TableDescriptor; 048import org.apache.hadoop.hbase.client.TableDescriptorBuilder; 049import org.apache.hadoop.hbase.mapreduce.ExportUtils; 050import org.apache.hadoop.hbase.mapreduce.Import; 051import org.apache.hadoop.hbase.protobuf.generated.VisibilityLabelsProtos; 052import org.apache.hadoop.hbase.security.HBaseKerberosUtils; 053import org.apache.hadoop.hbase.security.HadoopSecurityEnabledUserProviderForTesting; 054import org.apache.hadoop.hbase.security.User; 055import org.apache.hadoop.hbase.security.UserProvider; 056import org.apache.hadoop.hbase.security.access.AccessControlConstants; 057import org.apache.hadoop.hbase.security.access.AccessControlLists; 058import org.apache.hadoop.hbase.security.access.Permission; 059import org.apache.hadoop.hbase.security.access.SecureTestUtil; 060import org.apache.hadoop.hbase.security.access.SecureTestUtil.AccessTestAction; 061import org.apache.hadoop.hbase.security.visibility.Authorizations; 062import org.apache.hadoop.hbase.security.visibility.CellVisibility; 063import org.apache.hadoop.hbase.security.visibility.VisibilityClient; 064import org.apache.hadoop.hbase.security.visibility.VisibilityConstants; 065import org.apache.hadoop.hbase.security.visibility.VisibilityTestUtil; 066import org.apache.hadoop.hbase.testclassification.MediumTests; 067import org.apache.hadoop.hbase.util.Bytes; 068import org.apache.hadoop.hbase.util.Pair; 069import org.apache.hadoop.minikdc.MiniKdc; 070import org.apache.hadoop.security.UserGroupInformation; 071import org.apache.hadoop.util.ToolRunner; 072import org.junit.After; 073import org.junit.AfterClass; 074import org.junit.Before; 075import org.junit.BeforeClass; 076import org.junit.ClassRule; 077import org.junit.Rule; 078import org.junit.Test; 079import org.junit.experimental.categories.Category; 080import org.junit.rules.TestName; 081import org.slf4j.Logger; 082import org.slf4j.LoggerFactory; 083 084@Category({MediumTests.class}) 085public class TestSecureExport { 086 @ClassRule 087 public static final HBaseClassTestRule CLASS_RULE = 088 HBaseClassTestRule.forClass(TestSecureExport.class); 089 090 private static final Logger LOG = LoggerFactory.getLogger(TestSecureExport.class); 091 private static final HBaseTestingUtility UTIL = new HBaseTestingUtility(); 092 private static MiniKdc KDC; 093 private static final File KEYTAB_FILE = new File(UTIL.getDataTestDir("keytab").toUri().getPath()); 094 private static String USERNAME; 095 private static String SERVER_PRINCIPAL; 096 private static String HTTP_PRINCIPAL; 097 private static final String FAMILYA_STRING = "fma"; 098 private static final String FAMILYB_STRING = "fma"; 099 private static final byte[] FAMILYA = Bytes.toBytes(FAMILYA_STRING); 100 private static final byte[] FAMILYB = Bytes.toBytes(FAMILYB_STRING); 101 private static final byte[] ROW1 = Bytes.toBytes("row1"); 102 private static final byte[] ROW2 = Bytes.toBytes("row2"); 103 private static final byte[] ROW3 = Bytes.toBytes("row3"); 104 private static final byte[] QUAL = Bytes.toBytes("qual"); 105 private static final String LOCALHOST = "localhost"; 106 private static final long NOW = System.currentTimeMillis(); 107 // user granted with all global permission 108 private static final String USER_ADMIN = "admin"; 109 // user is table owner. will have all permissions on table 110 private static final String USER_OWNER = "owner"; 111 // user with rx permissions. 112 private static final String USER_RX = "rxuser"; 113 // user with exe-only permissions. 114 private static final String USER_XO = "xouser"; 115 // user with read-only permissions. 116 private static final String USER_RO = "rouser"; 117 // user with no permissions 118 private static final String USER_NONE = "noneuser"; 119 private static final String PRIVATE = "private"; 120 private static final String CONFIDENTIAL = "confidential"; 121 private static final String SECRET = "secret"; 122 private static final String TOPSECRET = "topsecret"; 123 @Rule 124 public final TestName name = new TestName(); 125 private static void setUpKdcServer() throws Exception { 126 KDC = UTIL.setupMiniKdc(KEYTAB_FILE); 127 USERNAME = UserGroupInformation.getLoginUser().getShortUserName(); 128 SERVER_PRINCIPAL = USERNAME + "/" + LOCALHOST; 129 HTTP_PRINCIPAL = "HTTP/" + LOCALHOST; 130 KDC.createPrincipal(KEYTAB_FILE, 131 SERVER_PRINCIPAL, 132 HTTP_PRINCIPAL, 133 USER_ADMIN + "/" + LOCALHOST, 134 USER_OWNER + "/" + LOCALHOST, 135 USER_RX + "/" + LOCALHOST, 136 USER_RO + "/" + LOCALHOST, 137 USER_XO + "/" + LOCALHOST, 138 USER_NONE + "/" + LOCALHOST); 139 } 140 141 private static User getUserByLogin(final String user) throws IOException { 142 return User.create(UserGroupInformation.loginUserFromKeytabAndReturnUGI( 143 getPrinciple(user), KEYTAB_FILE.getAbsolutePath())); 144 } 145 146 private static String getPrinciple(final String user) { 147 return user + "/" + LOCALHOST + "@" + KDC.getRealm(); 148 } 149 150 private static void setUpClusterKdc() throws Exception { 151 HBaseKerberosUtils.setSecuredConfiguration(UTIL.getConfiguration(), 152 SERVER_PRINCIPAL + "@" + KDC.getRealm(), HTTP_PRINCIPAL + "@" + KDC.getRealm()); 153 HBaseKerberosUtils.setSSLConfiguration(UTIL, TestSecureExport.class); 154 155 UTIL.getConfiguration().set(CoprocessorHost.REGION_COPROCESSOR_CONF_KEY, 156 UTIL.getConfiguration().get( 157 CoprocessorHost.REGION_COPROCESSOR_CONF_KEY) + "," + Export.class.getName()); 158 } 159 160 private static void addLabels(final Configuration conf, final List<String> users, 161 final List<String> labels) throws Exception { 162 PrivilegedExceptionAction<VisibilityLabelsProtos.VisibilityLabelsResponse> action 163 = () -> { 164 try (Connection conn = ConnectionFactory.createConnection(conf)) { 165 VisibilityClient.addLabels(conn, labels.toArray(new String[labels.size()])); 166 for (String user : users) { 167 VisibilityClient.setAuths(conn, labels.toArray(new String[labels.size()]), user); 168 } 169 } catch (Throwable t) { 170 throw new IOException(t); 171 } 172 return null; 173 }; 174 getUserByLogin(USER_ADMIN).runAs(action); 175 } 176 177 @Before 178 public void announce() { 179 LOG.info("Running " + name.getMethodName()); 180 } 181 182 @After 183 public void cleanup() throws IOException { 184 } 185 186 private static void clearOutput(Path path) throws IOException { 187 FileSystem fs = path.getFileSystem(UTIL.getConfiguration()); 188 if (fs.exists(path)) { 189 assertEquals(true, fs.delete(path, true)); 190 } 191 } 192 193 /** 194 * Sets the security firstly for getting the correct default realm. 195 */ 196 @BeforeClass 197 public static void beforeClass() throws Exception { 198 UserProvider.setUserProviderForTesting(UTIL.getConfiguration(), 199 HadoopSecurityEnabledUserProviderForTesting.class); 200 setUpKdcServer(); 201 SecureTestUtil.enableSecurity(UTIL.getConfiguration()); 202 UTIL.getConfiguration().setBoolean(AccessControlConstants.EXEC_PERMISSION_CHECKS_KEY, true); 203 VisibilityTestUtil.enableVisiblityLabels(UTIL.getConfiguration()); 204 SecureTestUtil.verifyConfiguration(UTIL.getConfiguration()); 205 setUpClusterKdc(); 206 UTIL.startMiniCluster(); 207 UTIL.waitUntilAllRegionsAssigned(AccessControlLists.ACL_TABLE_NAME); 208 UTIL.waitUntilAllRegionsAssigned(VisibilityConstants.LABELS_TABLE_NAME); 209 UTIL.waitTableEnabled(AccessControlLists.ACL_TABLE_NAME, 50000); 210 UTIL.waitTableEnabled(VisibilityConstants.LABELS_TABLE_NAME, 50000); 211 SecureTestUtil.grantGlobal(UTIL, USER_ADMIN, 212 Permission.Action.ADMIN, 213 Permission.Action.CREATE, 214 Permission.Action.EXEC, 215 Permission.Action.READ, 216 Permission.Action.WRITE); 217 addLabels(UTIL.getConfiguration(), Arrays.asList(USER_OWNER), 218 Arrays.asList(PRIVATE, CONFIDENTIAL, SECRET, TOPSECRET)); 219 } 220 221 @AfterClass 222 public static void afterClass() throws Exception { 223 if (KDC != null) { 224 KDC.stop(); 225 } 226 UTIL.shutdownMiniCluster(); 227 } 228 229 /** 230 * Test the ExportEndpoint's access levels. The {@link Export} test is ignored 231 * since the access exceptions cannot be collected from the mappers. 232 */ 233 @Test 234 public void testAccessCase() throws Throwable { 235 final String exportTable = name.getMethodName(); 236 TableDescriptor exportHtd = TableDescriptorBuilder 237 .newBuilder(TableName.valueOf(name.getMethodName())) 238 .setColumnFamily(ColumnFamilyDescriptorBuilder.of(FAMILYA)) 239 .setOwnerString(USER_OWNER) 240 .build(); 241 SecureTestUtil.createTable(UTIL, exportHtd, new byte[][]{Bytes.toBytes("s")}); 242 SecureTestUtil.grantOnTable(UTIL, USER_RO, 243 TableName.valueOf(exportTable), null, null, 244 Permission.Action.READ); 245 SecureTestUtil.grantOnTable(UTIL, USER_RX, 246 TableName.valueOf(exportTable), null, null, 247 Permission.Action.READ, 248 Permission.Action.EXEC); 249 SecureTestUtil.grantOnTable(UTIL, USER_XO, 250 TableName.valueOf(exportTable), null, null, 251 Permission.Action.EXEC); 252 assertEquals(4, AccessControlLists.getTablePermissions(UTIL.getConfiguration(), 253 TableName.valueOf(exportTable)).size()); 254 AccessTestAction putAction = () -> { 255 Put p = new Put(ROW1); 256 p.addColumn(FAMILYA, Bytes.toBytes("qual_0"), NOW, QUAL); 257 p.addColumn(FAMILYA, Bytes.toBytes("qual_1"), NOW, QUAL); 258 try (Connection conn = ConnectionFactory.createConnection(UTIL.getConfiguration()); 259 Table t = conn.getTable(TableName.valueOf(exportTable))) { 260 t.put(p); 261 } 262 return null; 263 }; 264 // no hdfs access. 265 SecureTestUtil.verifyAllowed(putAction, 266 getUserByLogin(USER_ADMIN), 267 getUserByLogin(USER_OWNER)); 268 SecureTestUtil.verifyDenied(putAction, 269 getUserByLogin(USER_RO), 270 getUserByLogin(USER_XO), 271 getUserByLogin(USER_RX), 272 getUserByLogin(USER_NONE)); 273 274 final FileSystem fs = UTIL.getDFSCluster().getFileSystem(); 275 final Path openDir = fs.makeQualified(new Path("testAccessCase")); 276 fs.mkdirs(openDir); 277 fs.setPermission(openDir, new FsPermission(FsAction.ALL, FsAction.ALL, FsAction.ALL)); 278 final Path output = fs.makeQualified(new Path(openDir, "output")); 279 AccessTestAction exportAction = () -> { 280 try { 281 String[] args = new String[]{exportTable, output.toString()}; 282 Map<byte[], Export.Response> result 283 = Export.run(new Configuration(UTIL.getConfiguration()), args); 284 long rowCount = 0; 285 long cellCount = 0; 286 for (Export.Response r : result.values()) { 287 rowCount += r.getRowCount(); 288 cellCount += r.getCellCount(); 289 } 290 assertEquals(1, rowCount); 291 assertEquals(2, cellCount); 292 return null; 293 } catch (ServiceException | IOException ex) { 294 throw ex; 295 } catch (Throwable ex) { 296 LOG.error(ex.toString(), ex); 297 throw new Exception(ex); 298 } finally { 299 if (fs.exists(new Path(openDir, "output"))) { 300 // if export completes successfully, every file under the output directory should be 301 // owned by the current user, not the hbase service user. 302 FileStatus outputDirFileStatus = fs.getFileStatus(new Path(openDir, "output")); 303 String currentUserName = User.getCurrent().getShortName(); 304 assertEquals("Unexpected file owner", currentUserName, outputDirFileStatus.getOwner()); 305 306 FileStatus[] outputFileStatus = fs.listStatus(new Path(openDir, "output")); 307 for (FileStatus fileStatus: outputFileStatus) { 308 assertEquals("Unexpected file owner", currentUserName, fileStatus.getOwner()); 309 } 310 } else { 311 LOG.info("output directory doesn't exist. Skip check"); 312 } 313 314 clearOutput(output); 315 } 316 }; 317 SecureTestUtil.verifyDenied(exportAction, 318 getUserByLogin(USER_RO), 319 getUserByLogin(USER_XO), 320 getUserByLogin(USER_NONE)); 321 SecureTestUtil.verifyAllowed(exportAction, 322 getUserByLogin(USER_ADMIN), 323 getUserByLogin(USER_OWNER), 324 getUserByLogin(USER_RX)); 325 AccessTestAction deleteAction = () -> { 326 UTIL.deleteTable(TableName.valueOf(exportTable)); 327 return null; 328 }; 329 SecureTestUtil.verifyAllowed(deleteAction, getUserByLogin(USER_OWNER)); 330 fs.delete(openDir, true); 331 } 332 333 @Test 334 public void testVisibilityLabels() throws IOException, Throwable { 335 final String exportTable = name.getMethodName() + "_export"; 336 final String importTable = name.getMethodName() + "_import"; 337 final TableDescriptor exportHtd = TableDescriptorBuilder 338 .newBuilder(TableName.valueOf(exportTable)) 339 .setColumnFamily(ColumnFamilyDescriptorBuilder.of(FAMILYA)) 340 .setOwnerString(USER_OWNER) 341 .build(); 342 SecureTestUtil.createTable(UTIL, exportHtd, new byte[][]{Bytes.toBytes("s")}); 343 AccessTestAction putAction = () -> { 344 Put p1 = new Put(ROW1); 345 p1.addColumn(FAMILYA, QUAL, NOW, QUAL); 346 p1.setCellVisibility(new CellVisibility(SECRET)); 347 Put p2 = new Put(ROW2); 348 p2.addColumn(FAMILYA, QUAL, NOW, QUAL); 349 p2.setCellVisibility(new CellVisibility(PRIVATE + " & " + CONFIDENTIAL)); 350 Put p3 = new Put(ROW3); 351 p3.addColumn(FAMILYA, QUAL, NOW, QUAL); 352 p3.setCellVisibility(new CellVisibility("!" + CONFIDENTIAL + " & " + TOPSECRET)); 353 try (Connection conn = ConnectionFactory.createConnection(UTIL.getConfiguration()); 354 Table t = conn.getTable(TableName.valueOf(exportTable))) { 355 t.put(p1); 356 t.put(p2); 357 t.put(p3); 358 } 359 return null; 360 }; 361 SecureTestUtil.verifyAllowed(putAction, getUserByLogin(USER_OWNER)); 362 List<Pair<List<String>, Integer>> labelsAndRowCounts = new LinkedList<>(); 363 labelsAndRowCounts.add(new Pair<>(Arrays.asList(SECRET), 1)); 364 labelsAndRowCounts.add(new Pair<>(Arrays.asList(PRIVATE, CONFIDENTIAL), 1)); 365 labelsAndRowCounts.add(new Pair<>(Arrays.asList(TOPSECRET), 1)); 366 labelsAndRowCounts.add(new Pair<>(Arrays.asList(TOPSECRET, CONFIDENTIAL), 0)); 367 labelsAndRowCounts.add(new Pair<>(Arrays.asList(TOPSECRET, CONFIDENTIAL, PRIVATE, SECRET), 2)); 368 for (final Pair<List<String>, Integer> labelsAndRowCount : labelsAndRowCounts) { 369 final List<String> labels = labelsAndRowCount.getFirst(); 370 final int rowCount = labelsAndRowCount.getSecond(); 371 //create a open permission directory. 372 final Path openDir = new Path("testAccessCase"); 373 final FileSystem fs = openDir.getFileSystem(UTIL.getConfiguration()); 374 fs.mkdirs(openDir); 375 fs.setPermission(openDir, new FsPermission(FsAction.ALL, FsAction.ALL, FsAction.ALL)); 376 final Path output = fs.makeQualified(new Path(openDir, "output")); 377 AccessTestAction exportAction = () -> { 378 StringBuilder buf = new StringBuilder(); 379 labels.forEach(v -> buf.append(v).append(",")); 380 buf.deleteCharAt(buf.length() - 1); 381 try { 382 String[] args = new String[]{ 383 "-D " + ExportUtils.EXPORT_VISIBILITY_LABELS + "=" + buf.toString(), 384 exportTable, 385 output.toString(),}; 386 Export.run(new Configuration(UTIL.getConfiguration()), args); 387 return null; 388 } catch (ServiceException | IOException ex) { 389 throw ex; 390 } catch (Throwable ex) { 391 throw new Exception(ex); 392 } 393 }; 394 SecureTestUtil.verifyAllowed(exportAction, getUserByLogin(USER_OWNER)); 395 final TableDescriptor importHtd = TableDescriptorBuilder 396 .newBuilder(TableName.valueOf(importTable)) 397 .setColumnFamily(ColumnFamilyDescriptorBuilder.of(FAMILYB)) 398 .setOwnerString(USER_OWNER) 399 .build(); 400 SecureTestUtil.createTable(UTIL, importHtd, new byte[][]{Bytes.toBytes("s")}); 401 AccessTestAction importAction = () -> { 402 String[] args = new String[]{ 403 "-D" + Import.CF_RENAME_PROP + "=" + FAMILYA_STRING + ":" + FAMILYB_STRING, 404 importTable, 405 output.toString() 406 }; 407 assertEquals(0, ToolRunner.run( 408 new Configuration(UTIL.getConfiguration()), new Import(), args)); 409 return null; 410 }; 411 SecureTestUtil.verifyAllowed(importAction, getUserByLogin(USER_OWNER)); 412 AccessTestAction scanAction = () -> { 413 Scan scan = new Scan(); 414 scan.setAuthorizations(new Authorizations(labels)); 415 try (Connection conn = ConnectionFactory.createConnection(UTIL.getConfiguration()); 416 Table table = conn.getTable(importHtd.getTableName()); 417 ResultScanner scanner = table.getScanner(scan)) { 418 int count = 0; 419 for (Result r : scanner) { 420 ++count; 421 } 422 assertEquals(rowCount, count); 423 } 424 return null; 425 }; 426 SecureTestUtil.verifyAllowed(scanAction, getUserByLogin(USER_OWNER)); 427 AccessTestAction deleteAction = () -> { 428 UTIL.deleteTable(importHtd.getTableName()); 429 return null; 430 }; 431 SecureTestUtil.verifyAllowed(deleteAction, getUserByLogin(USER_OWNER)); 432 clearOutput(output); 433 } 434 AccessTestAction deleteAction = () -> { 435 UTIL.deleteTable(exportHtd.getTableName()); 436 return null; 437 }; 438 SecureTestUtil.verifyAllowed(deleteAction, getUserByLogin(USER_OWNER)); 439 } 440}