Skip to content

Commit

Permalink
SQL: DATABASE() and USER() system functions (#35946)
Browse files Browse the repository at this point in the history
(cherry picked from commit aabff73)
  • Loading branch information
astefan committed Nov 28, 2018
1 parent f694319 commit 6869ace
Show file tree
Hide file tree
Showing 35 changed files with 786 additions and 112 deletions.
2 changes: 2 additions & 0 deletions docs/reference/sql/functions/index.asciidoc
Expand Up @@ -13,6 +13,7 @@
* <<sql-functions-string, String>>
* <<sql-functions-type-conversion,Type Conversion>>
* <<sql-functions-conditional, Conditional>>
* <<sql-functions-system, System>>

include::operators.asciidoc[]
include::aggs.asciidoc[]
Expand All @@ -22,3 +23,4 @@ include::math.asciidoc[]
include::string.asciidoc[]
include::type-conversion.asciidoc[]
include::conditional.asciidoc[]
include::system.asciidoc[]
52 changes: 52 additions & 0 deletions docs/reference/sql/functions/system.asciidoc
@@ -0,0 +1,52 @@
[role="xpack"]
[testenv="basic"]
[[sql-functions-system]]
=== System Functions

These functions return metadata type of information about the system being queried.

[[sql-functions-system-database]]
==== `DATABASE`

.Synopsis:
[source, sql]
--------------------------------------------------
DATABASE()
--------------------------------------------------

*Input*:

*Output*: string

.Description:

Returns the name of the database being queried. In the case of Elasticsearch SQL, this
is the name of the Elasticsearch cluster. This function should always return a non-null
value.

["source","sql",subs="attributes,callouts,macros"]
--------------------------------------------------
include-tagged::{sql-specs}/docs.csv-spec[database]
--------------------------------------------------

[[sql-functions-system-user]]
==== `USER`

.Synopsis:
[source, sql]
--------------------------------------------------
USER()
--------------------------------------------------
*Input*:

*Output*: string

.Description:

Returns the username of the authenticated user executing the query. This function can
return `null` in case Security is disabled.

["source","sql",subs="attributes,callouts,macros"]
--------------------------------------------------
include-tagged::{sql-specs}/docs.csv-spec[user]
--------------------------------------------------
Expand Up @@ -208,8 +208,8 @@ public String getStringFunctions() throws SQLException {

@Override
public String getSystemFunctions() throws SQLException {
// TODO: sync this with the grammar
return EMPTY;
// https://docs.microsoft.com/en-us/sql/odbc/reference/appendixes/system-functions?view=sql-server-2017
return "DATABASE, IFNULL, USER";
}

@Override
Expand Down
@@ -0,0 +1,223 @@
/*
* 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.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 org.junit.After;
import org.junit.Before;
import org.junit.Rule;
import org.junit.rules.TestName;

import java.io.IOException;
import java.io.InputStream;
import java.sql.JDBCType;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

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

public class UserFunctionIT extends ESRestTestCase {

private static final String SQL = "SELECT USER()";
// role defined in roles.yml
private static final String MINIMAL_ACCESS_ROLE = "rest_minimal";
private List<String> users;
@Rule
public TestName name = new TestName();

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

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

@Before
private void setUpUsers() throws IOException {
int usersCount = name.getMethodName().startsWith("testSingle") ? 1 : randomIntBetween(5, 15);
users = new ArrayList<String>(usersCount);

for(int i = 0; i < usersCount; i++) {
String randomUserName = randomAlphaOfLengthBetween(1, 15);
users.add(randomUserName);
createUser(randomUserName, MINIMAL_ACCESS_ROLE);
}
}

@After
private void clearUsers() throws IOException {
for (String user : users) {
deleteUser(user);
}
}

public void testSingleRandomUser() throws IOException {
String mode = randomMode().toString();
String randomUserName = users.get(0);

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 testSingleRandomUserWithWhereEvaluatingTrue() throws IOException {
index("{\"test\":\"doc1\"}",
"{\"test\":\"doc2\"}",
"{\"test\":\"doc3\"}");
String mode = randomMode().toString();
String randomUserName = users.get(0);

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, SQL + " FROM test WHERE USER()='" + randomUserName + "' LIMIT 3");
assertResponse(expected, actual);
}

@AwaitsFix(bugUrl="https://github.com/elastic/elasticsearch/issues/35980")
public void testSingleRandomUserWithWhereEvaluatingFalse() throws IOException {
index("{\"test\":\"doc1\"}",
"{\"test\":\"doc2\"}",
"{\"test\":\"doc3\"}");
String mode = randomMode().toString();
String randomUserName = users.get(0);

Map<String, Object> expected = new HashMap<>();
expected.put("columns", Arrays.asList(
columnInfo(mode, "USER", "keyword", JDBCType.VARCHAR, 0)));
expected.put("rows", Collections.<ArrayList<String>>emptyList());
String anotherRandomUserName = randomValueOtherThan(randomUserName, () -> randomAlphaOfLengthBetween(1, 15));
Map<String, Object> actual = runSql(randomUserName, mode, SQL + " FROM test WHERE USER()='" + anotherRandomUserName + "' LIMIT 3");
assertResponse(expected, actual);
}

public void testMultipleRandomUsersAccess() throws IOException {
// 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 testSingleUserSelectFromIndex() throws IOException {
index("{\"test\":\"doc1\"}",
"{\"test\":\"doc2\"}",
"{\"test\":\"doc3\"}");
String mode = randomMode().toString();
String randomUserName = users.get(0);

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 LIMIT 3");

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 void deleteUser(String name) throws IOException {
Request request = new Request("DELETE", "/_xpack/security/user/" + name);
client().performRequest(request);
}

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

private Map<String, Object> runSql(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", "");
}

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
Expand Up @@ -108,6 +108,8 @@ SUBSTRING |SCALAR
UCASE |SCALAR
CAST |SCALAR
CONVERT |SCALAR
DATABASE |SCALAR
USER |SCALAR
SCORE |SCORE
;

Expand Down
26 changes: 26 additions & 0 deletions x-pack/plugin/sql/qa/src/main/resources/docs.csv-spec
Expand Up @@ -285,6 +285,8 @@ SUBSTRING |SCALAR
UCASE |SCALAR
CAST |SCALAR
CONVERT |SCALAR
DATABASE |SCALAR
USER |SCALAR
SCORE |SCORE
// end::showFunctions
;
Expand Down Expand Up @@ -1683,3 +1685,27 @@ SELECT null <=> null AS "equals";
true
// end::nullEqualsCompareTwoNulls
;

// ignored because tests run with a docs-not-worthy cluster name
// at the time of this test being ignored, the cluster name was x-pack_plugin_sql_qa_single-node_integTestCluster
database-Ignore
// tag::database
SELECT DATABASE();

DATABASE
---------------
elasticsearch
// end::database
;

// ignored because tests run with a docs-not-worthy user name
// at the time of this test being ignored, there was no user name being used
user-Ignore
// tag::user
SELECT USER();

USER
---------------
elastic
// end::user
;
23 changes: 22 additions & 1 deletion x-pack/plugin/sql/qa/src/main/resources/select.sql-spec
Expand Up @@ -75,6 +75,7 @@ SELECT CAST(emp_no AS BOOL) AS emp_no_cast FROM "test_emp" ORDER BY emp_no LIMIT

//
// SELECT with IS NULL and IS NOT NULL
//
isNullAndIsNotNull
SELECT null IS NULL AS col1, null IS NOT NULL AS col2;
isNullAndIsNotNullAndNegation
Expand All @@ -84,7 +85,9 @@ SELECT (null = 1) IS NULL AS col1, (null = 1) IS NOT NULL AS col2;
isNullAndIsNotNullOverComparisonWithNegation
SELECT NOT((null = 1) IS NULL) AS col1, NOT((null = 1) IS NOT NULL) AS col2;

// with table columns
//
// SELECT with IS NULL and IS NOT NULL with table columns
//
isNullAndIsNotNull_onTableColumns
SELECT languages IS NULL AS col1, languages IS NOT NULL AS col2 FROM "test_emp" WHERE emp_no IN (10018, 10019, 10020) ORDER BY emp_no;
isNullAndIsNotNullAndNegation_onTableColumns
Expand All @@ -93,3 +96,21 @@ isNullAndIsNotNullOverComparison_onTableColumns
SELECT (languages = 2) IS NULL AS col1, (languages = 2) IS NOT NULL AS col2 FROM test_emp WHERE emp_no IN (10018, 10019, 10020) ORDER BY emp_no;
isNullAndIsNotNullOverComparisonWithNegation_onTableColumns
SELECT NOT((languages = 2) IS NULL) AS col1, NOT((languages = 2) IS NOT NULL) AS col2 FROM test_emp WHERE emp_no IN (10018, 10019, 10020) ORDER BY emp_no;

//
// SELECT with functions locally evaluated
//
selectMathPI
SELECT PI() AS pi;
selectMathPIFromIndex
SELECT PI() AS pi FROM test_emp LIMIT 3;
selectMathPIFromIndexWithWhereEvaluatingToTrue
SELECT PI() AS pi FROM test_emp WHERE ROUND(PI(),2)=3.14;
selectMathPIFromIndexWithWhereEvaluatingToTrueAndWithLimit
SELECT PI() AS pi FROM test_emp WHERE ROUND(PI(),2)=3.14 LIMIT 3;
// AwaitsFix https://github.com/elastic/elasticsearch/issues/35980
selectMathPIFromIndexWithWhereEvaluatingToFalse-Ignore
SELECT PI() AS pi FROM test_emp WHERE PI()=5;
// AwaitsFix https://github.com/elastic/elasticsearch/issues/35980
selectMathPIFromIndexWithWhereEvaluatingToFalseAndWithLimit-Ignore
SELECT PI() AS pi FROM test_emp WHERE PI()=5 LIMIT 3;

0 comments on commit 6869ace

Please sign in to comment.