Skip to content

SQL: DATABASE() and USER() system functions #35946

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

Merged
merged 12 commits into from
Nov 28, 2018
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/

package org.elasticsearch.xpack.sql.qa.security;

import org.apache.http.HttpEntity;
import org.apache.http.entity.ContentType;
import org.apache.http.entity.StringEntity;
import org.elasticsearch.client.Request;
import org.elasticsearch.client.RequestOptions;
import org.elasticsearch.client.Response;
import org.elasticsearch.common.Nullable;
import org.elasticsearch.common.Strings;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.common.xcontent.XContentBuilder;
import org.elasticsearch.common.xcontent.XContentHelper;
import org.elasticsearch.common.xcontent.json.JsonXContent;
import org.elasticsearch.test.NotEqualMessageBuilder;
import org.elasticsearch.test.rest.ESRestTestCase;

import java.io.IOException;
import java.io.InputStream;
import java.sql.JDBCType;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;

import static org.elasticsearch.xpack.sql.qa.rest.RestSqlTestCase.columnInfo;

public class UserFunctionIT extends ESRestTestCase {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice tests! I have some questions/comments though:

  1. Maybe add a test that USER() is used in WHERE clause.
  2. Have a test with null user
  3. What happens if somehow 2 test methods attempt to create the same user? Maybe we need to delete them after each method is run?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added tests with WHERE. null user is handled in https://github.com/elastic/elasticsearch/pull/35946/files#diff-36793f91ca7279a48976a3247c18272fR32. Good point about clashing users, so I've changed the way the usernames are managed.


private static final String SQL = "SELECT USER()";
// role defined in roles.yml
private static final String MINIMAL_ACCESS_ROLE = "rest_minimal";

@Override
protected Settings restClientSettings() {
return RestSqlIT.securitySettings();
}

@Override
protected String getProtocol() {
return RestSqlIT.SSL_ENABLED ? "https" : "http";
}

public void testSingleRandomUser() throws IOException {
String randomUserName = randomAlphaOfLengthBetween(1, 15);
String mode = randomMode().toString();
createUser(randomUserName, MINIMAL_ACCESS_ROLE);

Map<String, Object> expected = new HashMap<>();
expected.put("columns", Arrays.asList(
columnInfo(mode, "USER", "keyword", JDBCType.VARCHAR, 0)));
expected.put("rows", Arrays.asList(Arrays.asList(randomUserName)));
Map<String, Object> actual = runSql(randomUserName, mode, SQL);

assertResponse(expected, actual);
}

public void testMultipleRandomUsersAccess() throws IOException {
// create a random number of users
int usersCount = randomIntBetween(5, 15);
Set<String> users = new HashSet<String>();
for(int i = 0; i < usersCount; i++) {
String randomUserName = randomAlphaOfLengthBetween(1, 15);
users.add(randomUserName);
createUser(randomUserName, MINIMAL_ACCESS_ROLE);
}

// run 30 queries and pick randomly each time one of the 5-15 users created previously
for (int i = 0; i < 30; i++) {
String mode = randomMode().toString();
String randomlyPickedUsername = randomFrom(users);
Map<String, Object> expected = new HashMap<>();

expected.put("columns", Arrays.asList(
columnInfo(mode, "USER", "keyword", JDBCType.VARCHAR, 0)));
expected.put("rows", Arrays.asList(Arrays.asList(randomlyPickedUsername)));
Map<String, Object> actual = runSql(randomlyPickedUsername, mode, SQL);

// expect the user that ran the query to be the same as the one returned by the `USER()` function
assertResponse(expected, actual);
}
}

public void testSelectUserFromIndex() throws IOException {
index("{\"test\":\"doc1\"}",
"{\"test\":\"doc2\"}",
"{\"test\":\"doc3\"}");
String randomUserName = randomAlphaOfLengthBetween(1, 15);
String mode = randomMode().toString();
createUser(randomUserName, MINIMAL_ACCESS_ROLE);

Map<String, Object> expected = new HashMap<>();
expected.put("columns", Arrays.asList(
columnInfo(mode, "USER", "keyword", JDBCType.VARCHAR, 0)));
expected.put("rows", Arrays.asList(Arrays.asList(randomUserName),
Arrays.asList(randomUserName),
Arrays.asList(randomUserName)));
Map<String, Object> actual = runSql(randomUserName, mode, "SELECT USER() FROM test");

assertResponse(expected, actual);
}

private void createUser(String name, String role) throws IOException {
Request request = new Request("PUT", "/_xpack/security/user/" + name);
XContentBuilder user = JsonXContent.contentBuilder().prettyPrint();
user.startObject(); {
user.field("password", "testpass");
user.field("roles", role);
}
user.endObject();
request.setJsonEntity(Strings.toString(user));
client().performRequest(request);
}

private Map<String, Object> runSql(@Nullable String asUser, String mode, String sql) throws IOException {
return runSql(asUser, mode, new StringEntity("{\"query\": \"" + sql + "\"}", ContentType.APPLICATION_JSON));
}

private Map<String, Object> runSql(@Nullable String asUser, String mode, HttpEntity entity) throws IOException {
Request request = new Request("POST", "/_xpack/sql");
if (false == mode.isEmpty()) {
request.addParameter("mode", mode);
}
if (asUser != null) {
RequestOptions.Builder options = request.getOptions().toBuilder();
options.addHeader("es-security-runas-user", asUser);
request.setOptions(options);
}
request.setEntity(entity);
return toMap(client().performRequest(request));
}

private void assertResponse(Map<String, Object> expected, Map<String, Object> actual) {
if (false == expected.equals(actual)) {
NotEqualMessageBuilder message = new NotEqualMessageBuilder();
message.compareMaps(actual, expected);
fail("Response does not match:\n" + message.toString());
}
}

private static Map<String, Object> toMap(Response response) throws IOException {
try (InputStream content = response.getEntity().getContent()) {
return XContentHelper.convertToMap(JsonXContent.jsonXContent, content, false);
}
}

private String randomMode() {
return randomFrom("plain", "jdbc", "");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not use the Mode enum?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mode is not visible in the qa project.

}

private void index(String... docs) throws IOException {
Request request = new Request("POST", "/test/test/_bulk");
request.addParameter("refresh", "true");
StringBuilder bulk = new StringBuilder();
for (String doc : docs) {
bulk.append("{\"index\":{}\n");
bulk.append(doc + "\n");
}
request.setJsonEntity(bulk.toString());
client().performRequest(request);
}
}
2 changes: 2 additions & 0 deletions x-pack/plugin/sql/qa/src/main/resources/command.csv-spec
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,8 @@ SUBSTRING |SCALAR
UCASE |SCALAR
CAST |SCALAR
CONVERT |SCALAR
DATABASE |SCALAR
USER |SCALAR
SCORE |SCORE
;

