HBASE-26667 Integrate user-experience for hbase-client (#4064)

Signed-off-by: Josh Elser <elserj@apache.org>
This commit is contained in:
Andor Molnár 2022-03-25 23:21:04 +01:00 committed by GitHub
parent c11f3779c7
commit d94c472ef5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 181 additions and 7 deletions

View File

@ -19,7 +19,6 @@
package org.apache.hadoop.hbase.client; package org.apache.hadoop.hbase.client;
import static org.apache.hadoop.hbase.util.FutureUtils.addListener; import static org.apache.hadoop.hbase.util.FutureUtils.addListener;
import java.io.IOException; import java.io.IOException;
import java.lang.reflect.Constructor; import java.lang.reflect.Constructor;
import java.security.PrivilegedExceptionAction; import java.security.PrivilegedExceptionAction;
@ -30,6 +29,7 @@ import org.apache.hadoop.hbase.AuthUtil;
import org.apache.hadoop.hbase.HBaseConfiguration; import org.apache.hadoop.hbase.HBaseConfiguration;
import org.apache.hadoop.hbase.security.User; import org.apache.hadoop.hbase.security.User;
import org.apache.hadoop.hbase.security.UserProvider; import org.apache.hadoop.hbase.security.UserProvider;
import org.apache.hadoop.hbase.security.token.OAuthBearerTokenUtil;
import org.apache.hadoop.hbase.trace.TraceUtil; import org.apache.hadoop.hbase.trace.TraceUtil;
import org.apache.hadoop.hbase.util.FutureUtils; import org.apache.hadoop.hbase.util.FutureUtils;
import org.apache.hadoop.hbase.util.ReflectionUtils; import org.apache.hadoop.hbase.util.ReflectionUtils;
@ -70,7 +70,11 @@ import org.apache.yetus.audience.InterfaceAudience;
@InterfaceAudience.Public @InterfaceAudience.Public
public class ConnectionFactory { public class ConnectionFactory {
public static final String HBASE_CLIENT_ASYNC_CONNECTION_IMPL = "hbase.client.async.connection.impl"; public static final String HBASE_CLIENT_ASYNC_CONNECTION_IMPL =
"hbase.client.async.connection.impl";
/** Environment variable for OAuth Bearer token */
public static final String ENV_OAUTHBEARER_TOKEN = "HBASE_JWT";
/** No public c.tors */ /** No public c.tors */
protected ConnectionFactory() { protected ConnectionFactory() {
@ -214,6 +218,11 @@ public class ConnectionFactory {
*/ */
public static Connection createConnection(Configuration conf, ExecutorService pool, public static Connection createConnection(Configuration conf, ExecutorService pool,
final User user) throws IOException { final User user) throws IOException {
if (System.getenv().containsKey(ENV_OAUTHBEARER_TOKEN)) {
OAuthBearerTokenUtil.addTokenFromEnvironmentVar(user, System.getenv(ENV_OAUTHBEARER_TOKEN));
}
Class<?> clazz = conf.getClass(ConnectionUtils.HBASE_CLIENT_CONNECTION_IMPL, Class<?> clazz = conf.getClass(ConnectionUtils.HBASE_CLIENT_CONNECTION_IMPL,
ConnectionOverAsyncConnection.class, Connection.class); ConnectionOverAsyncConnection.class, Connection.class);
if (clazz != ConnectionOverAsyncConnection.class) { if (clazz != ConnectionOverAsyncConnection.class) {
@ -293,6 +302,11 @@ public class ConnectionFactory {
future.completeExceptionally(new IOException("clusterid came back null")); future.completeExceptionally(new IOException("clusterid came back null"));
return; return;
} }
if (System.getenv().containsKey(ENV_OAUTHBEARER_TOKEN)) {
OAuthBearerTokenUtil.addTokenFromEnvironmentVar(user, System.getenv(ENV_OAUTHBEARER_TOKEN));
}
Class<? extends AsyncConnection> clazz = conf.getClass(HBASE_CLIENT_ASYNC_CONNECTION_IMPL, Class<? extends AsyncConnection> clazz = conf.getClass(HBASE_CLIENT_ASYNC_CONNECTION_IMPL,
AsyncConnectionImpl.class, AsyncConnection.class); AsyncConnectionImpl.class, AsyncConnection.class);
try { try {

View File

@ -17,7 +17,7 @@
*/ */
package org.apache.hadoop.hbase.security.provider; package org.apache.hadoop.hbase.security.provider;
import static org.apache.hadoop.hbase.security.token.OAuthBearerTokenUtil.TOKEN_KIND; import static org.apache.hadoop.hbase.security.oauthbearer.OAuthBearerUtils.TOKEN_KIND;
import org.apache.hadoop.security.UserGroupInformation; import org.apache.hadoop.security.UserGroupInformation;
import org.apache.yetus.audience.InterfaceAudience; import org.apache.yetus.audience.InterfaceAudience;

View File

@ -17,7 +17,7 @@
*/ */
package org.apache.hadoop.hbase.security.provider; package org.apache.hadoop.hbase.security.provider;
import static org.apache.hadoop.hbase.security.token.OAuthBearerTokenUtil.TOKEN_KIND; import static org.apache.hadoop.hbase.security.oauthbearer.OAuthBearerUtils.TOKEN_KIND;
import java.util.Collection; import java.util.Collection;
import java.util.Optional; import java.util.Optional;
import org.apache.hadoop.conf.Configuration; import org.apache.hadoop.conf.Configuration;
@ -35,8 +35,7 @@ public class OAuthBearerSaslProviderSelector extends BuiltInProviderSelector {
private static final Logger LOG = LoggerFactory.getLogger(OAuthBearerSaslProviderSelector.class); private static final Logger LOG = LoggerFactory.getLogger(OAuthBearerSaslProviderSelector.class);
private final Text OAUTHBEARER_TOKEN_KIND_TEXT = private final Text OAUTHBEARER_TOKEN_KIND_TEXT = new Text(TOKEN_KIND);
new Text(TOKEN_KIND);
private OAuthBearerSaslClientAuthenticationProvider oauthbearer; private OAuthBearerSaslClientAuthenticationProvider oauthbearer;
@Override public void configure(Configuration conf, @Override public void configure(Configuration conf,

View File

@ -18,11 +18,19 @@
*/ */
package org.apache.hadoop.hbase.security.token; package org.apache.hadoop.hbase.security.token;
import static org.apache.hadoop.hbase.client.ConnectionFactory.ENV_OAUTHBEARER_TOKEN;
import static org.apache.hadoop.hbase.security.oauthbearer.OAuthBearerUtils.TOKEN_KIND;
import java.security.AccessController; import java.security.AccessController;
import java.security.PrivilegedAction; import java.security.PrivilegedAction;
import java.time.Instant;
import java.time.ZonedDateTime;
import java.time.format.DateTimeParseException;
import java.util.Optional;
import javax.security.auth.Subject; import javax.security.auth.Subject;
import org.apache.commons.lang3.StringUtils;
import org.apache.hadoop.hbase.security.User; import org.apache.hadoop.hbase.security.User;
import org.apache.hadoop.hbase.security.oauthbearer.OAuthBearerToken; import org.apache.hadoop.hbase.security.oauthbearer.OAuthBearerToken;
import org.apache.hadoop.hbase.security.oauthbearer.OAuthBearerUtils;
import org.apache.hadoop.hbase.security.oauthbearer.internals.OAuthBearerSaslClientProvider; import org.apache.hadoop.hbase.security.oauthbearer.internals.OAuthBearerSaslClientProvider;
import org.apache.hadoop.io.Text; import org.apache.hadoop.io.Text;
import org.apache.hadoop.security.token.Token; import org.apache.hadoop.security.token.Token;
@ -36,7 +44,6 @@ import org.slf4j.LoggerFactory;
@InterfaceAudience.Public @InterfaceAudience.Public
public final class OAuthBearerTokenUtil { public final class OAuthBearerTokenUtil {
private static final Logger LOG = LoggerFactory.getLogger(OAuthBearerTokenUtil.class); private static final Logger LOG = LoggerFactory.getLogger(OAuthBearerTokenUtil.class);
public static final String TOKEN_KIND = "JWT_AUTH_TOKEN";
static { static {
OAuthBearerSaslClientProvider.initialize(); // not part of public API OAuthBearerSaslClientProvider.initialize(); // not part of public API
@ -68,8 +75,46 @@ public final class OAuthBearerTokenUtil {
} }
}; };
subject.getPrivateCredentials().add(jwt); subject.getPrivateCredentials().add(jwt);
if (LOG.isDebugEnabled()) {
LOG.debug("JWT token has been added to user credentials with expiry {}",
lifetimeMs == 0 ? "0" : Instant.ofEpochMilli(lifetimeMs).toString());
}
return null; return null;
} }
}); });
} }
/**
* Check whether an OAuth Beaerer token is provided in environment variable HBASE_JWT.
* Parse and add it to user private credentials, but only if another token is not already present.
*/
public static void addTokenFromEnvironmentVar(User user, String token) {
Optional<Token<?>> oauthBearerToken = user.getTokens().stream()
.filter((t) -> new Text(OAuthBearerUtils.TOKEN_KIND).equals(t.getKind()))
.findFirst();
if (oauthBearerToken.isPresent()) {
LOG.warn("Ignoring OAuth Bearer token in " + ENV_OAUTHBEARER_TOKEN + " environment "
+ "variable, because another token is already present");
return;
}
String[] tokens = token.split(",");
if (StringUtils.isEmpty(tokens[0])) {
return;
}
long lifetimeMs = 0;
if (tokens.length > 1) {
try {
ZonedDateTime lifetime = ZonedDateTime.parse(tokens[1]);
lifetimeMs = lifetime.toInstant().toEpochMilli();
} catch (DateTimeParseException e) {
throw new RuntimeException("Unable to parse JWT expiry: " + tokens[1], e);
}
} else {
throw new RuntimeException("Expiry information of JWT is missing");
}
addTokenForUser(user, tokens[0], lifetimeMs);
}
} }

View File

@ -0,0 +1,115 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.hadoop.hbase.security.token;
import static org.apache.hadoop.hbase.security.oauthbearer.OAuthBearerUtils.TOKEN_KIND;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import java.security.AccessController;
import java.security.PrivilegedAction;
import java.time.Instant;
import java.util.Optional;
import java.util.Set;
import javax.security.auth.Subject;
import org.apache.hadoop.hbase.HBaseConfiguration;
import org.apache.hadoop.hbase.security.User;
import org.apache.hadoop.hbase.security.oauthbearer.OAuthBearerToken;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.security.token.Token;
import org.junit.Test;
public class TestOAuthBearerTokenUtil {
@Test
public void testAddTokenFromEnvVar() {
// Arrange
User user = User.createUserForTesting(HBaseConfiguration.create(), "testuser", new String[] {});
String testToken = "some_base64_encoded_stuff,2022-01-25T16:59:48.614000+00:00";
// Act
OAuthBearerTokenUtil.addTokenFromEnvironmentVar(user, testToken);
// Assert
Optional<Token<?>> oauthBearerToken = user.getTokens().stream()
.filter((t) -> new Text(TOKEN_KIND).equals(t.getKind()))
.findFirst();
assertTrue("Token cannot be found in user tokens", oauthBearerToken.isPresent());
user.runAs(new PrivilegedAction<Object>() {
@Override public Object run() {
Subject subject = Subject.getSubject(AccessController.getContext());
Set<OAuthBearerToken> tokens = subject.getPrivateCredentials(OAuthBearerToken.class);
assertFalse("Token cannot be found in subject's private credentials", tokens.isEmpty());
OAuthBearerToken jwt = tokens.iterator().next();
assertEquals("Invalid encoded JWT value", "some_base64_encoded_stuff", jwt.value());
assertEquals("Invalid JWT expiry", "2022-01-25T16:59:48.614Z",
Instant.ofEpochMilli(jwt.lifetimeMs()).toString());
return null;
}
});
}
@Test(expected = RuntimeException.class)
public void testAddTokenEnvVarWithoutExpiry() {
// Arrange
User user = User.createUserForTesting(new HBaseConfiguration(), "testuser", new String[] {});
String testToken = "some_base64_encoded_stuff";
// Act
OAuthBearerTokenUtil.addTokenFromEnvironmentVar(user, testToken);
// Assert
}
@Test(expected = RuntimeException.class)
public void testAddTokenEnvVarWithInvalidExpiry() {
// Arrange
User user = User.createUserForTesting(new HBaseConfiguration(), "testuser", new String[] {});
String testToken = "some_base64_encoded_stuff,foobarblahblah328742";
// Act
OAuthBearerTokenUtil.addTokenFromEnvironmentVar(user, testToken);
// Assert
}
@Test
public void testAddTokenEnvVarTokenAlreadyPresent() {
// Arrange
User user = User.createUserForTesting(new HBaseConfiguration(), "testuser", new String[] {});
user.addToken(new Token<>(null, null, new Text(TOKEN_KIND), null));
String testToken = "some_base64_encoded_stuff,foobarblahblah328742";
// Act
OAuthBearerTokenUtil.addTokenFromEnvironmentVar(user, testToken);
// Assert
long numberOfTokens = user.getTokens().stream()
.filter((t) -> new Text(TOKEN_KIND).equals(t.getKind()))
.count();
assertEquals("Invalid number of tokens on User", 1, numberOfTokens);
user.runAs(new PrivilegedAction<Object>() {
@Override public Object run() {
Subject subject = Subject.getSubject(AccessController.getContext());
Set<OAuthBearerToken> tokens = subject.getPrivateCredentials(OAuthBearerToken.class);
assertTrue("Token should not have been added to subject's credentials", tokens.isEmpty());
return null;
}
});
}
}

View File

@ -25,6 +25,7 @@ import org.apache.yetus.audience.InterfaceAudience;
@InterfaceAudience.Private @InterfaceAudience.Private
public final class OAuthBearerUtils { public final class OAuthBearerUtils {
public static final String OAUTHBEARER_MECHANISM = "OAUTHBEARER"; public static final String OAUTHBEARER_MECHANISM = "OAUTHBEARER";
public static final String TOKEN_KIND = "HBASE_JWT_TOKEN";
/** /**
* Verifies configuration for OAuth Bearer authentication mechanism. * Verifies configuration for OAuth Bearer authentication mechanism.