Skip to content

Commit 88ea038

Browse files
SQL: add support for API key to JDBC and CLI (elastic#142021)
1 parent 7b6cde3 commit 88ea038

File tree

17 files changed

+717
-14
lines changed

17 files changed

+717
-14
lines changed

docs/changelog/142021.yaml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
area: SQL
2+
issues: []
3+
pr: 142021
4+
summary: Add support for API key to JDBC and CLI
5+
type: enhancement

docs/reference/query-languages/sql/sql-cli.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,22 @@ If security is enabled on your cluster, you can pass the username and password i
2828
$ ./bin/elasticsearch-sql-cli https://sql_user:strongpassword@some.server:9200
2929
```
3030

31+
### API Key Authentication [sql-cli-apikey]
32+
33+
As an alternative to basic authentication, you can use API key authentication with the `--apikey` option. API keys can be created using the [Create API key API](docs-content://deploy-manage/api-keys/elasticsearch-api-keys.md). The API key should be provided in its encoded form (the `encoded` value returned by the Create API key API):
34+
35+
```bash
36+
$ ./bin/elasticsearch-sql-cli --apikey <encoded-api-key> https://some.server:9200
37+
```
38+
39+
::::{note}
40+
When using API key authentication, do not include username and password in the URL. The CLI will return an error if both API key and basic authentication credentials are provided.
41+
::::
42+
43+
::::{warning}
44+
Command line arguments are visible to other users on the system through process listing commands like `ps aux` or by inspecting `/proc/<pid>/cmdline`. Avoid using this method on shared systems where other users might be able to view your credentials.
45+
::::
46+
3147
Once the CLI is running you can use any [query](elasticsearch://reference/query-languages/sql/sql-spec.md) that Elasticsearch supports:
3248

3349
```sql

docs/reference/query-languages/sql/sql-jdbc.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,24 @@ $$$jdbc-cfg-timezone$$$
120120
: Basic Authentication password
121121

122122

123+
### API Key Authentication [jdbc-cfg-auth-apikey]
124+
125+
As an alternative to basic authentication, you can use API key authentication. API keys can be created using the [Create API key API](docs-content://deploy-manage/api-keys/elasticsearch-api-keys.md). The API key should be provided in its encoded form (the `encoded` value returned by the Create API key API).
126+
127+
`apiKey`
128+
: Encoded API key for authentication. Cannot be used together with `user`/`password` basic authentication.
129+
130+
::::{note}
131+
When using API key authentication, do not specify `user` or `password`. The driver will return an error if both API key and basic authentication credentials are provided.
132+
::::
133+
134+
Example connection URL using API key:
135+
136+
```text
137+
jdbc:es://http://server:9200/?apiKey=<encoded-api-key>
138+
```
139+
140+
123141
### SSL [jdbc-cfg-ssl]
124142

125143
`ssl` (default `false`)

docs/reference/query-languages/sql/sql-security.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,11 +20,14 @@ In case of an encrypted transport, the SSL/TLS support needs to be enabled in El
2020

2121
## Authentication [_authentication]
2222

23-
The authentication support in {{es}} SQL is of two types:
23+
The authentication support in {{es}} SQL is of three types:
2424

2525
Username/Password
2626
: Set these through `user` and `password` properties.
2727

28+
API Key
29+
: Use an API key for authentication by setting the `apiKey` property. API keys can be created using the [Create API key API](docs-content://deploy-manage/api-keys/elasticsearch-api-keys.md). The API key should be provided in its encoded form (the `encoded` value returned by the Create API key API). This is an alternative to username/password authentication and cannot be used together with it. For the CLI, use the `--apikey` command line option.
30+
2831
PKI/X.509
2932
: Use X.509 certificates to authenticate {{es}} SQL to {{es}}. For this, one would need to setup the `keystore` containing the private key and certificate to the appropriate user (configured in {{es}}) and the `truststore` with the CA certificate used to sign the SSL/TLS certificates in the {{es}} cluster. That is, one should setup the key to authenticate {{es}} SQL and also to verify that is the right one. To do so, one should set the `ssl.keystore.location` and `ssl.truststore.location` properties to indicate the `keystore` and `truststore` to use. It is recommended to have these secured through a password in which case `ssl.keystore.pass` and `ssl.truststore.pass` properties are required.
3033

libs/cli/src/main/java/org/elasticsearch/cli/Command.java

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -89,13 +89,19 @@ protected void mainWithoutErrorHandling(String[] args, Terminal terminal, Proces
8989
LoggerFactory loggerFactory = LoggerFactory.provider();
9090
if (options.has(silentOption)) {
9191
terminal.setVerbosity(Terminal.Verbosity.SILENT);
92-
loggerFactory.setRootLevel(Level.OFF);
92+
if (loggerFactory != null) {
93+
loggerFactory.setRootLevel(Level.OFF);
94+
}
9395
} else if (options.has(verboseOption)) {
9496
terminal.setVerbosity(Terminal.Verbosity.VERBOSE);
95-
loggerFactory.setRootLevel(Level.DEBUG);
97+
if (loggerFactory != null) {
98+
loggerFactory.setRootLevel(Level.DEBUG);
99+
}
96100
} else {
97101
terminal.setVerbosity(Terminal.Verbosity.NORMAL);
98-
loggerFactory.setRootLevel(Level.INFO);
102+
if (loggerFactory != null) {
103+
loggerFactory.setRootLevel(Level.INFO);
104+
}
99105
}
100106

101107
execute(terminal, options, processInfo);

x-pack/plugin/sql/jdbc/src/test/java/org/elasticsearch/xpack/sql/jdbc/JdbcConfigurationTests.java

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@
2525
import java.util.Properties;
2626
import java.util.stream.Collectors;
2727

28+
import static org.elasticsearch.xpack.sql.client.ConnectionConfiguration.AUTH_API_KEY;
29+
import static org.elasticsearch.xpack.sql.client.ConnectionConfiguration.AUTH_USER;
2830
import static org.elasticsearch.xpack.sql.client.ConnectionConfiguration.CONNECT_TIMEOUT;
2931
import static org.elasticsearch.xpack.sql.client.ConnectionConfiguration.NETWORK_TIMEOUT;
3032
import static org.elasticsearch.xpack.sql.client.ConnectionConfiguration.PAGE_SIZE;
@@ -33,6 +35,7 @@
3335
import static org.elasticsearch.xpack.sql.client.ConnectionConfiguration.QUERY_TIMEOUT;
3436
import static org.elasticsearch.xpack.sql.jdbc.JdbcConfiguration.URL_FULL_PREFIX;
3537
import static org.elasticsearch.xpack.sql.jdbc.JdbcConfiguration.URL_PREFIX;
38+
import static org.hamcrest.Matchers.containsString;
3639
import static org.hamcrest.Matchers.equalTo;
3740
import static org.hamcrest.Matchers.is;
3841

@@ -399,4 +402,40 @@ private void assertJdbcSqlException(String wrongSetting, String correctSetting,
399402
JdbcSQLException ex = expectThrows(JdbcSQLException.class, () -> JdbcConfiguration.create(url, props, 0));
400403
assertEquals("Unknown parameter [" + wrongSetting + "]; did you mean [" + correctSetting + "]", ex.getMessage());
401404
}
405+
406+
public void testApiKeyInUrl() throws Exception {
407+
String apiKey = "test_api_key_encoded";
408+
JdbcConfiguration ci = ci(jdbcPrefix() + "test:9200?apiKey=" + apiKey);
409+
assertThat(ci.apiKey(), is(apiKey));
410+
assertNull(ci.authUser());
411+
assertNull(ci.authPass());
412+
}
413+
414+
public void testApiKeyInProperties() throws Exception {
415+
String apiKey = "test_api_key_encoded";
416+
Properties props = new Properties();
417+
props.setProperty(AUTH_API_KEY, apiKey);
418+
JdbcConfiguration ci = JdbcConfiguration.create(jdbcPrefix() + "test:9200", props, 0);
419+
assertThat(ci.apiKey(), is(apiKey));
420+
assertNull(ci.authUser());
421+
assertNull(ci.authPass());
422+
}
423+
424+
public void testApiKeyAndUserMutuallyExclusive() {
425+
String apiKey = "test_api_key_encoded";
426+
Properties props = new Properties();
427+
props.setProperty(AUTH_API_KEY, apiKey);
428+
props.setProperty(AUTH_USER, "user");
429+
JdbcSQLException ex = expectThrows(JdbcSQLException.class, () -> JdbcConfiguration.create(jdbcPrefix() + "test:9200", props, 0));
430+
assertThat(ex.getMessage(), containsString("Cannot use both API key and basic authentication"));
431+
}
432+
433+
public void testApiKeyAndUserInUrlMutuallyExclusive() {
434+
String apiKey = "test_api_key_encoded";
435+
JdbcSQLException ex = expectThrows(
436+
JdbcSQLException.class,
437+
() -> ci(jdbcPrefix() + "test:9200?apiKey=" + apiKey + "&user=testuser")
438+
);
439+
assertThat(ex.getMessage(), containsString("Cannot use both API key and basic authentication"));
440+
}
402441
}
Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
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+
* 2.0; you may not use this file except in compliance with the Elastic License
5+
* 2.0.
6+
*/
7+
package org.elasticsearch.xpack.sql.qa.security;
8+
9+
import org.elasticsearch.client.Request;
10+
import org.elasticsearch.xpack.sql.qa.cli.EmbeddedCli;
11+
import org.elasticsearch.xpack.sql.qa.cli.EmbeddedCli.ApiKeySecurityConfig;
12+
13+
import static org.elasticsearch.xpack.sql.qa.security.RestSqlIT.SSL_ENABLED;
14+
import static org.hamcrest.Matchers.containsString;
15+
16+
/**
17+
* Integration tests for CLI connections using API key authentication.
18+
*/
19+
public class CliApiKeyIT extends SqlApiKeyTestCase {
20+
21+
public void testCliConnectionWithApiKey() throws Exception {
22+
String encodedApiKey = createApiKey("""
23+
{
24+
"name": "cli_test_key",
25+
"role_descriptors": {
26+
"role": {
27+
"cluster": ["monitor"],
28+
"indices": [
29+
{
30+
"names": ["*"],
31+
"privileges": ["all"]
32+
}
33+
]
34+
}
35+
}
36+
}
37+
""");
38+
39+
Request createIndex = new Request("PUT", "/test_cli_api_key");
40+
createIndex.setJsonEntity("""
41+
{
42+
"mappings": {
43+
"properties": {
44+
"value": { "type": "integer" }
45+
}
46+
}
47+
}
48+
""");
49+
client().performRequest(createIndex);
50+
51+
Request indexDoc = new Request("PUT", "/test_cli_api_key/_doc/1");
52+
indexDoc.addParameter("refresh", "true");
53+
indexDoc.setJsonEntity("""
54+
{
55+
"value": 123
56+
}
57+
""");
58+
client().performRequest(indexDoc);
59+
60+
ApiKeySecurityConfig apiKeyConfig = createApiKeySecurityConfig(encodedApiKey);
61+
62+
try (EmbeddedCli cli = new EmbeddedCli(elasticsearchAddress(), true, apiKeyConfig)) {
63+
String result = cli.command("SELECT value FROM test_cli_api_key");
64+
assertThat(result, containsString("value"));
65+
cli.readLine(); // separator line
66+
String valueLine = cli.readLine();
67+
assertThat(valueLine, containsString("123"));
68+
}
69+
}
70+
71+
public void testCliConnectionWithInvalidApiKey() throws Exception {
72+
ApiKeySecurityConfig apiKeyConfig = new ApiKeySecurityConfig(
73+
SSL_ENABLED,
74+
"invalid_api_key_value",
75+
SSL_ENABLED ? SqlSecurityTestCluster.getKeystorePath() : null,
76+
SSL_ENABLED ? SqlSecurityTestCluster.KEYSTORE_PASSWORD : null
77+
);
78+
79+
try (EmbeddedCli cli = new EmbeddedCli(elasticsearchAddress(), false, apiKeyConfig)) {
80+
String result = cli.command("SELECT 1");
81+
StringBuilder fullError = new StringBuilder(result);
82+
String line;
83+
while ((line = cli.readLine()) != null && !line.isEmpty()) {
84+
fullError.append(line);
85+
}
86+
String errorMessage = fullError.toString();
87+
assertTrue(
88+
"Expected authentication error but got: " + errorMessage,
89+
errorMessage.contains("security_exception") || errorMessage.contains("Communication error")
90+
);
91+
}
92+
}
93+
94+
public void testCliConnectionWithLimitedApiKey() throws Exception {
95+
Request createRestrictedIndex = new Request("PUT", "/cli_restricted_index");
96+
createRestrictedIndex.setJsonEntity("""
97+
{
98+
"mappings": {
99+
"properties": {
100+
"secret": { "type": "keyword" }
101+
}
102+
}
103+
}
104+
""");
105+
client().performRequest(createRestrictedIndex);
106+
107+
Request indexRestrictedDoc = new Request("PUT", "/cli_restricted_index/_doc/1");
108+
indexRestrictedDoc.addParameter("refresh", "true");
109+
indexRestrictedDoc.setJsonEntity("""
110+
{
111+
"secret": "confidential"
112+
}
113+
""");
114+
client().performRequest(indexRestrictedDoc);
115+
116+
String encodedApiKey = createApiKey("""
117+
{
118+
"name": "cli_limited_key",
119+
"role_descriptors": {
120+
"role": {
121+
"cluster": ["monitor"],
122+
"indices": [
123+
{
124+
"names": ["cli_allowed_*"],
125+
"privileges": ["read"]
126+
}
127+
]
128+
}
129+
}
130+
}
131+
""");
132+
133+
ApiKeySecurityConfig apiKeyConfig = createApiKeySecurityConfig(encodedApiKey);
134+
135+
try (EmbeddedCli cli = new EmbeddedCli(elasticsearchAddress(), true, apiKeyConfig)) {
136+
String result = cli.command("SELECT * FROM cli_restricted_index");
137+
String errorLine = cli.readLine();
138+
assertThat(errorLine, containsString("Unknown index [cli_restricted_index]"));
139+
}
140+
}
141+
142+
private ApiKeySecurityConfig createApiKeySecurityConfig(String apiKey) {
143+
return new ApiKeySecurityConfig(
144+
SSL_ENABLED,
145+
apiKey,
146+
SSL_ENABLED ? SqlSecurityTestCluster.getKeystorePath() : null,
147+
SSL_ENABLED ? SqlSecurityTestCluster.KEYSTORE_PASSWORD : null
148+
);
149+
}
150+
}

0 commit comments

Comments
 (0)