-
Notifications
You must be signed in to change notification settings - Fork 25.8k
SQL: add support for API key to JDBC and CLI #142021
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
Changes from all commits
07bc38b
9576b86
4a6ca7e
a262598
1eeb34f
369b5e3
6220e79
dcf4ec5
6aaa188
acb87f6
11a99b1
11c6571
9cd3dce
ef9b518
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| area: SQL | ||
| issues: [] | ||
| pr: 142021 | ||
| summary: Add support for API key to JDBC and CLI | ||
| type: enhancement |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -28,6 +28,22 @@ If security is enabled on your cluster, you can pass the username and password i | |
| $ ./bin/elasticsearch-sql-cli https://sql_user:strongpassword@some.server:9200 | ||
| ``` | ||
|
|
||
| ### API Key Authentication [sql-cli-apikey] | ||
|
|
||
| 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): | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is for consistency, JDBC and CLI use all lowercase parameters. |
||
|
|
||
| ```bash | ||
| $ ./bin/elasticsearch-sql-cli --apikey <encoded-api-key> https://some.server:9200 | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Same here. Uppercase
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Same as above |
||
| ``` | ||
|
|
||
| ::::{note} | ||
| 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. | ||
| :::: | ||
|
|
||
| ::::{warning} | ||
| 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. | ||
| :::: | ||
|
|
||
| Once the CLI is running you can use any [query](elasticsearch://reference/query-languages/sql/sql-spec.md) that Elasticsearch supports: | ||
|
|
||
| ```sql | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -89,13 +89,19 @@ protected void mainWithoutErrorHandling(String[] args, Terminal terminal, Proces | |
| LoggerFactory loggerFactory = LoggerFactory.provider(); | ||
| if (options.has(silentOption)) { | ||
| terminal.setVerbosity(Terminal.Verbosity.SILENT); | ||
| loggerFactory.setRootLevel(Level.OFF); | ||
| if (loggerFactory != null) { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why is this needed now but wasn't needed so far?
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is not a consequence of this PR.
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It's more complex than I thought. |
||
| loggerFactory.setRootLevel(Level.OFF); | ||
| } | ||
| } else if (options.has(verboseOption)) { | ||
| terminal.setVerbosity(Terminal.Verbosity.VERBOSE); | ||
| loggerFactory.setRootLevel(Level.DEBUG); | ||
| if (loggerFactory != null) { | ||
| loggerFactory.setRootLevel(Level.DEBUG); | ||
| } | ||
| } else { | ||
| terminal.setVerbosity(Terminal.Verbosity.NORMAL); | ||
| loggerFactory.setRootLevel(Level.INFO); | ||
| if (loggerFactory != null) { | ||
| loggerFactory.setRootLevel(Level.INFO); | ||
| } | ||
| } | ||
|
|
||
| execute(terminal, options, processInfo); | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,150 @@ | ||
| /* | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I like comments in code, but in these test classes I think there are too many comments imo.
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Let me remove the redundant comments |
||
| * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one | ||
| * or more contributor license agreements. Licensed under the Elastic License | ||
| * 2.0; you may not use this file except in compliance with the Elastic License | ||
| * 2.0. | ||
| */ | ||
| package org.elasticsearch.xpack.sql.qa.security; | ||
|
|
||
| import org.elasticsearch.client.Request; | ||
| import org.elasticsearch.xpack.sql.qa.cli.EmbeddedCli; | ||
| import org.elasticsearch.xpack.sql.qa.cli.EmbeddedCli.ApiKeySecurityConfig; | ||
|
|
||
| import static org.elasticsearch.xpack.sql.qa.security.RestSqlIT.SSL_ENABLED; | ||
| import static org.hamcrest.Matchers.containsString; | ||
|
|
||
| /** | ||
| * Integration tests for CLI connections using API key authentication. | ||
| */ | ||
| public class CliApiKeyIT extends SqlApiKeyTestCase { | ||
|
|
||
| public void testCliConnectionWithApiKey() throws Exception { | ||
| String encodedApiKey = createApiKey(""" | ||
| { | ||
| "name": "cli_test_key", | ||
| "role_descriptors": { | ||
| "role": { | ||
| "cluster": ["monitor"], | ||
| "indices": [ | ||
| { | ||
| "names": ["*"], | ||
| "privileges": ["all"] | ||
| } | ||
| ] | ||
| } | ||
| } | ||
| } | ||
| """); | ||
|
|
||
| Request createIndex = new Request("PUT", "/test_cli_api_key"); | ||
| createIndex.setJsonEntity(""" | ||
| { | ||
| "mappings": { | ||
| "properties": { | ||
| "value": { "type": "integer" } | ||
| } | ||
| } | ||
| } | ||
| """); | ||
| client().performRequest(createIndex); | ||
|
|
||
| Request indexDoc = new Request("PUT", "/test_cli_api_key/_doc/1"); | ||
| indexDoc.addParameter("refresh", "true"); | ||
| indexDoc.setJsonEntity(""" | ||
| { | ||
| "value": 123 | ||
| } | ||
| """); | ||
| client().performRequest(indexDoc); | ||
|
|
||
| ApiKeySecurityConfig apiKeyConfig = createApiKeySecurityConfig(encodedApiKey); | ||
|
|
||
| try (EmbeddedCli cli = new EmbeddedCli(elasticsearchAddress(), true, apiKeyConfig)) { | ||
| String result = cli.command("SELECT value FROM test_cli_api_key"); | ||
| assertThat(result, containsString("value")); | ||
| cli.readLine(); // separator line | ||
| String valueLine = cli.readLine(); | ||
| assertThat(valueLine, containsString("123")); | ||
| } | ||
| } | ||
|
|
||
| public void testCliConnectionWithInvalidApiKey() throws Exception { | ||
| ApiKeySecurityConfig apiKeyConfig = new ApiKeySecurityConfig( | ||
| SSL_ENABLED, | ||
| "invalid_api_key_value", | ||
| SSL_ENABLED ? SqlSecurityTestCluster.getKeystorePath() : null, | ||
| SSL_ENABLED ? SqlSecurityTestCluster.KEYSTORE_PASSWORD : null | ||
| ); | ||
|
|
||
| try (EmbeddedCli cli = new EmbeddedCli(elasticsearchAddress(), false, apiKeyConfig)) { | ||
| String result = cli.command("SELECT 1"); | ||
| StringBuilder fullError = new StringBuilder(result); | ||
| String line; | ||
| while ((line = cli.readLine()) != null && !line.isEmpty()) { | ||
| fullError.append(line); | ||
| } | ||
| String errorMessage = fullError.toString(); | ||
| assertTrue( | ||
| "Expected authentication error but got: " + errorMessage, | ||
| errorMessage.contains("security_exception") || errorMessage.contains("Communication error") | ||
| ); | ||
| } | ||
| } | ||
|
|
||
| public void testCliConnectionWithLimitedApiKey() throws Exception { | ||
| Request createRestrictedIndex = new Request("PUT", "/cli_restricted_index"); | ||
| createRestrictedIndex.setJsonEntity(""" | ||
| { | ||
| "mappings": { | ||
| "properties": { | ||
| "secret": { "type": "keyword" } | ||
| } | ||
| } | ||
| } | ||
| """); | ||
| client().performRequest(createRestrictedIndex); | ||
|
|
||
| Request indexRestrictedDoc = new Request("PUT", "/cli_restricted_index/_doc/1"); | ||
| indexRestrictedDoc.addParameter("refresh", "true"); | ||
| indexRestrictedDoc.setJsonEntity(""" | ||
| { | ||
| "secret": "confidential" | ||
| } | ||
| """); | ||
| client().performRequest(indexRestrictedDoc); | ||
|
|
||
| String encodedApiKey = createApiKey(""" | ||
| { | ||
| "name": "cli_limited_key", | ||
| "role_descriptors": { | ||
| "role": { | ||
| "cluster": ["monitor"], | ||
| "indices": [ | ||
| { | ||
| "names": ["cli_allowed_*"], | ||
| "privileges": ["read"] | ||
| } | ||
| ] | ||
| } | ||
| } | ||
| } | ||
| """); | ||
|
|
||
| ApiKeySecurityConfig apiKeyConfig = createApiKeySecurityConfig(encodedApiKey); | ||
|
|
||
| try (EmbeddedCli cli = new EmbeddedCli(elasticsearchAddress(), true, apiKeyConfig)) { | ||
| String result = cli.command("SELECT * FROM cli_restricted_index"); | ||
| String errorLine = cli.readLine(); | ||
| assertThat(errorLine, containsString("Unknown index [cli_restricted_index]")); | ||
| } | ||
| } | ||
|
|
||
| private ApiKeySecurityConfig createApiKeySecurityConfig(String apiKey) { | ||
| return new ApiKeySecurityConfig( | ||
| SSL_ENABLED, | ||
| apiKey, | ||
| SSL_ENABLED ? SqlSecurityTestCluster.getKeystorePath() : null, | ||
| SSL_ENABLED ? SqlSecurityTestCluster.KEYSTORE_PASSWORD : null | ||
| ); | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am wondering about
--apikey <value>on the command line being visible viaps auxor/proc/<pid>/cmdline. The docs mention not to combine API key with basic auth, but don'twarn about this security aspect. Should this be mentioned in docs, and provide alternative suggestions?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's a good point. I'm adding a note to the docs.