Skip to content

Commit f53b5b1

Browse files
authored
HBASE-30183 Refactoring scanner related code in hbase-thrift and hbase-rest (#8284) (#8302)
(cherry picked from commit 095de1d) Signed-off-by: Xiao Liu <liuxiaocs@apache.org>
1 parent e54c286 commit f53b5b1

9 files changed

Lines changed: 299 additions & 166 deletions

File tree

hbase-rest/src/main/java/org/apache/hadoop/hbase/rest/ScannerInstanceResource.java

Lines changed: 37 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -19,13 +19,15 @@
1919

2020
import java.io.IOException;
2121
import java.util.Base64;
22+
import java.util.Objects;
2223
import org.apache.hadoop.hbase.Cell;
2324
import org.apache.hadoop.hbase.CellUtil;
2425
import org.apache.hadoop.hbase.TableNotFoundException;
2526
import org.apache.hadoop.hbase.rest.model.CellModel;
2627
import org.apache.hadoop.hbase.rest.model.CellSetModel;
2728
import org.apache.hadoop.hbase.rest.model.RowModel;
2829
import org.apache.hadoop.hbase.util.Bytes;
30+
import org.apache.hadoop.hbase.util.ConnectionCache;
2931
import org.apache.yetus.audience.InterfaceAudience;
3032
import org.slf4j.Logger;
3133
import org.slf4j.LoggerFactory;
@@ -53,18 +55,31 @@ public class ScannerInstanceResource extends ResourceBase {
5355

5456
ResultGenerator generator = null;
5557
String id = null;
58+
String owner;
5659
int batch = 1;
5760

5861
public ScannerInstanceResource() throws IOException {
5962
}
6063

61-
public ScannerInstanceResource(String table, String id, ResultGenerator generator, int batch)
64+
public ScannerInstanceResource(String id, String owner, ResultGenerator generator, int batch)
6265
throws IOException {
6366
this.id = id;
67+
this.owner = owner;
6468
this.generator = generator;
6569
this.batch = batch;
6670
}
6771

72+
private Response checkOwner() {
73+
ConnectionCache connCache = RESTServlet.getInstance().getConnectionCache();
74+
if (!Objects.equals(connCache.getEffectiveUser(), owner)) {
75+
LOG.warn("User {} is trying to access scanner {} which belongs to user {}",
76+
connCache.getEffectiveUser(), id, owner);
77+
return Response.status(Response.Status.FORBIDDEN).type(MIMETYPE_TEXT)
78+
.entity("Not allowed" + CRLF).build();
79+
}
80+
return null;
81+
}
82+
6883
@GET
6984
@Produces({ MIMETYPE_XML, MIMETYPE_JSON, MIMETYPE_PROTOBUF, MIMETYPE_PROTOBUF_IETF })
7085
public Response get(final @Context UriInfo uriInfo, @QueryParam("n") int maxRows,
@@ -77,10 +92,13 @@ public Response get(final @Context UriInfo uriInfo, @QueryParam("n") int maxRows
7792
servlet.getMetrics().incrementFailedGetRequests(1);
7893
return Response.status(Response.Status.NOT_FOUND).type(MIMETYPE_TEXT)
7994
.entity("Not found" + CRLF).build();
80-
} else {
81-
// Updated the connection access time for each client next() call
82-
RESTServlet.getInstance().getConnectionCache().updateConnectionAccessTime();
8395
}
96+
Response checkResp = checkOwner();
97+
if (checkResp != null) {
98+
return checkResp;
99+
}
100+
// Updated the connection access time for each client next() call
101+
RESTServlet.getInstance().getConnectionCache().updateConnectionAccessTime();
84102
CellSetModel model = new CellSetModel();
85103
RowModel rowModel = null;
86104
byte[] rowKeyArray = null;
@@ -159,13 +177,18 @@ public Response getBinary(final @Context UriInfo uriInfo) {
159177
if (LOG.isTraceEnabled()) {
160178
LOG.trace("GET " + uriInfo.getAbsolutePath() + " as " + MIMETYPE_BINARY);
161179
}
180+
162181
servlet.getMetrics().incrementRequests(1);
182+
if (generator == null) {
183+
servlet.getMetrics().incrementFailedGetRequests(1);
184+
return Response.status(Response.Status.NOT_FOUND).type(MIMETYPE_TEXT)
185+
.entity("Not found" + CRLF).build();
186+
}
187+
Response checkResp = checkOwner();
188+
if (checkResp != null) {
189+
return checkResp;
190+
}
163191
try {
164-
if (generator == null) {
165-
servlet.getMetrics().incrementFailedGetRequests(1);
166-
return Response.status(Response.Status.NOT_FOUND).type(MIMETYPE_TEXT)
167-
.entity("Not found" + CRLF).build();
168-
}
169192
Cell value = generator.next();
170193
if (value == null) {
171194
if (LOG.isTraceEnabled()) {
@@ -199,6 +222,7 @@ public Response delete(final @Context UriInfo uriInfo) {
199222
if (LOG.isTraceEnabled()) {
200223
LOG.trace("DELETE " + uriInfo.getAbsolutePath());
201224
}
225+
202226
servlet.getMetrics().incrementRequests(1);
203227
if (servlet.isReadOnly()) {
204228
return Response.status(Response.Status.FORBIDDEN).type(MIMETYPE_TEXT)
@@ -209,6 +233,10 @@ public Response delete(final @Context UriInfo uriInfo) {
209233
return Response.status(Response.Status.NOT_FOUND).type(MIMETYPE_TEXT)
210234
.entity("Not found" + CRLF).build();
211235
}
236+
Response checkResp = checkOwner();
237+
if (checkResp != null) {
238+
return checkResp;
239+
}
212240
if (ScannerResource.delete(id)) {
213241
servlet.getMetrics().incrementSucessfulDeleteRequests(1);
214242
} else {

hbase-rest/src/main/java/org/apache/hadoop/hbase/rest/ScannerResource.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -114,8 +114,8 @@ Response update(final ScannerModel model, final boolean replace, final UriInfo u
114114
new ScannerResultGenerator(tableName, spec, filter, model.getCaching(),
115115
model.getCacheBlocks(), model.isIncludeStartRow(), model.isIncludeStopRow());
116116
String id = gen.getID();
117-
ScannerInstanceResource instance =
118-
new ScannerInstanceResource(tableName, id, gen, model.getBatch());
117+
ScannerInstanceResource instance = new ScannerInstanceResource(id,
118+
RESTServlet.getInstance().getConnectionCache().getEffectiveUser(), gen, model.getBatch());
119119
scanners.put(id, instance);
120120
if (LOG.isTraceEnabled()) {
121121
LOG.trace("new scanner: " + id);

hbase-rest/src/test/java/org/apache/hadoop/hbase/rest/TestSecureRESTServer.java

Lines changed: 83 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@
4646
import org.apache.hadoop.hbase.rest.model.CellModel;
4747
import org.apache.hadoop.hbase.rest.model.CellSetModel;
4848
import org.apache.hadoop.hbase.rest.model.RowModel;
49+
import org.apache.hadoop.hbase.rest.model.ScannerModel;
4950
import org.apache.hadoop.hbase.security.HBaseKerberosUtils;
5051
import org.apache.hadoop.hbase.security.access.AccessControlClient;
5152
import org.apache.hadoop.hbase.security.access.AccessControlConstants;
@@ -70,7 +71,9 @@
7071
import org.apache.http.client.CredentialsProvider;
7172
import org.apache.http.client.config.AuthSchemes;
7273
import org.apache.http.client.methods.CloseableHttpResponse;
74+
import org.apache.http.client.methods.HttpDelete;
7375
import org.apache.http.client.methods.HttpGet;
76+
import org.apache.http.client.methods.HttpPost;
7477
import org.apache.http.client.methods.HttpPut;
7578
import org.apache.http.client.protocol.HttpClientContext;
7679
import org.apache.http.config.Registry;
@@ -110,6 +113,7 @@ public class TestSecureRESTServer {
110113

111114
private static final String HOSTNAME = "localhost";
112115
private static final String CLIENT_PRINCIPAL = "client";
116+
private static final String CLIENT_PRINCIPAL2 = "client2";
113117
private static final String WHEEL_PRINCIPAL = "wheel";
114118
// The principal for accepting SPNEGO authn'ed requests (*must* be HTTP/fqdn)
115119
private static final String SPNEGO_SERVICE_PRINCIPAL = "HTTP/" + HOSTNAME;
@@ -156,7 +160,7 @@ public static void setupServer() throws Exception {
156160
* Start KDC
157161
*/
158162
KDC = TEST_UTIL.setupMiniKdc(serviceKeytab);
159-
KDC.createPrincipal(clientKeytab, CLIENT_PRINCIPAL);
163+
KDC.createPrincipal(clientKeytab, CLIENT_PRINCIPAL, CLIENT_PRINCIPAL2);
160164
KDC.createPrincipal(wheelKeytab, WHEEL_PRINCIPAL);
161165
KDC.createPrincipal(serviceKeytab, SERVICE_PRINCIPAL);
162166
// REST server's keytab contains keys for both principals REST uses
@@ -189,7 +193,7 @@ public static void setupServer() throws Exception {
189193
updateKerberosConfiguration(conf, REST_SERVER_PRINCIPAL, SPNEGO_SERVICE_PRINCIPAL,
190194
restServerKeytab);
191195

192-
// Start HDFS
196+
// Start HBase
193197
TEST_UTIL.startMiniCluster(
194198
StartMiniClusterOption.builder().numMasters(1).numRegionServers(1).numZkServers(1).build());
195199

@@ -330,10 +334,10 @@ public Void run() throws Exception {
330334
});
331335
}
332336

333-
public void testProxy(String extraArgs, String PRINCIPAL, File keytab, int responseCode)
337+
private void testProxy(String extraArgs, String PRINCIPAL, File keytab, int responseCode)
334338
throws Exception {
335-
UserGroupInformation superuser = UserGroupInformation
336-
.loginUserFromKeytabAndReturnUGI(SERVICE_PRINCIPAL, serviceKeytab.getAbsolutePath());
339+
UserGroupInformation.loginUserFromKeytabAndReturnUGI(SERVICE_PRINCIPAL,
340+
serviceKeytab.getAbsolutePath());
337341
final TableName table = TableName.valueOf("publicTable");
338342

339343
// Read that row as the client
@@ -417,6 +421,80 @@ public Void run() throws Exception {
417421
});
418422
}
419423

424+
@Test
425+
public void testScanWithDifferentClients() throws Exception {
426+
Pair<CloseableHttpClient, HttpClientContext> pair = getClient();
427+
CloseableHttpClient client = pair.getFirst();
428+
HttpClientContext context = pair.getSecond();
429+
430+
UserGroupInformation ugi = UserGroupInformation
431+
.loginUserFromKeytabAndReturnUGI(CLIENT_PRINCIPAL, clientKeytab.getAbsolutePath());
432+
433+
ObjectMapper mapper = new JacksonJaxbJsonProvider().locateMapper(ScannerModel.class,
434+
MediaType.APPLICATION_JSON_TYPE);
435+
TableName table = TableName.valueOf("publicTable");
436+
ScannerModel model = new ScannerModel();
437+
StringEntity entity =
438+
new StringEntity(mapper.writeValueAsString(model), ContentType.APPLICATION_JSON);
439+
HttpPost post =
440+
new HttpPost("http://localhost:" + REST_TEST.getServletPort() + "/" + table + "/scanner");
441+
post.setEntity(entity);
442+
String scannerURI = ugi.doAs(new PrivilegedExceptionAction<String>() {
443+
444+
@Override
445+
public String run() throws Exception {
446+
try (CloseableHttpResponse response = client.execute(post, context)) {
447+
final int statusCode = response.getStatusLine().getStatusCode();
448+
assertEquals(HttpURLConnection.HTTP_CREATED, statusCode);
449+
return response.getFirstHeader("Location").getValue();
450+
}
451+
}
452+
});
453+
454+
Pair<CloseableHttpClient, HttpClientContext> pair2 = getClient();
455+
CloseableHttpClient client2 = pair2.getFirst();
456+
HttpClientContext context2 = pair2.getSecond();
457+
458+
UserGroupInformation ugi2 = UserGroupInformation
459+
.loginUserFromKeytabAndReturnUGI(CLIENT_PRINCIPAL2, clientKeytab.getAbsolutePath());
460+
ugi2.doAs(new PrivilegedExceptionAction<Void>() {
461+
462+
@Override
463+
public Void run() throws Exception {
464+
HttpGet get = new HttpGet(scannerURI + "?n=1");
465+
try (CloseableHttpResponse response = client2.execute(get, context2)) {
466+
final int statusCode = response.getStatusLine().getStatusCode();
467+
assertEquals(HttpURLConnection.HTTP_FORBIDDEN, statusCode);
468+
}
469+
HttpDelete delete = new HttpDelete(scannerURI);
470+
try (CloseableHttpResponse response = client2.execute(delete, context2)) {
471+
final int statusCode = response.getStatusLine().getStatusCode();
472+
assertEquals(HttpURLConnection.HTTP_FORBIDDEN, statusCode);
473+
}
474+
return null;
475+
}
476+
});
477+
478+
ugi.doAs(new PrivilegedExceptionAction<Void>() {
479+
480+
@Override
481+
public Void run() throws Exception {
482+
HttpGet get = new HttpGet(scannerURI + "?n=1");
483+
try (CloseableHttpResponse response = client.execute(get, context)) {
484+
final int statusCode = response.getStatusLine().getStatusCode();
485+
assertEquals(HttpURLConnection.HTTP_OK, statusCode);
486+
}
487+
HttpDelete delete = new HttpDelete(scannerURI);
488+
try (CloseableHttpResponse response = client.execute(delete, context)) {
489+
final int statusCode = response.getStatusLine().getStatusCode();
490+
assertEquals(HttpURLConnection.HTTP_OK, statusCode);
491+
}
492+
return null;
493+
}
494+
});
495+
496+
}
497+
420498
private Pair<CloseableHttpClient, HttpClientContext> getClient() {
421499
HttpClientConnectionManager pool = new PoolingHttpClientConnectionManager();
422500
HttpHost host = new HttpHost("localhost", REST_TEST.getServletPort());

hbase-thrift/src/main/java/org/apache/hadoop/hbase/thrift/HBaseServiceHandler.java

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,35 +17,80 @@
1717
*/
1818
package org.apache.hadoop.hbase.thrift;
1919

20+
import static org.apache.hadoop.hbase.HConstants.DEFAULT_HBASE_CLIENT_SCANNER_TIMEOUT_PERIOD;
21+
import static org.apache.hadoop.hbase.HConstants.HBASE_CLIENT_SCANNER_TIMEOUT_PERIOD;
22+
2023
import java.io.IOException;
2124
import java.nio.ByteBuffer;
25+
import java.util.Objects;
26+
import java.util.concurrent.TimeUnit;
27+
import java.util.concurrent.atomic.AtomicInteger;
28+
import java.util.concurrent.locks.Lock;
2229
import org.apache.hadoop.conf.Configuration;
2330
import org.apache.hadoop.hbase.HBaseInterfaceAudience;
2431
import org.apache.hadoop.hbase.client.Admin;
32+
import org.apache.hadoop.hbase.client.ResultScanner;
2533
import org.apache.hadoop.hbase.client.Table;
34+
import org.apache.hadoop.hbase.security.AccessDeniedException;
2635
import org.apache.hadoop.hbase.security.UserProvider;
2736
import org.apache.hadoop.hbase.util.Bytes;
2837
import org.apache.hadoop.hbase.util.ConnectionCache;
38+
import org.apache.hadoop.hbase.util.KeyLocker;
2939
import org.apache.yetus.audience.InterfaceAudience;
40+
import org.slf4j.Logger;
41+
import org.slf4j.LoggerFactory;
42+
43+
import org.apache.hbase.thirdparty.com.google.common.cache.Cache;
44+
import org.apache.hbase.thirdparty.com.google.common.cache.CacheBuilder;
45+
import org.apache.hbase.thirdparty.com.google.common.cache.RemovalCause;
3046

3147
/**
3248
* abstract class for HBase handler providing a Connection cache and get table/admin method
3349
*/
3450
@InterfaceAudience.LimitedPrivate(HBaseInterfaceAudience.TOOLS)
3551
public abstract class HBaseServiceHandler {
52+
53+
private static final Logger LOG = LoggerFactory.getLogger(HBaseServiceHandler.class);
54+
3655
public static final String CLEANUP_INTERVAL = "hbase.thrift.connection.cleanup-interval";
3756
public static final String MAX_IDLETIME = "hbase.thrift.connection.max-idletime";
3857

3958
protected Configuration conf;
4059

4160
protected final ConnectionCache connectionCache;
4261

62+
protected static final class ResultScannerWrapper {
63+
public final ResultScanner scanner;
64+
public final boolean sortColumns;
65+
public final String owner;
66+
67+
public ResultScannerWrapper(ResultScanner scanner, boolean sortColumns, String owner) {
68+
this.scanner = scanner;
69+
this.sortColumns = sortColumns;
70+
this.owner = owner;
71+
}
72+
}
73+
74+
private final AtomicInteger nextScannerId = new AtomicInteger(0);
75+
private final Cache<Integer, ResultScannerWrapper> scannerMap;
76+
private final KeyLocker<Integer> removeScannerLock = new KeyLocker<>();
77+
4378
public HBaseServiceHandler(final Configuration c, final UserProvider userProvider)
4479
throws IOException {
4580
this.conf = c;
4681
int cleanInterval = conf.getInt(CLEANUP_INTERVAL, 10 * 1000);
4782
int maxIdleTime = conf.getInt(MAX_IDLETIME, 10 * 60 * 1000);
4883
connectionCache = new ConnectionCache(conf, userProvider, cleanInterval, maxIdleTime);
84+
long cacheTimeout = conf.getLong(HBASE_CLIENT_SCANNER_TIMEOUT_PERIOD,
85+
DEFAULT_HBASE_CLIENT_SCANNER_TIMEOUT_PERIOD);
86+
scannerMap = CacheBuilder.newBuilder().expireAfterAccess(cacheTimeout, TimeUnit.MILLISECONDS)
87+
.removalListener(notification -> {
88+
// do not close the scanner if it is removed manually, we will either add it back or close
89+
// it manually.
90+
if (notification.getCause() != RemovalCause.EXPLICIT) {
91+
((ResultScannerWrapper) notification.getValue()).scanner.close();
92+
}
93+
}).build();
4994
}
5095

5196
protected ThriftMetrics metrics = null;
@@ -58,6 +103,51 @@ public void setEffectiveUser(String effectiveUser) {
58103
connectionCache.setEffectiveUser(effectiveUser);
59104
}
60105

106+
/**
107+
* Assigns a unique ID to the scanner and adds the mapping to an internal HashMap.
108+
* @param scanner to add
109+
* @return Id for this Scanner
110+
*/
111+
protected int addScanner(ResultScanner scanner, boolean sortColumns) {
112+
int id = nextScannerId.getAndIncrement();
113+
ResultScannerWrapper wrapper =
114+
new ResultScannerWrapper(scanner, sortColumns, connectionCache.getEffectiveUser());
115+
scannerMap.put(id, wrapper);
116+
return id;
117+
}
118+
119+
/**
120+
* Add the given scanner back to scanner map.
121+
* <p>
122+
* When scanning, we need to remove the scanner from scanner map to prevent expiration during
123+
* scanning.
124+
*/
125+
protected void addScannerBack(int id, ResultScannerWrapper wrapper) {
126+
scannerMap.put(id, wrapper);
127+
}
128+
129+
/**
130+
* Removes the scanner associated with the specified ID from the internal HashMap.
131+
* @param id of the Scanner to remove
132+
* @throws AccessDeniedException if the scanner is not belong to the current user
133+
*/
134+
protected ResultScannerWrapper removeScanner(int id) throws IOException {
135+
Lock lock = removeScannerLock.acquireLock(id);
136+
try {
137+
ResultScannerWrapper wrapper = scannerMap.getIfPresent(id);
138+
if (wrapper != null && !Objects.equals(connectionCache.getEffectiveUser(), wrapper.owner)) {
139+
LOG.warn("User {} is trying to access scanner id = {} where owner = {}",
140+
connectionCache.getEffectiveUser(), id, wrapper.owner);
141+
throw new AccessDeniedException(
142+
"User " + connectionCache.getEffectiveUser() + " is not allowed to access scanner " + id);
143+
}
144+
scannerMap.invalidate(id);
145+
return wrapper;
146+
} finally {
147+
lock.unlock();
148+
}
149+
}
150+
61151
/**
62152
* Obtain HBaseAdmin. Creates the instance if it is not already created.
63153
*/

0 commit comments

Comments
 (0)