Expand Down
2 changes: 2 additions & 0 deletions x-pack/plugin/sql/qa/src/main/resources/docs.csv-spec
Original file line number Diff line number Diff line change
Expand Up @@ -285,6 +285,8 @@ SUBSTRING |SCALAR
UCASE |SCALAR
CAST |SCALAR
CONVERT |SCALAR
DATABASE |SCALAR
USER |SCALAR
SCORE |SCORE
// end::showFunctions
;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
import org.elasticsearch.xpack.sql.plan.logical.With;
import org.elasticsearch.xpack.sql.rule.Rule;
import org.elasticsearch.xpack.sql.rule.RuleExecutor;
import org.elasticsearch.xpack.sql.session.Configuration;
import org.elasticsearch.xpack.sql.type.DataType;
import org.elasticsearch.xpack.sql.type.DataTypeConversion;
import org.elasticsearch.xpack.sql.type.DataTypes;
Expand All @@ -60,7 +61,6 @@
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.TimeZone;

import static java.util.Collections.emptyList;
import static java.util.Collections.singletonList;
Expand All @@ -77,19 +77,19 @@ public class Analyzer extends RuleExecutor<LogicalPlan> {
*/
private final IndexResolution indexResolution;
/**
* Time zone in which we're executing this SQL. It is attached to functions
* that deal with date and time.
* Per-request specific settings needed in some of the functions (timezone, username and clustername),
* to which they are attached.
*/
private final TimeZone timeZone;
private final Configuration configuration;
/**
* The verifier has the role of checking the analyzed tree for failures and build a list of failures.
*/
private final Verifier verifier;

public Analyzer(FunctionRegistry functionRegistry, IndexResolution results, TimeZone timeZone, Verifier verifier) {
public Analyzer(FunctionRegistry functionRegistry, IndexResolution results, Configuration configuration, Verifier verifier) {
this.functionRegistry = functionRegistry;
this.indexResolution = results;
this.timeZone = timeZone;
this.configuration = configuration;
this.verifier = verifier;
}

Expand Down Expand Up @@ -815,7 +815,7 @@ protected LogicalPlan resolve(LogicalPlan plan, Map<String, List<Function>> seen
}
// TODO: look into Generator for significant terms, etc..
FunctionDefinition def = functionRegistry.resolveFunction(functionName);
Function f = uf.buildResolved(timeZone, def);
Function f = uf.buildResolved(configuration, def);

list.add(f);
return f;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
package org.elasticsearch.xpack.sql.expression.function;

import org.elasticsearch.xpack.sql.session.Configuration;

import java.util.List;
import java.util.Locale;
import java.util.TimeZone;

import static java.lang.String.format;

Expand All @@ -17,7 +18,7 @@ public class FunctionDefinition {
*/
@FunctionalInterface
public interface Builder {
Function build(UnresolvedFunction uf, boolean distinct, TimeZone tz);
Function build(UnresolvedFunction uf, boolean distinct, Configuration configuration);
}

private final String name;
Expand Down
Loading