Skip to content

Commit 100693d

Browse files
committed
IMPALA-12093: impala-shell to preserve all cookies
Updates impala-shell to preserve all cookies by default, defined as setting 'http_cookie_names=*'. Prior behavior of restricting cookies to a user-specified list is preserved when 'http_cookie_names' is given any value besides '*'. Setting 'http_cookie_names=' prevents any cookies from being preserved. Adds verbose output that prints all cookies that are preserved by the HTTP client. Existing cookie tests with LDAP still work. Adds a test where Impala returns an extra cookie, and test verifies that verbose mode prints all expected cookies. Change-Id: Ic81f790288460b086ab218e6701e8115a996dfa7 Reviewed-on: http://gerrit.cloudera.org:8080/19827 Reviewed-by: Impala Public Jenkins <[email protected]> Tested-by: Michael Smith <[email protected]>
1 parent aea057f commit 100693d

9 files changed

+121
-34
lines changed

be/src/rpc/authentication.cc

+7
Original file line numberDiff line numberDiff line change
@@ -218,6 +218,9 @@ DEFINE_bool(enable_group_filter_check_for_authenticated_kerberos_user, false,
218218
"is the same Active Directory server). "
219219
"The default value is false, which provides backwards-compatible behavior.");
220220

221+
DEFINE_string_hidden(test_cookie, "",
222+
"Adds Set-Cookie: <test_cookie> to BasicAuth for testing.");
223+
221224
namespace impala {
222225

223226
// Sasl callbacks. Why are these here? Well, Sasl isn't that bright, and
@@ -674,6 +677,10 @@ bool BasicAuth(ThriftServer::ConnectionContext* connection_context,
674677
// Create a cookie to return.
675678
connection_context->return_headers.push_back(
676679
Substitute("Set-Cookie: $0", GenerateCookie(username, hash)));
680+
if (!FLAGS_test_cookie.empty()) {
681+
connection_context->return_headers.push_back(
682+
Substitute("Set-Cookie: $0", FLAGS_test_cookie));
683+
}
677684
return true;
678685
}
679686
connection_context->return_headers.push_back("WWW-Authenticate: Basic");

fe/src/test/java/org/apache/impala/customcluster/LdapImpalaShellTest.java

+34-11
Original file line numberDiff line numberDiff line change
@@ -19,12 +19,15 @@
1919

2020
import static org.apache.impala.testutil.LdapUtil.*;
2121
import static org.junit.Assert.assertEquals;
22+
import static org.junit.Assert.assertFalse;
2223
import static org.junit.Assert.assertTrue;
2324

2425
import com.google.common.collect.Range;
2526

2627
import java.io.IOException;
2728
import java.util.Arrays;
29+
import java.util.ArrayList;
30+
import java.util.Collections;
2831
import java.util.List;
2932
import org.apache.directory.server.annotations.CreateLdapServer;
3033
import org.apache.directory.server.annotations.CreateTransport;
@@ -76,7 +79,8 @@ protected boolean pythonSupportsSSLContext() throws Exception {
7679
// python -c "import ssl; print hasattr(ssl, 'create_default_context')"
7780
String[] cmd = {
7881
"python", "-c", "import ssl; print(hasattr(ssl, 'create_default_context'))"};
79-
return Boolean.parseBoolean(RunShellCommand.Run(cmd, true, "", "").replace("\n", ""));
82+
return Boolean.parseBoolean(
83+
RunShellCommand.Run(cmd, true, "", "").stdout.replace("\n", ""));
8084
}
8185

8286
/**
@@ -129,33 +133,34 @@ protected String[] buildCommand(
129133
/**
130134
* Tests ldap authentication using impala-shell.
131135
*/
132-
protected void testShellLdapAuthImpl() throws Exception {
136+
protected void testShellLdapAuthImpl(String extra_cookie) throws Exception {
133137
String query = "select logged_in_user()";
134138
// Templated shell commands to test a simple 'show tables' command.
135139
// 1. Valid username, password and default http_cookie_names. Should succeed with
136140
// cookies are being used.
137141
String[] validCommand = {"impala-shell.sh", "", "--ldap", "--auth_creds_ok_in_clear",
142+
"--verbose",
138143
String.format("--user=%s", TEST_USER_1),
139144
String.format("--ldap_password_cmd=printf %s", TEST_PASSWORD_1),
140145
String.format("--query=%s", query)};
141146
// 2. Valid username, password and matching http_cookie_names. Should succeed with
142147
// cookies are being used.
143148
String[] validCommandMatchingCookieNames = {"impala-shell.sh", "", "--ldap",
144-
"--auth_creds_ok_in_clear", "--http_cookie_names=impala.auth",
149+
"--auth_creds_ok_in_clear", "--verbose", "--http_cookie_names=impala.auth",
145150
String.format("--user=%s", TEST_USER_1),
146151
String.format("--ldap_password_cmd=printf %s", TEST_PASSWORD_1),
147152
String.format("--query=%s", query)};
148153
// 3. Valid username and password, but not matching http_cookie_names. Should succeed
149154
// with cookies are not being used.
150155
String[] validCommandMismatchingCookieNames = {"impala-shell.sh", "", "--ldap",
151-
"--auth_creds_ok_in_clear", "--http_cookie_names=impala.conn",
156+
"--auth_creds_ok_in_clear", "--verbose", "--http_cookie_names=impala.conn",
152157
String.format("--user=%s", TEST_USER_1),
153158
String.format("--ldap_password_cmd=printf %s", TEST_PASSWORD_1),
154159
String.format("--query=%s", query)};
155160
// 4. Valid username and password, but empty http_cookie_names. Should succeed with
156161
// cookies are not being used.
157162
String[] validCommandEmptyCookieNames = {"impala-shell.sh", "", "--ldap",
158-
"--auth_creds_ok_in_clear", "--http_cookie_names=",
163+
"--auth_creds_ok_in_clear", "--verbose", "--http_cookie_names=",
159164
String.format("--user=%s", TEST_USER_1),
160165
String.format("--ldap_password_cmd=printf %s", TEST_PASSWORD_1),
161166
String.format("--query=%s", query)};
@@ -168,35 +173,53 @@ protected void testShellLdapAuthImpl() throws Exception {
168173
"impala-shell.sh", "", String.format("--query=%s", query)};
169174
String protocolTemplate = "--protocol=%s";
170175

176+
// Sorted list of cookies for validCommand, where all cookies are preserved.
177+
List<String> preservedCookiesList = new ArrayList<>();
178+
preservedCookiesList.add("impala.auth");
179+
if (extra_cookie != null) {
180+
preservedCookiesList.add(extra_cookie);
181+
}
182+
Collections.sort(preservedCookiesList);
183+
String preservedCookies = String.join(", ", preservedCookiesList);
184+
171185
for (String p : getProtocolsToTest()) {
172186
String protocol = String.format(protocolTemplate, p);
173187
validCommand[1] = protocol;
174-
RunShellCommand.Run(validCommand, /*shouldSucceed*/ true, TEST_USER_1,
188+
RunShellCommand.Output result = RunShellCommand.Run(validCommand,
189+
/*shouldSucceed*/ true, TEST_USER_1,
175190
"Starting Impala Shell with LDAP-based authentication");
176191
if (p.equals("hs2-http")) {
177192
// Check that cookies are being used.
178193
verifyMetrics(Range.atLeast(1L), zero, Range.atLeast(1L), zero);
194+
assertTrue(result.stderr,
195+
result.stderr.contains("Preserving cookies: " + preservedCookies));
179196
}
180197
validCommandMatchingCookieNames[1] = protocol;
181-
RunShellCommand.Run(validCommandMatchingCookieNames, /*shouldSucceed*/ true,
182-
TEST_USER_1, "Starting Impala Shell with LDAP-based authentication");
198+
result = RunShellCommand.Run(validCommandMatchingCookieNames,
199+
/*shouldSucceed*/ true, TEST_USER_1,
200+
"Starting Impala Shell with LDAP-based authentication");
183201
if (p.equals("hs2-http")) {
184202
// Check that cookies are being used.
185203
verifyMetrics(Range.atLeast(2L), zero, Range.atLeast(2L), zero);
204+
assertTrue(result.stderr,
205+
result.stderr.contains("Preserving cookies: impala.auth"));
186206
}
187207
validCommandMismatchingCookieNames[1] = protocol;
188-
RunShellCommand.Run(validCommandMismatchingCookieNames, /*shouldSucceed*/ true,
189-
TEST_USER_1, "Starting Impala Shell with LDAP-based authentication");
208+
result = RunShellCommand.Run(validCommandMismatchingCookieNames,
209+
/*shouldSucceed*/ true, TEST_USER_1,
210+
"Starting Impala Shell with LDAP-based authentication");
190211
if (p.equals("hs2-http")) {
191212
// Check that cookies are NOT being used.
192213
verifyMetrics(Range.atLeast(2L), zero, Range.atLeast(2L), zero);
214+
assertFalse(result.stderr, result.stderr.contains("Preserving cookies:"));
193215
}
194216
validCommandEmptyCookieNames[1] = protocol;
195-
RunShellCommand.Run(validCommandEmptyCookieNames, /*shouldSucceed*/ true,
217+
result = RunShellCommand.Run(validCommandEmptyCookieNames, /*shouldSucceed*/ true,
196218
TEST_USER_1, "Starting Impala Shell with LDAP-based authentication");
197219
if (p.equals("hs2-http")) {
198220
// Check that cookies are NOT being used.
199221
verifyMetrics(Range.atLeast(2L), zero, Range.atLeast(2L), zero);
222+
assertFalse(result.stderr, result.stderr.contains("Preserving cookies:"));
200223
}
201224

202225
invalidCommand[1] = protocol;

fe/src/test/java/org/apache/impala/customcluster/LdapSearchBindImpalaShellTest.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@ private String getKerberosArgs() throws IOException {
9797
public void testShellLdapAuth() throws Exception {
9898
setUp("--ldap_user_search_basedn=dc=myorg,dc=com "
9999
+ "--ldap_user_filter=(&(objectClass=person)(cn={0}))");
100-
testShellLdapAuthImpl();
100+
testShellLdapAuthImpl(null);
101101
}
102102

103103
/**

fe/src/test/java/org/apache/impala/customcluster/LdapSimpleBindImpalaShellTest.java

+2-2
Original file line numberDiff line numberDiff line change
@@ -94,8 +94,8 @@ private String getKerberosArgs() throws IOException {
9494
*/
9595
@Test
9696
public void testShellLdapAuth() throws Exception {
97-
setUp("");
98-
testShellLdapAuthImpl();
97+
setUp("--test_cookie=impala.ldap=testShellLdapAuth");
98+
testShellLdapAuthImpl("impala.ldap");
9999
}
100100

101101
/**

fe/src/test/java/org/apache/impala/customcluster/RunShellCommand.java

+18-5
Original file line numberDiff line numberDiff line change
@@ -27,12 +27,25 @@
2727
* Helper class to run a shell command.
2828
*/
2929
class RunShellCommand {
30+
/**
31+
* Tuple wrapping stdout, stderr from Run.
32+
*/
33+
public static class Output {
34+
public Output(String out, String err) {
35+
stdout = out;
36+
stderr = err;
37+
}
38+
39+
public String stdout;
40+
public String stderr;
41+
}
42+
3043
/**
3144
* Run a shell command 'cmd'. If 'shouldSucceed' is true, the command is expected to
32-
* succeed, otherwise it is expected to fail. Returns the output (stdout) of the
45+
* succeed, otherwise it is expected to fail. Returns the output (stdout, stderr) of the
3346
* command.
3447
*/
35-
public static String Run(String[] cmd, boolean shouldSucceed, String expectedOut,
48+
public static Output Run(String[] cmd, boolean shouldSucceed, String expectedOut,
3649
String expectedErr) throws Exception {
3750
// run the command with the env variables inherited from the current process
3851
return Run(cmd, null, shouldSucceed, expectedOut, expectedErr);
@@ -41,10 +54,10 @@ public static String Run(String[] cmd, boolean shouldSucceed, String expectedOut
4154
/**
4255
* Run a shell command 'cmd' with custom 'env' variables.
4356
* If 'shouldSucceed' is true, the command is expected to
44-
* succeed, otherwise it is expected to fail. Returns the output (stdout) of the
57+
* succeed, otherwise it is expected to fail. Returns the output (stdout, stderr) of the
4558
* command.
4659
*/
47-
public static String Run(String[] cmd, String[] env, boolean shouldSucceed,
60+
public static Output Run(String[] cmd, String[] env, boolean shouldSucceed,
4861
String expectedOut, String expectedErr) throws Exception {
4962
Runtime rt = Runtime.getRuntime();
5063
Process process = rt.exec(cmd, env);
@@ -71,6 +84,6 @@ public static String Run(String[] cmd, String[] env, boolean shouldSucceed,
7184
// If the query succeeds, assert that the output is correct.
7285
String stdout = stdoutBuf.toString();
7386
if (shouldSucceed) assertTrue(stdout, stdout.contains(expectedOut));
74-
return stdout;
87+
return new Output(stdout, stderr);
7588
}
7689
}

shell/ImpalaHttpClient.py

+31-9
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
# specific language governing permissions and limitations
1717
# under the License.
1818
#
19+
from __future__ import print_function, unicode_literals
1920

2021
from io import BytesIO
2122
import os
@@ -31,7 +32,7 @@
3132

3233
from thrift.transport.TTransport import TTransportBase
3334
from shell_exceptions import HttpError, AuthenticationException
34-
from cookie_util import get_all_matching_cookies, get_cookie_expiry
35+
from cookie_util import get_all_matching_cookies, get_all_cookies, get_cookie_expiry
3536

3637
import six
3738

@@ -52,7 +53,8 @@ class ImpalaHttpClient(TTransportBase):
5253
MIN_REQUEST_SIZE_FOR_EXPECT = 1024
5354

5455
def __init__(self, uri_or_host, port=None, path=None, cafile=None, cert_file=None,
55-
key_file=None, ssl_context=None, http_cookie_names=None, socket_timeout_s=None):
56+
key_file=None, ssl_context=None, http_cookie_names=None, socket_timeout_s=None,
57+
verbose=False):
5658
"""ImpalaHttpClient supports two different types of construction:
5759
5860
ImpalaHttpClient(host, port, path) - deprecated
@@ -66,7 +68,8 @@ def __init__(self, uri_or_host, port=None, path=None, cafile=None, cert_file=Non
6668
http_cookie_names is used to specify a comma-separated list of possible cookie names
6769
used for cookie-based authentication or session management. If a cookie with one of
6870
these names is returned in an http response by the server or an intermediate proxy
69-
then it will be included in each subsequent request for the same connection.
71+
then it will be included in each subsequent request for the same connection. If it
72+
is set as wildcards, all cookies in an http response will be preserved.
7073
"""
7174
if port is not None:
7275
warnings.warn(
@@ -111,16 +114,22 @@ def __init__(self, uri_or_host, port=None, path=None, cafile=None, cert_file=Non
111114
else:
112115
self.realhost = self.realport = self.proxy_auth = None
113116
if (not http_cookie_names) or (str(http_cookie_names).strip() == ""):
117+
self.__preserve_all_cookies = False
114118
self.__http_cookie_dict = None
115119
self.__auth_cookie_names = None
120+
elif str(http_cookie_names).strip() == "*":
121+
self.__preserve_all_cookies = True
122+
self.__http_cookie_dict = dict()
123+
self.__auth_cookie_names = set()
116124
else:
125+
self.__preserve_all_cookies = False
117126
# Build a dictionary that maps cookie name to namedtuple.
118127
cookie_names = http_cookie_names.split(',')
119128
self.__http_cookie_dict = \
120129
{cn: Cookie(cookie=None, expiry_time=None) for cn in cookie_names}
121130
# Store the auth cookie names in __auth_cookie_names.
122131
# Assume auth cookie names end with ".auth".
123-
self.__auth_cookie_names = [cn for cn in cookie_names if cn.endswith(".auth")]
132+
self.__auth_cookie_names = {cn for cn in cookie_names if cn.endswith(".auth")}
124133
# Set __are_matching_cookies_found as True if matching cookies are found in response.
125134
self.__are_matching_cookies_found = False
126135
self.__wbuf = BytesIO()
@@ -134,6 +143,7 @@ def __init__(self, uri_or_host, port=None, path=None, cafile=None, cert_file=Non
134143
self.__basic_auth = None
135144
self.__kerb_service = None
136145
self.__add_custom_headers_funcs = []
146+
self.__verbose = verbose
137147

138148
@staticmethod
139149
def basic_proxy_auth_header(proxy):
@@ -309,13 +319,25 @@ def getHttpCookieHeaderForRequest(self):
309319
# Extract cookies from response and save those cookies for which the cookie names
310320
# are in the cookie name list specified in the __init__().
311321
def extractHttpCookiesFromResponse(self):
312-
if self.__http_cookie_dict:
322+
if self.__preserve_all_cookies:
323+
matching_cookies = get_all_cookies(self.path, self.headers)
324+
elif self.__http_cookie_dict:
313325
matching_cookies = get_all_matching_cookies(
314326
self.__http_cookie_dict.keys(), self.path, self.headers)
315-
if matching_cookies:
316-
self.__are_matching_cookies_found = True
317-
for c in matching_cookies:
318-
self.__http_cookie_dict[c.key] = Cookie(c, get_cookie_expiry(c))
327+
else:
328+
matching_cookies = None
329+
330+
if matching_cookies:
331+
if self.__verbose:
332+
names = sorted([c.key for c in matching_cookies])
333+
print("Preserving cookies: " + ', '.join(names), file=sys.stderr)
334+
sys.stderr.flush()
335+
336+
self.__are_matching_cookies_found = True
337+
for c in matching_cookies:
338+
self.__http_cookie_dict[c.key] = Cookie(c, get_cookie_expiry(c))
339+
if c.key.endswith(".auth"):
340+
self.__auth_cookie_names.add(c.key)
319341

320342
# Return True if there are any saved cookies which are sent in previous request.
321343
def areHttpCookiesSaved(self):

shell/cookie_util.py

+20-2
Original file line numberDiff line numberDiff line change
@@ -50,8 +50,7 @@ def get_cookie_expiry(c):
5050
return None
5151

5252

53-
def get_all_matching_cookies(cookie_names, path, resp_headers):
54-
matching_cookies = None
53+
def get_cookies(resp_headers):
5554
if 'Set-Cookie' not in resp_headers:
5655
return None
5756

@@ -63,9 +62,28 @@ def get_all_matching_cookies(cookie_names, path, resp_headers):
6362
cookie_headers = resp_headers.get_all('Set-Cookie')
6463
for header in cookie_headers:
6564
cookies.load(header)
65+
return cookies
6666
except Exception:
6767
return None
6868

69+
70+
def get_all_cookies(path, resp_headers):
71+
cookies = get_cookies(resp_headers)
72+
if not cookies:
73+
return None
74+
75+
matching_cookies = []
76+
for c in cookies.values():
77+
if c and cookie_matches_path(c, path):
78+
matching_cookies.append(c)
79+
return matching_cookies
80+
81+
82+
def get_all_matching_cookies(cookie_names, path, resp_headers):
83+
cookies = get_cookies(resp_headers)
84+
if not cookies:
85+
return None
86+
6987
matching_cookies = []
7088
for cn in cookie_names:
7189
if cn in cookies:

shell/impala_client.py

+4-2
Original file line numberDiff line numberDiff line change
@@ -414,11 +414,13 @@ def _get_http_transport(self, connect_timeout_ms):
414414
url = "https://{0}/{1}".format(host_and_port, self.http_path)
415415
transport = ImpalaHttpClient(url, ssl_context=ssl_ctx,
416416
http_cookie_names=self.http_cookie_names,
417-
socket_timeout_s=self.http_socket_timeout_s)
417+
socket_timeout_s=self.http_socket_timeout_s,
418+
verbose=self.verbose)
418419
else:
419420
url = "http://{0}/{1}".format(host_and_port, self.http_path)
420421
transport = ImpalaHttpClient(url, http_cookie_names=self.http_cookie_names,
421-
socket_timeout_s=self.http_socket_timeout_s)
422+
socket_timeout_s=self.http_socket_timeout_s,
423+
verbose=self.verbose)
422424

423425
if self.use_ldap:
424426
# Set the BASIC authorization

shell/option_parser.py

+4-2
Original file line numberDiff line numberDiff line change
@@ -333,12 +333,14 @@ def get_option_parser(defaults):
333333
"spooling is disabled only a single row batch can be fetched at a "
334334
"time regardless of the specified fetch_size.")
335335
parser.add_option("--http_cookie_names", dest="http_cookie_names",
336-
default="impala.auth,impala.session.id",
336+
default="*",
337337
help="A comma-separated list of HTTP cookie names that are supported "
338338
"by the impala-shell. If a cookie with one of these names is "
339339
"returned in an http response by the server or an intermediate proxy "
340340
"then it will be included in each subsequent request for the same "
341-
"connection.")
341+
"connection. If set to wildcard (*), all cookies in an http response "
342+
"will be preserved. The name of an authentication cookie must end "
343+
"with '.auth', for example 'impala.auth'.")
342344
parser.add_option("--no_http_tracing", dest="no_http_tracing",
343345
action="store_true",
344346
help="Tracing http headers 'X-Request-Id', 'X-Impala-Session-Id', "

0 commit comments

Comments
 (0)