Skip to content

Commit

Permalink
Add support for API keys to access Elasticsearch (elastic#38291)
Browse files Browse the repository at this point in the history
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
  • Loading branch information
bizybot authored Feb 5, 2019
1 parent d255303 commit fe36861
Show file tree
Hide file tree
Showing 142 changed files with 10,697 additions and 946 deletions.
1 change: 1 addition & 0 deletions client/rest-high-level/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@ integTestCluster {
setting 'xpack.license.self_generated.type', 'trial'
setting 'xpack.security.enabled', 'true'
setting 'xpack.security.authc.token.enabled', 'true'
setting 'xpack.security.authc.api_key.enabled', 'true'
// Truststore settings are not used since TLS is not enabled. Included for testing the get certificates API
setting 'xpack.security.http.ssl.certificate_authorities', 'testnode.crt'
setting 'xpack.security.transport.ssl.truststore.path', 'testnode.jks'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@
import org.elasticsearch.client.security.ClearRealmCacheResponse;
import org.elasticsearch.client.security.ClearRolesCacheRequest;
import org.elasticsearch.client.security.ClearRolesCacheResponse;
import org.elasticsearch.client.security.CreateApiKeyRequest;
import org.elasticsearch.client.security.CreateApiKeyResponse;
import org.elasticsearch.client.security.CreateTokenRequest;
import org.elasticsearch.client.security.CreateTokenResponse;
import org.elasticsearch.client.security.DeletePrivilegesRequest;
Expand All @@ -39,6 +41,8 @@
import org.elasticsearch.client.security.DeleteUserResponse;
import org.elasticsearch.client.security.DisableUserRequest;
import org.elasticsearch.client.security.EnableUserRequest;
import org.elasticsearch.client.security.GetApiKeyRequest;
import org.elasticsearch.client.security.GetApiKeyResponse;
import org.elasticsearch.client.security.GetPrivilegesRequest;
import org.elasticsearch.client.security.GetPrivilegesResponse;
import org.elasticsearch.client.security.GetRoleMappingsRequest;
Expand All @@ -53,6 +57,8 @@
import org.elasticsearch.client.security.GetUsersResponse;
import org.elasticsearch.client.security.HasPrivilegesRequest;
import org.elasticsearch.client.security.HasPrivilegesResponse;
import org.elasticsearch.client.security.InvalidateApiKeyRequest;
import org.elasticsearch.client.security.InvalidateApiKeyResponse;
import org.elasticsearch.client.security.InvalidateTokenRequest;
import org.elasticsearch.client.security.InvalidateTokenResponse;
import org.elasticsearch.client.security.PutPrivilegesRequest;
Expand Down Expand Up @@ -842,4 +848,95 @@ public void deletePrivilegesAsync(DeletePrivilegesRequest request, RequestOption
restHighLevelClient.performRequestAsyncAndParseEntity(request, SecurityRequestConverters::deletePrivileges, options,
DeletePrivilegesResponse::fromXContent, listener, singleton(404));
}

/**
* Create an API Key.<br>
* See <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/security-api-create-api-key.html">
* the docs</a> for more.
*
* @param request the request to create a API key
* @param options the request options (e.g. headers), use {@link RequestOptions#DEFAULT} if nothing needs to be customized
* @return the response from the create API key call
* @throws IOException in case there is a problem sending the request or parsing back the response
*/
public CreateApiKeyResponse createApiKey(final CreateApiKeyRequest request, final RequestOptions options) throws IOException {
return restHighLevelClient.performRequestAndParseEntity(request, SecurityRequestConverters::createApiKey, options,
CreateApiKeyResponse::fromXContent, emptySet());
}

/**
* Asynchronously creates an API key.<br>
* See <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/security-api-create-api-key.html">
* the docs</a> for more.
*
* @param request the request to create a API key
* @param options the request options (e.g. headers), use {@link RequestOptions#DEFAULT} if nothing needs to be customized
* @param listener the listener to be notified upon request completion
*/
public void createApiKeyAsync(final CreateApiKeyRequest request, final RequestOptions options,
final ActionListener<CreateApiKeyResponse> listener) {
restHighLevelClient.performRequestAsyncAndParseEntity(request, SecurityRequestConverters::createApiKey, options,
CreateApiKeyResponse::fromXContent, listener, emptySet());
}

/**
* Retrieve API Key(s) information.<br>
* See <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/security-api-get-api-key.html">
* the docs</a> for more.
*
* @param request the request to retrieve API key(s)
* @param options the request options (e.g. headers), use {@link RequestOptions#DEFAULT} if nothing needs to be customized
* @return the response from the create API key call
* @throws IOException in case there is a problem sending the request or parsing back the response
*/
public GetApiKeyResponse getApiKey(final GetApiKeyRequest request, final RequestOptions options) throws IOException {
return restHighLevelClient.performRequestAndParseEntity(request, SecurityRequestConverters::getApiKey, options,
GetApiKeyResponse::fromXContent, emptySet());
}

/**
* Asynchronously retrieve API Key(s) information.<br>
* See <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/security-api-get-api-key.html">
* the docs</a> for more.
*
* @param request the request to retrieve API key(s)
* @param options the request options (e.g. headers), use {@link RequestOptions#DEFAULT} if nothing needs to be customized
* @param listener the listener to be notified upon request completion
*/
public void getApiKeyAsync(final GetApiKeyRequest request, final RequestOptions options,
final ActionListener<GetApiKeyResponse> listener) {
restHighLevelClient.performRequestAsyncAndParseEntity(request, SecurityRequestConverters::getApiKey, options,
GetApiKeyResponse::fromXContent, listener, emptySet());
}

/**
* Invalidate API Key(s).<br>
* See <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/security-api-invalidate-api-key.html">
* the docs</a> for more.
*
* @param request the request to invalidate API key(s)
* @param options the request options (e.g. headers), use {@link RequestOptions#DEFAULT} if nothing needs to be customized
* @return the response from the invalidate API key call
* @throws IOException in case there is a problem sending the request or parsing back the response
*/
public InvalidateApiKeyResponse invalidateApiKey(final InvalidateApiKeyRequest request, final RequestOptions options)
throws IOException {
return restHighLevelClient.performRequestAndParseEntity(request, SecurityRequestConverters::invalidateApiKey, options,
InvalidateApiKeyResponse::fromXContent, emptySet());
}

/**
* Asynchronously invalidates API key(s).<br>
* See <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/security-api-invalidate-api-key.html">
* the docs</a> for more.
*
* @param request the request to invalidate API key(s)
* @param options the request options (e.g. headers), use {@link RequestOptions#DEFAULT} if nothing needs to be customized
* @param listener the listener to be notified upon request completion
*/
public void invalidateApiKeyAsync(final InvalidateApiKeyRequest request, final RequestOptions options,
final ActionListener<InvalidateApiKeyResponse> listener) {
restHighLevelClient.performRequestAsyncAndParseEntity(request, SecurityRequestConverters::invalidateApiKey, options,
InvalidateApiKeyResponse::fromXContent, listener, emptySet());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,18 +26,21 @@
import org.elasticsearch.client.security.ChangePasswordRequest;
import org.elasticsearch.client.security.ClearRealmCacheRequest;
import org.elasticsearch.client.security.ClearRolesCacheRequest;
import org.elasticsearch.client.security.CreateApiKeyRequest;
import org.elasticsearch.client.security.CreateTokenRequest;
import org.elasticsearch.client.security.DeletePrivilegesRequest;
import org.elasticsearch.client.security.DeleteRoleMappingRequest;
import org.elasticsearch.client.security.DeleteRoleRequest;
import org.elasticsearch.client.security.DeleteUserRequest;
import org.elasticsearch.client.security.DisableUserRequest;
import org.elasticsearch.client.security.EnableUserRequest;
import org.elasticsearch.client.security.GetApiKeyRequest;
import org.elasticsearch.client.security.GetPrivilegesRequest;
import org.elasticsearch.client.security.GetRoleMappingsRequest;
import org.elasticsearch.client.security.GetRolesRequest;
import org.elasticsearch.client.security.GetUsersRequest;
import org.elasticsearch.client.security.HasPrivilegesRequest;
import org.elasticsearch.client.security.InvalidateApiKeyRequest;
import org.elasticsearch.client.security.InvalidateTokenRequest;
import org.elasticsearch.client.security.PutPrivilegesRequest;
import org.elasticsearch.client.security.PutRoleMappingRequest;
Expand Down Expand Up @@ -256,4 +259,36 @@ static Request putRole(final PutRoleRequest putRoleRequest) throws IOException {
params.withRefreshPolicy(putRoleRequest.getRefreshPolicy());
return request;
}

static Request createApiKey(final CreateApiKeyRequest createApiKeyRequest) throws IOException {
final Request request = new Request(HttpPost.METHOD_NAME, "/_security/api_key");
request.setEntity(createEntity(createApiKeyRequest, REQUEST_BODY_CONTENT_TYPE));
final RequestConverters.Params params = new RequestConverters.Params(request);
params.withRefreshPolicy(createApiKeyRequest.getRefreshPolicy());
return request;
}

static Request getApiKey(final GetApiKeyRequest getApiKeyRequest) throws IOException {
final Request request = new Request(HttpGet.METHOD_NAME, "/_security/api_key");
if (Strings.hasText(getApiKeyRequest.getId())) {
request.addParameter("id", getApiKeyRequest.getId());
}
if (Strings.hasText(getApiKeyRequest.getName())) {
request.addParameter("name", getApiKeyRequest.getName());
}
if (Strings.hasText(getApiKeyRequest.getUserName())) {
request.addParameter("username", getApiKeyRequest.getUserName());
}
if (Strings.hasText(getApiKeyRequest.getRealmName())) {
request.addParameter("realm_name", getApiKeyRequest.getRealmName());
}
return request;
}

static Request invalidateApiKey(final InvalidateApiKeyRequest invalidateApiKeyRequest) throws IOException {
final Request request = new Request(HttpDelete.METHOD_NAME, "/_security/api_key");
request.setEntity(createEntity(invalidateApiKeyRequest, REQUEST_BODY_CONTENT_TYPE));
final RequestConverters.Params params = new RequestConverters.Params(request);
return request;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
/*
* Licensed to Elasticsearch under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/

package org.elasticsearch.client.security;

import org.elasticsearch.client.Validatable;
import org.elasticsearch.client.security.user.privileges.Role;
import org.elasticsearch.common.Nullable;
import org.elasticsearch.common.Strings;
import org.elasticsearch.common.unit.TimeValue;
import org.elasticsearch.common.xcontent.ToXContentObject;
import org.elasticsearch.common.xcontent.XContentBuilder;

import java.io.IOException;
import java.util.List;
import java.util.Objects;

/**
* Request to create API key
*/
public final class CreateApiKeyRequest implements Validatable, ToXContentObject {

private final String name;
private final TimeValue expiration;
private final List<Role> roles;
private final RefreshPolicy refreshPolicy;

/**
* Create API Key request constructor
* @param name name for the API key
* @param roles list of {@link Role}s
* @param expiration to specify expiration for the API key
*/
public CreateApiKeyRequest(String name, List<Role> roles, @Nullable TimeValue expiration, @Nullable final RefreshPolicy refreshPolicy) {
if (Strings.hasText(name)) {
this.name = name;
} else {
throw new IllegalArgumentException("name must not be null or empty");
}
this.roles = Objects.requireNonNull(roles, "roles may not be null");
this.expiration = expiration;
this.refreshPolicy = (refreshPolicy == null) ? RefreshPolicy.getDefault() : refreshPolicy;
}

public String getName() {
return name;
}

public TimeValue getExpiration() {
return expiration;
}

public List<Role> getRoles() {
return roles;
}

public RefreshPolicy getRefreshPolicy() {
return refreshPolicy;
}

@Override
public int hashCode() {
return Objects.hash(name, refreshPolicy, roles, expiration);
}

@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
final CreateApiKeyRequest that = (CreateApiKeyRequest) o;
return Objects.equals(name, that.name) && Objects.equals(refreshPolicy, that.refreshPolicy) && Objects.equals(roles, that.roles)
&& Objects.equals(expiration, that.expiration);
}

@Override
public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
builder.startObject().field("name", name);
if (expiration != null) {
builder.field("expiration", expiration.getStringRep());
}
builder.startObject("role_descriptors");
for (Role role : roles) {
builder.startObject(role.getName());
if (role.getApplicationPrivileges() != null) {
builder.field(Role.APPLICATIONS.getPreferredName(), role.getApplicationPrivileges());
}
if (role.getClusterPrivileges() != null) {
builder.field(Role.CLUSTER.getPreferredName(), role.getClusterPrivileges());
}
if (role.getGlobalPrivileges() != null) {
builder.field(Role.GLOBAL.getPreferredName(), role.getGlobalPrivileges());
}
if (role.getIndicesPrivileges() != null) {
builder.field(Role.INDICES.getPreferredName(), role.getIndicesPrivileges());
}
if (role.getMetadata() != null) {
builder.field(Role.METADATA.getPreferredName(), role.getMetadata());
}
if (role.getRunAsPrivilege() != null) {
builder.field(Role.RUN_AS.getPreferredName(), role.getRunAsPrivilege());
}
builder.endObject();
}
builder.endObject();
return builder.endObject();
}

}
Loading

0 comments on commit fe36861

Please sign in to comment.