Skip to content

Commit 6869ace

Browse files
committed
SQL: DATABASE() and USER() system functions (#35946)
(cherry picked from commit aabff73)
1 parent f694319 commit 6869ace

File tree

35 files changed

+786
-112
lines changed

35 files changed

+786
-112
lines changed

docs/reference/sql/functions/index.asciidoc

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
* <<sql-functions-string, String>>
1414
* <<sql-functions-type-conversion,Type Conversion>>
1515
* <<sql-functions-conditional, Conditional>>
16+
* <<sql-functions-system, System>>
1617

1718
include::operators.asciidoc[]
1819
include::aggs.asciidoc[]
@@ -22,3 +23,4 @@ include::math.asciidoc[]
2223
include::string.asciidoc[]
2324
include::type-conversion.asciidoc[]
2425
include::conditional.asciidoc[]
26+
include::system.asciidoc[]
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
[role="xpack"]
2+
[testenv="basic"]
3+
[[sql-functions-system]]
4+
=== System Functions
5+
6+
These functions return metadata type of information about the system being queried.
7+
8+
[[sql-functions-system-database]]
9+
==== `DATABASE`
10+
11+
.Synopsis:
12+
[source, sql]
13+
--------------------------------------------------
14+
DATABASE()
15+
--------------------------------------------------
16+
17+
*Input*:
18+
19+
*Output*: string
20+
21+
.Description:
22+
23+
Returns the name of the database being queried. In the case of Elasticsearch SQL, this
24+
is the name of the Elasticsearch cluster. This function should always return a non-null
25+
value.
26+
27+
["source","sql",subs="attributes,callouts,macros"]
28+
--------------------------------------------------
29+
include-tagged::{sql-specs}/docs.csv-spec[database]
30+
--------------------------------------------------
31+
32+
[[sql-functions-system-user]]
33+
==== `USER`
34+
35+
.Synopsis:
36+
[source, sql]
37+
--------------------------------------------------
38+
USER()
39+
--------------------------------------------------
40+
*Input*:
41+
42+
*Output*: string
43+
44+
.Description:
45+
46+
Returns the username of the authenticated user executing the query. This function can
47+
return `null` in case Security is disabled.
48+
49+
["source","sql",subs="attributes,callouts,macros"]
50+
--------------------------------------------------
51+
include-tagged::{sql-specs}/docs.csv-spec[user]
52+
--------------------------------------------------

x-pack/plugin/sql/jdbc/src/main/java/org/elasticsearch/xpack/sql/jdbc/JdbcDatabaseMetaData.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -208,8 +208,8 @@ public String getStringFunctions() throws SQLException {
208208

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

215215
@Override
Lines changed: 223 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,223 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License;
4+
* you may not use this file except in compliance with the Elastic License.
5+
*/
6+
7+
package org.elasticsearch.xpack.sql.qa.security;
8+
9+
import org.apache.http.HttpEntity;
10+
import org.apache.http.entity.ContentType;
11+
import org.apache.http.entity.StringEntity;
12+
import org.elasticsearch.client.Request;
13+
import org.elasticsearch.client.RequestOptions;
14+
import org.elasticsearch.client.Response;
15+
import org.elasticsearch.common.Strings;
16+
import org.elasticsearch.common.settings.Settings;
17+
import org.elasticsearch.common.xcontent.XContentBuilder;
18+
import org.elasticsearch.common.xcontent.XContentHelper;
19+
import org.elasticsearch.common.xcontent.json.JsonXContent;
20+
import org.elasticsearch.test.NotEqualMessageBuilder;
21+
import org.elasticsearch.test.rest.ESRestTestCase;
22+
import org.junit.After;
23+
import org.junit.Before;
24+
import org.junit.Rule;
25+
import org.junit.rules.TestName;
26+
27+
import java.io.IOException;
28+
import java.io.InputStream;
29+
import java.sql.JDBCType;
30+
import java.util.ArrayList;
31+
import java.util.Arrays;
32+
import java.util.Collections;
33+
import java.util.HashMap;
34+
import java.util.List;
35+
import java.util.Map;
36+
37+
import static org.elasticsearch.xpack.sql.qa.rest.RestSqlTestCase.columnInfo;
38+
39+
public class UserFunctionIT extends ESRestTestCase {
40+
41+
private static final String SQL = "SELECT USER()";
42+
// role defined in roles.yml
43+
private static final String MINIMAL_ACCESS_ROLE = "rest_minimal";
44+
private List<String> users;
45+
@Rule
46+
public TestName name = new TestName();
47+
48+
@Override
49+
protected Settings restClientSettings() {
50+
return RestSqlIT.securitySettings();
51+
}
52+
53+
@Override
54+
protected String getProtocol() {
55+
return RestSqlIT.SSL_ENABLED ? "https" : "http";
56+
}
57+
58+
@Before
59+
private void setUpUsers() throws IOException {
60+
int usersCount = name.getMethodName().startsWith("testSingle") ? 1 : randomIntBetween(5, 15);
61+
users = new ArrayList<String>(usersCount);
62+
63+
for(int i = 0; i < usersCount; i++) {
64+
String randomUserName = randomAlphaOfLengthBetween(1, 15);
65+
users.add(randomUserName);
66+
createUser(randomUserName, MINIMAL_ACCESS_ROLE);
67+
}
68+
}
69+
70+
@After
71+
private void clearUsers() throws IOException {
72+
for (String user : users) {
73+
deleteUser(user);
74+
}
75+
}
76+
77+
public void testSingleRandomUser() throws IOException {
78+
String mode = randomMode().toString();
79+
String randomUserName = users.get(0);
80+
81+
Map<String, Object> expected = new HashMap<>();
82+
expected.put("columns", Arrays.asList(
83+
columnInfo(mode, "USER", "keyword", JDBCType.VARCHAR, 0)));
84+
expected.put("rows", Arrays.asList(Arrays.asList(randomUserName)));
85+
Map<String, Object> actual = runSql(randomUserName, mode, SQL);
86+
87+
assertResponse(expected, actual);
88+
}
89+
90+
public void testSingleRandomUserWithWhereEvaluatingTrue() throws IOException {
91+
index("{\"test\":\"doc1\"}",
92+
"{\"test\":\"doc2\"}",
93+
"{\"test\":\"doc3\"}");
94+
String mode = randomMode().toString();
95+
String randomUserName = users.get(0);
96+
97+
Map<String, Object> expected = new HashMap<>();
98+
expected.put("columns", Arrays.asList(
99+
columnInfo(mode, "USER", "keyword", JDBCType.VARCHAR, 0)));
100+
expected.put("rows", Arrays.asList(Arrays.asList(randomUserName),
101+
Arrays.asList(randomUserName),
102+
Arrays.asList(randomUserName)));
103+
Map<String, Object> actual = runSql(randomUserName, mode, SQL + " FROM test WHERE USER()='" + randomUserName + "' LIMIT 3");
104+
assertResponse(expected, actual);
105+
}
106+
107+
@AwaitsFix(bugUrl="https://github.com/elastic/elasticsearch/issues/35980")
108+
public void testSingleRandomUserWithWhereEvaluatingFalse() throws IOException {
109+
index("{\"test\":\"doc1\"}",
110+
"{\"test\":\"doc2\"}",
111+
"{\"test\":\"doc3\"}");
112+
String mode = randomMode().toString();
113+
String randomUserName = users.get(0);
114+
115+
Map<String, Object> expected = new HashMap<>();
116+
expected.put("columns", Arrays.asList(
117+
columnInfo(mode, "USER", "keyword", JDBCType.VARCHAR, 0)));
118+
expected.put("rows", Collections.<ArrayList<String>>emptyList());
119+
String anotherRandomUserName = randomValueOtherThan(randomUserName, () -> randomAlphaOfLengthBetween(1, 15));
120+
Map<String, Object> actual = runSql(randomUserName, mode, SQL + " FROM test WHERE USER()='" + anotherRandomUserName + "' LIMIT 3");
121+
assertResponse(expected, actual);
122+
}
123+
124+
public void testMultipleRandomUsersAccess() throws IOException {
125+
// run 30 queries and pick randomly each time one of the 5-15 users created previously
126+
for (int i = 0; i < 30; i++) {
127+
String mode = randomMode().toString();
128+
String randomlyPickedUsername = randomFrom(users);
129+
Map<String, Object> expected = new HashMap<>();
130+
131+
expected.put("columns", Arrays.asList(
132+
columnInfo(mode, "USER", "keyword", JDBCType.VARCHAR, 0)));
133+
expected.put("rows", Arrays.asList(Arrays.asList(randomlyPickedUsername)));
134+
Map<String, Object> actual = runSql(randomlyPickedUsername, mode, SQL);
135+
136+
// expect the user that ran the query to be the same as the one returned by the `USER()` function
137+
assertResponse(expected, actual);
138+
}
139+
}
140+
141+
public void testSingleUserSelectFromIndex() throws IOException {
142+
index("{\"test\":\"doc1\"}",
143+
"{\"test\":\"doc2\"}",
144+
"{\"test\":\"doc3\"}");
145+
String mode = randomMode().toString();
146+
String randomUserName = users.get(0);
147+
148+
Map<String, Object> expected = new HashMap<>();
149+
expected.put("columns", Arrays.asList(
150+
columnInfo(mode, "USER", "keyword", JDBCType.VARCHAR, 0)));
151+
expected.put("rows", Arrays.asList(Arrays.asList(randomUserName),
152+
Arrays.asList(randomUserName),
153+
Arrays.asList(randomUserName)));
154+
Map<String, Object> actual = runSql(randomUserName, mode, "SELECT USER() FROM test LIMIT 3");
155+
156+
assertResponse(expected, actual);
157+
}
158+
159+
private void createUser(String name, String role) throws IOException {
160+
Request request = new Request("PUT", "/_xpack/security/user/" + name);
161+
XContentBuilder user = JsonXContent.contentBuilder().prettyPrint();
162+
user.startObject(); {
163+
user.field("password", "testpass");
164+
user.field("roles", role);
165+
}
166+
user.endObject();
167+
request.setJsonEntity(Strings.toString(user));
168+
client().performRequest(request);
169+
}
170+
171+
private void deleteUser(String name) throws IOException {
172+
Request request = new Request("DELETE", "/_xpack/security/user/" + name);
173+
client().performRequest(request);
174+
}
175+
176+
private Map<String, Object> runSql(String asUser, String mode, String sql) throws IOException {
177+
return runSql(asUser, mode, new StringEntity("{\"query\": \"" + sql + "\"}", ContentType.APPLICATION_JSON));
178+
}
179+
180+
private Map<String, Object> runSql(String asUser, String mode, HttpEntity entity) throws IOException {
181+
Request request = new Request("POST", "/_xpack/sql");
182+
if (false == mode.isEmpty()) {
183+
request.addParameter("mode", mode);
184+
}
185+
if (asUser != null) {
186+
RequestOptions.Builder options = request.getOptions().toBuilder();
187+
options.addHeader("es-security-runas-user", asUser);
188+
request.setOptions(options);
189+
}
190+
request.setEntity(entity);
191+
return toMap(client().performRequest(request));
192+
}
193+
194+
private void assertResponse(Map<String, Object> expected, Map<String, Object> actual) {
195+
if (false == expected.equals(actual)) {
196+
NotEqualMessageBuilder message = new NotEqualMessageBuilder();
197+
message.compareMaps(actual, expected);
198+
fail("Response does not match:\n" + message.toString());
199+
}
200+
}
201+
202+
private static Map<String, Object> toMap(Response response) throws IOException {
203+
try (InputStream content = response.getEntity().getContent()) {
204+
return XContentHelper.convertToMap(JsonXContent.jsonXContent, content, false);
205+
}
206+
}
207+
208+
private String randomMode() {
209+
return randomFrom("plain", "jdbc", "");
210+
}
211+
212+
private void index(String... docs) throws IOException {
213+
Request request = new Request("POST", "/test/test/_bulk");
214+
request.addParameter("refresh", "true");
215+
StringBuilder bulk = new StringBuilder();
216+
for (String doc : docs) {
217+
bulk.append("{\"index\":{}\n");
218+
bulk.append(doc + "\n");
219+
}
220+
request.setJsonEntity(bulk.toString());
221+
client().performRequest(request);
222+
}
223+
}

x-pack/plugin/sql/qa/src/main/resources/command.csv-spec

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,8 @@ SUBSTRING |SCALAR
108108
UCASE |SCALAR
109109
CAST |SCALAR
110110
CONVERT |SCALAR
111+
DATABASE |SCALAR
112+
USER |SCALAR
111113
SCORE |SCORE
112114
;
113115

x-pack/plugin/sql/qa/src/main/resources/docs.csv-spec

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -285,6 +285,8 @@ SUBSTRING |SCALAR
285285
UCASE |SCALAR
286286
CAST |SCALAR
287287
CONVERT |SCALAR
288+
DATABASE |SCALAR
289+
USER |SCALAR
288290
SCORE |SCORE
289291
// end::showFunctions
290292
;
@@ -1683,3 +1685,27 @@ SELECT null <=> null AS "equals";
16831685
true
16841686
// end::nullEqualsCompareTwoNulls
16851687
;
1688+
1689+
// ignored because tests run with a docs-not-worthy cluster name
1690+
// at the time of this test being ignored, the cluster name was x-pack_plugin_sql_qa_single-node_integTestCluster
1691+
database-Ignore
1692+
// tag::database
1693+
SELECT DATABASE();
1694+
1695+
DATABASE
1696+
---------------
1697+
elasticsearch
1698+
// end::database
1699+
;
1700+
1701+
// ignored because tests run with a docs-not-worthy user name
1702+
// at the time of this test being ignored, there was no user name being used
1703+
user-Ignore
1704+
// tag::user
1705+
SELECT USER();
1706+
1707+
USER
1708+
---------------
1709+
elastic
1710+
// end::user
1711+
;

x-pack/plugin/sql/qa/src/main/resources/select.sql-spec

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ SELECT CAST(emp_no AS BOOL) AS emp_no_cast FROM "test_emp" ORDER BY emp_no LIMIT
7575

7676
//
7777
// SELECT with IS NULL and IS NOT NULL
78+
//
7879
isNullAndIsNotNull
7980
SELECT null IS NULL AS col1, null IS NOT NULL AS col2;
8081
isNullAndIsNotNullAndNegation
@@ -84,7 +85,9 @@ SELECT (null = 1) IS NULL AS col1, (null = 1) IS NOT NULL AS col2;
8485
isNullAndIsNotNullOverComparisonWithNegation
8586
SELECT NOT((null = 1) IS NULL) AS col1, NOT((null = 1) IS NOT NULL) AS col2;
8687

87-
// with table columns
88+
//
89+
// SELECT with IS NULL and IS NOT NULL with table columns
90+
//
8891
isNullAndIsNotNull_onTableColumns
8992
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;
9093
isNullAndIsNotNullAndNegation_onTableColumns
@@ -93,3 +96,21 @@ isNullAndIsNotNullOverComparison_onTableColumns
9396
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;
9497
isNullAndIsNotNullOverComparisonWithNegation_onTableColumns
9598
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;
99+
100+
//
101+
// SELECT with functions locally evaluated
102+
//
103+
selectMathPI
104+
SELECT PI() AS pi;
105+
selectMathPIFromIndex
106+
SELECT PI() AS pi FROM test_emp LIMIT 3;
107+
selectMathPIFromIndexWithWhereEvaluatingToTrue
108+
SELECT PI() AS pi FROM test_emp WHERE ROUND(PI(),2)=3.14;
109+
selectMathPIFromIndexWithWhereEvaluatingToTrueAndWithLimit
110+
SELECT PI() AS pi FROM test_emp WHERE ROUND(PI(),2)=3.14 LIMIT 3;
111+
// AwaitsFix https://github.com/elastic/elasticsearch/issues/35980
112+
selectMathPIFromIndexWithWhereEvaluatingToFalse-Ignore
113+
SELECT PI() AS pi FROM test_emp WHERE PI()=5;
114+
// AwaitsFix https://github.com/elastic/elasticsearch/issues/35980
115+
selectMathPIFromIndexWithWhereEvaluatingToFalseAndWithLimit-Ignore
116+
SELECT PI() AS pi FROM test_emp WHERE PI()=5 LIMIT 3;

0 commit comments

Comments
 (0)