Merge branch 'master' into index-lifecycle
This commit is contained in:
commit
e523030670
|
@ -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
|
||||
|
|
25
build.gradle
25
build.gradle
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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') {
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
|
||||
}
|
|
@ -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));
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
|
|
|
@ -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];
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
|
||||
|
|
|
@ -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
|
|
@ -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 don’t 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 don’t 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.
|
|
@ -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[]
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 + '}';
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
|
|
|
@ -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
|
||||
*/
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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"/]
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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'
|
||||
}
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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 {
|
||||
|
||||
|
|
|
@ -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> {
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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 + "]");
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)));
|
||||
}
|
||||
}
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -39,6 +39,7 @@ public class HasPrivilegesRequestBuilder
|
|||
request.username(username);
|
||||
request.indexPrivileges(role.getIndicesPrivileges());
|
||||
request.clusterPrivileges(role.getClusterPrivileges());
|
||||
request.applicationPrivileges(role.getApplicationPrivileges());
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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");
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 & 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);
|
||||
}
|
||||
|
||||
}
|
|
@ -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");
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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",
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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)));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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()));
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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()));
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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)));
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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
Loading…
Reference in New Issue