Merge branch 'master' into index-lifecycle

This commit is contained in:
Tal Levy 2018-07-24 11:23:44 -07:00
commit e523030670
258 changed files with 12865 additions and 1323 deletions

View File

@ -214,7 +214,7 @@ If your changes affect only the documentation, run:
```sh
./gradlew -p docs check
```
For more information about testing code examples in the documentation, see
For more information about testing code examples in the documentation, see
https://github.com/elastic/elasticsearch/blob/master/docs/README.asciidoc
### Project layout
@ -305,6 +305,39 @@ the `qa` subdirectory functions just like the top level `qa` subdirectory. The
Elasticsearch process. The `transport-client` subdirectory contains extensions
to Elasticsearch's standard transport client to work properly with x-pack.
### Gradle Build
We use Gradle to build Elasticsearch because it is flexible enough to not only
build and package Elasticsearch, but also orchestrate all of the ways that we
have to test Elasticsearch.
#### Configurations
Gradle organizes dependencies and build artifacts into "configurations" and
allows you to use these configurations arbitrarilly. Here are some of the most
common configurations in our build and how we use them:
<dl>
<dt>`compile`</dt><dd>Code that is on the classpath at both compile and
runtime. If the [`shadow`][shadow-plugin] plugin is applied to the project then
this code is bundled into the jar produced by the project.</dd>
<dt>`runtime`</dt><dd>Code that is not on the classpath at compile time but is
on the classpath at runtime. We mostly use this configuration to make sure that
we do not accidentally compile against dependencies of our dependencies also
known as "transitive" dependencies".</dd>
<dt>`compileOnly`</dt><dd>Code that is on the classpath at comile time but that
should not be shipped with the project because it is "provided" by the runtime
somehow. Elasticsearch plugins use this configuration to include dependencies
that are bundled with Elasticsearch's server.</dd>
<dt>`shadow`</dt><dd>Only available in projects with the shadow plugin. Code
that is on the classpath at both compile and runtime but it *not* bundled into
the jar produced by the project. If you depend on a project with the `shadow`
plugin then you need to depend on this configuration because it will bring
along all of the dependencies you need at runtime.</dd>
<dt>`testCompile`</dt><dd>Code that is on the classpath for compiling tests
that are part of this project but not production code. The canonical example
of this is `junit`.</dd>
</dl>
Contributing as part of a class
-------------------------------
@ -337,3 +370,4 @@ repeating in this section because it has come up in this context.
[eclipse]: http://www.eclipse.org/community/eclipse_newsletter/2017/june/
[intellij]: https://blog.jetbrains.com/idea/2017/07/intellij-idea-2017-2-is-here-smart-sleek-and-snappy/
[shadow-plugin]: https://github.com/johnrengelman/shadow

View File

