/*
* The Alluxio Open Foundation licenses this work under the Apache License, version 2.0
* (the "License"). You may not use this work except in compliance with the License, which is
* available at www.apache.org/licenses/LICENSE-2.0
*
* This software is distributed on an "AS IS" basis, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
* either express or implied, as more fully set forth in the License.
*
* See the NOTICE file distributed with this work for information regarding copyright ownership.
*/
package alluxio.security.authentication;
import alluxio.Configuration;
import alluxio.ConfigurationTestUtils;
import alluxio.PropertyKey;
import alluxio.exception.status.UnauthenticatedException;
import alluxio.util.network.NetworkAddressUtils;
import org.apache.thrift.protocol.TBinaryProtocol;
import org.apache.thrift.server.TThreadPoolServer;
import org.apache.thrift.transport.TServerSocket;
import org.apache.thrift.transport.TTransport;
import org.apache.thrift.transport.TTransportException;
import org.apache.thrift.transport.TTransportFactory;
import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import java.net.InetSocketAddress;
import javax.security.sasl.AuthenticationException;
/**
* Unit test for methods of {@link TransportProvider}.
*
* In order to test methods that return kinds of TTransport for connection in different mode, we
* build Thrift servers and clients with specific TTransport, and let them connect.
*/
public final class TransportProviderTest {
private TThreadPoolServer mServer;
private InetSocketAddress mServerAddress;
private TServerSocket mServerTSocket;
private TransportProvider mTransportProvider;
/**
* The exception expected to be thrown.
*/
@Rule
public ExpectedException mThrown = ExpectedException.none();
/**
* Sets up the server before running a test.
*/
@Before
public void before() throws Exception {
// Use port 0 to assign each test case an available port (possibly different)
String localhost = NetworkAddressUtils.getLocalHostName();
mServerTSocket = new TServerSocket(new InetSocketAddress(localhost, 0));
int port = NetworkAddressUtils.getThriftPort(mServerTSocket);
mServerAddress = new InetSocketAddress(localhost, port);
}
@After
public void after() {
ConfigurationTestUtils.resetConfiguration();
}
/**
* In NOSASL mode, the TTransport used should be the same as Alluxio original code.
*/
@Test
public void nosaslAuthentrication() throws Exception {
Configuration.set(PropertyKey.SECURITY_AUTHENTICATION_TYPE, AuthType.NOSASL.getAuthName());
mTransportProvider = TransportProvider.Factory.create();
// start server
startServerThread();
// create client and connect to server
TTransport client = mTransportProvider.getClientTransport(mServerAddress);
client.open();
Assert.assertTrue(client.isOpen());
// clean up
client.close();
mServer.stop();
}
/**
* In SIMPLE mode, the TTransport mechanism is PLAIN. When server authenticate the connected
* client user, it use {@link SimpleAuthenticationProvider}.
*/
@Test
public void simpleAuthentication() throws Exception {
Configuration.set(PropertyKey.SECURITY_AUTHENTICATION_TYPE, AuthType.SIMPLE.getAuthName());
mTransportProvider = TransportProvider.Factory.create();
// start server
startServerThread();
// when connecting, authentication happens. It is a no-op in Simple mode.
TTransport client = mTransportProvider.getClientTransport(mServerAddress);
client.open();
Assert.assertTrue(client.isOpen());
// clean up
client.close();
mServer.stop();
}
/**
* In SIMPLE mode, if client's username is null, an exception should be thrown in client side.
*/
@Test
public void simpleAuthenticationNullUser() throws Exception {
Configuration.set(PropertyKey.SECURITY_AUTHENTICATION_TYPE, AuthType.SIMPLE.getAuthName());
mTransportProvider = TransportProvider.Factory.create();
// check case that user is null
mThrown.expect(UnauthenticatedException.class);
mThrown.expectMessage("PLAIN: authorization ID and password must be specified");
((PlainSaslTransportProvider) mTransportProvider)
.getClientTransport(null, "whatever", mServerAddress);
}
/**
* In SIMPLE mode, if client's password is null, an exception should be thrown in client side.
*/
@Test
public void simpleAuthenticationNullPassword() throws Exception {
Configuration.set(PropertyKey.SECURITY_AUTHENTICATION_TYPE, AuthType.SIMPLE.getAuthName());
mTransportProvider = TransportProvider.Factory.create();
// check case that password is null
mThrown.expect(UnauthenticatedException.class);
mThrown.expectMessage("PLAIN: authorization ID and password must be specified");
((PlainSaslTransportProvider) mTransportProvider)
.getClientTransport("anyone", null, mServerAddress);
}
/**
* In SIMPLE mode, if client's username is empty, an exception should be thrown in server side.
*/
@Test
public void simpleAuthenticationEmptyUser() throws Exception {
Configuration.set(PropertyKey.SECURITY_AUTHENTICATION_TYPE, AuthType.SIMPLE.getAuthName());
mTransportProvider = TransportProvider.Factory.create();
// start server
startServerThread();
// check case that user is empty
mThrown.expect(TTransportException.class);
mThrown.expectMessage("Peer indicated failure: Plain authentication failed: No authentication"
+ " identity provided");
TTransport client = ((PlainSaslTransportProvider) mTransportProvider)
.getClientTransport("", "whatever", mServerAddress);
try {
client.open();
} finally {
mServer.stop();
}
}
/**
* In SIMPLE mode, if client's password is empty, an exception should be thrown in server side.
* Although password is actually not used and we do not really authenticate the user in SIMPLE
* mode, we need the Plain SASL server has ability to check empty password.
*/
@Test
public void simpleAuthenticationEmptyPassword() throws Exception {
Configuration.set(PropertyKey.SECURITY_AUTHENTICATION_TYPE, AuthType.SIMPLE.getAuthName());
mTransportProvider = TransportProvider.Factory.create();
// start server
startServerThread();
// check case that password is empty
mThrown.expect(TTransportException.class);
mThrown.expectMessage(
"Peer indicated failure: Plain authentication failed: No password " + "provided");
TTransport client = ((PlainSaslTransportProvider) mTransportProvider)
.getClientTransport("anyone", "", mServerAddress);
try {
client.open();
} finally {
mServer.stop();
}
}
/**
* In CUSTOM mode, the TTransport mechanism is PLAIN. When server authenticate the connected
* client user, it use configured AuthenticationProvider. If the username:password pair matches, a
* connection should be built.
*/
@Test
public void customAuthenticationExactNamePasswordMatch() throws Exception {
Configuration.set(PropertyKey.SECURITY_AUTHENTICATION_TYPE, AuthType.CUSTOM.getAuthName());
Configuration.set(PropertyKey.SECURITY_AUTHENTICATION_CUSTOM_PROVIDER_CLASS,
ExactlyMatchAuthenticationProvider.class.getName());
mTransportProvider = TransportProvider.Factory.create();
// start server
startServerThread();
// when connecting, authentication happens. User's name:pwd pair matches and auth pass.
TTransport client = ((PlainSaslTransportProvider) mTransportProvider)
.getClientTransport(ExactlyMatchAuthenticationProvider.USERNAME,
ExactlyMatchAuthenticationProvider.PASSWORD, mServerAddress);
client.open();
Assert.assertTrue(client.isOpen());
// clean up
client.close();
mServer.stop();
}
/**
* In CUSTOM mode, If the username:password pair does not match based on the configured
* AuthenticationProvider, an exception should be thrown in server side.
*/
@Test
public void customAuthenticationExactNamePasswordNotMatch() throws Exception {
Configuration.set(PropertyKey.SECURITY_AUTHENTICATION_TYPE, AuthType.CUSTOM.getAuthName());
Configuration.set(PropertyKey.SECURITY_AUTHENTICATION_CUSTOM_PROVIDER_CLASS,
ExactlyMatchAuthenticationProvider.class.getName());
mTransportProvider = TransportProvider.Factory.create();
// start server
startServerThread();
// User with wrong password can not pass auth, and throw exception.
TTransport wrongClient = ((PlainSaslTransportProvider) mTransportProvider)
.getClientTransport(ExactlyMatchAuthenticationProvider.USERNAME, "wrong-password",
mServerAddress);
mThrown.expect(TTransportException.class);
mThrown.expectMessage(
"Peer indicated failure: Plain authentication failed: " + "User authentication fails");
try {
wrongClient.open();
} finally {
mServer.stop();
}
}
/**
* In CUSTOM mode, if client's username is null, an exception should be thrown in client side.
*/
@Test
public void customAuthenticationNullUser() throws Exception {
Configuration.set(PropertyKey.SECURITY_AUTHENTICATION_TYPE, AuthType.CUSTOM.getAuthName());
mTransportProvider = TransportProvider.Factory.create();
// check case that user is null
mThrown.expect(UnauthenticatedException.class);
mThrown.expectMessage("PLAIN: authorization ID and password must be specified");
((PlainSaslTransportProvider) mTransportProvider)
.getClientTransport(null, ExactlyMatchAuthenticationProvider.PASSWORD, mServerAddress);
}
/**
* In CUSTOM mode, if client's password is null, an exception should be thrown in client side.
*/
@Test
public void customAuthenticationNullPassword() throws Exception {
Configuration.set(PropertyKey.SECURITY_AUTHENTICATION_TYPE, AuthType.CUSTOM.getAuthName());
mTransportProvider = TransportProvider.Factory.create();
// check case that password is null
mThrown.expect(UnauthenticatedException.class);
mThrown.expectMessage("PLAIN: authorization ID and password must be specified");
((PlainSaslTransportProvider) mTransportProvider)
.getClientTransport(ExactlyMatchAuthenticationProvider.USERNAME, null, mServerAddress);
}
/**
* In CUSTOM mode, if client's username is empty, an exception should be thrown in server side.
*/
@Test
public void customAuthenticationEmptyUser() throws Exception {
Configuration.set(PropertyKey.SECURITY_AUTHENTICATION_TYPE, AuthType.CUSTOM.getAuthName());
Configuration.set(PropertyKey.SECURITY_AUTHENTICATION_CUSTOM_PROVIDER_CLASS,
ExactlyMatchAuthenticationProvider.class.getName());
mTransportProvider = TransportProvider.Factory.create();
// start server
startServerThread();
// check case that user is empty
mThrown.expect(TTransportException.class);
mThrown.expectMessage("Peer indicated failure: Plain authentication failed: No authentication"
+ " identity provided");
TTransport client = ((PlainSaslTransportProvider) mTransportProvider)
.getClientTransport("", ExactlyMatchAuthenticationProvider.PASSWORD, mServerAddress);
try {
client.open();
} finally {
mServer.stop();
}
}
/**
* In CUSTOM mode, if client's password is empty, an exception should be thrown in server side.
*/
@Test
public void customAuthenticationEmptyPassword() throws Exception {
Configuration.set(PropertyKey.SECURITY_AUTHENTICATION_TYPE, AuthType.CUSTOM.getAuthName());
Configuration.set(PropertyKey.SECURITY_AUTHENTICATION_CUSTOM_PROVIDER_CLASS,
ExactlyMatchAuthenticationProvider.class.getName());
mTransportProvider = TransportProvider.Factory.create();
// start server
startServerThread();
// check case that password is empty
mThrown.expect(TTransportException.class);
mThrown.expectMessage(
"Peer indicated failure: Plain authentication failed: No password provided");
TTransport client = ((PlainSaslTransportProvider) mTransportProvider)
.getClientTransport(ExactlyMatchAuthenticationProvider.USERNAME, "", mServerAddress);
try {
client.open();
} finally {
mServer.stop();
}
}
/**
* TODO(dong): In KERBEROS mode, ...
* Tests that an exception is thrown when trying to use KERBEROS mode.
*/
@Test
public void kerberosAuthentication() throws Exception {
Configuration.set(PropertyKey.SECURITY_AUTHENTICATION_TYPE, AuthType.KERBEROS.getAuthName());
// throw unsupported exception currently
mThrown.expect(UnsupportedOperationException.class);
mThrown.expectMessage("Kerberos is not supported currently.");
mTransportProvider = TransportProvider.Factory.create();
}
private void startServerThread() throws Exception {
// create args and use them to build a Thrift TServer
TTransportFactory tTransportFactory = mTransportProvider.getServerTransportFactory();
mServer = new TThreadPoolServer(
new TThreadPoolServer.Args(mServerTSocket).maxWorkerThreads(2).minWorkerThreads(1)
.processor(null).transportFactory(tTransportFactory)
.protocolFactory(new TBinaryProtocol.Factory(true, true)));
// start the server in a new thread
Thread serverThread = new Thread(new Runnable() {
@Override
public void run() {
mServer.serve();
}
});
serverThread.start();
// ensure server is running, and break if it does not start serving in 2 seconds.
int count = 40;
while (!mServer.isServing() && serverThread.isAlive()) {
if (count <= 0) {
throw new RuntimeException("TThreadPoolServer does not start serving");
}
Thread.sleep(50);
count--;
}
}
/**
* This customized authentication provider is used in CUSTOM mode. It authenticates the user by
* verifying the specific username:password pair.
*/
public static class ExactlyMatchAuthenticationProvider implements AuthenticationProvider {
static final String USERNAME = "alluxio";
static final String PASSWORD = "correct-password";
@Override
public void authenticate(String user, String password) throws AuthenticationException {
if (!user.equals(USERNAME) || !password.equals(PASSWORD)) {
throw new AuthenticationException("User authentication fails");
}
}
}
}