Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Modify Shell to authenticate user on call to config #3440

Merged
merged 9 commits into from
Jun 8, 2023
72 changes: 44 additions & 28 deletions shell/src/main/java/org/apache/accumulo/shell/Shell.java
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,16 @@
*/
@AutoService(KeywordExecutable.class)
public class Shell extends ShellOptions implements KeywordExecutable {

public static interface Authenticator {
default boolean authenticateUser(AccumuloClient client, String principal,
AuthenticationToken token) throws AccumuloException, AccumuloSecurityException {
return client.securityOperations().authenticateUser(principal, token);
}
}

public static Authenticator AUTHENTICATOR = new Authenticator() {};
ctubbsii marked this conversation as resolved.
Show resolved Hide resolved

public static final Logger log = LoggerFactory.getLogger(Shell.class);
private static final Logger audit = LoggerFactory.getLogger(Shell.class.getName() + ".audit");

Expand Down Expand Up @@ -257,6 +267,32 @@ public Shell(LineReader reader) {
this.writer = terminal.writer();
}

private boolean authenticateUser(AccumuloClient client, AuthenticationToken token)
throws AccumuloException, AccumuloSecurityException {
return AUTHENTICATOR.authenticateUser(client, client.whoami(), token);
}

private AuthenticationToken getAuthenticationToken(String principal, String password) {
AuthenticationToken token = null;
if (password == null && clientProperties.containsKey(ClientProperty.AUTH_TOKEN.getKey())
&& principal.equals(ClientProperty.AUTH_PRINCIPAL.getValue(clientProperties))) {
token = ClientProperty.getAuthenticationToken(clientProperties);
}
if (token == null) {
// Read password if the user explicitly asked for it, or didn't specify anything at all
if (PasswordConverter.STDIN.equals(password) || password == null) {
password = reader.readLine("Password: ", '*');
}
if (password == null) {
// User cancel, e.g. Ctrl-D pressed
throw new ParameterException("No password or token option supplied");
} else {
token = new PasswordToken(password);
}
}
return token;
}

/**
* Configures the shell using the provided options. Not for client use.
*
Expand Down Expand Up @@ -332,26 +368,12 @@ public boolean config(String... args) throws IOException {
return false;
}
String password = options.getPassword();
AuthenticationToken token = null;
if (password == null && clientProperties.containsKey(ClientProperty.AUTH_TOKEN.getKey())
&& principal.equals(ClientProperty.AUTH_PRINCIPAL.getValue(clientProperties))) {
token = ClientProperty.getAuthenticationToken(clientProperties);
}
if (token == null) {
// Read password if the user explicitly asked for it, or didn't specify anything at all
if (PasswordConverter.STDIN.equals(password) || password == null) {
password = reader.readLine("Password: ", '*');
}
if (password == null) {
// User cancel, e.g. Ctrl-D pressed
throw new ParameterException("No password or token option supplied");
} else {
token = new PasswordToken(password);
}
}
final AuthenticationToken authToken = getAuthenticationToken(principal, password);
try {
this.setTableName("");
accumuloClient = Accumulo.newClient().from(clientProperties).as(principal, token).build();
accumuloClient =
Accumulo.newClient().from(clientProperties).as(principal, authToken).build();
authenticateUser(accumuloClient, authToken);
context = (ClientContext) accumuloClient;
} catch (Exception e) {
printException(e);
Expand Down Expand Up @@ -732,16 +754,10 @@ public void execCommand(String input, boolean ignoreAuthTimeout, boolean echoPro
writer.println("Shell has been idle for too long. Please re-authenticate.");
boolean authFailed = true;
do {
String pwd = readMaskedLine(
"Enter current password for '" + accumuloClient.whoami() + "': ", '*');
if (pwd == null) {
writer.println();
return;
} // user canceled

final AuthenticationToken authToken =
getAuthenticationToken(accumuloClient.whoami(), null);
try {
authFailed = !accumuloClient.securityOperations()
.authenticateUser(accumuloClient.whoami(), new PasswordToken(pwd));
authFailed = !authenticateUser(accumuloClient, authToken);
} catch (Exception e) {
++exitCode;
printException(e);
Expand Down Expand Up @@ -1188,7 +1204,7 @@ public void updateUser(String principal, AuthenticationToken token)
throws AccumuloException, AccumuloSecurityException {
var newClient = Accumulo.newClient().from(clientProperties).as(principal, token).build();
try {
newClient.securityOperations().authenticateUser(principal, token);
authenticateUser(newClient, token);
} catch (AccumuloSecurityException e) {
// new client can't authenticate; close and discard
newClient.close();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -239,7 +239,7 @@ public Properties getClientProperties() {
return props;
}

static class PositiveInteger implements IParameterValidator {
public static class PositiveInteger implements IParameterValidator {
@Override
public void validate(String name, String value) throws ParameterException {
int n = -1;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,10 @@
import java.nio.file.Files;
import java.util.List;

import org.apache.accumulo.core.client.AccumuloClient;
import org.apache.accumulo.core.client.AccumuloException;
import org.apache.accumulo.core.client.AccumuloSecurityException;
import org.apache.accumulo.core.client.security.tokens.AuthenticationToken;
import org.apache.accumulo.core.clientImpl.ClientContext;
import org.apache.accumulo.core.fate.AdminUtil;
import org.apache.accumulo.core.fate.ReadOnlyRepo;
Expand Down Expand Up @@ -130,6 +134,13 @@ public void reset() {
public static void setup() {
zk = createMock(ZooReaderWriter.class);
managerLockPath = createMock(ServiceLockPath.class);
Shell.AUTHENTICATOR = new Shell.Authenticator() {
@Override
public boolean authenticateUser(AccumuloClient client, String principal,
AuthenticationToken token) throws AccumuloException, AccumuloSecurityException {
return true;
}
};
}

@Test
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
package org.apache.accumulo.test.shell;

import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;

import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.nio.file.Files;
import java.util.Properties;
import java.util.TimeZone;

import org.apache.accumulo.harness.SharedMiniClusterBase;
import org.apache.accumulo.shell.Shell;
import org.apache.accumulo.test.shell.ShellIT.StringInputStream;
import org.apache.accumulo.test.shell.ShellIT.TestOutputStream;
import org.jline.reader.LineReader;
import org.jline.reader.LineReaderBuilder;
import org.jline.terminal.Size;
import org.jline.terminal.Terminal;
import org.jline.terminal.impl.DumbTerminal;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

public class ShellAuthenticatorTest extends SharedMiniClusterBase {

@BeforeAll
public static void setup() throws Exception {
SharedMiniClusterBase.startMiniCluster();
}

@AfterAll
public static void teardown() {
SharedMiniClusterBase.stopMiniCluster();
}

private StringInputStream input;
private TestOutputStream output;
private Shell shell;
private File config;
public LineReader reader;
public Terminal terminal;

@BeforeEach
public void setupShell() throws IOException {
TimeZone.setDefault(TimeZone.getTimeZone("UTC"));
output = new TestOutputStream();
input = new StringInputStream();
terminal = new DumbTerminal(input, output);
terminal.setSize(new Size(80, 24));
reader = LineReaderBuilder.builder().terminal(terminal).build();
if (config != null) {
config.delete();
}
config = Files.createTempFile(null, null).toFile();
try (FileWriter writer = new FileWriter(config)) {
Properties p = super.getClientProps();
p.store(writer, null);
}
config.deleteOnExit();
}

@AfterEach
public void tearDownShell() {
if (shell != null) {
shell.shutdown();
}
}

@Test
public void testClientPropertiesFile() throws IOException {
shell = new Shell(reader);
shell.setLogErrorsToConsole();
assertTrue(shell.config("--config-file", config.toString()));
}

@Test
public void testClientProperties() throws IOException {
shell = new Shell(reader);
shell.setLogErrorsToConsole();
assertTrue(shell.config("-u", "root", "-p", getRootPassword(), "-zi",
getCluster().getInstanceName(), "-zh", getCluster().getZooKeepers()));
}

@Test
public void testClientPropertiesBadPassword() throws IOException {
dlmarion marked this conversation as resolved.
Show resolved Hide resolved
shell = new Shell(reader);
shell.setLogErrorsToConsole();
assertFalse(shell.config("-u", "root", "-p", "BADPW", "-zi",
getCluster().getInstanceName(), "-zh", getCluster().getZooKeepers()));
}

@Test
public void testAuthTimeoutPropertiesFile() throws IOException, InterruptedException {
TimeZone.setDefault(TimeZone.getTimeZone("UTC"));
output = new TestOutputStream();
input = new StringInputStream();
terminal = new DumbTerminal(input, output);
terminal.setSize(new Size(80, 24));
reader = LineReaderBuilder.builder().terminal(terminal).build();
if (config != null) {
config.delete();
}
config = Files.createTempFile(null, null).toFile();
try (FileWriter writer = new FileWriter(config)) {
Properties p = super.getClientProps();
p.store(writer, null);
}
config.deleteOnExit();
shell = new Shell(reader);
shell.setLogErrorsToConsole();
assertTrue(shell.config("--auth-timeout", "2", "--config-file", config.toString()));
Thread.sleep(90000);
shell.execCommand("whoami", false, false);
assertTrue(output.get().contains("root"));
}

}