@ -516,6 +516,31 @@ allprojects {
tasks.eclipse.dependsOn(cleanEclipse, copyEclipseSettings)
}
allprojects {
/*
* IntelliJ and Eclipse don't know about the shadow plugin so when we're
* in "IntelliJ mode" or "Eclipse mode" add "runtime" dependencies
* eveywhere where we see a "shadow" dependency which will cause them to
* reference shadowed projects directly rather than rely on the shadowing
* to include them. This is the correct thing for it to do because it
* doesn't run the jar shadowing at all. This isn't needed for the project
* itself because the IDE configuration is done by SourceSets but it is
* *is* needed for projects that depends on the project doing the shadowing.
* Without this they won't properly depend on the shadowed project.
*/
if (isEclipse || isIdea) {
configurations.all { Configuration configuration ->
dependencies.all { Dependency dep ->
if (dep instanceof ProjectDependency) {
if (dep.getTargetConfiguration() == 'shadow') {
configuration.dependencies.add(project.dependencies.project(path: dep.dependencyProject.path, configuration: 'runtime'))
}
}
}
}
}
}
// we need to add the same --debug-jvm option as
// the real RunTask has, so we can pass it through
class Run extends DefaultTask {

View File

@ -391,6 +391,9 @@ class BuildPlugin implements Plugin<Project> {
project.configurations.compile.dependencies.all(disableTransitiveDeps)
project.configurations.testCompile.dependencies.all(disableTransitiveDeps)
project.configurations.compileOnly.dependencies.all(disableTransitiveDeps)
project.plugins.withType(ShadowPlugin).whenPluginAdded {
project.configurations.shadow.dependencies.all(disableTransitiveDeps)
}
}
/** Adds repositories used by ES dependencies */
@ -882,11 +885,20 @@ class BuildPlugin implements Plugin<Project> {
project.dependencyLicenses.dependencies = project.configurations.runtime.fileCollection {
it.group.startsWith('org.elasticsearch') == false
} - project.configurations.compileOnly
project.plugins.withType(ShadowPlugin).whenPluginAdded {
project.dependencyLicenses.dependencies += project.configurations.shadow.fileCollection {
it.group.startsWith('org.elasticsearch') == false
}
}
}
private static configureDependenciesInfo(Project project) {
Task deps = project.tasks.create("dependenciesInfo", DependenciesInfoTask.class)
deps.runtimeConfiguration = project.configurations.runtime
project.plugins.withType(ShadowPlugin).whenPluginAdded {
deps.runtimeConfiguration = project.configurations.create('infoDeps')
deps.runtimeConfiguration.extendsFrom(project.configurations.runtime, project.configurations.shadow)
}
deps.compileOnlyConfiguration = project.configurations.compileOnly
project.afterEvaluate {
deps.mappings = project.dependencyLicenses.mappings

View File

@ -48,18 +48,6 @@ public class PluginBuildPlugin extends BuildPlugin {
@Override
public void apply(Project project) {
super.apply(project)
project.plugins.withType(ShadowPlugin).whenPluginAdded {
/*
* We've not tested these plugins together and we're fairly sure
* they aren't going to work properly as is *and* we're not really
* sure *why* you'd want to shade stuff in plugins. So we throw an
* exception here to make you come and read this comment. If you
* have a need for shadow while building plugins then know that you
* are probably going to have to fight with gradle for a while....
*/
throw new InvalidUserDataException('elasticsearch.esplugin is not '
+ 'compatible with com.github.johnrengelman.shadow');
}
configureDependencies(project)
// this afterEvaluate must happen before the afterEvaluate added by integTest creation,
// so that the file name resolution for installing the plugin will be setup
@ -153,8 +141,13 @@ public class PluginBuildPlugin extends BuildPlugin {
include(buildProperties.descriptorOutput.name)
}
from pluginMetadata // metadata (eg custom security policy)
from project.jar // this plugin's jar
from project.configurations.runtime - project.configurations.compileOnly // the dep jars
/*
* If the plugin is using the shadow plugin then we need to bundle
* "shadow" things rather than the default jar and dependencies so
* we don't hit jar hell.
*/
from { project.plugins.hasPlugin(ShadowPlugin) ? project.shadowJar : project.jar }
from { project.plugins.hasPlugin(ShadowPlugin) ? project.configurations.shadow : project.configurations.runtime - project.configurations.compileOnly }
// extra files for the plugin to go into the zip
from('src/main/packaging') // TODO: move all config/bin/_size/etc into packaging
from('src/main') {

View File

@ -0,0 +1,66 @@
/*
* 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;
import org.elasticsearch.action.ActionListener;
import org.elasticsearch.protocol.xpack.license.PutLicenseRequest;
import org.elasticsearch.protocol.xpack.license.PutLicenseResponse;
import java.io.IOException;
import static java.util.Collections.emptySet;
/**
* A wrapper for the {@link RestHighLevelClient} that provides methods for
* accessing the Elastic License-related methods
* <p>
* See the <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/licensing-apis.html">
* X-Pack Licensing APIs on elastic.co</a> for more information.
*/
public class LicenseClient {
private final RestHighLevelClient restHighLevelClient;
LicenseClient(RestHighLevelClient restHighLevelClient) {
this.restHighLevelClient = restHighLevelClient;
}
/**
* Updates license for the cluster.
* @param options the request options (e.g. headers), use {@link RequestOptions#DEFAULT} if nothing needs to be customized
* @return the response
* @throws IOException in case there is a problem sending the request or parsing back the response
*/
public PutLicenseResponse putLicense(PutLicenseRequest request, RequestOptions options) throws IOException {
return restHighLevelClient.performRequestAndParseEntity(request, RequestConverters::putLicense, options,
PutLicenseResponse::fromXContent, emptySet());
}
/**
* Asynchronously updates license for the cluster cluster.
* @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 putLicenseAsync(PutLicenseRequest request, RequestOptions options, ActionListener<PutLicenseResponse> listener) {
restHighLevelClient.performRequestAsyncAndParseEntity(request, RequestConverters::putLicense, options,
PutLicenseResponse::fromXContent, listener, emptySet());
}
}

View File

@ -40,6 +40,7 @@ import org.elasticsearch.action.admin.cluster.settings.ClusterGetSettingsRequest
import org.elasticsearch.action.admin.cluster.settings.ClusterUpdateSettingsRequest;
import org.elasticsearch.action.admin.cluster.snapshots.create.CreateSnapshotRequest;
import org.elasticsearch.action.admin.cluster.snapshots.get.GetSnapshotsRequest;
import org.elasticsearch.action.admin.cluster.snapshots.restore.RestoreSnapshotRequest;
import org.elasticsearch.action.admin.cluster.storedscripts.DeleteStoredScriptRequest;
import org.elasticsearch.action.admin.cluster.storedscripts.GetStoredScriptRequest;
import org.elasticsearch.action.admin.cluster.snapshots.delete.DeleteSnapshotRequest;
@ -108,6 +109,7 @@ import org.elasticsearch.index.rankeval.RankEvalRequest;
import org.elasticsearch.protocol.xpack.XPackInfoRequest;
import org.elasticsearch.protocol.xpack.watcher.PutWatchRequest;
import org.elasticsearch.protocol.xpack.XPackUsageRequest;
import org.elasticsearch.protocol.xpack.license.PutLicenseRequest;
import org.elasticsearch.rest.action.search.RestSearchAction;
import org.elasticsearch.script.mustache.MultiSearchTemplateRequest;
import org.elasticsearch.script.mustache.SearchTemplateRequest;
@ -980,6 +982,20 @@ final class RequestConverters {
return request;
}
static Request restoreSnapshot(RestoreSnapshotRequest restoreSnapshotRequest) throws IOException {
String endpoint = new EndpointBuilder().addPathPartAsIs("_snapshot")
.addPathPart(restoreSnapshotRequest.repository())
.addPathPart(restoreSnapshotRequest.snapshot())
.addPathPartAsIs("_restore")
.build();
Request request = new Request(HttpPost.METHOD_NAME, endpoint);
Params parameters = new Params(request);
parameters.withMasterTimeout(restoreSnapshotRequest.masterNodeTimeout());
parameters.withWaitForCompletion(restoreSnapshotRequest.waitForCompletion());
request.setEntity(createEntity(restoreSnapshotRequest, REQUEST_BODY_CONTENT_TYPE));
return request;
}
static Request deleteSnapshot(DeleteSnapshotRequest deleteSnapshotRequest) {
String endpoint = new EndpointBuilder().addPathPartAsIs("_snapshot")
.addPathPart(deleteSnapshotRequest.repository())
@ -1124,6 +1140,18 @@ final class RequestConverters {
return request;
}
static Request putLicense(PutLicenseRequest putLicenseRequest) {
Request request = new Request(HttpPut.METHOD_NAME, "/_xpack/license");
Params parameters = new Params(request);
parameters.withTimeout(putLicenseRequest.timeout());
parameters.withMasterTimeout(putLicenseRequest.masterNodeTimeout());
if (putLicenseRequest.isAcknowledge()) {
parameters.putParam("acknowledge", "true");
}
request.setJsonEntity(putLicenseRequest.getLicenseDefinition());
return request;
}
private static HttpEntity createEntity(ToXContent toXContent, XContentType xContentType) throws IOException {
BytesRef source = XContentHelper.toXContent(toXContent, xContentType, false).toBytesRef();
return new ByteArrayEntity(source.bytes, source.offset, source.length, createContentType(xContentType));

View File

@ -30,6 +30,8 @@ import org.elasticsearch.action.admin.cluster.repositories.verify.VerifyReposito
import org.elasticsearch.action.admin.cluster.repositories.verify.VerifyRepositoryResponse;
import org.elasticsearch.action.admin.cluster.snapshots.create.CreateSnapshotRequest;
import org.elasticsearch.action.admin.cluster.snapshots.create.CreateSnapshotResponse;
import org.elasticsearch.action.admin.cluster.snapshots.restore.RestoreSnapshotRequest;
import org.elasticsearch.action.admin.cluster.snapshots.restore.RestoreSnapshotResponse;
import org.elasticsearch.action.admin.cluster.snapshots.status.SnapshotsStatusRequest;
import org.elasticsearch.action.admin.cluster.snapshots.status.SnapshotsStatusResponse;
import org.elasticsearch.action.admin.cluster.snapshots.delete.DeleteSnapshotRequest;
@ -252,6 +254,36 @@ public final class SnapshotClient {
SnapshotsStatusResponse::fromXContent, listener, emptySet());
}
/**
* Restores a snapshot.
* See <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/modules-snapshots.html"> Snapshot and Restore
* API on elastic.co</a>
*
* @param restoreSnapshotRequest the request
* @param options the request options (e.g. headers), use {@link RequestOptions#DEFAULT} if nothing needs to be customized
* @return the response
* @throws IOException in case there is a problem sending the request or parsing back the response
*/
public RestoreSnapshotResponse restore(RestoreSnapshotRequest restoreSnapshotRequest, RequestOptions options) throws IOException {
return restHighLevelClient.performRequestAndParseEntity(restoreSnapshotRequest, RequestConverters::restoreSnapshot, options,
RestoreSnapshotResponse::fromXContent, emptySet());
}
/**
* Asynchronously restores a snapshot.
* See <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/modules-snapshots.html"> Snapshot and Restore
* API on elastic.co</a>
*
* @param restoreSnapshotRequest the request
* @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 restoreAsync(RestoreSnapshotRequest restoreSnapshotRequest, RequestOptions options,
ActionListener<RestoreSnapshotResponse> listener) {
restHighLevelClient.performRequestAsyncAndParseEntity(restoreSnapshotRequest, RequestConverters::restoreSnapshot, options,
RestoreSnapshotResponse::fromXContent, listener, emptySet());
}
/**
* Deletes a snapshot.
* See <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/modules-snapshots.html"> Snapshot and Restore

View File

@ -42,10 +42,12 @@ public final class XPackClient {
private final RestHighLevelClient restHighLevelClient;
private final WatcherClient watcherClient;
private final LicenseClient licenseClient;
XPackClient(RestHighLevelClient restHighLevelClient) {
this.restHighLevelClient = restHighLevelClient;
this.watcherClient = new WatcherClient(restHighLevelClient);
this.licenseClient = new LicenseClient(restHighLevelClient);
}
public WatcherClient watcher() {
@ -100,4 +102,15 @@ public final class XPackClient {
restHighLevelClient.performRequestAsyncAndParseEntity(request, RequestConverters::xpackUsage, options,
XPackUsageResponse::fromXContent, listener, emptySet());
}
/**
* A wrapper for the {@link RestHighLevelClient} that provides methods for
* accessing the Elastic Licensing APIs.
* <p>
* See the <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/licensing-apis.html">
* X-Pack APIs on elastic.co</a> for more information.
*/
public LicenseClient license() {
return licenseClient;
}
}

View File

@ -22,7 +22,11 @@ package org.elasticsearch.client;
import org.elasticsearch.action.search.SearchRequest;
import org.elasticsearch.action.support.IndicesOptions;
import org.elasticsearch.index.query.MatchAllQueryBuilder;
import org.elasticsearch.index.rankeval.DiscountedCumulativeGain;
import org.elasticsearch.index.rankeval.EvalQueryQuality;
import org.elasticsearch.index.rankeval.EvaluationMetric;
import org.elasticsearch.index.rankeval.ExpectedReciprocalRank;
import org.elasticsearch.index.rankeval.MeanReciprocalRank;
import org.elasticsearch.index.rankeval.PrecisionAtK;
import org.elasticsearch.index.rankeval.RankEvalRequest;
import org.elasticsearch.index.rankeval.RankEvalResponse;
@ -35,8 +39,10 @@ import org.junit.Before;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import java.util.stream.Stream;
@ -64,15 +70,7 @@ public class RankEvalIT extends ESRestHighLevelClientTestCase {
* calculation where all unlabeled documents are treated as not relevant.
*/
public void testRankEvalRequest() throws IOException {
SearchSourceBuilder testQuery = new SearchSourceBuilder();
testQuery.query(new MatchAllQueryBuilder());
List<RatedDocument> amsterdamRatedDocs = createRelevant("index" , "amsterdam1", "amsterdam2", "amsterdam3", "amsterdam4");
amsterdamRatedDocs.addAll(createRelevant("index2", "amsterdam0"));
RatedRequest amsterdamRequest = new RatedRequest("amsterdam_query", amsterdamRatedDocs, testQuery);
RatedRequest berlinRequest = new RatedRequest("berlin_query", createRelevant("index", "berlin"), testQuery);
List<RatedRequest> specifications = new ArrayList<>();
specifications.add(amsterdamRequest);
specifications.add(berlinRequest);
List<RatedRequest> specifications = createTestEvaluationSpec();
PrecisionAtK metric = new PrecisionAtK(1, false, 10);
RankEvalSpec spec = new RankEvalSpec(specifications, metric);
@ -114,6 +112,38 @@ public class RankEvalIT extends ESRestHighLevelClientTestCase {
response = execute(rankEvalRequest, highLevelClient()::rankEval, highLevelClient()::rankEvalAsync);
}
private static List<RatedRequest> createTestEvaluationSpec() {
SearchSourceBuilder testQuery = new SearchSourceBuilder();
testQuery.query(new MatchAllQueryBuilder());
List<RatedDocument> amsterdamRatedDocs = createRelevant("index" , "amsterdam1", "amsterdam2", "amsterdam3", "amsterdam4");
amsterdamRatedDocs.addAll(createRelevant("index2", "amsterdam0"));
RatedRequest amsterdamRequest = new RatedRequest("amsterdam_query", amsterdamRatedDocs, testQuery);
RatedRequest berlinRequest = new RatedRequest("berlin_query", createRelevant("index", "berlin"), testQuery);
List<RatedRequest> specifications = new ArrayList<>();
specifications.add(amsterdamRequest);
specifications.add(berlinRequest);
return specifications;
}
/**
* Test case checks that the default metrics are registered and usable
*/
public void testMetrics() throws IOException {
List<RatedRequest> specifications = createTestEvaluationSpec();
List<Supplier<EvaluationMetric>> metrics = Arrays.asList(PrecisionAtK::new, MeanReciprocalRank::new, DiscountedCumulativeGain::new,
() -> new ExpectedReciprocalRank(1));
double expectedScores[] = new double[] {0.4285714285714286, 0.75, 1.6408962261063627, 0.4407738095238095};
int i = 0;
for (Supplier<EvaluationMetric> metricSupplier : metrics) {
RankEvalSpec spec = new RankEvalSpec(specifications, metricSupplier.get());
RankEvalRequest rankEvalRequest = new RankEvalRequest(spec, new String[] { "index", "index2" });
RankEvalResponse response = execute(rankEvalRequest, highLevelClient()::rankEval, highLevelClient()::rankEvalAsync);
assertEquals(expectedScores[i], response.getMetricScore(), Double.MIN_VALUE);
i++;
}
}
private static List<RatedDocument> createRelevant(String indexName, String... docs) {
return Stream.of(docs).map(s -> new RatedDocument(indexName, s, 1)).collect(Collectors.toList());
}

View File

@ -41,6 +41,7 @@ import org.elasticsearch.action.admin.cluster.settings.ClusterUpdateSettingsRequ
import org.elasticsearch.action.admin.cluster.snapshots.create.CreateSnapshotRequest;
import org.elasticsearch.action.admin.cluster.snapshots.delete.DeleteSnapshotRequest;
import org.elasticsearch.action.admin.cluster.snapshots.get.GetSnapshotsRequest;
import org.elasticsearch.action.admin.cluster.snapshots.restore.RestoreSnapshotRequest;
import org.elasticsearch.action.admin.cluster.snapshots.status.SnapshotsStatusRequest;
import org.elasticsearch.action.admin.cluster.storedscripts.DeleteStoredScriptRequest;
import org.elasticsearch.action.admin.cluster.storedscripts.GetStoredScriptRequest;
@ -2198,6 +2199,31 @@ public class RequestConvertersTests extends ESTestCase {
assertThat(request.getEntity(), is(nullValue()));
}
public void testRestoreSnapshot() throws IOException {
Map<String, String> expectedParams = new HashMap<>();
String repository = randomIndicesNames(1, 1)[0];
String snapshot = "snapshot-" + randomAlphaOfLengthBetween(2, 5).toLowerCase(Locale.ROOT);
String endpoint = String.format(Locale.ROOT, "/_snapshot/%s/%s/_restore", repository, snapshot);
RestoreSnapshotRequest restoreSnapshotRequest = new RestoreSnapshotRequest(repository, snapshot);
setRandomMasterTimeout(restoreSnapshotRequest, expectedParams);
if (randomBoolean()) {
restoreSnapshotRequest.waitForCompletion(true);
expectedParams.put("wait_for_completion", "true");
}
if (randomBoolean()) {
String timeout = randomTimeValue();
restoreSnapshotRequest.masterNodeTimeout(timeout);
expectedParams.put("master_timeout", timeout);
}
Request request = RequestConverters.restoreSnapshot(restoreSnapshotRequest);
assertThat(endpoint, equalTo(request.getEndpoint()));
assertThat(HttpPost.METHOD_NAME, equalTo(request.getMethod()));
assertThat(expectedParams, equalTo(request.getParameters()));
assertToXContentBody(restoreSnapshotRequest, request.getEntity());
}
public void testDeleteSnapshot() {
Map<String, String> expectedParams = new HashMap<>();
String repository = randomIndicesNames(1, 1)[0];

View File

@ -20,6 +20,7 @@
package org.elasticsearch.client;
import com.fasterxml.jackson.core.JsonParseException;
import org.apache.http.HttpEntity;
import org.apache.http.HttpHost;
import org.apache.http.HttpResponse;
@ -60,6 +61,7 @@ import org.elasticsearch.common.xcontent.cbor.CborXContent;
import org.elasticsearch.common.xcontent.smile.SmileXContent;
import org.elasticsearch.index.rankeval.DiscountedCumulativeGain;
import org.elasticsearch.index.rankeval.EvaluationMetric;
import org.elasticsearch.index.rankeval.ExpectedReciprocalRank;
import org.elasticsearch.index.rankeval.MeanReciprocalRank;
import org.elasticsearch.index.rankeval.MetricDetail;
import org.elasticsearch.index.rankeval.PrecisionAtK;
@ -616,7 +618,7 @@ public class RestHighLevelClientTests extends ESTestCase {
public void testProvidedNamedXContents() {
List<NamedXContentRegistry.Entry> namedXContents = RestHighLevelClient.getProvidedNamedXContents();
assertEquals(8, namedXContents.size());
assertEquals(10, namedXContents.size());
Map<Class<?>, Integer> categories = new HashMap<>();
List<String> names = new ArrayList<>();
for (NamedXContentRegistry.Entry namedXContent : namedXContents) {
@ -630,14 +632,16 @@ public class RestHighLevelClientTests extends ESTestCase {
assertEquals(Integer.valueOf(2), categories.get(Aggregation.class));
assertTrue(names.contains(ChildrenAggregationBuilder.NAME));
assertTrue(names.contains(MatrixStatsAggregationBuilder.NAME));
assertEquals(Integer.valueOf(3), categories.get(EvaluationMetric.class));
assertEquals(Integer.valueOf(4), categories.get(EvaluationMetric.class));
assertTrue(names.contains(PrecisionAtK.NAME));
assertTrue(names.contains(DiscountedCumulativeGain.NAME));
assertTrue(names.contains(MeanReciprocalRank.NAME));
assertEquals(Integer.valueOf(3), categories.get(MetricDetail.class));
assertTrue(names.contains(ExpectedReciprocalRank.NAME));
assertEquals(Integer.valueOf(4), categories.get(MetricDetail.class));
assertTrue(names.contains(PrecisionAtK.NAME));
assertTrue(names.contains(MeanReciprocalRank.NAME));
assertTrue(names.contains(DiscountedCumulativeGain.NAME));
assertTrue(names.contains(ExpectedReciprocalRank.NAME));
}
public void testApiNamingConventions() throws Exception {
@ -661,7 +665,6 @@ public class RestHighLevelClientTests extends ESTestCase {
"reindex_rethrottle",
"render_search_template",
"scripts_painless_execute",
"snapshot.restore",
"tasks.get",
"termvectors",
"update_by_query"

View File

@ -28,6 +28,8 @@ import org.elasticsearch.action.admin.cluster.repositories.put.PutRepositoryRequ
import org.elasticsearch.action.admin.cluster.repositories.put.PutRepositoryResponse;
import org.elasticsearch.action.admin.cluster.repositories.verify.VerifyRepositoryRequest;
import org.elasticsearch.action.admin.cluster.repositories.verify.VerifyRepositoryResponse;
import org.elasticsearch.action.admin.cluster.snapshots.restore.RestoreSnapshotRequest;
import org.elasticsearch.action.admin.cluster.snapshots.restore.RestoreSnapshotResponse;
import org.elasticsearch.action.admin.cluster.snapshots.status.SnapshotsStatusRequest;
import org.elasticsearch.action.admin.cluster.snapshots.status.SnapshotsStatusResponse;
import org.elasticsearch.common.settings.Settings;
@ -40,12 +42,15 @@ import org.elasticsearch.action.admin.cluster.snapshots.get.GetSnapshotsResponse
import org.elasticsearch.common.xcontent.XContentType;
import org.elasticsearch.repositories.fs.FsRepository;
import org.elasticsearch.rest.RestStatus;
import org.elasticsearch.snapshots.RestoreInfo;
import java.io.IOException;
import java.util.Collections;
import java.util.stream.Collectors;
import static org.hamcrest.Matchers.contains;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.greaterThan;
import static org.hamcrest.Matchers.is;
public class SnapshotIT extends ESRestHighLevelClientTestCase {
@ -205,6 +210,42 @@ public class SnapshotIT extends ESRestHighLevelClientTestCase {
assertThat(response.getSnapshots().get(0).getIndices().containsKey(testIndex), is(true));
}
public void testRestoreSnapshot() throws IOException {
String testRepository = "test";
String testSnapshot = "snapshot_1";
String testIndex = "test_index";
String restoredIndex = testIndex + "_restored";
PutRepositoryResponse putRepositoryResponse = createTestRepository(testRepository, FsRepository.TYPE, "{\"location\": \".\"}");
assertTrue(putRepositoryResponse.isAcknowledged());
createIndex(testIndex, Settings.EMPTY);
assertTrue("index [" + testIndex + "] should have been created", indexExists(testIndex));
CreateSnapshotRequest createSnapshotRequest = new CreateSnapshotRequest(testRepository, testSnapshot);
createSnapshotRequest.indices(testIndex);
createSnapshotRequest.waitForCompletion(true);
CreateSnapshotResponse createSnapshotResponse = createTestSnapshot(createSnapshotRequest);
assertEquals(RestStatus.OK, createSnapshotResponse.status());
deleteIndex(testIndex);
assertFalse("index [" + testIndex + "] should have been deleted", indexExists(testIndex));
RestoreSnapshotRequest request = new RestoreSnapshotRequest(testRepository, testSnapshot);
request.waitForCompletion(true);
request.renamePattern(testIndex);
request.renameReplacement(restoredIndex);
RestoreSnapshotResponse response = execute(request, highLevelClient().snapshot()::restore,
highLevelClient().snapshot()::restoreAsync);
RestoreInfo restoreInfo = response.getRestoreInfo();
assertThat(restoreInfo.name(), equalTo(testSnapshot));
assertThat(restoreInfo.indices(), equalTo(Collections.singletonList(restoredIndex)));
assertThat(restoreInfo.successfulShards(), greaterThan(0));
assertThat(restoreInfo.failedShards(), equalTo(0));
}
public void testDeleteSnapshot() throws IOException {
String repository = "test_repository";
String snapshot = "test_snapshot";

View File

@ -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.client.documentation;
import org.elasticsearch.action.ActionListener;
import org.elasticsearch.action.LatchedActionListener;
import org.elasticsearch.client.ESRestHighLevelClientTestCase;
import org.elasticsearch.client.RequestOptions;
import org.elasticsearch.client.RestHighLevelClient;
import org.elasticsearch.protocol.xpack.license.LicensesStatus;
import org.elasticsearch.protocol.xpack.license.PutLicenseRequest;
import org.elasticsearch.protocol.xpack.license.PutLicenseResponse;
import java.util.Map;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import static org.hamcrest.Matchers.hasSize;
import static org.hamcrest.Matchers.not;
import static org.hamcrest.Matchers.startsWith;
/**
* Documentation for Licensing APIs in the high level java client.
* Code wrapped in {@code tag} and {@code end} tags is included in the docs.
*/
public class LicensingDocumentationIT extends ESRestHighLevelClientTestCase {
public void testPutLicense() throws Exception {
RestHighLevelClient client = highLevelClient();
String license = "{\"license\": {\"uid\":\"893361dc-9749-4997-93cb-802e3d7fa4a8\",\"type\":\"gold\"," +
"\"issue_date_in_millis\":1411948800000,\"expiry_date_in_millis\":1914278399999,\"max_nodes\":1,\"issued_to\":\"issued_to\"," +
"\"issuer\":\"issuer\",\"signature\":\"AAAAAgAAAA3U8+YmnvwC+CWsV/mRAAABmC9ZN0hjZDBGYnVyRXpCOW5Bb3FjZDAxOWpSbTVoMVZwUzRxVk1PSm" +
"kxakxZdW5IMlhlTHNoN1N2MXMvRFk4d3JTZEx3R3RRZ0pzU3lobWJKZnQvSEFva0ppTHBkWkprZWZSQi9iNmRQNkw1SlpLN0lDalZCS095MXRGN1lIZlpYcVVTTn" +
"FrcTE2dzhJZmZrdFQrN3JQeGwxb0U0MXZ0dDJHSERiZTVLOHNzSDByWnpoZEphZHBEZjUrTVBxRENNSXNsWWJjZllaODdzVmEzUjNiWktNWGM5TUhQV2plaUo4Q1" +
"JOUml4MXNuL0pSOEhQaVB2azhmUk9QVzhFeTFoM1Q0RnJXSG53MWk2K055c28zSmRnVkF1b2JSQkFLV2VXUmVHNDZ2R3o2VE1qbVNQS2lxOHN5bUErZlNIWkZSVm" +
"ZIWEtaSU9wTTJENDVvT1NCYklacUYyK2FwRW9xa0t6dldMbmMzSGtQc3FWOTgzZ3ZUcXMvQkt2RUZwMFJnZzlvL2d2bDRWUzh6UG5pdENGWFRreXNKNkE9PQAAAQ" +
"Be8GfzDm6T537Iuuvjetb3xK5dvg0K5NQapv+rczWcQFxgCuzbF8plkgetP1aAGZP4uRESDQPMlOCsx4d0UqqAm9f7GbBQ3l93P+PogInPFeEH9NvOmaAQovmxVM" +
"9SE6DsDqlX4cXSO+bgWpXPTd2LmpoQc1fXd6BZ8GeuyYpVHVKp9hVU0tAYjw6HzYOE7+zuO1oJYOxElqy66AnIfkvHrvni+flym3tE7tDTgsDRaz7W3iBhaqiSnt" +
"EqabEkvHdPHQdSR99XGaEvnHO1paK01/35iZF6OXHsF7CCj+558GRXiVxzueOe7TsGSSt8g7YjZwV9bRCyU7oB4B/nidgI\"}}";
{
//tag::put-license-execute
PutLicenseRequest request = new PutLicenseRequest();
request.setLicenseDefinition(license); // <1>
request.setAcknowledge(false); // <2>
PutLicenseResponse response = client.xpack().license().putLicense(request, RequestOptions.DEFAULT);
//end::put-license-execute
//tag::put-license-response
LicensesStatus status = response.status(); // <1>
assertEquals(status, LicensesStatus.VALID); // <2>
boolean acknowledged = response.isAcknowledged(); // <3>
String acknowledgeHeader = response.acknowledgeHeader(); // <4>
Map<String, String[]> acknowledgeMessages = response.acknowledgeMessages(); // <5>
//end::put-license-response
assertFalse(acknowledged); // Should fail because we are trying to downgrade from platinum trial to gold
assertThat(acknowledgeHeader, startsWith("This license update requires acknowledgement."));
assertThat(acknowledgeMessages.keySet(), not(hasSize(0)));
}
{
PutLicenseRequest request = new PutLicenseRequest();
// tag::put-license-execute-listener
ActionListener<PutLicenseResponse> listener = new ActionListener<PutLicenseResponse>() {
@Override
public void onResponse(PutLicenseResponse indexResponse) {
// <1>
}
@Override
public void onFailure(Exception e) {
// <2>
}
};
// end::put-license-execute-listener
// Replace the empty listener by a blocking listener in test
final CountDownLatch latch = new CountDownLatch(1);
listener = new LatchedActionListener<>(listener, latch);
// tag::put-license-execute-async
client.xpack().license().putLicenseAsync(
request, RequestOptions.DEFAULT, listener); // <1>
// end::put-license-execute-async
assertTrue(latch.await(30L, TimeUnit.SECONDS));
}
}
}

View File

@ -33,6 +33,8 @@ import org.elasticsearch.action.admin.cluster.snapshots.create.CreateSnapshotReq
import org.elasticsearch.action.admin.cluster.snapshots.create.CreateSnapshotResponse;
import org.elasticsearch.action.admin.cluster.snapshots.get.GetSnapshotsRequest;
import org.elasticsearch.action.admin.cluster.snapshots.get.GetSnapshotsResponse;
import org.elasticsearch.action.admin.cluster.snapshots.restore.RestoreSnapshotRequest;
import org.elasticsearch.action.admin.cluster.snapshots.restore.RestoreSnapshotResponse;
import org.elasticsearch.action.admin.indices.create.CreateIndexRequest;
import org.elasticsearch.action.support.IndicesOptions;
import org.elasticsearch.action.admin.cluster.snapshots.delete.DeleteSnapshotRequest;
@ -53,12 +55,15 @@ import org.elasticsearch.common.unit.TimeValue;
import org.elasticsearch.common.xcontent.XContentType;
import org.elasticsearch.repositories.fs.FsRepository;
import org.elasticsearch.rest.RestStatus;
import org.elasticsearch.snapshots.RestoreInfo;
import org.elasticsearch.snapshots.SnapshotId;
import org.elasticsearch.snapshots.SnapshotInfo;
import org.elasticsearch.snapshots.SnapshotShardFailure;
import org.elasticsearch.snapshots.SnapshotState;
import java.io.IOException;
import java.util.Collections;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
@ -263,6 +268,107 @@ public class SnapshotClientDocumentationIT extends ESRestHighLevelClientTestCase
}
}
public void testRestoreSnapshot() throws IOException {
RestHighLevelClient client = highLevelClient();
createTestRepositories();
createTestIndex();
createTestSnapshots();
// tag::restore-snapshot-request
RestoreSnapshotRequest request = new RestoreSnapshotRequest(repositoryName, snapshotName);
// end::restore-snapshot-request
// we need to restore as a different index name
// tag::restore-snapshot-request-masterTimeout
request.masterNodeTimeout(TimeValue.timeValueMinutes(1)); // <1>
request.masterNodeTimeout("1m"); // <2>
// end::restore-snapshot-request-masterTimeout
// tag::restore-snapshot-request-waitForCompletion
request.waitForCompletion(true); // <1>
// end::restore-snapshot-request-waitForCompletion
// tag::restore-snapshot-request-partial
request.partial(false); // <1>
// end::restore-snapshot-request-partial
// tag::restore-snapshot-request-include-global-state
request.includeGlobalState(false); // <1>
// end::restore-snapshot-request-include-global-state
// tag::restore-snapshot-request-include-aliases
request.includeAliases(false); // <1>
// end::restore-snapshot-request-include-aliases
// tag::restore-snapshot-request-indices
request.indices("test_index");
// end::restore-snapshot-request-indices
String restoredIndexName = "restored_index";
// tag::restore-snapshot-request-rename
request.renamePattern("test_(.+)"); // <1>
request.renameReplacement("restored_$1"); // <2>
// end::restore-snapshot-request-rename
// tag::restore-snapshot-request-index-settings
request.indexSettings( // <1>
Settings.builder()
.put("index.number_of_replicas", 0)
.build());
request.ignoreIndexSettings("index.refresh_interval", "index.search.idle.after"); // <2>
request.indicesOptions(new IndicesOptions( // <3>
EnumSet.of(IndicesOptions.Option.IGNORE_UNAVAILABLE),
EnumSet.of(IndicesOptions.WildcardStates.OPEN)));
// end::restore-snapshot-request-index-settings
// tag::restore-snapshot-execute
RestoreSnapshotResponse response = client.snapshot().restore(request, RequestOptions.DEFAULT);
// end::restore-snapshot-execute
// tag::restore-snapshot-response
RestoreInfo restoreInfo = response.getRestoreInfo();
List<String> indices = restoreInfo.indices(); // <1>
// end::restore-snapshot-response
assertEquals(Collections.singletonList(restoredIndexName), indices);
assertEquals(0, restoreInfo.failedShards());
assertTrue(restoreInfo.successfulShards() > 0);
}
public void testRestoreSnapshotAsync() throws InterruptedException {
RestHighLevelClient client = highLevelClient();
{
RestoreSnapshotRequest request = new RestoreSnapshotRequest();
// tag::restore-snapshot-execute-listener
ActionListener<RestoreSnapshotResponse> listener =
new ActionListener<RestoreSnapshotResponse>() {
@Override
public void onResponse(RestoreSnapshotResponse restoreSnapshotResponse) {
// <1>
}
@Override
public void onFailure(Exception e) {
// <2>
}
};
// end::restore-snapshot-execute-listener
// Replace the empty listener by a blocking listener in test
final CountDownLatch latch = new CountDownLatch(1);
listener = new LatchedActionListener<>(listener, latch);
// tag::restore-snapshot-execute-async
client.snapshot().restoreAsync(request, RequestOptions.DEFAULT, listener); // <1>
// end::restore-snapshot-execute-async
assertTrue(latch.await(30L, TimeUnit.SECONDS));
}
}
public void testSnapshotDeleteRepository() throws IOException {
RestHighLevelClient client = highLevelClient();

View File

@ -0,0 +1,66 @@
[[java-rest-high-put-license]]
=== Update License
[[java-rest-high-put-license-execution]]
==== Execution
The license can be added or updated using the `putLicense()` method:
["source","java",subs="attributes,callouts,macros"]
--------------------------------------------------
include-tagged::{doc-tests}/LicensingDocumentationIT.java[put-license-execute]
--------------------------------------------------
<1> Set the categories of information to retrieve. The the default is to
return no information which is useful for checking if {xpack} is installed
but not much else.
<2> A JSON document containing the license information.
[[java-rest-high-put-license-response]]
==== Response
The returned `PutLicenseResponse` contains the `LicensesStatus`,
`acknowledged` flag and possible acknowledge messages. The acknowledge messages
are present if you previously had a license with more features than one you
are trying to update and you didn't set the `acknowledge` flag to `true`. In this case
you need to display the messages to the end user and if they agree, resubmit the
license with the `acknowledge` flag set to `true`. Please note that the request will
still return a 200 return code even if requires an acknowledgement. So, it is
necessary to check the `acknowledged` flag.
["source","java",subs="attributes,callouts,macros"]
--------------------------------------------------
include-tagged::{doc-tests}/LicensingDocumentationIT.java[put-license-response]
--------------------------------------------------
<1> The status of the license
<2> Make sure that the license is valid.
<3> Check the acknowledge flag.
<4> It should be true if license is acknowledge.
<5> Otherwise we can see the acknowledge messages in `acknowledgeHeader()`
<6> and check component-specific messages in `acknowledgeMessages()`.
[[java-rest-high-put-license-async]]
==== Asynchronous Execution
This request can be executed asynchronously:
["source","java",subs="attributes,callouts,macros"]
--------------------------------------------------
include-tagged::{doc-tests}/LicensingDocumentationIT.java[put-license-execute-async]
--------------------------------------------------
<1> The `PutLicenseRequest` to execute and the `ActionListener` to use when
the execution completes
The asynchronous method does not block and returns immediately. Once it is
completed the `ActionListener` is called back using the `onResponse` method
if the execution successfully completed or using the `onFailure` method if
it failed.
A typical listener for `PutLicenseResponse` looks like:
["source","java",subs="attributes,callouts,macros"]
--------------------------------------------------
include-tagged::{doc-tests}/LicensingDocumentationIT.java[put-license-execute-listener]
--------------------------------------------------
<1> Called when the execution is successfully completed. The response is
provided as an argument
<2> Called in case of failure. The raised exception is provided as an argument

View File

@ -0,0 +1,144 @@
[[java-rest-high-snapshot-restore-snapshot]]
=== Restore Snapshot API
The Restore Snapshot API allows to restore a snapshot.
[[java-rest-high-snapshot-restore-snapshot-request]]
==== Restore Snapshot Request
A `RestoreSnapshotRequest`:
["source","java",subs="attributes,callouts,macros"]
--------------------------------------------------
include-tagged::{doc-tests}/SnapshotClientDocumentationIT.java[restore-snapshot-request]
--------------------------------------------------
==== Limiting Indices to Restore
By default all indices are restored. With the `indices` property you can
provide a list of indices that should be restored:
["source","java",subs="attributes,callouts,macros"]
--------------------------------------------------
include-tagged::{doc-tests}/SnapshotClientDocumentationIT.java[restore-snapshot-request-indices]
--------------------------------------------------
<1> Request that Elasticsearch only restores "test_index".
==== Renaming Indices
You can rename indices using regular expressions when restoring a snapshot:
["source","java",subs="attributes,callouts,macros"]
--------------------------------------------------
include-tagged::{doc-tests}/SnapshotClientDocumentationIT.java[restore-snapshot-request-rename]
--------------------------------------------------
<1> A regular expression matching the indices that should be renamed.
<2> A replacement pattern that references the group from the regular
expression as `$1`. "test_index" from the snapshot is restored as
"restored_index" in this example.
==== Index Settings and Options
You can also customize index settings and options when restoring:
["source","java",subs="attributes,callouts,macros"]
--------------------------------------------------
include-tagged::{doc-tests}/SnapshotClientDocumentationIT.java[restore-snapshot-request-index-settings]
--------------------------------------------------
<1> Use `#indexSettings()` to set any specific index setting for the indices
that are restored.
<2> Use `#ignoreIndexSettings()` to provide index settings that should be
ignored from the original indices.
<3> Set `IndicesOptions.Option.IGNORE_UNAVAILABLE` in `#indicesOptions()` to
have the restore succeed even if indices are missing in the snapshot.
==== Further Arguments
The following arguments can optionally be provided:
["source","java",subs="attributes,callouts,macros"]
--------------------------------------------------
include-tagged::{doc-tests}/SnapshotClientDocumentationIT.java[restore-snapshot-request-masterTimeout]
--------------------------------------------------
<1> Timeout to connect to the master node as a `TimeValue`
<2> Timeout to connect to the master node as a `String`
["source","java",subs="attributes,callouts,macros"]
--------------------------------------------------
include-tagged::{doc-tests}/SnapshotClientDocumentationIT.java[restore-snapshot-request-waitForCompletion]
--------------------------------------------------
<1> Boolean indicating whether to wait until the snapshot has been restored.
["source","java",subs="attributes,callouts,macros"]
--------------------------------------------------
include-tagged::{doc-tests}/SnapshotClientDocumentationIT.java[restore-snapshot-request-partial]
--------------------------------------------------
<1> Boolean indicating whether the entire snapshot should succeed although one
or more indices participating in the snapshot dont have all primary
shards available.
["source","java",subs="attributes,callouts,macros"]
--------------------------------------------------
include-tagged::{doc-tests}/SnapshotClientDocumentationIT.java[restore-snapshot-request-include-global-state]
--------------------------------------------------
<1> Boolean indicating whether restored templates that dont currently exist
in the cluster are added and existing templates with the same name are
replaced by the restored templates. The restored persistent settings are
added to the existing persistent settings.
["source","java",subs="attributes,callouts,macros"]
--------------------------------------------------
include-tagged::{doc-tests}/SnapshotClientDocumentationIT.java[restore-snapshot-request-include-aliases]
--------------------------------------------------
<1> Boolean to control whether aliases should be restored. Set to `false` to
prevent aliases from being restored together with associated indices.
[[java-rest-high-snapshot-restore-snapshot-sync]]
==== Synchronous Execution
["source","java",subs="attributes,callouts,macros"]
--------------------------------------------------
include-tagged::{doc-tests}/SnapshotClientDocumentationIT.java[restore-snapshot-execute]
--------------------------------------------------
[[java-rest-high-snapshot-restore-snapshot-async]]
==== Asynchronous Execution
The asynchronous execution of a restore snapshot request requires both the
`RestoreSnapshotRequest` instance and an `ActionListener` instance to be
passed to the asynchronous method:
["source","java",subs="attributes,callouts,macros"]
--------------------------------------------------
include-tagged::{doc-tests}/SnapshotClientDocumentationIT.java[restore-snapshot-execute-async]
--------------------------------------------------
<1> The `RestoreSnapshotRequest` to execute and the `ActionListener`
to use when the execution completes
The asynchronous method does not block and returns immediately. Once it is
completed the `ActionListener` is called back using the `onResponse` method
if the execution successfully completed or using the `onFailure` method if
it failed.
A typical listener for `RestoreSnapshotResponse` looks like:
["source","java",subs="attributes,callouts,macros"]
--------------------------------------------------
include-tagged::{doc-tests}/SnapshotClientDocumentationIT.java[restore-snapshot-execute-listener]
--------------------------------------------------
<1> Called when the execution is successfully completed. The response is
provided as an argument.
<2> Called in case of a failure. The raised exception is provided as an argument.
[[java-rest-high-cluster-restore-snapshot-response]]
==== Restore Snapshot Response
The returned `RestoreSnapshotResponse` allows to retrieve information about the
executed operation as follows:
["source","java",subs="attributes,callouts,macros"]
--------------------------------------------------
include-tagged::{doc-tests}/SnapshotClientDocumentationIT.java[restore-snapshot-response]
--------------------------------------------------
<1> The `RestoreInfo` contains details about the restored snapshot like the indices or
the number of successfully restored and failed shards.

View File

@ -186,3 +186,12 @@ The Java High Level REST Client supports the following Scripts APIs:
include::script/get_script.asciidoc[]
include::script/delete_script.asciidoc[]
== Licensing APIs
The Java High Level REST Client supports the following Licensing APIs:
* <<java-rest-high-put-license>>
include::licensing/put-license.asciidoc[]

View File

@ -259,6 +259,56 @@ in the query. Defaults to 10.
|`normalize` | If set to `true`, this metric will calculate the https://en.wikipedia.org/wiki/Discounted_cumulative_gain#Normalized_DCG[Normalized DCG].
|=======================================================================
[float]
==== Expected Reciprocal Rank (ERR)
Expected Reciprocal Rank (ERR) is an extension of the classical reciprocal rank for the graded relevance case
(Olivier Chapelle, Donald Metzler, Ya Zhang, and Pierre Grinspan. 2009. http://olivier.chapelle.cc/pub/err.pdf[Expected reciprocal rank for graded relevance].)
It is based on the assumption of a cascade model of search, in which a user scans through ranked search
results in order and stops at the first document that satisfies the information need. For this reason, it
is a good metric for question answering and navigation queries, but less so for survey oriented information
needs where the user is interested in finding many relevant documents in the top k results.
The metric models the expectation of the reciprocal of the position at which a user stops reading through
the result list. This means that relevant document in top ranking positions will contribute much to the
overall score. However, the same document will contribute much less to the score if it appears in a lower rank,
even more so if there are some relevant (but maybe less relevant) documents preceding it.
In this way, the ERR metric discounts documents which are shown after very relevant documents. This introduces
a notion of dependency in the ordering of relevant documents that e.g. Precision or DCG don't account for.
[source,js]
--------------------------------
GET /twitter/_rank_eval
{
"requests": [
{
"id": "JFK query",
"request": { "query": { "match_all": {}}},
"ratings": []
}],
"metric": {
"expected_reciprocal_rank": {
"maximum_relevance" : 3,
"k" : 20
}
}
}
--------------------------------
// CONSOLE
// TEST[setup:twitter]
The `expected_reciprocal_rank` metric takes the following parameters:
[cols="<,<",options="header",]
|=======================================================================
|Parameter |Description
| `maximum_relevance` | Mandatory parameter. The highest relevance grade used in the user supplied
relevance judgments.
|`k` | sets the maximum number of documents retrieved per query. This value will act in place of the usual `size` parameter
in the query. Defaults to 10.
|=======================================================================
[float]
=== Response format

View File

@ -65,6 +65,9 @@ public class ExpectedReciprocalRank implements EvaluationMetric {
public static final String NAME = "expected_reciprocal_rank";
/**
* @param maxRelevance the highest expected relevance in the data
*/
public ExpectedReciprocalRank(int maxRelevance) {
this(maxRelevance, null, DEFAULT_K);
}

View File

@ -37,12 +37,17 @@ public class RankEvalNamedXContentProvider implements NamedXContentProvider {
MeanReciprocalRank::fromXContent));
namedXContent.add(new NamedXContentRegistry.Entry(EvaluationMetric.class, new ParseField(DiscountedCumulativeGain.NAME),
DiscountedCumulativeGain::fromXContent));
namedXContent.add(new NamedXContentRegistry.Entry(EvaluationMetric.class, new ParseField(ExpectedReciprocalRank.NAME),
ExpectedReciprocalRank::fromXContent));
namedXContent.add(new NamedXContentRegistry.Entry(MetricDetail.class, new ParseField(PrecisionAtK.NAME),
PrecisionAtK.Detail::fromXContent));
namedXContent.add(new NamedXContentRegistry.Entry(MetricDetail.class, new ParseField(MeanReciprocalRank.NAME),
MeanReciprocalRank.Detail::fromXContent));
namedXContent.add(new NamedXContentRegistry.Entry(MetricDetail.class, new ParseField(DiscountedCumulativeGain.NAME),
DiscountedCumulativeGain.Detail::fromXContent));
namedXContent.add(new NamedXContentRegistry.Entry(MetricDetail.class, new ParseField(ExpectedReciprocalRank.NAME),
ExpectedReciprocalRank.Detail::fromXContent));
return namedXContent;
}
}

View File

@ -60,10 +60,14 @@ public class RankEvalPlugin extends Plugin implements ActionPlugin {
namedWriteables.add(new NamedWriteableRegistry.Entry(EvaluationMetric.class, MeanReciprocalRank.NAME, MeanReciprocalRank::new));
namedWriteables.add(
new NamedWriteableRegistry.Entry(EvaluationMetric.class, DiscountedCumulativeGain.NAME, DiscountedCumulativeGain::new));
namedWriteables.add(
new NamedWriteableRegistry.Entry(EvaluationMetric.class, ExpectedReciprocalRank.NAME, ExpectedReciprocalRank::new));
namedWriteables.add(new NamedWriteableRegistry.Entry(MetricDetail.class, PrecisionAtK.NAME, PrecisionAtK.Detail::new));
namedWriteables.add(new NamedWriteableRegistry.Entry(MetricDetail.class, MeanReciprocalRank.NAME, MeanReciprocalRank.Detail::new));
namedWriteables.add(
new NamedWriteableRegistry.Entry(MetricDetail.class, DiscountedCumulativeGain.NAME, DiscountedCumulativeGain.Detail::new));
namedWriteables.add(
new NamedWriteableRegistry.Entry(MetricDetail.class, ExpectedReciprocalRank.NAME, ExpectedReciprocalRank.Detail::new));
return namedWriteables;
}

View File

@ -161,3 +161,37 @@ setup:
- match: {details.berlin_query.metric_details.mean_reciprocal_rank: {"first_relevant": 2}}
- match: {details.berlin_query.unrated_docs: [ {"_index": "foo", "_id": "doc1"}]}
---
"Expected Reciprocal Rank":
- skip:
version: " - 6.3.99"
reason: ERR was introduced in 6.4
- do:
rank_eval:
body: {
"requests" : [
{
"id": "amsterdam_query",
"request": { "query": { "match" : {"text" : "amsterdam" }}},
"ratings": [{"_index": "foo", "_id": "doc4", "rating": 1}]
},
{
"id" : "berlin_query",
"request": { "query": { "match" : { "text" : "berlin" } }, "size" : 10 },
"ratings": [{"_index": "foo", "_id": "doc4", "rating": 1}]
}
],
"metric" : {
"expected_reciprocal_rank": {
"maximum_relevance" : 1,
"k" : 5
}
}
}
- gt: {metric_score: 0.2083333}
- lt: {metric_score: 0.2083334}
- match: {details.amsterdam_query.metric_details.expected_reciprocal_rank.unrated_docs: 2}
- match: {details.berlin_query.metric_details.expected_reciprocal_rank.unrated_docs: 1}

View File

@ -27,14 +27,17 @@ import org.elasticsearch.common.Strings;
import org.elasticsearch.common.io.stream.StreamInput;
import org.elasticsearch.common.io.stream.StreamOutput;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.common.xcontent.ToXContentObject;
import org.elasticsearch.common.xcontent.XContentBuilder;
import org.elasticsearch.common.xcontent.XContentFactory;
import org.elasticsearch.common.xcontent.XContentType;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import static org.elasticsearch.action.ValidateActions.addValidationError;
import static org.elasticsearch.common.settings.Settings.readSettingsFromStream;
@ -45,7 +48,7 @@ import static org.elasticsearch.common.xcontent.support.XContentMapValues.nodeBo
/**
* Restore snapshot request
*/
public class RestoreSnapshotRequest extends MasterNodeRequest<RestoreSnapshotRequest> {
public class RestoreSnapshotRequest extends MasterNodeRequest<RestoreSnapshotRequest> implements ToXContentObject {
private String snapshot;
private String repository;
@ -563,6 +566,49 @@ public class RestoreSnapshotRequest extends MasterNodeRequest<RestoreSnapshotReq
return this;
}
@Override
public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
builder.startObject();
builder.startArray("indices");
for (String index : indices) {
builder.value(index);
}
builder.endArray();
if (indicesOptions != null) {
indicesOptions.toXContent(builder, params);
}
if (renamePattern != null) {
builder.field("rename_pattern", renamePattern);
}
if (renameReplacement != null) {
builder.field("rename_replacement", renameReplacement);
}
builder.field("include_global_state", includeGlobalState);
builder.field("partial", partial);
builder.field("include_aliases", includeAliases);
if (settings != null) {
builder.startObject("settings");
if (settings.isEmpty() == false) {
settings.toXContent(builder, params);
}
builder.endObject();
}
if (indexSettings != null) {
builder.startObject("index_settings");
if (indexSettings.isEmpty() == false) {
indexSettings.toXContent(builder, params);
}
builder.endObject();
}
builder.startArray("ignore_index_settings");
for (String ignoreIndexSetting : ignoreIndexSettings) {
builder.value(ignoreIndexSetting);
}
builder.endArray();
builder.endObject();
return builder;
}
@Override
public void readFrom(StreamInput in) throws IOException {
throw new UnsupportedOperationException("usage of Streamable is to be replaced by Writeable");
@ -573,4 +619,37 @@ public class RestoreSnapshotRequest extends MasterNodeRequest<RestoreSnapshotReq
return "snapshot [" + repository + ":" + snapshot + "]";
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
RestoreSnapshotRequest that = (RestoreSnapshotRequest) o;
return waitForCompletion == that.waitForCompletion &&
includeGlobalState == that.includeGlobalState &&
partial == that.partial &&
includeAliases == that.includeAliases &&
Objects.equals(snapshot, that.snapshot) &&
Objects.equals(repository, that.repository) &&
Arrays.equals(indices, that.indices) &&
Objects.equals(indicesOptions, that.indicesOptions) &&
Objects.equals(renamePattern, that.renamePattern) &&
Objects.equals(renameReplacement, that.renameReplacement) &&
Objects.equals(settings, that.settings) &&
Objects.equals(indexSettings, that.indexSettings) &&
Arrays.equals(ignoreIndexSettings, that.ignoreIndexSettings);
}
@Override
public int hashCode() {
int result = Objects.hash(snapshot, repository, indicesOptions, renamePattern, renameReplacement, waitForCompletion,
includeGlobalState, partial, includeAliases, settings, indexSettings);
result = 31 * result + Arrays.hashCode(indices);
result = 31 * result + Arrays.hashCode(ignoreIndexSettings);
return result;
}
@Override
public String toString() {
return Strings.toString(this);
}
}

View File

@ -21,15 +21,21 @@ package org.elasticsearch.action.admin.cluster.snapshots.restore;
import org.elasticsearch.action.ActionResponse;
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.xcontent.ConstructingObjectParser;
import org.elasticsearch.common.xcontent.ToXContent;
import org.elasticsearch.common.xcontent.ToXContentObject;
import org.elasticsearch.common.xcontent.XContentBuilder;
import org.elasticsearch.common.xcontent.XContentParser;
import org.elasticsearch.rest.RestStatus;
import org.elasticsearch.snapshots.RestoreInfo;
import java.io.IOException;
import java.util.Objects;
import static org.elasticsearch.common.xcontent.ConstructingObjectParser.optionalConstructorArg;
/**
* Contains information about restores snapshot
@ -86,4 +92,42 @@ public class RestoreSnapshotResponse extends ActionResponse implements ToXConten
builder.endObject();
return builder;
}
public static final ConstructingObjectParser<RestoreSnapshotResponse, Void> PARSER = new ConstructingObjectParser<>(
"restore_snapshot", true, v -> {
RestoreInfo restoreInfo = (RestoreInfo) v[0];
Boolean accepted = (Boolean) v[1];
assert (accepted == null && restoreInfo != null) ||
(accepted != null && accepted && restoreInfo == null) :
"accepted: [" + accepted + "], restoreInfo: [" + restoreInfo + "]";
return new RestoreSnapshotResponse(restoreInfo);
});
static {
PARSER.declareObject(optionalConstructorArg(), (parser, context) -> RestoreInfo.fromXContent(parser), new ParseField("snapshot"));
PARSER.declareBoolean(optionalConstructorArg(), new ParseField("accepted"));
}
public static RestoreSnapshotResponse fromXContent(XContentParser parser) throws IOException {
return PARSER.parse(parser, null);
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
RestoreSnapshotResponse that = (RestoreSnapshotResponse) o;
return Objects.equals(restoreInfo, that.restoreInfo);
}
@Override
public int hashCode() {
return Objects.hash(restoreInfo);
}
@Override
public String toString() {
return "RestoreSnapshotResponse{" + "restoreInfo=" + restoreInfo + '}';
}
}

View File

@ -59,6 +59,7 @@ import java.time.Instant;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.EnumSet;
@ -931,8 +932,23 @@ public abstract class StreamInput extends InputStream {
* Reads a list of objects
*/
public <T> List<T> readList(Writeable.Reader<T> reader) throws IOException {
return readCollection(reader, ArrayList::new);
}
/**
* Reads a set of objects
*/
public <T> Set<T> readSet(Writeable.Reader<T> reader) throws IOException {
return readCollection(reader, HashSet::new);
}
/**
* Reads a collection of objects
*/
private <T, C extends Collection<? super T>> C readCollection(Writeable.Reader<T> reader,
IntFunction<C> constructor) throws IOException {
int count = readArraySize();
List<T> builder = new ArrayList<>(count);
C builder = constructor.apply(count);
for (int i=0; i<count; i++) {
builder.add(reader.read(this));
}

View File

@ -55,6 +55,7 @@ import java.nio.file.FileSystemLoopException;
import java.nio.file.NoSuchFileException;
import java.nio.file.NotDirectoryException;
import java.time.ZonedDateTime;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.EnumMap;
@ -995,6 +996,16 @@ public abstract class StreamOutput extends OutputStream {
}
}
/**
* Writes a collection of generic objects via a {@link Writer}
*/
public <T> void writeCollection(Collection<T> collection, Writer<T> writer) throws IOException {
writeVInt(collection.size());
for (T val: collection) {
writer.write(this, val);
}
}
/**
* Writes a list of strings
*/

View File

@ -18,18 +18,22 @@
*/
package org.elasticsearch.snapshots;
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.Streamable;
import org.elasticsearch.common.xcontent.ToXContent.Params;
import org.elasticsearch.common.xcontent.ObjectParser;
import org.elasticsearch.common.xcontent.ToXContentObject;
import org.elasticsearch.common.xcontent.XContentBuilder;
import org.elasticsearch.common.xcontent.XContentParser;
import org.elasticsearch.rest.RestStatus;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
/**
* Information about successfully completed restore operation.
@ -120,9 +124,6 @@ public class RestoreInfo implements ToXContentObject, Streamable {
static final String SUCCESSFUL = "successful";
}
/**
* {@inheritDoc}
*/
@Override
public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
builder.startObject();
@ -141,9 +142,23 @@ public class RestoreInfo implements ToXContentObject, Streamable {
return builder;
}
/**
* {@inheritDoc}
*/
private static final ObjectParser<RestoreInfo, Void> PARSER = new ObjectParser<>(RestoreInfo.class.getName(), true, RestoreInfo::new);
static {
ObjectParser<RestoreInfo, Void> shardsParser = new ObjectParser<>("shards", true, null);
shardsParser.declareInt((r, s) -> r.totalShards = s, new ParseField(Fields.TOTAL));
shardsParser.declareInt((r, s) -> { /* only consume, don't set */ }, new ParseField(Fields.FAILED));
shardsParser.declareInt((r, s) -> r.successfulShards = s, new ParseField(Fields.SUCCESSFUL));
PARSER.declareString((r, n) -> r.name = n, new ParseField(Fields.SNAPSHOT));
PARSER.declareStringArray((r, i) -> r.indices = i, new ParseField(Fields.INDICES));
PARSER.declareField(shardsParser::parse, new ParseField(Fields.SHARDS), ObjectParser.ValueType.OBJECT);
}
public static RestoreInfo fromXContent(XContentParser parser) throws IOException {
return PARSER.parse(parser, null);
}
@Override
public void readFrom(StreamInput in) throws IOException {
name = in.readString();
@ -157,9 +172,6 @@ public class RestoreInfo implements ToXContentObject, Streamable {
successfulShards = in.readVInt();
}
/**
* {@inheritDoc}
*/
@Override
public void writeTo(StreamOutput out) throws IOException {
out.writeString(name);
@ -193,4 +205,24 @@ public class RestoreInfo implements ToXContentObject, Streamable {
return in.readOptionalStreamable(RestoreInfo::new);
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
RestoreInfo that = (RestoreInfo) o;
return totalShards == that.totalShards &&
successfulShards == that.successfulShards &&
Objects.equals(name, that.name) &&
Objects.equals(indices, that.indices);
}
@Override
public int hashCode() {
return Objects.hash(name, indices, totalShards, successfulShards);
}
@Override
public String toString() {
return Strings.toString(this);
}
}

View File

@ -0,0 +1,141 @@
/*
* 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.action.admin.cluster.snapshots.restore;
import org.elasticsearch.action.support.IndicesOptions;
import org.elasticsearch.common.bytes.BytesReference;
import org.elasticsearch.common.io.stream.Writeable;
import org.elasticsearch.common.xcontent.NamedXContentRegistry;
import org.elasticsearch.common.xcontent.ToXContent;
import org.elasticsearch.common.xcontent.XContentBuilder;
import org.elasticsearch.common.xcontent.XContentFactory;
import org.elasticsearch.common.xcontent.XContentParser;
import org.elasticsearch.common.xcontent.XContentType;
import org.elasticsearch.test.AbstractWireSerializingTestCase;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class RestoreSnapshotRequestTests extends AbstractWireSerializingTestCase<RestoreSnapshotRequest> {
private RestoreSnapshotRequest randomState(RestoreSnapshotRequest instance) {
if (randomBoolean()) {
List<String> indices = new ArrayList<>();
int count = randomInt(3) + 1;
for (int i = 0; i < count; ++i) {
indices.add(randomAlphaOfLength(randomInt(3) + 2));
}
instance.indices(indices);
}
if (randomBoolean()) {
instance.renamePattern(randomUnicodeOfLengthBetween(1, 100));
}
if (randomBoolean()) {
instance.renameReplacement(randomUnicodeOfLengthBetween(1, 100));
}
instance.partial(randomBoolean());
instance.includeAliases(randomBoolean());
if (randomBoolean()) {
Map<String, Object> settings = new HashMap<>();
int count = randomInt(3) + 1;
for (int i = 0; i < count; ++i) {
settings.put(randomAlphaOfLengthBetween(2, 5), randomAlphaOfLengthBetween(2, 5));
}
instance.settings(settings);
}
if (randomBoolean()) {
Map<String, Object> indexSettings = new HashMap<>();
int count = randomInt(3) + 1;
for (int i = 0; i < count; ++i) {
indexSettings.put(randomAlphaOfLengthBetween(2, 5), randomAlphaOfLengthBetween(2, 5));;
}
instance.indexSettings(indexSettings);
}
instance.includeGlobalState(randomBoolean());
if (randomBoolean()) {
Collection<IndicesOptions.WildcardStates> wildcardStates = randomSubsetOf(
Arrays.asList(IndicesOptions.WildcardStates.values()));
Collection<IndicesOptions.Option> options = randomSubsetOf(
Arrays.asList(IndicesOptions.Option.ALLOW_NO_INDICES, IndicesOptions.Option.IGNORE_UNAVAILABLE));
instance.indicesOptions(new IndicesOptions(
options.isEmpty() ? IndicesOptions.Option.NONE : EnumSet.copyOf(options),
wildcardStates.isEmpty() ? IndicesOptions.WildcardStates.NONE : EnumSet.copyOf(wildcardStates)));
}
instance.waitForCompletion(randomBoolean());
if (randomBoolean()) {
instance.masterNodeTimeout(randomTimeValue());
}
return instance;
}
@Override
protected RestoreSnapshotRequest createTestInstance() {
return randomState(new RestoreSnapshotRequest(randomAlphaOfLength(5), randomAlphaOfLength(10)));
}
@Override
protected Writeable.Reader<RestoreSnapshotRequest> instanceReader() {
return RestoreSnapshotRequest::new;
}
@Override
protected RestoreSnapshotRequest mutateInstance(RestoreSnapshotRequest instance) throws IOException {
RestoreSnapshotRequest copy = copyInstance(instance);
// ensure that at least one property is different
copy.repository("copied-" + instance.repository());
return randomState(copy);
}
public void testSource() throws IOException {
RestoreSnapshotRequest original = createTestInstance();
XContentBuilder builder = original.toXContent(XContentFactory.jsonBuilder(), new ToXContent.MapParams(Collections.emptyMap()));
XContentParser parser = XContentType.JSON.xContent().createParser(
NamedXContentRegistry.EMPTY, null, BytesReference.bytes(builder).streamInput());
Map<String, Object> map = parser.mapOrdered();
// we will only restore properties from the map that are contained in the request body. All other
// properties are restored from the original (in the actual REST action this is restored from the
// REST path and request parameters).
RestoreSnapshotRequest processed = new RestoreSnapshotRequest(original.repository(), original.snapshot());
processed.masterNodeTimeout(original.masterNodeTimeout());
processed.waitForCompletion(original.waitForCompletion());
processed.source(map);
assertEquals(original, processed);
}
}

View File

@ -0,0 +1,56 @@
/*
* 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.action.admin.cluster.snapshots.restore;
import org.elasticsearch.common.xcontent.XContentParser;
import org.elasticsearch.snapshots.RestoreInfo;
import org.elasticsearch.test.AbstractXContentTestCase;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
public class RestoreSnapshotResponseTests extends AbstractXContentTestCase<RestoreSnapshotResponse> {
@Override
protected RestoreSnapshotResponse createTestInstance() {
if (randomBoolean()) {
String name = randomRealisticUnicodeOfCodepointLengthBetween(1, 30);
List<String> indices = new ArrayList<>();
indices.add("test0");
indices.add("test1");
int totalShards = randomIntBetween(1, 1000);
int successfulShards = randomIntBetween(0, totalShards);
return new RestoreSnapshotResponse(new RestoreInfo(name, indices, totalShards, successfulShards));
} else {
return new RestoreSnapshotResponse(null);
}
}
@Override
protected RestoreSnapshotResponse doParseInstance(XContentParser parser) throws IOException {
return RestoreSnapshotResponse.fromXContent(parser);
}
@Override
protected boolean supportsUnknownFields() {
return true;
}
}

View File

@ -31,6 +31,7 @@ import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
@ -42,6 +43,7 @@ import java.util.stream.IntStream;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.hasToString;
import static org.hamcrest.Matchers.iterableWithSize;
public class StreamTests extends ESTestCase {
@ -65,7 +67,7 @@ public class StreamTests extends ESTestCase {
final Set<Byte> set = IntStream.range(Byte.MIN_VALUE, Byte.MAX_VALUE).mapToObj(v -> (byte) v).collect(Collectors.toSet());
set.remove((byte) 0);
set.remove((byte) 1);
final byte[] corruptBytes = new byte[] { randomFrom(set) };
final byte[] corruptBytes = new byte[]{randomFrom(set)};
final BytesReference corrupt = new BytesArray(corruptBytes);
final IllegalStateException e = expectThrows(IllegalStateException.class, () -> corrupt.streamInput().readBoolean());
final String message = String.format(Locale.ROOT, "unexpected byte [0x%02x]", corruptBytes[0]);
@ -100,7 +102,7 @@ public class StreamTests extends ESTestCase {
set.remove((byte) 0);
set.remove((byte) 1);
set.remove((byte) 2);
final byte[] corruptBytes = new byte[] { randomFrom(set) };
final byte[] corruptBytes = new byte[]{randomFrom(set)};
final BytesReference corrupt = new BytesArray(corruptBytes);
final IllegalStateException e = expectThrows(IllegalStateException.class, () -> corrupt.streamInput().readOptionalBoolean());
final String message = String.format(Locale.ROOT, "unexpected byte [0x%02x]", corruptBytes[0]);
@ -119,22 +121,22 @@ public class StreamTests extends ESTestCase {
public void testSpecificVLongSerialization() throws IOException {
List<Tuple<Long, byte[]>> values =
Arrays.asList(
new Tuple<>(0L, new byte[]{0}),
new Tuple<>(-1L, new byte[]{1}),
new Tuple<>(1L, new byte[]{2}),
new Tuple<>(-2L, new byte[]{3}),
new Tuple<>(2L, new byte[]{4}),
new Tuple<>(Long.MIN_VALUE, new byte[]{-1, -1, -1, -1, -1, -1, -1, -1, -1, 1}),
new Tuple<>(Long.MAX_VALUE, new byte[]{-2, -1, -1, -1, -1, -1, -1, -1, -1, 1})
Arrays.asList(
new Tuple<>(0L, new byte[]{0}),
new Tuple<>(-1L, new byte[]{1}),
new Tuple<>(1L, new byte[]{2}),
new Tuple<>(-2L, new byte[]{3}),
new Tuple<>(2L, new byte[]{4}),
new Tuple<>(Long.MIN_VALUE, new byte[]{-1, -1, -1, -1, -1, -1, -1, -1, -1, 1}),
new Tuple<>(Long.MAX_VALUE, new byte[]{-2, -1, -1, -1, -1, -1, -1, -1, -1, 1})
);
);
for (Tuple<Long, byte[]> value : values) {
BytesStreamOutput out = new BytesStreamOutput();
out.writeZLong(value.v1());
assertArrayEquals(Long.toString(value.v1()), value.v2(), BytesReference.toBytes(out.bytes()));
BytesReference bytes = new BytesArray(value.v2());
assertEquals(Arrays.toString(value.v2()), (long)value.v1(), bytes.streamInput().readZLong());
assertEquals(Arrays.toString(value.v2()), (long) value.v1(), bytes.streamInput().readZLong());
}
}
@ -158,7 +160,7 @@ public class StreamTests extends ESTestCase {
}
BytesStreamOutput out = new BytesStreamOutput();
out.writeGenericValue(write);
LinkedHashMap<String, Integer> read = (LinkedHashMap<String, Integer>)out.bytes().streamInput().readGenericValue();
LinkedHashMap<String, Integer> read = (LinkedHashMap<String, Integer>) out.bytes().streamInput().readGenericValue();
assertEquals(size, read.size());
int index = 0;
for (Map.Entry<String, Integer> entry : read.entrySet()) {
@ -172,7 +174,8 @@ public class StreamTests extends ESTestCase {
final int length = randomIntBetween(1, 1024);
StreamInput delegate = StreamInput.wrap(new byte[length]);
FilterStreamInput filterInputStream = new FilterStreamInput(delegate) {};
FilterStreamInput filterInputStream = new FilterStreamInput(delegate) {
};
assertEquals(filterInputStream.available(), length);
// read some bytes
@ -201,7 +204,7 @@ public class StreamTests extends ESTestCase {
}
stream.writeByteArray(array);
InputStreamStreamInput streamInput = new InputStreamStreamInput(StreamInput.wrap(BytesReference.toBytes(stream.bytes())), array
.length-1);
.length - 1);
expectThrows(EOFException.class, streamInput::readByteArray);
streamInput = new InputStreamStreamInput(StreamInput.wrap(BytesReference.toBytes(stream.bytes())), BytesReference.toBytes(stream
.bytes()).length);
@ -230,6 +233,21 @@ public class StreamTests extends ESTestCase {
assertThat(targetArray, equalTo(sourceArray));
}
public void testSetOfLongs() throws IOException {
final int size = randomIntBetween(0, 6);
final Set<Long> sourceSet = new HashSet<>(size);
for (int i = 0; i < size; i++) {
sourceSet.add(randomLongBetween(i * 1000, (i + 1) * 1000 - 1));
}
assertThat(sourceSet, iterableWithSize(size));
final BytesStreamOutput out = new BytesStreamOutput();
out.writeCollection(sourceSet, StreamOutput::writeLong);
final Set<Long> targetSet = out.bytes().streamInput().readSet(StreamInput::readLong);
assertThat(targetSet, equalTo(sourceSet));
}
static final class WriteableString implements Writeable {
final String string;

View File

@ -20,11 +20,14 @@
set -e
if [[ $# -lt 1 ]]; then
echo 'Usage: addprinc.sh <principalNameNoRealm>'
echo 'Usage: addprinc.sh principalName [password]'
echo ' principalName user principal name without realm'
echo ' password If provided then will set password for user else it will provision user with keytab'
exit 1
fi
PRINC="$1"
PASSWD="$2"
USER=$(echo $PRINC | tr "/" "_")
VDIR=/vagrant
@ -47,12 +50,17 @@ ADMIN_KTAB=$LOCALSTATEDIR/admin.keytab
USER_PRIN=$PRINC@$REALM
USER_KTAB=$LOCALSTATEDIR/$USER.keytab
if [ -f $USER_KTAB ]; then
if [ -f $USER_KTAB ] && [ -z "$PASSWD" ]; then
echo "Principal '${PRINC}@${REALM}' already exists. Re-copying keytab..."
sudo cp $USER_KTAB $KEYTAB_DIR/$USER.keytab
else
echo "Provisioning '${PRINC}@${REALM}' principal and keytab..."
sudo kadmin -p $ADMIN_PRIN -kt $ADMIN_KTAB -q "addprinc -randkey $USER_PRIN"
sudo kadmin -p $ADMIN_PRIN -kt $ADMIN_KTAB -q "ktadd -k $USER_KTAB $USER_PRIN"
if [ -z "$PASSWD" ]; then
echo "Provisioning '${PRINC}@${REALM}' principal and keytab..."
sudo kadmin -p $ADMIN_PRIN -kt $ADMIN_KTAB -q "addprinc -randkey $USER_PRIN"
sudo kadmin -p $ADMIN_PRIN -kt $ADMIN_KTAB -q "ktadd -k $USER_KTAB $USER_PRIN"
sudo cp $USER_KTAB $KEYTAB_DIR/$USER.keytab
else
echo "Provisioning '${PRINC}@${REALM}' principal with password..."
sudo kadmin -p $ADMIN_PRIN -kt $ADMIN_KTAB -q "addprinc -pw $PASSWD $PRINC"
fi
fi
sudo cp $USER_KTAB $KEYTAB_DIR/$USER.keytab

View File

@ -149,6 +149,7 @@ import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.BooleanSupplier;
import java.util.function.Consumer;
import java.util.function.IntFunction;
import java.util.function.Predicate;
import java.util.function.Supplier;
import java.util.stream.Collectors;
@ -717,6 +718,20 @@ public abstract class ESTestCase extends LuceneTestCase {
return generateRandomStringArray(maxArraySize, stringSize, allowNull, true);
}
public static <T> T[] randomArray(int maxArraySize, IntFunction<T[]> arrayConstructor, Supplier<T> valueConstructor) {
return randomArray(0, maxArraySize, arrayConstructor, valueConstructor);
}
public static <T> T[] randomArray(int minArraySize, int maxArraySize, IntFunction<T[]> arrayConstructor, Supplier<T> valueConstructor) {
final int size = randomIntBetween(minArraySize, maxArraySize);
final T[] array = arrayConstructor.apply(size);
for (int i = 0; i < array.length; i++) {
array[i] = valueConstructor.get();
}
return array;
}
private static final String[] TIME_SUFFIXES = new String[]{"d", "h", "ms", "s", "m", "micros", "nanos"};
public static String randomTimeValue(int lower, int upper, String... suffixes) {

View File

@ -536,6 +536,11 @@ public abstract class ESRestTestCase extends ESTestCase {
client().performRequest(request);
}
protected static void deleteIndex(String name) throws IOException {
Request request = new Request("DELETE", "/" + name);
client().performRequest(request);
}
protected static void updateIndexSettings(String index, Settings.Builder settings) throws IOException {
updateIndexSettings(index, settings.build());
}

View File

@ -30,7 +30,7 @@ buildRestTests.expectedUnconvertedCandidates = [
]
dependencies {
testCompile project(path: xpackModule('core'), configuration: 'runtime')
testCompile project(path: xpackModule('core'), configuration: 'shadow')
testCompile project(path: xpackModule('core'), configuration: 'testArtifacts')
testCompile project(path: xpackProject('plugin').path, configuration: 'testArtifacts')
}
@ -264,7 +264,7 @@ setups['farequote_index'] = '''
airline:
type: keyword
doc_count:
type: integer
type: integer
'''
setups['farequote_data'] = setups['farequote_index'] + '''
- do:
@ -277,7 +277,7 @@ setups['farequote_data'] = setups['farequote_index'] + '''
{"airline":"JZA","responsetime":990.4628,"time":"2016-02-07T00:00:00+0000", "doc_count": 5}
{"index": {"_id":"2"}}
{"airline":"JBU","responsetime":877.5927,"time":"2016-02-07T00:00:00+0000", "doc_count": 23}
{"index": {"_id":"3"}}
{"index": {"_id":"3"}}
{"airline":"KLM","responsetime":1355.4812,"time":"2016-02-07T00:00:00+0000", "doc_count": 42}
'''
setups['farequote_job'] = setups['farequote_data'] + '''
@ -309,7 +309,7 @@ setups['farequote_datafeed'] = setups['farequote_job'] + '''
"job_id":"farequote",
"indexes":"farequote"
}
'''
'''
setups['server_metrics_index'] = '''
- do:
indices.create:

View File

@ -84,7 +84,8 @@ The following example output indicates which privileges the "rdeniro" user has:
"read" : true,
"write" : false
}
}
},
"application" : {}
}
--------------------------------------------------
// TESTRESPONSE[s/"rdeniro"/"$body.username"/]

View File

@ -140,6 +140,7 @@ role. If the role is not defined in the `native` realm, the request 404s.
},
"query" : "{\"match\": {\"title\": \"foo\"}}"
} ],
"applications" : [ ],
"run_as" : [ "other_user" ],
"metadata" : {
"version" : 1

View File

@ -1,7 +1,7 @@
apply plugin: 'elasticsearch.build'
dependencies {
compile project(xpackModule('core'))
compile project(path: xpackModule('core'), configuration: 'shadow')
compile "org.elasticsearch:elasticsearch:${version}"
testCompile "org.elasticsearch.test:framework:${version}"
}
@ -17,7 +17,7 @@ task buildZip(type: Zip, dependsOn: jar) {
into(parentDir + '/lib') {
from jar
from configurations.runtime
}
}
into(parentDir + '/bin') {
from 'bin'
}

View File

@ -8,6 +8,7 @@ import java.nio.file.StandardCopyOption
apply plugin: 'elasticsearch.esplugin'
apply plugin: 'nebula.maven-base-publish'
apply plugin: 'nebula.maven-scm'
apply plugin: 'com.github.johnrengelman.shadow'
archivesBaseName = 'x-pack-core'
@ -27,17 +28,17 @@ dependencyLicenses {
dependencies {
compileOnly "org.elasticsearch:elasticsearch:${version}"
compile project(':x-pack:protocol')
compile "org.apache.httpcomponents:httpclient:${versions.httpclient}"
compile "org.apache.httpcomponents:httpcore:${versions.httpcore}"
compile "org.apache.httpcomponents:httpcore-nio:${versions.httpcore}"
compile "org.apache.httpcomponents:httpasyncclient:${versions.httpasyncclient}"
shadow "org.apache.httpcomponents:httpclient:${versions.httpclient}"
shadow "org.apache.httpcomponents:httpcore:${versions.httpcore}"
shadow "org.apache.httpcomponents:httpcore-nio:${versions.httpcore}"
shadow "org.apache.httpcomponents:httpasyncclient:${versions.httpasyncclient}"
compile "commons-logging:commons-logging:${versions.commonslogging}"
compile "commons-codec:commons-codec:${versions.commonscodec}"
shadow "commons-logging:commons-logging:${versions.commonslogging}"
shadow "commons-codec:commons-codec:${versions.commonscodec}"
// security deps
compile 'com.unboundid:unboundid-ldapsdk:3.2.0'
compile project(path: ':modules:transport-netty4', configuration: 'runtime')
shadow 'com.unboundid:unboundid-ldapsdk:3.2.0'
shadow project(path: ':modules:transport-netty4', configuration: 'runtime')
testCompile 'org.elasticsearch:securemock:1.2'
testCompile "org.elasticsearch:mocksocket:${versions.mocksocket}"
@ -107,7 +108,8 @@ test {
// TODO: don't publish test artifacts just to run messy tests, fix the tests!
// https://github.com/elastic/x-plugins/issues/724
configurations {
testArtifacts.extendsFrom testRuntime
testArtifacts.extendsFrom(testRuntime, shadow)
testArtifacts.exclude(group: project(':x-pack:protocol').group, module: project(':x-pack:protocol').name)
}
task testJar(type: Jar) {
appendix 'test'

View File

@ -28,6 +28,8 @@ import org.elasticsearch.discovery.DiscoveryModule;
import org.elasticsearch.env.Environment;
import org.elasticsearch.gateway.GatewayService;
import org.elasticsearch.protocol.xpack.XPackInfoResponse;
import org.elasticsearch.protocol.xpack.license.LicensesStatus;
import org.elasticsearch.protocol.xpack.license.PutLicenseResponse;
import org.elasticsearch.watcher.ResourceWatcherService;
import org.elasticsearch.xpack.core.XPackPlugin;
import org.elasticsearch.xpack.core.XPackSettings;

View File

@ -1,34 +0,0 @@
/*
* 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.license;
public enum LicensesStatus {
VALID((byte) 0),
INVALID((byte) 1),
EXPIRED((byte) 2);
private final byte id;
LicensesStatus(byte id) {
this.id = id;
}
public int id() {
return id;
}
public static LicensesStatus fromId(int id) {
if (id == 0) {
return VALID;
} else if (id == 1) {
return INVALID;
} else if (id == 2) {
return EXPIRED;
} else {
throw new IllegalStateException("no valid LicensesStatus for id=" + id);
}
}
}

View File

@ -7,6 +7,7 @@ package org.elasticsearch.license;
import org.elasticsearch.action.ActionListener;
import org.elasticsearch.client.ElasticsearchClient;
import org.elasticsearch.protocol.xpack.license.PutLicenseResponse;
public class LicensingClient {

View File

@ -6,6 +6,7 @@
package org.elasticsearch.license;
import org.elasticsearch.action.Action;
import org.elasticsearch.protocol.xpack.license.PutLicenseResponse;
public class PutLicenseAction extends Action<PutLicenseResponse> {

View File

@ -9,6 +9,7 @@ import org.elasticsearch.action.support.master.AcknowledgedRequestBuilder;
import org.elasticsearch.client.ElasticsearchClient;
import org.elasticsearch.common.bytes.BytesReference;
import org.elasticsearch.common.xcontent.XContentType;
import org.elasticsearch.protocol.xpack.license.PutLicenseResponse;
/**
* Register license request builder

View File

@ -1,119 +0,0 @@
/*
* 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.license;
import org.elasticsearch.action.support.master.AcknowledgedResponse;
import org.elasticsearch.common.Strings;
import org.elasticsearch.common.io.stream.StreamInput;
import org.elasticsearch.common.io.stream.StreamOutput;
import org.elasticsearch.common.xcontent.XContentBuilder;
import java.io.IOException;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
public class PutLicenseResponse extends AcknowledgedResponse {
private LicensesStatus status;
private Map<String, String[]> acknowledgeMessages;
private String acknowledgeHeader;
PutLicenseResponse() {
}
public PutLicenseResponse(boolean acknowledged, LicensesStatus status) {
this(acknowledged, status, null, Collections.<String, String[]>emptyMap());
}
public PutLicenseResponse(boolean acknowledged, LicensesStatus status, String acknowledgeHeader,
Map<String, String[]> acknowledgeMessages) {
super(acknowledged);
this.status = status;
this.acknowledgeHeader = acknowledgeHeader;
this.acknowledgeMessages = acknowledgeMessages;
}
public LicensesStatus status() {
return status;
}
public Map<String, String[]> acknowledgeMessages() {
return acknowledgeMessages;
}
public String acknowledgeHeader() {
return acknowledgeHeader;
}
@Override
public void readFrom(StreamInput in) throws IOException {
super.readFrom(in);
status = LicensesStatus.fromId(in.readVInt());
acknowledgeHeader = in.readOptionalString();
int size = in.readVInt();
Map<String, String[]> acknowledgeMessages = new HashMap<>(size);
for (int i = 0; i < size; i++) {
String feature = in.readString();
int nMessages = in.readVInt();
String[] messages = new String[nMessages];
for (int j = 0; j < nMessages; j++) {
messages[j] = in.readString();
}
acknowledgeMessages.put(feature, messages);
}
this.acknowledgeMessages = acknowledgeMessages;
}
@Override
public void writeTo(StreamOutput out) throws IOException {
super.writeTo(out);
out.writeVInt(status.id());
out.writeOptionalString(acknowledgeHeader);
out.writeVInt(acknowledgeMessages.size());
for (Map.Entry<String, String[]> entry : acknowledgeMessages.entrySet()) {
out.writeString(entry.getKey());
out.writeVInt(entry.getValue().length);
for (String message : entry.getValue()) {
out.writeString(message);
}
}
}
@Override
protected void addCustomFields(XContentBuilder builder, Params params) throws IOException {
switch (status) {
case VALID:
builder.field("license_status", "valid");
break;
case INVALID:
builder.field("license_status", "invalid");
break;
case EXPIRED:
builder.field("license_status", "expired");
break;
default:
throw new IllegalArgumentException("unknown status [" + status + "] found");
}
if (!acknowledgeMessages.isEmpty()) {
builder.startObject("acknowledge");
builder.field("message", acknowledgeHeader);
for (Map.Entry<String, String[]> entry : acknowledgeMessages.entrySet()) {
builder.startArray(entry.getKey());
for (String message : entry.getValue()) {
builder.value(message);
}
builder.endArray();
}
builder.endObject();
}
}
@Override
public String toString() {
return Strings.toString(this, true, true);
}
}

View File

@ -16,6 +16,7 @@ import org.elasticsearch.cluster.block.ClusterBlockLevel;
import org.elasticsearch.cluster.metadata.IndexNameExpressionResolver;
import org.elasticsearch.common.inject.Inject;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.protocol.xpack.license.PutLicenseResponse;
import org.elasticsearch.threadpool.ThreadPool;
import org.elasticsearch.transport.TransportService;

View File

@ -153,6 +153,8 @@ import org.elasticsearch.xpack.core.security.authc.support.mapper.expressiondsl.
import org.elasticsearch.xpack.core.security.authc.support.mapper.expressiondsl.ExceptExpression;
import org.elasticsearch.xpack.core.security.authc.support.mapper.expressiondsl.FieldExpression;
import org.elasticsearch.xpack.core.security.authc.support.mapper.expressiondsl.RoleMapperExpression;
import org.elasticsearch.xpack.core.security.authz.privilege.ConditionalClusterPrivileges;
import org.elasticsearch.xpack.core.security.authz.privilege.ConditionalClusterPrivilege;
import org.elasticsearch.xpack.core.security.transport.netty4.SecurityNetty4Transport;
import org.elasticsearch.xpack.core.ssl.SSLService;
import org.elasticsearch.xpack.core.ssl.action.GetCertificateInfoAction;
@ -371,6 +373,11 @@ public class XPackClientPlugin extends Plugin implements ActionPlugin, NetworkPl
new NamedWriteableRegistry.Entry(ClusterState.Custom.class, TokenMetaData.TYPE, TokenMetaData::new),
new NamedWriteableRegistry.Entry(NamedDiff.class, TokenMetaData.TYPE, TokenMetaData::readDiffFrom),
new NamedWriteableRegistry.Entry(XPackFeatureSet.Usage.class, XPackField.SECURITY, SecurityFeatureSetUsage::new),
// security : conditional privileges
new NamedWriteableRegistry.Entry(ConditionalClusterPrivilege.class,
ConditionalClusterPrivileges.ManageApplicationPrivileges.WRITEABLE_NAME,
ConditionalClusterPrivileges.ManageApplicationPrivileges::createFrom),
// security : role-mappings
new NamedWriteableRegistry.Entry(RoleMapperExpression.class, AllExpression.NAME, AllExpression::new),
new NamedWriteableRegistry.Entry(RoleMapperExpression.class, AnyExpression.NAME, AnyExpression::new),
new NamedWriteableRegistry.Entry(RoleMapperExpression.class, FieldExpression.NAME, FieldExpression::new),

View File

@ -52,7 +52,7 @@ import org.elasticsearch.xpack.core.action.TransportXPackInfoAction;
import org.elasticsearch.xpack.core.action.TransportXPackUsageAction;
import org.elasticsearch.xpack.core.action.XPackInfoAction;
import org.elasticsearch.xpack.core.action.XPackUsageAction;
import org.elasticsearch.xpack.core.ml.MLMetadataField;
import org.elasticsearch.xpack.core.ml.MlMetadata;
import org.elasticsearch.xpack.core.rest.action.RestXPackInfoAction;
import org.elasticsearch.xpack.core.rest.action.RestXPackUsageAction;
import org.elasticsearch.xpack.core.security.authc.TokenMetaData;
@ -197,7 +197,7 @@ public class XPackPlugin extends XPackClientPlugin implements ScriptPlugin, Exte
private static boolean alreadyContainsXPackCustomMetadata(ClusterState clusterState) {
final MetaData metaData = clusterState.metaData();
return metaData.custom(LicensesMetaData.TYPE) != null ||
metaData.custom(MLMetadataField.TYPE) != null ||
metaData.custom(MlMetadata.TYPE) != null ||
metaData.custom(WatcherMetaData.TYPE) != null ||
clusterState.custom(TokenMetaData.TYPE) != null;
}

View File

@ -1,21 +0,0 @@
/*
* 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.ml;
public final class MLMetadataField {
public static final String TYPE = "ml";
private MLMetadataField() {}
/**
* Namespaces the task ids for datafeeds.
* A job id can be used as a datafeed id, because they are stored separately in cluster state.
*/
public static String datafeedTaskId(String datafeedId) {
return "datafeed-" + datafeedId;
}
}

View File

@ -55,6 +55,7 @@ import java.util.stream.Collectors;
public class MlMetadata implements XPackPlugin.XPackMetaDataCustom {
public static final String TYPE = "ml";
private static final ParseField JOBS_FIELD = new ParseField("jobs");
private static final ParseField DATAFEEDS_FIELD = new ParseField("datafeeds");
@ -119,7 +120,7 @@ public class MlMetadata implements XPackPlugin.XPackMetaDataCustom {
@Override
public String getWriteableName() {
return MLMetadataField.TYPE;
return TYPE;
}
@Override
@ -213,7 +214,7 @@ public class MlMetadata implements XPackPlugin.XPackMetaDataCustom {
@Override
public String getWriteableName() {
return MLMetadataField.TYPE;
return TYPE;
}
static Diff<Job> readJobDiffFrom(StreamInput in) throws IOException {
@ -277,7 +278,7 @@ public class MlMetadata implements XPackPlugin.XPackMetaDataCustom {
public Builder deleteJob(String jobId, PersistentTasksCustomMetaData tasks) {
checkJobHasNoDatafeed(jobId);
JobState jobState = MlMetadata.getJobState(jobId, tasks);
JobState jobState = MlTasks.getJobState(jobId, tasks);
if (jobState.isAnyOf(JobState.CLOSED, JobState.FAILED) == false) {
throw ExceptionsHelper.conflictStatusException("Unexpected job state [" + jobState + "], expected [" +
JobState.CLOSED + " or " + JobState.FAILED + "]");
@ -362,7 +363,7 @@ public class MlMetadata implements XPackPlugin.XPackMetaDataCustom {
private void checkDatafeedIsStopped(Supplier<String> msg, String datafeedId, PersistentTasksCustomMetaData persistentTasks) {
if (persistentTasks != null) {
if (persistentTasks.getTask(MLMetadataField.datafeedTaskId(datafeedId)) != null) {
if (persistentTasks.getTask(MlTasks.datafeedTaskId(datafeedId)) != null) {
throw ExceptionsHelper.conflictStatusException(msg.get());
}
}
@ -399,7 +400,7 @@ public class MlMetadata implements XPackPlugin.XPackMetaDataCustom {
checkJobHasNoDatafeed(jobId);
if (allowDeleteOpenJob == false) {
PersistentTask<?> jobTask = getJobTask(jobId, tasks);
PersistentTask<?> jobTask = MlTasks.getJobTask(jobId, tasks);
if (jobTask != null) {
JobTaskState jobTaskState = (JobTaskState) jobTask.getState();
throw ExceptionsHelper.conflictStatusException("Cannot delete job [" + jobId + "] because the job is "
@ -420,56 +421,10 @@ public class MlMetadata implements XPackPlugin.XPackMetaDataCustom {
}
}
/**
* Namespaces the task ids for jobs.
* A datafeed id can be used as a job id, because they are stored separately in cluster state.
*/
public static String jobTaskId(String jobId) {
return "job-" + jobId;
}
@Nullable
public static PersistentTask<?> getJobTask(String jobId, @Nullable PersistentTasksCustomMetaData tasks) {
if (tasks == null) {
return null;
}
return tasks.getTask(jobTaskId(jobId));
}
@Nullable
public static PersistentTask<?> getDatafeedTask(String datafeedId, @Nullable PersistentTasksCustomMetaData tasks) {
if (tasks == null) {
return null;
}
return tasks.getTask(MLMetadataField.datafeedTaskId(datafeedId));
}
public static JobState getJobState(String jobId, @Nullable PersistentTasksCustomMetaData tasks) {
PersistentTask<?> task = getJobTask(jobId, tasks);
if (task != null) {
JobTaskState jobTaskState = (JobTaskState) task.getState();
if (jobTaskState == null) {
return JobState.OPENING;
}
return jobTaskState.getState();
}
// If we haven't opened a job than there will be no persistent task, which is the same as if the job was closed
return JobState.CLOSED;
}
public static DatafeedState getDatafeedState(String datafeedId, @Nullable PersistentTasksCustomMetaData tasks) {
PersistentTask<?> task = getDatafeedTask(datafeedId, tasks);
if (task != null && task.getState() != null) {
return (DatafeedState) task.getState();
} else {
// If we haven't started a datafeed then there will be no persistent task,
// which is the same as if the datafeed was't started
return DatafeedState.STOPPED;
}
}
public static MlMetadata getMlMetadata(ClusterState state) {
MlMetadata mlMetadata = (state == null) ? null : state.getMetaData().custom(MLMetadataField.TYPE);
MlMetadata mlMetadata = (state == null) ? null : state.getMetaData().custom(TYPE);
if (mlMetadata == null) {
return EMPTY_METADATA;
}

View File

@ -0,0 +1,70 @@
/*
* 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.ml;
import org.elasticsearch.common.Nullable;
import org.elasticsearch.persistent.PersistentTasksCustomMetaData;
import org.elasticsearch.xpack.core.ml.datafeed.DatafeedState;
import org.elasticsearch.xpack.core.ml.job.config.JobState;
import org.elasticsearch.xpack.core.ml.job.config.JobTaskState;
public final class MlTasks {
private MlTasks() {
}
/**
* Namespaces the task ids for jobs.
* A datafeed id can be used as a job id, because they are stored separately in cluster state.
*/
public static String jobTaskId(String jobId) {
return "job-" + jobId;
}
/**
* Namespaces the task ids for datafeeds.
* A job id can be used as a datafeed id, because they are stored separately in cluster state.
*/
public static String datafeedTaskId(String datafeedId) {
return "datafeed-" + datafeedId;
}
@Nullable
public static PersistentTasksCustomMetaData.PersistentTask<?> getJobTask(String jobId, @Nullable PersistentTasksCustomMetaData tasks) {
return tasks == null ? null : tasks.getTask(jobTaskId(jobId));
}
@Nullable
public static PersistentTasksCustomMetaData.PersistentTask<?> getDatafeedTask(String datafeedId,
@Nullable PersistentTasksCustomMetaData tasks) {
return tasks == null ? null : tasks.getTask(datafeedTaskId(datafeedId));
}
public static JobState getJobState(String jobId, @Nullable PersistentTasksCustomMetaData tasks) {
PersistentTasksCustomMetaData.PersistentTask<?> task = getJobTask(jobId, tasks);
if (task != null) {
JobTaskState jobTaskState = (JobTaskState) task.getState();
if (jobTaskState == null) {
return JobState.OPENING;
}
return jobTaskState.getState();
}
// If we haven't opened a job than there will be no persistent task, which is the same as if the job was closed
return JobState.CLOSED;
}
public static DatafeedState getDatafeedState(String datafeedId, @Nullable PersistentTasksCustomMetaData tasks) {
PersistentTasksCustomMetaData.PersistentTask<?> task = getDatafeedTask(datafeedId, tasks);
if (task != null && task.getState() != null) {
return (DatafeedState) task.getState();
} else {
// If we haven't started a datafeed then there will be no persistent task,
// which is the same as if the datafeed was't started
return DatafeedState.STOPPED;
}
}
}

View File

@ -19,7 +19,7 @@ import org.elasticsearch.common.xcontent.ToXContentObject;
import org.elasticsearch.common.xcontent.XContentBuilder;
import org.elasticsearch.common.xcontent.XContentParser;
import org.elasticsearch.tasks.Task;
import org.elasticsearch.xpack.core.ml.MLMetadataField;
import org.elasticsearch.xpack.core.ml.MlTasks;
import org.elasticsearch.xpack.core.ml.datafeed.DatafeedConfig;
import org.elasticsearch.xpack.core.ml.utils.ExceptionsHelper;
@ -84,11 +84,8 @@ public class IsolateDatafeedAction extends Action<IsolateDatafeedAction.Response
@Override
public boolean match(Task task) {
String expectedDescription = MLMetadataField.datafeedTaskId(datafeedId);
if (task instanceof StartDatafeedAction.DatafeedTaskMatcher && expectedDescription.equals(task.getDescription())){
return true;
}
return false;
String expectedDescription = MlTasks.datafeedTaskId(datafeedId);
return task instanceof StartDatafeedAction.DatafeedTaskMatcher && expectedDescription.equals(task.getDescription());
}
@Override

View File

@ -22,7 +22,7 @@ import org.elasticsearch.common.xcontent.ToXContentObject;
import org.elasticsearch.common.xcontent.XContentBuilder;
import org.elasticsearch.common.xcontent.XContentParser;
import org.elasticsearch.tasks.Task;
import org.elasticsearch.xpack.core.ml.MLMetadataField;
import org.elasticsearch.xpack.core.ml.MlTasks;
import org.elasticsearch.xpack.core.ml.datafeed.DatafeedConfig;
import org.elasticsearch.xpack.core.ml.utils.ExceptionsHelper;
@ -125,7 +125,7 @@ public class StopDatafeedAction extends Action<StopDatafeedAction.Response> {
@Override
public boolean match(Task task) {
for (String id : resolvedStartedDatafeedIds) {
String expectedDescription = MLMetadataField.datafeedTaskId(id);
String expectedDescription = MlTasks.datafeedTaskId(id);
if (task instanceof StartDatafeedAction.DatafeedTaskMatcher && expectedDescription.equals(task.getDescription())){
return true;
}

View File

@ -0,0 +1,17 @@
/*
* 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.privilege;
import java.util.Collection;
/**
* Interface implemented by all Requests that manage application privileges
*/
public interface ApplicationPrivilegesRequest {
Collection<String> getApplicationNames();
}

View File

@ -0,0 +1,26 @@
/*
* 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.privilege;
import org.elasticsearch.action.Action;
/**
* Action for deleting application privileges.
*/
public final class DeletePrivilegesAction extends Action<DeletePrivilegesResponse> {
public static final DeletePrivilegesAction INSTANCE = new DeletePrivilegesAction();
public static final String NAME = "cluster:admin/xpack/security/privilege/delete";
private DeletePrivilegesAction() {
super(NAME);
}
@Override
public DeletePrivilegesResponse newResponse() {
return new DeletePrivilegesResponse();
}
}

View File

@ -0,0 +1,101 @@
/*
* 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.privilege;
import org.elasticsearch.action.ActionRequest;
import org.elasticsearch.action.ActionRequestValidationException;
import org.elasticsearch.action.support.WriteRequest;
import org.elasticsearch.common.Strings;
import org.elasticsearch.common.io.stream.StreamInput;
import org.elasticsearch.common.io.stream.StreamOutput;
import java.io.IOException;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import static org.elasticsearch.action.ValidateActions.addValidationError;
/**
* A request to delete an application privilege.
*/
public final class DeletePrivilegesRequest extends ActionRequest
implements ApplicationPrivilegesRequest, WriteRequest<DeletePrivilegesRequest> {
private String application;
private String[] privileges;
private RefreshPolicy refreshPolicy = RefreshPolicy.IMMEDIATE;
public DeletePrivilegesRequest() {
this(null, Strings.EMPTY_ARRAY);
}
public DeletePrivilegesRequest(String application, String[] privileges) {
this.application = application;
this.privileges = privileges;
}
@Override
public DeletePrivilegesRequest setRefreshPolicy(RefreshPolicy refreshPolicy) {
this.refreshPolicy = refreshPolicy;
return this;
}
@Override
public RefreshPolicy getRefreshPolicy() {
return refreshPolicy;
}
@Override
public ActionRequestValidationException validate() {
ActionRequestValidationException validationException = null;
if (Strings.isNullOrEmpty(application)) {
validationException = addValidationError("application name is missing", validationException);
}
if (privileges == null || privileges.length == 0 || Arrays.stream(privileges).allMatch(Strings::isNullOrEmpty)) {
validationException = addValidationError("privileges are missing", validationException);
}
return validationException;
}
public void application(String application) {
this.application = application;
}
public String application() {
return application;
}
@Override
public Collection<String> getApplicationNames() {
return Collections.singleton(application);
}
public String[] privileges() {
return this.privileges;
}
public void privileges(String[] privileges) {
this.privileges = privileges;
}
@Override
public void readFrom(StreamInput in) throws IOException {
super.readFrom(in);
application = in.readString();
privileges = in.readStringArray();
refreshPolicy = RefreshPolicy.readFrom(in);
}
@Override
public void writeTo(StreamOutput out) throws IOException {
super.writeTo(out);
out.writeString(application);
out.writeStringArray(privileges);
refreshPolicy.writeTo(out);
}
}

View File

@ -0,0 +1,31 @@
/*
* 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.privilege;
import org.elasticsearch.action.ActionRequestBuilder;
import org.elasticsearch.action.support.WriteRequestBuilder;
import org.elasticsearch.client.ElasticsearchClient;
/**
* Builder for {@link DeletePrivilegesRequest}
*/
public final class DeletePrivilegesRequestBuilder extends ActionRequestBuilder<DeletePrivilegesRequest, DeletePrivilegesResponse>
implements WriteRequestBuilder<DeletePrivilegesRequestBuilder> {
public DeletePrivilegesRequestBuilder(ElasticsearchClient client, DeletePrivilegesAction action) {
super(client, action, new DeletePrivilegesRequest());
}
public DeletePrivilegesRequestBuilder privileges(String[] privileges) {
request.privileges(privileges);
return this;
}
public DeletePrivilegesRequestBuilder application(String applicationName) {
request.application(applicationName);
return this;
}
}

View File

@ -0,0 +1,57 @@
/*
* 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.privilege;
import org.elasticsearch.action.ActionResponse;
import org.elasticsearch.common.io.stream.StreamInput;
import org.elasticsearch.common.io.stream.StreamOutput;
import org.elasticsearch.common.xcontent.ToXContentObject;
import org.elasticsearch.common.xcontent.XContentBuilder;
import java.io.IOException;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
/**
* Response when deleting application privileges.
* Returns a collection of privileges that were successfully found and deleted.
*/
public final class DeletePrivilegesResponse extends ActionResponse implements ToXContentObject {
private Set<String> found;
public DeletePrivilegesResponse() {
}
public DeletePrivilegesResponse(Collection<String> found) {
this.found = Collections.unmodifiableSet(new HashSet<>(found));
}
@Override
public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
builder.startObject().field("found", found).endObject();
return builder;
}
public Set<String> found() {
return this.found;
}
@Override
public void readFrom(StreamInput in) throws IOException {
super.readFrom(in);
this.found = Collections.unmodifiableSet(in.readSet(StreamInput::readString));
}
@Override
public void writeTo(StreamOutput out) throws IOException {
super.writeTo(out);
out.writeCollection(found, StreamOutput::writeString);
}
}

View File

@ -0,0 +1,26 @@
/*
* 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.privilege;
import org.elasticsearch.action.Action;
/**
* Action for retrieving one or more application privileges from the security index
*/
public final class GetPrivilegesAction extends Action<GetPrivilegesResponse> {
public static final GetPrivilegesAction INSTANCE = new GetPrivilegesAction();
public static final String NAME = "cluster:admin/xpack/security/privilege/get";
private GetPrivilegesAction() {
super(NAME);
}
@Override
public GetPrivilegesResponse newResponse() {
return new GetPrivilegesResponse();
}
}

View File

@ -0,0 +1,78 @@
/*
* 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.privilege;
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 java.util.Collection;
import java.util.Collections;
import static org.elasticsearch.action.ValidateActions.addValidationError;
/**
* Request to retrieve one or more application privileges.
*/
public final class GetPrivilegesRequest extends ActionRequest implements ApplicationPrivilegesRequest {
@Nullable
private String application;
private String[] privileges;
public GetPrivilegesRequest() {
privileges = Strings.EMPTY_ARRAY;
}
@Override
public ActionRequestValidationException validate() {
ActionRequestValidationException validationException = null;
if (privileges == null) {
validationException = addValidationError("privileges cannot be null", validationException);
}
return validationException;
}
public void application(String application) {
this.application = application;
}
public String application() {
return this.application;
}
@Override
public Collection<String> getApplicationNames() {
return Collections.singleton(application);
}
public void privileges(String... privileges) {
this.privileges = privileges;
}
public String[] privileges() {
return this.privileges;
}
@Override
public void readFrom(StreamInput in) throws IOException {
super.readFrom(in);
application = in.readOptionalString();
privileges = in.readStringArray();
}
@Override
public void writeTo(StreamOutput out) throws IOException {
super.writeTo(out);
out.writeOptionalString(application);
out.writeStringArray(privileges);
}
}

View File

@ -0,0 +1,29 @@
/*
* 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.privilege;
import org.elasticsearch.action.ActionRequestBuilder;
import org.elasticsearch.client.ElasticsearchClient;
/**
* Builder for {@link GetPrivilegesRequest}
*/
public final class GetPrivilegesRequestBuilder extends ActionRequestBuilder<GetPrivilegesRequest, GetPrivilegesResponse> {
public GetPrivilegesRequestBuilder(ElasticsearchClient client, GetPrivilegesAction action) {
super(client, action, new GetPrivilegesRequest());
}
public GetPrivilegesRequestBuilder privileges(String... privileges) {
request.privileges(privileges);
return this;
}
public GetPrivilegesRequestBuilder application(String applicationName) {
request.application(applicationName);
return this;
}
}

View File

@ -0,0 +1,47 @@
/*
* 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.privilege;
import org.elasticsearch.action.ActionResponse;
import org.elasticsearch.common.io.stream.StreamInput;
import org.elasticsearch.common.io.stream.StreamOutput;
import org.elasticsearch.xpack.core.security.authz.privilege.ApplicationPrivilegeDescriptor;
import java.io.IOException;
import java.util.Collection;
/**
* Response containing one or more application privileges retrieved from the security index
*/
public final class GetPrivilegesResponse extends ActionResponse {
private ApplicationPrivilegeDescriptor[] privileges;
public GetPrivilegesResponse(ApplicationPrivilegeDescriptor... privileges) {
this.privileges = privileges;
}
public GetPrivilegesResponse(Collection<ApplicationPrivilegeDescriptor> privileges) {
this(privileges.toArray(new ApplicationPrivilegeDescriptor[privileges.size()]));
}
public ApplicationPrivilegeDescriptor[] privileges() {
return privileges;
}
@Override
public void readFrom(StreamInput in) throws IOException {
super.readFrom(in);
this.privileges = in.readArray(ApplicationPrivilegeDescriptor::new, ApplicationPrivilegeDescriptor[]::new);
}
@Override
public void writeTo(StreamOutput out) throws IOException {
super.writeTo(out);
out.writeArray(privileges);
}
}

View File

@ -0,0 +1,26 @@
/*
* 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.privilege;
import org.elasticsearch.action.Action;
/**
* Action for putting (adding/updating) one or more application privileges.
*/
public final class PutPrivilegesAction extends Action<PutPrivilegesResponse> {
public static final PutPrivilegesAction INSTANCE = new PutPrivilegesAction();
public static final String NAME = "cluster:admin/xpack/security/privilege/put";
private PutPrivilegesAction() {
super(NAME);
}
@Override
public PutPrivilegesResponse newResponse() {
return new PutPrivilegesResponse();
}
}

View File

@ -0,0 +1,124 @@
/*
* 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.privilege;
import org.elasticsearch.action.ActionRequest;
import org.elasticsearch.action.ActionRequestValidationException;
import org.elasticsearch.action.support.WriteRequest;
import org.elasticsearch.common.Strings;
import org.elasticsearch.common.io.stream.StreamInput;
import org.elasticsearch.common.io.stream.StreamOutput;
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.MetadataUtils;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
import static org.elasticsearch.action.ValidateActions.addValidationError;
/**
* Request object to put a one or more application privileges.
*/
public final class PutPrivilegesRequest extends ActionRequest implements ApplicationPrivilegesRequest, WriteRequest<PutPrivilegesRequest> {
private List<ApplicationPrivilegeDescriptor> privileges;
private RefreshPolicy refreshPolicy = RefreshPolicy.IMMEDIATE;
public PutPrivilegesRequest() {
privileges = Collections.emptyList();
}
@Override
public ActionRequestValidationException validate() {
ActionRequestValidationException validationException = null;
for (ApplicationPrivilegeDescriptor privilege : privileges) {
try {
ApplicationPrivilege.validateApplicationName(privilege.getApplication());
} catch (IllegalArgumentException e) {
validationException = addValidationError(e.getMessage(), validationException);
}
try {
ApplicationPrivilege.validatePrivilegeName(privilege.getName());
} catch (IllegalArgumentException e) {
validationException = addValidationError(e.getMessage(), validationException);
}
if (privilege.getActions().isEmpty()) {
validationException = addValidationError("Application privileges must have at least one action", validationException);
}
for (String action : privilege.getActions()) {
if (action.indexOf('/') == -1 && action.indexOf('*') == -1 && action.indexOf(':') == -1) {
validationException = addValidationError("action [" + action + "] must contain one of [ '/' , '*' , ':' ]",
validationException);
}
try {
ApplicationPrivilege.validatePrivilegeOrActionName(action);
} catch (IllegalArgumentException e) {
validationException = addValidationError(e.getMessage(), validationException);
}
}
if (MetadataUtils.containsReservedMetadata(privilege.getMetadata())) {
validationException = addValidationError("metadata keys may not start with [" + MetadataUtils.RESERVED_PREFIX
+ "] (in privilege " + privilege.getApplication() + ' ' + privilege.getName() + ")", validationException);
}
}
return validationException;
}
/**
* Should this request trigger a refresh ({@linkplain RefreshPolicy#IMMEDIATE}, the default), wait for a refresh (
* {@linkplain RefreshPolicy#WAIT_UNTIL}), or proceed ignore refreshes entirely ({@linkplain RefreshPolicy#NONE}).
*/
@Override
public RefreshPolicy getRefreshPolicy() {
return refreshPolicy;
}
@Override
public PutPrivilegesRequest setRefreshPolicy(RefreshPolicy refreshPolicy) {
this.refreshPolicy = refreshPolicy;
return this;
}
public List<ApplicationPrivilegeDescriptor> getPrivileges() {
return privileges;
}
public void setPrivileges(Collection<ApplicationPrivilegeDescriptor> privileges) {
this.privileges = Collections.unmodifiableList(new ArrayList<>(privileges));
}
@Override
public Collection<String> getApplicationNames() {
return Collections.unmodifiableSet(privileges.stream()
.map(ApplicationPrivilegeDescriptor::getApplication)
.collect(Collectors.toSet()));
}
@Override
public String toString() {
return getClass().getSimpleName() + "{[" + privileges.stream().map(Strings::toString).collect(Collectors.joining(","))
+ "];" + refreshPolicy + "}";
}
@Override
public void readFrom(StreamInput in) throws IOException {
super.readFrom(in);
privileges = Collections.unmodifiableList(in.readList(ApplicationPrivilegeDescriptor::new));
refreshPolicy = RefreshPolicy.readFrom(in);
}
@Override
public void writeTo(StreamOutput out) throws IOException {
super.writeTo(out);
out.writeList(privileges);
refreshPolicy.writeTo(out);
}
}

View File

@ -0,0 +1,131 @@
/*
* 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.privilege;
import org.elasticsearch.ElasticsearchParseException;
import org.elasticsearch.action.ActionRequestBuilder;
import org.elasticsearch.action.support.WriteRequestBuilder;
import org.elasticsearch.client.ElasticsearchClient;
import org.elasticsearch.common.Strings;
import org.elasticsearch.common.bytes.BytesReference;
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.privilege.ApplicationPrivilegeDescriptor;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
/**
* Request builder for {@link PutPrivilegesRequest}
*/
public final class PutPrivilegesRequestBuilder extends ActionRequestBuilder<PutPrivilegesRequest, PutPrivilegesResponse>
implements WriteRequestBuilder<PutPrivilegesRequestBuilder> {
public PutPrivilegesRequestBuilder(ElasticsearchClient client, PutPrivilegesAction action) {
super(client, action, new PutPrivilegesRequest());
}
/**
* Populate the put privileges request using the given source, application name and privilege name
* The source must contain a single privilege object which matches the application and privilege names.
*/
public PutPrivilegesRequestBuilder source(String applicationName, String expectedName,
BytesReference source, XContentType xContentType)
throws IOException {
Objects.requireNonNull(xContentType);
// EMPTY is ok here because we never call namedObject
try (InputStream stream = source.streamInput();
XContentParser parser = xContentType.xContent()
.createParser(NamedXContentRegistry.EMPTY, LoggingDeprecationHandler.INSTANCE, stream)) {
XContentParser.Token token = parser.currentToken();
if (token == null) {
token = parser.nextToken();
}
if (token == XContentParser.Token.START_OBJECT) {
final ApplicationPrivilegeDescriptor privilege = parsePrivilege(parser, applicationName, expectedName);
this.request.setPrivileges(Collections.singleton(privilege));
} else {
throw new ElasticsearchParseException("expected an object but found {} instead", token);
}
}
return this;
}
ApplicationPrivilegeDescriptor parsePrivilege(XContentParser parser, String applicationName, String privilegeName) throws IOException {
ApplicationPrivilegeDescriptor privilege = ApplicationPrivilegeDescriptor.parse(parser, applicationName, privilegeName, false);
checkPrivilegeName(privilege, applicationName, privilegeName);
return privilege;
}
/**
* Populate the put privileges request using the given source, application name and privilege name
* The source must contain a top-level object, keyed by application name.
* The value for each application-name, is an object keyed by privilege name.
* The value for each privilege-name is a privilege object which much match the application and privilege names in which it is nested.
*/
public PutPrivilegesRequestBuilder source(BytesReference source, XContentType xContentType)
throws IOException {
Objects.requireNonNull(xContentType);
// EMPTY is ok here because we never call namedObject
try (InputStream stream = source.streamInput();
XContentParser parser = xContentType.xContent()
.createParser(NamedXContentRegistry.EMPTY, LoggingDeprecationHandler.INSTANCE, stream)) {
XContentParser.Token token = parser.currentToken();
if (token == null) {
token = parser.nextToken();
}
if (token != XContentParser.Token.START_OBJECT) {
throw new ElasticsearchParseException("expected object but found {} instead", token);
}
List<ApplicationPrivilegeDescriptor> privileges = new ArrayList<>();
while (parser.nextToken() != XContentParser.Token.END_OBJECT) {
token = parser.currentToken();
assert token == XContentParser.Token.FIELD_NAME : "Invalid token " + token;
final String applicationName = parser.currentName();
token = parser.nextToken();
if (token != XContentParser.Token.START_OBJECT) {
throw new ElasticsearchParseException("expected the value for {} to be an object, but found {} instead",
applicationName, token);
}
while (parser.nextToken() != XContentParser.Token.END_OBJECT) {
token = parser.currentToken();
assert (token == XContentParser.Token.FIELD_NAME);
final String privilegeName = parser.currentName();
token = parser.nextToken();
if (token != XContentParser.Token.START_OBJECT) {
throw new ElasticsearchParseException("expected the value for {} to be an object, but found {} instead",
applicationName, token);
}
privileges.add(parsePrivilege(parser, applicationName, privilegeName));
}
}
request.setPrivileges(privileges);
}
return this;
}
private void checkPrivilegeName(ApplicationPrivilegeDescriptor privilege, String applicationName, String providedName) {
final String privilegeName = privilege.getName();
if (Strings.isNullOrEmpty(applicationName) == false && applicationName.equals(privilege.getApplication()) == false) {
throw new IllegalArgumentException("privilege application [" + privilege.getApplication()
+ "] in source does not match the provided application [" + applicationName + "]");
}
if (Strings.isNullOrEmpty(providedName) == false && providedName.equals(privilegeName) == false) {
throw new IllegalArgumentException("privilege name [" + privilegeName
+ "] in source does not match the provided name [" + providedName + "]");
}
}
}

View File

@ -0,0 +1,60 @@
/*
* 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.privilege;
import org.elasticsearch.action.ActionResponse;
import org.elasticsearch.common.io.stream.StreamInput;
import org.elasticsearch.common.io.stream.StreamOutput;
import org.elasticsearch.common.xcontent.ToXContentObject;
import org.elasticsearch.common.xcontent.XContentBuilder;
import java.io.IOException;
import java.util.Collections;
import java.util.List;
import java.util.Map;
/**
* Response when adding one or more application privileges to the security index.
* Returns a collection of the privileges that were created (by implication, any other privileges were updated).
*/
public final class PutPrivilegesResponse extends ActionResponse implements ToXContentObject {
private Map<String, List<String>> created;
PutPrivilegesResponse() {
this(Collections.emptyMap());
}
public PutPrivilegesResponse(Map<String, List<String>> created) {
this.created = Collections.unmodifiableMap(created);
}
/**
* Get a list of privileges that were created (as opposed to updated)
* @return A map from <em>Application Name</em> to a {@code List} of <em>privilege names</em>
*/
public Map<String, List<String>> created() {
return created;
}
@Override
public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
builder.startObject().field("created", created).endObject();
return builder;
}
@Override
public void writeTo(StreamOutput out) throws IOException {
super.writeTo(out);
out.writeMap(created, StreamOutput::writeString, StreamOutput::writeStringList);
}
@Override
public void readFrom(StreamInput in) throws IOException {
super.readFrom(in);
this.created = Collections.unmodifiableMap(in.readMap(StreamInput::readString, si -> si.readList(StreamInput::readString)));
}
}

View File

@ -5,6 +5,7 @@
*/
package org.elasticsearch.xpack.core.security.action.role;
import org.elasticsearch.Version;
import org.elasticsearch.action.ActionRequest;
import org.elasticsearch.action.ActionRequestValidationException;
import org.elasticsearch.action.support.WriteRequest;
@ -14,11 +15,15 @@ import org.elasticsearch.common.bytes.BytesReference;
import org.elasticsearch.common.io.stream.StreamInput;
import org.elasticsearch.common.io.stream.StreamOutput;
import org.elasticsearch.xpack.core.security.authz.RoleDescriptor;
import org.elasticsearch.xpack.core.security.authz.privilege.ApplicationPrivilege;
import org.elasticsearch.xpack.core.security.authz.privilege.ConditionalClusterPrivilege;
import org.elasticsearch.xpack.core.security.authz.privilege.ConditionalClusterPrivileges;
import org.elasticsearch.xpack.core.security.support.MetadataUtils;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;
@ -31,11 +36,13 @@ public class PutRoleRequest extends ActionRequest implements WriteRequest<PutRol
private String name;
private String[] clusterPrivileges = Strings.EMPTY_ARRAY;
private ConditionalClusterPrivilege[] conditionalClusterPrivileges = ConditionalClusterPrivileges.EMPTY_ARRAY;
private List<RoleDescriptor.IndicesPrivileges> indicesPrivileges = new ArrayList<>();
private List<RoleDescriptor.ApplicationResourcePrivileges> applicationPrivileges = new ArrayList<>();
private String[] runAs = Strings.EMPTY_ARRAY;
private RefreshPolicy refreshPolicy = RefreshPolicy.IMMEDIATE;
private Map<String, Object> metadata;
public PutRoleRequest() {
}
@ -45,9 +52,25 @@ public class PutRoleRequest extends ActionRequest implements WriteRequest<PutRol
if (name == null) {
validationException = addValidationError("role name is missing", validationException);
}
if(applicationPrivileges != null) {
for (RoleDescriptor.ApplicationResourcePrivileges privilege : applicationPrivileges) {
try {
ApplicationPrivilege.validateApplicationNameOrWildcard(privilege.getApplication());
} catch (IllegalArgumentException e) {
validationException = addValidationError(e.getMessage(), validationException);
}
for (String name : privilege.getPrivileges()) {
try {
ApplicationPrivilege.validatePrivilegeOrActionName(name);
} catch (IllegalArgumentException e) {
validationException = addValidationError(e.getMessage(), validationException);
}
}
}
}
if (metadata != null && MetadataUtils.containsReservedMetadata(metadata)) {
validationException =
addValidationError("metadata keys may not start with [" + MetadataUtils.RESERVED_PREFIX + "]", validationException);
addValidationError("metadata keys may not start with [" + MetadataUtils.RESERVED_PREFIX + "]", validationException);
}
return validationException;
}
@ -60,6 +83,10 @@ public class PutRoleRequest extends ActionRequest implements WriteRequest<PutRol
this.clusterPrivileges = clusterPrivileges;
}
void conditionalCluster(ConditionalClusterPrivilege... conditionalClusterPrivileges) {
this.conditionalClusterPrivileges = conditionalClusterPrivileges;
}
void addIndex(RoleDescriptor.IndicesPrivileges... privileges) {
this.indicesPrivileges.addAll(Arrays.asList(privileges));
}
@ -75,6 +102,10 @@ public class PutRoleRequest extends ActionRequest implements WriteRequest<PutRol
.build());
}
void addApplicationPrivileges(RoleDescriptor.ApplicationResourcePrivileges... privileges) {
this.applicationPrivileges.addAll(Arrays.asList(privileges));
}
public void runAs(String... usernames) {
this.runAs = usernames;
}
@ -110,6 +141,14 @@ public class PutRoleRequest extends ActionRequest implements WriteRequest<PutRol
return indicesPrivileges.toArray(new RoleDescriptor.IndicesPrivileges[indicesPrivileges.size()]);
}
public List<RoleDescriptor.ApplicationResourcePrivileges> applicationPrivileges() {
return Collections.unmodifiableList(applicationPrivileges);
}
public ConditionalClusterPrivilege[] conditionalClusterPrivileges() {
return conditionalClusterPrivileges;
}
public String[] runAs() {
return runAs;
}
@ -128,6 +167,10 @@ public class PutRoleRequest extends ActionRequest implements WriteRequest<PutRol
for (int i = 0; i < indicesSize; i++) {
indicesPrivileges.add(RoleDescriptor.IndicesPrivileges.createFrom(in));
}
if (in.getVersion().onOrAfter(Version.V_7_0_0_alpha1)) {
applicationPrivileges = in.readList(RoleDescriptor.ApplicationResourcePrivileges::createFrom);
conditionalClusterPrivileges = ConditionalClusterPrivileges.readArray(in);
}
runAs = in.readStringArray();
refreshPolicy = RefreshPolicy.readFrom(in);
metadata = in.readMap();
@ -142,6 +185,10 @@ public class PutRoleRequest extends ActionRequest implements WriteRequest<PutRol
for (RoleDescriptor.IndicesPrivileges index : indicesPrivileges) {
index.writeTo(out);
}
if (out.getVersion().onOrAfter(Version.V_7_0_0_alpha1)) {
out.writeStreamableList(applicationPrivileges);
ConditionalClusterPrivileges.writeArray(out, this.conditionalClusterPrivileges);
}
out.writeStringArray(runAs);
refreshPolicy.writeTo(out);
out.writeMap(metadata);
@ -151,7 +198,11 @@ public class PutRoleRequest extends ActionRequest implements WriteRequest<PutRol
return new RoleDescriptor(name,
clusterPrivileges,
indicesPrivileges.toArray(new RoleDescriptor.IndicesPrivileges[indicesPrivileges.size()]),
applicationPrivileges.toArray(new RoleDescriptor.ApplicationResourcePrivileges[applicationPrivileges.size()]),
conditionalClusterPrivileges,
runAs,
metadata);
metadata,
Collections.emptyMap());
}
}
}

View File

@ -40,7 +40,9 @@ public class PutRoleRequestBuilder extends ActionRequestBuilder<PutRoleRequest,
assert name.equals(descriptor.getName());
request.name(name);
request.cluster(descriptor.getClusterPrivileges());
request.conditionalCluster(descriptor.getConditionalClusterPrivileges());
request.addIndex(descriptor.getIndicesPrivileges());
request.addApplicationPrivileges(descriptor.getApplicationPrivileges());
request.runAs(descriptor.getRunAs());
request.metadata(descriptor.getMetadata());
return this;

View File

@ -5,11 +5,14 @@
*/
package org.elasticsearch.xpack.core.security.action.user;
import org.elasticsearch.Version;
import org.elasticsearch.action.ActionRequest;
import org.elasticsearch.action.ActionRequestValidationException;
import org.elasticsearch.common.io.stream.StreamInput;
import org.elasticsearch.common.io.stream.StreamOutput;
import org.elasticsearch.xpack.core.security.authz.RoleDescriptor;
import org.elasticsearch.xpack.core.security.authz.RoleDescriptor.ApplicationResourcePrivileges;
import org.elasticsearch.xpack.core.security.authz.privilege.ApplicationPrivilege;
import java.io.IOException;
@ -23,6 +26,7 @@ public class HasPrivilegesRequest extends ActionRequest implements UserRequest {
private String username;
private String[] clusterPrivileges;
private RoleDescriptor.IndicesPrivileges[] indexPrivileges;
private ApplicationResourcePrivileges[] applicationPrivileges;
@Override
public ActionRequestValidationException validate() {
@ -33,9 +37,21 @@ public class HasPrivilegesRequest extends ActionRequest implements UserRequest {
if (indexPrivileges == null) {
validationException = addValidationError("indexPrivileges must not be null", validationException);
}
if (clusterPrivileges != null && clusterPrivileges.length == 0 && indexPrivileges != null && indexPrivileges.length == 0) {
validationException = addValidationError("clusterPrivileges and indexPrivileges cannot both be empty",
validationException);
if (applicationPrivileges == null) {
validationException = addValidationError("applicationPrivileges must not be null", validationException);
} else {
for (ApplicationResourcePrivileges applicationPrivilege : applicationPrivileges) {
try {
ApplicationPrivilege.validateApplicationName(applicationPrivilege.getApplication());
} catch (IllegalArgumentException e) {
validationException = addValidationError(e.getMessage(), validationException);
}
}
}
if (clusterPrivileges != null && clusterPrivileges.length == 0
&& indexPrivileges != null && indexPrivileges.length == 0
&& applicationPrivileges != null && applicationPrivileges.length == 0) {
validationException = addValidationError("must specify at least one privilege", validationException);
}
return validationException;
}
@ -67,6 +83,10 @@ public class HasPrivilegesRequest extends ActionRequest implements UserRequest {
return clusterPrivileges;
}
public ApplicationResourcePrivileges[] applicationPrivileges() {
return applicationPrivileges;
}
public void indexPrivileges(RoleDescriptor.IndicesPrivileges... privileges) {
this.indexPrivileges = privileges;
}
@ -75,6 +95,10 @@ public class HasPrivilegesRequest extends ActionRequest implements UserRequest {
this.clusterPrivileges = privileges;
}
public void applicationPrivileges(ApplicationResourcePrivileges... appPrivileges) {
this.applicationPrivileges = appPrivileges;
}
@Override
public void readFrom(StreamInput in) throws IOException {
super.readFrom(in);
@ -85,6 +109,9 @@ public class HasPrivilegesRequest extends ActionRequest implements UserRequest {
for (int i = 0; i < indexSize; i++) {
indexPrivileges[i] = RoleDescriptor.IndicesPrivileges.createFrom(in);
}
if (in.getVersion().onOrAfter(Version.V_7_0_0_alpha1)) {
applicationPrivileges = in.readArray(ApplicationResourcePrivileges::createFrom, ApplicationResourcePrivileges[]::new);
}
}
@Override
@ -96,6 +123,9 @@ public class HasPrivilegesRequest extends ActionRequest implements UserRequest {
for (RoleDescriptor.IndicesPrivileges priv : indexPrivileges) {
priv.writeTo(out);
}
if (out.getVersion().onOrAfter(Version.V_7_0_0_alpha1)) {
out.writeArray(ApplicationResourcePrivileges::write, applicationPrivileges);
}
}
}

View File

@ -39,6 +39,7 @@ public class HasPrivilegesRequestBuilder
request.username(username);
request.indexPrivileges(role.getIndicesPrivileges());
request.clusterPrivileges(role.getClusterPrivileges());
request.applicationPrivileges(role.getApplicationPrivileges());
return this;
}
}

View File

@ -5,6 +5,11 @@
*/
package org.elasticsearch.xpack.core.security.action.user;
import org.elasticsearch.Version;
import org.elasticsearch.action.ActionResponse;
import org.elasticsearch.common.io.stream.StreamInput;
import org.elasticsearch.common.io.stream.StreamOutput;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
@ -14,27 +19,27 @@ import java.util.List;
import java.util.Map;
import java.util.Objects;
import org.elasticsearch.action.ActionResponse;
import org.elasticsearch.common.io.stream.StreamInput;
import org.elasticsearch.common.io.stream.StreamOutput;
/**
* Response for a {@link HasPrivilegesRequest}
*/
public class HasPrivilegesResponse extends ActionResponse {
private boolean completeMatch;
private Map<String, Boolean> cluster;
private List<IndexPrivileges> index;
private List<ResourcePrivileges> index;
private Map<String, List<ResourcePrivileges>> application;
public HasPrivilegesResponse() {
this(true, Collections.emptyMap(), Collections.emptyList());
this(true, Collections.emptyMap(), Collections.emptyList(), Collections.emptyMap());
}
public HasPrivilegesResponse(boolean completeMatch, Map<String, Boolean> cluster, Collection<IndexPrivileges> index) {
public HasPrivilegesResponse(boolean completeMatch, Map<String, Boolean> cluster, Collection<ResourcePrivileges> index,
Map<String, Collection<ResourcePrivileges>> application) {
super();
this.completeMatch = completeMatch;
this.cluster = new HashMap<>(cluster);
this.index = new ArrayList<>(index);
this.application = new HashMap<>();
application.forEach((key, val) -> this.application.put(key, Collections.unmodifiableList(new ArrayList<>(val))));
}
public boolean isCompleteMatch() {
@ -45,44 +50,67 @@ public class HasPrivilegesResponse extends ActionResponse {
return Collections.unmodifiableMap(cluster);
}
public List<IndexPrivileges> getIndexPrivileges() {
public List<ResourcePrivileges> getIndexPrivileges() {
return Collections.unmodifiableList(index);
}
/**
* Retrieves the results from checking application privileges,
* @return A {@code Map} keyed by application-name
*/
public Map<String, List<ResourcePrivileges>> getApplicationPrivileges() {
return Collections.unmodifiableMap(application);
}
public void readFrom(StreamInput in) throws IOException {
super.readFrom(in);
completeMatch = in.readBoolean();
int count = in.readVInt();
index = new ArrayList<>(count);
index = readResourcePrivileges(in);
if (in.getVersion().onOrAfter(Version.V_7_0_0_alpha1)) {
application = in.readMap(StreamInput::readString, HasPrivilegesResponse::readResourcePrivileges);
}
}
private static List<ResourcePrivileges> readResourcePrivileges(StreamInput in) throws IOException {
final int count = in.readVInt();
final List<ResourcePrivileges> list = new ArrayList<>(count);
for (int i = 0; i < count; i++) {
final String index = in.readString();
final Map<String, Boolean> privileges = in.readMap(StreamInput::readString, StreamInput::readBoolean);
this.index.add(new IndexPrivileges(index, privileges));
list.add(new ResourcePrivileges(index, privileges));
}
return list;
}
@Override
public void writeTo(StreamOutput out) throws IOException {
super.writeTo(out);
out.writeBoolean(completeMatch);
out.writeVInt(index.size());
for (IndexPrivileges index : index) {
out.writeString(index.index);
out.writeMap(index.privileges, StreamOutput::writeString, StreamOutput::writeBoolean);
writeResourcePrivileges(out, index);
if (out.getVersion().onOrAfter(Version.V_7_0_0_alpha1)) {
out.writeMap(application, StreamOutput::writeString, HasPrivilegesResponse::writeResourcePrivileges);
}
}
public static class IndexPrivileges {
private final String index;
private static void writeResourcePrivileges(StreamOutput out, List<ResourcePrivileges> privileges) throws IOException {
out.writeVInt(privileges.size());
for (ResourcePrivileges priv : privileges) {
out.writeString(priv.resource);
out.writeMap(priv.privileges, StreamOutput::writeString, StreamOutput::writeBoolean);
}
}
public static class ResourcePrivileges {
private final String resource;
private final Map<String, Boolean> privileges;
public IndexPrivileges(String index, Map<String, Boolean> privileges) {
this.index = Objects.requireNonNull(index);
public ResourcePrivileges(String resource, Map<String, Boolean> privileges) {
this.resource = Objects.requireNonNull(resource);
this.privileges = Collections.unmodifiableMap(privileges);
}
public String getIndex() {
return index;
public String getResource() {
return resource;
}
public Map<String, Boolean> getPrivileges() {
@ -92,14 +120,14 @@ public class HasPrivilegesResponse extends ActionResponse {
@Override
public String toString() {
return getClass().getSimpleName() + "{" +
"index='" + index + '\'' +
"resource='" + resource + '\'' +
", privileges=" + privileges +
'}';
}
@Override
public int hashCode() {
int result = index.hashCode();
int result = resource.hashCode();
result = 31 * result + privileges.hashCode();
return result;
}
@ -113,8 +141,8 @@ public class HasPrivilegesResponse extends ActionResponse {
return false;
}
final IndexPrivileges other = (IndexPrivileges) o;
return this.index.equals(other.index) && this.privileges.equals(other.privileges);
final ResourcePrivileges other = (ResourcePrivileges) o;
return this.resource.equals(other.resource) && this.privileges.equals(other.privileges);
}
}
}

View File

@ -10,60 +10,132 @@ import org.elasticsearch.common.util.concurrent.ThreadContext;
import org.elasticsearch.rest.RestRequest;
import org.elasticsearch.rest.RestStatus;
import org.elasticsearch.transport.TransportMessage;
import org.elasticsearch.xpack.core.XPackField;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import static org.elasticsearch.xpack.core.security.support.Exceptions.authenticationError;
/**
* The default implementation of a {@link AuthenticationFailureHandler}. This handler will return an exception with a
* RestStatus of 401 and the WWW-Authenticate header with a Basic challenge.
* The default implementation of a {@link AuthenticationFailureHandler}. This
* handler will return an exception with a RestStatus of 401 and default failure
* response headers like 'WWW-Authenticate'
*/
public class DefaultAuthenticationFailureHandler implements AuthenticationFailureHandler {
private final Map<String, List<String>> defaultFailureResponseHeaders;
/**
* Constructs default authentication failure handler
*
* @deprecated replaced by {@link #DefaultAuthenticationFailureHandler(Map)}
*/
@Deprecated
public DefaultAuthenticationFailureHandler() {
this(null);
}
/**
* Constructs default authentication failure handler with provided default
* response headers.
*
* @param failureResponseHeaders Map of header key and list of header values to
* be sent as failure response.
* @see Realm#getAuthenticationFailureHeaders()
*/
public DefaultAuthenticationFailureHandler(Map<String, List<String>> failureResponseHeaders) {
if (failureResponseHeaders == null || failureResponseHeaders.isEmpty()) {
failureResponseHeaders = Collections.singletonMap("WWW-Authenticate",
Collections.singletonList("Basic realm=\"" + XPackField.SECURITY + "\" charset=\"UTF-8\""));
}
this.defaultFailureResponseHeaders = Collections.unmodifiableMap(failureResponseHeaders);
}
@Override
public ElasticsearchSecurityException failedAuthentication(RestRequest request, AuthenticationToken token,
ThreadContext context) {
return authenticationError("unable to authenticate user [{}] for REST request [{}]", token.principal(), request.uri());
public ElasticsearchSecurityException failedAuthentication(RestRequest request, AuthenticationToken token, ThreadContext context) {
return createAuthenticationError("unable to authenticate user [{}] for REST request [{}]", null, token.principal(), request.uri());
}
@Override
public ElasticsearchSecurityException failedAuthentication(TransportMessage message, AuthenticationToken token, String action,
ThreadContext context) {
return authenticationError("unable to authenticate user [{}] for action [{}]", token.principal(), action);
ThreadContext context) {
return createAuthenticationError("unable to authenticate user [{}] for action [{}]", null, token.principal(), action);
}
@Override
public ElasticsearchSecurityException exceptionProcessingRequest(RestRequest request, Exception e, ThreadContext context) {
if (e instanceof ElasticsearchSecurityException) {
assert ((ElasticsearchSecurityException) e).status() == RestStatus.UNAUTHORIZED;
assert ((ElasticsearchSecurityException) e).getHeader("WWW-Authenticate").size() == 1;
return (ElasticsearchSecurityException) e;
}
return authenticationError("error attempting to authenticate request", e);
return createAuthenticationError("error attempting to authenticate request", e, (Object[]) null);
}
@Override
public ElasticsearchSecurityException exceptionProcessingRequest(TransportMessage message, String action, Exception e,
ThreadContext context) {
if (e instanceof ElasticsearchSecurityException) {
assert ((ElasticsearchSecurityException) e).status() == RestStatus.UNAUTHORIZED;
assert ((ElasticsearchSecurityException) e).getHeader("WWW-Authenticate").size() == 1;
return (ElasticsearchSecurityException) e;
}
return authenticationError("error attempting to authenticate request", e);
ThreadContext context) {
return createAuthenticationError("error attempting to authenticate request", e, (Object[]) null);
}
@Override
public ElasticsearchSecurityException missingToken(RestRequest request, ThreadContext context) {
return authenticationError("missing authentication token for REST request [{}]", request.uri());
return createAuthenticationError("missing authentication token for REST request [{}]", null, request.uri());
}
@Override
public ElasticsearchSecurityException missingToken(TransportMessage message, String action, ThreadContext context) {
return authenticationError("missing authentication token for action [{}]", action);
return createAuthenticationError("missing authentication token for action [{}]", null, action);
}
@Override
public ElasticsearchSecurityException authenticationRequired(String action, ThreadContext context) {
return authenticationError("action [{}] requires authentication", action);
return createAuthenticationError("action [{}] requires authentication", null, action);
}
/**
* Creates an instance of {@link ElasticsearchSecurityException} with
* {@link RestStatus#UNAUTHORIZED} status.
* <p>
* Also adds default failure response headers as configured for this
* {@link DefaultAuthenticationFailureHandler}
* <p>
* It may replace existing response headers if the cause is an instance of
* {@link ElasticsearchSecurityException}
*
* @param message error message
* @param t cause, if it is an instance of
* {@link ElasticsearchSecurityException} asserts status is
* RestStatus.UNAUTHORIZED and adds headers to it, else it will
* create a new instance of {@link ElasticsearchSecurityException}
* @param args error message args
* @return instance of {@link ElasticsearchSecurityException}
*/
private ElasticsearchSecurityException createAuthenticationError(final String message, final Throwable t, final Object... args) {
final ElasticsearchSecurityException ese;
final boolean containsNegotiateWithToken;
if (t instanceof ElasticsearchSecurityException) {
assert ((ElasticsearchSecurityException) t).status() == RestStatus.UNAUTHORIZED;
ese = (ElasticsearchSecurityException) t;
if (ese.getHeader("WWW-Authenticate") != null && ese.getHeader("WWW-Authenticate").isEmpty() == false) {
/**
* If 'WWW-Authenticate' header is present with 'Negotiate ' then do not
* replace. In case of kerberos spnego mechanism, we use
* 'WWW-Authenticate' header value to communicate outToken to peer.
*/
containsNegotiateWithToken =
ese.getHeader("WWW-Authenticate").stream()
.anyMatch(s -> s != null && s.regionMatches(true, 0, "Negotiate ", 0, "Negotiate ".length()));
} else {
containsNegotiateWithToken = false;
}
} else {
ese = authenticationError(message, t, args);
containsNegotiateWithToken = false;
}
defaultFailureResponseHeaders.entrySet().stream().forEach((e) -> {
if (containsNegotiateWithToken && e.getKey().equalsIgnoreCase("WWW-Authenticate")) {
return;
}
// If it is already present then it will replace the existing header.
ese.addHeader(e.getKey(), e.getValue());
});
return ese;
}
}

View File

@ -8,9 +8,12 @@ package org.elasticsearch.xpack.core.security.authc;
import org.apache.logging.log4j.Logger;
import org.elasticsearch.action.ActionListener;
import org.elasticsearch.common.util.concurrent.ThreadContext;
import org.elasticsearch.xpack.core.XPackField;
import org.elasticsearch.xpack.core.security.user.User;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
@ -56,6 +59,18 @@ public abstract class Realm implements Comparable<Realm> {
return config.order;
}
/**
* Each realm can define response headers to be sent on failure.
* <p>
* By default it adds 'WWW-Authenticate' header with auth scheme 'Basic'.
*
* @return Map of authentication failure response headers.
*/
public Map<String, List<String>> getAuthenticationFailureHeaders() {
return Collections.singletonMap("WWW-Authenticate",
Collections.singletonList("Basic realm=\"" + XPackField.SECURITY + "\" charset=\"UTF-8\""));
}
@Override
public int compareTo(Realm other) {
int result = Integer.compare(config.order, other.config.order);

View File

@ -0,0 +1,50 @@
/*
* 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.authc.kerberos;
import org.elasticsearch.common.settings.Setting;
import org.elasticsearch.common.settings.Setting.Property;
import org.elasticsearch.common.unit.TimeValue;
import org.elasticsearch.common.util.set.Sets;
import java.util.Set;
/**
* Kerberos Realm settings
*/
public final class KerberosRealmSettings {
public static final String TYPE = "kerberos";
/**
* Kerberos key tab for Elasticsearch service<br>
* Uses single key tab for multiple service accounts.
*/
public static final Setting<String> HTTP_SERVICE_KEYTAB_PATH =
Setting.simpleString("keytab.path", Property.NodeScope);
public static final Setting<Boolean> SETTING_KRB_DEBUG_ENABLE =
Setting.boolSetting("krb.debug", Boolean.FALSE, Property.NodeScope);
public static final Setting<Boolean> SETTING_REMOVE_REALM_NAME =
Setting.boolSetting("remove_realm_name", Boolean.FALSE, Property.NodeScope);
// Cache
private static final TimeValue DEFAULT_TTL = TimeValue.timeValueMinutes(20);
private static final int DEFAULT_MAX_USERS = 100_000; // 100k users
public static final Setting<TimeValue> CACHE_TTL_SETTING = Setting.timeSetting("cache.ttl", DEFAULT_TTL, Setting.Property.NodeScope);
public static final Setting<Integer> CACHE_MAX_USERS_SETTING =
Setting.intSetting("cache.max_users", DEFAULT_MAX_USERS, Property.NodeScope);
private KerberosRealmSettings() {
}
/**
* @return the valid set of {@link Setting}s for a {@value #TYPE} realm
*/
public static Set<Setting<?>> getSettings() {
return Sets.newHashSet(HTTP_SERVICE_KEYTAB_PATH, CACHE_TTL_SETTING, CACHE_MAX_USERS_SETTING, SETTING_KRB_DEBUG_ENABLE,
SETTING_REMOVE_REALM_NAME);
}
}

View File

@ -18,11 +18,14 @@ import org.elasticsearch.common.io.stream.StreamOutput;
import org.elasticsearch.common.io.stream.Streamable;
import org.elasticsearch.common.xcontent.LoggingDeprecationHandler;
import org.elasticsearch.common.xcontent.NamedXContentRegistry;
import org.elasticsearch.common.xcontent.ObjectParser;
import org.elasticsearch.common.xcontent.ToXContentObject;
import org.elasticsearch.common.xcontent.XContentBuilder;
import org.elasticsearch.common.xcontent.XContentParser;
import org.elasticsearch.common.xcontent.XContentType;
import org.elasticsearch.common.xcontent.json.JsonXContent;
import org.elasticsearch.xpack.core.security.authz.privilege.ConditionalClusterPrivilege;
import org.elasticsearch.xpack.core.security.authz.privilege.ConditionalClusterPrivileges;
import org.elasticsearch.xpack.core.security.support.MetadataUtils;
import org.elasticsearch.xpack.core.security.support.Validation;
import org.elasticsearch.xpack.core.security.xcontent.XContentUtils;
@ -31,9 +34,11 @@ import java.io.IOException;
import java.io.InputStream;
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;
/**
* A holder for a Role that contains user-readable information about the Role
@ -45,7 +50,9 @@ public class RoleDescriptor implements ToXContentObject {
private final String name;
private final String[] clusterPrivileges;
private final ConditionalClusterPrivilege[] conditionalClusterPrivileges;
private final IndicesPrivileges[] indicesPrivileges;
private final ApplicationResourcePrivileges[] applicationPrivileges;
private final String[] runAs;
private final Map<String, Object> metadata;
private final Map<String, Object> transientMetadata;
@ -57,6 +64,11 @@ public class RoleDescriptor implements ToXContentObject {
this(name, clusterPrivileges, indicesPrivileges, runAs, null);
}
/**
* @deprecated Use {@link #RoleDescriptor(String, String[], IndicesPrivileges[], ApplicationResourcePrivileges[],
* ConditionalClusterPrivilege[], String[], Map, Map)}
*/
@Deprecated
public RoleDescriptor(String name,
@Nullable String[] clusterPrivileges,
@Nullable IndicesPrivileges[] indicesPrivileges,
@ -65,16 +77,34 @@ public class RoleDescriptor implements ToXContentObject {
this(name, clusterPrivileges, indicesPrivileges, runAs, metadata, null);
}
/**
* @deprecated Use {@link #RoleDescriptor(String, String[], IndicesPrivileges[], ApplicationResourcePrivileges[],
* ConditionalClusterPrivilege[], String[], Map, Map)}
*/
@Deprecated
public RoleDescriptor(String name,
@Nullable String[] clusterPrivileges,
@Nullable IndicesPrivileges[] indicesPrivileges,
@Nullable String[] runAs,
@Nullable Map<String, Object> metadata,
@Nullable Map<String, Object> transientMetadata) {
this(name, clusterPrivileges, indicesPrivileges, null, null, runAs, metadata, transientMetadata);
}
public RoleDescriptor(String name,
@Nullable String[] clusterPrivileges,
@Nullable IndicesPrivileges[] indicesPrivileges,
@Nullable ApplicationResourcePrivileges[] applicationPrivileges,
@Nullable ConditionalClusterPrivilege[] conditionalClusterPrivileges,
@Nullable String[] runAs,
@Nullable Map<String, Object> metadata,
@Nullable Map<String, Object> transientMetadata) {
this.name = name;
this.clusterPrivileges = clusterPrivileges != null ? clusterPrivileges : Strings.EMPTY_ARRAY;
this.conditionalClusterPrivileges = conditionalClusterPrivileges != null
? conditionalClusterPrivileges : ConditionalClusterPrivileges.EMPTY_ARRAY;
this.indicesPrivileges = indicesPrivileges != null ? indicesPrivileges : IndicesPrivileges.NONE;
this.applicationPrivileges = applicationPrivileges != null ? applicationPrivileges : ApplicationResourcePrivileges.NONE;
this.runAs = runAs != null ? runAs : Strings.EMPTY_ARRAY;
this.metadata = metadata != null ? Collections.unmodifiableMap(metadata) : Collections.emptyMap();
this.transientMetadata = transientMetadata != null ? Collections.unmodifiableMap(transientMetadata) :
@ -89,10 +119,18 @@ public class RoleDescriptor implements ToXContentObject {
return this.clusterPrivileges;
}
public ConditionalClusterPrivilege[] getConditionalClusterPrivileges() {
return this.conditionalClusterPrivileges;
}
public IndicesPrivileges[] getIndicesPrivileges() {
return this.indicesPrivileges;
}
public ApplicationResourcePrivileges[] getApplicationPrivileges() {
return this.applicationPrivileges;
}
public String[] getRunAs() {
return this.runAs;
}
@ -114,10 +152,15 @@ public class RoleDescriptor implements ToXContentObject {
StringBuilder sb = new StringBuilder("Role[");
sb.append("name=").append(name);
sb.append(", cluster=[").append(Strings.arrayToCommaDelimitedString(clusterPrivileges));
sb.append("], global=[").append(Strings.arrayToCommaDelimitedString(conditionalClusterPrivileges));
sb.append("], indicesPrivileges=[");
for (IndicesPrivileges group : indicesPrivileges) {
sb.append(group.toString()).append(",");
}
sb.append("], applicationPrivileges=[");
for (ApplicationResourcePrivileges privilege : applicationPrivileges) {
sb.append(privilege.toString()).append(",");
}
sb.append("], runAs=[").append(Strings.arrayToCommaDelimitedString(runAs));
sb.append("], metadata=[");
MetadataUtils.writeValue(sb, metadata);
@ -134,7 +177,9 @@ public class RoleDescriptor implements ToXContentObject {
if (!name.equals(that.name)) return false;
if (!Arrays.equals(clusterPrivileges, that.clusterPrivileges)) return false;
if (!Arrays.equals(conditionalClusterPrivileges, that.conditionalClusterPrivileges)) return false;
if (!Arrays.equals(indicesPrivileges, that.indicesPrivileges)) return false;
if (!Arrays.equals(applicationPrivileges, that.applicationPrivileges)) return false;
if (!metadata.equals(that.getMetadata())) return false;
return Arrays.equals(runAs, that.runAs);
}
@ -143,7 +188,9 @@ public class RoleDescriptor implements ToXContentObject {
public int hashCode() {
int result = name.hashCode();
result = 31 * result + Arrays.hashCode(clusterPrivileges);
result = 31 * result + Arrays.hashCode(conditionalClusterPrivileges);
result = 31 * result + Arrays.hashCode(indicesPrivileges);
result = 31 * result + Arrays.hashCode(applicationPrivileges);
result = 31 * result + Arrays.hashCode(runAs);
result = 31 * result + metadata.hashCode();
return result;
@ -157,8 +204,8 @@ public class RoleDescriptor implements ToXContentObject {
/**
* Generates x-content for this {@link RoleDescriptor} instance.
*
* @param builder the x-content builder
* @param params the parameters for x-content generation directives
* @param builder the x-content builder
* @param params the parameters for x-content generation directives
* @param docCreation {@code true} if the x-content is being generated for creating a document
* in the security index, {@code false} if the x-content being generated
* is for API display purposes
@ -168,7 +215,12 @@ public class RoleDescriptor implements ToXContentObject {
public XContentBuilder toXContent(XContentBuilder builder, Params params, boolean docCreation) throws IOException {
builder.startObject();
builder.array(Fields.CLUSTER.getPreferredName(), clusterPrivileges);
if (conditionalClusterPrivileges.length != 0) {
builder.field(Fields.GLOBAL.getPreferredName());
ConditionalClusterPrivileges.toXContent(builder, params, Arrays.asList(conditionalClusterPrivileges));
}
builder.array(Fields.INDICES.getPreferredName(), (Object[]) indicesPrivileges);
builder.array(Fields.APPLICATIONS.getPreferredName(), (Object[]) applicationPrivileges);
if (runAs != null) {
builder.array(Fields.RUN_AS.getPreferredName(), runAs);
}
@ -198,7 +250,19 @@ public class RoleDescriptor implements ToXContentObject {
} else {
transientMetadata = Collections.emptyMap();
}
return new RoleDescriptor(name, clusterPrivileges, indicesPrivileges, runAs, metadata, transientMetadata);
final ApplicationResourcePrivileges[] applicationPrivileges;
final ConditionalClusterPrivilege[] conditionalClusterPrivileges;
if (in.getVersion().onOrAfter(Version.V_7_0_0_alpha1)) {
applicationPrivileges = in.readArray(ApplicationResourcePrivileges::createFrom, ApplicationResourcePrivileges[]::new);
conditionalClusterPrivileges = ConditionalClusterPrivileges.readArray(in);
} else {
applicationPrivileges = ApplicationResourcePrivileges.NONE;
conditionalClusterPrivileges = ConditionalClusterPrivileges.EMPTY_ARRAY;
}
return new RoleDescriptor(name, clusterPrivileges, indicesPrivileges, applicationPrivileges, conditionalClusterPrivileges,
runAs, metadata, transientMetadata);
}
public static void writeTo(RoleDescriptor descriptor, StreamOutput out) throws IOException {
@ -213,6 +277,10 @@ public class RoleDescriptor implements ToXContentObject {
if (out.getVersion().onOrAfter(Version.V_5_2_0)) {
out.writeMap(descriptor.transientMetadata);
}
if (out.getVersion().onOrAfter(Version.V_7_0_0_alpha1)) {
out.writeArray(ApplicationResourcePrivileges::write, descriptor.applicationPrivileges);
ConditionalClusterPrivileges.writeArray(out, descriptor.getConditionalClusterPrivileges());
}
}
public static RoleDescriptor parse(String name, BytesReference source, boolean allow2xFormat, XContentType xContentType)
@ -221,7 +289,7 @@ public class RoleDescriptor implements ToXContentObject {
// EMPTY is safe here because we never use namedObject
try (InputStream stream = source.streamInput();
XContentParser parser = xContentType.xContent()
.createParser(NamedXContentRegistry.EMPTY, LoggingDeprecationHandler.INSTANCE, stream)) {
.createParser(NamedXContentRegistry.EMPTY, LoggingDeprecationHandler.INSTANCE, stream)) {
return parse(name, parser, allow2xFormat);
}
}
@ -243,6 +311,8 @@ public class RoleDescriptor implements ToXContentObject {
String currentFieldName = null;
IndicesPrivileges[] indicesPrivileges = null;
String[] clusterPrivileges = null;
List<ConditionalClusterPrivilege> conditionalClusterPrivileges = Collections.emptyList();
ApplicationResourcePrivileges[] applicationPrivileges = null;
String[] runAsUsers = null;
Map<String, Object> metadata = null;
while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) {
@ -255,6 +325,11 @@ public class RoleDescriptor implements ToXContentObject {
runAsUsers = readStringArray(name, parser, true);
} else if (Fields.CLUSTER.match(currentFieldName, parser.getDeprecationHandler())) {
clusterPrivileges = readStringArray(name, parser, true);
} else if (Fields.APPLICATIONS.match(currentFieldName, parser.getDeprecationHandler())
|| Fields.APPLICATION.match(currentFieldName, parser.getDeprecationHandler())) {
applicationPrivileges = parseApplicationPrivileges(name, parser);
} else if (Fields.GLOBAL.match(currentFieldName, parser.getDeprecationHandler())) {
conditionalClusterPrivileges = ConditionalClusterPrivileges.parse(parser);
} else if (Fields.METADATA.match(currentFieldName, parser.getDeprecationHandler())) {
if (token != XContentParser.Token.START_OBJECT) {
throw new ElasticsearchParseException(
@ -266,8 +341,7 @@ public class RoleDescriptor implements ToXContentObject {
// consume object but just drop
parser.map();
} else {
throw new ElasticsearchParseException("expected field [{}] to be an object, but found [{}] instead",
currentFieldName, token);
throw new ElasticsearchParseException("failed to parse role [{}]. unexpected field [{}]", name, currentFieldName);
}
} else if (Fields.TYPE.match(currentFieldName, parser.getDeprecationHandler())) {
// don't need it
@ -275,7 +349,9 @@ public class RoleDescriptor implements ToXContentObject {
throw new ElasticsearchParseException("failed to parse role [{}]. unexpected field [{}]", name, currentFieldName);
}
}
return new RoleDescriptor(name, clusterPrivileges, indicesPrivileges, runAsUsers, metadata);
return new RoleDescriptor(name, clusterPrivileges, indicesPrivileges, applicationPrivileges,
conditionalClusterPrivileges.toArray(new ConditionalClusterPrivilege[conditionalClusterPrivileges.size()]), runAsUsers,
metadata, null);
}
private static String[] readStringArray(String roleName, XContentParser parser, boolean allowNull) throws IOException {
@ -291,7 +367,7 @@ public class RoleDescriptor implements ToXContentObject {
throws IOException {
try (InputStream stream = source.streamInput();
XContentParser parser = xContentType.xContent()
.createParser(NamedXContentRegistry.EMPTY, LoggingDeprecationHandler.INSTANCE, stream)) {
.createParser(NamedXContentRegistry.EMPTY, LoggingDeprecationHandler.INSTANCE, stream)) {
// advance to the START_OBJECT token
XContentParser.Token token = parser.nextToken();
if (token != XContentParser.Token.START_OBJECT) {
@ -301,6 +377,7 @@ public class RoleDescriptor implements ToXContentObject {
String currentFieldName = null;
IndicesPrivileges[] indexPrivileges = null;
String[] clusterPrivileges = null;
ApplicationResourcePrivileges[] applicationPrivileges = null;
while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) {
if (token == XContentParser.Token.FIELD_NAME) {
currentFieldName = parser.currentName();
@ -308,14 +385,17 @@ public class RoleDescriptor implements ToXContentObject {
indexPrivileges = parseIndices(description, parser, false);
} else if (Fields.CLUSTER.match(currentFieldName, parser.getDeprecationHandler())) {
clusterPrivileges = readStringArray(description, parser, true);
} else if (Fields.APPLICATIONS.match(currentFieldName, parser.getDeprecationHandler())
|| Fields.APPLICATION.match(currentFieldName, parser.getDeprecationHandler())) {
applicationPrivileges = parseApplicationPrivileges(description, parser);
} else {
throw new ElasticsearchParseException("failed to parse privileges check [{}]. unexpected field [{}]",
description, currentFieldName);
}
}
if (indexPrivileges == null && clusterPrivileges == null) {
throw new ElasticsearchParseException("failed to parse privileges check [{}]. fields [{}] and [{}] are both missing",
description, Fields.INDEX, Fields.CLUSTER);
if (indexPrivileges == null && clusterPrivileges == null && applicationPrivileges == null) {
throw new ElasticsearchParseException("failed to parse privileges check [{}]. All privilege fields [{},{},{}] are missing",
description, Fields.CLUSTER, Fields.INDEX, Fields.APPLICATIONS);
}
if (indexPrivileges != null) {
if (Arrays.stream(indexPrivileges).anyMatch(IndicesPrivileges::isUsingFieldLevelSecurity)) {
@ -326,7 +406,7 @@ public class RoleDescriptor implements ToXContentObject {
throw new ElasticsearchParseException("Field [{}] is not supported in a has_privileges request", Fields.QUERY);
}
}
return new RoleDescriptor(description, clusterPrivileges, indexPrivileges, null);
return new RoleDescriptor(description, clusterPrivileges, indexPrivileges, applicationPrivileges, null, null, null, null);
}
}
@ -361,7 +441,7 @@ public class RoleDescriptor implements ToXContentObject {
currentFieldName = parser.currentName();
} else if (Fields.NAMES.match(currentFieldName, parser.getDeprecationHandler())) {
if (token == XContentParser.Token.VALUE_STRING) {
names = new String[] { parser.text() };
names = new String[]{parser.text()};
} else if (token == XContentParser.Token.START_ARRAY) {
names = readStringArray(roleName, parser, false);
if (names.length == 0) {
@ -474,6 +554,37 @@ public class RoleDescriptor implements ToXContentObject {
.build();
}
private static ApplicationResourcePrivileges[] parseApplicationPrivileges(String roleName, XContentParser parser)
throws IOException {
if (parser.currentToken() != XContentParser.Token.START_ARRAY) {
throw new ElasticsearchParseException("failed to parse application privileges for role [{}]. expected field [{}] value " +
"to be an array, but found [{}] instead", roleName, parser.currentName(), parser.currentToken());
}
List<ApplicationResourcePrivileges> privileges = new ArrayList<>();
while (parser.nextToken() != XContentParser.Token.END_ARRAY) {
privileges.add(parseApplicationPrivilege(roleName, parser));
}
return privileges.toArray(new ApplicationResourcePrivileges[privileges.size()]);
}
private static ApplicationResourcePrivileges parseApplicationPrivilege(String roleName, XContentParser parser) throws IOException {
XContentParser.Token token = parser.currentToken();
if (token != XContentParser.Token.START_OBJECT) {
throw new ElasticsearchParseException("failed to parse application privileges for role [{}]. expected field [{}] value to " +
"be an array of objects, but found an array element of type [{}]", roleName, parser.currentName(), token);
}
final ApplicationResourcePrivileges.Builder builder = ApplicationResourcePrivileges.PARSER.parse(parser, null);
if (builder.hasResources() == false) {
throw new ElasticsearchParseException("failed to parse application privileges for role [{}]. missing required [{}] field",
roleName, Fields.RESOURCES.getPreferredName());
}
if (builder.hasPrivileges() == false) {
throw new ElasticsearchParseException("failed to parse application privileges for role [{}]. missing required [{}] field",
roleName, Fields.PRIVILEGES.getPreferredName());
}
return builder.build();
}
/**
* A class representing permissions for a group of indices mapped to
* privileges, field permissions, and a query.
@ -695,14 +806,176 @@ public class RoleDescriptor implements ToXContentObject {
}
}
public static class ApplicationResourcePrivileges implements ToXContentObject, Streamable {
private static final ApplicationResourcePrivileges[] NONE = new ApplicationResourcePrivileges[0];
private static final ObjectParser<ApplicationResourcePrivileges.Builder, Void> PARSER = new ObjectParser<>("application",
ApplicationResourcePrivileges::builder);
static {
PARSER.declareString(Builder::application, Fields.APPLICATION);
PARSER.declareStringArray(Builder::privileges, Fields.PRIVILEGES);
PARSER.declareStringArray(Builder::resources, Fields.RESOURCES);
}
private String application;
private String[] privileges;
private String[] resources;
private ApplicationResourcePrivileges() {
}
public static Builder builder() {
return new Builder();
}
public String getApplication() {
return application;
}
public String[] getResources() {
return this.resources;
}
public String[] getPrivileges() {
return this.privileges;
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder(getClass().getSimpleName())
.append("[application=")
.append(application)
.append(", privileges=[")
.append(Strings.arrayToCommaDelimitedString(privileges))
.append("], resources=[")
.append(Strings.arrayToCommaDelimitedString(resources))
.append("]]");
return sb.toString();
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || this.getClass() != o.getClass()) {
return false;
}
ApplicationResourcePrivileges that = (ApplicationResourcePrivileges) o;
return Objects.equals(this.application, that.application)
&& Arrays.equals(this.resources, that.resources)
&& Arrays.equals(this.privileges, that.privileges);
}
@Override
public int hashCode() {
int result = Arrays.hashCode(resources);
result = 31 * result + Arrays.hashCode(privileges);
return result;
}
@Override
public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
builder.startObject();
builder.field(Fields.APPLICATION.getPreferredName(), application);
builder.array(Fields.PRIVILEGES.getPreferredName(), privileges);
builder.array(Fields.RESOURCES.getPreferredName(), resources);
return builder.endObject();
}
public static ApplicationResourcePrivileges createFrom(StreamInput in) throws IOException {
ApplicationResourcePrivileges ip = new ApplicationResourcePrivileges();
ip.readFrom(in);
return ip;
}
@Override
public void readFrom(StreamInput in) throws IOException {
this.application = in.readString();
this.privileges = in.readStringArray();
this.resources = in.readStringArray();
}
@Override
public void writeTo(StreamOutput out) throws IOException {
out.writeString(application);
out.writeStringArray(privileges);
out.writeStringArray(resources);
}
public static void write(StreamOutput out, ApplicationResourcePrivileges privileges) throws IOException {
privileges.writeTo(out);
}
public static class Builder {
private ApplicationResourcePrivileges applicationPrivileges = new ApplicationResourcePrivileges();
private Builder() {
}
public Builder application(String appName) {
applicationPrivileges.application = appName;
return this;
}
public Builder resources(String... resources) {
applicationPrivileges.resources = resources;
return this;
}
public Builder resources(List<String> resources) {
return resources(resources.toArray(new String[resources.size()]));
}
public Builder privileges(String... privileges) {
applicationPrivileges.privileges = privileges;
return this;
}
public Builder privileges(Collection<String> privileges) {
return privileges(privileges.toArray(new String[privileges.size()]));
}
public boolean hasResources() {
return applicationPrivileges.resources != null;
}
public boolean hasPrivileges() {
return applicationPrivileges.privileges != null;
}
public ApplicationResourcePrivileges build() {
if (Strings.isNullOrEmpty(applicationPrivileges.application)) {
throw new IllegalArgumentException("application privileges must have an application name");
}
if (applicationPrivileges.privileges == null || applicationPrivileges.privileges.length == 0) {
throw new IllegalArgumentException("application privileges must define at least one privilege");
}
if (applicationPrivileges.resources == null || applicationPrivileges.resources.length == 0) {
throw new IllegalArgumentException("application privileges must refer to at least one resource");
}
return applicationPrivileges;
}
}
}
public interface Fields {
ParseField CLUSTER = new ParseField("cluster");
ParseField GLOBAL = new ParseField("global");
ParseField INDEX = new ParseField("index");
ParseField INDICES = new ParseField("indices");
ParseField APPLICATIONS = new ParseField("applications");
ParseField RUN_AS = new ParseField("run_as");
ParseField NAMES = new ParseField("names");
ParseField RESOURCES = new ParseField("resources");
ParseField QUERY = new ParseField("query");
ParseField PRIVILEGES = new ParseField("privileges");
ParseField APPLICATION = new ParseField("application");
ParseField FIELD_PERMISSIONS = new ParseField("field_security");
ParseField FIELD_PERMISSIONS_2X = new ParseField("fields");
ParseField GRANT_FIELDS = new ParseField("grant");

View File

@ -0,0 +1,110 @@
/*
* 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.logging.log4j.Logger;
import org.apache.lucene.util.automaton.Automaton;
import org.apache.lucene.util.automaton.Operations;
import org.elasticsearch.common.collect.Tuple;
import org.elasticsearch.common.logging.Loggers;
import org.elasticsearch.xpack.core.security.authz.privilege.ApplicationPrivilege;
import org.elasticsearch.xpack.core.security.support.Automatons;
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.Set;
import java.util.function.Predicate;
/**
* A permission that is based on privileges for application (non elasticsearch) capabilities
*/
public final class ApplicationPermission {
public static final ApplicationPermission NONE = new ApplicationPermission(Collections.emptyList());
private final Logger logger;
private final List<PermissionEntry> permissions;
/**
* @param privilegesAndResources A list of (privilege, resources). Each element in the {@link List} is a {@link Tuple} containing
* a single {@link ApplicationPrivilege} and the {@link Set} of resources to which that privilege is
* applied. The resources are treated as a wildcard {@link Automatons#pattern}.
*/
ApplicationPermission(List<Tuple<ApplicationPrivilege, Set<String>>> privilegesAndResources) {
this.logger = Loggers.getLogger(getClass());
Map<ApplicationPrivilege, PermissionEntry> permissionsByPrivilege = new HashMap<>();
privilegesAndResources.forEach(tup -> permissionsByPrivilege.compute(tup.v1(), (k, existing) -> {
final Automaton patterns = Automatons.patterns(tup.v2());
if (existing == null) {
return new PermissionEntry(k, patterns);
} else {
return new PermissionEntry(k, Automatons.unionAndMinimize(Arrays.asList(existing.resources, patterns)));
}
}));
this.permissions = Collections.unmodifiableList(new ArrayList<>(permissionsByPrivilege.values()));
}
/**
* Determines whether this permission grants the specified privilege on the given resource.
* <p>
* An {@link ApplicationPermission} consists of a sequence of permission entries, where each entry contains a single
* {@link ApplicationPrivilege} and one or more resource patterns.
* </p>
* <p>
* This method returns {@code true} if, one or more of those entries meet the following criteria
* </p>
* <ul>
* <li>The entry's application, when interpreted as an {@link Automaton} {@link Automatons#pattern(String) pattern} matches the
* application given in the argument (interpreted as a raw string)
* </li>
* <li>The {@link ApplicationPrivilege#getAutomaton automaton that defines the entry's actions} entirely covers the
* automaton given in the argument (that is, the argument is a subset of the entry's automaton)
* </li>
* <li>The entry's resources, when interpreted as an {@link Automaton} {@link Automatons#patterns(String...)} set of patterns} entirely
* covers the resource given in the argument (also interpreted as an {@link Automaton} {@link Automatons#pattern(String) pattern}.
* </li>
* </ul>
*/
public boolean grants(ApplicationPrivilege other, String resource) {
Automaton resourceAutomaton = Automatons.patterns(resource);
final boolean matched = permissions.stream().anyMatch(e -> e.grants(other, resourceAutomaton));
logger.trace("Permission [{}] {} grant [{} , {}]", this, matched ? "does" : "does not", other, resource);
return matched;
}
@Override
public String toString() {
return getClass().getSimpleName() + "{privileges=" + permissions + "}";
}
private static class PermissionEntry {
private final ApplicationPrivilege privilege;
private final Predicate<String> application;
private final Automaton resources;
private PermissionEntry(ApplicationPrivilege privilege, Automaton resources) {
this.privilege = privilege;
this.application = Automatons.predicate(privilege.getApplication());
this.resources = resources;
}
private boolean grants(ApplicationPrivilege other, Automaton resource) {
return this.application.test(other.getApplication())
&& Operations.isEmpty(privilege.getAutomaton()) == false
&& Operations.subsetOf(other.getAutomaton(), privilege.getAutomaton())
&& Operations.subsetOf(resource, this.resources);
}
@Override
public String toString() {
return privilege.toString();
}
}
}

View File

@ -5,30 +5,97 @@
*/
package org.elasticsearch.xpack.core.security.authz.permission;
import org.elasticsearch.transport.TransportRequest;
import org.elasticsearch.xpack.core.security.authz.privilege.ClusterPrivilege;
import org.elasticsearch.xpack.core.security.authz.privilege.ConditionalClusterPrivilege;
import java.util.Collection;
import java.util.Set;
import java.util.function.Predicate;
import java.util.stream.Collectors;
/**
* A permission that is based on privileges for cluster wide actions
* A permission that is based on privileges for cluster wide actions, with the optional ability to inspect the request object
*/
public final class ClusterPermission {
public static final ClusterPermission NONE = new ClusterPermission(ClusterPrivilege.NONE);
public abstract class ClusterPermission {
private final ClusterPrivilege privilege;
private final Predicate<String> predicate;
ClusterPermission(ClusterPrivilege privilege) {
this.privilege = privilege;
this.predicate = privilege.predicate();
}
public ClusterPrivilege privilege() {
return privilege;
}
public boolean check(String action) {
return predicate.test(action);
public abstract boolean check(String action, TransportRequest request);
/**
* A permission that is based solely on cluster privileges and does not consider request state
*/
public static class SimpleClusterPermission extends ClusterPermission {
public static final SimpleClusterPermission NONE = new SimpleClusterPermission(ClusterPrivilege.NONE);
private final Predicate<String> predicate;
SimpleClusterPermission(ClusterPrivilege privilege) {
super(privilege);
this.predicate = privilege.predicate();
}
@Override
public boolean check(String action, TransportRequest request) {
return predicate.test(action);
}
}
/**
* A permission that makes use of both cluster privileges and request inspection
*/
public static class ConditionalClusterPermission extends ClusterPermission {
private final Predicate<String> actionPredicate;
private final Predicate<TransportRequest> requestPredicate;
public ConditionalClusterPermission(ConditionalClusterPrivilege conditionalPrivilege) {
this(conditionalPrivilege.getPrivilege(), conditionalPrivilege.getRequestPredicate());
}
public ConditionalClusterPermission(ClusterPrivilege privilege, Predicate<TransportRequest> requestPredicate) {
super(privilege);
this.actionPredicate = privilege.predicate();
this.requestPredicate = requestPredicate;
}
@Override
public boolean check(String action, TransportRequest request) {
return actionPredicate.test(action) && requestPredicate.test(request);
}
}
/**
* A permission that composes a number of other cluster permissions
*/
public static class CompositeClusterPermission extends ClusterPermission {
private final Collection<ClusterPermission> children;
public CompositeClusterPermission(Collection<ClusterPermission> children) {
super(buildPrivilege(children));
this.children = children;
}
private static ClusterPrivilege buildPrivilege(Collection<ClusterPermission> children) {
final Set<String> names = children.stream()
.map(ClusterPermission::privilege)
.map(ClusterPrivilege::name)
.flatMap(Set::stream)
.collect(Collectors.toSet());
return ClusterPrivilege.get(names);
}
@Override
public boolean check(String action, TransportRequest request) {
return children.stream().anyMatch(p -> p.check(action, request));
}
}
}

View File

@ -8,14 +8,18 @@ package org.elasticsearch.xpack.core.security.authz.permission;
import org.elasticsearch.cluster.metadata.MetaData;
import org.elasticsearch.common.Nullable;
import org.elasticsearch.common.bytes.BytesReference;
import org.elasticsearch.common.collect.Tuple;
import org.elasticsearch.common.util.set.Sets;
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.ClusterPrivilege;
import org.elasticsearch.xpack.core.security.authz.privilege.ConditionalClusterPrivilege;
import org.elasticsearch.xpack.core.security.authz.privilege.IndexPrivilege;
import org.elasticsearch.xpack.core.security.authz.privilege.Privilege;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;
@ -29,12 +33,14 @@ public final class Role {
private final String[] names;
private final ClusterPermission cluster;
private final IndicesPermission indices;
private final ApplicationPermission application;
private final RunAsPermission runAs;
Role(String[] names, ClusterPermission cluster, IndicesPermission indices, RunAsPermission runAs) {
Role(String[] names, ClusterPermission cluster, IndicesPermission indices, ApplicationPermission application, RunAsPermission runAs) {
this.names = names;
this.cluster = Objects.requireNonNull(cluster);
this.indices = Objects.requireNonNull(indices);
this.application = Objects.requireNonNull(application);
this.runAs = Objects.requireNonNull(runAs);
}
@ -50,6 +56,10 @@ public final class Role {
return indices;
}
public ApplicationPermission application() {
return application;
}
public RunAsPermission runAs() {
return runAs;
}
@ -70,7 +80,7 @@ public final class Role {
public IndicesAccessControl authorize(String action, Set<String> requestedIndicesOrAliases, MetaData metaData,
FieldPermissionsCache fieldPermissionsCache) {
Map<String, IndicesAccessControl.IndexAccessControl> indexPermissions = indices.authorize(
action, requestedIndicesOrAliases, metaData, fieldPermissionsCache
action, requestedIndicesOrAliases, metaData, fieldPermissionsCache
);
// At least one role / indices permission set need to match with all the requested indices/aliases:
@ -87,9 +97,10 @@ public final class Role {
public static class Builder {
private final String[] names;
private ClusterPermission cluster = ClusterPermission.NONE;
private ClusterPermission cluster = ClusterPermission.SimpleClusterPermission.NONE;
private RunAsPermission runAs = RunAsPermission.NONE;
private List<IndicesPermission.Group> groups = new ArrayList<>();
private List<Tuple<ApplicationPrivilege, Set<String>>> applicationPrivs = new ArrayList<>();
private Builder(String[] names) {
this.names = names;
@ -97,20 +108,44 @@ public final class Role {
private Builder(RoleDescriptor rd, @Nullable FieldPermissionsCache fieldPermissionsCache) {
this.names = new String[] { rd.getName() };
if (rd.getClusterPrivileges().length == 0) {
cluster = ClusterPermission.NONE;
} else {
this.cluster(ClusterPrivilege.get(Sets.newHashSet(rd.getClusterPrivileges())));
}
cluster(Sets.newHashSet(rd.getClusterPrivileges()), Arrays.asList(rd.getConditionalClusterPrivileges()));
groups.addAll(convertFromIndicesPrivileges(rd.getIndicesPrivileges(), fieldPermissionsCache));
final RoleDescriptor.ApplicationResourcePrivileges[] applicationPrivileges = rd.getApplicationPrivileges();
for (int i = 0; i < applicationPrivileges.length; i++) {
applicationPrivs.add(convertApplicationPrivilege(rd.getName(), i, applicationPrivileges[i]));
}
String[] rdRunAs = rd.getRunAs();
if (rdRunAs != null && rdRunAs.length > 0) {
this.runAs(new Privilege(Sets.newHashSet(rdRunAs), rdRunAs));
}
}
public Builder cluster(Set<String> privilegeNames, Iterable<ConditionalClusterPrivilege> conditionalClusterPrivileges) {
List<ClusterPermission> clusterPermissions = new ArrayList<>();
if (privilegeNames.isEmpty() == false) {
clusterPermissions.add(new ClusterPermission.SimpleClusterPermission(ClusterPrivilege.get(privilegeNames)));
}
for (ConditionalClusterPrivilege ccp : conditionalClusterPrivileges) {
clusterPermissions.add(new ClusterPermission.ConditionalClusterPermission(ccp));
}
if (clusterPermissions.isEmpty()) {
this.cluster = ClusterPermission.SimpleClusterPermission.NONE;
} else if (clusterPermissions.size() == 1) {
this.cluster = clusterPermissions.get(0);
} else {
this.cluster = new ClusterPermission.CompositeClusterPermission(clusterPermissions);
}
return this;
}
/**
* @deprecated Use {@link #cluster(Set, Iterable)}
*/
@Deprecated
public Builder cluster(ClusterPrivilege privilege) {
cluster = new ClusterPermission(privilege);
cluster = new ClusterPermission.SimpleClusterPermission(privilege);
return this;
}
@ -129,10 +164,17 @@ public final class Role {
return this;
}
public Builder addApplicationPrivilege(ApplicationPrivilege privilege, Set<String> resources) {
applicationPrivs.add(new Tuple<>(privilege, resources));
return this;
}
public Role build() {
IndicesPermission indices = groups.isEmpty() ? IndicesPermission.NONE :
new IndicesPermission(groups.toArray(new IndicesPermission.Group[groups.size()]));
return new Role(names, cluster, indices, runAs);
new IndicesPermission(groups.toArray(new IndicesPermission.Group[groups.size()]));
final ApplicationPermission applicationPermission
= applicationPrivs.isEmpty() ? ApplicationPermission.NONE : new ApplicationPermission(applicationPrivs);
return new Role(names, cluster, indices, applicationPermission, runAs);
}
static List<IndicesPermission.Group> convertFromIndicesPrivileges(RoleDescriptor.IndicesPrivileges[] indicesPrivileges,
@ -144,16 +186,24 @@ public final class Role {
fieldPermissions = fieldPermissionsCache.getFieldPermissions(privilege.getGrantedFields(), privilege.getDeniedFields());
} else {
fieldPermissions = new FieldPermissions(
new FieldPermissionsDefinition(privilege.getGrantedFields(), privilege.getDeniedFields()));
new FieldPermissionsDefinition(privilege.getGrantedFields(), privilege.getDeniedFields()));
}
final Set<BytesReference> query = privilege.getQuery() == null ? null : Collections.singleton(privilege.getQuery());
list.add(new IndicesPermission.Group(IndexPrivilege.get(Sets.newHashSet(privilege.getPrivileges())),
fieldPermissions,
query,
privilege.getIndices()));
fieldPermissions,
query,
privilege.getIndices()));
}
return list;
}
static Tuple<ApplicationPrivilege, Set<String>> convertApplicationPrivilege(String role, int index,
RoleDescriptor.ApplicationResourcePrivileges arp) {
return new Tuple<>(new ApplicationPrivilege(arp.getApplication(),
"role." + role.replaceAll("[^a-zA-Z0-9]", "") + "." + index,
arp.getPrivileges()
), Sets.newHashSet(arp.getResources()));
}
}
}

View File

@ -0,0 +1,225 @@
/*
* 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.privilege;
import org.elasticsearch.common.Strings;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.function.Function;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
/**
* An application privilege has an application name (e.g. {@code "my-app"}) that identifies an application (that exists
* outside of elasticsearch), a privilege name (e.g. {@code "admin}) that is meaningful to that application, and one or
* more "action patterns" (e.g {@code "admin/user/*", "admin/team/*"}).
* Action patterns must contain at least one special character from ({@code /}, {@code :}, {@code *}) to distinguish them
* from privilege names.
* The action patterns are entirely optional - many application will find that simple "privilege names" are sufficient, but
* they allow applications to define high level abstract privileges that map to multiple low level capabilities.
*/
public final class ApplicationPrivilege extends Privilege {
private static final Pattern VALID_APPLICATION_PREFIX = Pattern.compile("^[a-z][A-Za-z0-9]*$");
private static final Pattern WHITESPACE = Pattern.compile("[\\v\\h]");
private static final Pattern VALID_NAME = Pattern.compile("^[a-z][a-zA-Z0-9_.-]*$");
/**
* A name or action must be composed of printable, visible ASCII characters.
* That is: letters, numbers &amp; symbols, but no whitespace.
*/
private static final Pattern VALID_NAME_OR_ACTION = Pattern.compile("^\\p{Graph}*$");
public static final Function<String, ApplicationPrivilege> NONE = app -> new ApplicationPrivilege(app, "none", new String[0]);
private final String application;
private final String[] patterns;
public ApplicationPrivilege(String application, String privilegeName, String... patterns) {
this(application, Collections.singleton(privilegeName), patterns);
}
public ApplicationPrivilege(String application, Set<String> name, String... patterns) {
super(name, patterns);
this.application = application;
this.patterns = patterns;
}
public String getApplication() {
return application;
}
// Package level for testing
String[] getPatterns() {
return patterns;
}
/**
* Validate that the provided application name is valid, and throws an exception otherwise
*
* @throws IllegalArgumentException if the name is not valid
*/
public static void validateApplicationName(String application) {
validateApplicationName(application, false);
}
/**
* Validate that the provided name is a valid application, or a wildcard pattern for an application and throws an exception otherwise
*
* @throws IllegalArgumentException if the name is not valid
*/
public static void validateApplicationNameOrWildcard(String application) {
validateApplicationName(application, true);
}
/**
* Validates that an application name matches the following rules:
* - consist of a "prefix", optionally followed by either "-" or "_" and a suffix
* - the prefix must begin with a lowercase ASCII letter
* - the prefix only contain ASCII letter or digits
* - the prefix must be at least 3 characters long
* - the suffix must only contain {@link Strings#validFileName valid filename} characters
* - no part of the name may contain whitespace
* If {@code allowWildcard} is true, then the names that end with a '*', and would match a valid
* application name are also accepted.
*/
private static void validateApplicationName(String application, boolean allowWildcard) {
if (Strings.isEmpty(application)) {
throw new IllegalArgumentException("Application names cannot be blank");
}
final int asterisk = application.indexOf('*');
if (asterisk != -1) {
if (allowWildcard == false) {
throw new IllegalArgumentException("Application names may not contain '*' (found '" + application + "')");
}
if(application.equals("*")) {
// this is allowed and short-circuiting here makes the later validation simpler
return;
}
if (asterisk != application.length() - 1) {
throw new IllegalArgumentException("Application name patterns only support trailing wildcards (found '" + application
+ "')");
}
}
if (WHITESPACE.matcher(application).find()) {
throw new IllegalArgumentException("Application names may not contain whitespace (found '" + application + "')");
}
final String[] parts = application.split("[_-]", 2);
String prefix = parts[0];
if (prefix.endsWith("*")) {
prefix = prefix.substring(0, prefix.length() - 1);
}
if (VALID_APPLICATION_PREFIX.matcher(prefix).matches() == false) {
throw new IllegalArgumentException("An application name prefix must match the pattern " + VALID_APPLICATION_PREFIX.pattern()
+ " (found '" + prefix + "')");
}
if (prefix.length() < 3 && asterisk == -1) {
throw new IllegalArgumentException("An application name prefix must be at least 3 characters long (found '" + prefix + "')");
}
if (parts.length > 1) {
final String suffix = parts[1];
if (Strings.validFileName(suffix) == false) {
throw new IllegalArgumentException("An application name suffix may not contain any of the characters '" +
Strings.collectionToDelimitedString(Strings.INVALID_FILENAME_CHARS, "") + "' (found '" + suffix + "')");
}
}
}
/**
* Validate that the provided privilege name is valid, and throws an exception otherwise
*
* @throws IllegalArgumentException if the name is not valid
*/
public static void validatePrivilegeName(String name) {
if (isValidPrivilegeName(name) == false) {
throw new IllegalArgumentException("Application privilege names must match the pattern " + VALID_NAME.pattern()
+ " (found '" + name + "')");
}
}
private static boolean isValidPrivilegeName(String name) {
return VALID_NAME.matcher(name).matches();
}
/**
* Validate that the provided name is a valid privilege name or action name, and throws an exception otherwise
*
* @throws IllegalArgumentException if the name is not valid
*/
public static void validatePrivilegeOrActionName(String name) {
if (VALID_NAME_OR_ACTION.matcher(name).matches() == false) {
throw new IllegalArgumentException("Application privilege names and actions must match the pattern "
+ VALID_NAME_OR_ACTION.pattern() + " (found '" + name + "')");
}
}
/**
* Finds or creates an application privileges with the provided names.
* Each element in {@code name} may be the name of a stored privilege (to be resolved from {@code stored}, or a bespoke action pattern.
*/
public static ApplicationPrivilege get(String application, Set<String> name, Collection<ApplicationPrivilegeDescriptor> stored) {
if (name.isEmpty()) {
return NONE.apply(application);
} else {
Map<String, ApplicationPrivilegeDescriptor> lookup = stored.stream()
.filter(apd -> apd.getApplication().equals(application))
.collect(Collectors.toMap(ApplicationPrivilegeDescriptor::getName, Function.identity()));
return resolve(application, name, lookup);
}
}
private static ApplicationPrivilege resolve(String application, Set<String> names, Map<String, ApplicationPrivilegeDescriptor> lookup) {
final int size = names.size();
if (size == 0) {
throw new IllegalArgumentException("empty set should not be used");
}
Set<String> actions = new HashSet<>();
Set<String> patterns = new HashSet<>();
for (String name : names) {
if (isValidPrivilegeName(name)) {
ApplicationPrivilegeDescriptor descriptor = lookup.get(name);
if (descriptor != null) {
patterns.addAll(descriptor.getActions());
}
} else {
actions.add(name);
}
}
patterns.addAll(actions);
return new ApplicationPrivilege(application, names, patterns.toArray(new String[patterns.size()]));
}
@Override
public String toString() {
return application + ":" + super.toString() + "(" + Strings.arrayToCommaDelimitedString(patterns) + ")";
}
@Override
public int hashCode() {
int result = super.hashCode();
result = 31 * result + Objects.hashCode(application);
result = 31 * result + Arrays.hashCode(patterns);
return result;
}
@Override
public boolean equals(Object o) {
return super.equals(o)
&& Objects.equals(this.application, ((ApplicationPrivilege) o).application)
&& Arrays.equals(this.patterns, ((ApplicationPrivilege) o).patterns);
}
}

View File

@ -0,0 +1,194 @@
/*
* 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.privilege;
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.ObjectParser;
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.HashSet;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
/**
* An {@code ApplicationPrivilegeDescriptor} is a representation of a <em>stored</em> {@link ApplicationPrivilege}.
* A user (via a role) can be granted an application privilege by name (e.g. ("myapp", "read").
* In general, this privilege name will correspond to a pre-defined {@link ApplicationPrivilegeDescriptor}, which then
* is used to determine the set of actions granted by the privilege.
*/
public class ApplicationPrivilegeDescriptor implements ToXContentObject, Writeable {
public static final String DOC_TYPE_VALUE = "application-privilege";
private static final ObjectParser<Builder, Boolean> PARSER = new ObjectParser<>(DOC_TYPE_VALUE, Builder::new);
static {
PARSER.declareString(Builder::applicationName, Fields.APPLICATION);
PARSER.declareString(Builder::privilegeName, Fields.NAME);
PARSER.declareStringArray(Builder::actions, Fields.ACTIONS);
PARSER.declareObject(Builder::metadata, (parser, context) -> parser.map(), Fields.METADATA);
PARSER.declareField((parser, builder, allowType) -> builder.type(parser.text(), allowType), Fields.TYPE,
ObjectParser.ValueType.STRING);
}
private String application;
private String name;
private Set<String> actions;
private Map<String, Object> metadata;
public ApplicationPrivilegeDescriptor(String application, String name, Set<String> actions, Map<String, Object> metadata) {
this.application = Objects.requireNonNull(application);
this.name = Objects.requireNonNull(name);
this.actions = Collections.unmodifiableSet(actions);
this.metadata = Collections.unmodifiableMap(metadata);
}
public ApplicationPrivilegeDescriptor(StreamInput input) throws IOException {
this.application = input.readString();
this.name = input.readString();
this.actions = Collections.unmodifiableSet(input.readSet(StreamInput::readString));
this.metadata = Collections.unmodifiableMap(input.readMap());
}
@Override
public void writeTo(StreamOutput out) throws IOException {
out.writeString(application);
out.writeString(name);
out.writeCollection(actions, StreamOutput::writeString);
out.writeMap(metadata);
}
public String getApplication() {
return application;
}
public String getName() {
return name;
}
public Set<String> getActions() {
return actions;
}
public Map<String, Object> getMetadata() {
return metadata;
}
@Override
public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
return toXContent(builder, false);
}
public XContentBuilder toXContent(XContentBuilder builder, boolean includeTypeField) throws IOException {
builder.startObject()
.field(Fields.APPLICATION.getPreferredName(), application)
.field(Fields.NAME.getPreferredName(), name)
.field(Fields.ACTIONS.getPreferredName(), actions)
.field(Fields.METADATA.getPreferredName(), metadata);
if (includeTypeField) {
builder.field(Fields.TYPE.getPreferredName(), DOC_TYPE_VALUE);
}
return builder.endObject();
}
/**
* Construct a new {@link ApplicationPrivilegeDescriptor} from XContent.
*
* @param defaultApplication The application name to use if none is specified in the XContent body
* @param defaultName The privilege name to use if none is specified in the XContent body
* @param allowType If true, accept a "type" field (for which the value must match {@link #DOC_TYPE_VALUE});
*/
public static ApplicationPrivilegeDescriptor parse(XContentParser parser, String defaultApplication, String defaultName,
boolean allowType) throws IOException {
final Builder builder = PARSER.parse(parser, allowType);
if (builder.applicationName == null) {
builder.applicationName(defaultApplication);
}
if (builder.privilegeName == null) {
builder.privilegeName(defaultName);
}
return builder.build();
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
final ApplicationPrivilegeDescriptor that = (ApplicationPrivilegeDescriptor) o;
return Objects.equals(this.application, that.application) &&
Objects.equals(this.name, that.name) &&
Objects.equals(this.actions, that.actions) &&
Objects.equals(this.metadata, that.metadata);
}
@Override
public int hashCode() {
return Objects.hash(application, name, actions, metadata);
}
private static final class Builder {
private String applicationName;
private String privilegeName;
private Set<String> actions = Collections.emptySet();
private Map<String, Object> metadata = Collections.emptyMap();
private Builder applicationName(String applicationName) {
this.applicationName = applicationName;
return this;
}
private Builder privilegeName(String privilegeName) {
this.privilegeName = privilegeName;
return this;
}
private Builder actions(Collection<String> actions) {
this.actions = new HashSet<>(actions);
return this;
}
private Builder metadata(Map<String, Object> metadata) {
this.metadata = metadata;
return this;
}
private Builder type(String type, boolean allowed) {
if (allowed == false) {
throw new IllegalStateException("Field " + Fields.TYPE.getPreferredName() + " cannot be specified here");
}
if (ApplicationPrivilegeDescriptor.DOC_TYPE_VALUE.equals(type) == false) {
throw new IllegalStateException("XContent has wrong " + Fields.TYPE.getPreferredName() + " field " + type);
}
return this;
}
private ApplicationPrivilegeDescriptor build() {
return new ApplicationPrivilegeDescriptor(applicationName, privilegeName, actions, metadata);
}
}
public interface Fields {
ParseField APPLICATION = new ParseField("application");
ParseField NAME = new ParseField("name");
ParseField ACTIONS = new ParseField("actions");
ParseField METADATA = new ParseField("metadata");
ParseField TYPE = new ParseField("type");
}
}

View File

@ -0,0 +1,63 @@
/*
* 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.privilege;
import org.elasticsearch.common.ParseField;
import org.elasticsearch.common.io.stream.NamedWriteable;
import org.elasticsearch.common.xcontent.ToXContentFragment;
import org.elasticsearch.common.xcontent.XContentBuilder;
import org.elasticsearch.transport.TransportRequest;
import java.io.IOException;
import java.util.Collection;
import java.util.function.Predicate;
/**
* A ConditionalClusterPrivilege is a composition of a {@link ClusterPrivilege} (that determines which actions may be executed)
* with a {@link Predicate} for a {@link TransportRequest} (that determines which requests may be executed).
* The a given execution of an action is considered to be permitted if both the action and the request are permitted.
*/
public interface ConditionalClusterPrivilege extends NamedWriteable, ToXContentFragment {
/**
* The category under which this privilege should be rendered when output as XContent.
*/
Category getCategory();
/**
* The action-level privilege that is required by this conditional privilege.
*/
ClusterPrivilege getPrivilege();
/**
* The request-level privilege (as a {@link Predicate}) that is required by this conditional privilege.
*/
Predicate<TransportRequest> getRequestPredicate();
/**
* A {@link ConditionalClusterPrivilege} should generate a fragment of {@code XContent}, which consists of
* a single field name, followed by its value (which may be an object, an array, or a simple value).
*/
@Override
XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException;
/**
* Categories exist for to segment privileges for the purposes of rendering to XContent.
* {@link ConditionalClusterPrivileges#toXContent(XContentBuilder, Params, Collection)} builds one XContent
* object for a collection of {@link ConditionalClusterPrivilege} instances, with the top level fields built
* from the categories.
*/
enum Category {
APPLICATION(new ParseField("application"));
public final ParseField field;
Category(ParseField field) {
this.field = field;
}
}
}

View File

@ -0,0 +1,225 @@
/*
* 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.privilege;
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.xcontent.ToXContent;
import org.elasticsearch.common.xcontent.XContentBuilder;
import org.elasticsearch.common.xcontent.XContentParseException;
import org.elasticsearch.common.xcontent.XContentParser;
import org.elasticsearch.transport.TransportRequest;
import org.elasticsearch.xpack.core.security.action.privilege.ApplicationPrivilegesRequest;
import org.elasticsearch.xpack.core.security.authz.privilege.ConditionalClusterPrivilege.Category;
import org.elasticsearch.xpack.core.security.support.Automatons;
import org.elasticsearch.xpack.core.security.xcontent.XContentUtils;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
import java.util.function.Predicate;
/**
* Static utility class for working with {@link ConditionalClusterPrivilege} instances
*/
public final class ConditionalClusterPrivileges {
public static final ConditionalClusterPrivilege[] EMPTY_ARRAY = new ConditionalClusterPrivilege[0];
private ConditionalClusterPrivileges() {
}
/**
* Utility method to read an array of {@link ConditionalClusterPrivilege} objects from a {@link StreamInput}
*/
public static ConditionalClusterPrivilege[] readArray(StreamInput in) throws IOException {
return in.readArray(in1 ->
in1.readNamedWriteable(ConditionalClusterPrivilege.class), ConditionalClusterPrivilege[]::new);
}
/**
* Utility method to write an array of {@link ConditionalClusterPrivilege} objects to a {@link StreamOutput}
*/
public static void writeArray(StreamOutput out, ConditionalClusterPrivilege[] privileges) throws IOException {
out.writeArray((out1, value) -> out1.writeNamedWriteable(value), privileges);
}
/**
* Writes a single object value to the {@code builder} that contains each of the provided privileges.
* The privileges are grouped according to their {@link ConditionalClusterPrivilege#getCategory() categories}
*/
public static XContentBuilder toXContent(XContentBuilder builder, ToXContent.Params params,
Collection<ConditionalClusterPrivilege> privileges) throws IOException {
builder.startObject();
for (Category category : Category.values()) {
builder.startObject(category.field.getPreferredName());
for (ConditionalClusterPrivilege privilege : privileges) {
if (category == privilege.getCategory()) {
privilege.toXContent(builder, params);
}
}
builder.endObject();
}
return builder.endObject();
}
/**
* Read a list of privileges from the parser. The parser should be positioned at the
* {@link XContentParser.Token#START_OBJECT} token for the privileges value
*/
public static List<ConditionalClusterPrivilege> parse(XContentParser parser) throws IOException {
List<ConditionalClusterPrivilege> privileges = new ArrayList<>();
expectedToken(parser.currentToken(), parser, XContentParser.Token.START_OBJECT);
while (parser.nextToken() != XContentParser.Token.END_OBJECT) {
expectedToken(parser.currentToken(), parser, XContentParser.Token.FIELD_NAME);
expectFieldName(parser, Category.APPLICATION.field);
expectedToken(parser.nextToken(), parser, XContentParser.Token.START_OBJECT);
expectedToken(parser.nextToken(), parser, XContentParser.Token.FIELD_NAME);
expectFieldName(parser, ManageApplicationPrivileges.Fields.MANAGE);
privileges.add(ManageApplicationPrivileges.parse(parser));
expectedToken(parser.nextToken(), parser, XContentParser.Token.END_OBJECT);
}
return privileges;
}
private static void expectedToken(XContentParser.Token read, XContentParser parser, XContentParser.Token expected) {
if (read != expected) {
throw new XContentParseException(parser.getTokenLocation(),
"failed to parse privilege. expected [" + expected + "] but found [" + read + "] instead");
}
}
private static void expectFieldName(XContentParser parser, ParseField... fields) throws IOException {
final String fieldName = parser.currentName();
if (Arrays.stream(fields).anyMatch(pf -> pf.match(fieldName, parser.getDeprecationHandler())) == false) {
throw new XContentParseException(parser.getTokenLocation(),
"failed to parse privilege. expected " + (fields.length == 1 ? "field name" : "one of") + " ["
+ Strings.arrayToCommaDelimitedString(fields) + "] but found [" + fieldName + "] instead");
}
}
/**
* The {@code ManageApplicationPrivileges} privilege is a {@link ConditionalClusterPrivilege} that grants the
* ability to execute actions related to the management of application privileges (Get, Put, Delete) for a subset
* of applications (identified by a wildcard-aware application-name).
*/
public static class ManageApplicationPrivileges implements ConditionalClusterPrivilege {
private static final ClusterPrivilege PRIVILEGE = ClusterPrivilege.get(
Collections.singleton("cluster:admin/xpack/security/privilege/*")
);
public static final String WRITEABLE_NAME = "manage-application-privileges";
private final Set<String> applicationNames;
private final Predicate<String> applicationPredicate;
private final Predicate<TransportRequest> requestPredicate;
public ManageApplicationPrivileges(Set<String> applicationNames) {
this.applicationNames = Collections.unmodifiableSet(applicationNames);
this.applicationPredicate = Automatons.predicate(applicationNames);
this.requestPredicate = request -> {
if (request instanceof ApplicationPrivilegesRequest) {
final ApplicationPrivilegesRequest privRequest = (ApplicationPrivilegesRequest) request;
return privRequest.getApplicationNames().stream().allMatch(application -> applicationPredicate.test(application));
}
return false;
};
}
@Override
public Category getCategory() {
return Category.APPLICATION;
}
@Override
public ClusterPrivilege getPrivilege() {
return PRIVILEGE;
}
@Override
public Predicate<TransportRequest> getRequestPredicate() {
return this.requestPredicate;
}
public Collection<String> getApplicationNames() {
return Collections.unmodifiableCollection(this.applicationNames);
}
@Override
public String getWriteableName() {
return WRITEABLE_NAME;
}
@Override
public void writeTo(StreamOutput out) throws IOException {
out.writeCollection(this.applicationNames, StreamOutput::writeString);
}
public static ManageApplicationPrivileges createFrom(StreamInput in) throws IOException {
final Set<String> applications = in.readSet(StreamInput::readString);
return new ManageApplicationPrivileges(applications);
}
@Override
public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
return builder.field(Fields.MANAGE.getPreferredName(),
Collections.singletonMap(Fields.APPLICATIONS.getPreferredName(), applicationNames)
);
}
public static ManageApplicationPrivileges parse(XContentParser parser) throws IOException {
expectedToken(parser.currentToken(), parser, XContentParser.Token.FIELD_NAME);
expectFieldName(parser, Fields.MANAGE);
expectedToken(parser.nextToken(), parser, XContentParser.Token.START_OBJECT);
expectedToken(parser.nextToken(), parser, XContentParser.Token.FIELD_NAME);
expectFieldName(parser, Fields.APPLICATIONS);
expectedToken(parser.nextToken(), parser, XContentParser.Token.START_ARRAY);
final String[] applications = XContentUtils.readStringArray(parser, false);
expectedToken(parser.nextToken(), parser, XContentParser.Token.END_OBJECT);
return new ManageApplicationPrivileges(new LinkedHashSet<>(Arrays.asList(applications)));
}
@Override
public String toString() {
return "{" + getCategory() + ":" + Fields.MANAGE.getPreferredName() + ":" + Fields.APPLICATIONS.getPreferredName() + "="
+ Strings.collectionToDelimitedString(applicationNames, ",") + "}";
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
final ManageApplicationPrivileges that = (ManageApplicationPrivileges) o;
return this.applicationNames.equals(that.applicationNames);
}
@Override
public int hashCode() {
return applicationNames.hashCode();
}
private interface Fields {
ParseField MANAGE = new ParseField("manage");
ParseField APPLICATIONS = new ParseField("applications");
}
}
}

View File

@ -9,6 +9,8 @@ import org.elasticsearch.common.collect.MapBuilder;
import org.elasticsearch.xpack.core.monitoring.action.MonitoringBulkAction;
import org.elasticsearch.xpack.core.security.authz.RoleDescriptor;
import org.elasticsearch.xpack.core.security.authz.permission.Role;
import org.elasticsearch.xpack.core.security.authz.privilege.ConditionalClusterPrivilege;
import org.elasticsearch.xpack.core.security.authz.privilege.ConditionalClusterPrivileges.ManageApplicationPrivileges;
import org.elasticsearch.xpack.core.security.support.MetadataUtils;
import org.elasticsearch.xpack.core.security.user.KibanaUser;
import org.elasticsearch.xpack.core.security.user.UsernamesField;
@ -27,8 +29,11 @@ public class ReservedRolesStore {
new String[] { "all" },
new RoleDescriptor.IndicesPrivileges[] {
RoleDescriptor.IndicesPrivileges.builder().indices("*").privileges("all").build()},
new String[] { "*" },
MetadataUtils.DEFAULT_RESERVED_METADATA);
new RoleDescriptor.ApplicationResourcePrivileges[] {
RoleDescriptor.ApplicationResourcePrivileges.builder().application("*").privileges("*").resources("*").build()
},
null, new String[] { "*" },
MetadataUtils.DEFAULT_RESERVED_METADATA, Collections.emptyMap());
public static final Role SUPERUSER_ROLE = Role.builder(SUPERUSER_ROLE_DESCRIPTOR, null).build();
private static final Map<String, RoleDescriptor> RESERVED_ROLES = initializeReservedRoles();
@ -43,7 +48,11 @@ public class ReservedRolesStore {
MetadataUtils.DEFAULT_RESERVED_METADATA))
.put("kibana_user", new RoleDescriptor("kibana_user", null, new RoleDescriptor.IndicesPrivileges[] {
RoleDescriptor.IndicesPrivileges.builder().indices(".kibana*").privileges("manage", "read", "index", "delete")
.build() }, null, MetadataUtils.DEFAULT_RESERVED_METADATA))
.build() }, new RoleDescriptor.ApplicationResourcePrivileges[] {
RoleDescriptor.ApplicationResourcePrivileges.builder()
.application("kibana-.kibana").resources("*").privileges("all").build() },
null, null,
MetadataUtils.DEFAULT_RESERVED_METADATA, null))
.put("monitoring_user", new RoleDescriptor("monitoring_user",
new String[] { "cluster:monitor/main" },
new RoleDescriptor.IndicesPrivileges[] {
@ -70,13 +79,19 @@ public class ReservedRolesStore {
"kibana_dashboard_only_user",
null,
new RoleDescriptor.IndicesPrivileges[] {
RoleDescriptor.IndicesPrivileges.builder()
.indices(".kibana*").privileges("read", "view_index_metadata").build()
RoleDescriptor.IndicesPrivileges.builder()
.indices(".kibana*").privileges("read", "view_index_metadata").build()
},
null,
MetadataUtils.DEFAULT_RESERVED_METADATA))
new RoleDescriptor.ApplicationResourcePrivileges[] {
RoleDescriptor.ApplicationResourcePrivileges.builder()
.application("kibana-.kibana").resources("*").privileges("read").build() },
null, null,
MetadataUtils.DEFAULT_RESERVED_METADATA,
null))
.put(KibanaUser.ROLE_NAME, new RoleDescriptor(KibanaUser.ROLE_NAME,
new String[] { "monitor", "manage_index_templates", MonitoringBulkAction.NAME, "manage_saml" },
new String[] {
"monitor", "manage_index_templates", MonitoringBulkAction.NAME, "manage_saml",
},
new RoleDescriptor.IndicesPrivileges[] {
RoleDescriptor.IndicesPrivileges.builder().indices(".kibana*", ".reporting-*").privileges("all").build(),
RoleDescriptor.IndicesPrivileges.builder()
@ -84,7 +99,9 @@ public class ReservedRolesStore {
RoleDescriptor.IndicesPrivileges.builder()
.indices(".management-beats").privileges("create_index", "read", "write").build()
},
null, MetadataUtils.DEFAULT_RESERVED_METADATA))
null,
new ConditionalClusterPrivilege[] { new ManageApplicationPrivileges(Collections.singleton("kibana-*")) },
null, MetadataUtils.DEFAULT_RESERVED_METADATA, null))
.put("logstash_system", new RoleDescriptor("logstash_system", new String[] { "monitor", MonitoringBulkAction.NAME},
null, null, MetadataUtils.DEFAULT_RESERVED_METADATA))
.put("beats_admin", new RoleDescriptor("beats_admin",

View File

@ -10,6 +10,12 @@ import org.elasticsearch.action.ActionListener;
import org.elasticsearch.client.ElasticsearchClient;
import org.elasticsearch.common.bytes.BytesReference;
import org.elasticsearch.common.xcontent.XContentType;
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;
import org.elasticsearch.xpack.core.security.action.privilege.GetPrivilegesRequestBuilder;
import org.elasticsearch.xpack.core.security.action.privilege.PutPrivilegesAction;
import org.elasticsearch.xpack.core.security.action.privilege.PutPrivilegesRequestBuilder;
import org.elasticsearch.xpack.core.security.action.realm.ClearRealmCacheAction;
import org.elasticsearch.xpack.core.security.action.realm.ClearRealmCacheRequest;
import org.elasticsearch.xpack.core.security.action.realm.ClearRealmCacheRequestBuilder;
@ -167,7 +173,9 @@ public class SecurityClient {
client.execute(HasPrivilegesAction.INSTANCE, request, listener);
}
/** User Management */
/**
* User Management
*/
public GetUsersRequestBuilder prepareGetUsers(String... usernames) {
return new GetUsersRequestBuilder(client).usernames(usernames);
@ -223,7 +231,9 @@ public class SecurityClient {
client.execute(SetEnabledAction.INSTANCE, request, listener);
}
/** Role Management */
/**
* Role Management
*/
public GetRolesRequestBuilder prepareGetRoles(String... names) {
return new GetRolesRequestBuilder(client).names(names);
@ -253,7 +263,9 @@ public class SecurityClient {
client.execute(PutRoleAction.INSTANCE, request, listener);
}
/** Role Mappings */
/**
* Role Mappings
*/
public GetRoleMappingsRequestBuilder prepareGetRoleMappings(String... names) {
return new GetRoleMappingsRequestBuilder(client, GetRoleMappingsAction.INSTANCE)
@ -275,6 +287,27 @@ public class SecurityClient {
.name(name);
}
/* -- Application Privileges -- */
public GetPrivilegesRequestBuilder prepareGetPrivileges(String applicationName, String[] privileges) {
return new GetPrivilegesRequestBuilder(client, GetPrivilegesAction.INSTANCE).application(applicationName).privileges(privileges);
}
public PutPrivilegesRequestBuilder preparePutPrivilege(String applicationName, String privilegeName,
BytesReference bytesReference, XContentType xContentType) throws IOException {
return new PutPrivilegesRequestBuilder(client, PutPrivilegesAction.INSTANCE)
.source(applicationName, privilegeName, bytesReference, xContentType);
}
public PutPrivilegesRequestBuilder preparePutPrivileges(BytesReference bytesReference, XContentType xContentType) throws IOException {
return new PutPrivilegesRequestBuilder(client, PutPrivilegesAction.INSTANCE).source(bytesReference, xContentType);
}
public DeletePrivilegesRequestBuilder prepareDeletePrivileges(String applicationName, String[] privileges) {
return new DeletePrivilegesRequestBuilder(client, DeletePrivilegesAction.INSTANCE)
.application(applicationName)
.privileges(privileges);
}
public CreateTokenRequestBuilder prepareCreateToken() {
return new CreateTokenRequestBuilder(client, CreateTokenAction.INSTANCE);
}
@ -298,7 +331,7 @@ public class SecurityClient {
return builder;
}
public void samlAuthenticate(SamlAuthenticateRequest request, ActionListener< SamlAuthenticateResponse> listener) {
public void samlAuthenticate(SamlAuthenticateRequest request, ActionListener<SamlAuthenticateResponse> listener) {
client.execute(SamlAuthenticateAction.INSTANCE, request, listener);
}

View File

@ -91,6 +91,41 @@
}
}
},
"applications": {
"type": "object",
"properties": {
"application": {
"type": "keyword"
},
"privileges": {
"type": "keyword"
},
"resources": {
"type": "keyword"
}
}
},
"application" : {
"type" : "keyword"
},
"global": {
"type": "object",
"properties": {
"application": {
"type": "object",
"properties": {
"manage": {
"type": "object",
"properties": {
"applications": {
"type": "keyword"
}
}
}
}
}
}
},
"name" : {
"type" : "keyword"
},
@ -103,6 +138,9 @@
"type" : {
"type" : "keyword"
},
"actions" : {
"type" : "keyword"
},
"expiration_time" : {
"type" : "date",
"format" : "epoch_millis"

View File

@ -12,6 +12,7 @@ import org.elasticsearch.cluster.node.DiscoveryNode;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.common.transport.TransportAddress;
import org.elasticsearch.common.unit.TimeValue;
import org.elasticsearch.protocol.xpack.license.PutLicenseResponse;
import java.net.InetAddress;

View File

@ -9,6 +9,8 @@ import org.elasticsearch.action.ActionListener;
import org.elasticsearch.cluster.ClusterStateUpdateTask;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.common.unit.TimeValue;
import org.elasticsearch.protocol.xpack.license.LicensesStatus;
import org.elasticsearch.protocol.xpack.license.PutLicenseResponse;
import static org.elasticsearch.common.unit.TimeValue.timeValueHours;
import static org.hamcrest.Matchers.equalTo;

View File

@ -11,6 +11,7 @@ import org.elasticsearch.cluster.service.ClusterService;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.common.unit.TimeValue;
import org.elasticsearch.plugins.Plugin;
import org.elasticsearch.protocol.xpack.license.LicensesStatus;
import org.elasticsearch.test.ESSingleNodeTestCase;
import org.elasticsearch.xpack.core.LocalStateCompositeXPackPlugin;
import org.elasticsearch.xpack.core.XPackSettings;
@ -145,4 +146,4 @@ public class LicensesManagerServiceTests extends ESSingleNodeTestCase {
}
assertThat("remove license(s) failed", success.get(), equalTo(true));
}
}
}

View File

@ -12,6 +12,8 @@ import org.elasticsearch.common.unit.TimeValue;
import org.elasticsearch.common.xcontent.XContentType;
import org.elasticsearch.node.Node;
import org.elasticsearch.plugins.Plugin;
import org.elasticsearch.protocol.xpack.license.LicensesStatus;
import org.elasticsearch.protocol.xpack.license.PutLicenseResponse;
import org.elasticsearch.test.ESSingleNodeTestCase;
import org.elasticsearch.xpack.core.LocalStateCompositeXPackPlugin;
import org.elasticsearch.xpack.core.XPackSettings;
@ -230,4 +232,4 @@ public class LicensesTransportTests extends ESSingleNodeTestCase {
assertThat(putLicenseResponse.isAcknowledged(), equalTo(true));
assertThat(putLicenseResponse.status(), equalTo(LicensesStatus.VALID));
}
}
}

View File

@ -1,103 +0,0 @@
/*
* 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.license;
import org.elasticsearch.common.bytes.BytesReference;
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.common.xcontent.XContentHelper;
import org.elasticsearch.test.ESTestCase;
import java.io.IOException;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.not;
import static org.hamcrest.Matchers.sameInstance;
public class PutLicenseResponseTests extends ESTestCase {
@SuppressWarnings("unchecked")
public void testSerialization() throws Exception {
boolean acknowledged = randomBoolean();
LicensesStatus status = randomFrom(LicensesStatus.VALID, LicensesStatus.INVALID, LicensesStatus.EXPIRED);
Map<String, String[]> ackMessages = randomAckMessages();
PutLicenseResponse response = new PutLicenseResponse(acknowledged, status, "", ackMessages);
XContentBuilder contentBuilder = XContentFactory.jsonBuilder();
response.toXContent(contentBuilder, ToXContent.EMPTY_PARAMS);
Map<String, Object> map = XContentHelper.convertToMap(BytesReference.bytes(contentBuilder), false,
contentBuilder.contentType()).v2();
assertThat(map.containsKey("acknowledged"), equalTo(true));
boolean actualAcknowledged = (boolean) map.get("acknowledged");
assertThat(actualAcknowledged, equalTo(acknowledged));
assertThat(map.containsKey("license_status"), equalTo(true));
String actualStatus = (String) map.get("license_status");
assertThat(actualStatus, equalTo(status.name().toLowerCase(Locale.ROOT)));
assertThat(map.containsKey("acknowledge"), equalTo(true));
Map<String, List<String>> actualAckMessages = (Map<String, List<String>>) map.get("acknowledge");
assertTrue(actualAckMessages.containsKey("message"));
actualAckMessages.remove("message");
assertThat(actualAckMessages.keySet(), equalTo(ackMessages.keySet()));
for (Map.Entry<String, List<String>> entry : actualAckMessages.entrySet()) {
assertArrayEquals(entry.getValue().toArray(), ackMessages.get(entry.getKey()));
}
}
public void testStreamSerialization() throws IOException {
boolean acknowledged = randomBoolean();
LicensesStatus status = randomFrom(LicensesStatus.VALID, LicensesStatus.INVALID, LicensesStatus.EXPIRED);
Map<String, String[]> ackMessages = randomAckMessages();
// write the steam so that we can attempt to read it back
BytesStreamOutput output = new BytesStreamOutput();
PutLicenseResponse response = new PutLicenseResponse(acknowledged, status, "", ackMessages);
// write it out
response.writeTo(output);
StreamInput input = output.bytes().streamInput();
// read it back in
response.readFrom(input);
assertThat(response.isAcknowledged(), equalTo(acknowledged));
assertThat(response.status(), equalTo(status));
assertThat(response.acknowledgeMessages(), not(sameInstance(ackMessages)));
assertThat(response.acknowledgeMessages().size(), equalTo(ackMessages.size()));
for (String key : ackMessages.keySet()) {
assertArrayEquals(ackMessages.get(key), response.acknowledgeMessages().get(key));
}
}
private static Map<String, String[]> randomAckMessages() {
int nFeatures = randomIntBetween(1, 5);
Map<String, String[]> ackMessages = new HashMap<>();
for (int i = 0; i < nFeatures; i++) {
String feature = randomAlphaOfLengthBetween(9, 15);
int nMessages = randomIntBetween(1, 5);
String[] messages = new String[nMessages];
for (int j = 0; j < nMessages; j++) {
messages[j] = randomAlphaOfLengthBetween(10, 30);
}
ackMessages.put(feature, messages);
}
return ackMessages;
}
}

View File

@ -20,6 +20,8 @@ import org.elasticsearch.common.xcontent.XContentBuilder;
import org.elasticsearch.common.xcontent.XContentFactory;
import org.elasticsearch.common.xcontent.XContentType;
import org.elasticsearch.license.licensor.LicenseSigner;
import org.elasticsearch.protocol.xpack.license.LicensesStatus;
import org.elasticsearch.protocol.xpack.license.PutLicenseResponse;
import org.hamcrest.MatcherAssert;
import org.joda.time.format.DateTimeFormatter;
import org.junit.Assert;

View File

@ -5,16 +5,16 @@
*/
package org.elasticsearch.test;
import org.hamcrest.CustomMatcher;
import org.hamcrest.Matcher;
import org.hamcrest.Matchers;
import java.nio.file.Files;
import java.nio.file.LinkOption;
import java.nio.file.Path;
import java.util.function.Predicate;
import java.util.regex.Pattern;
import org.hamcrest.CustomMatcher;
import org.hamcrest.Matcher;
import org.hamcrest.Matchers;
public class TestMatchers extends Matchers {
public static Matcher<Path> pathExists(Path path, LinkOption... options) {
@ -26,6 +26,19 @@ public class TestMatchers extends Matchers {
};
}
public static <T> Matcher<Predicate<T>> predicateMatches(T value) {
return new CustomMatcher<Predicate<T>>("Matches " + value) {
@Override
public boolean matches(Object item) {
if (Predicate.class.isInstance(item)) {
return ((Predicate<T>) item).test(value);
} else {
return false;
}
}
};
}
public static Matcher<String> matchesPattern(String regex) {
return matchesPattern(Pattern.compile(regex));
}
@ -34,16 +47,17 @@ public class TestMatchers extends Matchers {
return predicate("Matches " + pattern.pattern(), String.class, pattern.asPredicate());
}
private static <T> Matcher<T> predicate(String description, Class<T> type, Predicate<T> stringPredicate) {
private static <T> Matcher<T> predicate(String description, Class<T> type, Predicate<T> predicate) {
return new CustomMatcher<T>(description) {
@Override
public boolean matches(Object item) {
if (type.isInstance(item)) {
return stringPredicate.test(type.cast(item));
return predicate.test(type.cast(item));
} else {
return false;
}
}
};
}
}

View File

@ -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.privilege;
import org.elasticsearch.action.ActionRequestValidationException;
import org.elasticsearch.action.support.WriteRequest;
import org.elasticsearch.common.io.stream.BytesStreamOutput;
import org.elasticsearch.test.ESTestCase;
import org.hamcrest.Matchers;
import java.io.IOException;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.notNullValue;
import static org.hamcrest.Matchers.nullValue;
public class DeletePrivilegesRequestTests extends ESTestCase {
public void testSerialization() throws IOException {
final DeletePrivilegesRequest original = new DeletePrivilegesRequest(
randomAlphaOfLengthBetween(3, 8), generateRandomStringArray(5, randomIntBetween(3, 8), false, false));
original.setRefreshPolicy(randomFrom(WriteRequest.RefreshPolicy.values()));
final BytesStreamOutput output = new BytesStreamOutput();
original.writeTo(output);
output.flush();
final DeletePrivilegesRequest copy = new DeletePrivilegesRequest();
copy.readFrom(output.bytes().streamInput());
assertThat(copy.application(), equalTo(original.application()));
assertThat(copy.privileges(), equalTo(original.privileges()));
assertThat(copy.getRefreshPolicy(), equalTo(original.getRefreshPolicy()));
}
public void testValidation() {
assertValidationFailure(new DeletePrivilegesRequest(null, null), "application name", "privileges");
assertValidationFailure(new DeletePrivilegesRequest("", null), "application name", "privileges");
assertValidationFailure(new DeletePrivilegesRequest(null, new String[0]), "application name", "privileges");
assertValidationFailure(new DeletePrivilegesRequest("", new String[0]), "application name", "privileges");
assertValidationFailure(new DeletePrivilegesRequest(null, new String[]{"all"}), "application name");
assertValidationFailure(new DeletePrivilegesRequest("", new String[]{"all"}), "application name");
assertValidationFailure(new DeletePrivilegesRequest("app", null), "privileges");
assertValidationFailure(new DeletePrivilegesRequest("app", new String[0]), "privileges");
assertValidationFailure(new DeletePrivilegesRequest("app", new String[]{""}), "privileges");
assertThat(new DeletePrivilegesRequest("app", new String[]{"all"}).validate(), nullValue());
assertThat(new DeletePrivilegesRequest("app", new String[]{"all", "some"}).validate(), nullValue());
}
private void assertValidationFailure(DeletePrivilegesRequest request, String... messages) {
final ActionRequestValidationException exception = request.validate();
assertThat(exception, notNullValue());
for (String message : messages) {
assertThat(exception.validationErrors(), Matchers.hasItem(containsString(message)));
}
}
}

View File

@ -0,0 +1,30 @@
/*
* 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.privilege;
import org.elasticsearch.common.io.stream.BytesStreamOutput;
import org.elasticsearch.test.ESTestCase;
import java.io.IOException;
import java.util.Arrays;
import static org.hamcrest.Matchers.equalTo;
public class DeletePrivilegesResponseTests extends ESTestCase {
public void testSerialization() throws IOException {
final DeletePrivilegesResponse original = new DeletePrivilegesResponse(
Arrays.asList(generateRandomStringArray(5, randomIntBetween(3, 8), false, true)));
final BytesStreamOutput output = new BytesStreamOutput();
original.writeTo(output);
output.flush();
final DeletePrivilegesResponse copy = new DeletePrivilegesResponse();
copy.readFrom(output.bytes().streamInput());
assertThat(copy.found(), equalTo(original.found()));
}
}

View File

@ -0,0 +1,58 @@
/*
* 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.privilege;
import org.elasticsearch.action.ActionRequestValidationException;
import org.elasticsearch.common.io.stream.BytesStreamOutput;
import org.elasticsearch.test.ESTestCase;
import org.hamcrest.Matchers;
import java.io.IOException;
import static org.hamcrest.Matchers.containsInAnyOrder;
import static org.hamcrest.Matchers.notNullValue;
import static org.hamcrest.Matchers.nullValue;
public class GetPrivilegesRequestTests extends ESTestCase {
public void testSerialization() throws IOException {
final GetPrivilegesRequest original = new GetPrivilegesRequest();
if (randomBoolean()) {
original.application(randomAlphaOfLengthBetween(3, 8));
}
original.privileges(generateRandomStringArray(3, 5, false, true));
final BytesStreamOutput out = new BytesStreamOutput();
original.writeTo(out);
final GetPrivilegesRequest copy = new GetPrivilegesRequest();
copy.readFrom(out.bytes().streamInput());
assertThat(original.application(), Matchers.equalTo(copy.application()));
assertThat(original.privileges(), Matchers.equalTo(copy.privileges()));
}
public void testValidation() {
assertThat(request(null).validate(), nullValue());
assertThat(request(null, "all").validate(), nullValue());
assertThat(request(null, "read", "write").validate(), nullValue());
assertThat(request("my_app").validate(), nullValue());
assertThat(request("my_app", "all").validate(), nullValue());
assertThat(request("my_app", "read", "write").validate(), nullValue());
final ActionRequestValidationException exception = request("my_app", ((String[]) null)).validate();
assertThat(exception, notNullValue());
assertThat(exception.validationErrors(), containsInAnyOrder("privileges cannot be null"));
}
private GetPrivilegesRequest request(String application, String... privileges) {
final GetPrivilegesRequest request = new GetPrivilegesRequest();
request.application(application);
request.privileges(privileges);
return request;
}
}

View File

@ -0,0 +1,41 @@
/*
* 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.privilege;
import org.elasticsearch.common.io.stream.BytesStreamOutput;
import org.elasticsearch.common.util.set.Sets;
import org.elasticsearch.test.ESTestCase;
import org.elasticsearch.xpack.core.security.authz.privilege.ApplicationPrivilegeDescriptor;
import org.hamcrest.Matchers;
import java.io.IOException;
import java.util.Collections;
import java.util.Locale;
public class GetPrivilegesResponseTests extends ESTestCase {
public void testSerialization() throws IOException {
ApplicationPrivilegeDescriptor[] privileges = randomArray(6, ApplicationPrivilegeDescriptor[]::new, () ->
new ApplicationPrivilegeDescriptor(
randomAlphaOfLengthBetween(3, 8).toLowerCase(Locale.ROOT),
randomAlphaOfLengthBetween(3, 8).toLowerCase(Locale.ROOT),
Sets.newHashSet(randomArray(3, String[]::new, () -> randomAlphaOfLength(3).toLowerCase(Locale.ROOT) + "/*")),
Collections.emptyMap()
)
);
final GetPrivilegesResponse original = new GetPrivilegesResponse(privileges);
final BytesStreamOutput out = new BytesStreamOutput();
original.writeTo(out);
final GetPrivilegesResponse copy = new GetPrivilegesResponse();
copy.readFrom(out.bytes().streamInput());
assertThat(copy.privileges(), Matchers.equalTo(original.privileges()));
}
}

View File

@ -0,0 +1,97 @@
/*
* 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.privilege;
import org.elasticsearch.action.ActionRequestValidationException;
import org.elasticsearch.action.support.WriteRequest;
import org.elasticsearch.common.io.stream.BytesStreamOutput;
import org.elasticsearch.common.util.set.Sets;
import org.elasticsearch.test.ESTestCase;
import org.elasticsearch.xpack.core.security.authz.privilege.ApplicationPrivilegeDescriptor;
import org.hamcrest.Matchers;
import java.io.IOException;
import java.util.Arrays;
import java.util.Collections;
import java.util.Locale;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.hasItem;
import static org.hamcrest.Matchers.notNullValue;
public class PutPrivilegesRequestTests extends ESTestCase {
public void testSerialization() throws IOException {
final PutPrivilegesRequest original = request(randomArray(8, ApplicationPrivilegeDescriptor[]::new,
() -> new ApplicationPrivilegeDescriptor(
randomAlphaOfLengthBetween(3, 8).toLowerCase(Locale.ROOT),
randomAlphaOfLengthBetween(3, 8).toLowerCase(Locale.ROOT),
Sets.newHashSet(randomArray(3, String[]::new, () -> randomAlphaOfLength(3).toLowerCase(Locale.ROOT) + "/*")),
Collections.emptyMap()
)
));
original.setRefreshPolicy(randomFrom(WriteRequest.RefreshPolicy.values()));
final BytesStreamOutput out = new BytesStreamOutput();
original.writeTo(out);
final PutPrivilegesRequest copy = new PutPrivilegesRequest();
copy.readFrom(out.bytes().streamInput());
assertThat(original.getPrivileges(), Matchers.equalTo(copy.getPrivileges()));
assertThat(original.getRefreshPolicy(), Matchers.equalTo(copy.getRefreshPolicy()));
}
public void testValidation() {
// wildcard app name
final ApplicationPrivilegeDescriptor wildcardApp = descriptor("*", "all", "*");
assertValidationFailure(request(wildcardApp), "Application names may not contain");
// invalid priv names
final ApplicationPrivilegeDescriptor spaceName = descriptor("app", "r e a d", "read/*");
final ApplicationPrivilegeDescriptor numericName = descriptor("app", "7346", "read/*");
assertValidationFailure(request(spaceName), "Application privilege names must match");
assertValidationFailure(request(numericName), "Application privilege names must match");
// no actions
final ApplicationPrivilegeDescriptor nothing = descriptor("*", "nothing");
assertValidationFailure(request(nothing), "Application privileges must have at least one action");
// reserved metadata
final ApplicationPrivilegeDescriptor reservedMetadata = new ApplicationPrivilegeDescriptor("app", "all",
Collections.emptySet(), Collections.singletonMap("_notAllowed", true)
);
assertValidationFailure(request(reservedMetadata), "metadata keys may not start");
ApplicationPrivilegeDescriptor badAction = descriptor("app", "foo", randomFrom("data.read", "data_read", "data+read", "read"));
assertValidationFailure(request(badAction), "must contain one of");
// mixed
assertValidationFailure(request(wildcardApp, numericName, reservedMetadata, badAction),
"Application names may not contain", "Application privilege names must match", "metadata keys may not start",
"must contain one of");
}
private ApplicationPrivilegeDescriptor descriptor(String application, String name, String... actions) {
return new ApplicationPrivilegeDescriptor(application, name, Sets.newHashSet(actions), Collections.emptyMap());
}
private void assertValidationFailure(PutPrivilegesRequest request, String... messages) {
final ActionRequestValidationException exception = request.validate();
assertThat(exception, notNullValue());
for (String message : messages) {
assertThat(exception.validationErrors(), hasItem(containsString(message)));
}
}
private PutPrivilegesRequest request(ApplicationPrivilegeDescriptor... privileges) {
final PutPrivilegesRequest original = new PutPrivilegesRequest();
original.setPrivileges(Arrays.asList(privileges));
return original;
}
}

View File

@ -0,0 +1,42 @@
/*
* 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.privilege;
import org.elasticsearch.common.Strings;
import org.elasticsearch.common.io.stream.BytesStreamOutput;
import org.elasticsearch.test.ESTestCase;
import java.io.IOException;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import static org.hamcrest.Matchers.equalTo;
public class PutPrivilegesResponseTests extends ESTestCase {
public void testSerialization() throws IOException {
final int applicationCount = randomInt(3);
final Map<String, List<String>> map = new HashMap<>(applicationCount);
for (int i = 0; i < applicationCount; i++) {
map.put(randomAlphaOfLengthBetween(3, 8),
Arrays.asList(generateRandomStringArray(5, 6, false, true))
);
}
final PutPrivilegesResponse original = new PutPrivilegesResponse(map);
final BytesStreamOutput output = new BytesStreamOutput();
original.writeTo(output);
output.flush();
final PutPrivilegesResponse copy = new PutPrivilegesResponse();
copy.readFrom(output.bytes().streamInput());
assertThat(copy.created(), equalTo(original.created()));
assertThat(Strings.toString(copy), equalTo(Strings.toString(original)));
}
}

View File

@ -0,0 +1,164 @@
/*
* 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.role;
import org.elasticsearch.Version;
import org.elasticsearch.action.ActionRequestValidationException;
import org.elasticsearch.action.support.WriteRequest;
import org.elasticsearch.common.Strings;
import org.elasticsearch.common.bytes.BytesReference;
import org.elasticsearch.common.io.stream.ByteBufferStreamInput;
import org.elasticsearch.common.io.stream.BytesStreamOutput;
import org.elasticsearch.common.io.stream.NamedWriteableAwareStreamInput;
import org.elasticsearch.common.io.stream.NamedWriteableRegistry;
import org.elasticsearch.common.io.stream.StreamInput;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.common.util.set.Sets;
import org.elasticsearch.test.ESTestCase;
import org.elasticsearch.test.VersionUtils;
import org.elasticsearch.xpack.core.XPackClientPlugin;
import org.elasticsearch.xpack.core.security.authz.RoleDescriptor.ApplicationResourcePrivileges;
import org.elasticsearch.xpack.core.security.authz.privilege.ConditionalClusterPrivileges;
import java.io.IOException;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import java.util.function.Supplier;
import static org.hamcrest.Matchers.arrayWithSize;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.hasItem;
import static org.hamcrest.Matchers.iterableWithSize;
import static org.hamcrest.Matchers.notNullValue;
import static org.hamcrest.Matchers.nullValue;
public class PutRoleRequestTests extends ESTestCase {
public void testValidationOfApplicationPrivileges() {
assertSuccessfulValidation(buildRequestWithApplicationPrivilege("app", new String[]{"read"}, new String[]{"*"}));
assertSuccessfulValidation(buildRequestWithApplicationPrivilege("app", new String[]{"action:login"}, new String[]{"/"}));
assertSuccessfulValidation(buildRequestWithApplicationPrivilege("*", new String[]{"data/read:user"}, new String[]{"user/123"}));
// Fail
assertValidationError("privilege names and actions must match the pattern",
buildRequestWithApplicationPrivilege("app", new String[]{"in valid"}, new String[]{"*"}));
assertValidationError("An application name prefix must match the pattern",
buildRequestWithApplicationPrivilege("000", new String[]{"all"}, new String[]{"*"}));
assertValidationError("An application name prefix must match the pattern",
buildRequestWithApplicationPrivilege("%*", new String[]{"all"}, new String[]{"*"}));
}
public void testSerialization() throws IOException {
final PutRoleRequest original = buildRandomRequest();
final BytesStreamOutput out = new BytesStreamOutput();
original.writeTo(out);
final PutRoleRequest copy = new PutRoleRequest();
final NamedWriteableRegistry registry = new NamedWriteableRegistry(new XPackClientPlugin(Settings.EMPTY).getNamedWriteables());
StreamInput in = new NamedWriteableAwareStreamInput(ByteBufferStreamInput.wrap(BytesReference.toBytes(out.bytes())), registry);
copy.readFrom(in);
assertThat(copy.roleDescriptor(), equalTo(original.roleDescriptor()));
}
public void testSerializationV63AndBefore() throws IOException {
final PutRoleRequest original = buildRandomRequest();
final BytesStreamOutput out = new BytesStreamOutput();
final Version version = VersionUtils.randomVersionBetween(random(), Version.V_5_6_0, Version.V_6_3_2);
out.setVersion(version);
original.writeTo(out);
final PutRoleRequest copy = new PutRoleRequest();
final StreamInput in = out.bytes().streamInput();
in.setVersion(version);
copy.readFrom(in);
assertThat(copy.name(), equalTo(original.name()));
assertThat(copy.cluster(), equalTo(original.cluster()));
assertThat(copy.indices(), equalTo(original.indices()));
assertThat(copy.runAs(), equalTo(original.runAs()));
assertThat(copy.metadata(), equalTo(original.metadata()));
assertThat(copy.getRefreshPolicy(), equalTo(original.getRefreshPolicy()));
assertThat(copy.applicationPrivileges(), iterableWithSize(0));
assertThat(copy.conditionalClusterPrivileges(), arrayWithSize(0));
}
private void assertSuccessfulValidation(PutRoleRequest request) {
final ActionRequestValidationException exception = request.validate();
assertThat(exception, nullValue());
}
private void assertValidationError(String message, PutRoleRequest request) {
final ActionRequestValidationException exception = request.validate();
assertThat(exception, notNullValue());
assertThat(exception.validationErrors(), hasItem(containsString(message)));
}
private PutRoleRequest buildRequestWithApplicationPrivilege(String appName, String[] privileges, String[] resources) {
final PutRoleRequest request = new PutRoleRequest();
request.name("test");
final ApplicationResourcePrivileges privilege = ApplicationResourcePrivileges.builder()
.application(appName)
.privileges(privileges)
.resources(resources)
.build();
request.addApplicationPrivileges(new ApplicationResourcePrivileges[]{privilege});
return request;
}
private PutRoleRequest buildRandomRequest() {
final PutRoleRequest request = new PutRoleRequest();
request.name(randomAlphaOfLengthBetween(4, 9));
request.cluster(randomSubsetOf(Arrays.asList("monitor", "manage", "all", "manage_security", "manage_ml", "monitor_watcher"))
.toArray(Strings.EMPTY_ARRAY));
for (int i = randomIntBetween(0, 4); i > 0; i--) {
request.addIndex(
generateRandomStringArray(randomIntBetween(1, 3), randomIntBetween(3, 8), false, false),
randomSubsetOf(randomIntBetween(1, 2), "read", "write", "index", "all").toArray(Strings.EMPTY_ARRAY),
generateRandomStringArray(randomIntBetween(1, 3), randomIntBetween(3, 8), true),
generateRandomStringArray(randomIntBetween(1, 3), randomIntBetween(3, 8), true),
null
);
}
final Supplier<String> stringWithInitialLowercase = ()
-> randomAlphaOfLength(1).toLowerCase(Locale.ROOT) + randomAlphaOfLengthBetween(3, 12);
final ApplicationResourcePrivileges[] applicationPrivileges = new ApplicationResourcePrivileges[randomIntBetween(0, 5)];
for (int i = 0; i < applicationPrivileges.length; i++) {
applicationPrivileges[i] = ApplicationResourcePrivileges.builder()
.application(stringWithInitialLowercase.get())
.privileges(randomArray(1, 3, String[]::new, stringWithInitialLowercase))
.resources(generateRandomStringArray(5, randomIntBetween(3, 8), false, false))
.build();
}
request.addApplicationPrivileges(applicationPrivileges);
if (randomBoolean()) {
final String[] appNames = randomArray(1, 4, String[]::new, stringWithInitialLowercase);
request.conditionalCluster(new ConditionalClusterPrivileges.ManageApplicationPrivileges(Sets.newHashSet(appNames)));
}
request.runAs(generateRandomStringArray(4, 3, false, true));
final Map<String, Object> metadata = new HashMap<>();
for (String key : generateRandomStringArray(3, 5, false, true)) {
metadata.put(key, randomFrom(Boolean.TRUE, Boolean.FALSE, 1, 2, randomAlphaOfLengthBetween(2, 9)));
}
request.metadata(metadata);
request.setRefreshPolicy(randomFrom(WriteRequest.RefreshPolicy.values()));
return request;
}
}

View File

@ -0,0 +1,125 @@
/*
* 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.user;
import org.elasticsearch.Version;
import org.elasticsearch.action.ActionRequestValidationException;
import org.elasticsearch.common.Strings;
import org.elasticsearch.common.io.stream.BytesStreamOutput;
import org.elasticsearch.common.io.stream.StreamInput;
import org.elasticsearch.test.ESTestCase;
import org.elasticsearch.xpack.core.security.authz.RoleDescriptor.ApplicationResourcePrivileges;
import org.elasticsearch.xpack.core.security.authz.RoleDescriptor.IndicesPrivileges;
import org.elasticsearch.xpack.core.security.authz.privilege.ClusterPrivilege;
import java.io.IOException;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.hasItem;
import static org.hamcrest.Matchers.notNullValue;
import static org.hamcrest.Matchers.nullValue;
public class HasPrivilegesRequestTests extends ESTestCase {
public void testSerializationV7() throws IOException {
final HasPrivilegesRequest original = randomRequest();
final HasPrivilegesRequest copy = serializeAndDeserialize(original, Version.V_7_0_0_alpha1);
assertThat(copy.username(), equalTo(original.username()));
assertThat(copy.clusterPrivileges(), equalTo(original.clusterPrivileges()));
assertThat(copy.indexPrivileges(), equalTo(original.indexPrivileges()));
assertThat(copy.applicationPrivileges(), equalTo(original.applicationPrivileges()));
}
public void testSerializationV63() throws IOException {
final HasPrivilegesRequest original = randomRequest();
final HasPrivilegesRequest copy = serializeAndDeserialize(original, Version.V_6_3_0);
assertThat(copy.username(), equalTo(original.username()));
assertThat(copy.clusterPrivileges(), equalTo(original.clusterPrivileges()));
assertThat(copy.indexPrivileges(), equalTo(original.indexPrivileges()));
assertThat(copy.applicationPrivileges(), nullValue());
}
public void testValidateNullPrivileges() {
final HasPrivilegesRequest request = new HasPrivilegesRequest();
final ActionRequestValidationException exception = request.validate();
assertThat(exception, notNullValue());
assertThat(exception.validationErrors(), hasItem("clusterPrivileges must not be null"));
assertThat(exception.validationErrors(), hasItem("indexPrivileges must not be null"));
assertThat(exception.validationErrors(), hasItem("applicationPrivileges must not be null"));
}
public void testValidateEmptyPrivileges() {
final HasPrivilegesRequest request = new HasPrivilegesRequest();
request.clusterPrivileges(new String[0]);
request.indexPrivileges(new IndicesPrivileges[0]);
request.applicationPrivileges(new ApplicationResourcePrivileges[0]);
final ActionRequestValidationException exception = request.validate();
assertThat(exception, notNullValue());
assertThat(exception.validationErrors(), hasItem("must specify at least one privilege"));
}
public void testValidateNoWildcardApplicationPrivileges() {
final HasPrivilegesRequest request = new HasPrivilegesRequest();
request.clusterPrivileges(new String[0]);
request.indexPrivileges(new IndicesPrivileges[0]);
request.applicationPrivileges(new ApplicationResourcePrivileges[] {
ApplicationResourcePrivileges.builder().privileges("read").application("*").resources("item/1").build()
});
final ActionRequestValidationException exception = request.validate();
assertThat(exception, notNullValue());
assertThat(exception.validationErrors(), hasItem("Application names may not contain '*' (found '*')"));
}
private HasPrivilegesRequest serializeAndDeserialize(HasPrivilegesRequest original, Version version) throws IOException {
final BytesStreamOutput out = new BytesStreamOutput();
out.setVersion(version);
original.writeTo(out);
final HasPrivilegesRequest copy = new HasPrivilegesRequest();
final StreamInput in = out.bytes().streamInput();
in.setVersion(version);
copy.readFrom(in);
assertThat(in.read(), equalTo(-1));
return copy;
}
private HasPrivilegesRequest randomRequest() {
final HasPrivilegesRequest request = new HasPrivilegesRequest();
request.username(randomAlphaOfLength(8));
final List<String> clusterPrivileges = randomSubsetOf(Arrays.asList(ClusterPrivilege.MONITOR, ClusterPrivilege.MANAGE,
ClusterPrivilege.MANAGE_ML, ClusterPrivilege.MANAGE_SECURITY, ClusterPrivilege.MANAGE_PIPELINE, ClusterPrivilege.ALL))
.stream().flatMap(p -> p.name().stream()).collect(Collectors.toList());
request.clusterPrivileges(clusterPrivileges.toArray(Strings.EMPTY_ARRAY));
IndicesPrivileges[] indicesPrivileges = new IndicesPrivileges[randomInt(5)];
for (int i = 0; i < indicesPrivileges.length; i++) {
indicesPrivileges[i] = IndicesPrivileges.builder()
.privileges(randomFrom("read", "write", "create", "delete", "all"))
.indices(randomAlphaOfLengthBetween(2, 8) + (randomBoolean() ? "*" : ""))
.build();
}
request.indexPrivileges(indicesPrivileges);
final ApplicationResourcePrivileges[] appPrivileges = new ApplicationResourcePrivileges[randomInt(5)];
for (int i = 0; i < appPrivileges.length; i++) {
appPrivileges[i] = ApplicationResourcePrivileges.builder()
.application(randomAlphaOfLengthBetween(3, 8))
.resources(randomAlphaOfLengthBetween(5, 7) + (randomBoolean() ? "*" : ""))
.privileges(generateRandomStringArray(6, 7, false, false))
.build();
}
request.applicationPrivileges(appPrivileges);
return request;
}
}

View File

@ -0,0 +1,114 @@
/*
* 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.authc;
import org.elasticsearch.ElasticsearchSecurityException;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.common.util.concurrent.ThreadContext;
import org.elasticsearch.rest.RestRequest;
import org.elasticsearch.rest.RestStatus;
import org.elasticsearch.test.ESTestCase;
import org.elasticsearch.xpack.core.XPackField;
import org.mockito.Mockito;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import static org.hamcrest.Matchers.contains;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.notNullValue;
import static org.hamcrest.Matchers.sameInstance;
public class DefaultAuthenticationFailureHandlerTests extends ESTestCase {
public void testAuthenticationRequired() {
final boolean testDefault = randomBoolean();
final String basicAuthScheme = "Basic realm=\"" + XPackField.SECURITY + "\" charset=\"UTF-8\"";
final String bearerAuthScheme = "Bearer realm=\"" + XPackField.SECURITY + "\"";
final DefaultAuthenticationFailureHandler failuerHandler;
if (testDefault) {
failuerHandler = new DefaultAuthenticationFailureHandler();
} else {
final Map<String, List<String>> failureResponeHeaders = new HashMap<>();
failureResponeHeaders.put("WWW-Authenticate", Arrays.asList(basicAuthScheme, bearerAuthScheme));
failuerHandler = new DefaultAuthenticationFailureHandler(failureResponeHeaders);
}
assertThat(failuerHandler, is(notNullValue()));
final ElasticsearchSecurityException ese =
failuerHandler.authenticationRequired("someaction", new ThreadContext(Settings.builder().build()));
assertThat(ese, is(notNullValue()));
assertThat(ese.getMessage(), equalTo("action [someaction] requires authentication"));
assertThat(ese.getHeader("WWW-Authenticate"), is(notNullValue()));
if (testDefault) {
assertWWWAuthenticateWithSchemes(ese, basicAuthScheme);
} else {
assertWWWAuthenticateWithSchemes(ese, basicAuthScheme, bearerAuthScheme);
}
}
public void testExceptionProcessingRequest() {
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 Map<String, List<String>> failureResponeHeaders = new HashMap<>();
failureResponeHeaders.put("WWW-Authenticate", Arrays.asList(basicAuthScheme, bearerAuthScheme, negotiateAuthScheme));
final DefaultAuthenticationFailureHandler failuerHandler = new DefaultAuthenticationFailureHandler(failureResponeHeaders);
assertThat(failuerHandler, is(notNullValue()));
final boolean causeIsElasticsearchSecurityException = randomBoolean();
final boolean causeIsEseAndUnauthorized = causeIsElasticsearchSecurityException && randomBoolean();
final ElasticsearchSecurityException eseCause = (causeIsEseAndUnauthorized)
? new ElasticsearchSecurityException("unauthorized", RestStatus.UNAUTHORIZED, null, (Object[]) null)
: new ElasticsearchSecurityException("different error", RestStatus.BAD_REQUEST, null, (Object[]) null);
final Exception cause = causeIsElasticsearchSecurityException ? eseCause : new Exception("other error");
final boolean withAuthenticateHeader = randomBoolean();
final String selectedScheme = randomFrom(bearerAuthScheme, basicAuthScheme, negotiateAuthScheme);
if (withAuthenticateHeader) {
eseCause.addHeader("WWW-Authenticate", Collections.singletonList(selectedScheme));
}
if (causeIsElasticsearchSecurityException) {
if (causeIsEseAndUnauthorized) {
final ElasticsearchSecurityException ese = failuerHandler.exceptionProcessingRequest(Mockito.mock(RestRequest.class), cause,
new ThreadContext(Settings.builder().build()));
assertThat(ese, is(notNullValue()));
assertThat(ese.getHeader("WWW-Authenticate"), is(notNullValue()));
assertThat(ese, is(sameInstance(cause)));
if (withAuthenticateHeader == false) {
assertWWWAuthenticateWithSchemes(ese, basicAuthScheme, bearerAuthScheme, negotiateAuthScheme);
} else {
if (selectedScheme.contains("Negotiate ")) {
assertWWWAuthenticateWithSchemes(ese, selectedScheme);
} else {
assertWWWAuthenticateWithSchemes(ese, basicAuthScheme, bearerAuthScheme, negotiateAuthScheme);
}
}
assertThat(ese.getMessage(), equalTo("unauthorized"));
} else {
expectThrows(AssertionError.class, () -> failuerHandler.exceptionProcessingRequest(Mockito.mock(RestRequest.class), cause,
new ThreadContext(Settings.builder().build())));
}
} else {
final ElasticsearchSecurityException ese = failuerHandler.exceptionProcessingRequest(Mockito.mock(RestRequest.class), cause,
new ThreadContext(Settings.builder().build()));
assertThat(ese, is(notNullValue()));
assertThat(ese.getHeader("WWW-Authenticate"), is(notNullValue()));
assertThat(ese.getMessage(), equalTo("error attempting to authenticate request"));
assertWWWAuthenticateWithSchemes(ese, basicAuthScheme, bearerAuthScheme, negotiateAuthScheme);
}
}
private void assertWWWAuthenticateWithSchemes(final ElasticsearchSecurityException ese, final String... schemes) {
assertThat(ese.getHeader("WWW-Authenticate").size(), is(schemes.length));
assertThat(ese.getHeader("WWW-Authenticate"), contains(schemes));
}
}

Some files were not shown because too many files have changed in this diff Show More