From 3554850d8a538311726eac8f953cc1bfa38039d5 Mon Sep 17 00:00:00 2001 From: Yogesh Gaikwad <902768+bizybot@users.noreply.github.com> Date: Tue, 5 Feb 2019 14:21:57 +1100 Subject: [PATCH] Add support for API keys to access Elasticsearch (#38291) 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 #34383 --- client/rest-high-level/build.gradle | 1 + .../elasticsearch/client/SecurityClient.java | 97 ++ .../client/SecurityRequestConverters.java | 35 + .../client/security/CreateApiKeyRequest.java | 128 +++ .../client/security/CreateApiKeyResponse.java | 105 +++ .../client/security/GetApiKeyRequest.java | 133 +++ .../client/security/GetApiKeyResponse.java | 91 ++ .../security/InvalidateApiKeyRequest.java | 145 +++ .../security/InvalidateApiKeyResponse.java | 121 +++ .../client/security/support/ApiKey.java | 152 ++++ .../SecurityRequestConvertersTests.java | 54 +- .../SecurityDocumentationIT.java | 401 ++++++++- .../security/CreateApiKeyRequestTests.java | 105 +++ .../security/CreateApiKeyResponseTests.java | 101 +++ .../security/GetApiKeyRequestTests.java | 72 ++ .../security/GetApiKeyResponseTests.java | 100 ++ .../InvalidateApiKeyRequestTests.java | 73 ++ .../InvalidateApiKeyResponseTests.java | 111 +++ .../security/create-api-key.asciidoc | 40 + .../high-level/security/get-api-key.asciidoc | 67 ++ .../security/invalidate-api-key.asciidoc | 75 ++ .../high-level/supported-apis.asciidoc | 6 + .../rest-api-spec/test/README.asciidoc | 25 + .../common/RandomBasedUUIDGenerator.java | 34 +- .../java/org/elasticsearch/common/UUIDs.java | 7 + .../common/io/stream/StreamInput.java | 17 + .../common/io/stream/StreamOutput.java | 21 + .../elasticsearch/common/util/set/Sets.java | 15 + .../common/io/stream/StreamTests.java | 32 + .../common/util/set/SetsTests.java | 12 + .../test/rest/yaml/Features.java | 4 +- .../rest/yaml/section/ExecutableSection.java | 1 + .../yaml/section/TransformAndSetSection.java | 106 +++ .../section/TransformAndSetSectionTests.java | 96 ++ x-pack/docs/build.gradle | 1 + x-pack/docs/en/rest-api/security.asciidoc | 14 + .../security/create-api-keys.asciidoc | 99 ++ .../rest-api/security/get-api-keys.asciidoc | 118 +++ .../security/invalidate-api-keys.asciidoc | 140 +++ x-pack/plugin/build.gradle | 1 + .../xpack/ccr/CcrLicenseChecker.java | 7 +- .../xpack/core/XPackClientPlugin.java | 6 + .../xpack/core/XPackSettings.java | 7 +- .../xpack/core/security/SecurityContext.java | 11 +- .../xpack/core/security/action/ApiKey.java | 165 ++++ .../security/action/CreateApiKeyAction.java | 39 + .../security/action/CreateApiKeyRequest.java | 136 +++ .../action/CreateApiKeyRequestBuilder.java | 85 ++ .../security/action/CreateApiKeyResponse.java | 168 ++++ .../core/security/action/GetApiKeyAction.java | 39 + .../security/action/GetApiKeyRequest.java | 150 +++ .../action/GetApiKeyRequestBuilder.java | 21 + .../security/action/GetApiKeyResponse.java | 88 ++ .../action/InvalidateApiKeyAction.java | 40 + .../action/InvalidateApiKeyRequest.java | 150 +++ .../InvalidateApiKeyRequestBuilder.java | 22 + .../action/InvalidateApiKeyResponse.java | 141 +++ .../action/role/GetRolesResponse.java | 4 +- .../action/user/HasPrivilegesResponse.java | 61 +- .../core/security/authc/Authentication.java | 62 +- .../DefaultAuthenticationFailureHandler.java | 6 +- .../core/security/authz/RoleDescriptor.java | 46 +- .../accesscontrol/IndicesAccessControl.java | 72 +- .../SecurityIndexSearcherWrapper.java | 179 +--- .../permission/ApplicationPermission.java | 36 + .../authz/permission/ClusterPermission.java | 10 + .../authz/permission/DocumentPermissions.java | 262 ++++++ .../authz/permission/FieldPermissions.java | 39 +- .../authz/permission/IndicesPermission.java | 48 +- .../authz/permission/LimitedRole.java | 152 ++++ .../authz/permission/ResourcePrivileges.java | 93 ++ .../permission/ResourcePrivilegesMap.java | 121 +++ .../core/security/authz/permission/Role.java | 81 +- .../SecurityQueryTemplateEvaluator.java | 92 ++ .../core/security/client/SecurityClient.java | 31 + .../core/security/support/Automatons.java | 6 + .../resources/security-index-template.json | 34 + .../CreateApiKeyRequestBuilderTests.java | 62 ++ .../action/CreateApiKeyRequestTests.java | 113 +++ .../action/CreateApiKeyResponseTests.java | 81 ++ .../action/GetApiKeyRequestTests.java | 103 +++ .../action/GetApiKeyResponseTests.java | 64 ++ .../action/InvalidateApiKeyRequestTests.java | 104 +++ .../action/InvalidateApiKeyResponseTests.java | 88 ++ .../user/HasPrivilegesResponseTests.java | 36 +- ...aultAuthenticationFailureHandlerTests.java | 5 +- ...yIndexSearcherWrapperIntegrationTests.java | 131 ++- ...SecurityIndexSearcherWrapperUnitTests.java | 152 +--- .../permission/DocumentPermissionsTests.java | 123 +++ .../permission/FieldPermissionsTests.java | 81 ++ .../authz/permission/LimitedRoleTests.java | 403 +++++++++ .../ResourcePrivilegesMapTests.java | 91 ++ .../permission/ResourcePrivilegesTests.java | 70 ++ .../SecurityQueryTemplateEvaluatorTests.java | 94 ++ .../ml/action/TransportPutDatafeedAction.java | 3 +- .../security/ApiKeySSLBootstrapCheck.java | 37 + .../xpack/security/Security.java | 57 +- .../action/TransportCreateApiKeyAction.java | 51 ++ .../action/TransportGetApiKeyAction.java | 48 + .../TransportInvalidateApiKeyAction.java | 46 + .../BulkShardRequestInterceptor.java | 2 +- ...cumentLevelSecurityRequestInterceptor.java | 2 +- .../IndicesAliasesRequestInterceptor.java | 2 +- .../interceptor/ResizeRequestInterceptor.java | 2 +- .../TransportGetUserPrivilegesAction.java | 5 +- .../user/TransportHasPrivilegesAction.java | 124 +-- .../xpack/security/authc/ApiKeyService.java | 851 ++++++++++++++++++ .../security/authc/AuthenticationService.java | 40 +- .../security/authc/ExpiredApiKeysRemover.java | 116 +++ .../xpack/security/authc/TokenService.java | 9 +- .../security/authz/AuthorizationService.java | 59 +- .../security/authz/AuthorizationUtils.java | 5 +- .../security/authz/AuthorizedIndices.java | 2 +- .../authz/store/CompositeRolesStore.java | 144 ++- .../rest/action/RestCreateApiKeyAction.java | 56 ++ .../rest/action/RestGetApiKeyAction.java | 63 ++ .../action/RestInvalidateApiKeyAction.java | 70 ++ .../ApiKeySSLBootstrapCheckTests.java | 32 + .../xpack/security/SecurityContextTests.java | 7 + .../xpack/security/SecurityTests.java | 9 +- .../security/TokenSSLBootsrapCheckTests.java | 9 +- .../filter/SecurityActionFilterTests.java | 16 +- ...IndicesAliasesRequestInterceptorTests.java | 4 +- .../ResizeRequestInterceptorTests.java | 4 +- .../TransportHasPrivilegesActionTests.java | 142 ++- .../security/authc/ApiKeyIntegTests.java | 494 ++++++++++ .../security/authc/ApiKeyServiceTests.java | 295 ++++++ .../authc/AuthenticationServiceTests.java | 166 +++- .../security/authc/TokenServiceTests.java | 20 +- .../authz/AuthorizationServiceTests.java | 213 +++-- .../authz/IndicesAndAliasesResolverTests.java | 13 +- .../security/authz/RoleDescriptorTests.java | 8 +- .../IndicesAccessControlTests.java | 101 +++ .../accesscontrol/IndicesPermissionTests.java | 53 +- .../accesscontrol/OptOutQueryCacheTests.java | 28 +- .../authz/store/CompositeRolesStoreTests.java | 74 +- .../action/RestCreateApiKeyActionTests.java | 116 +++ .../rest/action/RestGetApiKeyActionTests.java | 142 +++ .../RestInvalidateApiKeyActionTests.java | 126 +++ .../transport/ServerTransportFilterTests.java | 20 +- .../api/security.create_api_key.json | 22 + .../api/security.get_api_key.json | 30 + .../api/security.invalidate_api_key.json | 15 + .../rest-api-spec/test/api_key/10_basic.yml | 290 ++++++ 144 files changed, 10792 insertions(+), 918 deletions(-) create mode 100644 client/rest-high-level/src/main/java/org/elasticsearch/client/security/CreateApiKeyRequest.java create mode 100644 client/rest-high-level/src/main/java/org/elasticsearch/client/security/CreateApiKeyResponse.java create mode 100644 client/rest-high-level/src/main/java/org/elasticsearch/client/security/GetApiKeyRequest.java create mode 100644 client/rest-high-level/src/main/java/org/elasticsearch/client/security/GetApiKeyResponse.java create mode 100644 client/rest-high-level/src/main/java/org/elasticsearch/client/security/InvalidateApiKeyRequest.java create mode 100644 client/rest-high-level/src/main/java/org/elasticsearch/client/security/InvalidateApiKeyResponse.java create mode 100644 client/rest-high-level/src/main/java/org/elasticsearch/client/security/support/ApiKey.java create mode 100644 client/rest-high-level/src/test/java/org/elasticsearch/client/security/CreateApiKeyRequestTests.java create mode 100644 client/rest-high-level/src/test/java/org/elasticsearch/client/security/CreateApiKeyResponseTests.java create mode 100644 client/rest-high-level/src/test/java/org/elasticsearch/client/security/GetApiKeyRequestTests.java create mode 100644 client/rest-high-level/src/test/java/org/elasticsearch/client/security/GetApiKeyResponseTests.java create mode 100644 client/rest-high-level/src/test/java/org/elasticsearch/client/security/InvalidateApiKeyRequestTests.java create mode 100644 client/rest-high-level/src/test/java/org/elasticsearch/client/security/InvalidateApiKeyResponseTests.java create mode 100644 docs/java-rest/high-level/security/create-api-key.asciidoc create mode 100644 docs/java-rest/high-level/security/get-api-key.asciidoc create mode 100644 docs/java-rest/high-level/security/invalidate-api-key.asciidoc create mode 100644 test/framework/src/main/java/org/elasticsearch/test/rest/yaml/section/TransformAndSetSection.java create mode 100644 test/framework/src/test/java/org/elasticsearch/test/rest/yaml/section/TransformAndSetSectionTests.java create mode 100644 x-pack/docs/en/rest-api/security/create-api-keys.asciidoc create mode 100644 x-pack/docs/en/rest-api/security/get-api-keys.asciidoc create mode 100644 x-pack/docs/en/rest-api/security/invalidate-api-keys.asciidoc create mode 100644 x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/ApiKey.java create mode 100644 x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/CreateApiKeyAction.java create mode 100644 x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/CreateApiKeyRequest.java create mode 100644 x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/CreateApiKeyRequestBuilder.java create mode 100644 x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/CreateApiKeyResponse.java create mode 100644 x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/GetApiKeyAction.java create mode 100644 x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/GetApiKeyRequest.java create mode 100644 x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/GetApiKeyRequestBuilder.java create mode 100644 x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/GetApiKeyResponse.java create mode 100644 x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/InvalidateApiKeyAction.java create mode 100644 x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/InvalidateApiKeyRequest.java create mode 100644 x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/InvalidateApiKeyRequestBuilder.java create mode 100644 x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/InvalidateApiKeyResponse.java create mode 100644 x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/DocumentPermissions.java create mode 100644 x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/LimitedRole.java create mode 100644 x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/ResourcePrivileges.java create mode 100644 x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/ResourcePrivilegesMap.java create mode 100644 x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/support/SecurityQueryTemplateEvaluator.java create mode 100644 x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/CreateApiKeyRequestBuilderTests.java create mode 100644 x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/CreateApiKeyRequestTests.java create mode 100644 x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/CreateApiKeyResponseTests.java create mode 100644 x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/GetApiKeyRequestTests.java create mode 100644 x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/GetApiKeyResponseTests.java create mode 100644 x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/InvalidateApiKeyRequestTests.java create mode 100644 x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/InvalidateApiKeyResponseTests.java create mode 100644 x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/permission/DocumentPermissionsTests.java create mode 100644 x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/permission/FieldPermissionsTests.java create mode 100644 x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/permission/LimitedRoleTests.java create mode 100644 x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/permission/ResourcePrivilegesMapTests.java create mode 100644 x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/permission/ResourcePrivilegesTests.java create mode 100644 x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/support/SecurityQueryTemplateEvaluatorTests.java create mode 100644 x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/ApiKeySSLBootstrapCheck.java create mode 100644 x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/TransportCreateApiKeyAction.java create mode 100644 x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/TransportGetApiKeyAction.java create mode 100644 x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/TransportInvalidateApiKeyAction.java create mode 100644 x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/ApiKeyService.java create mode 100644 x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/ExpiredApiKeysRemover.java create mode 100644 x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/RestCreateApiKeyAction.java create mode 100644 x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/RestGetApiKeyAction.java create mode 100644 x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/RestInvalidateApiKeyAction.java create mode 100644 x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/ApiKeySSLBootstrapCheckTests.java create mode 100644 x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/ApiKeyIntegTests.java create mode 100644 x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/ApiKeyServiceTests.java create mode 100644 x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/rest/action/RestCreateApiKeyActionTests.java create mode 100644 x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/rest/action/RestGetApiKeyActionTests.java create mode 100644 x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/rest/action/RestInvalidateApiKeyActionTests.java create mode 100644 x-pack/plugin/src/test/resources/rest-api-spec/api/security.create_api_key.json create mode 100644 x-pack/plugin/src/test/resources/rest-api-spec/api/security.get_api_key.json create mode 100644 x-pack/plugin/src/test/resources/rest-api-spec/api/security.invalidate_api_key.json create mode 100644 x-pack/plugin/src/test/resources/rest-api-spec/test/api_key/10_basic.yml diff --git a/client/rest-high-level/build.gradle b/client/rest-high-level/build.gradle index 22e6252892a7d..44262f09346de 100644 --- a/client/rest-high-level/build.gradle +++ b/client/rest-high-level/build.gradle @@ -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' diff --git a/client/rest-high-level/src/main/java/org/elasticsearch/client/SecurityClient.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/SecurityClient.java index 4d8d1d5db43aa..f66826b3c5b3b 100644 --- a/client/rest-high-level/src/main/java/org/elasticsearch/client/SecurityClient.java +++ b/client/rest-high-level/src/main/java/org/elasticsearch/client/SecurityClient.java @@ -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; @@ -40,6 +42,8 @@ import org.elasticsearch.client.security.DisableUserRequest; import org.elasticsearch.client.security.EmptyResponse; 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; @@ -54,6 +58,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; @@ -850,4 +856,95 @@ public void deletePrivilegesAsync(DeletePrivilegesRequest request, RequestOption restHighLevelClient.performRequestAsyncAndParseEntity(request, SecurityRequestConverters::deletePrivileges, options, DeletePrivilegesResponse::fromXContent, listener, singleton(404)); } + + /** + * Create an API Key.
+ * See + * the docs 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.
+ * See + * the docs 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 listener) { + restHighLevelClient.performRequestAsyncAndParseEntity(request, SecurityRequestConverters::createApiKey, options, + CreateApiKeyResponse::fromXContent, listener, emptySet()); + } + + /** + * Retrieve API Key(s) information.
+ * See + * the docs 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.
+ * See + * the docs 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 listener) { + restHighLevelClient.performRequestAsyncAndParseEntity(request, SecurityRequestConverters::getApiKey, options, + GetApiKeyResponse::fromXContent, listener, emptySet()); + } + + /** + * Invalidate API Key(s).
+ * See + * the docs 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).
+ * See + * the docs 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 listener) { + restHighLevelClient.performRequestAsyncAndParseEntity(request, SecurityRequestConverters::invalidateApiKey, options, + InvalidateApiKeyResponse::fromXContent, listener, emptySet()); + } } diff --git a/client/rest-high-level/src/main/java/org/elasticsearch/client/SecurityRequestConverters.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/SecurityRequestConverters.java index 290b9eb843eac..d4b5ceeae0d55 100644 --- a/client/rest-high-level/src/main/java/org/elasticsearch/client/SecurityRequestConverters.java +++ b/client/rest-high-level/src/main/java/org/elasticsearch/client/SecurityRequestConverters.java @@ -26,6 +26,7 @@ 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; @@ -33,11 +34,13 @@ 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; @@ -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; + } } diff --git a/client/rest-high-level/src/main/java/org/elasticsearch/client/security/CreateApiKeyRequest.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/security/CreateApiKeyRequest.java new file mode 100644 index 0000000000000..ad5f0a9ba2cf6 --- /dev/null +++ b/client/rest-high-level/src/main/java/org/elasticsearch/client/security/CreateApiKeyRequest.java @@ -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 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 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 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(); + } + +} diff --git a/client/rest-high-level/src/main/java/org/elasticsearch/client/security/CreateApiKeyResponse.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/security/CreateApiKeyResponse.java new file mode 100644 index 0000000000000..9c5037237407b --- /dev/null +++ b/client/rest-high-level/src/main/java/org/elasticsearch/client/security/CreateApiKeyResponse.java @@ -0,0 +1,105 @@ +/* + * 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.common.Nullable; +import org.elasticsearch.common.ParseField; +import org.elasticsearch.common.settings.SecureString; +import org.elasticsearch.common.xcontent.ConstructingObjectParser; +import org.elasticsearch.common.xcontent.XContentParser; + +import java.io.IOException; +import java.time.Instant; +import java.util.Objects; + +import static org.elasticsearch.common.xcontent.ConstructingObjectParser.constructorArg; +import static org.elasticsearch.common.xcontent.ConstructingObjectParser.optionalConstructorArg; + +/** + * Response for create API key + */ +public final class CreateApiKeyResponse { + + private final String name; + private final String id; + private final SecureString key; + private final Instant expiration; + + public CreateApiKeyResponse(String name, String id, SecureString key, Instant expiration) { + this.name = name; + this.id = id; + this.key = key; + // As we do not yet support the nanosecond precision when we serialize to JSON, + // here creating the 'Instant' of milliseconds precision. + // This Instant can then be used for date comparison. + this.expiration = (expiration != null) ? Instant.ofEpochMilli(expiration.toEpochMilli()): null; + } + + public String getName() { + return name; + } + + public String getId() { + return id; + } + + public SecureString getKey() { + return key; + } + + @Nullable + public Instant getExpiration() { + return expiration; + } + + @Override + public int hashCode() { + return Objects.hash(id, name, key, expiration); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + final CreateApiKeyResponse other = (CreateApiKeyResponse) obj; + return Objects.equals(id, other.id) + && Objects.equals(key, other.key) + && Objects.equals(name, other.name) + && Objects.equals(expiration, other.expiration); + } + + static ConstructingObjectParser PARSER = new ConstructingObjectParser<>("create_api_key_response", + args -> new CreateApiKeyResponse((String) args[0], (String) args[1], new SecureString((String) args[2]), + (args[3] == null) ? null : Instant.ofEpochMilli((Long) args[3]))); + static { + PARSER.declareString(constructorArg(), new ParseField("name")); + PARSER.declareString(constructorArg(), new ParseField("id")); + PARSER.declareString(constructorArg(), new ParseField("api_key")); + PARSER.declareLong(optionalConstructorArg(), new ParseField("expiration")); + } + + public static CreateApiKeyResponse fromXContent(XContentParser parser) throws IOException { + return PARSER.parse(parser, null); + } +} diff --git a/client/rest-high-level/src/main/java/org/elasticsearch/client/security/GetApiKeyRequest.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/security/GetApiKeyRequest.java new file mode 100644 index 0000000000000..6fa98ec549b07 --- /dev/null +++ b/client/rest-high-level/src/main/java/org/elasticsearch/client/security/GetApiKeyRequest.java @@ -0,0 +1,133 @@ +/* + * 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.common.Nullable; +import org.elasticsearch.common.Strings; +import org.elasticsearch.common.xcontent.ToXContentObject; +import org.elasticsearch.common.xcontent.XContentBuilder; + +import java.io.IOException; + +/** + * Request for get API key + */ +public final class GetApiKeyRequest implements Validatable, ToXContentObject { + + private final String realmName; + private final String userName; + private final String id; + private final String name; + + // pkg scope for testing + GetApiKeyRequest(@Nullable String realmName, @Nullable String userName, @Nullable String apiKeyId, + @Nullable String apiKeyName) { + if (Strings.hasText(realmName) == false && Strings.hasText(userName) == false && Strings.hasText(apiKeyId) == false + && Strings.hasText(apiKeyName) == false) { + throwValidationError("One of [api key id, api key name, username, realm name] must be specified"); + } + if (Strings.hasText(apiKeyId) || Strings.hasText(apiKeyName)) { + if (Strings.hasText(realmName) || Strings.hasText(userName)) { + throwValidationError( + "username or realm name must not be specified when the api key id or api key name is specified"); + } + } + if (Strings.hasText(apiKeyId) && Strings.hasText(apiKeyName)) { + throwValidationError("only one of [api key id, api key name] can be specified"); + } + this.realmName = realmName; + this.userName = userName; + this.id = apiKeyId; + this.name = apiKeyName; + } + + private void throwValidationError(String message) { + throw new IllegalArgumentException(message); + } + + public String getRealmName() { + return realmName; + } + + public String getUserName() { + return userName; + } + + public String getId() { + return id; + } + + public String getName() { + return name; + } + + /** + * Creates get API key request for given realm name + * @param realmName realm name + * @return {@link GetApiKeyRequest} + */ + public static GetApiKeyRequest usingRealmName(String realmName) { + return new GetApiKeyRequest(realmName, null, null, null); + } + + /** + * Creates get API key request for given user name + * @param userName user name + * @return {@link GetApiKeyRequest} + */ + public static GetApiKeyRequest usingUserName(String userName) { + return new GetApiKeyRequest(null, userName, null, null); + } + + /** + * Creates get API key request for given realm and user name + * @param realmName realm name + * @param userName user name + * @return {@link GetApiKeyRequest} + */ + public static GetApiKeyRequest usingRealmAndUserName(String realmName, String userName) { + return new GetApiKeyRequest(realmName, userName, null, null); + } + + /** + * Creates get API key request for given api key id + * @param apiKeyId api key id + * @return {@link GetApiKeyRequest} + */ + public static GetApiKeyRequest usingApiKeyId(String apiKeyId) { + return new GetApiKeyRequest(null, null, apiKeyId, null); + } + + /** + * Creates get API key request for given api key name + * @param apiKeyName api key name + * @return {@link GetApiKeyRequest} + */ + public static GetApiKeyRequest usingApiKeyName(String apiKeyName) { + return new GetApiKeyRequest(null, null, null, apiKeyName); + } + + @Override + public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { + return builder; + } + +} diff --git a/client/rest-high-level/src/main/java/org/elasticsearch/client/security/GetApiKeyResponse.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/security/GetApiKeyResponse.java new file mode 100644 index 0000000000000..58e3e8effbb09 --- /dev/null +++ b/client/rest-high-level/src/main/java/org/elasticsearch/client/security/GetApiKeyResponse.java @@ -0,0 +1,91 @@ +/* + * 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.security.support.ApiKey; +import org.elasticsearch.common.ParseField; +import org.elasticsearch.common.xcontent.ConstructingObjectParser; +import org.elasticsearch.common.xcontent.XContentParser; + +import java.io.IOException; +import java.util.Collections; +import java.util.List; +import java.util.Objects; + +import static org.elasticsearch.common.xcontent.ConstructingObjectParser.optionalConstructorArg; + +/** + * Response for get API keys.
+ * The result contains information about the API keys that were found. + */ +public final class GetApiKeyResponse { + + private final List foundApiKeysInfo; + + public GetApiKeyResponse(List foundApiKeysInfo) { + Objects.requireNonNull(foundApiKeysInfo, "found_api_keys_info must be provided"); + this.foundApiKeysInfo = Collections.unmodifiableList(foundApiKeysInfo); + } + + public static GetApiKeyResponse emptyResponse() { + return new GetApiKeyResponse(Collections.emptyList()); + } + + public List getApiKeyInfos() { + return foundApiKeysInfo; + } + + @Override + public int hashCode() { + return Objects.hash(foundApiKeysInfo); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + final GetApiKeyResponse other = (GetApiKeyResponse) obj; + return Objects.equals(foundApiKeysInfo, other.foundApiKeysInfo); + } + + @SuppressWarnings("unchecked") + static ConstructingObjectParser PARSER = new ConstructingObjectParser<>("get_api_key_response", args -> { + return (args[0] == null) ? GetApiKeyResponse.emptyResponse() : new GetApiKeyResponse((List) args[0]); + }); + static { + PARSER.declareObjectArray(optionalConstructorArg(), (p, c) -> ApiKey.fromXContent(p), new ParseField("api_keys")); + } + + public static GetApiKeyResponse fromXContent(XContentParser parser) throws IOException { + return PARSER.parse(parser, null); + } + + @Override + public String toString() { + return "GetApiKeyResponse [foundApiKeysInfo=" + foundApiKeysInfo + "]"; + } +} diff --git a/client/rest-high-level/src/main/java/org/elasticsearch/client/security/InvalidateApiKeyRequest.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/security/InvalidateApiKeyRequest.java new file mode 100644 index 0000000000000..d3203354b7ab1 --- /dev/null +++ b/client/rest-high-level/src/main/java/org/elasticsearch/client/security/InvalidateApiKeyRequest.java @@ -0,0 +1,145 @@ +/* + * 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.common.Nullable; +import org.elasticsearch.common.Strings; +import org.elasticsearch.common.xcontent.ToXContentObject; +import org.elasticsearch.common.xcontent.XContentBuilder; + +import java.io.IOException; + +/** + * Request for invalidating API key(s) so that it can no longer be used + */ +public final class InvalidateApiKeyRequest implements Validatable, ToXContentObject { + + private final String realmName; + private final String userName; + private final String id; + private final String name; + + // pkg scope for testing + InvalidateApiKeyRequest(@Nullable String realmName, @Nullable String userName, @Nullable String apiKeyId, + @Nullable String apiKeyName) { + if (Strings.hasText(realmName) == false && Strings.hasText(userName) == false && Strings.hasText(apiKeyId) == false + && Strings.hasText(apiKeyName) == false) { + throwValidationError("One of [api key id, api key name, username, realm name] must be specified"); + } + if (Strings.hasText(apiKeyId) || Strings.hasText(apiKeyName)) { + if (Strings.hasText(realmName) || Strings.hasText(userName)) { + throwValidationError( + "username or realm name must not be specified when the api key id or api key name is specified"); + } + } + if (Strings.hasText(apiKeyId) && Strings.hasText(apiKeyName)) { + throwValidationError("only one of [api key id, api key name] can be specified"); + } + this.realmName = realmName; + this.userName = userName; + this.id = apiKeyId; + this.name = apiKeyName; + } + + private void throwValidationError(String message) { + throw new IllegalArgumentException(message); + } + + public String getRealmName() { + return realmName; + } + + public String getUserName() { + return userName; + } + + public String getId() { + return id; + } + + public String getName() { + return name; + } + + /** + * Creates invalidate API key request for given realm name + * @param realmName realm name + * @return {@link InvalidateApiKeyRequest} + */ + public static InvalidateApiKeyRequest usingRealmName(String realmName) { + return new InvalidateApiKeyRequest(realmName, null, null, null); + } + + /** + * Creates invalidate API key request for given user name + * @param userName user name + * @return {@link InvalidateApiKeyRequest} + */ + public static InvalidateApiKeyRequest usingUserName(String userName) { + return new InvalidateApiKeyRequest(null, userName, null, null); + } + + /** + * Creates invalidate API key request for given realm and user name + * @param realmName realm name + * @param userName user name + * @return {@link InvalidateApiKeyRequest} + */ + public static InvalidateApiKeyRequest usingRealmAndUserName(String realmName, String userName) { + return new InvalidateApiKeyRequest(realmName, userName, null, null); + } + + /** + * Creates invalidate API key request for given api key id + * @param apiKeyId api key id + * @return {@link InvalidateApiKeyRequest} + */ + public static InvalidateApiKeyRequest usingApiKeyId(String apiKeyId) { + return new InvalidateApiKeyRequest(null, null, apiKeyId, null); + } + + /** + * Creates invalidate API key request for given api key name + * @param apiKeyName api key name + * @return {@link InvalidateApiKeyRequest} + */ + public static InvalidateApiKeyRequest usingApiKeyName(String apiKeyName) { + return new InvalidateApiKeyRequest(null, null, null, apiKeyName); + } + + @Override + public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { + builder.startObject(); + if (realmName != null) { + builder.field("realm_name", realmName); + } + if (userName != null) { + builder.field("username", userName); + } + if (id != null) { + builder.field("id", id); + } + if (name != null) { + builder.field("name", name); + } + return builder.endObject(); + } +} diff --git a/client/rest-high-level/src/main/java/org/elasticsearch/client/security/InvalidateApiKeyResponse.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/security/InvalidateApiKeyResponse.java new file mode 100644 index 0000000000000..48df9d0f7f12b --- /dev/null +++ b/client/rest-high-level/src/main/java/org/elasticsearch/client/security/InvalidateApiKeyResponse.java @@ -0,0 +1,121 @@ +/* + * 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.ElasticsearchException; +import org.elasticsearch.common.Nullable; +import org.elasticsearch.common.ParseField; +import org.elasticsearch.common.xcontent.ConstructingObjectParser; +import org.elasticsearch.common.xcontent.XContentParser; + +import java.io.IOException; +import java.util.Collections; +import java.util.List; +import java.util.Objects; + +import static org.elasticsearch.common.xcontent.ConstructingObjectParser.constructorArg; +import static org.elasticsearch.common.xcontent.ConstructingObjectParser.optionalConstructorArg; + +public final class InvalidateApiKeyResponse { + + private final List invalidatedApiKeys; + private final List previouslyInvalidatedApiKeys; + private final List errors; + + /** + * Constructor for API keys invalidation response + * @param invalidatedApiKeys list of invalidated API key ids + * @param previouslyInvalidatedApiKeys list of previously invalidated API key ids + * @param errors list of encountered errors while invalidating API keys + */ + public InvalidateApiKeyResponse(List invalidatedApiKeys, List previouslyInvalidatedApiKeys, + @Nullable List errors) { + this.invalidatedApiKeys = Objects.requireNonNull(invalidatedApiKeys, "invalidated_api_keys must be provided"); + this.previouslyInvalidatedApiKeys = Objects.requireNonNull(previouslyInvalidatedApiKeys, + "previously_invalidated_api_keys must be provided"); + if (null != errors) { + this.errors = errors; + } else { + this.errors = Collections.emptyList(); + } + } + + public static InvalidateApiKeyResponse emptyResponse() { + return new InvalidateApiKeyResponse(Collections.emptyList(), Collections.emptyList(), Collections.emptyList()); + } + + public List getInvalidatedApiKeys() { + return invalidatedApiKeys; + } + + public List getPreviouslyInvalidatedApiKeys() { + return previouslyInvalidatedApiKeys; + } + + public List getErrors() { + return errors; + } + + @SuppressWarnings("unchecked") + static ConstructingObjectParser PARSER = new ConstructingObjectParser<>("invalidate_api_key_response", + args -> { + return new InvalidateApiKeyResponse((List) args[0], (List) args[1], (List) args[3]); + }); + static { + PARSER.declareStringArray(constructorArg(), new ParseField("invalidated_api_keys")); + PARSER.declareStringArray(constructorArg(), new ParseField("previously_invalidated_api_keys")); + // error count is parsed but ignored as we have list of errors + PARSER.declareInt(constructorArg(), new ParseField("error_count")); + PARSER.declareObjectArray(optionalConstructorArg(), (p, c) -> ElasticsearchException.fromXContent(p), + new ParseField("error_details")); + } + + public static InvalidateApiKeyResponse fromXContent(XContentParser parser) throws IOException { + return PARSER.parse(parser, null); + } + + @Override + public int hashCode() { + return Objects.hash(invalidatedApiKeys, previouslyInvalidatedApiKeys, errors); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + InvalidateApiKeyResponse other = (InvalidateApiKeyResponse) obj; + return Objects.equals(invalidatedApiKeys, other.invalidatedApiKeys) + && Objects.equals(previouslyInvalidatedApiKeys, other.previouslyInvalidatedApiKeys) + && Objects.equals(errors, other.errors); + } + + @Override + public String toString() { + return "ApiKeysInvalidationResult [invalidatedApiKeys=" + invalidatedApiKeys + ", previouslyInvalidatedApiKeys=" + + previouslyInvalidatedApiKeys + ", errors=" + errors + "]"; + } +} diff --git a/client/rest-high-level/src/main/java/org/elasticsearch/client/security/support/ApiKey.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/security/support/ApiKey.java new file mode 100644 index 0000000000000..d021628f750cb --- /dev/null +++ b/client/rest-high-level/src/main/java/org/elasticsearch/client/security/support/ApiKey.java @@ -0,0 +1,152 @@ +/* + * 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.support; + +import org.elasticsearch.common.ParseField; +import org.elasticsearch.common.xcontent.ConstructingObjectParser; +import org.elasticsearch.common.xcontent.XContentParser; + +import java.io.IOException; +import java.time.Instant; +import java.util.Objects; + +import static org.elasticsearch.common.xcontent.ConstructingObjectParser.constructorArg; +import static org.elasticsearch.common.xcontent.ConstructingObjectParser.optionalConstructorArg; + +/** + * API key information + */ +public final class ApiKey { + + private final String name; + private final String id; + private final Instant creation; + private final Instant expiration; + private final boolean invalidated; + private final String username; + private final String realm; + + public ApiKey(String name, String id, Instant creation, Instant expiration, boolean invalidated, String username, String realm) { + this.name = name; + this.id = id; + // As we do not yet support the nanosecond precision when we serialize to JSON, + // here creating the 'Instant' of milliseconds precision. + // This Instant can then be used for date comparison. + this.creation = Instant.ofEpochMilli(creation.toEpochMilli()); + this.expiration = (expiration != null) ? Instant.ofEpochMilli(expiration.toEpochMilli()): null; + this.invalidated = invalidated; + this.username = username; + this.realm = realm; + } + + public String getId() { + return id; + } + + public String getName() { + return name; + } + + /** + * @return a instance of {@link Instant} when this API key was created. + */ + public Instant getCreation() { + return creation; + } + + /** + * @return a instance of {@link Instant} when this API key will expire. In case the API key does not expire then will return + * {@code null} + */ + public Instant getExpiration() { + return expiration; + } + + /** + * @return {@code true} if this API key has been invalidated else returns {@code false} + */ + public boolean isInvalidated() { + return invalidated; + } + + /** + * @return the username for which this API key was created. + */ + public String getUsername() { + return username; + } + + /** + * @return the realm name of the user for which this API key was created. + */ + public String getRealm() { + return realm; + } + + @Override + public int hashCode() { + return Objects.hash(name, id, creation, expiration, invalidated, username, realm); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + ApiKey other = (ApiKey) obj; + return Objects.equals(name, other.name) + && Objects.equals(id, other.id) + && Objects.equals(creation, other.creation) + && Objects.equals(expiration, other.expiration) + && Objects.equals(invalidated, other.invalidated) + && Objects.equals(username, other.username) + && Objects.equals(realm, other.realm); + } + + static ConstructingObjectParser PARSER = new ConstructingObjectParser<>("api_key", args -> { + return new ApiKey((String) args[0], (String) args[1], Instant.ofEpochMilli((Long) args[2]), + (args[3] == null) ? null : Instant.ofEpochMilli((Long) args[3]), (Boolean) args[4], (String) args[5], (String) args[6]); + }); + static { + PARSER.declareString(constructorArg(), new ParseField("name")); + PARSER.declareString(constructorArg(), new ParseField("id")); + PARSER.declareLong(constructorArg(), new ParseField("creation")); + PARSER.declareLong(optionalConstructorArg(), new ParseField("expiration")); + PARSER.declareBoolean(constructorArg(), new ParseField("invalidated")); + PARSER.declareString(constructorArg(), new ParseField("username")); + PARSER.declareString(constructorArg(), new ParseField("realm")); + } + + public static ApiKey fromXContent(XContentParser parser) throws IOException { + return PARSER.parse(parser, null); + } + + @Override + public String toString() { + return "ApiKey [name=" + name + ", id=" + id + ", creation=" + creation + ", expiration=" + expiration + ", invalidated=" + + invalidated + ", username=" + username + ", realm=" + realm + "]"; + } +} diff --git a/client/rest-high-level/src/test/java/org/elasticsearch/client/SecurityRequestConvertersTests.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/SecurityRequestConvertersTests.java index ae04ffd258af9..0e36a0656c1f1 100644 --- a/client/rest-high-level/src/test/java/org/elasticsearch/client/SecurityRequestConvertersTests.java +++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/SecurityRequestConvertersTests.java @@ -24,6 +24,7 @@ import org.apache.http.client.methods.HttpPost; import org.apache.http.client.methods.HttpPut; import org.elasticsearch.client.security.ChangePasswordRequest; +import org.elasticsearch.client.security.CreateApiKeyRequest; import org.elasticsearch.client.security.CreateTokenRequest; import org.elasticsearch.client.security.DeletePrivilegesRequest; import org.elasticsearch.client.security.DeleteRoleMappingRequest; @@ -31,10 +32,12 @@ 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.InvalidateApiKeyRequest; import org.elasticsearch.client.security.PutPrivilegesRequest; import org.elasticsearch.client.security.PutRoleMappingRequest; import org.elasticsearch.client.security.PutRoleRequest; @@ -44,11 +47,14 @@ import org.elasticsearch.client.security.support.expressiondsl.expressions.AnyRoleMapperExpression; import org.elasticsearch.client.security.support.expressiondsl.fields.FieldRoleMapperExpression; import org.elasticsearch.client.security.user.User; -import org.elasticsearch.client.security.user.privileges.ApplicationResourcePrivileges; import org.elasticsearch.client.security.user.privileges.ApplicationPrivilege; +import org.elasticsearch.client.security.user.privileges.ApplicationResourcePrivileges; import org.elasticsearch.client.security.user.privileges.IndicesPrivileges; import org.elasticsearch.client.security.user.privileges.Role; +import org.elasticsearch.client.security.user.privileges.Role.ClusterPrivilegeName; +import org.elasticsearch.client.security.user.privileges.Role.IndexPrivilegeName; import org.elasticsearch.common.Strings; +import org.elasticsearch.common.unit.TimeValue; import org.elasticsearch.common.util.set.Sets; import org.elasticsearch.test.ESTestCase; @@ -61,6 +67,7 @@ import java.util.Map; import static org.elasticsearch.client.RequestConvertersTests.assertToXContentBody; +import static org.hamcrest.Matchers.equalTo; public class SecurityRequestConvertersTests extends ESTestCase { @@ -411,4 +418,47 @@ public void testPutRole() throws IOException { assertEquals(expectedParams, request.getParameters()); assertToXContentBody(putRoleRequest, request.getEntity()); } -} + + public void testCreateApiKey() throws IOException { + final String name = randomAlphaOfLengthBetween(4, 7); + final List roles = Collections.singletonList(Role.builder().name("r1").clusterPrivileges(ClusterPrivilegeName.ALL) + .indicesPrivileges(IndicesPrivileges.builder().indices("ind-x").privileges(IndexPrivilegeName.ALL).build()).build()); + final TimeValue expiration = randomBoolean() ? null : TimeValue.timeValueHours(24); + final RefreshPolicy refreshPolicy = randomFrom(RefreshPolicy.values()); + final Map expectedParams; + if (refreshPolicy != RefreshPolicy.NONE) { + expectedParams = Collections.singletonMap("refresh", refreshPolicy.getValue()); + } else { + expectedParams = Collections.emptyMap(); + } + final CreateApiKeyRequest createApiKeyRequest = new CreateApiKeyRequest(name, roles, expiration, refreshPolicy); + final Request request = SecurityRequestConverters.createApiKey(createApiKeyRequest); + assertEquals(HttpPost.METHOD_NAME, request.getMethod()); + assertEquals("/_security/api_key", request.getEndpoint()); + assertEquals(expectedParams, request.getParameters()); + assertToXContentBody(createApiKeyRequest, request.getEntity()); + } + + public void testGetApiKey() throws IOException { + String realmName = randomAlphaOfLength(5); + String userName = randomAlphaOfLength(7); + final GetApiKeyRequest getApiKeyRequest = GetApiKeyRequest.usingRealmAndUserName(realmName, userName); + final Request request = SecurityRequestConverters.getApiKey(getApiKeyRequest); + assertEquals(HttpGet.METHOD_NAME, request.getMethod()); + assertEquals("/_security/api_key", request.getEndpoint()); + Map mapOfParameters = new HashMap<>(); + mapOfParameters.put("realm_name", realmName); + mapOfParameters.put("username", userName); + assertThat(request.getParameters(), equalTo(mapOfParameters)); + } + + public void testInvalidateApiKey() throws IOException { + String realmName = randomAlphaOfLength(5); + String userName = randomAlphaOfLength(7); + final InvalidateApiKeyRequest invalidateApiKeyRequest = InvalidateApiKeyRequest.usingRealmAndUserName(realmName, userName); + final Request request = SecurityRequestConverters.invalidateApiKey(invalidateApiKeyRequest); + assertEquals(HttpDelete.METHOD_NAME, request.getMethod()); + assertEquals("/_security/api_key", request.getEndpoint()); + assertToXContentBody(invalidateApiKeyRequest, request.getEntity()); + } + } diff --git a/client/rest-high-level/src/test/java/org/elasticsearch/client/documentation/SecurityDocumentationIT.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/documentation/SecurityDocumentationIT.java index fe98b2540183f..0ee9621f64333 100644 --- a/client/rest-high-level/src/test/java/org/elasticsearch/client/documentation/SecurityDocumentationIT.java +++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/documentation/SecurityDocumentationIT.java @@ -33,6 +33,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; @@ -46,6 +48,8 @@ import org.elasticsearch.client.security.DisableUserRequest; import org.elasticsearch.client.security.EnableUserRequest; import org.elasticsearch.client.security.ExpressionRoleMapping; +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; @@ -58,6 +62,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.user.privileges.ApplicationResourcePrivileges; import org.elasticsearch.client.security.user.privileges.UserIndicesPrivileges; import org.elasticsearch.client.security.InvalidateTokenRequest; @@ -71,6 +77,7 @@ import org.elasticsearch.client.security.PutUserRequest; import org.elasticsearch.client.security.PutUserResponse; import org.elasticsearch.client.security.RefreshPolicy; +import org.elasticsearch.client.security.support.ApiKey; import org.elasticsearch.client.security.support.CertificateInfo; import org.elasticsearch.client.security.support.expressiondsl.RoleMapperExpression; import org.elasticsearch.client.security.support.expressiondsl.expressions.AnyRoleMapperExpression; @@ -79,12 +86,17 @@ import org.elasticsearch.client.security.user.privileges.ApplicationPrivilege; import org.elasticsearch.client.security.user.privileges.IndicesPrivileges; import org.elasticsearch.client.security.user.privileges.Role; +import org.elasticsearch.client.security.user.privileges.Role.ClusterPrivilegeName; +import org.elasticsearch.client.security.user.privileges.Role.IndexPrivilegeName; +import org.elasticsearch.client.security.user.privileges.UserIndicesPrivileges; +import org.elasticsearch.common.Strings; +import org.elasticsearch.common.settings.SecureString; +import org.elasticsearch.common.unit.TimeValue; import org.elasticsearch.common.util.set.Sets; import org.hamcrest.Matchers; -import javax.crypto.SecretKeyFactory; -import javax.crypto.spec.PBEKeySpec; import java.io.IOException; +import java.time.Instant; import java.util.ArrayList; import java.util.Arrays; import java.util.Base64; @@ -97,15 +109,20 @@ import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; +import javax.crypto.SecretKeyFactory; +import javax.crypto.spec.PBEKeySpec; + import static org.hamcrest.Matchers.contains; import static org.hamcrest.Matchers.containsInAnyOrder; import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.empty; import static org.hamcrest.Matchers.emptyIterable; import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.greaterThan; import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.isIn; import static org.hamcrest.Matchers.not; +import static org.hamcrest.Matchers.notNullValue; import static org.hamcrest.Matchers.nullValue; public class SecurityDocumentationIT extends ESRestHighLevelClientTestCase { @@ -336,7 +353,7 @@ public void onFailure(Exception e) { private void addUser(RestHighLevelClient client, String userName, String password) throws IOException { User user = new User(userName, Collections.singletonList(userName)); - PutUserRequest request = new PutUserRequest(user, password.toCharArray(), true, RefreshPolicy.NONE); + PutUserRequest request = PutUserRequest.withPassword(user, password.toCharArray(), true, RefreshPolicy.NONE); PutUserResponse response = client.security().putUser(request, RequestOptions.DEFAULT); assertTrue(response.isCreated()); } @@ -510,7 +527,7 @@ public void testEnableUser() throws Exception { RestHighLevelClient client = highLevelClient(); char[] password = new char[]{'p', 'a', 's', 's', 'w', 'o', 'r', 'd'}; User enable_user = new User("enable_user", Collections.singletonList("superuser")); - PutUserRequest putUserRequest = new PutUserRequest(enable_user, password, true, RefreshPolicy.IMMEDIATE); + PutUserRequest putUserRequest = PutUserRequest.withPassword(enable_user, password, true, RefreshPolicy.IMMEDIATE); PutUserResponse putUserResponse = client.security().putUser(putUserRequest, RequestOptions.DEFAULT); assertTrue(putUserResponse.isCreated()); @@ -555,7 +572,7 @@ public void testDisableUser() throws Exception { RestHighLevelClient client = highLevelClient(); char[] password = new char[]{'p', 'a', 's', 's', 'w', 'o', 'r', 'd'}; User disable_user = new User("disable_user", Collections.singletonList("superuser")); - PutUserRequest putUserRequest = new PutUserRequest(disable_user, password, true, RefreshPolicy.IMMEDIATE); + PutUserRequest putUserRequest = PutUserRequest.withPassword(disable_user, password, true, RefreshPolicy.IMMEDIATE); PutUserResponse putUserResponse = client.security().putUser(putUserRequest, RequestOptions.DEFAULT); assertTrue(putUserResponse.isCreated()); { @@ -1033,7 +1050,7 @@ public void testChangePassword() throws Exception { char[] password = new char[]{'p', 'a', 's', 's', 'w', 'o', 'r', 'd'}; char[] newPassword = new char[]{'n', 'e', 'w', 'p', 'a', 's', 's', 'w', 'o', 'r', 'd'}; User user = new User("change_password_user", Collections.singletonList("superuser"), Collections.emptyMap(), null, null); - PutUserRequest putUserRequest = new PutUserRequest(user, password, true, RefreshPolicy.NONE); + PutUserRequest putUserRequest = PutUserRequest.withPassword(user, password, true, RefreshPolicy.NONE); PutUserResponse putUserResponse = client.security().putUser(putUserRequest, RequestOptions.DEFAULT); assertTrue(putUserResponse.isCreated()); { @@ -1250,7 +1267,8 @@ public void testCreateToken() throws Exception { { // Setup user User token_user = new User("token_user", Collections.singletonList("kibana_user")); - PutUserRequest putUserRequest = new PutUserRequest(token_user, "password".toCharArray(), true, RefreshPolicy.IMMEDIATE); + PutUserRequest putUserRequest = PutUserRequest.withPassword(token_user, "password".toCharArray(), true, + RefreshPolicy.IMMEDIATE); PutUserResponse putUserResponse = client.security().putUser(putUserRequest, RequestOptions.DEFAULT); assertTrue(putUserResponse.isCreated()); } @@ -1328,27 +1346,27 @@ public void testInvalidateToken() throws Exception { // Setup users final char[] password = "password".toCharArray(); User user = new User("user", Collections.singletonList("kibana_user")); - PutUserRequest putUserRequest = new PutUserRequest(user, password, true, RefreshPolicy.IMMEDIATE); + PutUserRequest putUserRequest = PutUserRequest.withPassword(user, password, true, RefreshPolicy.IMMEDIATE); PutUserResponse putUserResponse = client.security().putUser(putUserRequest, RequestOptions.DEFAULT); assertTrue(putUserResponse.isCreated()); User this_user = new User("this_user", Collections.singletonList("kibana_user")); - PutUserRequest putThisUserRequest = new PutUserRequest(this_user, password, true, RefreshPolicy.IMMEDIATE); + PutUserRequest putThisUserRequest = PutUserRequest.withPassword(this_user, password, true, RefreshPolicy.IMMEDIATE); PutUserResponse putThisUserResponse = client.security().putUser(putThisUserRequest, RequestOptions.DEFAULT); assertTrue(putThisUserResponse.isCreated()); User that_user = new User("that_user", Collections.singletonList("kibana_user")); - PutUserRequest putThatUserRequest = new PutUserRequest(that_user, password, true, RefreshPolicy.IMMEDIATE); + PutUserRequest putThatUserRequest = PutUserRequest.withPassword(that_user, password, true, RefreshPolicy.IMMEDIATE); PutUserResponse putThatUserResponse = client.security().putUser(putThatUserRequest, RequestOptions.DEFAULT); assertTrue(putThatUserResponse.isCreated()); User other_user = new User("other_user", Collections.singletonList("kibana_user")); - PutUserRequest putOtherUserRequest = new PutUserRequest(other_user, password, true, RefreshPolicy.IMMEDIATE); + PutUserRequest putOtherUserRequest = PutUserRequest.withPassword(other_user, password, true, RefreshPolicy.IMMEDIATE); PutUserResponse putOtherUserResponse = client.security().putUser(putOtherUserRequest, RequestOptions.DEFAULT); assertTrue(putOtherUserResponse.isCreated()); User extra_user = new User("extra_user", Collections.singletonList("kibana_user")); - PutUserRequest putExtraUserRequest = new PutUserRequest(extra_user, password, true, RefreshPolicy.IMMEDIATE); + PutUserRequest putExtraUserRequest = PutUserRequest.withPassword(extra_user, password, true, RefreshPolicy.IMMEDIATE); PutUserResponse putExtraUserResponse = client.security().putUser(putExtraUserRequest, RequestOptions.DEFAULT); assertTrue(putExtraUserResponse.isCreated()); @@ -1748,4 +1766,363 @@ public void onFailure(Exception e) { } } + public void testCreateApiKey() throws Exception { + RestHighLevelClient client = highLevelClient(); + + List roles = Collections.singletonList(Role.builder().name("r1").clusterPrivileges(ClusterPrivilegeName.ALL) + .indicesPrivileges(IndicesPrivileges.builder().indices("ind-x").privileges(IndexPrivilegeName.ALL).build()).build()); + final TimeValue expiration = TimeValue.timeValueHours(24); + final RefreshPolicy refreshPolicy = randomFrom(RefreshPolicy.values()); + { + final String name = randomAlphaOfLength(5); + // tag::create-api-key-request + CreateApiKeyRequest createApiKeyRequest = new CreateApiKeyRequest(name, roles, expiration, refreshPolicy); + // end::create-api-key-request + + // tag::create-api-key-execute + CreateApiKeyResponse createApiKeyResponse = client.security().createApiKey(createApiKeyRequest, RequestOptions.DEFAULT); + // end::create-api-key-execute + + // tag::create-api-key-response + SecureString apiKey = createApiKeyResponse.getKey(); // <1> + Instant apiKeyExpiration = createApiKeyResponse.getExpiration(); // <2> + // end::create-api-key-response + assertThat(createApiKeyResponse.getName(), equalTo(name)); + assertNotNull(apiKey); + assertNotNull(apiKeyExpiration); + } + + { + final String name = randomAlphaOfLength(5); + CreateApiKeyRequest createApiKeyRequest = new CreateApiKeyRequest(name, roles, expiration, refreshPolicy); + + ActionListener listener; + // tag::create-api-key-execute-listener + listener = new ActionListener() { + @Override + public void onResponse(CreateApiKeyResponse createApiKeyResponse) { + // <1> + } + + @Override + public void onFailure(Exception e) { + // <2> + } + }; + // end::create-api-key-execute-listener + + // Avoid unused variable warning + assertNotNull(listener); + + // Replace the empty listener by a blocking listener in test + final PlainActionFuture future = new PlainActionFuture<>(); + listener = future; + + // tag::create-api-key-execute-async + client.security().createApiKeyAsync(createApiKeyRequest, RequestOptions.DEFAULT, listener); // <1> + // end::create-api-key-execute-async + + assertNotNull(future.get(30, TimeUnit.SECONDS)); + assertThat(future.get().getName(), equalTo(name)); + assertNotNull(future.get().getKey()); + assertNotNull(future.get().getExpiration()); + } + } + + public void testGetApiKey() throws Exception { + RestHighLevelClient client = highLevelClient(); + + List roles = Collections.singletonList(Role.builder().name("r1").clusterPrivileges(ClusterPrivilegeName.ALL) + .indicesPrivileges(IndicesPrivileges.builder().indices("ind-x").privileges(IndexPrivilegeName.ALL).build()).build()); + final TimeValue expiration = TimeValue.timeValueHours(24); + final RefreshPolicy refreshPolicy = randomFrom(RefreshPolicy.values()); + // Create API Keys + CreateApiKeyRequest createApiKeyRequest = new CreateApiKeyRequest("k1", roles, expiration, refreshPolicy); + CreateApiKeyResponse createApiKeyResponse1 = client.security().createApiKey(createApiKeyRequest, RequestOptions.DEFAULT); + assertThat(createApiKeyResponse1.getName(), equalTo("k1")); + assertNotNull(createApiKeyResponse1.getKey()); + + final ApiKey expectedApiKeyInfo = new ApiKey(createApiKeyResponse1.getName(), createApiKeyResponse1.getId(), Instant.now(), + Instant.now().plusMillis(expiration.getMillis()), false, "test_user", "default_file"); + { + // tag::get-api-key-id-request + GetApiKeyRequest getApiKeyRequest = GetApiKeyRequest.usingApiKeyId(createApiKeyResponse1.getId()); + // end::get-api-key-id-request + + // tag::get-api-key-execute + GetApiKeyResponse getApiKeyResponse = client.security().getApiKey(getApiKeyRequest, RequestOptions.DEFAULT); + // end::get-api-key-execute + + assertThat(getApiKeyResponse.getApiKeyInfos(), is(notNullValue())); + assertThat(getApiKeyResponse.getApiKeyInfos().size(), is(1)); + verifyApiKey(getApiKeyResponse.getApiKeyInfos().get(0), expectedApiKeyInfo); + } + + { + // tag::get-api-key-name-request + GetApiKeyRequest getApiKeyRequest = GetApiKeyRequest.usingApiKeyName(createApiKeyResponse1.getName()); + // end::get-api-key-name-request + + GetApiKeyResponse getApiKeyResponse = client.security().getApiKey(getApiKeyRequest, RequestOptions.DEFAULT); + + assertThat(getApiKeyResponse.getApiKeyInfos(), is(notNullValue())); + assertThat(getApiKeyResponse.getApiKeyInfos().size(), is(1)); + verifyApiKey(getApiKeyResponse.getApiKeyInfos().get(0), expectedApiKeyInfo); + } + + { + // tag::get-realm-api-keys-request + GetApiKeyRequest getApiKeyRequest = GetApiKeyRequest.usingRealmName("default_file"); + // end::get-realm-api-keys-request + + GetApiKeyResponse getApiKeyResponse = client.security().getApiKey(getApiKeyRequest, RequestOptions.DEFAULT); + + assertThat(getApiKeyResponse.getApiKeyInfos(), is(notNullValue())); + assertThat(getApiKeyResponse.getApiKeyInfos().size(), is(1)); + verifyApiKey(getApiKeyResponse.getApiKeyInfos().get(0), expectedApiKeyInfo); + } + + { + // tag::get-user-api-keys-request + GetApiKeyRequest getApiKeyRequest = GetApiKeyRequest.usingUserName("test_user"); + // end::get-user-api-keys-request + + GetApiKeyResponse getApiKeyResponse = client.security().getApiKey(getApiKeyRequest, RequestOptions.DEFAULT); + + assertThat(getApiKeyResponse.getApiKeyInfos(), is(notNullValue())); + assertThat(getApiKeyResponse.getApiKeyInfos().size(), is(1)); + verifyApiKey(getApiKeyResponse.getApiKeyInfos().get(0), expectedApiKeyInfo); + } + + { + // tag::get-user-realm-api-keys-request + GetApiKeyRequest getApiKeyRequest = GetApiKeyRequest.usingRealmAndUserName("default_file", "test_user"); + // end::get-user-realm-api-keys-request + + // tag::get-api-key-response + GetApiKeyResponse getApiKeyResponse = client.security().getApiKey(getApiKeyRequest, RequestOptions.DEFAULT); + // end::get-api-key-response + + assertThat(getApiKeyResponse.getApiKeyInfos(), is(notNullValue())); + assertThat(getApiKeyResponse.getApiKeyInfos().size(), is(1)); + verifyApiKey(getApiKeyResponse.getApiKeyInfos().get(0), expectedApiKeyInfo); + } + + { + GetApiKeyRequest getApiKeyRequest = GetApiKeyRequest.usingApiKeyId(createApiKeyResponse1.getId()); + + ActionListener listener; + // tag::get-api-key-execute-listener + listener = new ActionListener() { + @Override + public void onResponse(GetApiKeyResponse getApiKeyResponse) { + // <1> + } + + @Override + public void onFailure(Exception e) { + // <2> + } + }; + // end::get-api-key-execute-listener + + // Avoid unused variable warning + assertNotNull(listener); + + // Replace the empty listener by a blocking listener in test + final PlainActionFuture future = new PlainActionFuture<>(); + listener = future; + + // tag::get-api-key-execute-async + client.security().getApiKeyAsync(getApiKeyRequest, RequestOptions.DEFAULT, listener); // <1> + // end::get-api-key-execute-async + + final GetApiKeyResponse response = future.get(30, TimeUnit.SECONDS); + assertNotNull(response); + + assertThat(response.getApiKeyInfos(), is(notNullValue())); + assertThat(response.getApiKeyInfos().size(), is(1)); + verifyApiKey(response.getApiKeyInfos().get(0), expectedApiKeyInfo); + } + } + + private void verifyApiKey(final ApiKey actual, final ApiKey expected) { + assertThat(actual.getId(), is(expected.getId())); + assertThat(actual.getName(), is(expected.getName())); + assertThat(actual.getUsername(), is(expected.getUsername())); + assertThat(actual.getRealm(), is(expected.getRealm())); + assertThat(actual.isInvalidated(), is(expected.isInvalidated())); + assertThat(actual.getExpiration(), is(greaterThan(Instant.now()))); + } + + public void testInvalidateApiKey() throws Exception { + RestHighLevelClient client = highLevelClient(); + + List roles = Collections.singletonList(Role.builder().name("r1").clusterPrivileges(ClusterPrivilegeName.ALL) + .indicesPrivileges(IndicesPrivileges.builder().indices("ind-x").privileges(IndexPrivilegeName.ALL).build()).build()); + final TimeValue expiration = TimeValue.timeValueHours(24); + final RefreshPolicy refreshPolicy = randomFrom(RefreshPolicy.values()); + // Create API Keys + CreateApiKeyRequest createApiKeyRequest = new CreateApiKeyRequest("k1", roles, expiration, refreshPolicy); + CreateApiKeyResponse createApiKeyResponse1 = client.security().createApiKey(createApiKeyRequest, RequestOptions.DEFAULT); + assertThat(createApiKeyResponse1.getName(), equalTo("k1")); + assertNotNull(createApiKeyResponse1.getKey()); + + { + // tag::invalidate-api-key-id-request + InvalidateApiKeyRequest invalidateApiKeyRequest = InvalidateApiKeyRequest.usingApiKeyId(createApiKeyResponse1.getId()); + // end::invalidate-api-key-id-request + + // tag::invalidate-api-key-execute + InvalidateApiKeyResponse invalidateApiKeyResponse = client.security().invalidateApiKey(invalidateApiKeyRequest, + RequestOptions.DEFAULT); + // end::invalidate-api-key-execute + + final List errors = invalidateApiKeyResponse.getErrors(); + final List invalidatedApiKeyIds = invalidateApiKeyResponse.getInvalidatedApiKeys(); + final List previouslyInvalidatedApiKeyIds = invalidateApiKeyResponse.getPreviouslyInvalidatedApiKeys(); + + assertTrue(errors.isEmpty()); + List expectedInvalidatedApiKeyIds = Arrays.asList(createApiKeyResponse1.getId()); + assertThat(invalidatedApiKeyIds, containsInAnyOrder(expectedInvalidatedApiKeyIds.toArray(Strings.EMPTY_ARRAY))); + assertThat(previouslyInvalidatedApiKeyIds.size(), equalTo(0)); + } + + { + createApiKeyRequest = new CreateApiKeyRequest("k2", roles, expiration, refreshPolicy); + CreateApiKeyResponse createApiKeyResponse2 = client.security().createApiKey(createApiKeyRequest, RequestOptions.DEFAULT); + assertThat(createApiKeyResponse2.getName(), equalTo("k2")); + assertNotNull(createApiKeyResponse2.getKey()); + + // tag::invalidate-api-key-name-request + InvalidateApiKeyRequest invalidateApiKeyRequest = InvalidateApiKeyRequest.usingApiKeyName(createApiKeyResponse2.getName()); + // end::invalidate-api-key-name-request + + InvalidateApiKeyResponse invalidateApiKeyResponse = client.security().invalidateApiKey(invalidateApiKeyRequest, + RequestOptions.DEFAULT); + + final List errors = invalidateApiKeyResponse.getErrors(); + final List invalidatedApiKeyIds = invalidateApiKeyResponse.getInvalidatedApiKeys(); + final List previouslyInvalidatedApiKeyIds = invalidateApiKeyResponse.getPreviouslyInvalidatedApiKeys(); + + assertTrue(errors.isEmpty()); + List expectedInvalidatedApiKeyIds = Arrays.asList(createApiKeyResponse2.getId()); + assertThat(invalidatedApiKeyIds, containsInAnyOrder(expectedInvalidatedApiKeyIds.toArray(Strings.EMPTY_ARRAY))); + assertThat(previouslyInvalidatedApiKeyIds.size(), equalTo(0)); + } + + { + createApiKeyRequest = new CreateApiKeyRequest("k3", roles, expiration, refreshPolicy); + CreateApiKeyResponse createApiKeyResponse3 = client.security().createApiKey(createApiKeyRequest, RequestOptions.DEFAULT); + assertThat(createApiKeyResponse3.getName(), equalTo("k3")); + assertNotNull(createApiKeyResponse3.getKey()); + + // tag::invalidate-realm-api-keys-request + InvalidateApiKeyRequest invalidateApiKeyRequest = InvalidateApiKeyRequest.usingRealmName("default_file"); + // end::invalidate-realm-api-keys-request + + InvalidateApiKeyResponse invalidateApiKeyResponse = client.security().invalidateApiKey(invalidateApiKeyRequest, + RequestOptions.DEFAULT); + + final List errors = invalidateApiKeyResponse.getErrors(); + final List invalidatedApiKeyIds = invalidateApiKeyResponse.getInvalidatedApiKeys(); + final List previouslyInvalidatedApiKeyIds = invalidateApiKeyResponse.getPreviouslyInvalidatedApiKeys(); + + assertTrue(errors.isEmpty()); + List expectedInvalidatedApiKeyIds = Arrays.asList(createApiKeyResponse3.getId()); + assertThat(invalidatedApiKeyIds, containsInAnyOrder(expectedInvalidatedApiKeyIds.toArray(Strings.EMPTY_ARRAY))); + assertThat(previouslyInvalidatedApiKeyIds.size(), equalTo(0)); + } + + { + createApiKeyRequest = new CreateApiKeyRequest("k4", roles, expiration, refreshPolicy); + CreateApiKeyResponse createApiKeyResponse4 = client.security().createApiKey(createApiKeyRequest, RequestOptions.DEFAULT); + assertThat(createApiKeyResponse4.getName(), equalTo("k4")); + assertNotNull(createApiKeyResponse4.getKey()); + + // tag::invalidate-user-api-keys-request + InvalidateApiKeyRequest invalidateApiKeyRequest = InvalidateApiKeyRequest.usingUserName("test_user"); + // end::invalidate-user-api-keys-request + + InvalidateApiKeyResponse invalidateApiKeyResponse = client.security().invalidateApiKey(invalidateApiKeyRequest, + RequestOptions.DEFAULT); + + final List errors = invalidateApiKeyResponse.getErrors(); + final List invalidatedApiKeyIds = invalidateApiKeyResponse.getInvalidatedApiKeys(); + final List previouslyInvalidatedApiKeyIds = invalidateApiKeyResponse.getPreviouslyInvalidatedApiKeys(); + + assertTrue(errors.isEmpty()); + List expectedInvalidatedApiKeyIds = Arrays.asList(createApiKeyResponse4.getId()); + assertThat(invalidatedApiKeyIds, containsInAnyOrder(expectedInvalidatedApiKeyIds.toArray(Strings.EMPTY_ARRAY))); + assertThat(previouslyInvalidatedApiKeyIds.size(), equalTo(0)); + } + + { + createApiKeyRequest = new CreateApiKeyRequest("k5", roles, expiration, refreshPolicy); + CreateApiKeyResponse createApiKeyResponse5 = client.security().createApiKey(createApiKeyRequest, RequestOptions.DEFAULT); + assertThat(createApiKeyResponse5.getName(), equalTo("k5")); + assertNotNull(createApiKeyResponse5.getKey()); + + // tag::invalidate-user-realm-api-keys-request + InvalidateApiKeyRequest invalidateApiKeyRequest = InvalidateApiKeyRequest.usingRealmAndUserName("default_file", "test_user"); + // end::invalidate-user-realm-api-keys-request + + // tag::invalidate-api-key-response + InvalidateApiKeyResponse invalidateApiKeyResponse = client.security().invalidateApiKey(invalidateApiKeyRequest, + RequestOptions.DEFAULT); + // end::invalidate-api-key-response + + final List errors = invalidateApiKeyResponse.getErrors(); + final List invalidatedApiKeyIds = invalidateApiKeyResponse.getInvalidatedApiKeys(); + final List previouslyInvalidatedApiKeyIds = invalidateApiKeyResponse.getPreviouslyInvalidatedApiKeys(); + + assertTrue(errors.isEmpty()); + List expectedInvalidatedApiKeyIds = Arrays.asList(createApiKeyResponse5.getId()); + assertThat(invalidatedApiKeyIds, containsInAnyOrder(expectedInvalidatedApiKeyIds.toArray(Strings.EMPTY_ARRAY))); + assertThat(previouslyInvalidatedApiKeyIds.size(), equalTo(0)); + } + + { + createApiKeyRequest = new CreateApiKeyRequest("k6", roles, expiration, refreshPolicy); + CreateApiKeyResponse createApiKeyResponse6 = client.security().createApiKey(createApiKeyRequest, RequestOptions.DEFAULT); + assertThat(createApiKeyResponse6.getName(), equalTo("k6")); + assertNotNull(createApiKeyResponse6.getKey()); + + InvalidateApiKeyRequest invalidateApiKeyRequest = InvalidateApiKeyRequest.usingApiKeyId(createApiKeyResponse6.getId()); + + ActionListener listener; + // tag::invalidate-api-key-execute-listener + listener = new ActionListener() { + @Override + public void onResponse(InvalidateApiKeyResponse invalidateApiKeyResponse) { + // <1> + } + + @Override + public void onFailure(Exception e) { + // <2> + } + }; + // end::invalidate-api-key-execute-listener + + // Avoid unused variable warning + assertNotNull(listener); + + // Replace the empty listener by a blocking listener in test + final PlainActionFuture future = new PlainActionFuture<>(); + listener = future; + + // tag::invalidate-api-key-execute-async + client.security().invalidateApiKeyAsync(invalidateApiKeyRequest, RequestOptions.DEFAULT, listener); // <1> + // end::invalidate-api-key-execute-async + + final InvalidateApiKeyResponse response = future.get(30, TimeUnit.SECONDS); + assertNotNull(response); + final List invalidatedApiKeyIds = response.getInvalidatedApiKeys(); + List expectedInvalidatedApiKeyIds = Arrays.asList(createApiKeyResponse6.getId()); + assertTrue(response.getErrors().isEmpty()); + assertThat(invalidatedApiKeyIds, containsInAnyOrder(expectedInvalidatedApiKeyIds.toArray(Strings.EMPTY_ARRAY))); + assertThat(response.getPreviouslyInvalidatedApiKeys().size(), equalTo(0)); + } + } } diff --git a/client/rest-high-level/src/test/java/org/elasticsearch/client/security/CreateApiKeyRequestTests.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/security/CreateApiKeyRequestTests.java new file mode 100644 index 0000000000000..188493deeb78a --- /dev/null +++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/security/CreateApiKeyRequestTests.java @@ -0,0 +1,105 @@ +/* + * 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.security.user.privileges.IndicesPrivileges; +import org.elasticsearch.client.security.user.privileges.Role; +import org.elasticsearch.client.security.user.privileges.Role.ClusterPrivilegeName; +import org.elasticsearch.client.security.user.privileges.Role.IndexPrivilegeName; +import org.elasticsearch.common.Strings; +import org.elasticsearch.common.unit.TimeValue; +import org.elasticsearch.common.xcontent.ToXContent; +import org.elasticsearch.common.xcontent.XContentBuilder; +import org.elasticsearch.common.xcontent.XContentFactory; +import org.elasticsearch.test.ESTestCase; +import org.elasticsearch.test.EqualsHashCodeTestUtils; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; + +import static org.hamcrest.Matchers.equalTo; + +public class CreateApiKeyRequestTests extends ESTestCase { + + public void test() throws IOException { + List roles = new ArrayList<>(); + roles.add(Role.builder().name("r1").clusterPrivileges(ClusterPrivilegeName.ALL) + .indicesPrivileges(IndicesPrivileges.builder().indices("ind-x").privileges(IndexPrivilegeName.ALL).build()).build()); + roles.add(Role.builder().name("r2").clusterPrivileges(ClusterPrivilegeName.ALL) + .indicesPrivileges(IndicesPrivileges.builder().indices("ind-y").privileges(IndexPrivilegeName.ALL).build()).build()); + + CreateApiKeyRequest createApiKeyRequest = new CreateApiKeyRequest("api-key", roles, null, null); + final XContentBuilder builder = XContentFactory.jsonBuilder(); + createApiKeyRequest.toXContent(builder, ToXContent.EMPTY_PARAMS); + final String output = Strings.toString(builder); + assertThat(output, equalTo( + "{\"name\":\"api-key\",\"role_descriptors\":{\"r1\":{\"applications\":[],\"cluster\":[\"all\"],\"indices\":[{\"names\":" + + "[\"ind-x\"],\"privileges\":[\"all\"],\"allow_restricted_indices\":false}],\"metadata\":{},\"run_as\":[]}," + + "\"r2\":{\"applications\":[],\"cluster\":" + + "[\"all\"],\"indices\":[{\"names\":[\"ind-y\"],\"privileges\":[\"all\"],\"allow_restricted_indices\":false}]," + + "\"metadata\":{},\"run_as\":[]}}}")); + } + + public void testEqualsHashCode() { + final String name = randomAlphaOfLength(5); + List roles = Collections.singletonList(Role.builder().name("r1").clusterPrivileges(ClusterPrivilegeName.ALL) + .indicesPrivileges(IndicesPrivileges.builder().indices("ind-x").privileges(IndexPrivilegeName.ALL).build()).build()); + final TimeValue expiration = null; + final RefreshPolicy refreshPolicy = randomFrom(RefreshPolicy.values()); + + CreateApiKeyRequest createApiKeyRequest = new CreateApiKeyRequest(name, roles, expiration, refreshPolicy); + + EqualsHashCodeTestUtils.checkEqualsAndHashCode(createApiKeyRequest, (original) -> { + return new CreateApiKeyRequest(original.getName(), original.getRoles(), original.getExpiration(), original.getRefreshPolicy()); + }); + EqualsHashCodeTestUtils.checkEqualsAndHashCode(createApiKeyRequest, (original) -> { + return new CreateApiKeyRequest(original.getName(), original.getRoles(), original.getExpiration(), original.getRefreshPolicy()); + }, CreateApiKeyRequestTests::mutateTestItem); + } + + private static CreateApiKeyRequest mutateTestItem(CreateApiKeyRequest original) { + switch (randomIntBetween(0, 3)) { + case 0: + return new CreateApiKeyRequest(randomAlphaOfLength(5), original.getRoles(), original.getExpiration(), + original.getRefreshPolicy()); + case 1: + return new CreateApiKeyRequest(original.getName(), + Collections.singletonList(Role.builder().name(randomAlphaOfLength(6)).clusterPrivileges(ClusterPrivilegeName.ALL) + .indicesPrivileges( + IndicesPrivileges.builder().indices(randomAlphaOfLength(4)).privileges(IndexPrivilegeName.ALL).build()) + .build()), + original.getExpiration(), original.getRefreshPolicy()); + case 2: + return new CreateApiKeyRequest(original.getName(), original.getRoles(), TimeValue.timeValueSeconds(10000), + original.getRefreshPolicy()); + case 3: + List values = Arrays.stream(RefreshPolicy.values()).filter(rp -> rp != original.getRefreshPolicy()) + .collect(Collectors.toList()); + return new CreateApiKeyRequest(original.getName(), original.getRoles(), original.getExpiration(), randomFrom(values)); + default: + return new CreateApiKeyRequest(randomAlphaOfLength(5), original.getRoles(), original.getExpiration(), + original.getRefreshPolicy()); + } + } +} diff --git a/client/rest-high-level/src/test/java/org/elasticsearch/client/security/CreateApiKeyResponseTests.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/security/CreateApiKeyResponseTests.java new file mode 100644 index 0000000000000..4481d70c80b37 --- /dev/null +++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/security/CreateApiKeyResponseTests.java @@ -0,0 +1,101 @@ +/* + * 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.common.CharArrays; +import org.elasticsearch.common.UUIDs; +import org.elasticsearch.common.bytes.BytesReference; +import org.elasticsearch.common.settings.SecureString; +import org.elasticsearch.common.xcontent.XContentBuilder; +import org.elasticsearch.common.xcontent.XContentFactory; +import org.elasticsearch.common.xcontent.XContentType; +import org.elasticsearch.test.ESTestCase; +import org.elasticsearch.test.EqualsHashCodeTestUtils; + +import java.io.IOException; +import java.time.Instant; +import java.util.Arrays; + +import static org.hamcrest.Matchers.equalTo; + +public class CreateApiKeyResponseTests extends ESTestCase { + + public void testFromXContent() throws IOException { + final String id = randomAlphaOfLengthBetween(4, 8); + final String name = randomAlphaOfLength(5); + final SecureString apiKey = UUIDs.randomBase64UUIDSecureString(); + final Instant expiration = randomBoolean() ? null : Instant.ofEpochMilli(10000); + + final XContentType xContentType = randomFrom(XContentType.values()); + final XContentBuilder builder = XContentFactory.contentBuilder(xContentType); + builder.startObject().field("id", id).field("name", name); + if (expiration != null) { + builder.field("expiration", expiration.toEpochMilli()); + } + byte[] charBytes = CharArrays.toUtf8Bytes(apiKey.getChars()); + try { + builder.field("api_key").utf8Value(charBytes, 0, charBytes.length); + } finally { + Arrays.fill(charBytes, (byte) 0); + } + builder.endObject(); + BytesReference xContent = BytesReference.bytes(builder); + + final CreateApiKeyResponse response = CreateApiKeyResponse.fromXContent(createParser(xContentType.xContent(), xContent)); + assertThat(response.getId(), equalTo(id)); + assertThat(response.getName(), equalTo(name)); + assertThat(response.getKey(), equalTo(apiKey)); + if (expiration != null) { + assertThat(response.getExpiration(), equalTo(expiration)); + } + } + + public void testEqualsHashCode() { + final String id = randomAlphaOfLengthBetween(4, 8); + final String name = randomAlphaOfLength(5); + final SecureString apiKey = UUIDs.randomBase64UUIDSecureString(); + final Instant expiration = Instant.ofEpochMilli(10000); + CreateApiKeyResponse createApiKeyResponse = new CreateApiKeyResponse(name, id, apiKey, expiration); + + EqualsHashCodeTestUtils.checkEqualsAndHashCode(createApiKeyResponse, (original) -> { + return new CreateApiKeyResponse(original.getName(), original.getId(), original.getKey(), original.getExpiration()); + }); + EqualsHashCodeTestUtils.checkEqualsAndHashCode(createApiKeyResponse, (original) -> { + return new CreateApiKeyResponse(original.getName(), original.getId(), original.getKey(), original.getExpiration()); + }, CreateApiKeyResponseTests::mutateTestItem); + } + + private static CreateApiKeyResponse mutateTestItem(CreateApiKeyResponse original) { + switch (randomIntBetween(0, 3)) { + case 0: + return new CreateApiKeyResponse(randomAlphaOfLength(7), original.getId(), original.getKey(), original.getExpiration()); + case 1: + return new CreateApiKeyResponse(original.getName(), randomAlphaOfLengthBetween(4, 8), original.getKey(), + original.getExpiration()); + case 2: + return new CreateApiKeyResponse(original.getName(), original.getId(), UUIDs.randomBase64UUIDSecureString(), + original.getExpiration()); + case 3: + return new CreateApiKeyResponse(original.getName(), original.getId(), original.getKey(), Instant.ofEpochMilli(150000)); + default: + return new CreateApiKeyResponse(randomAlphaOfLength(7), original.getId(), original.getKey(), original.getExpiration()); + } + } +} diff --git a/client/rest-high-level/src/test/java/org/elasticsearch/client/security/GetApiKeyRequestTests.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/security/GetApiKeyRequestTests.java new file mode 100644 index 0000000000000..79551e1e73e92 --- /dev/null +++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/security/GetApiKeyRequestTests.java @@ -0,0 +1,72 @@ +/* + * 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.ValidationException; +import org.elasticsearch.test.ESTestCase; + +import java.io.IOException; +import java.util.Optional; + +import static org.hamcrest.Matchers.equalTo; + +public class GetApiKeyRequestTests extends ESTestCase { + + public void testRequestValidation() { + GetApiKeyRequest request = GetApiKeyRequest.usingApiKeyId(randomAlphaOfLength(5)); + Optional ve = request.validate(); + assertFalse(ve.isPresent()); + request = GetApiKeyRequest.usingApiKeyName(randomAlphaOfLength(5)); + ve = request.validate(); + assertFalse(ve.isPresent()); + request = GetApiKeyRequest.usingRealmName(randomAlphaOfLength(5)); + ve = request.validate(); + assertFalse(ve.isPresent()); + request = GetApiKeyRequest.usingUserName(randomAlphaOfLength(5)); + ve = request.validate(); + assertFalse(ve.isPresent()); + request = GetApiKeyRequest.usingRealmAndUserName(randomAlphaOfLength(5), randomAlphaOfLength(7)); + ve = request.validate(); + assertFalse(ve.isPresent()); + } + + public void testRequestValidationFailureScenarios() throws IOException { + String[][] inputs = new String[][] { + { randomFrom(new String[] { null, "" }), randomFrom(new String[] { null, "" }), randomFrom(new String[] { null, "" }), + randomFrom(new String[] { null, "" }) }, + { randomFrom(new String[] { null, "" }), "user", "api-kid", "api-kname" }, + { "realm", randomFrom(new String[] { null, "" }), "api-kid", "api-kname" }, + { "realm", "user", "api-kid", randomFrom(new String[] { null, "" }) }, + { randomFrom(new String[] { null, "" }), randomFrom(new String[] { null, "" }), "api-kid", "api-kname" } }; + String[] expectedErrorMessages = new String[] { "One of [api key id, api key name, username, realm name] must be specified", + "username or realm name must not be specified when the api key id or api key name is specified", + "username or realm name must not be specified when the api key id or api key name is specified", + "username or realm name must not be specified when the api key id or api key name is specified", + "only one of [api key id, api key name] can be specified" }; + + for (int i = 0; i < inputs.length; i++) { + final int caseNo = i; + IllegalArgumentException ve = expectThrows(IllegalArgumentException.class, + () -> new GetApiKeyRequest(inputs[caseNo][0], inputs[caseNo][1], inputs[caseNo][2], inputs[caseNo][3])); + assertNotNull(ve); + assertThat(ve.getMessage(), equalTo(expectedErrorMessages[caseNo])); + } + } +} diff --git a/client/rest-high-level/src/test/java/org/elasticsearch/client/security/GetApiKeyResponseTests.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/security/GetApiKeyResponseTests.java new file mode 100644 index 0000000000000..7aa92e4f212a4 --- /dev/null +++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/security/GetApiKeyResponseTests.java @@ -0,0 +1,100 @@ +/* + * 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.security.support.ApiKey; +import org.elasticsearch.common.bytes.BytesReference; +import org.elasticsearch.common.xcontent.XContentBuilder; +import org.elasticsearch.common.xcontent.XContentFactory; +import org.elasticsearch.common.xcontent.XContentType; +import org.elasticsearch.test.ESTestCase; +import org.elasticsearch.test.EqualsHashCodeTestUtils; + +import java.io.IOException; +import java.time.Instant; +import java.util.Arrays; + +import static org.hamcrest.Matchers.equalTo; + +public class GetApiKeyResponseTests extends ESTestCase { + + public void testFromXContent() throws IOException { + ApiKey apiKeyInfo1 = createApiKeyInfo("name1", "id-1", Instant.ofEpochMilli(100000L), Instant.ofEpochMilli(10000000L), false, + "user-a", "realm-x"); + ApiKey apiKeyInfo2 = createApiKeyInfo("name2", "id-2", Instant.ofEpochMilli(100000L), Instant.ofEpochMilli(10000000L), true, + "user-b", "realm-y"); + GetApiKeyResponse response = new GetApiKeyResponse(Arrays.asList(apiKeyInfo1, apiKeyInfo2)); + final XContentType xContentType = randomFrom(XContentType.values()); + final XContentBuilder builder = XContentFactory.contentBuilder(xContentType); + toXContent(response, builder); + BytesReference xContent = BytesReference.bytes(builder); + GetApiKeyResponse responseParsed = GetApiKeyResponse.fromXContent(createParser(xContentType.xContent(), xContent)); + assertThat(responseParsed, equalTo(response)); + } + + private void toXContent(GetApiKeyResponse response, final XContentBuilder builder) throws IOException { + builder.startObject(); + builder.startArray("api_keys"); + for (ApiKey apiKey : response.getApiKeyInfos()) { + builder.startObject() + .field("id", apiKey.getId()) + .field("name", apiKey.getName()) + .field("creation", apiKey.getCreation().toEpochMilli()); + if (apiKey.getExpiration() != null) { + builder.field("expiration", apiKey.getExpiration().toEpochMilli()); + } + builder.field("invalidated", apiKey.isInvalidated()) + .field("username", apiKey.getUsername()) + .field("realm", apiKey.getRealm()); + builder.endObject(); + } + builder.endArray(); + builder.endObject(); + } + + public void testEqualsHashCode() { + ApiKey apiKeyInfo1 = createApiKeyInfo("name1", "id-1", Instant.ofEpochMilli(100000L), Instant.ofEpochMilli(10000000L), false, + "user-a", "realm-x"); + GetApiKeyResponse response = new GetApiKeyResponse(Arrays.asList(apiKeyInfo1)); + + EqualsHashCodeTestUtils.checkEqualsAndHashCode(response, (original) -> { + return new GetApiKeyResponse(original.getApiKeyInfos()); + }); + EqualsHashCodeTestUtils.checkEqualsAndHashCode(response, (original) -> { + return new GetApiKeyResponse(original.getApiKeyInfos()); + }, GetApiKeyResponseTests::mutateTestItem); + } + + private static GetApiKeyResponse mutateTestItem(GetApiKeyResponse original) { + ApiKey apiKeyInfo = createApiKeyInfo("name2", "id-2", Instant.ofEpochMilli(100000L), Instant.ofEpochMilli(10000000L), true, + "user-b", "realm-y"); + switch (randomIntBetween(0, 2)) { + case 0: + return new GetApiKeyResponse(Arrays.asList(apiKeyInfo)); + default: + return new GetApiKeyResponse(Arrays.asList(apiKeyInfo)); + } + } + + private static ApiKey createApiKeyInfo(String name, String id, Instant creation, Instant expiration, boolean invalidated, + String username, String realm) { + return new ApiKey(name, id, creation, expiration, invalidated, username, realm); + } +} diff --git a/client/rest-high-level/src/test/java/org/elasticsearch/client/security/InvalidateApiKeyRequestTests.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/security/InvalidateApiKeyRequestTests.java new file mode 100644 index 0000000000000..25ee4bb05bcc4 --- /dev/null +++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/security/InvalidateApiKeyRequestTests.java @@ -0,0 +1,73 @@ +/* + * 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.ValidationException; +import org.elasticsearch.test.ESTestCase; + +import java.io.IOException; +import java.util.Optional; + +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.is; + +public class InvalidateApiKeyRequestTests extends ESTestCase { + + public void testRequestValidation() { + InvalidateApiKeyRequest request = InvalidateApiKeyRequest.usingApiKeyId(randomAlphaOfLength(5)); + Optional ve = request.validate(); + assertThat(ve.isPresent(), is(false)); + request = InvalidateApiKeyRequest.usingApiKeyName(randomAlphaOfLength(5)); + ve = request.validate(); + assertThat(ve.isPresent(), is(false)); + request = InvalidateApiKeyRequest.usingRealmName(randomAlphaOfLength(5)); + ve = request.validate(); + assertThat(ve.isPresent(), is(false)); + request = InvalidateApiKeyRequest.usingUserName(randomAlphaOfLength(5)); + ve = request.validate(); + assertThat(ve.isPresent(), is(false)); + request = InvalidateApiKeyRequest.usingRealmAndUserName(randomAlphaOfLength(5), randomAlphaOfLength(7)); + ve = request.validate(); + assertThat(ve.isPresent(), is(false)); + } + + public void testRequestValidationFailureScenarios() throws IOException { + String[][] inputs = new String[][] { + { randomFrom(new String[] { null, "" }), randomFrom(new String[] { null, "" }), randomFrom(new String[] { null, "" }), + randomFrom(new String[] { null, "" }) }, + { randomFrom(new String[] { null, "" }), "user", "api-kid", "api-kname" }, + { "realm", randomFrom(new String[] { null, "" }), "api-kid", "api-kname" }, + { "realm", "user", "api-kid", randomFrom(new String[] { null, "" }) }, + { randomFrom(new String[] { null, "" }), randomFrom(new String[] { null, "" }), "api-kid", "api-kname" } }; + String[] expectedErrorMessages = new String[] { "One of [api key id, api key name, username, realm name] must be specified", + "username or realm name must not be specified when the api key id or api key name is specified", + "username or realm name must not be specified when the api key id or api key name is specified", + "username or realm name must not be specified when the api key id or api key name is specified", + "only one of [api key id, api key name] can be specified" }; + + for (int i = 0; i < inputs.length; i++) { + final int caseNo = i; + IllegalArgumentException ve = expectThrows(IllegalArgumentException.class, + () -> new InvalidateApiKeyRequest(inputs[caseNo][0], inputs[caseNo][1], inputs[caseNo][2], inputs[caseNo][3])); + assertNotNull(ve); + assertThat(ve.getMessage(), equalTo(expectedErrorMessages[caseNo])); + } + } +} diff --git a/client/rest-high-level/src/test/java/org/elasticsearch/client/security/InvalidateApiKeyResponseTests.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/security/InvalidateApiKeyResponseTests.java new file mode 100644 index 0000000000000..f5cd403536fc2 --- /dev/null +++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/security/InvalidateApiKeyResponseTests.java @@ -0,0 +1,111 @@ +/* + * 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.ElasticsearchException; +import org.elasticsearch.common.Strings; +import org.elasticsearch.common.bytes.BytesReference; +import org.elasticsearch.common.xcontent.ToXContent; +import org.elasticsearch.common.xcontent.XContentBuilder; +import org.elasticsearch.common.xcontent.XContentFactory; +import org.elasticsearch.common.xcontent.XContentType; +import org.elasticsearch.test.ESTestCase; +import org.elasticsearch.test.EqualsHashCodeTestUtils; + +import java.io.IOException; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import static org.hamcrest.Matchers.containsInAnyOrder; +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.notNullValue; + +public class InvalidateApiKeyResponseTests extends ESTestCase { + + public void testFromXContent() throws IOException { + List invalidatedApiKeys = Arrays.asList(randomArray(2, 5, String[]::new, () -> randomAlphaOfLength(5))); + List previouslyInvalidatedApiKeys = Arrays.asList(randomArray(2, 3, String[]::new, () -> randomAlphaOfLength(5))); + List errors = Arrays.asList(randomArray(2, 5, ElasticsearchException[]::new, + () -> new ElasticsearchException(randomAlphaOfLength(5), new IllegalArgumentException(randomAlphaOfLength(4))))); + + final XContentType xContentType = randomFrom(XContentType.values()); + final XContentBuilder builder = XContentFactory.contentBuilder(xContentType); + builder.startObject().array("invalidated_api_keys", invalidatedApiKeys.toArray(Strings.EMPTY_ARRAY)) + .array("previously_invalidated_api_keys", previouslyInvalidatedApiKeys.toArray(Strings.EMPTY_ARRAY)) + .field("error_count", errors.size()); + if (errors.isEmpty() == false) { + builder.field("error_details"); + builder.startArray(); + for (ElasticsearchException e : errors) { + builder.startObject(); + ElasticsearchException.generateThrowableXContent(builder, ToXContent.EMPTY_PARAMS, e); + builder.endObject(); + } + builder.endArray(); + } + builder.endObject(); + BytesReference xContent = BytesReference.bytes(builder); + + final InvalidateApiKeyResponse response = InvalidateApiKeyResponse.fromXContent(createParser(xContentType.xContent(), xContent)); + assertThat(response.getInvalidatedApiKeys(), containsInAnyOrder(invalidatedApiKeys.toArray(Strings.EMPTY_ARRAY))); + assertThat(response.getPreviouslyInvalidatedApiKeys(), + containsInAnyOrder(previouslyInvalidatedApiKeys.toArray(Strings.EMPTY_ARRAY))); + assertThat(response.getErrors(), is(notNullValue())); + assertThat(response.getErrors().size(), is(errors.size())); + assertThat(response.getErrors().get(0).toString(), containsString("type=illegal_argument_exception")); + assertThat(response.getErrors().get(1).toString(), containsString("type=illegal_argument_exception")); + } + + public void testEqualsHashCode() { + List invalidatedApiKeys = Arrays.asList(randomArray(2, 5, String[]::new, () -> randomAlphaOfLength(5))); + List previouslyInvalidatedApiKeys = Arrays.asList(randomArray(2, 3, String[]::new, () -> randomAlphaOfLength(5))); + List errors = Arrays.asList(randomArray(2, 5, ElasticsearchException[]::new, + () -> new ElasticsearchException(randomAlphaOfLength(5), new IllegalArgumentException(randomAlphaOfLength(4))))); + InvalidateApiKeyResponse invalidateApiKeyResponse = new InvalidateApiKeyResponse(invalidatedApiKeys, previouslyInvalidatedApiKeys, + errors); + + EqualsHashCodeTestUtils.checkEqualsAndHashCode(invalidateApiKeyResponse, (original) -> { + return new InvalidateApiKeyResponse(original.getInvalidatedApiKeys(), original.getPreviouslyInvalidatedApiKeys(), + original.getErrors()); + }); + EqualsHashCodeTestUtils.checkEqualsAndHashCode(invalidateApiKeyResponse, (original) -> { + return new InvalidateApiKeyResponse(original.getInvalidatedApiKeys(), original.getPreviouslyInvalidatedApiKeys(), + original.getErrors()); + }, InvalidateApiKeyResponseTests::mutateTestItem); + } + + private static InvalidateApiKeyResponse mutateTestItem(InvalidateApiKeyResponse original) { + switch (randomIntBetween(0, 2)) { + case 0: + return new InvalidateApiKeyResponse(Arrays.asList(randomArray(2, 5, String[]::new, () -> randomAlphaOfLength(5))), + original.getPreviouslyInvalidatedApiKeys(), original.getErrors()); + case 1: + return new InvalidateApiKeyResponse(original.getInvalidatedApiKeys(), Collections.emptyList(), original.getErrors()); + case 2: + return new InvalidateApiKeyResponse(original.getInvalidatedApiKeys(), original.getPreviouslyInvalidatedApiKeys(), + Collections.emptyList()); + default: + return new InvalidateApiKeyResponse(Arrays.asList(randomArray(2, 5, String[]::new, () -> randomAlphaOfLength(5))), + original.getPreviouslyInvalidatedApiKeys(), original.getErrors()); + } + } +} diff --git a/docs/java-rest/high-level/security/create-api-key.asciidoc b/docs/java-rest/high-level/security/create-api-key.asciidoc new file mode 100644 index 0000000000000..93c3fa16de1da --- /dev/null +++ b/docs/java-rest/high-level/security/create-api-key.asciidoc @@ -0,0 +1,40 @@ +-- +:api: create-api-key +:request: CreateApiKeyRequest +:response: CreateApiKeyResponse +-- + +[id="{upid}-{api}"] +=== Create API Key API + +API Key can be created using this API. + +[id="{upid}-{api}-request"] +==== Create API Key Request + +A +{request}+ contains name for the API key, +list of role descriptors to define permissions and +optional expiration for the generated API key. +If expiration is not provided then by default the API +keys do not expire. + +["source","java",subs="attributes,callouts,macros"] +-------------------------------------------------- +include-tagged::{doc-tests-file}[{api}-request] +-------------------------------------------------- + +include::../execution.asciidoc[] + +[id="{upid}-{api}-response"] +==== Create API Key Response + +The returned +{response}+ contains an id, +API key, name for the API key and optional +expiration. + +["source","java",subs="attributes,callouts,macros"] +-------------------------------------------------- +include-tagged::{doc-tests-file}[{api}-response] +-------------------------------------------------- +<1> the API key that can be used to authenticate to Elasticsearch. +<2> expiration if the API keys expire \ No newline at end of file diff --git a/docs/java-rest/high-level/security/get-api-key.asciidoc b/docs/java-rest/high-level/security/get-api-key.asciidoc new file mode 100644 index 0000000000000..bb98b527d22ba --- /dev/null +++ b/docs/java-rest/high-level/security/get-api-key.asciidoc @@ -0,0 +1,67 @@ +-- +:api: get-api-key +:request: GetApiKeyRequest +:response: GetApiKeyResponse +-- + +[id="{upid}-{api}"] +=== Get API Key information API + +API Key(s) information can be retrieved using this API. + +[id="{upid}-{api}-request"] +==== Get API Key Request +The +{request}+ supports retrieving API key information for + +. A specific API key + +. All API keys for a specific realm + +. All API keys for a specific user + +. All API keys for a specific user in a specific realm + +===== Retrieve a specific API key by its id +["source","java",subs="attributes,callouts,macros"] +-------------------------------------------------- +include-tagged::{doc-tests-file}[get-api-key-id-request] +-------------------------------------------------- + +===== Retrieve a specific API key by its name +["source","java",subs="attributes,callouts,macros"] +-------------------------------------------------- +include-tagged::{doc-tests-file}[get-api-key-name-request] +-------------------------------------------------- + +===== Retrieve all API keys for given realm +["source","java",subs="attributes,callouts,macros"] +-------------------------------------------------- +include-tagged::{doc-tests-file}[get-realm-api-keys-request] +-------------------------------------------------- + +===== Retrieve all API keys for a given user +["source","java",subs="attributes,callouts,macros"] +-------------------------------------------------- +include-tagged::{doc-tests-file}[get-user-api-keys-request] +-------------------------------------------------- + +===== Retrieve all API keys for given user in a realm +["source","java",subs="attributes,callouts,macros"] +-------------------------------------------------- +include-tagged::{doc-tests-file}[get-user-realm-api-keys-request] +-------------------------------------------------- + +include::../execution.asciidoc[] + +[id="{upid}-{api}-response"] +==== Get API Key information API Response + +The returned +{response}+ contains the information regarding the API keys that were +requested. + +`api_keys`:: Available using `getApiKeyInfos`, contains list of API keys that were retrieved for this request. + +["source","java",subs="attributes,callouts,macros"] +-------------------------------------------------- +include-tagged::{doc-tests-file}[{api}-response] +-------------------------------------------------- diff --git a/docs/java-rest/high-level/security/invalidate-api-key.asciidoc b/docs/java-rest/high-level/security/invalidate-api-key.asciidoc new file mode 100644 index 0000000000000..7f9c43b3165a8 --- /dev/null +++ b/docs/java-rest/high-level/security/invalidate-api-key.asciidoc @@ -0,0 +1,75 @@ +-- +:api: invalidate-api-key +:request: InvalidateApiKeyRequest +:response: InvalidateApiKeyResponse +-- + +[id="{upid}-{api}"] +=== Invalidate API Key API + +API Key(s) can be invalidated using this API. + +[id="{upid}-{api}-request"] +==== Invalidate API Key Request +The +{request}+ supports invalidating + +. A specific API key + +. All API keys for a specific realm + +. All API keys for a specific user + +. All API keys for a specific user in a specific realm + +===== Specific API key by API key id +["source","java",subs="attributes,callouts,macros"] +-------------------------------------------------- +include-tagged::{doc-tests-file}[invalidate-api-key-id-request] +-------------------------------------------------- + +===== Specific API key by API key name +["source","java",subs="attributes,callouts,macros"] +-------------------------------------------------- +include-tagged::{doc-tests-file}[invalidate-api-key-name-request] +-------------------------------------------------- + +===== All API keys for realm +["source","java",subs="attributes,callouts,macros"] +-------------------------------------------------- +include-tagged::{doc-tests-file}[invalidate-realm-api-keys-request] +-------------------------------------------------- + +===== All API keys for user +["source","java",subs="attributes,callouts,macros"] +-------------------------------------------------- +include-tagged::{doc-tests-file}[invalidate-user-api-keys-request] +-------------------------------------------------- + +===== All API key for user in realm +["source","java",subs="attributes,callouts,macros"] +-------------------------------------------------- +include-tagged::{doc-tests-file}[invalidate-user-realm-api-keys-request] +-------------------------------------------------- + +include::../execution.asciidoc[] + +[id="{upid}-{api}-response"] +==== Invalidate API Key Response + +The returned +{response}+ contains the information regarding the API keys that the request +invalidated. + +`invalidatedApiKeys`:: Available using `getInvalidatedApiKeys` lists the API keys + that this request invalidated. + +`previouslyInvalidatedApiKeys`:: Available using `getPreviouslyInvalidatedApiKeys` lists the API keys + that this request attempted to invalidate + but were already invalid. + +`errors`:: Available using `getErrors` contains possible errors that were encountered while + attempting to invalidate API keys. + +["source","java",subs="attributes,callouts,macros"] +-------------------------------------------------- +include-tagged::{doc-tests-file}[{api}-response] +-------------------------------------------------- \ No newline at end of file diff --git a/docs/java-rest/high-level/supported-apis.asciidoc b/docs/java-rest/high-level/supported-apis.asciidoc index b17b667155862..125d56c00f1ed 100644 --- a/docs/java-rest/high-level/supported-apis.asciidoc +++ b/docs/java-rest/high-level/supported-apis.asciidoc @@ -410,6 +410,9 @@ The Java High Level REST Client supports the following Security APIs: * <<{upid}-get-privileges>> * <<{upid}-put-privileges>> * <<{upid}-delete-privileges>> +* <<{upid}-create-api-key>> +* <<{upid}-get-api-key>> +* <<{upid}-invalidate-api-key>> include::security/put-user.asciidoc[] include::security/get-users.asciidoc[] @@ -434,6 +437,9 @@ include::security/delete-role-mapping.asciidoc[] include::security/create-token.asciidoc[] include::security/invalidate-token.asciidoc[] include::security/put-privileges.asciidoc[] +include::security/create-api-key.asciidoc[] +include::security/get-api-key.asciidoc[] +include::security/invalidate-api-key.asciidoc[] == Watcher APIs diff --git a/rest-api-spec/src/main/resources/rest-api-spec/test/README.asciidoc b/rest-api-spec/src/main/resources/rest-api-spec/test/README.asciidoc index a9b6639359e24..c83edb69b3e62 100644 --- a/rest-api-spec/src/main/resources/rest-api-spec/test/README.asciidoc +++ b/rest-api-spec/src/main/resources/rest-api-spec/test/README.asciidoc @@ -280,6 +280,31 @@ example above), but the same goes for actual values: The stash should be reset at the beginning of each test file. +=== `transform_and_set` + +For some tests, it is necessary to extract a value and transform it from the previous `response`, in +order to reuse it in a subsequent `do` and other tests. +Currently, it only has support for `base64EncodeCredentials`, for unknown transformations it will not +do anything and stash the value as is. +For instance, when testing you may want to base64 encode username and password for +`Basic` authorization header: + +.... + - do: + index: + index: test + type: test + - transform_and_set: { login_creds: "#base64EncodeCredentials(user,password)" } # stash the base64 encoded credentials of `response.user` and `response.password` as `login_creds` + - do: + headers: + Authorization: Basic ${login_creds} # replace `$login_creds` with the stashed value + get: + index: test + type: test +.... + +Stashed values can be used as described in the `set` section + === `is_true` The specified key exists and has a true value (ie not `0`, `false`, `undefined`, `null` diff --git a/server/src/main/java/org/elasticsearch/common/RandomBasedUUIDGenerator.java b/server/src/main/java/org/elasticsearch/common/RandomBasedUUIDGenerator.java index 59e5960b99d09..b5b35b477efbd 100644 --- a/server/src/main/java/org/elasticsearch/common/RandomBasedUUIDGenerator.java +++ b/server/src/main/java/org/elasticsearch/common/RandomBasedUUIDGenerator.java @@ -20,6 +20,9 @@ package org.elasticsearch.common; +import org.elasticsearch.common.settings.SecureString; + +import java.util.Arrays; import java.util.Base64; import java.util.Random; @@ -34,12 +37,37 @@ public String getBase64UUID() { return getBase64UUID(SecureRandomHolder.INSTANCE); } + /** + * Returns a Base64 encoded {@link SecureString} of a Version 4.0 compatible UUID + * as defined here: http://www.ietf.org/rfc/rfc4122.txt + */ + public SecureString getBase64UUIDSecureString() { + byte[] uuidBytes = null; + byte[] encodedBytes = null; + try { + uuidBytes = getUUIDBytes(SecureRandomHolder.INSTANCE); + encodedBytes = Base64.getUrlEncoder().withoutPadding().encode(uuidBytes); + return new SecureString(CharArrays.utf8BytesToChars(encodedBytes)); + } finally { + if (uuidBytes != null) { + Arrays.fill(uuidBytes, (byte) 0); + } + if (encodedBytes != null) { + Arrays.fill(encodedBytes, (byte) 0); + } + } + } + /** * Returns a Base64 encoded version of a Version 4.0 compatible UUID * randomly initialized by the given {@link java.util.Random} instance * as defined here: http://www.ietf.org/rfc/rfc4122.txt */ public String getBase64UUID(Random random) { + return Base64.getUrlEncoder().withoutPadding().encodeToString(getUUIDBytes(random)); + } + + private byte[] getUUIDBytes(Random random) { final byte[] randomBytes = new byte[16]; random.nextBytes(randomBytes); /* Set the version to version 4 (see http://www.ietf.org/rfc/rfc4122.txt) @@ -48,12 +76,12 @@ public String getBase64UUID(Random random) { * stamp (bits 4 through 7 of the time_hi_and_version field).*/ randomBytes[6] &= 0x0f; /* clear the 4 most significant bits for the version */ randomBytes[6] |= 0x40; /* set the version to 0100 / 0x40 */ - - /* Set the variant: + + /* Set the variant: * The high field of th clock sequence multiplexed with the variant. * We set only the MSB of the variant*/ randomBytes[8] &= 0x3f; /* clear the 2 most significant bits */ randomBytes[8] |= 0x80; /* set the variant (MSB is set)*/ - return Base64.getUrlEncoder().withoutPadding().encodeToString(randomBytes); + return randomBytes; } } diff --git a/server/src/main/java/org/elasticsearch/common/UUIDs.java b/server/src/main/java/org/elasticsearch/common/UUIDs.java index 63fcaedde0f5c..a6a314c2cccb0 100644 --- a/server/src/main/java/org/elasticsearch/common/UUIDs.java +++ b/server/src/main/java/org/elasticsearch/common/UUIDs.java @@ -19,6 +19,8 @@ package org.elasticsearch.common; +import org.elasticsearch.common.settings.SecureString; + import java.util.Random; public class UUIDs { @@ -50,4 +52,9 @@ public static String randomBase64UUID() { return RANDOM_UUID_GENERATOR.getBase64UUID(); } + /** Returns a Base64 encoded {@link SecureString} of a Version 4.0 compatible UUID as defined here: http://www.ietf.org/rfc/rfc4122.txt, + * using a private {@code SecureRandom} instance */ + public static SecureString randomBase64UUIDSecureString() { + return RANDOM_UUID_GENERATOR.getBase64UUIDSecureString(); + } } diff --git a/server/src/main/java/org/elasticsearch/common/io/stream/StreamInput.java b/server/src/main/java/org/elasticsearch/common/io/stream/StreamInput.java index 5adcdd3813a48..16f20bc570ffa 100644 --- a/server/src/main/java/org/elasticsearch/common/io/stream/StreamInput.java +++ b/server/src/main/java/org/elasticsearch/common/io/stream/StreamInput.java @@ -579,6 +579,23 @@ public Object readGenericValue() throws IOException { } } + /** + * Read an {@link Instant} from the stream with nanosecond resolution + */ + public final Instant readInstant() throws IOException { + return Instant.ofEpochSecond(readLong(), readInt()); + } + + /** + * Read an optional {@link Instant} from the stream. Returns null when + * no instant is present. + */ + @Nullable + public final Instant readOptionalInstant() throws IOException { + final boolean present = readBoolean(); + return present ? readInstant() : null; + } + @SuppressWarnings("unchecked") private List readArrayList() throws IOException { int size = readArraySize(); diff --git a/server/src/main/java/org/elasticsearch/common/io/stream/StreamOutput.java b/server/src/main/java/org/elasticsearch/common/io/stream/StreamOutput.java index 651e0271f517b..da2f065d36038 100644 --- a/server/src/main/java/org/elasticsearch/common/io/stream/StreamOutput.java +++ b/server/src/main/java/org/elasticsearch/common/io/stream/StreamOutput.java @@ -55,6 +55,7 @@ import java.nio.file.FileSystemLoopException; import java.nio.file.NoSuchFileException; import java.nio.file.NotDirectoryException; +import java.time.Instant; import java.time.ZonedDateTime; import java.util.Collection; import java.util.Collections; @@ -560,6 +561,26 @@ public final void writeMap(final Map map, final Writer keyWriter } } + /** + * Writes an {@link Instant} to the stream with nanosecond resolution + */ + public final void writeInstant(Instant instant) throws IOException { + writeLong(instant.getEpochSecond()); + writeInt(instant.getNano()); + } + + /** + * Writes an {@link Instant} to the stream, which could possibly be null + */ + public final void writeOptionalInstant(@Nullable Instant instant) throws IOException { + if (instant == null) { + writeBoolean(false); + } else { + writeBoolean(true); + writeInstant(instant); + } + } + private static final Map, Writer> WRITERS; static { diff --git a/server/src/main/java/org/elasticsearch/common/util/set/Sets.java b/server/src/main/java/org/elasticsearch/common/util/set/Sets.java index 0f1fe22c02010..02d534552100c 100644 --- a/server/src/main/java/org/elasticsearch/common/util/set/Sets.java +++ b/server/src/main/java/org/elasticsearch/common/util/set/Sets.java @@ -144,4 +144,19 @@ public static Set union(Set left, Set right) { union.addAll(right); return union; } + + public static Set intersection(Set set1, Set set2) { + Objects.requireNonNull(set1); + Objects.requireNonNull(set2); + final Set left; + final Set right; + if (set1.size() < set2.size()) { + left = set1; + right = set2; + } else { + left = set2; + right = set1; + } + return left.stream().filter(o -> right.contains(o)).collect(Collectors.toSet()); + } } diff --git a/server/src/test/java/org/elasticsearch/common/io/stream/StreamTests.java b/server/src/test/java/org/elasticsearch/common/io/stream/StreamTests.java index e2cdaf3c7d5b8..837c0202faf92 100644 --- a/server/src/test/java/org/elasticsearch/common/io/stream/StreamTests.java +++ b/server/src/test/java/org/elasticsearch/common/io/stream/StreamTests.java @@ -30,6 +30,7 @@ import java.io.ByteArrayInputStream; import java.io.EOFException; import java.io.IOException; +import java.time.Instant; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; @@ -336,6 +337,37 @@ public void testSetOfLongs() throws IOException { assertThat(targetSet, equalTo(sourceSet)); } + public void testInstantSerialization() throws IOException { + final Instant instant = Instant.now(); + try (BytesStreamOutput out = new BytesStreamOutput()) { + out.writeInstant(instant); + try (StreamInput in = out.bytes().streamInput()) { + final Instant serialized = in.readInstant(); + assertEquals(instant, serialized); + } + } + } + + public void testOptionalInstantSerialization() throws IOException { + final Instant instant = Instant.now(); + try (BytesStreamOutput out = new BytesStreamOutput()) { + out.writeOptionalInstant(instant); + try (StreamInput in = out.bytes().streamInput()) { + final Instant serialized = in.readOptionalInstant(); + assertEquals(instant, serialized); + } + } + + final Instant missing = null; + try (BytesStreamOutput out = new BytesStreamOutput()) { + out.writeOptionalInstant(missing); + try (StreamInput in = out.bytes().streamInput()) { + final Instant serialized = in.readOptionalInstant(); + assertEquals(missing, serialized); + } + } + } + static final class WriteableString implements Writeable { final String string; diff --git a/server/src/test/java/org/elasticsearch/common/util/set/SetsTests.java b/server/src/test/java/org/elasticsearch/common/util/set/SetsTests.java index 0c1869a6b4086..f4337daf4346c 100644 --- a/server/src/test/java/org/elasticsearch/common/util/set/SetsTests.java +++ b/server/src/test/java/org/elasticsearch/common/util/set/SetsTests.java @@ -28,6 +28,7 @@ import java.util.stream.Collectors; import java.util.stream.IntStream; +import static org.hamcrest.Matchers.containsInAnyOrder; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.greaterThan; @@ -56,6 +57,17 @@ public void testSortedDifference() { } } + public void testIntersection() { + final int endExclusive = randomIntBetween(0, 256); + final Tuple, Set> sets = randomSets(endExclusive); + final Set intersection = Sets.intersection(sets.v1(), sets.v2()); + final Set expectedIntersection = IntStream.range(0, endExclusive) + .boxed() + .filter(i -> (sets.v1().contains(i) && sets.v2().contains(i))) + .collect(Collectors.toSet()); + assertThat(intersection, containsInAnyOrder(expectedIntersection.toArray(new Integer[0]))); + } + /** * Assert the difference between two sets is as expected. * diff --git a/test/framework/src/main/java/org/elasticsearch/test/rest/yaml/Features.java b/test/framework/src/main/java/org/elasticsearch/test/rest/yaml/Features.java index 7fabacc3c9e68..2833140eff95c 100644 --- a/test/framework/src/main/java/org/elasticsearch/test/rest/yaml/Features.java +++ b/test/framework/src/main/java/org/elasticsearch/test/rest/yaml/Features.java @@ -45,7 +45,9 @@ public final class Features { "stash_in_path", "stash_path_replace", "warnings", - "yaml")); + "yaml", + "transform_and_set" + )); private Features() { diff --git a/test/framework/src/main/java/org/elasticsearch/test/rest/yaml/section/ExecutableSection.java b/test/framework/src/main/java/org/elasticsearch/test/rest/yaml/section/ExecutableSection.java index ff02d6d16aa4a..135a60cca3431 100644 --- a/test/framework/src/main/java/org/elasticsearch/test/rest/yaml/section/ExecutableSection.java +++ b/test/framework/src/main/java/org/elasticsearch/test/rest/yaml/section/ExecutableSection.java @@ -40,6 +40,7 @@ public interface ExecutableSection { List DEFAULT_EXECUTABLE_CONTEXTS = unmodifiableList(Arrays.asList( new NamedXContentRegistry.Entry(ExecutableSection.class, new ParseField("do"), DoSection::parse), new NamedXContentRegistry.Entry(ExecutableSection.class, new ParseField("set"), SetSection::parse), + new NamedXContentRegistry.Entry(ExecutableSection.class, new ParseField("transform_and_set"), TransformAndSetSection::parse), new NamedXContentRegistry.Entry(ExecutableSection.class, new ParseField("match"), MatchAssertion::parse), new NamedXContentRegistry.Entry(ExecutableSection.class, new ParseField("is_true"), IsTrueAssertion::parse), new NamedXContentRegistry.Entry(ExecutableSection.class, new ParseField("is_false"), IsFalseAssertion::parse), diff --git a/test/framework/src/main/java/org/elasticsearch/test/rest/yaml/section/TransformAndSetSection.java b/test/framework/src/main/java/org/elasticsearch/test/rest/yaml/section/TransformAndSetSection.java new file mode 100644 index 0000000000000..7b0b915dd97df --- /dev/null +++ b/test/framework/src/main/java/org/elasticsearch/test/rest/yaml/section/TransformAndSetSection.java @@ -0,0 +1,106 @@ +/* + * 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.test.rest.yaml.section; + +import org.elasticsearch.common.ParsingException; +import org.elasticsearch.common.xcontent.XContentLocation; +import org.elasticsearch.common.xcontent.XContentParser; +import org.elasticsearch.test.rest.yaml.ClientYamlTestExecutionContext; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.Base64; +import java.util.HashMap; +import java.util.Map; + +/** + * Represents a transform_and_set section: + *

+ * + * In the following example,
+ * - transform_and_set: { login_creds: "#base64EncodeCredentials(user,password)" }
+ * user and password are from the response which are joined by ':' and Base64 encoded and then stashed as 'login_creds' + * + */ +public class TransformAndSetSection implements ExecutableSection { + public static TransformAndSetSection parse(XContentParser parser) throws IOException { + String currentFieldName = null; + XContentParser.Token token; + + TransformAndSetSection transformAndStashSection = new TransformAndSetSection(parser.getTokenLocation()); + + while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) { + if (token == XContentParser.Token.FIELD_NAME) { + currentFieldName = parser.currentName(); + } else if (token.isValue()) { + transformAndStashSection.addSet(currentFieldName, parser.text()); + } + } + + parser.nextToken(); + + if (transformAndStashSection.getStash().isEmpty()) { + throw new ParsingException(transformAndStashSection.location, "transform_and_set section must set at least a value"); + } + + return transformAndStashSection; + } + + private final Map transformStash = new HashMap<>(); + private final XContentLocation location; + + public TransformAndSetSection(XContentLocation location) { + this.location = location; + } + + public void addSet(String stashedField, String transformThis) { + transformStash.put(stashedField, transformThis); + } + + public Map getStash() { + return transformStash; + } + + @Override + public XContentLocation getLocation() { + return location; + } + + @Override + public void execute(ClientYamlTestExecutionContext executionContext) throws IOException { + for (Map.Entry entry : transformStash.entrySet()) { + String key = entry.getKey(); + String value = entry.getValue(); + if (value.startsWith("#base64EncodeCredentials(") && value.endsWith(")")) { + value = entry.getValue().substring("#base64EncodeCredentials(".length(), entry.getValue().lastIndexOf(")")); + String[] idAndPassword = value.split(","); + if (idAndPassword.length == 2) { + String credentials = executionContext.response(idAndPassword[0].trim()) + ":" + + executionContext.response(idAndPassword[1].trim()); + value = Base64.getEncoder().encodeToString(credentials.getBytes(StandardCharsets.UTF_8)); + } else { + throw new IllegalArgumentException("base64EncodeCredentials requires a username/id and a password parameters"); + } + } + executionContext.stash().stashValue(key, value); + } + } + +} diff --git a/test/framework/src/test/java/org/elasticsearch/test/rest/yaml/section/TransformAndSetSectionTests.java b/test/framework/src/test/java/org/elasticsearch/test/rest/yaml/section/TransformAndSetSectionTests.java new file mode 100644 index 0000000000000..a61f91de287e7 --- /dev/null +++ b/test/framework/src/test/java/org/elasticsearch/test/rest/yaml/section/TransformAndSetSectionTests.java @@ -0,0 +1,96 @@ +/* + * 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.test.rest.yaml.section; + +import org.elasticsearch.common.ParsingException; +import org.elasticsearch.common.xcontent.yaml.YamlXContent; +import org.elasticsearch.test.rest.yaml.ClientYamlTestExecutionContext; +import org.elasticsearch.test.rest.yaml.Stash; + +import java.nio.charset.StandardCharsets; +import java.util.Base64; + +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.notNullValue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.Mockito.when; + +public class TransformAndSetSectionTests extends AbstractClientYamlTestFragmentParserTestCase { + + public void testParseSingleValue() throws Exception { + parser = createParser(YamlXContent.yamlXContent, + "{ key: value }" + ); + + TransformAndSetSection transformAndSet = TransformAndSetSection.parse(parser); + assertThat(transformAndSet, notNullValue()); + assertThat(transformAndSet.getStash(), notNullValue()); + assertThat(transformAndSet.getStash().size(), equalTo(1)); + assertThat(transformAndSet.getStash().get("key"), equalTo("value")); + } + + public void testParseMultipleValues() throws Exception { + parser = createParser(YamlXContent.yamlXContent, + "{ key1: value1, key2: value2 }" + ); + + TransformAndSetSection transformAndSet = TransformAndSetSection.parse(parser); + assertThat(transformAndSet, notNullValue()); + assertThat(transformAndSet.getStash(), notNullValue()); + assertThat(transformAndSet.getStash().size(), equalTo(2)); + assertThat(transformAndSet.getStash().get("key1"), equalTo("value1")); + assertThat(transformAndSet.getStash().get("key2"), equalTo("value2")); + } + + public void testTransformation() throws Exception { + parser = createParser(YamlXContent.yamlXContent, "{ login_creds: \"#base64EncodeCredentials(id,api_key)\" }"); + + TransformAndSetSection transformAndSet = TransformAndSetSection.parse(parser); + assertThat(transformAndSet, notNullValue()); + assertThat(transformAndSet.getStash(), notNullValue()); + assertThat(transformAndSet.getStash().size(), equalTo(1)); + assertThat(transformAndSet.getStash().get("login_creds"), equalTo("#base64EncodeCredentials(id,api_key)")); + + ClientYamlTestExecutionContext executionContext = mock(ClientYamlTestExecutionContext.class); + when(executionContext.response("id")).thenReturn("user"); + when(executionContext.response("api_key")).thenReturn("password"); + Stash stash = new Stash(); + when(executionContext.stash()).thenReturn(stash); + transformAndSet.execute(executionContext); + verify(executionContext).response("id"); + verify(executionContext).response("api_key"); + verify(executionContext).stash(); + assertThat(stash.getValue("$login_creds"), + equalTo(Base64.getEncoder().encodeToString("user:password".getBytes(StandardCharsets.UTF_8)))); + verifyNoMoreInteractions(executionContext); + } + + public void testParseSetSectionNoValues() throws Exception { + parser = createParser(YamlXContent.yamlXContent, + "{ }" + ); + + Exception e = expectThrows(ParsingException.class, () -> TransformAndSetSection.parse(parser)); + assertThat(e.getMessage(), is("transform_and_set section must set at least a value")); + } +} diff --git a/x-pack/docs/build.gradle b/x-pack/docs/build.gradle index b5bff78045511..713c3c1a5815f 100644 --- a/x-pack/docs/build.gradle +++ b/x-pack/docs/build.gradle @@ -73,6 +73,7 @@ project.copyRestSpec.from(xpackResources) { } integTestCluster { setting 'xpack.security.enabled', 'true' + setting 'xpack.security.authc.api_key.enabled', 'true' setting 'xpack.security.authc.token.enabled', 'true' // Disable monitoring exporters for the docs tests setting 'xpack.monitoring.exporters._local.type', 'local' diff --git a/x-pack/docs/en/rest-api/security.asciidoc b/x-pack/docs/en/rest-api/security.asciidoc index 851bd2ba327b2..c59c44312ae60 100644 --- a/x-pack/docs/en/rest-api/security.asciidoc +++ b/x-pack/docs/en/rest-api/security.asciidoc @@ -51,6 +51,17 @@ without requiring basic authentication: * <> * <> +[float] +[[security-api-keys]] +=== API Keys + +You can use the following APIs to create, retrieve and invalidate API keys for access +without requiring basic authentication: + +* <> +* <> +* <> + [float] [[security-user-apis]] === Users @@ -88,3 +99,6 @@ include::security/get-users.asciidoc[] include::security/has-privileges.asciidoc[] include::security/invalidate-tokens.asciidoc[] include::security/ssl.asciidoc[] +include::security/create-api-keys.asciidoc[] +include::security/invalidate-api-keys.asciidoc[] +include::security/get-api-keys.asciidoc[] diff --git a/x-pack/docs/en/rest-api/security/create-api-keys.asciidoc b/x-pack/docs/en/rest-api/security/create-api-keys.asciidoc new file mode 100644 index 0000000000000..e4fa1be71d40e --- /dev/null +++ b/x-pack/docs/en/rest-api/security/create-api-keys.asciidoc @@ -0,0 +1,99 @@ +[role="xpack"] +[[security-api-create-api-key]] +=== Create API Key API + +Creates an API key for access without requiring basic authentication. + +==== Request + +`POST /_security/api_key` +`PUT /_security/api_key` + +==== Description + +The API keys are created by the {es} API key service, which is automatically enabled +when you configure TLS on the HTTP interface. See <>. Alternatively, +you can explicitly enable the `xpack.security.authc.api_key.enabled` setting. When +you are running in production mode, a bootstrap check prevents you from enabling +the API key service unless you also enable TLS on the HTTP interface. + +A successful create API key API call returns a JSON structure that contains +the unique id, the name to identify API key, the API key and the expiration if +applicable for the API key in milliseconds. + +NOTE: By default API keys never expire. You can specify expiration at the time of +creation for the API keys. + +==== Request Body + +The following parameters can be specified in the body of a POST or PUT request: + +`name`:: +(string) Specifies the name for this API key. + +`role_descriptors`:: +(array-of-role-descriptor) Optional array of role descriptor for this API key. The role descriptor +must be a subset of permissions of the authenticated user. The structure of role +descriptor is same as the request for create role API. For more details on role +see <>. +If the role descriptors are not provided then permissions of the authenticated user are applied. + +`expiration`:: +(string) Optional expiration time for the API key. By default API keys never expire. + +==== Examples + +The following example creates an API key: + +[source, js] +------------------------------------------------------------ +POST /_security/api_key +{ + "name": "my-api-key", + "expiration": "1d", <1> + "role_descriptors": { <2> + "role-a": { + "cluster": ["all"], + "index": [ + { + "names": ["index-a*"], + "privileges": ["read"] + } + ] + }, + "role-b": { + "cluster": ["all"], + "index": [ + { + "names": ["index-b*"], + "privileges": ["all"] + } + ] + } + } +} +------------------------------------------------------------ +// CONSOLE +<1> optional expiration for the API key being generated. If expiration is not + provided then the API keys do not expire. +<2> optional role descriptors for this API key, if not provided then permissions + of authenticated user are applied. + +A successful call returns a JSON structure that provides +API key information. + +[source,js] +-------------------------------------------------- +{ + "id":"VuaCfGcBCdbkQm-e5aOx", <1> + "name":"my-api-key", + "expiration":1544068612110, <2> + "api_key":"ui2lp2axTNmsyakw9tvNnw" <3> +} +-------------------------------------------------- +// TESTRESPONSE[s/VuaCfGcBCdbkQm-e5aOx/$body.id/] +// TESTRESPONSE[s/1544068612110/$body.expiration/] +// TESTRESPONSE[s/ui2lp2axTNmsyakw9tvNnw/$body.api_key/] +<1> unique id for this API key +<2> optional expiration in milliseconds for this API key +<3> generated API key diff --git a/x-pack/docs/en/rest-api/security/get-api-keys.asciidoc b/x-pack/docs/en/rest-api/security/get-api-keys.asciidoc new file mode 100644 index 0000000000000..ab2ef770cb124 --- /dev/null +++ b/x-pack/docs/en/rest-api/security/get-api-keys.asciidoc @@ -0,0 +1,118 @@ +[role="xpack"] +[[security-api-get-api-key]] +=== Get API Key information API +++++ +Get API key information +++++ + +Retrieves information for one or more API keys. + +==== Request + +`GET /_security/api_key` + +==== Description + +The information for the API keys created by <> can be retrieved +using this API. + +==== Request Body + +The following parameters can be specified in the query parameters of a GET request and +pertain to retrieving api keys: + +`id` (optional):: +(string) An API key id. This parameter cannot be used with any of `name`, `realm_name` or + `username` are used. + +`name` (optional):: +(string) An API key name. This parameter cannot be used with any of `id`, `realm_name` or + `username` are used. + +`realm_name` (optional):: +(string) The name of an authentication realm. This parameter cannot be used with either `id` or `name`. + +`username` (optional):: +(string) The username of a user. This parameter cannot be used with either `id` or `name`. + +NOTE: While all parameters are optional, at least one of them is required. + +==== Examples + +The following example to retrieve the API key identified by specified `id`: + +[source,js] +-------------------------------------------------- +GET /_security/api_key?id=dGhpcyBpcyBub3QgYSByZWFsIHRva2VuIGJ1dCBpdCBpcyBvbmx5IHRlc3QgZGF0YS4gZG8gbm90IHRyeSB0byByZWFkIHRva2VuIQ== +-------------------------------------------------- +// NOTCONSOLE + +whereas the following example to retrieve the API key identified by specified `name`: + +[source,js] +-------------------------------------------------- +GET /_security/api_key?name=hadoop_myuser_key +-------------------------------------------------- +// NOTCONSOLE + +The following example retrieves all API keys for the `native1` realm: + +[source,js] +-------------------------------------------------- +GET /_xpack/api_key?realm_name=native1 +-------------------------------------------------- +// NOTCONSOLE + +The following example retrieves all API keys for the user `myuser` in all realms: + +[source,js] +-------------------------------------------------- +GET /_xpack/api_key?username=myuser +-------------------------------------------------- +// NOTCONSOLE + +Finally, the following example retrieves all API keys for the user `myuser` in + the `native1` realm immediately: + +[source,js] +-------------------------------------------------- +GET /_xpack/api_key?username=myuser&realm_name=native1 +-------------------------------------------------- +// NOTCONSOLE + +A successful call returns a JSON structure that contains the information of one or more API keys that were retrieved. + +[source,js] +-------------------------------------------------- +{ + "api_keys": [ <1> + { + "id": "dGhpcyBpcyBub3QgYSByZWFsIHRva2VuIGJ1dCBpdCBpcyBvbmx5IHRlc3QgZGF0YS4gZG8gbm90IHRyeSB0byByZWFkIHRva2VuIQ==", <2> + "name": "hadoop_myuser_key", <3> + "creation": 1548550550158, <4> + "expiration": 1548551550158, <5> + "invalidated": false, <6> + "username": "myuser", <7> + "realm": "native1" <8> + }, + { + "id": "api-key-id-2", + "name": "api-key-name-2", + "creation": 1548550550158, + "invalidated": false, + "username": "user-y", + "realm": "realm-2" + } + ] +} +-------------------------------------------------- +// NOTCONSOLE + +<1> The list of API keys that were retrieved for this request. +<2> Id for the API key +<3> Name of the API key +<4> Creation time for the API key in milliseconds +<5> optional expiration time for the API key in milliseconds +<6> invalidation status for the API key, `true` if the key has been invalidated else `false` +<7> principal for which this API key was created +<8> realm name of the principal for which this API key was created diff --git a/x-pack/docs/en/rest-api/security/invalidate-api-keys.asciidoc b/x-pack/docs/en/rest-api/security/invalidate-api-keys.asciidoc new file mode 100644 index 0000000000000..4809e267ebd80 --- /dev/null +++ b/x-pack/docs/en/rest-api/security/invalidate-api-keys.asciidoc @@ -0,0 +1,140 @@ +[role="xpack"] +[[security-api-invalidate-api-key]] +=== Invalidate API Key API +++++ +Invalidate API key +++++ + +Invalidates one or more API keys. + +==== Request + +`DELETE /_security/api_key` + +==== Description + +The API keys created by <> can be invalidated +using this API. + +==== Request Body + +The following parameters can be specified in the body of a DELETE request and +pertain to invalidating api keys: + +`id` (optional):: +(string) An API key id. This parameter cannot be used with any of `name`, `realm_name` or + `username` are used. + +`name` (optional):: +(string) An API key name. This parameter cannot be used with any of `id`, `realm_name` or + `username` are used. + +`realm_name` (optional):: +(string) The name of an authentication realm. This parameter cannot be used with either `api_key_id` or `api_key_name`. + +`username` (optional):: +(string) The username of a user. This parameter cannot be used with either `api_key_id` or `api_key_name`. + +NOTE: While all parameters are optional, at least one of them is required. + +==== Examples + +The following example invalidates the API key identified by specified `id` immediately: + +[source,js] +-------------------------------------------------- +DELETE /_security/api_key +{ + "id" : "dGhpcyBpcyBub3QgYSByZWFsIHRva2VuIGJ1dCBpdCBpcyBvbmx5IHRlc3QgZGF0YS4gZG8gbm90IHRyeSB0byByZWFkIHRva2VuIQ==" +} +-------------------------------------------------- +// NOTCONSOLE + +whereas the following example invalidates the API key identified by specified `name` immediately: + +[source,js] +-------------------------------------------------- +DELETE /_security/api_key +{ + "name" : "hadoop_myuser_key" +} +-------------------------------------------------- +// NOTCONSOLE + +The following example invalidates all API keys for the `native1` realm immediately: + +[source,js] +-------------------------------------------------- +DELETE /_xpack/api_key +{ + "realm_name" : "native1" +} +-------------------------------------------------- +// NOTCONSOLE + +The following example invalidates all API keys for the user `myuser` in all realms immediately: + +[source,js] +-------------------------------------------------- +DELETE /_xpack/api_key +{ + "username" : "myuser" +} +-------------------------------------------------- +// NOTCONSOLE + +Finally, the following example invalidates all API keys for the user `myuser` in + the `native1` realm immediately: + +[source,js] +-------------------------------------------------- +DELETE /_xpack/api_key +{ + "username" : "myuser", + "realm_name" : "native1" +} +-------------------------------------------------- +// NOTCONSOLE + +A successful call returns a JSON structure that contains the ids of the API keys that were invalidated, the ids +of the API keys that had already been invalidated, and potentially a list of errors encountered while invalidating +specific api keys. + +[source,js] +-------------------------------------------------- +{ + "invalidated_api_keys": [ <1> + "api-key-id-1" + ], + "previously_invalidated_api_keys": [ <2> + "api-key-id-2", + "api-key-id-3" + ], + "error_count": 2, <3> + "error_details": [ <4> + { + "type": "exception", + "reason": "error occurred while invalidating api keys", + "caused_by": { + "type": "illegal_argument_exception", + "reason": "invalid api key id" + } + }, + { + "type": "exception", + "reason": "error occurred while invalidating api keys", + "caused_by": { + "type": "illegal_argument_exception", + "reason": "invalid api key id" + } + } + ] +} +-------------------------------------------------- +// NOTCONSOLE + +<1> The ids of the API keys that were invalidated as part of this request. +<2> The ids of the API keys that were already invalidated. +<3> The number of errors that were encountered when invalidating the API keys. +<4> Details about these errors. This field is not present in the response when + `error_count` is 0. diff --git a/x-pack/plugin/build.gradle b/x-pack/plugin/build.gradle index dbb4e86dc197b..debd4dca867b6 100644 --- a/x-pack/plugin/build.gradle +++ b/x-pack/plugin/build.gradle @@ -133,6 +133,7 @@ integTestCluster { setting 'xpack.monitoring.exporters._local.type', 'local' setting 'xpack.monitoring.exporters._local.enabled', 'false' setting 'xpack.security.authc.token.enabled', 'true' + setting 'xpack.security.authc.api_key.enabled', 'true' setting 'xpack.security.transport.ssl.enabled', 'true' setting 'xpack.security.transport.ssl.key', nodeKey.name setting 'xpack.security.transport.ssl.certificate', nodeCert.name diff --git a/x-pack/plugin/ccr/src/main/java/org/elasticsearch/xpack/ccr/CcrLicenseChecker.java b/x-pack/plugin/ccr/src/main/java/org/elasticsearch/xpack/ccr/CcrLicenseChecker.java index 884d12afe6650..06a686541005d 100644 --- a/x-pack/plugin/ccr/src/main/java/org/elasticsearch/xpack/ccr/CcrLicenseChecker.java +++ b/x-pack/plugin/ccr/src/main/java/org/elasticsearch/xpack/ccr/CcrLicenseChecker.java @@ -14,13 +14,13 @@ import org.elasticsearch.action.ActionResponse; import org.elasticsearch.action.admin.cluster.state.ClusterStateRequest; import org.elasticsearch.action.admin.cluster.state.ClusterStateResponse; -import org.elasticsearch.action.support.ContextPreservingActionListener; import org.elasticsearch.action.admin.indices.stats.IndexShardStats; import org.elasticsearch.action.admin.indices.stats.IndexStats; import org.elasticsearch.action.admin.indices.stats.IndicesStatsAction; import org.elasticsearch.action.admin.indices.stats.IndicesStatsRequest; import org.elasticsearch.action.admin.indices.stats.IndicesStatsResponse; import org.elasticsearch.action.admin.indices.stats.ShardStats; +import org.elasticsearch.action.support.ContextPreservingActionListener; import org.elasticsearch.client.Client; import org.elasticsearch.client.FilterClient; import org.elasticsearch.cluster.ClusterState; @@ -37,14 +37,15 @@ import org.elasticsearch.license.RemoteClusterLicenseChecker; import org.elasticsearch.license.XPackLicenseState; import org.elasticsearch.rest.RestStatus; -import org.elasticsearch.xpack.ccr.action.ShardFollowTask; import org.elasticsearch.xpack.ccr.action.ShardChangesAction; +import org.elasticsearch.xpack.ccr.action.ShardFollowTask; import org.elasticsearch.xpack.core.XPackPlugin; import org.elasticsearch.xpack.core.security.SecurityContext; import org.elasticsearch.xpack.core.security.action.user.HasPrivilegesAction; import org.elasticsearch.xpack.core.security.action.user.HasPrivilegesRequest; import org.elasticsearch.xpack.core.security.action.user.HasPrivilegesResponse; import org.elasticsearch.xpack.core.security.authz.RoleDescriptor; +import org.elasticsearch.xpack.core.security.authz.permission.ResourcePrivileges; import org.elasticsearch.xpack.core.security.support.Exceptions; import java.util.Arrays; @@ -334,7 +335,7 @@ public void hasPrivilegesToFollowIndices(final Client remoteClient, final String message.append(indices.length == 1 ? " index " : " indices "); message.append(Arrays.toString(indices)); - HasPrivilegesResponse.ResourcePrivileges resourcePrivileges = response.getIndexPrivileges().iterator().next(); + ResourcePrivileges resourcePrivileges = response.getIndexPrivileges().iterator().next(); for (Map.Entry entry : resourcePrivileges.getPrivileges().entrySet()) { if (entry.getValue() == false) { message.append(", privilege for action ["); diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/XPackClientPlugin.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/XPackClientPlugin.java index b16409b6a3ccf..10ea252f5cd5f 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/XPackClientPlugin.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/XPackClientPlugin.java @@ -136,6 +136,9 @@ import org.elasticsearch.xpack.core.security.SecurityFeatureSetUsage; import org.elasticsearch.xpack.core.security.SecurityField; import org.elasticsearch.xpack.core.security.SecuritySettings; +import org.elasticsearch.xpack.core.security.action.CreateApiKeyAction; +import org.elasticsearch.xpack.core.security.action.GetApiKeyAction; +import org.elasticsearch.xpack.core.security.action.InvalidateApiKeyAction; import org.elasticsearch.xpack.core.security.action.realm.ClearRealmCacheAction; import org.elasticsearch.xpack.core.security.action.role.ClearRolesCacheAction; import org.elasticsearch.xpack.core.security.action.role.DeleteRoleAction; @@ -314,6 +317,9 @@ public List getClientActions() { InvalidateTokenAction.INSTANCE, GetCertificateInfoAction.INSTANCE, RefreshTokenAction.INSTANCE, + CreateApiKeyAction.INSTANCE, + InvalidateApiKeyAction.INSTANCE, + GetApiKeyAction.INSTANCE, // upgrade IndexUpgradeInfoAction.INSTANCE, IndexUpgradeAction.INSTANCE, diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/XPackSettings.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/XPackSettings.java index 288811fc1af9b..89e572bff8237 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/XPackSettings.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/XPackSettings.java @@ -100,7 +100,7 @@ private XPackSettings() { public static final Setting RESERVED_REALM_ENABLED_SETTING = Setting.boolSetting("xpack.security.authc.reserved_realm.enabled", true, Setting.Property.NodeScope); - /** Setting for enabling or disabling the token service. Defaults to true */ + /** Setting for enabling or disabling the token service. Defaults to the value of https being enabled */ public static final Setting TOKEN_SERVICE_ENABLED_SETTING = Setting.boolSetting("xpack.security.authc.token.enabled", (s) -> { if (NetworkModule.HTTP_ENABLED.get(s)) { return XPackSettings.HTTP_SSL_ENABLED.getRaw(s); @@ -109,6 +109,10 @@ private XPackSettings() { } }, Setting.Property.NodeScope); + /** Setting for enabling or disabling the api key service. Defaults to the value of https being enabled */ + public static final Setting API_KEY_SERVICE_ENABLED_SETTING = Setting.boolSetting("xpack.security.authc.api_key.enabled", + XPackSettings.HTTP_SSL_ENABLED::getRaw, Setting.Property.NodeScope); + /** Setting for enabling or disabling FIPS mode. Defaults to false */ public static final Setting FIPS_MODE_ENABLED = Setting.boolSetting("xpack.security.fips_mode.enabled", false, Property.NodeScope); @@ -197,6 +201,7 @@ public static List> getAllSettings() { settings.add(HTTP_SSL_ENABLED); settings.add(RESERVED_REALM_ENABLED_SETTING); settings.add(TOKEN_SERVICE_ENABLED_SETTING); + settings.add(API_KEY_SERVICE_ENABLED_SETTING); settings.add(SQL_ENABLED); settings.add(USER_SETTING); settings.add(ROLLUP_ENABLED); diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/SecurityContext.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/SecurityContext.java index c737ab75d81aa..0da07a52996ad 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/SecurityContext.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/SecurityContext.java @@ -13,9 +13,11 @@ import org.elasticsearch.common.util.concurrent.ThreadContext.StoredContext; import org.elasticsearch.node.Node; import org.elasticsearch.xpack.core.security.authc.Authentication; +import org.elasticsearch.xpack.core.security.authc.Authentication.AuthenticationType; import org.elasticsearch.xpack.core.security.user.User; import java.io.IOException; +import java.util.Collections; import java.util.Objects; import java.util.function.Consumer; @@ -71,7 +73,8 @@ public void setUser(User user, Version version) { } else { lookedUpBy = null; } - setAuthentication(new Authentication(user, authenticatedBy, lookedUpBy, version)); + setAuthentication( + new Authentication(user, authenticatedBy, lookedUpBy, version, AuthenticationType.INTERNAL, Collections.emptyMap())); } /** Writes the authentication to the thread context */ @@ -89,7 +92,7 @@ private void setAuthentication(Authentication authentication) { */ public void executeAsUser(User user, Consumer consumer, Version version) { final StoredContext original = threadContext.newStoredContext(true); - try (ThreadContext.StoredContext ctx = threadContext.stashContext()) { + try (ThreadContext.StoredContext ignore = threadContext.stashContext()) { setUser(user, version); consumer.accept(original); } @@ -102,9 +105,9 @@ public void executeAsUser(User user, Consumer consumer, Version v public void executeAfterRewritingAuthentication(Consumer consumer, Version version) { final StoredContext original = threadContext.newStoredContext(true); final Authentication authentication = Objects.requireNonNull(userSettings.getAuthentication()); - try (ThreadContext.StoredContext ctx = threadContext.stashContext()) { + try (ThreadContext.StoredContext ignore = threadContext.stashContext()) { setAuthentication(new Authentication(authentication.getUser(), authentication.getAuthenticatedBy(), - authentication.getLookedUpBy(), version)); + authentication.getLookedUpBy(), version, authentication.getAuthenticationType(), authentication.getMetadata())); consumer.accept(original); } } diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/ApiKey.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/ApiKey.java new file mode 100644 index 0000000000000..bfe9f523062a0 --- /dev/null +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/ApiKey.java @@ -0,0 +1,165 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +package org.elasticsearch.xpack.core.security.action; + +import org.elasticsearch.common.ParseField; +import org.elasticsearch.common.io.stream.StreamInput; +import org.elasticsearch.common.io.stream.StreamOutput; +import org.elasticsearch.common.io.stream.Writeable; +import org.elasticsearch.common.xcontent.ConstructingObjectParser; +import org.elasticsearch.common.xcontent.ToXContentObject; +import org.elasticsearch.common.xcontent.XContentBuilder; +import org.elasticsearch.common.xcontent.XContentParser; + +import java.io.IOException; +import java.time.Instant; +import java.util.Objects; + +import static org.elasticsearch.common.xcontent.ConstructingObjectParser.constructorArg; +import static org.elasticsearch.common.xcontent.ConstructingObjectParser.optionalConstructorArg; + +/** + * API key information + */ +public final class ApiKey implements ToXContentObject, Writeable { + + private final String name; + private final String id; + private final Instant creation; + private final Instant expiration; + private final boolean invalidated; + private final String username; + private final String realm; + + public ApiKey(String name, String id, Instant creation, Instant expiration, boolean invalidated, String username, String realm) { + this.name = name; + this.id = id; + // As we do not yet support the nanosecond precision when we serialize to JSON, + // here creating the 'Instant' of milliseconds precision. + // This Instant can then be used for date comparison. + this.creation = Instant.ofEpochMilli(creation.toEpochMilli()); + this.expiration = (expiration != null) ? Instant.ofEpochMilli(expiration.toEpochMilli()): null; + this.invalidated = invalidated; + this.username = username; + this.realm = realm; + } + + public ApiKey(StreamInput in) throws IOException { + this.name = in.readString(); + this.id = in.readString(); + this.creation = in.readInstant(); + this.expiration = in.readOptionalInstant(); + this.invalidated = in.readBoolean(); + this.username = in.readString(); + this.realm = in.readString(); + } + + public String getId() { + return id; + } + + public String getName() { + return name; + } + + public Instant getCreation() { + return creation; + } + + public Instant getExpiration() { + return expiration; + } + + public boolean isInvalidated() { + return invalidated; + } + + public String getUsername() { + return username; + } + + public String getRealm() { + return realm; + } + + @Override + public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { + builder.startObject() + .field("id", id) + .field("name", name) + .field("creation", creation.toEpochMilli()); + if (expiration != null) { + builder.field("expiration", expiration.toEpochMilli()); + } + builder.field("invalidated", invalidated) + .field("username", username) + .field("realm", realm); + return builder.endObject(); + } + + @Override + public void writeTo(StreamOutput out) throws IOException { + out.writeString(name); + out.writeString(id); + out.writeInstant(creation); + out.writeOptionalInstant(expiration); + out.writeBoolean(invalidated); + out.writeString(username); + out.writeString(realm); + } + + @Override + public int hashCode() { + return Objects.hash(name, id, creation, expiration, invalidated, username, realm); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + ApiKey other = (ApiKey) obj; + return Objects.equals(name, other.name) + && Objects.equals(id, other.id) + && Objects.equals(creation, other.creation) + && Objects.equals(expiration, other.expiration) + && Objects.equals(invalidated, other.invalidated) + && Objects.equals(username, other.username) + && Objects.equals(realm, other.realm); + } + + static ConstructingObjectParser PARSER = new ConstructingObjectParser<>("api_key", args -> { + return new ApiKey((String) args[0], (String) args[1], Instant.ofEpochMilli((Long) args[2]), + (args[3] == null) ? null : Instant.ofEpochMilli((Long) args[3]), (Boolean) args[4], (String) args[5], (String) args[6]); + }); + static { + PARSER.declareString(constructorArg(), new ParseField("name")); + PARSER.declareString(constructorArg(), new ParseField("id")); + PARSER.declareLong(constructorArg(), new ParseField("creation")); + PARSER.declareLong(optionalConstructorArg(), new ParseField("expiration")); + PARSER.declareBoolean(constructorArg(), new ParseField("invalidated")); + PARSER.declareString(constructorArg(), new ParseField("username")); + PARSER.declareString(constructorArg(), new ParseField("realm")); + } + + public static ApiKey fromXContent(XContentParser parser) throws IOException { + return PARSER.parse(parser, null); + } + + @Override + public String toString() { + return "ApiKey [name=" + name + ", id=" + id + ", creation=" + creation + ", expiration=" + expiration + ", invalidated=" + + invalidated + ", username=" + username + ", realm=" + realm + "]"; + } + +} diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/CreateApiKeyAction.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/CreateApiKeyAction.java new file mode 100644 index 0000000000000..52d290e10ca3a --- /dev/null +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/CreateApiKeyAction.java @@ -0,0 +1,39 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +package org.elasticsearch.xpack.core.security.action; + +import org.elasticsearch.action.Action; +import org.elasticsearch.client.ElasticsearchClient; +import org.elasticsearch.common.io.stream.Writeable; + +/** + * Action for the creation of an API key + */ +public final class CreateApiKeyAction extends Action { + + public static final String NAME = "cluster:admin/xpack/security/api_key/create"; + public static final CreateApiKeyAction INSTANCE = new CreateApiKeyAction(); + + private CreateApiKeyAction() { + super(NAME); + } + + @Override + public CreateApiKeyResponse newResponse() { + throw new UnsupportedOperationException("usage of Streamable is to be replaced by Writeable"); + } + + @Override + public Writeable.Reader getResponseReader() { + return CreateApiKeyResponse::new; + } + + @Override + public CreateApiKeyRequestBuilder newRequestBuilder(ElasticsearchClient client) { + return new CreateApiKeyRequestBuilder(client); + } +} diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/CreateApiKeyRequest.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/CreateApiKeyRequest.java new file mode 100644 index 0000000000000..c3f7ece21fc79 --- /dev/null +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/CreateApiKeyRequest.java @@ -0,0 +1,136 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +package org.elasticsearch.xpack.core.security.action; + +import org.elasticsearch.action.ActionRequest; +import org.elasticsearch.action.ActionRequestValidationException; +import org.elasticsearch.action.support.WriteRequest; +import org.elasticsearch.common.Nullable; +import org.elasticsearch.common.Strings; +import org.elasticsearch.common.io.stream.StreamInput; +import org.elasticsearch.common.io.stream.StreamOutput; +import org.elasticsearch.common.unit.TimeValue; +import org.elasticsearch.xpack.core.security.authz.RoleDescriptor; + +import java.io.IOException; +import java.util.Collections; +import java.util.List; +import java.util.Objects; + +import static org.elasticsearch.action.ValidateActions.addValidationError; + +/** + * Request class used for the creation of an API key. The request requires a name to be provided + * and optionally an expiration time and permission limitation can be provided. + */ +public final class CreateApiKeyRequest extends ActionRequest { + public static final WriteRequest.RefreshPolicy DEFAULT_REFRESH_POLICY = WriteRequest.RefreshPolicy.WAIT_UNTIL; + + private String name; + private TimeValue expiration; + private List roleDescriptors = Collections.emptyList(); + private WriteRequest.RefreshPolicy refreshPolicy = DEFAULT_REFRESH_POLICY; + + public CreateApiKeyRequest() {} + + /** + * Create API Key request constructor + * @param name name for the API key + * @param roleDescriptors list of {@link RoleDescriptor}s + * @param expiration to specify expiration for the API key + */ + public CreateApiKeyRequest(String name, List roleDescriptors, @Nullable TimeValue expiration) { + if (Strings.hasText(name)) { + this.name = name; + } else { + throw new IllegalArgumentException("name must not be null or empty"); + } + this.roleDescriptors = Objects.requireNonNull(roleDescriptors, "role descriptors may not be null"); + this.expiration = expiration; + } + + public CreateApiKeyRequest(StreamInput in) throws IOException { + super(in); + this.name = in.readString(); + this.expiration = in.readOptionalTimeValue(); + this.roleDescriptors = Collections.unmodifiableList(in.readList(RoleDescriptor::new)); + this.refreshPolicy = WriteRequest.RefreshPolicy.readFrom(in); + } + + public String getName() { + return name; + } + + public void setName(String name) { + if (Strings.hasText(name)) { + this.name = name; + } else { + throw new IllegalArgumentException("name must not be null or empty"); + } + } + + public TimeValue getExpiration() { + return expiration; + } + + public void setExpiration(TimeValue expiration) { + this.expiration = expiration; + } + + public List getRoleDescriptors() { + return roleDescriptors; + } + + public void setRoleDescriptors(List roleDescriptors) { + this.roleDescriptors = Collections.unmodifiableList(Objects.requireNonNull(roleDescriptors, "role descriptors may not be null")); + } + + public WriteRequest.RefreshPolicy getRefreshPolicy() { + return refreshPolicy; + } + + public void setRefreshPolicy(WriteRequest.RefreshPolicy refreshPolicy) { + this.refreshPolicy = Objects.requireNonNull(refreshPolicy, "refresh policy may not be null"); + } + + @Override + public ActionRequestValidationException validate() { + ActionRequestValidationException validationException = null; + if (Strings.isNullOrEmpty(name)) { + validationException = addValidationError("name is required", validationException); + } else { + if (name.length() > 256) { + validationException = addValidationError("name may not be more than 256 characters long", validationException); + } + if (name.equals(name.trim()) == false) { + validationException = addValidationError("name may not begin or end with whitespace", validationException); + } + if (name.startsWith("_")) { + validationException = addValidationError("name may not begin with an underscore", validationException); + } + } + return validationException; + } + + @Override + public void writeTo(StreamOutput out) throws IOException { + super.writeTo(out); + out.writeString(name); + out.writeOptionalTimeValue(expiration); + out.writeList(roleDescriptors); + refreshPolicy.writeTo(out); + } + + @Override + public void readFrom(StreamInput in) throws IOException { + super.readFrom(in); + this.name = in.readString(); + this.expiration = in.readOptionalTimeValue(); + this.roleDescriptors = Collections.unmodifiableList(in.readList(RoleDescriptor::new)); + this.refreshPolicy = WriteRequest.RefreshPolicy.readFrom(in); + } +} diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/CreateApiKeyRequestBuilder.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/CreateApiKeyRequestBuilder.java new file mode 100644 index 0000000000000..e089ec826da17 --- /dev/null +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/CreateApiKeyRequestBuilder.java @@ -0,0 +1,85 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +package org.elasticsearch.xpack.core.security.action; + +import org.elasticsearch.action.ActionRequestBuilder; +import org.elasticsearch.action.support.WriteRequest; +import org.elasticsearch.client.ElasticsearchClient; +import org.elasticsearch.common.ParseField; +import org.elasticsearch.common.bytes.BytesReference; +import org.elasticsearch.common.unit.TimeValue; +import org.elasticsearch.common.xcontent.ConstructingObjectParser; +import org.elasticsearch.common.xcontent.LoggingDeprecationHandler; +import org.elasticsearch.common.xcontent.NamedXContentRegistry; +import org.elasticsearch.common.xcontent.XContentParser; +import org.elasticsearch.common.xcontent.XContentType; +import org.elasticsearch.xpack.core.security.authz.RoleDescriptor; + +import java.io.IOException; +import java.io.InputStream; +import java.util.List; + +import static org.elasticsearch.common.xcontent.ConstructingObjectParser.constructorArg; +import static org.elasticsearch.common.xcontent.ConstructingObjectParser.optionalConstructorArg; + +/** + * Request builder for populating a {@link CreateApiKeyRequest} + */ +public final class CreateApiKeyRequestBuilder + extends ActionRequestBuilder { + + @SuppressWarnings("unchecked") + static final ConstructingObjectParser PARSER = new ConstructingObjectParser<>( + "api_key_request", false, (args, v) -> { + return new CreateApiKeyRequest((String) args[0], (List) args[1], + TimeValue.parseTimeValue((String) args[2], null, "expiration")); + }); + + static { + PARSER.declareString(constructorArg(), new ParseField("name")); + PARSER.declareNamedObjects(constructorArg(), (p, c, n) -> { + p.nextToken(); + return RoleDescriptor.parse(n, p, false); + }, new ParseField("role_descriptors")); + PARSER.declareString(optionalConstructorArg(), new ParseField("expiration")); + } + + public CreateApiKeyRequestBuilder(ElasticsearchClient client) { + super(client, CreateApiKeyAction.INSTANCE, new CreateApiKeyRequest()); + } + + public CreateApiKeyRequestBuilder setName(String name) { + request.setName(name); + return this; + } + + public CreateApiKeyRequestBuilder setExpiration(TimeValue expiration) { + request.setExpiration(expiration); + return this; + } + + public CreateApiKeyRequestBuilder setRoleDescriptors(List roleDescriptors) { + request.setRoleDescriptors(roleDescriptors); + return this; + } + + public CreateApiKeyRequestBuilder setRefreshPolicy(WriteRequest.RefreshPolicy refreshPolicy) { + request.setRefreshPolicy(refreshPolicy); + return this; + } + + public CreateApiKeyRequestBuilder source(BytesReference source, XContentType xContentType) throws IOException { + final NamedXContentRegistry registry = NamedXContentRegistry.EMPTY; + try (InputStream stream = source.streamInput(); + XContentParser parser = xContentType.xContent().createParser(registry, LoggingDeprecationHandler.INSTANCE, stream)) { + CreateApiKeyRequest createApiKeyRequest = PARSER.parse(parser, null); + setName(createApiKeyRequest.getName()); + setRoleDescriptors(createApiKeyRequest.getRoleDescriptors()); + setExpiration(createApiKeyRequest.getExpiration()); + } + return this; + } +} diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/CreateApiKeyResponse.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/CreateApiKeyResponse.java new file mode 100644 index 0000000000000..a774413c3c4a2 --- /dev/null +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/CreateApiKeyResponse.java @@ -0,0 +1,168 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +package org.elasticsearch.xpack.core.security.action; + +import org.elasticsearch.action.ActionResponse; +import org.elasticsearch.common.CharArrays; +import org.elasticsearch.common.Nullable; +import org.elasticsearch.common.ParseField; +import org.elasticsearch.common.io.stream.StreamInput; +import org.elasticsearch.common.io.stream.StreamOutput; +import org.elasticsearch.common.settings.SecureString; +import org.elasticsearch.common.xcontent.ConstructingObjectParser; +import org.elasticsearch.common.xcontent.ToXContentObject; +import org.elasticsearch.common.xcontent.XContentBuilder; +import org.elasticsearch.common.xcontent.XContentParser; + +import java.io.IOException; +import java.time.Instant; +import java.util.Arrays; +import java.util.Objects; + +import static org.elasticsearch.common.xcontent.ConstructingObjectParser.constructorArg; +import static org.elasticsearch.common.xcontent.ConstructingObjectParser.optionalConstructorArg; + +/** + * Response for the successful creation of an api key + */ +public final class CreateApiKeyResponse extends ActionResponse implements ToXContentObject { + + static ConstructingObjectParser PARSER = new ConstructingObjectParser<>("create_api_key_response", + args -> new CreateApiKeyResponse((String) args[0], (String) args[1], new SecureString((String) args[2]), + (args[3] == null) ? null : Instant.ofEpochMilli((Long) args[3]))); + static { + PARSER.declareString(constructorArg(), new ParseField("name")); + PARSER.declareString(constructorArg(), new ParseField("id")); + PARSER.declareString(constructorArg(), new ParseField("api_key")); + PARSER.declareLong(optionalConstructorArg(), new ParseField("expiration")); + } + + private final String name; + private final String id; + private final SecureString key; + private final Instant expiration; + + public CreateApiKeyResponse(String name, String id, SecureString key, Instant expiration) { + this.name = name; + this.id = id; + this.key = key; + // As we do not yet support the nanosecond precision when we serialize to JSON, + // here creating the 'Instant' of milliseconds precision. + // This Instant can then be used for date comparison. + this.expiration = (expiration != null) ? Instant.ofEpochMilli(expiration.toEpochMilli()): null; + } + + public CreateApiKeyResponse(StreamInput in) throws IOException { + super(in); + this.name = in.readString(); + this.id = in.readString(); + byte[] bytes = null; + try { + bytes = in.readByteArray(); + this.key = new SecureString(CharArrays.utf8BytesToChars(bytes)); + } finally { + if (bytes != null) { + Arrays.fill(bytes, (byte) 0); + } + } + this.expiration = in.readOptionalInstant(); + } + + public String getName() { + return name; + } + + public String getId() { + return id; + } + + public SecureString getKey() { + return key; + } + + @Nullable + public Instant getExpiration() { + return expiration; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((expiration == null) ? 0 : expiration.hashCode()); + result = prime * result + Objects.hash(id, name, key); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + final CreateApiKeyResponse other = (CreateApiKeyResponse) obj; + if (expiration == null) { + if (other.expiration != null) + return false; + } else if (!Objects.equals(expiration, other.expiration)) + return false; + return Objects.equals(id, other.id) + && Objects.equals(key, other.key) + && Objects.equals(name, other.name); + } + + @Override + public void writeTo(StreamOutput out) throws IOException { + super.writeTo(out); + out.writeString(name); + out.writeString(id); + byte[] bytes = null; + try { + bytes = CharArrays.toUtf8Bytes(key.getChars()); + out.writeByteArray(bytes); + } finally { + if (bytes != null) { + Arrays.fill(bytes, (byte) 0); + } + } + out.writeOptionalInstant(expiration); + } + + @Override + public void readFrom(StreamInput in) { + throw new UnsupportedOperationException("usage of Streamable is to be replaced by Writeable"); + } + + public static CreateApiKeyResponse fromXContent(XContentParser parser) throws IOException { + return PARSER.parse(parser, null); + } + + @Override + public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { + builder.startObject() + .field("id", id) + .field("name", name); + if (expiration != null) { + builder.field("expiration", expiration.toEpochMilli()); + } + byte[] charBytes = CharArrays.toUtf8Bytes(key.getChars()); + try { + builder.field("api_key").utf8Value(charBytes, 0, charBytes.length); + } finally { + Arrays.fill(charBytes, (byte) 0); + } + return builder.endObject(); + } + + @Override + public String toString() { + return "CreateApiKeyResponse [name=" + name + ", id=" + id + ", expiration=" + expiration + "]"; + } + +} diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/GetApiKeyAction.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/GetApiKeyAction.java new file mode 100644 index 0000000000000..6729a23618ee6 --- /dev/null +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/GetApiKeyAction.java @@ -0,0 +1,39 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +package org.elasticsearch.xpack.core.security.action; + +import org.elasticsearch.action.Action; +import org.elasticsearch.client.ElasticsearchClient; +import org.elasticsearch.common.io.stream.Writeable; + +/** + * Action for retrieving API key(s) + */ +public final class GetApiKeyAction extends Action { + + public static final String NAME = "cluster:admin/xpack/security/api_key/get"; + public static final GetApiKeyAction INSTANCE = new GetApiKeyAction(); + + private GetApiKeyAction() { + super(NAME); + } + + @Override + public GetApiKeyResponse newResponse() { + throw new UnsupportedOperationException("usage of Streamable is to be replaced by Writeable"); + } + + @Override + public Writeable.Reader getResponseReader() { + return GetApiKeyResponse::new; + } + + @Override + public GetApiKeyRequestBuilder newRequestBuilder(ElasticsearchClient client) { + return new GetApiKeyRequestBuilder(client); + } +} \ No newline at end of file diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/GetApiKeyRequest.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/GetApiKeyRequest.java new file mode 100644 index 0000000000000..819a64151d20a --- /dev/null +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/GetApiKeyRequest.java @@ -0,0 +1,150 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +package org.elasticsearch.xpack.core.security.action; + +import org.elasticsearch.action.ActionRequest; +import org.elasticsearch.action.ActionRequestValidationException; +import org.elasticsearch.common.Nullable; +import org.elasticsearch.common.Strings; +import org.elasticsearch.common.io.stream.StreamInput; +import org.elasticsearch.common.io.stream.StreamOutput; + +import java.io.IOException; + +import static org.elasticsearch.action.ValidateActions.addValidationError; + +/** + * Request for get API key + */ +public final class GetApiKeyRequest extends ActionRequest { + + private String realmName; + private String userName; + private String apiKeyId; + private String apiKeyName; + + public GetApiKeyRequest() { + this(null, null, null, null); + } + + public GetApiKeyRequest(StreamInput in) throws IOException { + super(in); + realmName = in.readOptionalString(); + userName = in.readOptionalString(); + apiKeyId = in.readOptionalString(); + apiKeyName = in.readOptionalString(); + } + + public GetApiKeyRequest(@Nullable String realmName, @Nullable String userName, @Nullable String apiKeyId, + @Nullable String apiKeyName) { + this.realmName = realmName; + this.userName = userName; + this.apiKeyId = apiKeyId; + this.apiKeyName = apiKeyName; + } + + public String getRealmName() { + return realmName; + } + + public String getUserName() { + return userName; + } + + public String getApiKeyId() { + return apiKeyId; + } + + public String getApiKeyName() { + return apiKeyName; + } + + /** + * Creates get API key request for given realm name + * @param realmName realm name + * @return {@link GetApiKeyRequest} + */ + public static GetApiKeyRequest usingRealmName(String realmName) { + return new GetApiKeyRequest(realmName, null, null, null); + } + + /** + * Creates get API key request for given user name + * @param userName user name + * @return {@link GetApiKeyRequest} + */ + public static GetApiKeyRequest usingUserName(String userName) { + return new GetApiKeyRequest(null, userName, null, null); + } + + /** + * Creates get API key request for given realm and user name + * @param realmName realm name + * @param userName user name + * @return {@link GetApiKeyRequest} + */ + public static GetApiKeyRequest usingRealmAndUserName(String realmName, String userName) { + return new GetApiKeyRequest(realmName, userName, null, null); + } + + /** + * Creates get API key request for given api key id + * @param apiKeyId api key id + * @return {@link GetApiKeyRequest} + */ + public static GetApiKeyRequest usingApiKeyId(String apiKeyId) { + return new GetApiKeyRequest(null, null, apiKeyId, null); + } + + /** + * Creates get api key request for given api key name + * @param apiKeyName api key name + * @return {@link GetApiKeyRequest} + */ + public static GetApiKeyRequest usingApiKeyName(String apiKeyName) { + return new GetApiKeyRequest(null, null, null, apiKeyName); + } + + @Override + public ActionRequestValidationException validate() { + ActionRequestValidationException validationException = null; + if (Strings.hasText(realmName) == false && Strings.hasText(userName) == false && Strings.hasText(apiKeyId) == false + && Strings.hasText(apiKeyName) == false) { + validationException = addValidationError("One of [api key id, api key name, username, realm name] must be specified", + validationException); + } + if (Strings.hasText(apiKeyId) || Strings.hasText(apiKeyName)) { + if (Strings.hasText(realmName) || Strings.hasText(userName)) { + validationException = addValidationError( + "username or realm name must not be specified when the api key id or api key name is specified", + validationException); + } + } + if (Strings.hasText(apiKeyId) && Strings.hasText(apiKeyName)) { + validationException = addValidationError("only one of [api key id, api key name] can be specified", validationException); + } + return validationException; + } + + @Override + public void writeTo(StreamOutput out) throws IOException { + super.writeTo(out); + out.writeOptionalString(realmName); + out.writeOptionalString(userName); + out.writeOptionalString(apiKeyId); + out.writeOptionalString(apiKeyName); + } + + @Override + public void readFrom(StreamInput in) throws IOException { + super.readFrom(in); + realmName = in.readOptionalString(); + userName = in.readOptionalString(); + apiKeyId = in.readOptionalString(); + apiKeyName = in.readOptionalString(); + } +} diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/GetApiKeyRequestBuilder.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/GetApiKeyRequestBuilder.java new file mode 100644 index 0000000000000..9a8b0b2910f55 --- /dev/null +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/GetApiKeyRequestBuilder.java @@ -0,0 +1,21 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +package org.elasticsearch.xpack.core.security.action; + +import org.elasticsearch.action.ActionRequestBuilder; +import org.elasticsearch.client.ElasticsearchClient; + +/** + * Request builder for populating a {@link GetApiKeyRequest} + */ +public class GetApiKeyRequestBuilder extends ActionRequestBuilder { + + protected GetApiKeyRequestBuilder(ElasticsearchClient client) { + super(client, GetApiKeyAction.INSTANCE, new GetApiKeyRequest()); + } + +} diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/GetApiKeyResponse.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/GetApiKeyResponse.java new file mode 100644 index 0000000000000..97b8f380f6940 --- /dev/null +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/GetApiKeyResponse.java @@ -0,0 +1,88 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +package org.elasticsearch.xpack.core.security.action; + +import org.elasticsearch.action.ActionResponse; +import org.elasticsearch.common.ParseField; +import org.elasticsearch.common.io.stream.StreamInput; +import org.elasticsearch.common.io.stream.StreamOutput; +import org.elasticsearch.common.io.stream.Writeable; +import org.elasticsearch.common.xcontent.ConstructingObjectParser; +import org.elasticsearch.common.xcontent.ToXContentObject; +import org.elasticsearch.common.xcontent.XContentBuilder; +import org.elasticsearch.common.xcontent.XContentParser; + +import java.io.IOException; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Objects; + +import static org.elasticsearch.common.xcontent.ConstructingObjectParser.optionalConstructorArg; + +/** + * Response for get API keys.
+ * The result contains information about the API keys that were found. + */ +public final class GetApiKeyResponse extends ActionResponse implements ToXContentObject, Writeable { + + private final ApiKey[] foundApiKeysInfo; + + public GetApiKeyResponse(StreamInput in) throws IOException { + super(in); + this.foundApiKeysInfo = in.readArray(ApiKey::new, ApiKey[]::new); + } + + public GetApiKeyResponse(Collection foundApiKeysInfo) { + Objects.requireNonNull(foundApiKeysInfo, "found_api_keys_info must be provided"); + this.foundApiKeysInfo = foundApiKeysInfo.toArray(new ApiKey[0]); + } + + public static GetApiKeyResponse emptyResponse() { + return new GetApiKeyResponse(Collections.emptyList()); + } + + public ApiKey[] getApiKeyInfos() { + return foundApiKeysInfo; + } + + @Override + public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { + builder.startObject() + .array("api_keys", (Object[]) foundApiKeysInfo); + return builder.endObject(); + } + + @Override + public void readFrom(StreamInput in) throws IOException { + throw new UnsupportedOperationException("usage of Streamable is to be replaced by Writeable"); + } + + @Override + public void writeTo(StreamOutput out) throws IOException { + super.writeTo(out); + out.writeArray(foundApiKeysInfo); + } + + @SuppressWarnings("unchecked") + static ConstructingObjectParser PARSER = new ConstructingObjectParser<>("get_api_key_response", args -> { + return (args[0] == null) ? GetApiKeyResponse.emptyResponse() : new GetApiKeyResponse((List) args[0]); + }); + static { + PARSER.declareObjectArray(optionalConstructorArg(), (p, c) -> ApiKey.fromXContent(p), new ParseField("api_keys")); + } + + public static GetApiKeyResponse fromXContent(XContentParser parser) throws IOException { + return PARSER.parse(parser, null); + } + + @Override + public String toString() { + return "GetApiKeyResponse [foundApiKeysInfo=" + foundApiKeysInfo + "]"; + } + +} \ No newline at end of file diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/InvalidateApiKeyAction.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/InvalidateApiKeyAction.java new file mode 100644 index 0000000000000..9cac055ed9351 --- /dev/null +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/InvalidateApiKeyAction.java @@ -0,0 +1,40 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +package org.elasticsearch.xpack.core.security.action; + +import org.elasticsearch.action.Action; +import org.elasticsearch.client.ElasticsearchClient; +import org.elasticsearch.common.io.stream.Writeable; + +/** + * Action for invalidating API key + */ +public final class InvalidateApiKeyAction + extends Action { + + public static final String NAME = "cluster:admin/xpack/security/api_key/invalidate"; + public static final InvalidateApiKeyAction INSTANCE = new InvalidateApiKeyAction(); + + private InvalidateApiKeyAction() { + super(NAME); + } + + @Override + public InvalidateApiKeyResponse newResponse() { + throw new UnsupportedOperationException("usage of Streamable is to be replaced by Writeable"); + } + + @Override + public Writeable.Reader getResponseReader() { + return InvalidateApiKeyResponse::new; + } + + @Override + public InvalidateApiKeyRequestBuilder newRequestBuilder(ElasticsearchClient client) { + return new InvalidateApiKeyRequestBuilder(client); + } +} diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/InvalidateApiKeyRequest.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/InvalidateApiKeyRequest.java new file mode 100644 index 0000000000000..1f6939fa5a95c --- /dev/null +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/InvalidateApiKeyRequest.java @@ -0,0 +1,150 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +package org.elasticsearch.xpack.core.security.action; + +import org.elasticsearch.action.ActionRequest; +import org.elasticsearch.action.ActionRequestValidationException; +import org.elasticsearch.common.Nullable; +import org.elasticsearch.common.Strings; +import org.elasticsearch.common.io.stream.StreamInput; +import org.elasticsearch.common.io.stream.StreamOutput; + +import java.io.IOException; + +import static org.elasticsearch.action.ValidateActions.addValidationError; + +/** + * Request for invalidating API key(s) so that it can no longer be used + */ +public final class InvalidateApiKeyRequest extends ActionRequest { + + private String realmName; + private String userName; + private String id; + private String name; + + public InvalidateApiKeyRequest() { + this(null, null, null, null); + } + + public InvalidateApiKeyRequest(StreamInput in) throws IOException { + super(in); + realmName = in.readOptionalString(); + userName = in.readOptionalString(); + id = in.readOptionalString(); + name = in.readOptionalString(); + } + + public InvalidateApiKeyRequest(@Nullable String realmName, @Nullable String userName, @Nullable String id, + @Nullable String name) { + this.realmName = realmName; + this.userName = userName; + this.id = id; + this.name = name; + } + + public String getRealmName() { + return realmName; + } + + public String getUserName() { + return userName; + } + + public String getId() { + return id; + } + + public String getName() { + return name; + } + + /** + * Creates invalidate api key request for given realm name + * @param realmName realm name + * @return {@link InvalidateApiKeyRequest} + */ + public static InvalidateApiKeyRequest usingRealmName(String realmName) { + return new InvalidateApiKeyRequest(realmName, null, null, null); + } + + /** + * Creates invalidate API key request for given user name + * @param userName user name + * @return {@link InvalidateApiKeyRequest} + */ + public static InvalidateApiKeyRequest usingUserName(String userName) { + return new InvalidateApiKeyRequest(null, userName, null, null); + } + + /** + * Creates invalidate API key request for given realm and user name + * @param realmName realm name + * @param userName user name + * @return {@link InvalidateApiKeyRequest} + */ + public static InvalidateApiKeyRequest usingRealmAndUserName(String realmName, String userName) { + return new InvalidateApiKeyRequest(realmName, userName, null, null); + } + + /** + * Creates invalidate API key request for given api key id + * @param id api key id + * @return {@link InvalidateApiKeyRequest} + */ + public static InvalidateApiKeyRequest usingApiKeyId(String id) { + return new InvalidateApiKeyRequest(null, null, id, null); + } + + /** + * Creates invalidate api key request for given api key name + * @param name api key name + * @return {@link InvalidateApiKeyRequest} + */ + public static InvalidateApiKeyRequest usingApiKeyName(String name) { + return new InvalidateApiKeyRequest(null, null, null, name); + } + + @Override + public ActionRequestValidationException validate() { + ActionRequestValidationException validationException = null; + if (Strings.hasText(realmName) == false && Strings.hasText(userName) == false && Strings.hasText(id) == false + && Strings.hasText(name) == false) { + validationException = addValidationError("One of [api key id, api key name, username, realm name] must be specified", + validationException); + } + if (Strings.hasText(id) || Strings.hasText(name)) { + if (Strings.hasText(realmName) || Strings.hasText(userName)) { + validationException = addValidationError( + "username or realm name must not be specified when the api key id or api key name is specified", + validationException); + } + } + if (Strings.hasText(id) && Strings.hasText(name)) { + validationException = addValidationError("only one of [api key id, api key name] can be specified", validationException); + } + return validationException; + } + + @Override + public void writeTo(StreamOutput out) throws IOException { + super.writeTo(out); + out.writeOptionalString(realmName); + out.writeOptionalString(userName); + out.writeOptionalString(id); + out.writeOptionalString(name); + } + + @Override + public void readFrom(StreamInput in) throws IOException { + super.readFrom(in); + realmName = in.readOptionalString(); + userName = in.readOptionalString(); + id = in.readOptionalString(); + name = in.readOptionalString(); + } +} diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/InvalidateApiKeyRequestBuilder.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/InvalidateApiKeyRequestBuilder.java new file mode 100644 index 0000000000000..cb71c91e5ecef --- /dev/null +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/InvalidateApiKeyRequestBuilder.java @@ -0,0 +1,22 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +package org.elasticsearch.xpack.core.security.action; + +import org.elasticsearch.action.ActionRequestBuilder; +import org.elasticsearch.client.ElasticsearchClient; + +/** + * Request builder for populating a {@link InvalidateApiKeyRequest} + */ +public final class InvalidateApiKeyRequestBuilder + extends ActionRequestBuilder { + + protected InvalidateApiKeyRequestBuilder(ElasticsearchClient client) { + super(client, InvalidateApiKeyAction.INSTANCE, new InvalidateApiKeyRequest()); + } + +} diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/InvalidateApiKeyResponse.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/InvalidateApiKeyResponse.java new file mode 100644 index 0000000000000..e9580c93d9086 --- /dev/null +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/InvalidateApiKeyResponse.java @@ -0,0 +1,141 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +package org.elasticsearch.xpack.core.security.action; + +import org.elasticsearch.ElasticsearchException; +import org.elasticsearch.action.ActionResponse; +import org.elasticsearch.common.Nullable; +import org.elasticsearch.common.ParseField; +import org.elasticsearch.common.Strings; +import org.elasticsearch.common.io.stream.StreamInput; +import org.elasticsearch.common.io.stream.StreamOutput; +import org.elasticsearch.common.io.stream.Writeable; +import org.elasticsearch.common.xcontent.ConstructingObjectParser; +import org.elasticsearch.common.xcontent.ToXContentObject; +import org.elasticsearch.common.xcontent.XContentBuilder; +import org.elasticsearch.common.xcontent.XContentParser; + +import java.io.IOException; +import java.util.Collections; +import java.util.List; +import java.util.Objects; + +import static org.elasticsearch.common.xcontent.ConstructingObjectParser.constructorArg; +import static org.elasticsearch.common.xcontent.ConstructingObjectParser.optionalConstructorArg; + +/** + * Response for invalidation of one or more API keys result.
+ * The result contains information about: + *

    + *
  • API key ids that were actually invalidated
  • + *
  • API key ids that were not invalidated in this request because they were already invalidated
  • + *
  • how many errors were encountered while invalidating API keys and the error details
  • + *
+ */ +public final class InvalidateApiKeyResponse extends ActionResponse implements ToXContentObject, Writeable { + + private final List invalidatedApiKeys; + private final List previouslyInvalidatedApiKeys; + private final List errors; + + public InvalidateApiKeyResponse(StreamInput in) throws IOException { + super(in); + this.invalidatedApiKeys = in.readList(StreamInput::readString); + this.previouslyInvalidatedApiKeys = in.readList(StreamInput::readString); + this.errors = in.readList(StreamInput::readException); + } + + /** + * Constructor for API keys invalidation response + * @param invalidatedApiKeys list of invalidated API key ids + * @param previouslyInvalidatedApiKeys list of previously invalidated API key ids + * @param errors list of encountered errors while invalidating API keys + */ + public InvalidateApiKeyResponse(List invalidatedApiKeys, List previouslyInvalidatedApiKeys, + @Nullable List errors) { + this.invalidatedApiKeys = Objects.requireNonNull(invalidatedApiKeys, "invalidated_api_keys must be provided"); + this.previouslyInvalidatedApiKeys = Objects.requireNonNull(previouslyInvalidatedApiKeys, + "previously_invalidated_api_keys must be provided"); + if (null != errors) { + this.errors = errors; + } else { + this.errors = Collections.emptyList(); + } + } + + public static InvalidateApiKeyResponse emptyResponse() { + return new InvalidateApiKeyResponse(Collections.emptyList(), Collections.emptyList(), Collections.emptyList()); + } + + public List getInvalidatedApiKeys() { + return invalidatedApiKeys; + } + + public List getPreviouslyInvalidatedApiKeys() { + return previouslyInvalidatedApiKeys; + } + + public List getErrors() { + return errors; + } + + @Override + public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { + builder.startObject() + .array("invalidated_api_keys", invalidatedApiKeys.toArray(Strings.EMPTY_ARRAY)) + .array("previously_invalidated_api_keys", previouslyInvalidatedApiKeys.toArray(Strings.EMPTY_ARRAY)) + .field("error_count", errors.size()); + if (errors.isEmpty() == false) { + builder.field("error_details"); + builder.startArray(); + for (ElasticsearchException e : errors) { + builder.startObject(); + ElasticsearchException.generateThrowableXContent(builder, params, e); + builder.endObject(); + } + builder.endArray(); + } + return builder.endObject(); + } + + @Override + public void readFrom(StreamInput in) throws IOException { + throw new UnsupportedOperationException("usage of Streamable is to be replaced by Writeable"); + } + + @Override + public void writeTo(StreamOutput out) throws IOException { + super.writeTo(out); + out.writeStringCollection(invalidatedApiKeys); + out.writeStringCollection(previouslyInvalidatedApiKeys); + out.writeCollection(errors, StreamOutput::writeException); + } + + static ConstructingObjectParser PARSER = new ConstructingObjectParser<>("invalidate_api_key_response", + args -> { + return new InvalidateApiKeyResponse((List) args[0], (List) args[1], (List) args[3]); + }); + static { + PARSER.declareStringArray(constructorArg(), new ParseField("invalidated_api_keys")); + PARSER.declareStringArray(constructorArg(), new ParseField("previously_invalidated_api_keys")); + // we parse error_count but ignore it while constructing response + PARSER.declareInt(constructorArg(), new ParseField("error_count")); + PARSER.declareObjectArray(optionalConstructorArg(), (p, c) -> ElasticsearchException.fromXContent(p), + new ParseField("error_details")); + } + + public static InvalidateApiKeyResponse fromXContent(XContentParser parser) throws IOException { + return PARSER.parse(parser, null); + } + + @Override + public String toString() { + return "InvalidateApiKeyResponse [invalidatedApiKeys=" + invalidatedApiKeys + ", previouslyInvalidatedApiKeys=" + + previouslyInvalidatedApiKeys + ", errors=" + errors + "]"; + } + +} diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/role/GetRolesResponse.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/role/GetRolesResponse.java index 93c9d6bca9b64..27079eebcc36b 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/role/GetRolesResponse.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/role/GetRolesResponse.java @@ -37,7 +37,7 @@ public void readFrom(StreamInput in) throws IOException { int size = in.readVInt(); roles = new RoleDescriptor[size]; for (int i = 0; i < size; i++) { - roles[i] = RoleDescriptor.readFrom(in); + roles[i] = new RoleDescriptor(in); } } @@ -46,7 +46,7 @@ public void writeTo(StreamOutput out) throws IOException { super.writeTo(out); out.writeVInt(roles.length); for (RoleDescriptor role : roles) { - RoleDescriptor.writeTo(role, out); + role.writeTo(out); } } } diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/user/HasPrivilegesResponse.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/user/HasPrivilegesResponse.java index 71aafbb76187a..2885e702944ed 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/user/HasPrivilegesResponse.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/user/HasPrivilegesResponse.java @@ -11,6 +11,7 @@ import org.elasticsearch.common.io.stream.StreamOutput; import org.elasticsearch.common.xcontent.ToXContentObject; import org.elasticsearch.common.xcontent.XContentBuilder; +import org.elasticsearch.xpack.core.security.authz.permission.ResourcePrivileges; import java.io.IOException; import java.util.Collection; @@ -49,7 +50,7 @@ public HasPrivilegesResponse(String username, boolean completeMatch, Map sorted(Collection resources) { - final Set set = new TreeSet<>(Comparator.comparing(o -> o.resource)); + final Set set = new TreeSet<>(Comparator.comparing(o -> o.getResource())); set.addAll(resources); return set; } @@ -116,11 +117,11 @@ public void readFrom(StreamInput in) throws IOException { private static Set readResourcePrivileges(StreamInput in) throws IOException { final int count = in.readVInt(); - final Set set = new TreeSet<>(Comparator.comparing(o -> o.resource)); + final Set set = new TreeSet<>(Comparator.comparing(o -> o.getResource())); for (int i = 0; i < count; i++) { final String index = in.readString(); final Map privileges = in.readMap(StreamInput::readString, StreamInput::readBoolean); - set.add(new ResourcePrivileges(index, privileges)); + set.add(ResourcePrivileges.builder(index).addPrivileges(privileges).build()); } return set; } @@ -144,8 +145,8 @@ public void writeTo(StreamOutput out) throws IOException { private static void writeResourcePrivileges(StreamOutput out, Set privileges) throws IOException { out.writeVInt(privileges.size()); for (ResourcePrivileges priv : privileges) { - out.writeString(priv.resource); - out.writeMap(priv.privileges, StreamOutput::writeString, StreamOutput::writeBoolean); + out.writeString(priv.getResource()); + out.writeMap(priv.getPrivileges(), StreamOutput::writeString, StreamOutput::writeBoolean); } } @@ -181,60 +182,14 @@ public XContentBuilder toXContent(XContentBuilder builder, Params params) throws return builder; } - private void appendResources(XContentBuilder builder, String field, Set privileges) + private void appendResources(XContentBuilder builder, String field, Set privileges) throws IOException { builder.startObject(field); - for (HasPrivilegesResponse.ResourcePrivileges privilege : privileges) { + for (ResourcePrivileges privilege : privileges) { builder.field(privilege.getResource()); builder.map(privilege.getPrivileges()); } builder.endObject(); } - - public static class ResourcePrivileges { - private final String resource; - private final Map privileges; - - public ResourcePrivileges(String resource, Map privileges) { - this.resource = Objects.requireNonNull(resource); - this.privileges = Collections.unmodifiableMap(privileges); - } - - public String getResource() { - return resource; - } - - public Map getPrivileges() { - return privileges; - } - - @Override - public String toString() { - return getClass().getSimpleName() + "{" + - "resource='" + resource + '\'' + - ", privileges=" + privileges + - '}'; - } - - @Override - public int hashCode() { - int result = resource.hashCode(); - result = 31 * result + privileges.hashCode(); - return result; - } - - @Override - public boolean equals(Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - - final ResourcePrivileges other = (ResourcePrivileges) o; - return this.resource.equals(other.resource) && this.privileges.equals(other.privileges); - } - } } diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/Authentication.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/Authentication.java index b9dbe0a948ff2..a18c35c651e52 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/Authentication.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/Authentication.java @@ -18,6 +18,8 @@ import java.io.IOException; import java.util.Base64; +import java.util.Collections; +import java.util.Map; import java.util.Objects; // TODO(hub-cap) Clean this up after moving User over - This class can re-inherit its field AUTHENTICATION_KEY in AuthenticationField. @@ -28,16 +30,25 @@ public class Authentication implements ToXContentObject { private final RealmRef authenticatedBy; private final RealmRef lookedUpBy; private final Version version; + private final AuthenticationType type; + private final Map metadata; public Authentication(User user, RealmRef authenticatedBy, RealmRef lookedUpBy) { this(user, authenticatedBy, lookedUpBy, Version.CURRENT); } public Authentication(User user, RealmRef authenticatedBy, RealmRef lookedUpBy, Version version) { + this(user, authenticatedBy, lookedUpBy, version, AuthenticationType.REALM, Collections.emptyMap()); + } + + public Authentication(User user, RealmRef authenticatedBy, RealmRef lookedUpBy, Version version, + AuthenticationType type, Map metadata) { this.user = Objects.requireNonNull(user); this.authenticatedBy = Objects.requireNonNull(authenticatedBy); this.lookedUpBy = lookedUpBy; this.version = version; + this.type = type; + this.metadata = metadata; } public Authentication(StreamInput in) throws IOException { @@ -49,6 +60,13 @@ public Authentication(StreamInput in) throws IOException { this.lookedUpBy = null; } this.version = in.getVersion(); + if (in.getVersion().onOrAfter(Version.V_6_6_0)) { + type = AuthenticationType.values()[in.readVInt()]; + metadata = in.readMap(); + } else { + type = AuthenticationType.REALM; + metadata = Collections.emptyMap(); + } } public User getUser() { @@ -67,8 +85,15 @@ public Version getVersion() { return version; } - public static Authentication readFromContext(ThreadContext ctx) - throws IOException, IllegalArgumentException { + public AuthenticationType getAuthenticationType() { + return type; + } + + public Map getMetadata() { + return metadata; + } + + public static Authentication readFromContext(ThreadContext ctx) throws IOException, IllegalArgumentException { Authentication authentication = ctx.getTransient(AuthenticationField.AUTHENTICATION_KEY); if (authentication != null) { assert ctx.getHeader(AuthenticationField.AUTHENTICATION_KEY) != null; @@ -107,8 +132,7 @@ public static Authentication decode(String header) throws IOException { * Writes the authentication to the context. There must not be an existing authentication in the context and if there is an * {@link IllegalStateException} will be thrown */ - public void writeToContext(ThreadContext ctx) - throws IOException, IllegalArgumentException { + public void writeToContext(ThreadContext ctx) throws IOException, IllegalArgumentException { ensureContextDoesNotContainAuthentication(ctx); String header = encode(); ctx.putTransient(AuthenticationField.AUTHENTICATION_KEY, this); @@ -141,28 +165,28 @@ public void writeTo(StreamOutput out) throws IOException { } else { out.writeBoolean(false); } + if (out.getVersion().onOrAfter(Version.V_6_6_0)) { + out.writeVInt(type.ordinal()); + out.writeMap(metadata); + } } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; - Authentication that = (Authentication) o; - - if (!user.equals(that.user)) return false; - if (!authenticatedBy.equals(that.authenticatedBy)) return false; - if (lookedUpBy != null ? !lookedUpBy.equals(that.lookedUpBy) : that.lookedUpBy != null) return false; - return version.equals(that.version); + return user.equals(that.user) && + authenticatedBy.equals(that.authenticatedBy) && + Objects.equals(lookedUpBy, that.lookedUpBy) && + version.equals(that.version) && + type == that.type && + metadata.equals(that.metadata); } @Override public int hashCode() { - int result = user.hashCode(); - result = 31 * result + authenticatedBy.hashCode(); - result = 31 * result + (lookedUpBy != null ? lookedUpBy.hashCode() : 0); - result = 31 * result + version.hashCode(); - return result; + return Objects.hash(user, authenticatedBy, lookedUpBy, version, type, metadata); } @Override @@ -246,5 +270,13 @@ public int hashCode() { return result; } } + + public enum AuthenticationType { + REALM, + API_KEY, + TOKEN, + ANONYMOUS, + INTERNAL + } } diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/DefaultAuthenticationFailureHandler.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/DefaultAuthenticationFailureHandler.java index 736b9378e3876..a7c51b206db9c 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/DefaultAuthenticationFailureHandler.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/DefaultAuthenticationFailureHandler.java @@ -77,10 +77,12 @@ private static Integer authSchemePriority(final String headerValue) { return 0; } else if (headerValue.regionMatches(true, 0, "bearer", 0, "bearer".length())) { return 1; - } else if (headerValue.regionMatches(true, 0, "basic", 0, "basic".length())) { + } else if (headerValue.regionMatches(true, 0, "apikey", 0, "apikey".length())) { return 2; - } else { + } else if (headerValue.regionMatches(true, 0, "basic", 0, "basic".length())) { return 3; + } else { + return 4; } } diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/RoleDescriptor.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/RoleDescriptor.java index 7472a510c38b2..dc506881b1db1 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/RoleDescriptor.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/RoleDescriptor.java @@ -43,7 +43,7 @@ * A holder for a Role that contains user-readable information about the Role * without containing the actual Role object. */ -public class RoleDescriptor implements ToXContentObject { +public class RoleDescriptor implements ToXContentObject, Writeable { public static final String ROLE_TYPE = "role"; @@ -110,6 +110,27 @@ public RoleDescriptor(String name, Collections.singletonMap("enabled", true); } + public RoleDescriptor(StreamInput in) throws IOException { + this.name = in.readString(); + this.clusterPrivileges = in.readStringArray(); + int size = in.readVInt(); + this.indicesPrivileges = new IndicesPrivileges[size]; + for (int i = 0; i < size; i++) { + indicesPrivileges[i] = new IndicesPrivileges(in); + } + this.runAs = in.readStringArray(); + this.metadata = in.readMap(); + this.transientMetadata = in.readMap(); + + if (in.getVersion().onOrAfter(Version.V_6_4_0)) { + this.applicationPrivileges = in.readArray(ApplicationResourcePrivileges::new, ApplicationResourcePrivileges[]::new); + this.conditionalClusterPrivileges = ConditionalClusterPrivileges.readArray(in); + } else { + this.applicationPrivileges = ApplicationResourcePrivileges.NONE; + this.conditionalClusterPrivileges = ConditionalClusterPrivileges.EMPTY_ARRAY; + } + } + public String getName() { return this.name; } @@ -264,21 +285,20 @@ public static RoleDescriptor readFrom(StreamInput in) throws IOException { runAs, metadata, transientMetadata); } - public static void writeTo(RoleDescriptor descriptor, StreamOutput out) throws IOException { - out.writeString(descriptor.name); - out.writeStringArray(descriptor.clusterPrivileges); - out.writeVInt(descriptor.indicesPrivileges.length); - for (IndicesPrivileges group : descriptor.indicesPrivileges) { + @Override + public void writeTo(StreamOutput out) throws IOException { + out.writeString(name); + out.writeStringArray(clusterPrivileges); + out.writeVInt(indicesPrivileges.length); + for (IndicesPrivileges group : indicesPrivileges) { group.writeTo(out); } - out.writeStringArray(descriptor.runAs); - out.writeMap(descriptor.metadata); - if (out.getVersion().onOrAfter(Version.V_5_2_0)) { - out.writeMap(descriptor.transientMetadata); - } + out.writeStringArray(runAs); + out.writeMap(metadata); + out.writeMap(transientMetadata); if (out.getVersion().onOrAfter(Version.V_6_4_0)) { - out.writeArray(ApplicationResourcePrivileges::write, descriptor.applicationPrivileges); - ConditionalClusterPrivileges.writeArray(out, descriptor.getConditionalClusterPrivileges()); + out.writeArray(ApplicationResourcePrivileges::write, applicationPrivileges); + ConditionalClusterPrivileges.writeArray(out, getConditionalClusterPrivileges()); } } diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/accesscontrol/IndicesAccessControl.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/accesscontrol/IndicesAccessControl.java index 6df9ad834c1e5..8cdf099e676d8 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/accesscontrol/IndicesAccessControl.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/accesscontrol/IndicesAccessControl.java @@ -6,11 +6,13 @@ package org.elasticsearch.xpack.core.security.authz.accesscontrol; import org.elasticsearch.common.Nullable; -import org.elasticsearch.common.bytes.BytesReference; +import org.elasticsearch.common.util.set.Sets; import org.elasticsearch.xpack.core.security.authz.IndicesAndAliasesResolverField; +import org.elasticsearch.xpack.core.security.authz.permission.DocumentPermissions; import org.elasticsearch.xpack.core.security.authz.permission.FieldPermissions; import java.util.Collections; +import java.util.HashMap; import java.util.Map; import java.util.Set; @@ -22,7 +24,7 @@ public class IndicesAccessControl { public static final IndicesAccessControl ALLOW_ALL = new IndicesAccessControl(true, Collections.emptyMap()); public static final IndicesAccessControl ALLOW_NO_INDICES = new IndicesAccessControl(true, Collections.singletonMap(IndicesAndAliasesResolverField.NO_INDEX_PLACEHOLDER, - new IndicesAccessControl.IndexAccessControl(true, new FieldPermissions(), null))); + new IndicesAccessControl.IndexAccessControl(true, new FieldPermissions(), DocumentPermissions.allowAll()))); private final boolean granted; private final Map indexPermissions; @@ -55,12 +57,12 @@ public static class IndexAccessControl { private final boolean granted; private final FieldPermissions fieldPermissions; - private final Set queries; + private final DocumentPermissions documentPermissions; - public IndexAccessControl(boolean granted, FieldPermissions fieldPermissions, Set queries) { + public IndexAccessControl(boolean granted, FieldPermissions fieldPermissions, DocumentPermissions documentPermissions) { this.granted = granted; - this.fieldPermissions = fieldPermissions; - this.queries = queries; + this.fieldPermissions = (fieldPermissions == null) ? FieldPermissions.DEFAULT : fieldPermissions; + this.documentPermissions = (documentPermissions == null) ? DocumentPermissions.allowAll() : documentPermissions; } /** @@ -82,8 +84,33 @@ public FieldPermissions getFieldPermissions() { * then this means that there are no document level restrictions */ @Nullable - public Set getQueries() { - return queries; + public DocumentPermissions getDocumentPermissions() { + return documentPermissions; + } + + /** + * Returns a instance of {@link IndexAccessControl}, where the privileges for {@code this} object are constrained by the privileges + * contained in the provided parameter.
+ * Allowed fields for this index permission would be an intersection of allowed fields.
+ * Allowed documents for this index permission would be an intersection of allowed documents.
+ * + * @param limitedByIndexAccessControl {@link IndexAccessControl} + * @return {@link IndexAccessControl} + * @see FieldPermissions#limitFieldPermissions(FieldPermissions) + * @see DocumentPermissions#limitDocumentPermissions(DocumentPermissions) + */ + public IndexAccessControl limitIndexAccessControl(IndexAccessControl limitedByIndexAccessControl) { + final boolean granted; + if (this.granted == limitedByIndexAccessControl.granted) { + granted = this.granted; + } else { + granted = false; + } + FieldPermissions fieldPermissions = getFieldPermissions().limitFieldPermissions( + limitedByIndexAccessControl.fieldPermissions); + DocumentPermissions documentPermissions = getDocumentPermissions() + .limitDocumentPermissions(limitedByIndexAccessControl.getDocumentPermissions()); + return new IndexAccessControl(granted, fieldPermissions, documentPermissions); } @Override @@ -91,11 +118,38 @@ public String toString() { return "IndexAccessControl{" + "granted=" + granted + ", fieldPermissions=" + fieldPermissions + - ", queries=" + queries + + ", documentPermissions=" + documentPermissions + '}'; } } + /** + * Returns a instance of {@link IndicesAccessControl}, where the privileges for {@code this} + * object are constrained by the privileges contained in the provided parameter.
+ * + * @param limitedByIndicesAccessControl {@link IndicesAccessControl} + * @return {@link IndicesAccessControl} + */ + public IndicesAccessControl limitIndicesAccessControl(IndicesAccessControl limitedByIndicesAccessControl) { + final boolean granted; + if (this.granted == limitedByIndicesAccessControl.granted) { + granted = this.granted; + } else { + granted = false; + } + Set indexes = indexPermissions.keySet(); + Set otherIndexes = limitedByIndicesAccessControl.indexPermissions.keySet(); + Set commonIndexes = Sets.intersection(indexes, otherIndexes); + + Map indexPermissions = new HashMap<>(commonIndexes.size()); + for (String index : commonIndexes) { + IndexAccessControl indexAccessControl = getIndexPermissions(index); + IndexAccessControl limitedByIndexAccessControl = limitedByIndicesAccessControl.getIndexPermissions(index); + indexPermissions.put(index, indexAccessControl.limitIndexAccessControl(limitedByIndexAccessControl)); + } + return new IndicesAccessControl(granted, indexPermissions); + } + @Override public String toString() { return "IndicesAccessControl{" + diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/accesscontrol/SecurityIndexSearcherWrapper.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/accesscontrol/SecurityIndexSearcherWrapper.java index 997db63c47423..1169cfff5794c 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/accesscontrol/SecurityIndexSearcherWrapper.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/accesscontrol/SecurityIndexSearcherWrapper.java @@ -5,8 +5,8 @@ */ package org.elasticsearch.xpack.core.security.authz.accesscontrol; -import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; import org.apache.lucene.index.DirectoryReader; import org.apache.lucene.index.LeafReaderContext; import org.apache.lucene.search.BooleanQuery; @@ -18,64 +18,35 @@ import org.apache.lucene.search.DocIdSetIterator; import org.apache.lucene.search.IndexSearcher; import org.apache.lucene.search.LeafCollector; -import org.apache.lucene.search.Query; import org.apache.lucene.search.Scorer; import org.apache.lucene.search.Weight; -import org.apache.lucene.search.join.BitSetProducer; -import org.apache.lucene.search.join.ToChildBlockJoinQuery; import org.apache.lucene.util.BitSet; import org.apache.lucene.util.BitSetIterator; import org.apache.lucene.util.Bits; import org.apache.lucene.util.SparseFixedBitSet; -import org.elasticsearch.ElasticsearchParseException; import org.elasticsearch.ExceptionsHelper; -import org.elasticsearch.common.bytes.BytesReference; import org.elasticsearch.common.logging.LoggerMessageFormat; -import org.elasticsearch.common.lucene.search.Queries; import org.elasticsearch.common.util.concurrent.ThreadContext; -import org.elasticsearch.common.xcontent.LoggingDeprecationHandler; -import org.elasticsearch.common.xcontent.NamedXContentRegistry; -import org.elasticsearch.common.xcontent.XContentFactory; -import org.elasticsearch.common.xcontent.XContentParser; import org.elasticsearch.index.cache.bitset.BitsetFilterCache; import org.elasticsearch.index.engine.EngineException; -import org.elasticsearch.index.query.BoolQueryBuilder; -import org.elasticsearch.index.query.BoostingQueryBuilder; -import org.elasticsearch.index.query.ConstantScoreQueryBuilder; -import org.elasticsearch.index.query.GeoShapeQueryBuilder; -import org.elasticsearch.index.query.QueryBuilder; -import org.elasticsearch.index.query.QueryRewriteContext; import org.elasticsearch.index.query.QueryShardContext; -import org.elasticsearch.index.query.Rewriteable; -import org.elasticsearch.index.query.TermsQueryBuilder; -import org.elasticsearch.index.query.functionscore.FunctionScoreQueryBuilder; -import org.elasticsearch.index.search.NestedHelper; import org.elasticsearch.index.shard.IndexSearcherWrapper; import org.elasticsearch.index.shard.ShardId; import org.elasticsearch.index.shard.ShardUtils; import org.elasticsearch.license.XPackLicenseState; -import org.elasticsearch.script.Script; import org.elasticsearch.script.ScriptService; -import org.elasticsearch.script.ScriptType; -import org.elasticsearch.script.TemplateScript; import org.elasticsearch.xpack.core.security.authc.Authentication; import org.elasticsearch.xpack.core.security.authz.AuthorizationServiceField; import org.elasticsearch.xpack.core.security.authz.accesscontrol.DocumentSubsetReader.DocumentSubsetDirectoryReader; +import org.elasticsearch.xpack.core.security.authz.permission.DocumentPermissions; import org.elasticsearch.xpack.core.security.support.Exceptions; import org.elasticsearch.xpack.core.security.user.User; import java.io.IOException; -import java.util.ArrayList; import java.util.Arrays; -import java.util.Collections; -import java.util.HashMap; import java.util.List; -import java.util.Map; import java.util.function.Function; -import static org.apache.lucene.search.BooleanClause.Occur.FILTER; -import static org.apache.lucene.search.BooleanClause.Occur.SHOULD; - /** * An {@link IndexSearcherWrapper} implementation that is used for field and document level security. *

@@ -107,7 +78,7 @@ public SecurityIndexSearcherWrapper(Function querySh } @Override - protected DirectoryReader wrap(DirectoryReader reader) { + protected DirectoryReader wrap(final DirectoryReader reader) { if (licenseState.isDocumentAndFieldLevelSecurityAllowed() == false) { return reader; } @@ -120,47 +91,22 @@ protected DirectoryReader wrap(DirectoryReader reader) { throw new IllegalStateException(LoggerMessageFormat.format("couldn't extract shardId from reader [{}]", reader)); } - IndicesAccessControl.IndexAccessControl permissions = indicesAccessControl.getIndexPermissions(shardId.getIndexName()); + final IndicesAccessControl.IndexAccessControl permissions = indicesAccessControl.getIndexPermissions(shardId.getIndexName()); // No permissions have been defined for an index, so don't intercept the index reader for access control if (permissions == null) { return reader; } - if (permissions.getQueries() != null) { - BooleanQuery.Builder filter = new BooleanQuery.Builder(); - for (BytesReference bytesReference : permissions.getQueries()) { - QueryShardContext queryShardContext = queryShardContextProvider.apply(shardId); - String templateResult = evaluateTemplate(bytesReference.utf8ToString()); - try (XContentParser parser = XContentFactory.xContent(templateResult) - .createParser(queryShardContext.getXContentRegistry(), LoggingDeprecationHandler.INSTANCE, templateResult)) { - QueryBuilder queryBuilder = queryShardContext.parseInnerQueryBuilder(parser); - verifyRoleQuery(queryBuilder); - failIfQueryUsesClient(queryBuilder, queryShardContext); - Query roleQuery = queryShardContext.toFilter(queryBuilder).query(); - filter.add(roleQuery, SHOULD); - if (queryShardContext.getMapperService().hasNested()) { - NestedHelper nestedHelper = new NestedHelper(queryShardContext.getMapperService()); - if (nestedHelper.mightMatchNestedDocs(roleQuery)) { - roleQuery = new BooleanQuery.Builder() - .add(roleQuery, FILTER) - .add(Queries.newNonNestedFilter(queryShardContext.indexVersionCreated()), FILTER) - .build(); - } - // If access is allowed on root doc then also access is allowed on all nested docs of that root document: - BitSetProducer rootDocs = queryShardContext.bitsetFilter( - Queries.newNonNestedFilter(queryShardContext.indexVersionCreated())); - ToChildBlockJoinQuery includeNestedDocs = new ToChildBlockJoinQuery(roleQuery, rootDocs); - filter.add(includeNestedDocs, SHOULD); - } - } + DirectoryReader wrappedReader = reader; + DocumentPermissions documentPermissions = permissions.getDocumentPermissions(); + if (documentPermissions != null && documentPermissions.hasDocumentLevelPermissions()) { + BooleanQuery filterQuery = documentPermissions.filter(getUser(), scriptService, shardId, queryShardContextProvider); + if (filterQuery != null) { + wrappedReader = DocumentSubsetReader.wrap(wrappedReader, bitsetFilterCache, new ConstantScoreQuery(filterQuery)); } - - // at least one of the queries should match - filter.setMinimumNumberShouldMatch(1); - reader = DocumentSubsetReader.wrap(reader, bitsetFilterCache, new ConstantScoreQuery(filter.build())); } - return permissions.getFieldPermissions().filter(reader); + return permissions.getFieldPermissions().filter(wrappedReader); } catch (IOException e) { logger.error("Unable to apply field level security"); throw ExceptionsHelper.convertToElastic(e); @@ -255,48 +201,6 @@ static void intersectScorerAndRoleBits(Scorer scorer, SparseFixedBitSet roleBits } } - String evaluateTemplate(String querySource) throws IOException { - // EMPTY is safe here because we never use namedObject - try (XContentParser parser = XContentFactory.xContent(querySource).createParser(NamedXContentRegistry.EMPTY, - LoggingDeprecationHandler.INSTANCE, querySource)) { - XContentParser.Token token = parser.nextToken(); - if (token != XContentParser.Token.START_OBJECT) { - throw new ElasticsearchParseException("Unexpected token [" + token + "]"); - } - token = parser.nextToken(); - if (token != XContentParser.Token.FIELD_NAME) { - throw new ElasticsearchParseException("Unexpected token [" + token + "]"); - } - if ("template".equals(parser.currentName())) { - token = parser.nextToken(); - if (token != XContentParser.Token.START_OBJECT) { - throw new ElasticsearchParseException("Unexpected token [" + token + "]"); - } - Script script = Script.parse(parser); - // Add the user details to the params - Map params = new HashMap<>(); - if (script.getParams() != null) { - params.putAll(script.getParams()); - } - User user = getUser(); - Map userModel = new HashMap<>(); - userModel.put("username", user.principal()); - userModel.put("full_name", user.fullName()); - userModel.put("email", user.email()); - userModel.put("roles", Arrays.asList(user.roles())); - userModel.put("metadata", Collections.unmodifiableMap(user.metadata())); - params.put("_user", userModel); - // Always enforce mustache script lang: - script = new Script(script.getType(), - script.getType() == ScriptType.STORED ? null : "mustache", script.getIdOrCode(), script.getOptions(), params); - TemplateScript compiledTemplate = scriptService.compile(script, TemplateScript.CONTEXT).newInstance(script.getParams()); - return compiledTemplate.execute(); - } else { - return querySource; - } - } - } - protected IndicesAccessControl getIndicesAccessControl() { IndicesAccessControl indicesAccessControl = threadContext.getTransient(AuthorizationServiceField.INDICES_PERMISSIONS_KEY); if (indicesAccessControl == null) { @@ -310,65 +214,4 @@ protected User getUser(){ return authentication.getUser(); } - /** - * Checks whether the role query contains queries we know can't be used as DLS role query. - */ - static void verifyRoleQuery(QueryBuilder queryBuilder) throws IOException { - if (queryBuilder instanceof TermsQueryBuilder) { - TermsQueryBuilder termsQueryBuilder = (TermsQueryBuilder) queryBuilder; - if (termsQueryBuilder.termsLookup() != null) { - throw new IllegalArgumentException("terms query with terms lookup isn't supported as part of a role query"); - } - } else if (queryBuilder instanceof GeoShapeQueryBuilder) { - GeoShapeQueryBuilder geoShapeQueryBuilder = (GeoShapeQueryBuilder) queryBuilder; - if (geoShapeQueryBuilder.shape() == null) { - throw new IllegalArgumentException("geoshape query referring to indexed shapes isn't support as part of a role query"); - } - } else if (queryBuilder.getName().equals("percolate")) { - // actually only if percolate query is referring to an existing document then this is problematic, - // a normal percolate query does work. However we can't check that here as this query builder is inside - // another module. So we don't allow the entire percolate query. I don't think users would ever use - // a percolate query as role query, so this restriction shouldn't prohibit anyone from using dls. - throw new IllegalArgumentException("percolate query isn't support as part of a role query"); - } else if (queryBuilder.getName().equals("has_child")) { - throw new IllegalArgumentException("has_child query isn't support as part of a role query"); - } else if (queryBuilder.getName().equals("has_parent")) { - throw new IllegalArgumentException("has_parent query isn't support as part of a role query"); - } else if (queryBuilder instanceof BoolQueryBuilder) { - BoolQueryBuilder boolQueryBuilder = (BoolQueryBuilder) queryBuilder; - List clauses = new ArrayList<>(); - clauses.addAll(boolQueryBuilder.filter()); - clauses.addAll(boolQueryBuilder.must()); - clauses.addAll(boolQueryBuilder.mustNot()); - clauses.addAll(boolQueryBuilder.should()); - for (QueryBuilder clause : clauses) { - verifyRoleQuery(clause); - } - } else if (queryBuilder instanceof ConstantScoreQueryBuilder) { - verifyRoleQuery(((ConstantScoreQueryBuilder) queryBuilder).innerQuery()); - } else if (queryBuilder instanceof FunctionScoreQueryBuilder) { - verifyRoleQuery(((FunctionScoreQueryBuilder) queryBuilder).query()); - } else if (queryBuilder instanceof BoostingQueryBuilder) { - verifyRoleQuery(((BoostingQueryBuilder) queryBuilder).negativeQuery()); - verifyRoleQuery(((BoostingQueryBuilder) queryBuilder).positiveQuery()); - } - } - - /** - * Fall back validation that verifies that queries during rewrite don't use - * the client to make remote calls. In the case of DLS this can cause a dead - * lock if DLS is also applied on these remote calls. For example in the - * case of terms query with lookup, this can cause recursive execution of - * the DLS query until the get thread pool has been exhausted: - * https://github.com/elastic/x-plugins/issues/3145 - */ - static void failIfQueryUsesClient(QueryBuilder queryBuilder, QueryRewriteContext original) - throws IOException { - QueryRewriteContext copy = new QueryRewriteContext( - original.getXContentRegistry(), original.getWriteableRegistry(), null, original::nowInMillis); - Rewriteable.rewrite(queryBuilder, copy); - if (copy.hasAsyncActions()) { - throw new IllegalStateException("role queries are not allowed to execute additional requests"); - } - } } diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/ApplicationPermission.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/ApplicationPermission.java index 073e92f7faf44..0cd4e8a8b0ddc 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/ApplicationPermission.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/ApplicationPermission.java @@ -12,10 +12,12 @@ import org.elasticsearch.common.collect.Tuple; import org.elasticsearch.common.util.set.Sets; import org.elasticsearch.xpack.core.security.authz.privilege.ApplicationPrivilege; +import org.elasticsearch.xpack.core.security.authz.privilege.ApplicationPrivilegeDescriptor; import org.elasticsearch.xpack.core.security.support.Automatons; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.List; @@ -83,6 +85,40 @@ public boolean grants(ApplicationPrivilege other, String resource) { return matched; } + /** + * For a given application, checks for the privileges for resources and returns an instance of {@link ResourcePrivilegesMap} holding a + * map of resource to {@link ResourcePrivileges} where the resource is application resource and the map of application privilege to + * whether it is allowed or not. + * + * @param applicationName checks privileges for the provided application name + * @param checkForResources check permission grants for the set of resources + * @param checkForPrivilegeNames check permission grants for the set of privilege names + * @param storedPrivileges stored {@link ApplicationPrivilegeDescriptor} for an application against which the access checks are + * performed + * @return an instance of {@link ResourcePrivilegesMap} + */ + public ResourcePrivilegesMap checkResourcePrivileges(final String applicationName, Set checkForResources, + Set checkForPrivilegeNames, + Collection storedPrivileges) { + final ResourcePrivilegesMap.Builder resourcePrivilegesMapBuilder = ResourcePrivilegesMap.builder(); + for (String checkResource : checkForResources) { + for (String checkPrivilegeName : checkForPrivilegeNames) { + final Set nameSet = Collections.singleton(checkPrivilegeName); + final ApplicationPrivilege checkPrivilege = ApplicationPrivilege.get(applicationName, nameSet, storedPrivileges); + assert checkPrivilege.getApplication().equals(applicationName) : "Privilege " + checkPrivilege + " should have application " + + applicationName; + assert checkPrivilege.name().equals(nameSet) : "Privilege " + checkPrivilege + " should have name " + nameSet; + + if (grants(checkPrivilege, checkResource)) { + resourcePrivilegesMapBuilder.addResourcePrivilege(checkResource, checkPrivilegeName, Boolean.TRUE); + } else { + resourcePrivilegesMapBuilder.addResourcePrivilege(checkResource, checkPrivilegeName, Boolean.FALSE); + } + } + } + return resourcePrivilegesMapBuilder.build(); + } + @Override public String toString() { return getClass().getSimpleName() + "{privileges=" + permissions + "}"; diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/ClusterPermission.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/ClusterPermission.java index 3af016959d4ed..687798971399f 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/ClusterPermission.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/ClusterPermission.java @@ -5,6 +5,7 @@ */ package org.elasticsearch.xpack.core.security.authz.permission; +import org.apache.lucene.util.automaton.Operations; import org.elasticsearch.common.collect.Tuple; import org.elasticsearch.transport.TransportRequest; import org.elasticsearch.xpack.core.security.authz.privilege.ClusterPrivilege; @@ -33,6 +34,10 @@ public ClusterPrivilege privilege() { public abstract boolean check(String action, TransportRequest request); + public boolean grants(ClusterPrivilege clusterPrivilege) { + return Operations.subsetOf(clusterPrivilege.getAutomaton(), this.privilege().getAutomaton()); + } + public abstract List> privileges(); /** @@ -111,5 +116,10 @@ public List> privileges() { public boolean check(String action, TransportRequest request) { return children.stream().anyMatch(p -> p.check(action, request)); } + + @Override + public boolean grants(ClusterPrivilege clusterPrivilege) { + return children.stream().anyMatch(p -> p.grants(clusterPrivilege)); + } } } diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/DocumentPermissions.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/DocumentPermissions.java new file mode 100644 index 0000000000000..08d754b4e5357 --- /dev/null +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/DocumentPermissions.java @@ -0,0 +1,262 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +package org.elasticsearch.xpack.core.security.authz.permission; + +import org.apache.lucene.search.BooleanQuery; +import org.apache.lucene.search.Query; +import org.apache.lucene.search.join.BitSetProducer; +import org.apache.lucene.search.join.ToChildBlockJoinQuery; +import org.elasticsearch.common.bytes.BytesReference; +import org.elasticsearch.common.lucene.search.Queries; +import org.elasticsearch.common.xcontent.LoggingDeprecationHandler; +import org.elasticsearch.common.xcontent.XContentFactory; +import org.elasticsearch.common.xcontent.XContentParser; +import org.elasticsearch.index.query.BoolQueryBuilder; +import org.elasticsearch.index.query.BoostingQueryBuilder; +import org.elasticsearch.index.query.ConstantScoreQueryBuilder; +import org.elasticsearch.index.query.GeoShapeQueryBuilder; +import org.elasticsearch.index.query.QueryBuilder; +import org.elasticsearch.index.query.QueryRewriteContext; +import org.elasticsearch.index.query.QueryShardContext; +import org.elasticsearch.index.query.Rewriteable; +import org.elasticsearch.index.query.TermsQueryBuilder; +import org.elasticsearch.index.query.functionscore.FunctionScoreQueryBuilder; +import org.elasticsearch.index.search.NestedHelper; +import org.elasticsearch.index.shard.ShardId; +import org.elasticsearch.script.ScriptService; +import org.elasticsearch.xpack.core.security.authz.support.SecurityQueryTemplateEvaluator; +import org.elasticsearch.xpack.core.security.user.User; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Set; +import java.util.function.Function; + +import static org.apache.lucene.search.BooleanClause.Occur.FILTER; +import static org.apache.lucene.search.BooleanClause.Occur.SHOULD; + +/** + * Stores document level permissions in the form queries that match all the accessible documents.
+ * The document level permissions may be limited by another set of queries in that case the limited + * queries are used as an additional filter. + */ +public final class DocumentPermissions { + private final Set queries; + private final Set limitedByQueries; + + private static DocumentPermissions ALLOW_ALL = new DocumentPermissions(); + + DocumentPermissions() { + this.queries = null; + this.limitedByQueries = null; + } + + DocumentPermissions(Set queries) { + this(queries, null); + } + + DocumentPermissions(Set queries, Set scopedByQueries) { + if (queries == null && scopedByQueries == null) { + throw new IllegalArgumentException("one of the queries or scoped queries must be provided"); + } + this.queries = (queries != null) ? Collections.unmodifiableSet(queries) : queries; + this.limitedByQueries = (scopedByQueries != null) ? Collections.unmodifiableSet(scopedByQueries) : scopedByQueries; + } + + public Set getQueries() { + return queries; + } + + public Set getLimitedByQueries() { + return limitedByQueries; + } + + /** + * @return {@code true} if either queries or scoped queries are present for document level security else returns {@code false} + */ + public boolean hasDocumentLevelPermissions() { + return queries != null || limitedByQueries != null; + } + + /** + * Creates a {@link BooleanQuery} to be used as filter to restrict access to documents.
+ * Document permission queries are used to create an boolean query.
+ * If the document permissions are limited, then there is an additional filter added restricting access to documents only allowed by the + * limited queries. + * + * @param user authenticated {@link User} + * @param scriptService {@link ScriptService} for evaluating query templates + * @param shardId {@link ShardId} + * @param queryShardContextProvider {@link QueryShardContext} + * @return {@link BooleanQuery} for the filter + * @throws IOException thrown if there is an exception during parsing + */ + public BooleanQuery filter(User user, ScriptService scriptService, ShardId shardId, + Function queryShardContextProvider) throws IOException { + if (hasDocumentLevelPermissions()) { + BooleanQuery.Builder filter; + if (queries != null && limitedByQueries != null) { + filter = new BooleanQuery.Builder(); + BooleanQuery.Builder scopedFilter = new BooleanQuery.Builder(); + buildRoleQuery(user, scriptService, shardId, queryShardContextProvider, limitedByQueries, scopedFilter); + filter.add(scopedFilter.build(), FILTER); + + buildRoleQuery(user, scriptService, shardId, queryShardContextProvider, queries, filter); + } else if (queries != null) { + filter = new BooleanQuery.Builder(); + buildRoleQuery(user, scriptService, shardId, queryShardContextProvider, queries, filter); + } else if (limitedByQueries != null) { + filter = new BooleanQuery.Builder(); + buildRoleQuery(user, scriptService, shardId, queryShardContextProvider, limitedByQueries, filter); + } else { + return null; + } + return filter.build(); + } + return null; + } + + private static void buildRoleQuery(User user, ScriptService scriptService, ShardId shardId, + Function queryShardContextProvider, Set queries, + BooleanQuery.Builder filter) throws IOException { + for (BytesReference bytesReference : queries) { + QueryShardContext queryShardContext = queryShardContextProvider.apply(shardId); + String templateResult = SecurityQueryTemplateEvaluator.evaluateTemplate(bytesReference.utf8ToString(), scriptService, user); + try (XContentParser parser = XContentFactory.xContent(templateResult).createParser(queryShardContext.getXContentRegistry(), + LoggingDeprecationHandler.INSTANCE, templateResult)) { + QueryBuilder queryBuilder = queryShardContext.parseInnerQueryBuilder(parser); + verifyRoleQuery(queryBuilder); + failIfQueryUsesClient(queryBuilder, queryShardContext); + Query roleQuery = queryShardContext.toQuery(queryBuilder).query(); + filter.add(roleQuery, SHOULD); + if (queryShardContext.getMapperService().hasNested()) { + NestedHelper nestedHelper = new NestedHelper(queryShardContext.getMapperService()); + if (nestedHelper.mightMatchNestedDocs(roleQuery)) { + roleQuery = new BooleanQuery.Builder().add(roleQuery, FILTER) + .add(Queries.newNonNestedFilter(queryShardContext.indexVersionCreated()), FILTER).build(); + } + // If access is allowed on root doc then also access is allowed on all nested docs of that root document: + BitSetProducer rootDocs = queryShardContext + .bitsetFilter(Queries.newNonNestedFilter(queryShardContext.indexVersionCreated())); + ToChildBlockJoinQuery includeNestedDocs = new ToChildBlockJoinQuery(roleQuery, rootDocs); + filter.add(includeNestedDocs, SHOULD); + } + } + } + // at least one of the queries should match + filter.setMinimumNumberShouldMatch(1); + } + + /** + * Checks whether the role query contains queries we know can't be used as DLS role query. + */ + static void verifyRoleQuery(QueryBuilder queryBuilder) throws IOException { + if (queryBuilder instanceof TermsQueryBuilder) { + TermsQueryBuilder termsQueryBuilder = (TermsQueryBuilder) queryBuilder; + if (termsQueryBuilder.termsLookup() != null) { + throw new IllegalArgumentException("terms query with terms lookup isn't supported as part of a role query"); + } + } else if (queryBuilder instanceof GeoShapeQueryBuilder) { + GeoShapeQueryBuilder geoShapeQueryBuilder = (GeoShapeQueryBuilder) queryBuilder; + if (geoShapeQueryBuilder.shape() == null) { + throw new IllegalArgumentException("geoshape query referring to indexed shapes isn't support as part of a role query"); + } + } else if (queryBuilder.getName().equals("percolate")) { + // actually only if percolate query is referring to an existing document then this is problematic, + // a normal percolate query does work. However we can't check that here as this query builder is inside + // another module. So we don't allow the entire percolate query. I don't think users would ever use + // a percolate query as role query, so this restriction shouldn't prohibit anyone from using dls. + throw new IllegalArgumentException("percolate query isn't support as part of a role query"); + } else if (queryBuilder.getName().equals("has_child")) { + throw new IllegalArgumentException("has_child query isn't support as part of a role query"); + } else if (queryBuilder.getName().equals("has_parent")) { + throw new IllegalArgumentException("has_parent query isn't support as part of a role query"); + } else if (queryBuilder instanceof BoolQueryBuilder) { + BoolQueryBuilder boolQueryBuilder = (BoolQueryBuilder) queryBuilder; + List clauses = new ArrayList<>(); + clauses.addAll(boolQueryBuilder.filter()); + clauses.addAll(boolQueryBuilder.must()); + clauses.addAll(boolQueryBuilder.mustNot()); + clauses.addAll(boolQueryBuilder.should()); + for (QueryBuilder clause : clauses) { + verifyRoleQuery(clause); + } + } else if (queryBuilder instanceof ConstantScoreQueryBuilder) { + verifyRoleQuery(((ConstantScoreQueryBuilder) queryBuilder).innerQuery()); + } else if (queryBuilder instanceof FunctionScoreQueryBuilder) { + verifyRoleQuery(((FunctionScoreQueryBuilder) queryBuilder).query()); + } else if (queryBuilder instanceof BoostingQueryBuilder) { + verifyRoleQuery(((BoostingQueryBuilder) queryBuilder).negativeQuery()); + verifyRoleQuery(((BoostingQueryBuilder) queryBuilder).positiveQuery()); + } + } + + /** + * Fall back validation that verifies that queries during rewrite don't use + * the client to make remote calls. In the case of DLS this can cause a dead + * lock if DLS is also applied on these remote calls. For example in the + * case of terms query with lookup, this can cause recursive execution of + * the DLS query until the get thread pool has been exhausted: + * https://github.com/elastic/x-plugins/issues/3145 + */ + static void failIfQueryUsesClient(QueryBuilder queryBuilder, QueryRewriteContext original) + throws IOException { + QueryRewriteContext copy = new QueryRewriteContext( + original.getXContentRegistry(), original.getWriteableRegistry(), null, original::nowInMillis); + Rewriteable.rewrite(queryBuilder, copy); + if (copy.hasAsyncActions()) { + throw new IllegalStateException("role queries are not allowed to execute additional requests"); + } + } + + /** + * Create {@link DocumentPermissions} for given set of queries + * @param queries set of queries + * @return {@link DocumentPermissions} + */ + public static DocumentPermissions filteredBy(Set queries) { + if (queries == null || queries.isEmpty()) { + throw new IllegalArgumentException("null or empty queries not permitted"); + } + return new DocumentPermissions(queries); + } + + /** + * Create {@link DocumentPermissions} with no restriction. The {@link #getQueries()} + * will return {@code null} in this case and {@link #hasDocumentLevelPermissions()} + * will be {@code false} + * @return {@link DocumentPermissions} + */ + public static DocumentPermissions allowAll() { + return ALLOW_ALL; + } + + /** + * Create a document permissions, where the permissions for {@code this} are + * limited by the queries from other document permissions.
+ * + * @param limitedByDocumentPermissions {@link DocumentPermissions} used to limit the document level access + * @return instance of {@link DocumentPermissions} + */ + public DocumentPermissions limitDocumentPermissions( + DocumentPermissions limitedByDocumentPermissions) { + assert limitedByQueries == null + && limitedByDocumentPermissions.limitedByQueries == null : "nested scoping for document permissions is not permitted"; + if (queries == null && limitedByDocumentPermissions.queries == null) { + return DocumentPermissions.allowAll(); + } + return new DocumentPermissions(queries, limitedByDocumentPermissions.queries); + } + + @Override + public String toString() { + return "DocumentPermissions [queries=" + queries + ", scopedByQueries=" + limitedByQueries + "]"; + } + +} diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/FieldPermissions.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/FieldPermissions.java index e7dd9d2be4c88..53ea785913557 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/FieldPermissions.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/FieldPermissions.java @@ -93,13 +93,15 @@ public FieldPermissions(FieldPermissionsDefinition fieldPermissionsDefinition) { long ramBytesUsed = BASE_FIELD_PERM_DEF_BYTES; - for (FieldGrantExcludeGroup group : fieldPermissionsDefinition.getFieldGrantExcludeGroups()) { - ramBytesUsed += BASE_FIELD_GROUP_BYTES + BASE_HASHSET_ENTRY_SIZE; - if (group.getGrantedFields() != null) { - ramBytesUsed += RamUsageEstimator.shallowSizeOf(group.getGrantedFields()); - } - if (group.getExcludedFields() != null) { - ramBytesUsed += RamUsageEstimator.shallowSizeOf(group.getExcludedFields()); + if (fieldPermissionsDefinition != null) { + for (FieldGrantExcludeGroup group : fieldPermissionsDefinition.getFieldGrantExcludeGroups()) { + ramBytesUsed += BASE_FIELD_GROUP_BYTES + BASE_HASHSET_ENTRY_SIZE; + if (group.getGrantedFields() != null) { + ramBytesUsed += RamUsageEstimator.shallowSizeOf(group.getGrantedFields()); + } + if (group.getExcludedFields() != null) { + ramBytesUsed += RamUsageEstimator.shallowSizeOf(group.getExcludedFields()); + } } } ramBytesUsed += permittedFieldsAutomaton.ramBytesUsed(); @@ -170,6 +172,28 @@ private static boolean containsAllField(String[] fields) { return fields != null && Arrays.stream(fields).anyMatch(AllFieldMapper.NAME::equals); } + /** + * Returns a field permissions instance where it is limited by the given field permissions.
+ * If the current and the other field permissions have field level security then it takes + * an intersection of permitted fields.
+ * If none of the permissions have field level security enabled, then returns permissions + * instance where all fields are allowed. + * + * @param limitedBy {@link FieldPermissions} used to limit current field permissions + * @return {@link FieldPermissions} + */ + public FieldPermissions limitFieldPermissions(FieldPermissions limitedBy) { + if (hasFieldLevelSecurity() && limitedBy != null && limitedBy.hasFieldLevelSecurity()) { + Automaton permittedFieldsAutomaton = Automatons.intersectAndMinimize(getIncludeAutomaton(), limitedBy.getIncludeAutomaton()); + return new FieldPermissions(null, permittedFieldsAutomaton); + } else if (limitedBy != null && limitedBy.hasFieldLevelSecurity()) { + return new FieldPermissions(limitedBy.getFieldPermissionsDefinition(), limitedBy.getIncludeAutomaton()); + } else if (hasFieldLevelSecurity()) { + return new FieldPermissions(getFieldPermissionsDefinition(), getIncludeAutomaton()); + } + return FieldPermissions.DEFAULT; + } + /** * Returns true if this field permission policy allows access to the field and false if not. * fieldName can be a wildcard. @@ -195,7 +219,6 @@ public DirectoryReader filter(DirectoryReader reader) throws IOException { return FieldSubsetReader.wrap(reader, permittedFieldsAutomaton); } - // for testing only Automaton getIncludeAutomaton() { return originalAutomaton; } diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/IndicesPermission.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/IndicesPermission.java index 4c2a479721a2a..da8c3701ed300 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/IndicesPermission.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/IndicesPermission.java @@ -7,6 +7,7 @@ import org.apache.logging.log4j.LogManager; import org.apache.lucene.util.automaton.Automaton; +import org.apache.lucene.util.automaton.Operations; import org.apache.lucene.util.automaton.TooComplexToDeterminizeException; import org.elasticsearch.ElasticsearchSecurityException; import org.elasticsearch.cluster.metadata.AliasOrIndex; @@ -23,6 +24,7 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; +import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.List; @@ -123,6 +125,49 @@ public boolean check(String action) { return false; } + /** + * For given index patterns and index privileges determines allowed privileges and creates an instance of {@link ResourcePrivilegesMap} + * holding a map of resource to {@link ResourcePrivileges} where resource is index pattern and the map of index privilege to whether it + * is allowed or not. + * + * @param checkForIndexPatterns check permission grants for the set of index patterns + * @param allowRestrictedIndices if {@code true} then checks permission grants even for restricted indices by index matching + * @param checkForPrivileges check permission grants for the set of index privileges + * @return an instance of {@link ResourcePrivilegesMap} + */ + public ResourcePrivilegesMap checkResourcePrivileges(Set checkForIndexPatterns, boolean allowRestrictedIndices, + Set checkForPrivileges) { + final ResourcePrivilegesMap.Builder resourcePrivilegesMapBuilder = ResourcePrivilegesMap.builder(); + final Map predicateCache = new HashMap<>(); + for (String forIndexPattern : checkForIndexPatterns) { + final Automaton checkIndexAutomaton = IndicesPermission.Group.buildIndexMatcherAutomaton(allowRestrictedIndices, + forIndexPattern); + Automaton allowedIndexPrivilegesAutomaton = null; + for (Group group : groups) { + final Automaton groupIndexAutomaton = predicateCache.computeIfAbsent(group, + g -> IndicesPermission.Group.buildIndexMatcherAutomaton(g.allowRestrictedIndices(), g.indices())); + if (Operations.subsetOf(checkIndexAutomaton, groupIndexAutomaton)) { + if (allowedIndexPrivilegesAutomaton != null) { + allowedIndexPrivilegesAutomaton = Automatons + .unionAndMinimize(Arrays.asList(allowedIndexPrivilegesAutomaton, group.privilege().getAutomaton())); + } else { + allowedIndexPrivilegesAutomaton = group.privilege().getAutomaton(); + } + } + } + for (String privilege : checkForPrivileges) { + IndexPrivilege indexPrivilege = IndexPrivilege.get(Collections.singleton(privilege)); + if (allowedIndexPrivilegesAutomaton != null + && Operations.subsetOf(indexPrivilege.getAutomaton(), allowedIndexPrivilegesAutomaton)) { + resourcePrivilegesMapBuilder.addResourcePrivilege(forIndexPattern, privilege, Boolean.TRUE); + } else { + resourcePrivilegesMapBuilder.addResourcePrivilege(forIndexPattern, privilege, Boolean.FALSE); + } + } + } + return resourcePrivilegesMapBuilder.build(); + } + public Automaton allowedActionsMatcher(String index) { List automatonList = new ArrayList<>(); for (Group group : groups) { @@ -207,7 +252,8 @@ public Map authorize(String act } else { fieldPermissions = FieldPermissions.DEFAULT; } - indexPermissions.put(index, new IndicesAccessControl.IndexAccessControl(entry.getValue(), fieldPermissions, roleQueries)); + indexPermissions.put(index, new IndicesAccessControl.IndexAccessControl(entry.getValue(), fieldPermissions, + (roleQueries != null) ? DocumentPermissions.filteredBy(roleQueries) : DocumentPermissions.allowAll())); } return unmodifiableMap(indexPermissions); } diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/LimitedRole.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/LimitedRole.java new file mode 100644 index 0000000000000..809b95965340e --- /dev/null +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/LimitedRole.java @@ -0,0 +1,152 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +package org.elasticsearch.xpack.core.security.authz.permission; + +import org.elasticsearch.cluster.metadata.MetaData; +import org.elasticsearch.transport.TransportRequest; +import org.elasticsearch.xpack.core.security.authz.accesscontrol.IndicesAccessControl; +import org.elasticsearch.xpack.core.security.authz.privilege.ApplicationPrivilegeDescriptor; +import org.elasticsearch.xpack.core.security.authz.privilege.ClusterPrivilege; + +import java.util.Collection; +import java.util.Objects; +import java.util.Set; +import java.util.function.Predicate; + +/** + * A {@link Role} limited by another role.
+ * The effective permissions returned on {@link #authorize(String, Set, MetaData, FieldPermissionsCache)} call would be limited by the + * provided role. + */ +public final class LimitedRole extends Role { + private final Role limitedBy; + + LimitedRole(String[] names, ClusterPermission cluster, IndicesPermission indices, ApplicationPermission application, + RunAsPermission runAs, Role limitedBy) { + super(names, cluster, indices, application, runAs); + assert limitedBy != null : "limiting role is required"; + this.limitedBy = limitedBy; + } + + public Role limitedBy() { + return limitedBy; + } + + @Override + public IndicesAccessControl authorize(String action, Set requestedIndicesOrAliases, MetaData metaData, + FieldPermissionsCache fieldPermissionsCache) { + IndicesAccessControl indicesAccessControl = super.authorize(action, requestedIndicesOrAliases, metaData, fieldPermissionsCache); + IndicesAccessControl limitedByIndicesAccessControl = limitedBy.authorize(action, requestedIndicesOrAliases, metaData, + fieldPermissionsCache); + + return indicesAccessControl.limitIndicesAccessControl(limitedByIndicesAccessControl); + } + + /** + * @return A predicate that will match all the indices that this role and the limited by role has the privilege for executing the given + * action on. + */ + @Override + public Predicate allowedIndicesMatcher(String action) { + Predicate predicate = indices().allowedIndicesMatcher(action); + predicate = predicate.and(limitedBy.indices().allowedIndicesMatcher(action)); + return predicate; + } + + /** + * Check if indices permissions allow for the given action, also checks whether the limited by role allows the given actions + * + * @param action indices action + * @return {@code true} if action is allowed else returns {@code false} + */ + @Override + public boolean checkIndicesAction(String action) { + return super.checkIndicesAction(action) && limitedBy.checkIndicesAction(action); + } + + /** + * For given index patterns and index privileges determines allowed privileges and creates an instance of {@link ResourcePrivilegesMap} + * holding a map of resource to {@link ResourcePrivileges} where resource is index pattern and the map of index privilege to whether it + * is allowed or not.
+ * This one takes intersection of resource privileges with the resource privileges from the limited-by role. + * + * @param checkForIndexPatterns check permission grants for the set of index patterns + * @param allowRestrictedIndices if {@code true} then checks permission grants even for restricted indices by index matching + * @param checkForPrivileges check permission grants for the set of index privileges + * @return an instance of {@link ResourcePrivilegesMap} + */ + @Override + public ResourcePrivilegesMap checkIndicesPrivileges(Set checkForIndexPatterns, boolean allowRestrictedIndices, + Set checkForPrivileges) { + ResourcePrivilegesMap resourcePrivilegesMap = super.indices().checkResourcePrivileges(checkForIndexPatterns, allowRestrictedIndices, + checkForPrivileges); + ResourcePrivilegesMap resourcePrivilegesMapForLimitedRole = limitedBy.indices().checkResourcePrivileges(checkForIndexPatterns, + allowRestrictedIndices, checkForPrivileges); + return ResourcePrivilegesMap.intersection(resourcePrivilegesMap, resourcePrivilegesMapForLimitedRole); + } + + /** + * Check if cluster permissions allow for the given action, also checks whether the limited by role allows the given actions + * + * @param action cluster action + * @param request {@link TransportRequest} + * @return {@code true} if action is allowed else returns {@code false} + */ + @Override + public boolean checkClusterAction(String action, TransportRequest request) { + return super.checkClusterAction(action, request) && limitedBy.checkClusterAction(action, request); + } + + /** + * Check if cluster permissions grants the given cluster privilege, also checks whether the limited by role grants the given cluster + * privilege + * + * @param clusterPrivilege cluster privilege + * @return {@code true} if cluster privilege is allowed else returns {@code false} + */ + @Override + public boolean grants(ClusterPrivilege clusterPrivilege) { + return super.grants(clusterPrivilege) && limitedBy.grants(clusterPrivilege); + } + + /** + * For a given application, checks for the privileges for resources and returns an instance of {@link ResourcePrivilegesMap} holding a + * map of resource to {@link ResourcePrivileges} where the resource is application resource and the map of application privilege to + * whether it is allowed or not.
+ * This one takes intersection of resource privileges with the resource privileges from the limited-by role. + * + * @param applicationName checks privileges for the provided application name + * @param checkForResources check permission grants for the set of resources + * @param checkForPrivilegeNames check permission grants for the set of privilege names + * @param storedPrivileges stored {@link ApplicationPrivilegeDescriptor} for an application against which the access checks are + * performed + * @return an instance of {@link ResourcePrivilegesMap} + */ + @Override + public ResourcePrivilegesMap checkApplicationResourcePrivileges(final String applicationName, Set checkForResources, + Set checkForPrivilegeNames, + Collection storedPrivileges) { + ResourcePrivilegesMap resourcePrivilegesMap = super.application().checkResourcePrivileges(applicationName, checkForResources, + checkForPrivilegeNames, storedPrivileges); + ResourcePrivilegesMap resourcePrivilegesMapForLimitedRole = limitedBy.application().checkResourcePrivileges(applicationName, + checkForResources, checkForPrivilegeNames, storedPrivileges); + return ResourcePrivilegesMap.intersection(resourcePrivilegesMap, resourcePrivilegesMapForLimitedRole); + } + + /** + * Create a new role defined by given role and the limited role. + * + * @param fromRole existing role {@link Role} + * @param limitedByRole restrict the newly formed role to the permissions defined by this limited {@link Role} + * @return {@link LimitedRole} + */ + public static LimitedRole createLimitedRole(Role fromRole, Role limitedByRole) { + Objects.requireNonNull(limitedByRole, "limited by role is required to create limited role"); + return new LimitedRole(fromRole.names(), fromRole.cluster(), fromRole.indices(), fromRole.application(), fromRole.runAs(), + limitedByRole); + } +} diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/ResourcePrivileges.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/ResourcePrivileges.java new file mode 100644 index 0000000000000..3c64cc4afa8a1 --- /dev/null +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/ResourcePrivileges.java @@ -0,0 +1,93 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +package org.elasticsearch.xpack.core.security.authz.permission; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Objects; + +/** + * A generic structure to encapsulate resource to privileges map. + */ +public final class ResourcePrivileges { + + private final String resource; + private final Map privileges; + + ResourcePrivileges(String resource, Map privileges) { + this.resource = Objects.requireNonNull(resource); + this.privileges = Collections.unmodifiableMap(privileges); + } + + public String getResource() { + return resource; + } + + public Map getPrivileges() { + return privileges; + } + + public boolean isAllowed(String privilege) { + return Boolean.TRUE.equals(privileges.get(privilege)); + } + + @Override + public String toString() { + return getClass().getSimpleName() + "{" + "resource='" + resource + '\'' + ", privileges=" + privileges + '}'; + } + + @Override + public int hashCode() { + int result = resource.hashCode(); + result = 31 * result + privileges.hashCode(); + return result; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + + final ResourcePrivileges other = (ResourcePrivileges) o; + return this.resource.equals(other.resource) && this.privileges.equals(other.privileges); + } + + public static Builder builder(String resource) { + return new Builder(resource); + } + + public static final class Builder { + private final String resource; + private Map privileges = new HashMap<>(); + + private Builder(String resource) { + this.resource = resource; + } + + public Builder addPrivilege(String privilege, Boolean allowed) { + this.privileges.compute(privilege, (k, v) -> ((v == null) ? allowed : v && allowed)); + return this; + } + + public Builder addPrivileges(Map privileges) { + for (Entry entry : privileges.entrySet()) { + addPrivilege(entry.getKey(), entry.getValue()); + } + return this; + } + + public ResourcePrivileges build() { + return new ResourcePrivileges(resource, privileges); + } + } +} diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/ResourcePrivilegesMap.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/ResourcePrivilegesMap.java new file mode 100644 index 0000000000000..814a6ed29d39f --- /dev/null +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/ResourcePrivilegesMap.java @@ -0,0 +1,121 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +package org.elasticsearch.xpack.core.security.authz.permission; + +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Objects; +import java.util.stream.Collectors; + +/** + * A generic structure to encapsulate resources to {@link ResourcePrivileges}. Also keeps track of whether the resource privileges allow + * permissions to all resources. + */ +public final class ResourcePrivilegesMap { + + private final boolean allAllowed; + private final Map resourceToResourcePrivileges; + + public ResourcePrivilegesMap(boolean allAllowed, Map resToResPriv) { + this.allAllowed = allAllowed; + this.resourceToResourcePrivileges = Collections.unmodifiableMap(Objects.requireNonNull(resToResPriv)); + } + + public boolean allAllowed() { + return allAllowed; + } + + public Map getResourceToResourcePrivileges() { + return resourceToResourcePrivileges; + } + + @Override + public int hashCode() { + return Objects.hash(allAllowed, resourceToResourcePrivileges); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + final ResourcePrivilegesMap other = (ResourcePrivilegesMap) obj; + return allAllowed == other.allAllowed && Objects.equals(resourceToResourcePrivileges, other.resourceToResourcePrivileges); + } + + @Override + public String toString() { + return "ResourcePrivilegesMap [allAllowed=" + allAllowed + ", resourceToResourcePrivileges=" + resourceToResourcePrivileges + "]"; + } + + public static final class Builder { + private boolean allowAll = true; + private Map resourceToResourcePrivilegesBuilder = new LinkedHashMap<>(); + + public Builder addResourcePrivilege(String resource, String privilege, Boolean allowed) { + assert resource != null && privilege != null + && allowed != null : "resource, privilege and permission(allowed or denied) are required"; + ResourcePrivileges.Builder builder = resourceToResourcePrivilegesBuilder.computeIfAbsent(resource, ResourcePrivileges::builder); + builder.addPrivilege(privilege, allowed); + allowAll = allowAll && allowed; + return this; + } + + public Builder addResourcePrivilege(String resource, Map privilegePermissions) { + assert resource != null && privilegePermissions != null : "resource, privilege permissions(allowed or denied) are required"; + ResourcePrivileges.Builder builder = resourceToResourcePrivilegesBuilder.computeIfAbsent(resource, ResourcePrivileges::builder); + builder.addPrivileges(privilegePermissions); + allowAll = allowAll && privilegePermissions.values().stream().allMatch(b -> Boolean.TRUE.equals(b)); + return this; + } + + public Builder addResourcePrivilegesMap(ResourcePrivilegesMap resourcePrivilegesMap) { + resourcePrivilegesMap.getResourceToResourcePrivileges().entrySet().stream() + .forEach(e -> this.addResourcePrivilege(e.getKey(), e.getValue().getPrivileges())); + return this; + } + + public ResourcePrivilegesMap build() { + Map result = resourceToResourcePrivilegesBuilder.entrySet().stream() + .collect(Collectors.toMap(e -> e.getKey(), e -> e.getValue().build())); + return new ResourcePrivilegesMap(allowAll, result); + } + } + + public static Builder builder() { + return new Builder(); + } + + /** + * Takes an intersection of resource privileges and returns a new instance of {@link ResourcePrivilegesMap}. If one of the resource + * privileges map does not allow access to a resource then the resulting map would also not allow access. + * + * @param left an instance of {@link ResourcePrivilegesMap} + * @param right an instance of {@link ResourcePrivilegesMap} + * @return a new instance of {@link ResourcePrivilegesMap}, an intersection of resource privileges. + */ + public static ResourcePrivilegesMap intersection(final ResourcePrivilegesMap left, final ResourcePrivilegesMap right) { + Objects.requireNonNull(left); + Objects.requireNonNull(right); + final ResourcePrivilegesMap.Builder builder = ResourcePrivilegesMap.builder(); + for (Entry leftResPrivsEntry : left.getResourceToResourcePrivileges().entrySet()) { + final ResourcePrivileges leftResPrivs = leftResPrivsEntry.getValue(); + final ResourcePrivileges rightResPrivs = right.getResourceToResourcePrivileges().get(leftResPrivsEntry.getKey()); + builder.addResourcePrivilege(leftResPrivsEntry.getKey(), leftResPrivs.getPrivileges()); + builder.addResourcePrivilege(leftResPrivsEntry.getKey(), rightResPrivs.getPrivileges()); + } + return builder.build(); + } +} diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/Role.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/Role.java index f01869b4ea8dc..c63e8049193af 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/Role.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/Role.java @@ -10,9 +10,11 @@ import org.elasticsearch.common.bytes.BytesReference; import org.elasticsearch.common.collect.Tuple; import org.elasticsearch.common.util.set.Sets; +import org.elasticsearch.transport.TransportRequest; import org.elasticsearch.xpack.core.security.authz.RoleDescriptor; import org.elasticsearch.xpack.core.security.authz.accesscontrol.IndicesAccessControl; import org.elasticsearch.xpack.core.security.authz.privilege.ApplicationPrivilege; +import org.elasticsearch.xpack.core.security.authz.privilege.ApplicationPrivilegeDescriptor; import org.elasticsearch.xpack.core.security.authz.privilege.ClusterPrivilege; import org.elasticsearch.xpack.core.security.authz.privilege.ConditionalClusterPrivilege; import org.elasticsearch.xpack.core.security.authz.privilege.IndexPrivilege; @@ -20,13 +22,15 @@ import java.util.ArrayList; import java.util.Arrays; +import java.util.Collection; import java.util.Collections; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Set; +import java.util.function.Predicate; -public final class Role { +public class Role { public static final Role EMPTY = Role.builder("__empty").build(); @@ -44,6 +48,7 @@ public final class Role { this.runAs = Objects.requireNonNull(runAs); } + public String[] names() { return names; } @@ -76,6 +81,79 @@ public static Builder builder(RoleDescriptor rd, FieldPermissionsCache fieldPerm return new Builder(rd, fieldPermissionsCache); } + /** + * @return A predicate that will match all the indices that this role + * has the privilege for executing the given action on. + */ + public Predicate allowedIndicesMatcher(String action) { + return indices().allowedIndicesMatcher(action); + } + + /** + * Check if indices permissions allow for the given action + * + * @param action indices action + * @return {@code true} if action is allowed else returns {@code false} + */ + public boolean checkIndicesAction(String action) { + return indices().check(action); + } + + + /** + * For given index patterns and index privileges determines allowed privileges and creates an instance of {@link ResourcePrivilegesMap} + * holding a map of resource to {@link ResourcePrivileges} where resource is index pattern and the map of index privilege to whether it + * is allowed or not. + * + * @param checkForIndexPatterns check permission grants for the set of index patterns + * @param allowRestrictedIndices if {@code true} then checks permission grants even for restricted indices by index matching + * @param checkForPrivileges check permission grants for the set of index privileges + * @return an instance of {@link ResourcePrivilegesMap} + */ + public ResourcePrivilegesMap checkIndicesPrivileges(Set checkForIndexPatterns, boolean allowRestrictedIndices, + Set checkForPrivileges) { + return indices().checkResourcePrivileges(checkForIndexPatterns, allowRestrictedIndices, checkForPrivileges); + } + + /** + * Check if cluster permissions allow for the given action + * + * @param action cluster action + * @param request {@link TransportRequest} + * @return {@code true} if action is allowed else returns {@code false} + */ + public boolean checkClusterAction(String action, TransportRequest request) { + return cluster().check(action, request); + } + + /** + * Check if cluster permissions grants the given cluster privilege + * + * @param clusterPrivilege cluster privilege + * @return {@code true} if cluster privilege is allowed else returns {@code false} + */ + public boolean grants(ClusterPrivilege clusterPrivilege) { + return cluster().grants(clusterPrivilege); + } + + /** + * For a given application, checks for the privileges for resources and returns an instance of {@link ResourcePrivilegesMap} holding a + * map of resource to {@link ResourcePrivileges} where the resource is application resource and the map of application privilege to + * whether it is allowed or not. + * + * @param applicationName checks privileges for the provided application name + * @param checkForResources check permission grants for the set of resources + * @param checkForPrivilegeNames check permission grants for the set of privilege names + * @param storedPrivileges stored {@link ApplicationPrivilegeDescriptor} for an application against which the access checks are + * performed + * @return an instance of {@link ResourcePrivilegesMap} + */ + public ResourcePrivilegesMap checkApplicationResourcePrivileges(final String applicationName, Set checkForResources, + Set checkForPrivilegeNames, + Collection storedPrivileges) { + return application().checkResourcePrivileges(applicationName, checkForResources, checkForPrivilegeNames, storedPrivileges); + } + /** * Returns whether at least one group encapsulated by this indices permissions is authorized to execute the * specified action with the requested indices/aliases. At the same time if field and/or document level security @@ -211,4 +289,5 @@ static Tuple> convertApplicationPrivilege(Stri ), Sets.newHashSet(arp.getResources())); } } + } diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/support/SecurityQueryTemplateEvaluator.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/support/SecurityQueryTemplateEvaluator.java new file mode 100644 index 0000000000000..951c4acf10d0d --- /dev/null +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/support/SecurityQueryTemplateEvaluator.java @@ -0,0 +1,92 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +package org.elasticsearch.xpack.core.security.authz.support; + +import org.elasticsearch.ElasticsearchParseException; +import org.elasticsearch.common.xcontent.LoggingDeprecationHandler; +import org.elasticsearch.common.xcontent.NamedXContentRegistry; +import org.elasticsearch.common.xcontent.XContentFactory; +import org.elasticsearch.common.xcontent.XContentParser; +import org.elasticsearch.script.Script; +import org.elasticsearch.script.ScriptService; +import org.elasticsearch.script.ScriptType; +import org.elasticsearch.script.TemplateScript; +import org.elasticsearch.xpack.core.security.user.User; + +import java.io.IOException; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +/** + * Helper class that helps to evaluate the query source template. + */ +public final class SecurityQueryTemplateEvaluator { + + private SecurityQueryTemplateEvaluator() { + } + + /** + * If the query source is a template, then parses the script, compiles the + * script with user details parameters and then executes it to return the + * query string. + *

+ * Note: This method always enforces "mustache" script language for the + * template. + * + * @param querySource query string template to be evaluated. + * @param scriptService {@link ScriptService} + * @param user {@link User} details for user defined parameters in the + * script. + * @return resultant query string after compiling and executing the script. + * If the source does not contain template then it will return the query + * source without any modifications. + * @throws IOException thrown when there is any error parsing the query + * string. + */ + public static String evaluateTemplate(final String querySource, final ScriptService scriptService, final User user) throws IOException { + // EMPTY is safe here because we never use namedObject + try (XContentParser parser = XContentFactory.xContent(querySource).createParser(NamedXContentRegistry.EMPTY, + LoggingDeprecationHandler.INSTANCE, querySource)) { + XContentParser.Token token = parser.nextToken(); + if (token != XContentParser.Token.START_OBJECT) { + throw new ElasticsearchParseException("Unexpected token [" + token + "]"); + } + token = parser.nextToken(); + if (token != XContentParser.Token.FIELD_NAME) { + throw new ElasticsearchParseException("Unexpected token [" + token + "]"); + } + if ("template".equals(parser.currentName())) { + token = parser.nextToken(); + if (token != XContentParser.Token.START_OBJECT) { + throw new ElasticsearchParseException("Unexpected token [" + token + "]"); + } + Script script = Script.parse(parser); + // Add the user details to the params + Map params = new HashMap<>(); + if (script.getParams() != null) { + params.putAll(script.getParams()); + } + Map userModel = new HashMap<>(); + userModel.put("username", user.principal()); + userModel.put("full_name", user.fullName()); + userModel.put("email", user.email()); + userModel.put("roles", Arrays.asList(user.roles())); + userModel.put("metadata", Collections.unmodifiableMap(user.metadata())); + params.put("_user", userModel); + // Always enforce mustache script lang: + script = new Script(script.getType(), script.getType() == ScriptType.STORED ? null : "mustache", script.getIdOrCode(), + script.getOptions(), params); + TemplateScript compiledTemplate = scriptService.compile(script, TemplateScript.CONTEXT).newInstance(script.getParams()); + return compiledTemplate.execute(); + } else { + return querySource; + } + } + } +} diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/client/SecurityClient.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/client/SecurityClient.java index 709f12ecf498e..55026c553ccb0 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/client/SecurityClient.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/client/SecurityClient.java @@ -10,6 +10,16 @@ import org.elasticsearch.client.ElasticsearchClient; import org.elasticsearch.common.bytes.BytesReference; import org.elasticsearch.common.xcontent.XContentType; +import org.elasticsearch.xpack.core.security.action.CreateApiKeyAction; +import org.elasticsearch.xpack.core.security.action.CreateApiKeyRequest; +import org.elasticsearch.xpack.core.security.action.CreateApiKeyRequestBuilder; +import org.elasticsearch.xpack.core.security.action.CreateApiKeyResponse; +import org.elasticsearch.xpack.core.security.action.GetApiKeyAction; +import org.elasticsearch.xpack.core.security.action.GetApiKeyRequest; +import org.elasticsearch.xpack.core.security.action.GetApiKeyResponse; +import org.elasticsearch.xpack.core.security.action.InvalidateApiKeyAction; +import org.elasticsearch.xpack.core.security.action.InvalidateApiKeyRequest; +import org.elasticsearch.xpack.core.security.action.InvalidateApiKeyResponse; import org.elasticsearch.xpack.core.security.action.privilege.DeletePrivilegesAction; import org.elasticsearch.xpack.core.security.action.privilege.DeletePrivilegesRequestBuilder; import org.elasticsearch.xpack.core.security.action.privilege.GetPrivilegesAction; @@ -337,6 +347,27 @@ public void invalidateToken(InvalidateTokenRequest request, ActionListener listener) { + client.execute(CreateApiKeyAction.INSTANCE, request, listener); + } + + public void invalidateApiKey(InvalidateApiKeyRequest request, ActionListener listener) { + client.execute(InvalidateApiKeyAction.INSTANCE, request, listener); + } + + public void getApiKey(GetApiKeyRequest request, ActionListener listener) { + client.execute(GetApiKeyAction.INSTANCE, request, listener); + } + public SamlAuthenticateRequestBuilder prepareSamlAuthenticate(byte[] xmlContent, List validIds) { final SamlAuthenticateRequestBuilder builder = new SamlAuthenticateRequestBuilder(client); builder.saml(xmlContent); diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/support/Automatons.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/support/Automatons.java index 87a0099580b5f..7e6fd7ca46283 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/support/Automatons.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/support/Automatons.java @@ -26,6 +26,7 @@ import static org.apache.lucene.util.automaton.MinimizationOperations.minimize; import static org.apache.lucene.util.automaton.Operations.DEFAULT_MAX_DETERMINIZED_STATES; import static org.apache.lucene.util.automaton.Operations.concatenate; +import static org.apache.lucene.util.automaton.Operations.intersection; import static org.apache.lucene.util.automaton.Operations.minus; import static org.apache.lucene.util.automaton.Operations.union; import static org.elasticsearch.common.Strings.collectionToDelimitedString; @@ -173,6 +174,11 @@ public static Automaton minusAndMinimize(Automaton a1, Automaton a2) { return minimize(res, maxDeterminizedStates); } + public static Automaton intersectAndMinimize(Automaton a1, Automaton a2) { + Automaton res = intersection(a1, a2); + return minimize(res, maxDeterminizedStates); + } + public static Predicate predicate(String... patterns) { return predicate(Arrays.asList(patterns)); } diff --git a/x-pack/plugin/core/src/main/resources/security-index-template.json b/x-pack/plugin/core/src/main/resources/security-index-template.json index 3723aff9054de..183ffff4ea534 100644 --- a/x-pack/plugin/core/src/main/resources/security-index-template.json +++ b/x-pack/plugin/core/src/main/resources/security-index-template.json @@ -152,6 +152,40 @@ "type" : "date", "format" : "epoch_millis" }, + "api_key_hash" : { + "type" : "keyword", + "index": false, + "doc_values": false + }, + "api_key_invalidated" : { + "type" : "boolean" + }, + "role_descriptors" : { + "type" : "object", + "enabled": false + }, + "limited_by_role_descriptors" : { + "type" : "object", + "enabled": false + }, + "version" : { + "type" : "integer" + }, + "creator" : { + "type" : "object", + "properties" : { + "principal" : { + "type": "keyword" + }, + "metadata" : { + "type" : "object", + "dynamic" : true + }, + "realm" : { + "type" : "keyword" + } + } + }, "rules" : { "type" : "object", "dynamic" : true diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/CreateApiKeyRequestBuilderTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/CreateApiKeyRequestBuilderTests.java new file mode 100644 index 0000000000000..fb4f87089e8e7 --- /dev/null +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/CreateApiKeyRequestBuilderTests.java @@ -0,0 +1,62 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +package org.elasticsearch.xpack.core.security.action; + +import org.elasticsearch.client.node.NodeClient; +import org.elasticsearch.common.bytes.BytesArray; +import org.elasticsearch.common.unit.TimeValue; +import org.elasticsearch.common.xcontent.XContentType; +import org.elasticsearch.test.ESTestCase; +import org.elasticsearch.xpack.core.security.authz.RoleDescriptor; +import org.elasticsearch.xpack.core.security.authz.RoleDescriptor.IndicesPrivileges; + +import java.io.IOException; +import java.util.List; + +import static org.hamcrest.Matchers.arrayContainingInAnyOrder; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.is; +import static org.mockito.Mockito.mock; + +public class CreateApiKeyRequestBuilderTests extends ESTestCase { + + public void testParserAndCreateApiRequestBuilder() throws IOException { + boolean withExpiration = randomBoolean(); + final String json = "{ \"name\" : \"my-api-key\", " + + ((withExpiration) ? " \"expiration\": \"1d\", " : "") + +" \"role_descriptors\": { \"role-a\": {\"cluster\":[\"a-1\", \"a-2\"]," + + " \"index\": [{\"names\": [\"indx-a\"], \"privileges\": [\"read\"] }] }, " + + " \"role-b\": {\"cluster\":[\"b\"]," + + " \"index\": [{\"names\": [\"indx-b\"], \"privileges\": [\"read\"] }] } " + + "} }"; + final BytesArray source = new BytesArray(json); + final NodeClient mockClient = mock(NodeClient.class); + final CreateApiKeyRequest request = new CreateApiKeyRequestBuilder(mockClient).source(source, XContentType.JSON).request(); + final List actualRoleDescriptors = request.getRoleDescriptors(); + assertThat(request.getName(), equalTo("my-api-key")); + assertThat(actualRoleDescriptors.size(), is(2)); + for (RoleDescriptor rd : actualRoleDescriptors) { + String[] clusters = null; + IndicesPrivileges indicesPrivileges = null; + if (rd.getName().equals("role-a")) { + clusters = new String[] { "a-1", "a-2" }; + indicesPrivileges = RoleDescriptor.IndicesPrivileges.builder().indices("indx-a").privileges("read").build(); + } else if (rd.getName().equals("role-b")){ + clusters = new String[] { "b" }; + indicesPrivileges = RoleDescriptor.IndicesPrivileges.builder().indices("indx-b").privileges("read").build(); + } else { + fail("unexpected role name"); + } + assertThat(rd.getClusterPrivileges(), arrayContainingInAnyOrder(clusters)); + assertThat(rd.getIndicesPrivileges(), + arrayContainingInAnyOrder(indicesPrivileges)); + } + if (withExpiration) { + assertThat(request.getExpiration(), equalTo(TimeValue.parseTimeValue("1d", "expiration"))); + } + } +} diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/CreateApiKeyRequestTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/CreateApiKeyRequestTests.java new file mode 100644 index 0000000000000..654d56b42130e --- /dev/null +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/CreateApiKeyRequestTests.java @@ -0,0 +1,113 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +package org.elasticsearch.xpack.core.security.action; + +import org.elasticsearch.action.ActionRequestValidationException; +import org.elasticsearch.action.support.WriteRequest; +import org.elasticsearch.common.io.stream.BytesStreamOutput; +import org.elasticsearch.common.io.stream.StreamInput; +import org.elasticsearch.common.unit.TimeValue; +import org.elasticsearch.test.ESTestCase; +import org.elasticsearch.xpack.core.security.authz.RoleDescriptor; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.is; + +public class CreateApiKeyRequestTests extends ESTestCase { + + public void testNameValidation() { + final String name = randomAlphaOfLengthBetween(1, 256); + CreateApiKeyRequest request = new CreateApiKeyRequest(); + + ActionRequestValidationException ve = request.validate(); + assertNotNull(ve); + assertThat(ve.validationErrors().size(), is(1)); + assertThat(ve.validationErrors().get(0), containsString("name is required")); + + request.setName(name); + ve = request.validate(); + assertNull(ve); + + IllegalArgumentException e = expectThrows(IllegalArgumentException.class, () -> request.setName("")); + assertThat(e.getMessage(), containsString("name must not be null or empty")); + + e = expectThrows(IllegalArgumentException.class, () -> request.setName(null)); + assertThat(e.getMessage(), containsString("name must not be null or empty")); + + request.setName(randomAlphaOfLength(257)); + ve = request.validate(); + assertNotNull(ve); + assertThat(ve.validationErrors().size(), is(1)); + assertThat(ve.validationErrors().get(0), containsString("name may not be more than 256 characters long")); + + request.setName(" leading space"); + ve = request.validate(); + assertNotNull(ve); + assertThat(ve.validationErrors().size(), is(1)); + assertThat(ve.validationErrors().get(0), containsString("name may not begin or end with whitespace")); + + request.setName("trailing space "); + ve = request.validate(); + assertNotNull(ve); + assertThat(ve.validationErrors().size(), is(1)); + assertThat(ve.validationErrors().get(0), containsString("name may not begin or end with whitespace")); + + request.setName(" leading and trailing space "); + ve = request.validate(); + assertNotNull(ve); + assertThat(ve.validationErrors().size(), is(1)); + assertThat(ve.validationErrors().get(0), containsString("name may not begin or end with whitespace")); + + request.setName("inner space"); + ve = request.validate(); + assertNull(ve); + + request.setName("_foo"); + ve = request.validate(); + assertNotNull(ve); + assertThat(ve.validationErrors().size(), is(1)); + assertThat(ve.validationErrors().get(0), containsString("name may not begin with an underscore")); + } + + public void testSerialization() throws IOException { + final String name = randomAlphaOfLengthBetween(1, 256); + final TimeValue expiration = randomBoolean() ? null : + TimeValue.parseTimeValue(randomTimeValue(), "test serialization of create api key"); + final WriteRequest.RefreshPolicy refreshPolicy = randomFrom(WriteRequest.RefreshPolicy.values()); + final int numDescriptors = randomIntBetween(0, 4); + final List descriptorList = new ArrayList<>(); + for (int i = 0; i < numDescriptors; i++) { + descriptorList.add(new RoleDescriptor("role_" + i, new String[] { "all" }, null, null)); + } + + final CreateApiKeyRequest request = new CreateApiKeyRequest(); + request.setName(name); + request.setExpiration(expiration); + + if (refreshPolicy != request.getRefreshPolicy() || randomBoolean()) { + request.setRefreshPolicy(refreshPolicy); + } + if (descriptorList.isEmpty() == false || randomBoolean()) { + request.setRoleDescriptors(descriptorList); + } + + try (BytesStreamOutput out = new BytesStreamOutput()) { + request.writeTo(out); + try (StreamInput in = out.bytes().streamInput()) { + final CreateApiKeyRequest serialized = new CreateApiKeyRequest(in); + assertEquals(name, serialized.getName()); + assertEquals(expiration, serialized.getExpiration()); + assertEquals(refreshPolicy, serialized.getRefreshPolicy()); + assertEquals(descriptorList, serialized.getRoleDescriptors()); + } + } + } +} diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/CreateApiKeyResponseTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/CreateApiKeyResponseTests.java new file mode 100644 index 0000000000000..20ff4bc251d15 --- /dev/null +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/CreateApiKeyResponseTests.java @@ -0,0 +1,81 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +package org.elasticsearch.xpack.core.security.action; + +import org.elasticsearch.common.UUIDs; +import org.elasticsearch.common.io.stream.BytesStreamOutput; +import org.elasticsearch.common.io.stream.StreamInput; +import org.elasticsearch.common.settings.SecureString; +import org.elasticsearch.common.xcontent.XContentParser; +import org.elasticsearch.test.AbstractXContentTestCase; +import org.elasticsearch.test.EqualsHashCodeTestUtils; + +import java.io.IOException; +import java.time.Instant; +import java.time.temporal.ChronoUnit; + +import static org.hamcrest.Matchers.equalTo; + +public class CreateApiKeyResponseTests extends AbstractXContentTestCase { + + @Override + protected CreateApiKeyResponse doParseInstance(XContentParser parser) throws IOException { + return CreateApiKeyResponse.fromXContent(parser); + } + + @Override + protected CreateApiKeyResponse createTestInstance() { + final String name = randomAlphaOfLengthBetween(1, 256); + final SecureString key = new SecureString(UUIDs.randomBase64UUID().toCharArray()); + final Instant expiration = randomBoolean() ? Instant.now().plus(7L, ChronoUnit.DAYS) : null; + final String id = randomAlphaOfLength(100); + return new CreateApiKeyResponse(name, id, key, expiration); + } + + @Override + protected boolean supportsUnknownFields() { + return false; + } + + public void testSerialization() throws IOException { + final CreateApiKeyResponse response = createTestInstance(); + try (BytesStreamOutput out = new BytesStreamOutput()) { + response.writeTo(out); + try (StreamInput in = out.bytes().streamInput()) { + CreateApiKeyResponse serialized = new CreateApiKeyResponse(in); + assertThat(serialized, equalTo(response)); + } + } + } + + public void testEqualsHashCode() { + CreateApiKeyResponse createApiKeyResponse = createTestInstance(); + + EqualsHashCodeTestUtils.checkEqualsAndHashCode(createApiKeyResponse, (original) -> { + return new CreateApiKeyResponse(original.getName(), original.getId(), original.getKey(), original.getExpiration()); + }); + EqualsHashCodeTestUtils.checkEqualsAndHashCode(createApiKeyResponse, (original) -> { + return new CreateApiKeyResponse(original.getName(), original.getId(), original.getKey(), original.getExpiration()); + }, CreateApiKeyResponseTests::mutateTestItem); + } + + private static CreateApiKeyResponse mutateTestItem(CreateApiKeyResponse original) { + switch (randomIntBetween(0, 3)) { + case 0: + return new CreateApiKeyResponse(randomAlphaOfLength(5), original.getId(), original.getKey(), original.getExpiration()); + case 1: + return new CreateApiKeyResponse(original.getName(), randomAlphaOfLength(5), original.getKey(), original.getExpiration()); + case 2: + return new CreateApiKeyResponse(original.getName(), original.getId(), new SecureString(UUIDs.randomBase64UUID().toCharArray()), + original.getExpiration()); + case 3: + return new CreateApiKeyResponse(original.getName(), original.getId(), original.getKey(), Instant.now()); + default: + return new CreateApiKeyResponse(randomAlphaOfLength(5), original.getId(), original.getKey(), original.getExpiration()); + } + } +} diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/GetApiKeyRequestTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/GetApiKeyRequestTests.java new file mode 100644 index 0000000000000..27be0d88eb82c --- /dev/null +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/GetApiKeyRequestTests.java @@ -0,0 +1,103 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +package org.elasticsearch.xpack.core.security.action; + +import org.elasticsearch.action.ActionRequest; +import org.elasticsearch.action.ActionRequestValidationException; +import org.elasticsearch.common.io.stream.InputStreamStreamInput; +import org.elasticsearch.common.io.stream.OutputStreamStreamOutput; +import org.elasticsearch.common.io.stream.StreamOutput; +import org.elasticsearch.test.ESTestCase; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; + +import static org.hamcrest.Matchers.containsInAnyOrder; + +public class GetApiKeyRequestTests extends ESTestCase { + + public void testRequestValidation() { + GetApiKeyRequest request = GetApiKeyRequest.usingApiKeyId(randomAlphaOfLength(5)); + ActionRequestValidationException ve = request.validate(); + assertNull(ve); + request = GetApiKeyRequest.usingApiKeyName(randomAlphaOfLength(5)); + ve = request.validate(); + assertNull(ve); + request = GetApiKeyRequest.usingRealmName(randomAlphaOfLength(5)); + ve = request.validate(); + assertNull(ve); + request = GetApiKeyRequest.usingUserName(randomAlphaOfLength(5)); + ve = request.validate(); + assertNull(ve); + request = GetApiKeyRequest.usingRealmAndUserName(randomAlphaOfLength(5), randomAlphaOfLength(7)); + ve = request.validate(); + assertNull(ve); + } + + public void testRequestValidationFailureScenarios() throws IOException { + class Dummy extends ActionRequest { + String realm; + String user; + String apiKeyId; + String apiKeyName; + + Dummy(String[] a) { + realm = a[0]; + user = a[1]; + apiKeyId = a[2]; + apiKeyName = a[3]; + } + + @Override + public ActionRequestValidationException validate() { + return null; + } + + @Override + public void writeTo(StreamOutput out) throws IOException { + super.writeTo(out); + out.writeOptionalString(realm); + out.writeOptionalString(user); + out.writeOptionalString(apiKeyId); + out.writeOptionalString(apiKeyName); + } + } + + String[][] inputs = new String[][] { + { randomFrom(new String[] { null, "" }), randomFrom(new String[] { null, "" }), randomFrom(new String[] { null, "" }), + randomFrom(new String[] { null, "" }) }, + { randomFrom(new String[] { null, "" }), "user", "api-kid", "api-kname" }, + { "realm", randomFrom(new String[] { null, "" }), "api-kid", "api-kname" }, + { "realm", "user", "api-kid", randomFrom(new String[] { null, "" }) }, + { randomFrom(new String[] { null, "" }), randomFrom(new String[] { null, "" }), "api-kid", "api-kname" } }; + String[][] expectedErrorMessages = new String[][] { { "One of [api key id, api key name, username, realm name] must be specified" }, + { "username or realm name must not be specified when the api key id or api key name is specified", + "only one of [api key id, api key name] can be specified" }, + { "username or realm name must not be specified when the api key id or api key name is specified", + "only one of [api key id, api key name] can be specified" }, + { "username or realm name must not be specified when the api key id or api key name is specified" }, + { "only one of [api key id, api key name] can be specified" } }; + + for (int caseNo = 0; caseNo < inputs.length; caseNo++) { + try (ByteArrayOutputStream bos = new ByteArrayOutputStream(); + OutputStreamStreamOutput osso = new OutputStreamStreamOutput(bos)) { + Dummy d = new Dummy(inputs[caseNo]); + d.writeTo(osso); + + ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray()); + InputStreamStreamInput issi = new InputStreamStreamInput(bis); + + GetApiKeyRequest request = new GetApiKeyRequest(issi); + ActionRequestValidationException ve = request.validate(); + assertNotNull(ve); + assertEquals(expectedErrorMessages[caseNo].length, ve.validationErrors().size()); + assertThat(ve.validationErrors(), containsInAnyOrder(expectedErrorMessages[caseNo])); + } + } + } +} diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/GetApiKeyResponseTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/GetApiKeyResponseTests.java new file mode 100644 index 0000000000000..c278c135edaf8 --- /dev/null +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/GetApiKeyResponseTests.java @@ -0,0 +1,64 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +package org.elasticsearch.xpack.core.security.action; + +import org.elasticsearch.common.Strings; +import org.elasticsearch.common.io.stream.BytesStreamOutput; +import org.elasticsearch.common.io.stream.StreamInput; +import org.elasticsearch.common.xcontent.ToXContent; +import org.elasticsearch.common.xcontent.XContentBuilder; +import org.elasticsearch.common.xcontent.XContentFactory; +import org.elasticsearch.test.ESTestCase; + +import java.io.IOException; +import java.time.Instant; +import java.util.Arrays; +import java.util.Collections; + +import static org.hamcrest.Matchers.equalTo; + +public class GetApiKeyResponseTests extends ESTestCase { + + public void testSerialization() throws IOException { + boolean withExpiration = randomBoolean(); + ApiKey apiKeyInfo = createApiKeyInfo(randomAlphaOfLength(4), randomAlphaOfLength(5), Instant.now(), + (withExpiration) ? Instant.now() : null, false, randomAlphaOfLength(4), randomAlphaOfLength(5)); + GetApiKeyResponse response = new GetApiKeyResponse(Collections.singletonList(apiKeyInfo)); + try (BytesStreamOutput output = new BytesStreamOutput()) { + response.writeTo(output); + try (StreamInput input = output.bytes().streamInput()) { + GetApiKeyResponse serialized = new GetApiKeyResponse(input); + assertThat(serialized.getApiKeyInfos(), equalTo(response.getApiKeyInfos())); + } + } + } + + public void testToXContent() throws IOException { + ApiKey apiKeyInfo1 = createApiKeyInfo("name1", "id-1", Instant.ofEpochMilli(100000L), Instant.ofEpochMilli(10000000L), false, + "user-a", "realm-x"); + ApiKey apiKeyInfo2 = createApiKeyInfo("name2", "id-2", Instant.ofEpochMilli(100000L), Instant.ofEpochMilli(10000000L), true, + "user-b", "realm-y"); + GetApiKeyResponse response = new GetApiKeyResponse(Arrays.asList(apiKeyInfo1, apiKeyInfo2)); + XContentBuilder builder = XContentFactory.jsonBuilder(); + response.toXContent(builder, ToXContent.EMPTY_PARAMS); + assertThat(Strings.toString(builder), equalTo( + "{" + + "\"api_keys\":[" + + "{\"id\":\"id-1\",\"name\":\"name1\",\"creation\":100000,\"expiration\":10000000,\"invalidated\":false," + + "\"username\":\"user-a\",\"realm\":\"realm-x\"}," + + "{\"id\":\"id-2\",\"name\":\"name2\",\"creation\":100000,\"expiration\":10000000,\"invalidated\":true," + + "\"username\":\"user-b\",\"realm\":\"realm-y\"}" + + "]" + + "}")); + } + + private ApiKey createApiKeyInfo(String name, String id, Instant creation, Instant expiration, boolean invalidated, String username, + String realm) { + return new ApiKey(name, id, creation, expiration, invalidated, username, realm); + } +} + diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/InvalidateApiKeyRequestTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/InvalidateApiKeyRequestTests.java new file mode 100644 index 0000000000000..3d7fd90234286 --- /dev/null +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/InvalidateApiKeyRequestTests.java @@ -0,0 +1,104 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +package org.elasticsearch.xpack.core.security.action; + +import org.elasticsearch.action.ActionRequest; +import org.elasticsearch.action.ActionRequestValidationException; +import org.elasticsearch.common.io.stream.InputStreamStreamInput; +import org.elasticsearch.common.io.stream.OutputStreamStreamOutput; +import org.elasticsearch.common.io.stream.StreamOutput; +import org.elasticsearch.test.ESTestCase; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; + +import static org.hamcrest.Matchers.containsInAnyOrder; + +public class InvalidateApiKeyRequestTests extends ESTestCase { + + public void testRequestValidation() { + InvalidateApiKeyRequest request = InvalidateApiKeyRequest.usingApiKeyId(randomAlphaOfLength(5)); + ActionRequestValidationException ve = request.validate(); + assertNull(ve); + request = InvalidateApiKeyRequest.usingApiKeyName(randomAlphaOfLength(5)); + ve = request.validate(); + assertNull(ve); + request = InvalidateApiKeyRequest.usingRealmName(randomAlphaOfLength(5)); + ve = request.validate(); + assertNull(ve); + request = InvalidateApiKeyRequest.usingUserName(randomAlphaOfLength(5)); + ve = request.validate(); + assertNull(ve); + request = InvalidateApiKeyRequest.usingRealmAndUserName(randomAlphaOfLength(5), randomAlphaOfLength(7)); + ve = request.validate(); + assertNull(ve); + } + + public void testRequestValidationFailureScenarios() throws IOException { + class Dummy extends ActionRequest { + String realm; + String user; + String apiKeyId; + String apiKeyName; + + Dummy(String[] a) { + realm = a[0]; + user = a[1]; + apiKeyId = a[2]; + apiKeyName = a[3]; + } + + @Override + public ActionRequestValidationException validate() { + return null; + } + + @Override + public void writeTo(StreamOutput out) throws IOException { + super.writeTo(out); + out.writeOptionalString(realm); + out.writeOptionalString(user); + out.writeOptionalString(apiKeyId); + out.writeOptionalString(apiKeyName); + } + } + + String[][] inputs = new String[][] { + { randomFrom(new String[] { null, "" }), randomFrom(new String[] { null, "" }), randomFrom(new String[] { null, "" }), + randomFrom(new String[] { null, "" }) }, + { randomFrom(new String[] { null, "" }), "user", "api-kid", "api-kname" }, + { "realm", randomFrom(new String[] { null, "" }), "api-kid", "api-kname" }, + { "realm", "user", "api-kid", randomFrom(new String[] { null, "" }) }, + { randomFrom(new String[] { null, "" }), randomFrom(new String[] { null, "" }), "api-kid", "api-kname" } }; + String[][] expectedErrorMessages = new String[][] { { "One of [api key id, api key name, username, realm name] must be specified" }, + { "username or realm name must not be specified when the api key id or api key name is specified", + "only one of [api key id, api key name] can be specified" }, + { "username or realm name must not be specified when the api key id or api key name is specified", + "only one of [api key id, api key name] can be specified" }, + { "username or realm name must not be specified when the api key id or api key name is specified" }, + { "only one of [api key id, api key name] can be specified" } }; + + + for (int caseNo = 0; caseNo < inputs.length; caseNo++) { + try (ByteArrayOutputStream bos = new ByteArrayOutputStream(); + OutputStreamStreamOutput osso = new OutputStreamStreamOutput(bos)) { + Dummy d = new Dummy(inputs[caseNo]); + d.writeTo(osso); + + ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray()); + InputStreamStreamInput issi = new InputStreamStreamInput(bis); + + InvalidateApiKeyRequest request = new InvalidateApiKeyRequest(issi); + ActionRequestValidationException ve = request.validate(); + assertNotNull(ve); + assertEquals(expectedErrorMessages[caseNo].length, ve.validationErrors().size()); + assertThat(ve.validationErrors(), containsInAnyOrder(expectedErrorMessages[caseNo])); + } + } + } +} diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/InvalidateApiKeyResponseTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/InvalidateApiKeyResponseTests.java new file mode 100644 index 0000000000000..f4606a4f20f1b --- /dev/null +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/InvalidateApiKeyResponseTests.java @@ -0,0 +1,88 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +package org.elasticsearch.xpack.core.security.action; + +import org.elasticsearch.ElasticsearchException; +import org.elasticsearch.common.Strings; +import org.elasticsearch.common.io.stream.BytesStreamOutput; +import org.elasticsearch.common.io.stream.StreamInput; +import org.elasticsearch.common.xcontent.ToXContent; +import org.elasticsearch.common.xcontent.XContentBuilder; +import org.elasticsearch.common.xcontent.XContentFactory; +import org.elasticsearch.test.ESTestCase; + +import java.io.IOException; +import java.util.Arrays; +import java.util.Collections; + +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.equalTo; + +public class InvalidateApiKeyResponseTests extends ESTestCase { + + public void testSerialization() throws IOException { + InvalidateApiKeyResponse response = new InvalidateApiKeyResponse(Arrays.asList("api-key-id-1"), + Arrays.asList("api-key-id-2", "api-key-id-3"), + Arrays.asList(new ElasticsearchException("error1"), + new ElasticsearchException("error2"))); + try (BytesStreamOutput output = new BytesStreamOutput()) { + response.writeTo(output); + try (StreamInput input = output.bytes().streamInput()) { + InvalidateApiKeyResponse serialized = new InvalidateApiKeyResponse(input); + assertThat(serialized.getInvalidatedApiKeys(), equalTo(response.getInvalidatedApiKeys())); + assertThat(serialized.getPreviouslyInvalidatedApiKeys(), + equalTo(response.getPreviouslyInvalidatedApiKeys())); + assertThat(serialized.getErrors().size(), equalTo(response.getErrors().size())); + assertThat(serialized.getErrors().get(0).toString(), containsString("error1")); + assertThat(serialized.getErrors().get(1).toString(), containsString("error2")); + } + } + + response = new InvalidateApiKeyResponse(Arrays.asList(generateRandomStringArray(20, 15, false)), + Arrays.asList(generateRandomStringArray(20, 15, false)), + Collections.emptyList()); + try (BytesStreamOutput output = new BytesStreamOutput()) { + response.writeTo(output); + try (StreamInput input = output.bytes().streamInput()) { + InvalidateApiKeyResponse serialized = new InvalidateApiKeyResponse(input); + assertThat(serialized.getInvalidatedApiKeys(), equalTo(response.getInvalidatedApiKeys())); + assertThat(serialized.getPreviouslyInvalidatedApiKeys(), + equalTo(response.getPreviouslyInvalidatedApiKeys())); + assertThat(serialized.getErrors().size(), equalTo(response.getErrors().size())); + } + } + } + + public void testToXContent() throws IOException { + InvalidateApiKeyResponse response = new InvalidateApiKeyResponse(Arrays.asList("api-key-id-1"), + Arrays.asList("api-key-id-2", "api-key-id-3"), + Arrays.asList(new ElasticsearchException("error1", new IllegalArgumentException("msg - 1")), + new ElasticsearchException("error2", new IllegalArgumentException("msg - 2")))); + XContentBuilder builder = XContentFactory.jsonBuilder(); + response.toXContent(builder, ToXContent.EMPTY_PARAMS); + assertThat(Strings.toString(builder), + equalTo("{" + + "\"invalidated_api_keys\":[\"api-key-id-1\"]," + + "\"previously_invalidated_api_keys\":[\"api-key-id-2\",\"api-key-id-3\"]," + + "\"error_count\":2," + + "\"error_details\":[" + + "{\"type\":\"exception\"," + + "\"reason\":\"error1\"," + + "\"caused_by\":{" + + "\"type\":\"illegal_argument_exception\"," + + "\"reason\":\"msg - 1\"}" + + "}," + + "{\"type\":\"exception\"," + + "\"reason\":\"error2\"," + + "\"caused_by\":" + + "{\"type\":\"illegal_argument_exception\"," + + "\"reason\":\"msg - 2\"}" + + "}" + + "]" + + "}")); + } +} diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/user/HasPrivilegesResponseTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/user/HasPrivilegesResponseTests.java index 0481e01e74ac3..a605917f01c2d 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/user/HasPrivilegesResponseTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/user/HasPrivilegesResponseTests.java @@ -18,6 +18,7 @@ import org.elasticsearch.common.xcontent.XContentType; import org.elasticsearch.protocol.AbstractHlrcStreamableXContentTestCase; import org.elasticsearch.test.VersionUtils; +import org.elasticsearch.xpack.core.security.authz.permission.ResourcePrivileges; import org.hamcrest.Matchers; import java.io.IOException; @@ -59,16 +60,17 @@ public void testSerializationV63() throws IOException { } public void testToXContent() throws Exception { - final HasPrivilegesResponse response = new HasPrivilegesResponse("daredevil", false, - Collections.singletonMap("manage", true), - Arrays.asList( - new HasPrivilegesResponse.ResourcePrivileges("staff", - MapBuilder.newMapBuilder(new LinkedHashMap<>()) - .put("read", true).put("index", true).put("delete", false).put("manage", false).map()), - new HasPrivilegesResponse.ResourcePrivileges("customers", - MapBuilder.newMapBuilder(new LinkedHashMap<>()) - .put("read", true).put("index", true).put("delete", true).put("manage", false).map()) - ), Collections.emptyMap()); + final HasPrivilegesResponse response = new HasPrivilegesResponse("daredevil", false, Collections.singletonMap("manage", true), + Arrays.asList( + ResourcePrivileges.builder("staff") + .addPrivileges(MapBuilder.newMapBuilder(new LinkedHashMap<>()).put("read", true) + .put("index", true).put("delete", false).put("manage", false).map()) + .build(), + ResourcePrivileges.builder("customers") + .addPrivileges(MapBuilder.newMapBuilder(new LinkedHashMap<>()).put("read", true) + .put("index", true).put("delete", true).put("manage", false).map()) + .build()), + Collections.emptyMap()); final XContentBuilder builder = XContentBuilder.builder(XContentType.JSON.xContent()); response.toXContent(builder, ToXContent.EMPTY_PARAMS); @@ -120,9 +122,9 @@ public HasPrivilegesResponse convertHlrcToInternal(org.elasticsearch.client.secu ); } - private static List toResourcePrivileges(Map> map) { + private static List toResourcePrivileges(Map> map) { return map.entrySet().stream() - .map(e -> new HasPrivilegesResponse.ResourcePrivileges(e.getKey(), e.getValue())) + .map(e -> ResourcePrivileges.builder(e.getKey()).addPrivileges(e.getValue()).build()) .collect(Collectors.toList()); } @@ -146,23 +148,23 @@ private HasPrivilegesResponse randomResponse() { for (String priv : randomArray(1, 6, String[]::new, () -> randomAlphaOfLengthBetween(3, 12))) { cluster.put(priv, randomBoolean()); } - final Collection index = randomResourcePrivileges(); - final Map> application = new HashMap<>(); + final Collection index = randomResourcePrivileges(); + final Map> application = new HashMap<>(); for (String app : randomArray(1, 3, String[]::new, () -> randomAlphaOfLengthBetween(3, 6).toLowerCase(Locale.ROOT))) { application.put(app, randomResourcePrivileges()); } return new HasPrivilegesResponse(username, randomBoolean(), cluster, index, application); } - private Collection randomResourcePrivileges() { - final Collection list = new ArrayList<>(); + private Collection randomResourcePrivileges() { + final Collection list = new ArrayList<>(); // Use hash set to force a unique set of resources for (String resource : Sets.newHashSet(randomArray(1, 3, String[]::new, () -> randomAlphaOfLengthBetween(2, 6)))) { final Map privileges = new HashMap<>(); for (String priv : randomArray(1, 5, String[]::new, () -> randomAlphaOfLengthBetween(3, 8))) { privileges.put(priv, randomBoolean()); } - list.add(new HasPrivilegesResponse.ResourcePrivileges(resource, privileges)); + list.add(ResourcePrivileges.builder(resource).addPrivileges(privileges).build()); } return list; } diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authc/DefaultAuthenticationFailureHandlerTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authc/DefaultAuthenticationFailureHandlerTests.java index 15593f0b82ea5..24f9d16324f2d 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authc/DefaultAuthenticationFailureHandlerTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authc/DefaultAuthenticationFailureHandlerTests.java @@ -111,8 +111,9 @@ public void testSortsWWWAuthenticateHeaderValues() { final String basicAuthScheme = "Basic realm=\"" + XPackField.SECURITY + "\" charset=\"UTF-8\""; final String bearerAuthScheme = "Bearer realm=\"" + XPackField.SECURITY + "\""; final String negotiateAuthScheme = randomFrom("Negotiate", "Negotiate Ijoijksdk"); + final String apiKeyAuthScheme = "ApiKey"; final Map> failureResponeHeaders = new HashMap<>(); - final List supportedSchemes = Arrays.asList(basicAuthScheme, bearerAuthScheme, negotiateAuthScheme); + final List supportedSchemes = Arrays.asList(basicAuthScheme, bearerAuthScheme, negotiateAuthScheme, apiKeyAuthScheme); Collections.shuffle(supportedSchemes, random()); failureResponeHeaders.put("WWW-Authenticate", supportedSchemes); final DefaultAuthenticationFailureHandler failuerHandler = new DefaultAuthenticationFailureHandler(failureResponeHeaders); @@ -123,7 +124,7 @@ public void testSortsWWWAuthenticateHeaderValues() { assertThat(ese, is(notNullValue())); assertThat(ese.getHeader("WWW-Authenticate"), is(notNullValue())); assertThat(ese.getMessage(), equalTo("error attempting to authenticate request")); - assertWWWAuthenticateWithSchemes(ese, negotiateAuthScheme, bearerAuthScheme, basicAuthScheme); + assertWWWAuthenticateWithSchemes(ese, negotiateAuthScheme, bearerAuthScheme, apiKeyAuthScheme, basicAuthScheme); } private void assertWWWAuthenticateWithSchemes(final ElasticsearchSecurityException ese, final String... schemes) { diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/accesscontrol/SecurityIndexSearcherWrapperIntegrationTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/accesscontrol/SecurityIndexSearcherWrapperIntegrationTests.java index 72b6c5b628ee7..2b6c269018fae 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/accesscontrol/SecurityIndexSearcherWrapperIntegrationTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/accesscontrol/SecurityIndexSearcherWrapperIntegrationTests.java @@ -8,6 +8,7 @@ import org.apache.lucene.analysis.standard.StandardAnalyzer; import org.apache.lucene.document.Document; import org.apache.lucene.document.Field; +import org.apache.lucene.document.Field.Store; import org.apache.lucene.document.StringField; import org.apache.lucene.index.DirectoryReader; import org.apache.lucene.index.IndexWriter; @@ -16,12 +17,14 @@ import org.apache.lucene.index.Term; import org.apache.lucene.search.IndexSearcher; import org.apache.lucene.search.MatchAllDocsQuery; +import org.apache.lucene.search.ScoreDoc; import org.apache.lucene.search.TermQuery; import org.apache.lucene.search.TotalHitCountCollector; import org.apache.lucene.store.Directory; import org.apache.lucene.util.Accountable; import org.elasticsearch.client.Client; import org.elasticsearch.common.bytes.BytesArray; +import org.elasticsearch.common.bytes.BytesReference; import org.elasticsearch.common.lucene.index.ElasticsearchDirectoryReader; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.util.concurrent.ThreadContext; @@ -36,14 +39,21 @@ import org.elasticsearch.index.shard.ShardId; import org.elasticsearch.license.XPackLicenseState; import org.elasticsearch.script.ScriptService; -import org.elasticsearch.test.ESTestCase; +import org.elasticsearch.test.AbstractBuilderTestCase; import org.elasticsearch.test.IndexSettingsModule; +import org.elasticsearch.xpack.core.security.authc.Authentication; +import org.elasticsearch.xpack.core.security.authc.AuthenticationField; +import org.elasticsearch.xpack.core.security.authz.permission.DocumentPermissions; import org.elasticsearch.xpack.core.security.authz.permission.FieldPermissions; +import org.elasticsearch.xpack.core.security.user.User; import java.util.Collections; +import java.util.HashSet; +import java.util.Set; import static java.util.Collections.singleton; import static java.util.Collections.singletonMap; +import static org.hamcrest.Matchers.containsInAnyOrder; import static org.hamcrest.Matchers.equalTo; import static org.mockito.Matchers.any; import static org.mockito.Matchers.anyBoolean; @@ -53,7 +63,7 @@ import static org.mockito.Mockito.spy; import static org.mockito.Mockito.when; -public class SecurityIndexSearcherWrapperIntegrationTests extends ESTestCase { +public class SecurityIndexSearcherWrapperIntegrationTests extends AbstractBuilderTestCase { public void testDLS() throws Exception { ShardId shardId = new ShardId("_index", "_na_", 0); @@ -64,9 +74,12 @@ public void testDLS() throws Exception { .then(invocationOnMock -> Collections.singletonList((String) invocationOnMock.getArguments()[0])); ThreadContext threadContext = new ThreadContext(Settings.EMPTY); + final Authentication authentication = mock(Authentication.class); + when(authentication.getUser()).thenReturn(mock(User.class)); + threadContext.putTransient(AuthenticationField.AUTHENTICATION_KEY, authentication); IndicesAccessControl.IndexAccessControl indexAccessControl = new IndicesAccessControl.IndexAccessControl(true, new FieldPermissions(), - singleton(new BytesArray("{\"match_all\" : {}}"))); + DocumentPermissions.filteredBy(singleton(new BytesArray("{\"match_all\" : {}}")))); IndexSettings indexSettings = IndexSettingsModule.newIndexSettings(shardId.getIndex(), Settings.EMPTY); Client client = mock(Client.class); when(client.settings()).thenReturn(Settings.EMPTY); @@ -159,4 +172,116 @@ protected IndicesAccessControl getIndicesAccessControl() { directoryReader.close(); directory.close(); } + + public void testDLSWithLimitedPermissions() throws Exception { + ShardId shardId = new ShardId("_index", "_na_", 0); + MapperService mapperService = mock(MapperService.class); + ScriptService scriptService = mock(ScriptService.class); + when(mapperService.documentMapper(any(String.class))).thenReturn(null); + when(mapperService.simpleMatchToFullName(anyString())) + .then(invocationOnMock -> Collections.singletonList((String) invocationOnMock.getArguments()[0])); + + ThreadContext threadContext = new ThreadContext(Settings.EMPTY); + final Authentication authentication = mock(Authentication.class); + when(authentication.getUser()).thenReturn(mock(User.class)); + threadContext.putTransient(AuthenticationField.AUTHENTICATION_KEY, authentication); + final boolean noFilteredIndexPermissions = randomBoolean(); + boolean restrictiveLimitedIndexPermissions = false; + if (noFilteredIndexPermissions == false) { + restrictiveLimitedIndexPermissions = randomBoolean(); + } + Set queries = new HashSet<>(); + queries.add(new BytesArray("{\"terms\" : { \"f2\" : [\"fv22\"] } }")); + queries.add(new BytesArray("{\"terms\" : { \"f2\" : [\"fv32\"] } }")); + IndicesAccessControl.IndexAccessControl indexAccessControl = new IndicesAccessControl.IndexAccessControl(true, new + FieldPermissions(), + DocumentPermissions.filteredBy(queries)); + queries = singleton(new BytesArray("{\"terms\" : { \"f1\" : [\"fv11\", \"fv21\", \"fv31\"] } }")); + if (restrictiveLimitedIndexPermissions) { + queries = singleton(new BytesArray("{\"terms\" : { \"f1\" : [\"fv11\", \"fv31\"] } }")); + } + IndicesAccessControl.IndexAccessControl limitedIndexAccessControl = new IndicesAccessControl.IndexAccessControl(true, new + FieldPermissions(), + DocumentPermissions.filteredBy(queries)); + IndexSettings indexSettings = IndexSettingsModule.newIndexSettings(shardId.getIndex(), Settings.EMPTY); + Client client = mock(Client.class); + when(client.settings()).thenReturn(Settings.EMPTY); + final long nowInMillis = randomNonNegativeLong(); + QueryShardContext realQueryShardContext = new QueryShardContext(shardId.id(), indexSettings, null, null, mapperService, null, + null, xContentRegistry(), writableRegistry(), client, null, () -> nowInMillis, null); + QueryShardContext queryShardContext = spy(realQueryShardContext); + IndexSettings settings = IndexSettingsModule.newIndexSettings("_index", Settings.EMPTY); + BitsetFilterCache bitsetFilterCache = new BitsetFilterCache(settings, new BitsetFilterCache.Listener() { + @Override + public void onCache(ShardId shardId, Accountable accountable) { + } + + @Override + public void onRemoval(ShardId shardId, Accountable accountable) { + } + }); + + XPackLicenseState licenseState = mock(XPackLicenseState.class); + when(licenseState.isDocumentAndFieldLevelSecurityAllowed()).thenReturn(true); + SecurityIndexSearcherWrapper wrapper = new SecurityIndexSearcherWrapper(s -> queryShardContext, + bitsetFilterCache, threadContext, licenseState, scriptService) { + + @Override + protected IndicesAccessControl getIndicesAccessControl() { + IndicesAccessControl indicesAccessControl = new IndicesAccessControl(true, singletonMap("_index", indexAccessControl)); + if (noFilteredIndexPermissions) { + return indicesAccessControl; + } + IndicesAccessControl limitedByIndicesAccessControl = new IndicesAccessControl(true, + singletonMap("_index", limitedIndexAccessControl)); + return indicesAccessControl.limitIndicesAccessControl(limitedByIndicesAccessControl); + } + }; + + Directory directory = newDirectory(); + IndexWriter iw = new IndexWriter( + directory, + new IndexWriterConfig(new StandardAnalyzer()).setMergePolicy(NoMergePolicy.INSTANCE) + ); + + Document doc1 = new Document(); + doc1.add(new StringField("f1", "fv11", Store.NO)); + doc1.add(new StringField("f2", "fv12", Store.NO)); + iw.addDocument(doc1); + Document doc2 = new Document(); + doc2.add(new StringField("f1", "fv21", Store.NO)); + doc2.add(new StringField("f2", "fv22", Store.NO)); + iw.addDocument(doc2); + Document doc3 = new Document(); + doc3.add(new StringField("f1", "fv31", Store.NO)); + doc3.add(new StringField("f2", "fv32", Store.NO)); + iw.addDocument(doc3); + iw.commit(); + iw.close(); + + DirectoryReader directoryReader = ElasticsearchDirectoryReader.wrap(DirectoryReader.open(directory), shardId); + DirectoryReader wrappedDirectoryReader = wrapper.wrap(directoryReader); + IndexSearcher indexSearcher = wrapper.wrap(new IndexSearcher(wrappedDirectoryReader)); + + ScoreDoc[] hits = indexSearcher.search(new MatchAllDocsQuery(), 1000).scoreDocs; + Set actualDocIds = new HashSet<>(); + for (ScoreDoc doc : hits) { + actualDocIds.add(doc.doc); + } + + if (noFilteredIndexPermissions) { + assertThat(actualDocIds, containsInAnyOrder(1, 2)); + } else { + if (restrictiveLimitedIndexPermissions) { + assertThat(actualDocIds, containsInAnyOrder(2)); + } else { + assertThat(actualDocIds, containsInAnyOrder(1, 2)); + } + } + + bitsetFilterCache.close(); + directoryReader.close(); + directory.close(); + } + } diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/accesscontrol/SecurityIndexSearcherWrapperUnitTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/accesscontrol/SecurityIndexSearcherWrapperUnitTests.java index 078de0fe7cb14..8747b8537ca21 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/accesscontrol/SecurityIndexSearcherWrapperUnitTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/accesscontrol/SecurityIndexSearcherWrapperUnitTests.java @@ -28,21 +28,16 @@ import org.apache.lucene.search.Scorer; import org.apache.lucene.search.TermQuery; import org.apache.lucene.search.Weight; -import org.apache.lucene.search.join.ScoreMode; import org.apache.lucene.store.Directory; -import org.apache.lucene.store.RAMDirectory; +import org.apache.lucene.store.MMapDirectory; import org.apache.lucene.util.Accountable; import org.apache.lucene.util.BitSet; import org.apache.lucene.util.FixedBitSet; -import org.elasticsearch.core.internal.io.IOUtils; import org.apache.lucene.util.SparseFixedBitSet; -import org.elasticsearch.client.Client; -import org.elasticsearch.common.Strings; import org.elasticsearch.common.lucene.index.ElasticsearchDirectoryReader; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.util.concurrent.ThreadContext; -import org.elasticsearch.common.xcontent.ToXContent; -import org.elasticsearch.common.xcontent.XContentBuilder; +import org.elasticsearch.core.internal.io.IOUtils; import org.elasticsearch.index.Index; import org.elasticsearch.index.IndexSettings; import org.elasticsearch.index.cache.bitset.BitsetFilterCache; @@ -52,59 +47,35 @@ import org.elasticsearch.index.mapper.ParentFieldMapper; import org.elasticsearch.index.mapper.SeqNoFieldMapper; import org.elasticsearch.index.mapper.SourceFieldMapper; -import org.elasticsearch.index.query.BoolQueryBuilder; -import org.elasticsearch.index.query.BoostingQueryBuilder; -import org.elasticsearch.index.query.ConstantScoreQueryBuilder; -import org.elasticsearch.index.query.GeoShapeQueryBuilder; -import org.elasticsearch.index.query.MatchAllQueryBuilder; -import org.elasticsearch.index.query.QueryBuilder; -import org.elasticsearch.index.query.QueryRewriteContext; -import org.elasticsearch.index.query.TermQueryBuilder; -import org.elasticsearch.index.query.TermsQueryBuilder; -import org.elasticsearch.index.query.functionscore.FunctionScoreQueryBuilder; import org.elasticsearch.index.shard.IndexShard; import org.elasticsearch.index.shard.ShardId; -import org.elasticsearch.indices.TermsLookup; -import org.elasticsearch.join.query.HasChildQueryBuilder; -import org.elasticsearch.join.query.HasParentQueryBuilder; import org.elasticsearch.license.XPackLicenseState; -import org.elasticsearch.script.Script; import org.elasticsearch.script.ScriptService; -import org.elasticsearch.script.ScriptType; -import org.elasticsearch.script.TemplateScript; import org.elasticsearch.search.aggregations.LeafBucketCollector; import org.elasticsearch.test.ESTestCase; import org.elasticsearch.test.IndexSettingsModule; import org.elasticsearch.xpack.core.security.authz.accesscontrol.DocumentSubsetReader.DocumentSubsetDirectoryReader; +import org.elasticsearch.xpack.core.security.authz.permission.DocumentPermissions; import org.elasticsearch.xpack.core.security.authz.permission.FieldPermissions; import org.elasticsearch.xpack.core.security.authz.permission.FieldPermissionsDefinition; -import org.elasticsearch.xpack.core.security.user.User; import org.junit.After; import org.junit.Before; -import org.mockito.ArgumentCaptor; import java.io.IOException; import java.util.Arrays; import java.util.Collections; -import java.util.HashMap; import java.util.HashSet; import java.util.IdentityHashMap; -import java.util.Map; import java.util.Set; import static java.util.Collections.singletonMap; -import static org.elasticsearch.common.xcontent.XContentFactory.jsonBuilder; import static org.elasticsearch.xpack.core.security.authz.accesscontrol.SecurityIndexSearcherWrapper.intersectScorerAndRoleBits; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.instanceOf; import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.not; import static org.hamcrest.Matchers.sameInstance; -import static org.mockito.Matchers.any; -import static org.mockito.Matchers.eq; import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.verifyZeroInteractions; import static org.mockito.Mockito.when; public class SecurityIndexSearcherWrapperUnitTests extends ESTestCase { @@ -139,7 +110,7 @@ public void setup() throws Exception { IndexShard indexShard = mock(IndexShard.class); when(indexShard.shardId()).thenReturn(shardId); - Directory directory = new RAMDirectory(); + Directory directory = new MMapDirectory(createTempDir()); IndexWriter writer = new IndexWriter(directory, newIndexWriterConfig()); writer.close(); @@ -159,7 +130,7 @@ public void testDefaultMetaFields() throws Exception { @Override protected IndicesAccessControl getIndicesAccessControl() { IndicesAccessControl.IndexAccessControl indexAccessControl = new IndicesAccessControl.IndexAccessControl(true, - new FieldPermissions(fieldPermissionDef(new String[]{}, null)), null); + new FieldPermissions(fieldPermissionDef(new String[]{}, null)), DocumentPermissions.allowAll()); return new IndicesAccessControl(true, singletonMap("_index", indexAccessControl)); } }; @@ -444,66 +415,6 @@ public void testIndexSearcherWrapperDenseWithDeletions() throws IOException { doTestIndexSearcherWrapper(false, true); } - public void testTemplating() throws Exception { - User user = new User("_username", new String[]{"role1", "role2"}, "_full_name", "_email", - Collections.singletonMap("key", "value"), true); - securityIndexSearcherWrapper = - new SecurityIndexSearcherWrapper(null, null, threadContext, licenseState, scriptService) { - - @Override - protected User getUser() { - return user; - } - }; - - TemplateScript.Factory compiledTemplate = templateParams -> - new TemplateScript(templateParams) { - @Override - public String execute() { - return "rendered_text"; - } - }; - - when(scriptService.compile(any(Script.class), eq(TemplateScript.CONTEXT))).thenReturn(compiledTemplate); - - XContentBuilder builder = jsonBuilder(); - String query = Strings.toString(new TermQueryBuilder("field", "{{_user.username}}").toXContent(builder, ToXContent.EMPTY_PARAMS)); - Script script = new Script(ScriptType.INLINE, "mustache", query, Collections.singletonMap("custom", "value")); - builder = jsonBuilder().startObject().field("template"); - script.toXContent(builder, ToXContent.EMPTY_PARAMS); - String querySource = Strings.toString(builder.endObject()); - - securityIndexSearcherWrapper.evaluateTemplate(querySource); - ArgumentCaptor