Skip to content

Commit 7b743a0

Browse files
authored
Add support for API keys to access Elasticsearch (elastic#38291) (elastic#38399)
X-Pack security supports built-in authentication service `token-service` that allows access tokens to be used to access Elasticsearch without using Basic authentication. The tokens are generated by `token-service` based on OAuth2 spec. The access token is a short-lived token (defaults to 20m) and refresh token with a lifetime of 24 hours, making them unsuitable for long-lived or recurring tasks where the system might go offline thereby failing refresh of tokens. This commit introduces a built-in authentication service `api-key-service` that adds support for long-lived tokens aka API keys to access Elasticsearch. The `api-key-service` is consulted after `token-service` in the authentication chain. By default, if TLS is enabled then `api-key-service` is also enabled. The service can be disabled using the configuration setting. The API keys:- - by default do not have an expiration but expiration can be configured where the API keys need to be expired after a certain amount of time. - when generated will keep authentication information of the user that generated them. - can be defined with a role describing the privileges for accessing Elasticsearch and will be limited by the role of the user that generated them - can be invalidated via invalidation API - information can be retrieved via a get API - that have been expired or invalidated will be retained for 1 week before being deleted. The expired API keys remover task handles this. Following are the API key management APIs:- 1. Create API Key - `PUT/POST /_security/api_key` 2. Get API key(s) - `GET /_security/api_key` 3. Invalidate API Key(s) `DELETE /_security/api_key` The API keys can be used to access Elasticsearch using `Authorization` header, where the auth scheme is `ApiKey` and the credentials, is the base64 encoding of API key Id and API key separated by a colon. Example:- ``` curl -H "Authorization: ApiKey YXBpLWtleS1pZDphcGkta2V5" http://localhost:9200/_cluster/health ``` Closes elastic#34383
1 parent 9507d94 commit 7b743a0

File tree

144 files changed

+10792
-918
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

144 files changed

+10792
-918
lines changed

client/rest-high-level/build.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,7 @@ integTestCluster {
104104
setting 'xpack.license.self_generated.type', 'trial'
105105
setting 'xpack.security.enabled', 'true'
106106
setting 'xpack.security.authc.token.enabled', 'true'
107+
setting 'xpack.security.authc.api_key.enabled', 'true'
107108
// Truststore settings are not used since TLS is not enabled. Included for testing the get certificates API
108109
setting 'xpack.security.http.ssl.certificate_authorities', 'testnode.crt'
109110
setting 'xpack.security.http.ssl.supported_protocols', 'TLSv1.2,TLSv1.1,TLSv1'

client/rest-high-level/src/main/java/org/elasticsearch/client/SecurityClient.java

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@
2727
import org.elasticsearch.client.security.ClearRealmCacheResponse;
2828
import org.elasticsearch.client.security.ClearRolesCacheRequest;
2929
import org.elasticsearch.client.security.ClearRolesCacheResponse;
30+
import org.elasticsearch.client.security.CreateApiKeyRequest;
31+
import org.elasticsearch.client.security.CreateApiKeyResponse;
3032
import org.elasticsearch.client.security.CreateTokenRequest;
3133
import org.elasticsearch.client.security.CreateTokenResponse;
3234
import org.elasticsearch.client.security.DeletePrivilegesRequest;
@@ -40,6 +42,8 @@
4042
import org.elasticsearch.client.security.DisableUserRequest;
4143
import org.elasticsearch.client.security.EmptyResponse;
4244
import org.elasticsearch.client.security.EnableUserRequest;
45+
import org.elasticsearch.client.security.GetApiKeyRequest;
46+
import org.elasticsearch.client.security.GetApiKeyResponse;
4347
import org.elasticsearch.client.security.GetPrivilegesRequest;
4448
import org.elasticsearch.client.security.GetPrivilegesResponse;
4549
import org.elasticsearch.client.security.GetRoleMappingsRequest;
@@ -54,6 +58,8 @@
5458
import org.elasticsearch.client.security.GetUsersResponse;
5559
import org.elasticsearch.client.security.HasPrivilegesRequest;
5660
import org.elasticsearch.client.security.HasPrivilegesResponse;
61+
import org.elasticsearch.client.security.InvalidateApiKeyRequest;
62+
import org.elasticsearch.client.security.InvalidateApiKeyResponse;
5763
import org.elasticsearch.client.security.InvalidateTokenRequest;
5864
import org.elasticsearch.client.security.InvalidateTokenResponse;
5965
import org.elasticsearch.client.security.PutPrivilegesRequest;
@@ -850,4 +856,95 @@ public void deletePrivilegesAsync(DeletePrivilegesRequest request, RequestOption
850856
restHighLevelClient.performRequestAsyncAndParseEntity(request, SecurityRequestConverters::deletePrivileges, options,
851857
DeletePrivilegesResponse::fromXContent, listener, singleton(404));
852858
}
859+
860+
/**
861+
* Create an API Key.<br>
862+
* See <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/security-api-create-api-key.html">
863+
* the docs</a> for more.
864+
*
865+
* @param request the request to create a API key
866+
* @param options the request options (e.g. headers), use {@link RequestOptions#DEFAULT} if nothing needs to be customized
867+
* @return the response from the create API key call
868+
* @throws IOException in case there is a problem sending the request or parsing back the response
869+
*/
870+
public CreateApiKeyResponse createApiKey(final CreateApiKeyRequest request, final RequestOptions options) throws IOException {
871+
return restHighLevelClient.performRequestAndParseEntity(request, SecurityRequestConverters::createApiKey, options,
872+
CreateApiKeyResponse::fromXContent, emptySet());
873+
}
874+
875+
/**
876+
* Asynchronously creates an API key.<br>
877+
* See <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/security-api-create-api-key.html">
878+
* the docs</a> for more.
879+
*
880+
* @param request the request to create a API key
881+
* @param options the request options (e.g. headers), use {@link RequestOptions#DEFAULT} if nothing needs to be customized
882+
* @param listener the listener to be notified upon request completion
883+
*/
884+
public void createApiKeyAsync(final CreateApiKeyRequest request, final RequestOptions options,
885+
final ActionListener<CreateApiKeyResponse> listener) {
886+
restHighLevelClient.performRequestAsyncAndParseEntity(request, SecurityRequestConverters::createApiKey, options,
887+
CreateApiKeyResponse::fromXContent, listener, emptySet());
888+
}
889+
890+
/**
891+
* Retrieve API Key(s) information.<br>
892+
* See <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/security-api-get-api-key.html">
893+
* the docs</a> for more.
894+
*
895+
* @param request the request to retrieve API key(s)
896+
* @param options the request options (e.g. headers), use {@link RequestOptions#DEFAULT} if nothing needs to be customized
897+
* @return the response from the create API key call
898+
* @throws IOException in case there is a problem sending the request or parsing back the response
899+
*/
900+
public GetApiKeyResponse getApiKey(final GetApiKeyRequest request, final RequestOptions options) throws IOException {
901+
return restHighLevelClient.performRequestAndParseEntity(request, SecurityRequestConverters::getApiKey, options,
902+
GetApiKeyResponse::fromXContent, emptySet());
903+
}
904+
905+
/**
906+
* Asynchronously retrieve API Key(s) information.<br>
907+
* See <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/security-api-get-api-key.html">
908+
* the docs</a> for more.
909+
*
910+
* @param request the request to retrieve API key(s)
911+
* @param options the request options (e.g. headers), use {@link RequestOptions#DEFAULT} if nothing needs to be customized
912+
* @param listener the listener to be notified upon request completion
913+
*/
914+
public void getApiKeyAsync(final GetApiKeyRequest request, final RequestOptions options,
915+
final ActionListener<GetApiKeyResponse> listener) {
916+
restHighLevelClient.performRequestAsyncAndParseEntity(request, SecurityRequestConverters::getApiKey, options,
917+
GetApiKeyResponse::fromXContent, listener, emptySet());
918+
}
919+
920+
/**
921+
* Invalidate API Key(s).<br>
922+
* See <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/security-api-invalidate-api-key.html">
923+
* the docs</a> for more.
924+
*
925+
* @param request the request to invalidate API key(s)
926+
* @param options the request options (e.g. headers), use {@link RequestOptions#DEFAULT} if nothing needs to be customized
927+
* @return the response from the invalidate API key call
928+
* @throws IOException in case there is a problem sending the request or parsing back the response
929+
*/
930+
public InvalidateApiKeyResponse invalidateApiKey(final InvalidateApiKeyRequest request, final RequestOptions options)
931+
throws IOException {
932+
return restHighLevelClient.performRequestAndParseEntity(request, SecurityRequestConverters::invalidateApiKey, options,
933+
InvalidateApiKeyResponse::fromXContent, emptySet());
934+
}
935+
936+
/**
937+
* Asynchronously invalidates API key(s).<br>
938+
* See <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/security-api-invalidate-api-key.html">
939+
* the docs</a> for more.
940+
*
941+
* @param request the request to invalidate API key(s)
942+
* @param options the request options (e.g. headers), use {@link RequestOptions#DEFAULT} if nothing needs to be customized
943+
* @param listener the listener to be notified upon request completion
944+
*/
945+
public void invalidateApiKeyAsync(final InvalidateApiKeyRequest request, final RequestOptions options,
946+
final ActionListener<InvalidateApiKeyResponse> listener) {
947+
restHighLevelClient.performRequestAsyncAndParseEntity(request, SecurityRequestConverters::invalidateApiKey, options,
948+
InvalidateApiKeyResponse::fromXContent, listener, emptySet());
949+
}
853950
}

client/rest-high-level/src/main/java/org/elasticsearch/client/SecurityRequestConverters.java

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,18 +26,21 @@
2626
import org.elasticsearch.client.security.ChangePasswordRequest;
2727
import org.elasticsearch.client.security.ClearRealmCacheRequest;
2828
import org.elasticsearch.client.security.ClearRolesCacheRequest;
29+
import org.elasticsearch.client.security.CreateApiKeyRequest;
2930
import org.elasticsearch.client.security.CreateTokenRequest;
3031
import org.elasticsearch.client.security.DeletePrivilegesRequest;
3132
import org.elasticsearch.client.security.DeleteRoleMappingRequest;
3233
import org.elasticsearch.client.security.DeleteRoleRequest;
3334
import org.elasticsearch.client.security.DeleteUserRequest;
3435
import org.elasticsearch.client.security.DisableUserRequest;
3536
import org.elasticsearch.client.security.EnableUserRequest;
37+
import org.elasticsearch.client.security.GetApiKeyRequest;
3638
import org.elasticsearch.client.security.GetPrivilegesRequest;
3739
import org.elasticsearch.client.security.GetRoleMappingsRequest;
3840
import org.elasticsearch.client.security.GetRolesRequest;
3941
import org.elasticsearch.client.security.GetUsersRequest;
4042
import org.elasticsearch.client.security.HasPrivilegesRequest;
43+
import org.elasticsearch.client.security.InvalidateApiKeyRequest;
4144
import org.elasticsearch.client.security.InvalidateTokenRequest;
4245
import org.elasticsearch.client.security.PutPrivilegesRequest;
4346
import org.elasticsearch.client.security.PutRoleMappingRequest;
@@ -256,4 +259,36 @@ static Request putRole(final PutRoleRequest putRoleRequest) throws IOException {
256259
params.withRefreshPolicy(putRoleRequest.getRefreshPolicy());
257260
return request;
258261
}
262+
263+
static Request createApiKey(final CreateApiKeyRequest createApiKeyRequest) throws IOException {
264+
final Request request = new Request(HttpPost.METHOD_NAME, "/_security/api_key");
265+
request.setEntity(createEntity(createApiKeyRequest, REQUEST_BODY_CONTENT_TYPE));
266+
final RequestConverters.Params params = new RequestConverters.Params(request);
267+
params.withRefreshPolicy(createApiKeyRequest.getRefreshPolicy());
268+
return request;
269+
}
270+
271+
static Request getApiKey(final GetApiKeyRequest getApiKeyRequest) throws IOException {
272+
final Request request = new Request(HttpGet.METHOD_NAME, "/_security/api_key");
273+
if (Strings.hasText(getApiKeyRequest.getId())) {
274+
request.addParameter("id", getApiKeyRequest.getId());
275+
}
276+
if (Strings.hasText(getApiKeyRequest.getName())) {
277+
request.addParameter("name", getApiKeyRequest.getName());
278+
}
279+
if (Strings.hasText(getApiKeyRequest.getUserName())) {
280+
request.addParameter("username", getApiKeyRequest.getUserName());
281+
}
282+
if (Strings.hasText(getApiKeyRequest.getRealmName())) {
283+
request.addParameter("realm_name", getApiKeyRequest.getRealmName());
284+
}
285+
return request;
286+
}
287+
288+
static Request invalidateApiKey(final InvalidateApiKeyRequest invalidateApiKeyRequest) throws IOException {
289+
final Request request = new Request(HttpDelete.METHOD_NAME, "/_security/api_key");
290+
request.setEntity(createEntity(invalidateApiKeyRequest, REQUEST_BODY_CONTENT_TYPE));
291+
final RequestConverters.Params params = new RequestConverters.Params(request);
292+
return request;
293+
}
259294
}
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
/*
2+
* Licensed to Elasticsearch under one or more contributor
3+
* license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright
5+
* ownership. Elasticsearch licenses this file to you under
6+
* the Apache License, Version 2.0 (the "License"); you may
7+
* not use this file except in compliance with the License.
8+
* You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
20+
package org.elasticsearch.client.security;
21+
22+
import org.elasticsearch.client.Validatable;
23+
import org.elasticsearch.client.security.user.privileges.Role;
24+
import org.elasticsearch.common.Nullable;
25+
import org.elasticsearch.common.Strings;
26+
import org.elasticsearch.common.unit.TimeValue;
27+
import org.elasticsearch.common.xcontent.ToXContentObject;
28+
import org.elasticsearch.common.xcontent.XContentBuilder;
29+
30+
import java.io.IOException;
31+
import java.util.List;
32+
import java.util.Objects;
33+
34+
/**
35+
* Request to create API key
36+
*/
37+
public final class CreateApiKeyRequest implements Validatable, ToXContentObject {
38+
39+
private final String name;
40+
private final TimeValue expiration;
41+
private final List<Role> roles;
42+
private final RefreshPolicy refreshPolicy;
43+
44+
/**
45+
* Create API Key request constructor
46+
* @param name name for the API key
47+
* @param roles list of {@link Role}s
48+
* @param expiration to specify expiration for the API key
49+
*/
50+
public CreateApiKeyRequest(String name, List<Role> roles, @Nullable TimeValue expiration, @Nullable final RefreshPolicy refreshPolicy) {
51+
if (Strings.hasText(name)) {
52+
this.name = name;
53+
} else {
54+
throw new IllegalArgumentException("name must not be null or empty");
55+
}
56+
this.roles = Objects.requireNonNull(roles, "roles may not be null");
57+
this.expiration = expiration;
58+
this.refreshPolicy = (refreshPolicy == null) ? RefreshPolicy.getDefault() : refreshPolicy;
59+
}
60+
61+
public String getName() {
62+
return name;
63+
}
64+
65+
public TimeValue getExpiration() {
66+
return expiration;
67+
}
68+
69+
public List<Role> getRoles() {
70+
return roles;
71+
}
72+
73+
public RefreshPolicy getRefreshPolicy() {
74+
return refreshPolicy;
75+
}
76+
77+
@Override
78+
public int hashCode() {
79+
return Objects.hash(name, refreshPolicy, roles, expiration);
80+
}
81+
82+
@Override
83+
public boolean equals(Object o) {
84+
if (this == o) {
85+
return true;
86+
}
87+
if (o == null || getClass() != o.getClass()) {
88+
return false;
89+
}
90+
final CreateApiKeyRequest that = (CreateApiKeyRequest) o;
91+
return Objects.equals(name, that.name) && Objects.equals(refreshPolicy, that.refreshPolicy) && Objects.equals(roles, that.roles)
92+
&& Objects.equals(expiration, that.expiration);
93+
}
94+
95+
@Override
96+
public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
97+
builder.startObject().field("name", name);
98+
if (expiration != null) {
99+
builder.field("expiration", expiration.getStringRep());
100+
}
101+
builder.startObject("role_descriptors");
102+
for (Role role : roles) {
103+
builder.startObject(role.getName());
104+
if (role.getApplicationPrivileges() != null) {
105+
builder.field(Role.APPLICATIONS.getPreferredName(), role.getApplicationPrivileges());
106+
}
107+
if (role.getClusterPrivileges() != null) {
108+
builder.field(Role.CLUSTER.getPreferredName(), role.getClusterPrivileges());
109+
}
110+
if (role.getGlobalPrivileges() != null) {
111+
builder.field(Role.GLOBAL.getPreferredName(), role.getGlobalPrivileges());
112+
}
113+
if (role.getIndicesPrivileges() != null) {
114+
builder.field(Role.INDICES.getPreferredName(), role.getIndicesPrivileges());
115+
}
116+
if (role.getMetadata() != null) {
117+
builder.field(Role.METADATA.getPreferredName(), role.getMetadata());
118+
}
119+
if (role.getRunAsPrivilege() != null) {
120+
builder.field(Role.RUN_AS.getPreferredName(), role.getRunAsPrivilege());
121+
}
122+
builder.endObject();
123+
}
124+
builder.endObject();
125+
return builder.endObject();
126+
}
127+
128+
}

0 commit comments

Comments
 (0)