Skip to content

Commit

Permalink
Modify Shell to authenticate user on call to config (#3440)
Browse files Browse the repository at this point in the history
Closes #3433 

---------

Co-authored-by: Christopher Tubbs <ctubbsii@apache.org>
  • Loading branch information
dlmarion and ctubbsii committed Jun 8, 2023
1 parent 3439fde commit 0f23897
Show file tree
Hide file tree
Showing 4 changed files with 166 additions and 32 deletions.
66 changes: 38 additions & 28 deletions shell/src/main/java/org/apache/accumulo/shell/Shell.java
Expand Up @@ -189,6 +189,7 @@
*/
@AutoService(KeywordExecutable.class)
public class Shell extends ShellOptions implements KeywordExecutable {

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 +258,35 @@ public Shell(LineReader reader) {
this.writer = terminal.writer();
}

// this is visible only for FateCommandTest, otherwise, should be private or inline
protected boolean authenticateUser(AccumuloClient client, AuthenticationToken token)
throws AccumuloException, AccumuloSecurityException {
return client.securityOperations().authenticateUser(client.whoami(), token);
}

private AuthenticationToken getAuthenticationToken(String principal, String authenticationString,
String passwordPrompt) {
AuthenticationToken token = null;
if (authenticationString == 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(authenticationString) || authenticationString == null) {
authenticationString = reader.readLine(passwordPrompt, '*');
}
if (authenticationString == null) {
// User cancel, e.g. Ctrl-D pressed
throw new ParameterException("No password or token option supplied");
} else {
token = new PasswordToken(authenticationString);
}
}
return token;
}

/**
* Configures the shell using the provided options. Not for client use.
*
Expand Down Expand Up @@ -331,27 +361,13 @@ public boolean config(String... args) throws IOException {
exitCode = 1;
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);
}
}
String authenticationString = options.getPassword();
final AuthenticationToken token =
getAuthenticationToken(principal, authenticationString, "Password: ");
try {
this.setTableName("");
accumuloClient = Accumulo.newClient().from(clientProperties).as(principal, token).build();
authenticateUser(accumuloClient, token);
context = (ClientContext) accumuloClient;
} catch (Exception e) {
printException(e);
Expand Down Expand Up @@ -732,16 +748,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, "Enter current password for '" + accumuloClient.whoami() + "': ");
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 +1198,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
Expand Up @@ -50,7 +50,7 @@ public class ShellOptionsJC {
+ " 'file:<local file containing the password>', 'env:<variable containing"
+ " the pass>', or stdin)",
converter = ClientOpts.PasswordConverter.class)
private String password;
private String authenticationString;

@DynamicParameter(names = {"-l"},
description = "command line properties in the format key=value. Reuse -l for each property")
Expand Down Expand Up @@ -145,7 +145,7 @@ public String getUsername() throws Exception {
}

public String getPassword() {
return password;
return authenticationString;
}

public boolean isTabCompletionDisabled() {
Expand Down 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
Expand Up @@ -36,6 +36,8 @@
import java.nio.file.Files;
import java.util.List;

import org.apache.accumulo.core.client.AccumuloClient;
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 @@ -320,7 +322,12 @@ private Shell createShell(TestOutputStream output) throws IOException {
Terminal terminal = new DumbTerminal(new FileInputStream(FileDescriptor.in), output);
terminal.setSize(new Size(80, 24));
LineReader reader = LineReaderBuilder.builder().terminal(terminal).build();
Shell shell = new Shell(reader);
Shell shell = new Shell(reader) {
@Override
protected boolean authenticateUser(AccumuloClient client, AuthenticationToken token) {
return true;
}
};
shell.setLogErrorsToConsole();
return shell;
}
Expand Down
@@ -0,0 +1,117 @@
/*
* 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
*
* https://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.accumulo.test.shell;

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

import java.io.IOException;
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 ShellAuthenticatorIT 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;
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();
}

@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", getCluster().getClientPropsPath()));
}

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

@Test
public void testClientPropertiesBadPassword() throws IOException {
shell = new Shell(reader);
shell.setLogErrorsToConsole();
assertFalse(shell.config("-u", getAdminPrincipal(), "-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();
shell = new Shell(reader);
shell.setLogErrorsToConsole();
assertTrue(
shell.config("--auth-timeout", "1", "--config-file", getCluster().getClientPropsPath()));
Thread.sleep(90000);
shell.execCommand("whoami", false, false);
assertTrue(output.get().contains("root"));
}

}

0 comments on commit 0f23897

Please sign in to comment.