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.rest; 019 020import static org.junit.jupiter.api.Assertions.assertEquals; 021 022import com.fasterxml.jackson.databind.ObjectMapper; 023import java.io.IOException; 024import java.net.URLEncoder; 025import java.nio.charset.StandardCharsets; 026import java.util.Base64; 027import java.util.Base64.Encoder; 028import java.util.stream.Stream; 029import org.apache.hadoop.conf.Configuration; 030import org.apache.hadoop.hbase.HBaseParameterizedTestTemplate; 031import org.apache.hadoop.hbase.HBaseTestingUtil; 032import org.apache.hadoop.hbase.TableName; 033import org.apache.hadoop.hbase.client.Admin; 034import org.apache.hadoop.hbase.client.ColumnFamilyDescriptor; 035import org.apache.hadoop.hbase.client.ColumnFamilyDescriptorBuilder; 036import org.apache.hadoop.hbase.client.TableDescriptorBuilder; 037import org.apache.hadoop.hbase.rest.client.Client; 038import org.apache.hadoop.hbase.rest.client.Cluster; 039import org.apache.hadoop.hbase.rest.client.Response; 040import org.apache.hadoop.hbase.rest.model.CellModel; 041import org.apache.hadoop.hbase.rest.model.CellSetModel; 042import org.apache.hadoop.hbase.rest.model.RowModel; 043import org.apache.hadoop.hbase.testclassification.MediumTests; 044import org.apache.hadoop.hbase.testclassification.RestTests; 045import org.apache.hadoop.hbase.util.Bytes; 046import org.apache.http.Header; 047import org.apache.http.message.BasicHeader; 048import org.junit.jupiter.api.AfterAll; 049import org.junit.jupiter.api.BeforeAll; 050import org.junit.jupiter.api.Tag; 051import org.junit.jupiter.api.TestTemplate; 052import org.junit.jupiter.params.provider.Arguments; 053 054import org.apache.hbase.thirdparty.com.fasterxml.jackson.jaxrs.json.JacksonJaxbJsonProvider; 055import org.apache.hbase.thirdparty.javax.ws.rs.core.MediaType; 056 057@Tag(RestTests.TAG) 058@Tag(MediumTests.TAG) 059@HBaseParameterizedTestTemplate(name = "{index}: csrfEnabled = {0}") 060public class TestMultiRowResource { 061 062 private static final TableName TABLE = TableName.valueOf("TestRowResource"); 063 private static final String CFA = "a"; 064 private static final String CFB = "b"; 065 private static final String COLUMN_1 = CFA + ":1"; 066 private static final String COLUMN_2 = CFB + ":2"; 067 private static final String ROW_1 = "testrow5"; 068 private static final String VALUE_1 = "testvalue5"; 069 private static final String ROW_2 = "testrow6"; 070 private static final String VALUE_2 = "testvalue6"; 071 072 private static final HBaseTestingUtil TEST_UTIL = new HBaseTestingUtil(); 073 private static final HBaseRESTTestingUtility REST_TEST_UTIL = new HBaseRESTTestingUtility(); 074 075 private static final Encoder base64UrlEncoder = java.util.Base64.getUrlEncoder().withoutPadding(); 076 077 private static Client client; 078 private static Configuration conf; 079 080 private static Header extraHdr = null; 081 private static boolean csrfEnabled = true; 082 083 public TestMultiRowResource(boolean csrf) { 084 csrfEnabled = csrf; 085 } 086 087 public static Stream<Arguments> parameters() { 088 return Stream.of(Arguments.of(false), Arguments.of(true)); 089 } 090 091 @BeforeAll 092 public static void setUpBeforeClass() throws Exception { 093 conf = TEST_UTIL.getConfiguration(); 094 conf.setBoolean(RESTServer.REST_CSRF_ENABLED_KEY, TestMultiRowResource.csrfEnabled); 095 if (TestMultiRowResource.csrfEnabled) { 096 conf.set(RESTServer.REST_CSRF_BROWSER_USERAGENTS_REGEX_KEY, ".*"); 097 } 098 extraHdr = new BasicHeader(RESTServer.REST_CSRF_CUSTOM_HEADER_DEFAULT, ""); 099 TEST_UTIL.startMiniCluster(); 100 REST_TEST_UTIL.startServletContainer(conf); 101 client = new Client(new Cluster().add("localhost", REST_TEST_UTIL.getServletPort())); 102 Admin admin = TEST_UTIL.getAdmin(); 103 if (admin.tableExists(TABLE)) { 104 return; 105 } 106 TableDescriptorBuilder tableDescriptorBuilder = TableDescriptorBuilder.newBuilder(TABLE); 107 ColumnFamilyDescriptor columnFamilyDescriptor = 108 ColumnFamilyDescriptorBuilder.newBuilder(Bytes.toBytes(CFA)).build(); 109 tableDescriptorBuilder.setColumnFamily(columnFamilyDescriptor); 110 columnFamilyDescriptor = ColumnFamilyDescriptorBuilder.newBuilder(Bytes.toBytes(CFB)).build(); 111 tableDescriptorBuilder.setColumnFamily(columnFamilyDescriptor); 112 admin.createTable(tableDescriptorBuilder.build()); 113 } 114 115 @AfterAll 116 public static void tearDownAfterClass() throws Exception { 117 REST_TEST_UTIL.shutdownServletContainer(); 118 TEST_UTIL.shutdownMiniCluster(); 119 } 120 121 @TestTemplate 122 public void testMultiCellGetJSON() throws IOException { 123 String row_5_url = "/" + TABLE + "/" + ROW_1 + "/" + COLUMN_1; 124 String row_6_url = "/" + TABLE + "/" + ROW_2 + "/" + COLUMN_2; 125 126 StringBuilder path = new StringBuilder(); 127 path.append("/"); 128 path.append(TABLE); 129 path.append("/multiget/?row="); 130 path.append(ROW_1); 131 path.append("&row="); 132 path.append(ROW_2); 133 134 if (csrfEnabled) { 135 Response response = client.post(row_5_url, Constants.MIMETYPE_BINARY, Bytes.toBytes(VALUE_1)); 136 assertEquals(400, response.getCode()); 137 } 138 139 client.post(row_5_url, Constants.MIMETYPE_BINARY, Bytes.toBytes(VALUE_1), extraHdr); 140 client.post(row_6_url, Constants.MIMETYPE_BINARY, Bytes.toBytes(VALUE_2), extraHdr); 141 142 Response response = client.get(path.toString(), Constants.MIMETYPE_JSON); 143 assertEquals(200, response.getCode()); 144 assertEquals(Constants.MIMETYPE_JSON, response.getHeader("content-type")); 145 146 client.delete(row_5_url, extraHdr); 147 client.delete(row_6_url, extraHdr); 148 } 149 150 private void checkMultiCellGetJSON(Response response) throws IOException { 151 assertEquals(200, response.getCode()); 152 assertEquals(Constants.MIMETYPE_JSON, response.getHeader("content-type")); 153 154 ObjectMapper mapper = new JacksonJaxbJsonProvider().locateMapper(CellSetModel.class, 155 MediaType.APPLICATION_JSON_TYPE); 156 CellSetModel cellSet = mapper.readValue(response.getBody(), CellSetModel.class); 157 158 RowModel rowModel = cellSet.getRows().get(0); 159 assertEquals(ROW_1, new String(rowModel.getKey())); 160 assertEquals(1, rowModel.getCells().size()); 161 CellModel cell = rowModel.getCells().get(0); 162 assertEquals(COLUMN_1, new String(cell.getColumn())); 163 assertEquals(VALUE_1, new String(cell.getValue())); 164 165 rowModel = cellSet.getRows().get(1); 166 assertEquals(ROW_2, new String(rowModel.getKey())); 167 assertEquals(1, rowModel.getCells().size()); 168 cell = rowModel.getCells().get(0); 169 assertEquals(COLUMN_2, new String(cell.getColumn())); 170 assertEquals(VALUE_2, new String(cell.getValue())); 171 } 172 173 // See https://issues.apache.org/jira/browse/HBASE-28174 174 @TestTemplate 175 public void testMultiCellGetJSONB64() throws IOException { 176 String row_5_url = "/" + TABLE + "/" + ROW_1 + "/" + COLUMN_1; 177 String row_6_url = "/" + TABLE + "/" + ROW_2 + "/" + COLUMN_2; 178 179 if (csrfEnabled) { 180 Response response = client.post(row_5_url, Constants.MIMETYPE_BINARY, Bytes.toBytes(VALUE_1)); 181 assertEquals(400, response.getCode()); 182 } 183 184 client.post(row_5_url, Constants.MIMETYPE_BINARY, Bytes.toBytes(VALUE_1), extraHdr); 185 client.post(row_6_url, Constants.MIMETYPE_BINARY, Bytes.toBytes(VALUE_2), extraHdr); 186 187 StringBuilder path = new StringBuilder(); 188 Base64.Encoder encoder = Base64.getUrlEncoder().withoutPadding(); 189 path.append("/"); 190 path.append(TABLE); 191 path.append("/multiget/?row="); 192 path.append(encoder.encodeToString(ROW_1.getBytes(StandardCharsets.UTF_8))); 193 path.append("&row="); 194 path.append(encoder.encodeToString(ROW_2.getBytes(StandardCharsets.UTF_8))); 195 path.append("&e=b64"); // Specify encoding via query string 196 197 Response response = client.get(path.toString(), Constants.MIMETYPE_JSON); 198 199 checkMultiCellGetJSON(response); 200 201 path = new StringBuilder(); 202 path.append("/"); 203 path.append(TABLE); 204 path.append("/multiget/?row="); 205 path.append(encoder.encodeToString(ROW_1.getBytes(StandardCharsets.UTF_8))); 206 path.append("&row="); 207 path.append(encoder.encodeToString(ROW_2.getBytes(StandardCharsets.UTF_8))); 208 209 Header[] headers = new Header[] { new BasicHeader("Accept", Constants.MIMETYPE_JSON), 210 new BasicHeader("Encoding", "b64") // Specify encoding via header 211 }; 212 response = client.get(path.toString(), headers); 213 214 checkMultiCellGetJSON(response); 215 216 client.delete(row_5_url, extraHdr); 217 client.delete(row_6_url, extraHdr); 218 } 219 220 @TestTemplate 221 public void testMultiCellGetNoKeys() throws IOException { 222 StringBuilder path = new StringBuilder(); 223 path.append("/"); 224 path.append(TABLE); 225 path.append("/multiget"); 226 227 Response response = client.get(path.toString(), Constants.MIMETYPE_XML); 228 assertEquals(404, response.getCode()); 229 } 230 231 @TestTemplate 232 public void testMultiCellGetXML() throws IOException { 233 String row_5_url = "/" + TABLE + "/" + ROW_1 + "/" + COLUMN_1; 234 String row_6_url = "/" + TABLE + "/" + ROW_2 + "/" + COLUMN_2; 235 236 StringBuilder path = new StringBuilder(); 237 path.append("/"); 238 path.append(TABLE); 239 path.append("/multiget/?row="); 240 path.append(ROW_1); 241 path.append("&row="); 242 path.append(ROW_2); 243 244 client.post(row_5_url, Constants.MIMETYPE_BINARY, Bytes.toBytes(VALUE_1), extraHdr); 245 client.post(row_6_url, Constants.MIMETYPE_BINARY, Bytes.toBytes(VALUE_2), extraHdr); 246 247 Response response = client.get(path.toString(), Constants.MIMETYPE_XML); 248 assertEquals(200, response.getCode()); 249 assertEquals(Constants.MIMETYPE_XML, response.getHeader("content-type")); 250 251 client.delete(row_5_url, extraHdr); 252 client.delete(row_6_url, extraHdr); 253 } 254 255 @TestTemplate 256 public void testMultiCellGetWithColsJSON() throws IOException { 257 String row_5_url = "/" + TABLE + "/" + ROW_1 + "/" + COLUMN_1; 258 String row_6_url = "/" + TABLE + "/" + ROW_2 + "/" + COLUMN_2; 259 260 StringBuilder path = new StringBuilder(); 261 path.append("/"); 262 path.append(TABLE); 263 path.append("/multiget"); 264 path.append("/" + COLUMN_1 + "," + CFB); 265 path.append("?row="); 266 path.append(ROW_1); 267 path.append("&row="); 268 path.append(ROW_2); 269 270 client.post(row_5_url, Constants.MIMETYPE_BINARY, Bytes.toBytes(VALUE_1), extraHdr); 271 client.post(row_6_url, Constants.MIMETYPE_BINARY, Bytes.toBytes(VALUE_2), extraHdr); 272 273 Response response = client.get(path.toString(), Constants.MIMETYPE_JSON); 274 assertEquals(200, response.getCode()); 275 ObjectMapper mapper = new JacksonJaxbJsonProvider().locateMapper(CellSetModel.class, 276 MediaType.APPLICATION_JSON_TYPE); 277 CellSetModel cellSet = mapper.readValue(response.getBody(), CellSetModel.class); 278 assertEquals(2, cellSet.getRows().size()); 279 assertEquals(ROW_1, Bytes.toString(cellSet.getRows().get(0).getKey())); 280 assertEquals(VALUE_1, Bytes.toString(cellSet.getRows().get(0).getCells().get(0).getValue())); 281 assertEquals(ROW_2, Bytes.toString(cellSet.getRows().get(1).getKey())); 282 assertEquals(VALUE_2, Bytes.toString(cellSet.getRows().get(1).getCells().get(0).getValue())); 283 284 client.delete(row_5_url, extraHdr); 285 client.delete(row_6_url, extraHdr); 286 } 287 288 @TestTemplate 289 public void testMultiCellGetJSONNotFound() throws IOException { 290 String row_5_url = "/" + TABLE + "/" + ROW_1 + "/" + COLUMN_1; 291 292 StringBuilder path = new StringBuilder(); 293 path.append("/"); 294 path.append(TABLE); 295 path.append("/multiget/?row="); 296 path.append(ROW_1); 297 path.append("&row="); 298 path.append(ROW_2); 299 300 client.post(row_5_url, Constants.MIMETYPE_BINARY, Bytes.toBytes(VALUE_1), extraHdr); 301 Response response = client.get(path.toString(), Constants.MIMETYPE_JSON); 302 assertEquals(200, response.getCode()); 303 ObjectMapper mapper = new JacksonJaxbJsonProvider().locateMapper(CellSetModel.class, 304 MediaType.APPLICATION_JSON_TYPE); 305 CellSetModel cellSet = (CellSetModel) mapper.readValue(response.getBody(), CellSetModel.class); 306 assertEquals(1, cellSet.getRows().size()); 307 assertEquals(ROW_1, Bytes.toString(cellSet.getRows().get(0).getKey())); 308 assertEquals(VALUE_1, Bytes.toString(cellSet.getRows().get(0).getCells().get(0).getValue())); 309 client.delete(row_5_url, extraHdr); 310 } 311 312 @TestTemplate 313 public void testMultiCellGetWithColsInQueryPathJSON() throws IOException { 314 String row_5_url = "/" + TABLE + "/" + ROW_1 + "/" + COLUMN_1; 315 String row_6_url = "/" + TABLE + "/" + ROW_2 + "/" + COLUMN_2; 316 317 StringBuilder path = new StringBuilder(); 318 path.append("/"); 319 path.append(TABLE); 320 path.append("/multiget/?row="); 321 path.append(ROW_1); 322 path.append("/"); 323 path.append(COLUMN_1); 324 path.append("&row="); 325 path.append(ROW_2); 326 path.append("/"); 327 path.append(COLUMN_1); 328 329 client.post(row_5_url, Constants.MIMETYPE_BINARY, Bytes.toBytes(VALUE_1), extraHdr); 330 client.post(row_6_url, Constants.MIMETYPE_BINARY, Bytes.toBytes(VALUE_2), extraHdr); 331 332 Response response = client.get(path.toString(), Constants.MIMETYPE_JSON); 333 assertEquals(200, response.getCode()); 334 ObjectMapper mapper = new JacksonJaxbJsonProvider().locateMapper(CellSetModel.class, 335 MediaType.APPLICATION_JSON_TYPE); 336 CellSetModel cellSet = mapper.readValue(response.getBody(), CellSetModel.class); 337 assertEquals(1, cellSet.getRows().size()); 338 assertEquals(ROW_1, Bytes.toString(cellSet.getRows().get(0).getKey())); 339 assertEquals(VALUE_1, Bytes.toString(cellSet.getRows().get(0).getCells().get(0).getValue())); 340 341 client.delete(row_5_url, extraHdr); 342 client.delete(row_6_url, extraHdr); 343 } 344 345 @TestTemplate 346 public void testMultiCellGetFilterJSON() throws IOException { 347 String row_5_url = "/" + TABLE + "/" + ROW_1 + "/" + COLUMN_1; 348 String row_6_url = "/" + TABLE + "/" + ROW_2 + "/" + COLUMN_2; 349 350 StringBuilder path = new StringBuilder(); 351 path.append("/"); 352 path.append(TABLE); 353 path.append("/multiget/?row="); 354 path.append(ROW_1); 355 path.append("&row="); 356 path.append(ROW_2); 357 358 if (csrfEnabled) { 359 Response response = client.post(row_5_url, Constants.MIMETYPE_BINARY, Bytes.toBytes(VALUE_1)); 360 assertEquals(400, response.getCode()); 361 } 362 363 client.post(row_5_url, Constants.MIMETYPE_BINARY, Bytes.toBytes(VALUE_1), extraHdr); 364 client.post(row_6_url, Constants.MIMETYPE_BINARY, Bytes.toBytes(VALUE_2), extraHdr); 365 366 Response response = client.get(path.toString(), Constants.MIMETYPE_JSON); 367 assertEquals(200, response.getCode()); 368 assertEquals(Constants.MIMETYPE_JSON, response.getHeader("content-type")); 369 370 // If the filter is used, then we get the same result 371 String positivePath = path.toString() + ("&" + Constants.FILTER_B64 + "=" + base64UrlEncoder 372 .encodeToString("PrefixFilter('testrow')".getBytes(StandardCharsets.UTF_8.toString()))); 373 response = client.get(positivePath, Constants.MIMETYPE_JSON); 374 checkMultiCellGetJSON(response); 375 376 // Same with non binary clean param 377 positivePath = path.toString() + ("&" + Constants.FILTER + "=" 378 + URLEncoder.encode("PrefixFilter('testrow')", StandardCharsets.UTF_8.name())); 379 response = client.get(positivePath, Constants.MIMETYPE_JSON); 380 checkMultiCellGetJSON(response); 381 382 // This filter doesn't match the found rows 383 String negativePath = path.toString() + ("&" + Constants.FILTER_B64 + "=" + base64UrlEncoder 384 .encodeToString("PrefixFilter('notfound')".getBytes(StandardCharsets.UTF_8.toString()))); 385 response = client.get(negativePath, Constants.MIMETYPE_JSON); 386 assertEquals(404, response.getCode()); 387 388 // Same with non binary clean param 389 negativePath = path.toString() + ("&" + Constants.FILTER + "=" 390 + URLEncoder.encode("PrefixFilter('notfound')", StandardCharsets.UTF_8.name())); 391 response = client.get(negativePath, Constants.MIMETYPE_JSON); 392 assertEquals(404, response.getCode()); 393 394 // Check with binary parameters 395 // positive case 396 positivePath = path.toString() + ("&" + Constants.FILTER_B64 + "=" + base64UrlEncoder 397 .encodeToString(Bytes.toBytesBinary("ColumnRangeFilter ('\\x00', true, '\\xff', true)"))); 398 response = client.get(positivePath, Constants.MIMETYPE_JSON); 399 checkMultiCellGetJSON(response); 400 401 // negative case 402 negativePath = path.toString() + ("&" + Constants.FILTER_B64 + "=" + base64UrlEncoder 403 .encodeToString(Bytes.toBytesBinary("ColumnRangeFilter ('\\x00', true, '1', false)"))); 404 response = client.get(negativePath, Constants.MIMETYPE_JSON); 405 assertEquals(404, response.getCode()); 406 407 client.delete(row_5_url, extraHdr); 408 client.delete(row_6_url, extraHdr); 409 } 410}