Merge branch 'master' into feature/http_client

Original commit: elastic/x-pack-elasticsearch@f8d810f0e8
This commit is contained in:
javanna 2016-06-15 11:49:34 +02:00 committed by Luca Cavanna
commit 82a19cda0e
26 changed files with 647 additions and 38 deletions

View File

@ -37,6 +37,7 @@ processResources {
task buildZip(type:Zip, dependsOn: [jar]) {
from 'build/resources/main/x-pack-extension-descriptor.properties'
from 'build/resources/main/x-pack-extension-security.policy'
from project.jar
}

View File

@ -11,6 +11,9 @@ import org.elasticsearch.example.realm.CustomRealmFactory;
import org.elasticsearch.shield.authc.AuthenticationModule;
import org.elasticsearch.xpack.extensions.XPackExtension;
import java.security.AccessController;
import java.security.PrivilegedAction;
public class ExampleRealmExtension extends XPackExtension {
@Override
public String name() {
@ -25,5 +28,10 @@ public class ExampleRealmExtension extends XPackExtension {
public void onModule(AuthenticationModule authenticationModule) {
authenticationModule.addCustomRealm(CustomRealm.TYPE, CustomRealmFactory.class);
authenticationModule.setAuthenticationFailureHandler(CustomAuthenticationFailureHandler.class);
// check that the extension's policy works.
AccessController.doPrivileged((PrivilegedAction<Void>) () -> {
System.getSecurityManager().checkPrintJobAccess();
return null;
});
}
}

View File

@ -0,0 +1,3 @@
grant {
permission java.lang.RuntimePermission "queuePrintJob";
};

View File

@ -149,9 +149,7 @@ public class GraphExploreRequest extends ActionRequest<GraphExploreRequest> impl
indicesOptions = IndicesOptions.readIndicesOptions(in);
types = in.readStringArray();
routing = in.readOptionalString();
if (in.readBoolean()) {
timeout = TimeValue.readTimeValue(in);
}
timeout = in.readOptionalWriteable(TimeValue::new);
sampleSize = in.readInt();
sampleDiversityField = in.readOptionalString();
maxDocsPerDiversityValue = in.readInt();
@ -177,7 +175,7 @@ public class GraphExploreRequest extends ActionRequest<GraphExploreRequest> impl
indicesOptions.writeIndicesOptions(out);
out.writeStringArray(types);
out.writeOptionalString(routing);
out.writeOptionalStreamable(timeout);
out.writeOptionalWriteable(timeout);
out.writeInt(sampleSize);
out.writeOptionalString(sampleDiversityField);

View File

@ -6,6 +6,7 @@
package org.elasticsearch.shield.authc;
import org.elasticsearch.ElasticsearchException;
import org.elasticsearch.common.collect.Iterators;
import org.elasticsearch.common.component.AbstractLifecycleComponent;
import org.elasticsearch.common.inject.Inject;
import org.elasticsearch.common.settings.Setting;
@ -101,6 +102,10 @@ public class Realms extends AbstractLifecycleComponent<Realms> implements Iterab
@Override
public Iterator<Realm> iterator() {
if (shieldLicenseState.authenticationAndAuthorizationEnabled() == false) {
return Collections.emptyIterator();
}
EnabledRealmType enabledRealmType = shieldLicenseState.enabledRealmType();
switch (enabledRealmType) {
case ALL:

View File

@ -35,6 +35,7 @@ import org.elasticsearch.index.cache.bitset.BitsetFilterCache;
import org.elasticsearch.index.engine.EngineException;
import org.elasticsearch.index.mapper.DocumentMapper;
import org.elasticsearch.index.mapper.MapperService;
import org.elasticsearch.index.mapper.internal.FieldNamesFieldMapper;
import org.elasticsearch.index.mapper.internal.ParentFieldMapper;
import org.elasticsearch.index.query.ParsedQuery;
import org.elasticsearch.index.query.QueryBuilder;
@ -90,6 +91,7 @@ public class ShieldIndexSearcherWrapper extends IndexSearcherWrapper {
Set<String> allowedMetaFields = new HashSet<>();
allowedMetaFields.addAll(Arrays.asList(MapperService.getAllMetaFields()));
allowedMetaFields.add(FieldNamesFieldMapper.NAME); // TODO: add _field_names to MapperService#META_FIELDS?
allowedMetaFields.add("_source"); // TODO: add _source to MapperService#META_FIELDS?
allowedMetaFields.add("_version"); // TODO: add _version to MapperService#META_FIELDS?
allowedMetaFields.remove("_all"); // The _all field contains actual data and we can't include that by default.

View File

@ -34,6 +34,7 @@ import java.util.Collections;
import static org.elasticsearch.action.support.WriteRequest.RefreshPolicy.IMMEDIATE;
import static org.elasticsearch.index.query.QueryBuilders.constantScoreQuery;
import static org.elasticsearch.index.query.QueryBuilders.existsQuery;
import static org.elasticsearch.index.query.QueryBuilders.hasChildQuery;
import static org.elasticsearch.index.query.QueryBuilders.matchQuery;
import static org.elasticsearch.index.query.QueryBuilders.termQuery;
@ -482,8 +483,6 @@ public class FieldLevelSecurityTests extends ShieldIntegTestCase {
assertThat(response.getResponses()[0].getResponse().getSource().get("field2").toString(), equalTo("value2"));
}
// norelease - we need to fix the issue so that only fields a user can see are returned
@AwaitsFix(bugUrl = "https://github.com/elastic/x-plugins/issues/2120")
public void testFieldStatsApi() throws Exception {
assertAcked(client().admin().indices().prepareCreate("test")
.addMapping("type1", "field1", "type=text", "field2", "type=text", "field3", "type=text")
@ -883,28 +882,28 @@ public class FieldLevelSecurityTests extends ShieldIntegTestCase {
.prepareSearch("test")
.addSort("field1", SortOrder.ASC)
.get();
assertThat((Long) response.getHits().getAt(0).sortValues()[0], equalTo(1L));
assertThat(response.getHits().getAt(0).sortValues()[0], equalTo(1L));
// user2 is not granted to use field1, so the default missing sort value is included
response = client().filterWithHeader(Collections.singletonMap(BASIC_AUTH_HEADER, basicAuthHeaderValue("user2", USERS_PASSWD)))
.prepareSearch("test")
.addSort("field1", SortOrder.ASC)
.get();
assertThat((Long) response.getHits().getAt(0).sortValues()[0], equalTo(Long.MAX_VALUE));
assertThat(response.getHits().getAt(0).sortValues()[0], equalTo(Long.MAX_VALUE));
// user1 is not granted to use field2, so the default missing sort value is included
response = client().filterWithHeader(Collections.singletonMap(BASIC_AUTH_HEADER, basicAuthHeaderValue("user1", USERS_PASSWD)))
.prepareSearch("test")
.addSort("field2", SortOrder.ASC)
.get();
assertThat((Long) response.getHits().getAt(0).sortValues()[0], equalTo(Long.MAX_VALUE));
assertThat(response.getHits().getAt(0).sortValues()[0], equalTo(Long.MAX_VALUE));
// user2 is granted to use field2, so it is included in the sort_values
response = client().filterWithHeader(Collections.singletonMap(BASIC_AUTH_HEADER, basicAuthHeaderValue("user2", USERS_PASSWD)))
.prepareSearch("test")
.addSort("field2", SortOrder.ASC)
.get();
assertThat((Long) response.getHits().getAt(0).sortValues()[0], equalTo(2L));
assertThat(response.getHits().getAt(0).sortValues()[0], equalTo(2L));
}
public void testAggs() throws Exception {
@ -1225,4 +1224,64 @@ public class FieldLevelSecurityTests extends ShieldIntegTestCase {
assertThat(response.getHits().getAt(0).sourceAsMap().get("field2").toString(), equalTo("value2"));
}
public void testExistQuery() {
assertAcked(client().admin().indices().prepareCreate("test")
.addMapping("type1", "field1", "type=text", "field2", "type=text", "field3", "type=text")
);
client().prepareIndex("test", "type1", "1").setSource("field1", "value1", "field2", "value2", "field3", "value3")
.setRefreshPolicy(IMMEDIATE)
.get();
// user1 has access to field1, so the query should match with the document:
SearchResponse response = client()
.filterWithHeader(Collections.singletonMap(BASIC_AUTH_HEADER, basicAuthHeaderValue("user1", USERS_PASSWD)))
.prepareSearch("test")
.setQuery(existsQuery("field1"))
.get();
assertHitCount(response, 1);
// user1 has no access to field2, so the query should not match with the document:
response = client()
.filterWithHeader(Collections.singletonMap(BASIC_AUTH_HEADER, basicAuthHeaderValue("user1", USERS_PASSWD)))
.prepareSearch("test")
.setQuery(existsQuery("field2"))
.get();
assertHitCount(response, 0);
// user2 has no access to field1, so the query should not match with the document:
response = client().filterWithHeader(Collections.singletonMap(BASIC_AUTH_HEADER, basicAuthHeaderValue("user2", USERS_PASSWD)))
.prepareSearch("test")
.setQuery(existsQuery("field1"))
.get();
assertHitCount(response, 0);
// user2 has access to field2, so the query should match with the document:
response = client().filterWithHeader(Collections.singletonMap(BASIC_AUTH_HEADER, basicAuthHeaderValue("user2", USERS_PASSWD)))
.prepareSearch("test")
.setQuery(existsQuery("field2"))
.get();
assertHitCount(response, 1);
// user3 has access to field1 and field2, so the query should match with the document:
response = client().filterWithHeader(Collections.singletonMap(BASIC_AUTH_HEADER, basicAuthHeaderValue("user3", USERS_PASSWD)))
.prepareSearch("test")
.setQuery(existsQuery("field1"))
.get();
assertHitCount(response, 1);
// user3 has access to field1 and field2, so the query should match with the document:
response = client().filterWithHeader(Collections.singletonMap(BASIC_AUTH_HEADER, basicAuthHeaderValue("user3", USERS_PASSWD)))
.prepareSearch("test")
.setQuery(existsQuery("field2"))
.get();
assertHitCount(response, 1);
// user4 has access to no fields, so the query should not match with the document:
response = client().filterWithHeader(Collections.singletonMap(BASIC_AUTH_HEADER, basicAuthHeaderValue("user4", USERS_PASSWD)))
.prepareSearch("test")
.setQuery(existsQuery("field1"))
.get();
assertHitCount(response, 0);
// user4 has access to no fields, so the query should not match with the document:
response = client().filterWithHeader(Collections.singletonMap(BASIC_AUTH_HEADER, basicAuthHeaderValue("user4", USERS_PASSWD)))
.prepareSearch("test")
.setQuery(existsQuery("field2"))
.get();
assertHitCount(response, 0);
}
}

View File

@ -15,6 +15,7 @@ import org.elasticsearch.xpack.watcher.support.xcontent.XContentSource;
import org.junit.Before;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@ -92,7 +93,7 @@ public class SecurityFeatureSetTests extends ESTestCase {
realmUsage.put("key3", i % 2 == 0);
when(realm.usageStats()).thenReturn(realmUsage);
}
when(realms.iterator()).thenReturn(realmsList.iterator());
when(realms.iterator()).thenReturn(available ? realmsList.iterator() : Collections.<Realm>emptyIterator());
SecurityFeatureSet featureSet = new SecurityFeatureSet(settings.build(), licenseState, realms, namedWriteableRegistry);
XPackFeatureSet.Usage usage = featureSet.usage();
@ -102,12 +103,14 @@ public class SecurityFeatureSetTests extends ESTestCase {
assertThat(usage.available(), is(available));
XContentSource source = new XContentSource(usage);
if (enabled) {
if (enabled && available) {
for (int i = 0; i < 5; i++) {
assertThat(source.getValue("enabled_realms." + i + ".key1"), is("value" + i));
assertThat(source.getValue("enabled_realms." + i + ".key2"), is(i));
assertThat(source.getValue("enabled_realms." + i + ".key3"), is(i % 2 == 0));
}
} else if (enabled) {
assertThat(source.getValue("enabled_realms"), is(notNullValue()));
} else {
assertThat(source.getValue("enabled_realms"), is(nullValue()));
}

View File

@ -37,6 +37,6 @@ public class VersionCompatibilityTests extends ESTestCase {
*
*/
assertThat("Remove workaround in LicenseService class when es core supports merging cluster level custom metadata",
Version.CURRENT.equals(Version.V_5_0_0), is(true));
Version.CURRENT.equals(Version.V_5_0_0_alpha4), is(true));
}
}

View File

@ -94,6 +94,7 @@ public class InternalAuthenticationServiceTests extends ESTestCase {
Settings settings = Settings.builder().put("path.home", createTempDir()).build();
SecurityLicenseState shieldLicenseState = mock(SecurityLicenseState.class);
when(shieldLicenseState.enabledRealmType()).thenReturn(EnabledRealmType.ALL);
when(shieldLicenseState.authenticationAndAuthorizationEnabled()).thenReturn(true);
realms = new Realms(Settings.EMPTY, new Environment(settings), Collections.<String, Realm.Factory>emptyMap(), shieldLicenseState,
mock(ReservedRealm.class)) {

View File

@ -52,6 +52,7 @@ public class RealmsTests extends ESTestCase {
}
shieldLicenseState = mock(SecurityLicenseState.class);
reservedRealm = mock(ReservedRealm.class);
when(shieldLicenseState.authenticationAndAuthorizationEnabled()).thenReturn(true);
when(shieldLicenseState.enabledRealmType()).thenReturn(EnabledRealmType.ALL);
}
@ -338,6 +339,21 @@ public class RealmsTests extends ESTestCase {
assertThat(count, equalTo(orderToIndex.size()));
}
public void testAuthcAuthzDisabled() {
Settings settings = Settings.builder()
.put("path.home", createTempDir())
.put("xpack.security.authc.realms.realm_1.type", FileRealm.TYPE)
.put("xpack.security.authc.realms.realm_1.order", 0)
.build();
Environment env = new Environment(settings);
Realms realms = new Realms(settings, env, factories, shieldLicenseState, reservedRealm).start();
assertThat(realms.iterator().hasNext(), is(true));
when(shieldLicenseState.authenticationAndAuthorizationEnabled()).thenReturn(false);
assertThat(realms.iterator().hasNext(), is(false));
}
static class DummyRealm extends Realm {
public DummyRealm(String type, RealmConfig config) {

View File

@ -133,7 +133,7 @@ public class ShieldIndexSearcherWrapperUnitTests extends ESTestCase {
FieldSubsetReader.FieldSubsetDirectoryReader result =
(FieldSubsetReader.FieldSubsetDirectoryReader) shieldIndexSearcherWrapper.wrap(esIn);
assertThat(result.getFieldNames().size(), equalTo(11));
assertThat(result.getFieldNames().size(), equalTo(12));
assertThat(result.getFieldNames().contains("_uid"), is(true));
assertThat(result.getFieldNames().contains("_id"), is(true));
assertThat(result.getFieldNames().contains("_version"), is(true));
@ -145,6 +145,7 @@ public class ShieldIndexSearcherWrapperUnitTests extends ESTestCase {
assertThat(result.getFieldNames().contains("_ttl"), is(true));
assertThat(result.getFieldNames().contains("_size"), is(true));
assertThat(result.getFieldNames().contains("_index"), is(true));
assertThat(result.getFieldNames().contains("_field_names"), is(true));
// _all contains actual user data and therefor can't be included by default
assertThat(result.getFieldNames().contains("_all"), is(false));
}
@ -468,6 +469,16 @@ public class ShieldIndexSearcherWrapperUnitTests extends ESTestCase {
public Weight createWeight(IndexSearcher searcher, boolean needsScores) throws IOException {
return new CreateScorerOnceWeight(query.createWeight(searcher, needsScores));
}
@Override
public boolean equals(Object obj) {
return sameClassAs(obj) && query.equals(((CreateScorerOnceQuery) obj).query);
}
@Override
public int hashCode() {
return 31 * classHash() + query.hashCode();
}
}
public void doTestIndexSearcherWrapper(boolean sparse, boolean deletions) throws IOException {

View File

@ -20,6 +20,7 @@ cluster:monitor/nodes/stats
cluster:monitor/state
cluster:monitor/stats
cluster:monitor/task
cluster:monitor/task/get
cluster:monitor/tasks/lists
cluster:monitor/xpack/watcher/stats
indices:admin/aliases

View File

@ -8,6 +8,7 @@ package org.elasticsearch.xpack.extensions;
import joptsimple.OptionSet;
import joptsimple.OptionSpec;
import org.apache.lucene.util.IOUtils;
import org.elasticsearch.SpecialPermission;
import org.elasticsearch.bootstrap.JarHell;
import org.elasticsearch.cli.ExitCodes;
import org.elasticsearch.cli.SettingCommand;
@ -26,13 +27,26 @@ import java.net.URLDecoder;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.List;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.Collections;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
import java.security.Policy;
import java.security.PermissionCollection;
import java.security.Permission;
import java.security.NoSuchAlgorithmException;
import java.security.Permissions;
import java.security.PrivilegedAction;
import java.security.AccessController;
import java.security.UnresolvedPermission;
import java.security.URIParameter;
import static org.elasticsearch.cli.Terminal.Verbosity.VERBOSE;
import static org.elasticsearch.xpack.XPackPlugin.resolveXPackExtensionsFile;
@ -49,18 +63,19 @@ import static org.elasticsearch.xpack.XPackPlugin.resolveXPackExtensionsFile;
* <ul>
* <li>The property file exists and contains valid metadata. See {@link XPackExtensionInfo#readFromProperties(Path)}</li>
* <li>Jar hell does not exist, either between the extension's own jars or with the parent classloader (elasticsearch + x-pack)</li>
* <li>If the extension contains extra security permissions, the policy file is validated</li>
* </ul>
*/
class InstallXPackExtensionCommand extends SettingCommand {
final class InstallXPackExtensionCommand extends SettingCommand {
private final OptionSpec<Void> batchOption;
private final OptionSpec<String> arguments;
InstallXPackExtensionCommand() {
super("Install a plugin");
super("Install an extension");
this.batchOption = parser.acceptsAll(Arrays.asList("b", "batch"),
"Enable batch mode explicitly, automatic confirmation of security permission");
this.arguments = parser.nonOptions("plugin id");
this.arguments = parser.nonOptions("extension id");
}
@Override
@ -86,7 +101,7 @@ class InstallXPackExtensionCommand extends SettingCommand {
Path extensionZip = download(terminal, extensionId, env.tmpFile());
Path extractedZip = unzip(extensionZip, resolveXPackExtensionsFile(env));
install(terminal, extractedZip, env);
install(terminal, extractedZip, env, isBatch);
}
/** Downloads the extension and returns the file it was downloaded to. */
@ -133,13 +148,21 @@ class InstallXPackExtensionCommand extends SettingCommand {
}
/** Load information about the extension, and verify it can be installed with no errors. */
private XPackExtensionInfo verify(Terminal terminal, Path extensionRoot, Environment env) throws Exception {
private XPackExtensionInfo verify(Terminal terminal, Path extensionRoot, Environment env, boolean isBatch) throws Exception {
// read and validate the extension descriptor
XPackExtensionInfo info = XPackExtensionInfo.readFromProperties(extensionRoot);
terminal.println(VERBOSE, info.toString());
// check for jar hell before any copying
jarHellCheck(extensionRoot);
// read optional security policy (extra permissions)
// if it exists, confirm or warn the user
Path policy = extensionRoot.resolve(XPackExtensionInfo.XPACK_EXTENSION_POLICY);
if (Files.exists(policy)) {
readPolicy(policy, terminal, env, isBatch);
}
return info;
}
@ -165,11 +188,11 @@ class InstallXPackExtensionCommand extends SettingCommand {
/**
* Installs the extension from {@code tmpRoot} into the extensions dir.
*/
private void install(Terminal terminal, Path tmpRoot, Environment env) throws Exception {
private void install(Terminal terminal, Path tmpRoot, Environment env, boolean isBatch) throws Exception {
List<Path> deleteOnFailure = new ArrayList<>();
deleteOnFailure.add(tmpRoot);
try {
XPackExtensionInfo info = verify(terminal, tmpRoot, env);
XPackExtensionInfo info = verify(terminal, tmpRoot, env, isBatch);
final Path destination = resolveXPackExtensionsFile(env).resolve(info.getName());
if (Files.exists(destination)) {
throw new UserError(ExitCodes.USAGE,
@ -188,4 +211,150 @@ class InstallXPackExtensionCommand extends SettingCommand {
throw installProblem;
}
}
/** Format permission type, name, and actions into a string */
static String formatPermission(Permission permission) {
StringBuilder sb = new StringBuilder();
String clazz = null;
if (permission instanceof UnresolvedPermission) {
clazz = ((UnresolvedPermission) permission).getUnresolvedType();
} else {
clazz = permission.getClass().getName();
}
sb.append(clazz);
String name = null;
if (permission instanceof UnresolvedPermission) {
name = ((UnresolvedPermission) permission).getUnresolvedName();
} else {
name = permission.getName();
}
if (name != null && name.length() > 0) {
sb.append(' ');
sb.append(name);
}
String actions = null;
if (permission instanceof UnresolvedPermission) {
actions = ((UnresolvedPermission) permission).getUnresolvedActions();
} else {
actions = permission.getActions();
}
if (actions != null && actions.length() > 0) {
sb.append(' ');
sb.append(actions);
}
return sb.toString();
}
/**
* Parses extension policy into a set of permissions
*/
static PermissionCollection parsePermissions(Path file, Path tmpDir) throws IOException {
// create a zero byte file for "comparison"
// this is necessary because the default policy impl automatically grants two permissions:
// 1. permission to exitVM (which we ignore)
// 2. read permission to the code itself (e.g. jar file of the code)
Path emptyPolicyFile = Files.createTempFile(tmpDir, "empty", "tmp");
final Policy emptyPolicy;
SecurityManager sm = System.getSecurityManager();
if (sm != null) {
sm.checkPermission(new SpecialPermission());
}
emptyPolicy =
AccessController.doPrivileged((PrivilegedAction<Policy>) () -> {
try {
return Policy.getInstance("JavaPolicy", new URIParameter(emptyPolicyFile.toUri()));
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException(e);
}
});
IOUtils.rm(emptyPolicyFile);
// parse the extension's policy file into a set of permissions
final Policy policy =
AccessController.doPrivileged((PrivilegedAction<Policy>) () -> {
try {
return Policy.getInstance("JavaPolicy", new URIParameter(file.toUri()));
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException(e);
}
});
PermissionCollection permissions = policy.getPermissions(XPackExtensionSecurity.class.getProtectionDomain());
// this method is supported with the specific implementation we use, but just check for safety.
if (permissions == Policy.UNSUPPORTED_EMPTY_COLLECTION) {
throw new UnsupportedOperationException("JavaPolicy implementation does not support retrieving permissions");
}
PermissionCollection actualPermissions = new Permissions();
for (Permission permission : Collections.list(permissions.elements())) {
if (!emptyPolicy.implies(XPackExtensionSecurity.class.getProtectionDomain(), permission)) {
actualPermissions.add(permission);
}
}
actualPermissions.setReadOnly();
return actualPermissions;
}
/**
* Reads extension policy, prints/confirms exceptions
*/
static void readPolicy(Path file, Terminal terminal, Environment environment, boolean batch) throws IOException {
PermissionCollection permissions = parsePermissions(file, environment.tmpFile());
List<Permission> requested = Collections.list(permissions.elements());
if (requested.isEmpty()) {
terminal.println(Terminal.Verbosity.VERBOSE, "extension has a policy file with no additional permissions");
return;
}
// sort permissions in a reasonable order
Collections.sort(requested, new Comparator<Permission>() {
@Override
public int compare(Permission o1, Permission o2) {
int cmp = o1.getClass().getName().compareTo(o2.getClass().getName());
if (cmp == 0) {
String name1 = o1.getName();
String name2 = o2.getName();
if (name1 == null) {
name1 = "";
}
if (name2 == null) {
name2 = "";
}
cmp = name1.compareTo(name2);
if (cmp == 0) {
String actions1 = o1.getActions();
String actions2 = o2.getActions();
if (actions1 == null) {
actions1 = "";
}
if (actions2 == null) {
actions2 = "";
}
cmp = actions1.compareTo(actions2);
}
}
return cmp;
}
});
terminal.println(Terminal.Verbosity.NORMAL, "@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@");
terminal.println(Terminal.Verbosity.NORMAL, "@ WARNING: x-pack extension requires additional permissions @");
terminal.println(Terminal.Verbosity.NORMAL, "@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@");
// print all permissions:
for (Permission permission : requested) {
terminal.println(Terminal.Verbosity.NORMAL, "* " + formatPermission(permission));
}
terminal.println(Terminal.Verbosity.NORMAL, "See http://docs.oracle.com/javase/8/docs/technotes/guides/security/permissions.html");
terminal.println(Terminal.Verbosity.NORMAL, "for descriptions of what these permissions allow and the associated risks.");
if (!batch) {
terminal.println(Terminal.Verbosity.NORMAL, "");
String text = terminal.readText("Continue with installation? [y/N]");
if (!text.equalsIgnoreCase("y")) {
throw new RuntimeException("installation aborted by user");
}
}
}
}

View File

@ -16,6 +16,7 @@ import java.util.Properties;
public class XPackExtensionInfo {
public static final String XPACK_EXTENSION_PROPERTIES = "x-pack-extension-descriptor.properties";
public static final String XPACK_EXTENSION_POLICY = "x-pack-extension-security.policy";
private String name;
private String description;

View File

@ -0,0 +1,67 @@
/*
* 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.extensions;
import org.elasticsearch.common.SuppressForbidden;
import java.net.URL;
import java.security.Policy;
import java.security.ProtectionDomain;
import java.security.CodeSource;
import java.security.Permission;
import java.security.SecurityPermission;
import java.util.Map;
final class XPackExtensionPolicy extends Policy {
static final Permission SET_POLICY_PERMISSION = new SecurityPermission("setPolicy");
static final Permission GET_POLICY_PERMISSION = new SecurityPermission("getPolicy");
static final Permission CREATE_POLICY_PERMISSION = new SecurityPermission("createPolicy.JavaPolicy");
// the base policy (es + plugins)
final Policy basePolicy;
// policy extensions
final Map<String, Policy> extensions;
// xpack code source location
final URL xpackURL;
/**
*
* @param basePolicy The base policy
* @param extensions Extra code source extension's policy
*/
public XPackExtensionPolicy(Policy basePolicy, Map<String, Policy> extensions) {
this.basePolicy = basePolicy;
this.extensions = extensions;
xpackURL = XPackExtensionPolicy.class.getProtectionDomain().getCodeSource().getLocation();
}
private boolean isPolicyPermission(Permission permission) {
return GET_POLICY_PERMISSION.equals(permission) ||
CREATE_POLICY_PERMISSION.equals(permission) ||
SET_POLICY_PERMISSION.equals(permission);
}
@Override @SuppressForbidden(reason = "fast equals check is desired")
public boolean implies(ProtectionDomain domain, Permission permission) {
CodeSource codeSource = domain.getCodeSource();
if (codeSource != null && codeSource.getLocation() != null) {
if (codeSource.getLocation().equals(xpackURL) &&
isPolicyPermission(permission)) {
// forbid to get, create and set java policy in xpack codesource
// it is only granted at startup in order to let xpack add the extensions policy
// and make this policy the default.
return false;
}
// check for an additional extension permission: extension policy is
// only consulted for its codesources.
Policy extension = extensions.get(codeSource.getLocation().getFile());
if (extension != null && extension.implies(domain, permission)) {
return true;
}
}
return basePolicy.implies(domain, permission);
}
}

View File

@ -0,0 +1,144 @@
/*
* 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.extensions;
import org.elasticsearch.SpecialPermission;
import org.elasticsearch.common.SuppressForbidden;
import org.elasticsearch.common.io.PathUtils;
import java.io.IOException;
import java.net.URISyntaxException;
import java.net.URL;
import java.nio.file.DirectoryStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Map;
import java.util.HashMap;
import java.util.List;
import java.util.ArrayList;
import java.util.Collections;
import java.security.Policy;
import java.security.PrivilegedAction;
import java.security.AccessController;
import java.security.URIParameter;
import java.security.NoSuchAlgorithmException;
final class XPackExtensionSecurity {
private XPackExtensionSecurity() {}
/**
* Initializes the XPackExtensionPolicy
* Can only happen once!
*
* @param extsDirectory the directory where the extensions are installed
*/
static void configure(Path extsDirectory) throws IOException {
Map<String, Policy> map = getExtensionsPermissions(extsDirectory);
if (map.size() > 0) {
SecurityManager sm = System.getSecurityManager();
if (sm != null) {
sm.checkPermission(new SpecialPermission());
}
AccessController.doPrivileged((PrivilegedAction<Void>) () -> {
Policy newPolicy = new XPackExtensionPolicy(Policy.getPolicy(), map);
Policy.setPolicy(newPolicy);
return null;
});
}
}
/**
* Sets properties (codebase URLs) for policy files.
* we look for matching extensions and set URLs to fit
*/
@SuppressForbidden(reason = "proper use of URL")
static Map<String, Policy> getExtensionsPermissions(Path extsDirectory) throws IOException {
Map<String, Policy> map = new HashMap<>();
// collect up lists of extensions
List<Path> extensionPaths = new ArrayList<>();
if (Files.exists(extsDirectory)) {
try (DirectoryStream<Path> stream = Files.newDirectoryStream(extsDirectory)) {
for (Path extension : stream) {
extensionPaths.add(extension);
}
}
}
// now process each one
for (Path extension : extensionPaths) {
Path policyFile = extension.resolve(XPackExtensionInfo.XPACK_EXTENSION_POLICY);
if (Files.exists(policyFile)) {
// first get a list of URLs for the extension's jars:
// we resolve symlinks so map is keyed on the normalize codebase name
List<URL> codebases = new ArrayList<>();
try (DirectoryStream<Path> jarStream = Files.newDirectoryStream(extension, "*.jar")) {
for (Path jar : jarStream) {
codebases.add(jar.toRealPath().toUri().toURL());
}
}
// parse the extension's policy file into a set of permissions
Policy policy = readPolicy(policyFile.toUri().toURL(), codebases.toArray(new URL[codebases.size()]));
// consult this policy for each of the extension's jars:
for (URL url : codebases) {
if (map.put(url.getFile(), policy) != null) {
// just be paranoid ok?
throw new IllegalStateException("per-extension permissions already granted for jar file: " + url);
}
}
}
}
return Collections.unmodifiableMap(map);
}
/**
* Reads and returns the specified {@code policyFile}.
* <p>
* Resources (e.g. jar files and directories) listed in {@code codebases} location
* will be provided to the policy file via a system property of the short name:
* e.g. <code>${codebase.joda-convert-1.2.jar}</code> would map to full URL.
*/
@SuppressForbidden(reason = "accesses fully qualified URLs to configure security")
static Policy readPolicy(URL policyFile, URL codebases[]) throws IOException {
SecurityManager sm = System.getSecurityManager();
if (sm != null) {
sm.checkPermission(new SpecialPermission());
}
try {
try {
// set codebase properties
for (URL url : codebases) {
String shortName = PathUtils.get(url.toURI()).getFileName().toString();
AccessController.doPrivileged((PrivilegedAction<Void>) () -> {
System.setProperty("codebase." + shortName, url.toString());
return null;
});
}
URIParameter uri = new URIParameter(policyFile.toURI());
return AccessController.doPrivileged((PrivilegedAction<Policy>) () -> {
try {
return Policy.getInstance("JavaPolicy", uri);
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException(e);
}
});
} finally {
// clear codebase properties
for (URL url : codebases) {
String shortName = PathUtils.get(url.toURI()).getFileName().toString();
AccessController.doPrivileged((PrivilegedAction<Void>) () -> {
System.clearProperty("codebase." + shortName);
return null;
});
}
}
} catch (URISyntaxException e) {
throw new IllegalArgumentException("unable to parse policy file `" + policyFile + "`", e);
}
}
}

View File

@ -20,10 +20,10 @@ import java.net.URLClassLoader;
import java.nio.file.DirectoryStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.ArrayList;
import java.util.Arrays;
import static org.elasticsearch.common.io.FileSystemUtils.isAccessibleDirectory;
@ -37,18 +37,25 @@ public class XPackExtensionsService {
/**
* We keep around a list of extensions
*/
private final List<Tuple<XPackExtensionInfo, XPackExtension> > extensions;
private final List<Tuple<XPackExtensionInfo, XPackExtension>> extensions;
/**
* Constructs a new XPackExtensionsService
*
* @param settings The settings of the system
* @param extsDirectory The directory extensions exist in, or null if extensions should not be loaded from the filesystem
* @param classpathExtensions Extensions that exist in the classpath which should be loaded
*/
public XPackExtensionsService(Settings settings, Path extsDirectory, Collection<Class<? extends XPackExtension>> classpathExtensions) {
public XPackExtensionsService(Settings settings, Path extsDirectory,
Collection<Class<? extends XPackExtension>> classpathExtensions) {
try {
XPackExtensionSecurity.configure(extsDirectory);
} catch (Exception e) {
throw new IllegalStateException("Unable to configure extension policy", e);
}
this.settings = settings;
List<Tuple<XPackExtensionInfo, XPackExtension>> extensionsLoaded = new ArrayList<>();
// first we load extensions that are on the classpath. this is for tests
for (Class<? extends XPackExtension> extClass : classpathExtensions) {
XPackExtension ext = loadExtension(extClass, settings);
@ -123,7 +130,7 @@ public class XPackExtensionsService {
return bundles;
}
private List<Tuple<XPackExtensionInfo, XPackExtension> > loadBundles(List<Bundle> bundles) {
private List<Tuple<XPackExtensionInfo, XPackExtension>> loadBundles(List<Bundle> bundles) {
List<Tuple<XPackExtensionInfo, XPackExtension>> exts = new ArrayList<>();
for (Bundle bundle : bundles) {

View File

@ -16,4 +16,9 @@ grant {
// bouncy castle
permission java.security.SecurityPermission "putProviderProperty.BC";
// needed for x-pack security extension
permission java.security.SecurityPermission "createPolicy.JavaPolicy";
permission java.security.SecurityPermission "getPolicy";
permission java.security.SecurityPermission "setPolicy";
};

View File

@ -95,7 +95,7 @@ public class InstallXPackExtensionCommandTests extends ESTestCase {
return terminal;
}
void assertExtension(String name, Path original, Environment env) throws IOException {
void assertExtension(String name, Environment env) throws IOException {
Path got = env.pluginsFile().resolve("x-pack").resolve("extensions").resolve(name);
assertTrue("dir " + name + " exists", Files.exists(got));
assertTrue("jar was copied", Files.exists(got.resolve("extension.jar")));
@ -116,7 +116,7 @@ public class InstallXPackExtensionCommandTests extends ESTestCase {
Path extDir = createTempDir();
String extZip = createExtension("fake", extDir);
installExtension(extZip, home);
assertExtension("fake", extDir, env);
assertExtension("fake", env);
}
public void testSpaceInUrl() throws Exception {
@ -127,7 +127,7 @@ public class InstallXPackExtensionCommandTests extends ESTestCase {
Files.copy(in, extZipWithSpaces, StandardCopyOption.REPLACE_EXISTING);
}
installExtension(extZipWithSpaces.toUri().toURL().toString(), home);
assertExtension("fake", extDir, env);
assertExtension("fake", env);
}
public void testMalformedUrlNotMaven() throws Exception {
@ -155,8 +155,8 @@ public class InstallXPackExtensionCommandTests extends ESTestCase {
Path extDir2 = createTempDir();
String extZip2 = createExtension("fake2", extDir2);
installExtension(extZip2, home);
assertExtension("fake1", extDir1, env);
assertExtension("fake2", extDir2, env);
assertExtension("fake1", env);
assertExtension("fake2", env);
}
public void testExistingExtension() throws Exception {

View File

@ -0,0 +1,59 @@
/*
* 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.extensions;
import org.elasticsearch.test.ESTestCase;
import java.nio.file.Path;
import java.security.Permission;
import java.security.PermissionCollection;
import java.security.Permissions;
import java.util.Collections;
import java.util.List;
public class XPackExtensionSecurityTests extends ESTestCase {
/** Test that we can parse the set of permissions correctly for a simple policy */
public void testParsePermissions() throws Exception {
Path scratch = createTempDir();
Path testFile = this.getDataPath("security/simple-x-pack-extension-security.policy");
Permissions expected = new Permissions();
expected.add(new RuntimePermission("queuePrintJob"));
PermissionCollection actual = InstallXPackExtensionCommand.parsePermissions(testFile, scratch);
assertEquals(expected, actual);
}
/** Test that we can parse the set of permissions correctly for a complex policy */
public void testParseTwoPermissions() throws Exception {
Path scratch = createTempDir();
Path testFile = this.getDataPath("security/complex-x-pack-extension-security.policy");
Permissions expected = new Permissions();
expected.add(new RuntimePermission("getClassLoader"));
expected.add(new RuntimePermission("closeClassLoader"));
PermissionCollection actual = InstallXPackExtensionCommand.parsePermissions(testFile, scratch);
assertEquals(expected, actual);
}
/** Test that we can format some simple permissions properly */
public void testFormatSimplePermission() throws Exception {
assertEquals("java.lang.RuntimePermission queuePrintJob",
InstallXPackExtensionCommand.formatPermission(new RuntimePermission("queuePrintJob")));
}
/** Test that we can format an unresolved permission properly */
public void testFormatUnresolvedPermission() throws Exception {
Path scratch = createTempDir();
Path testFile = this.getDataPath("security/unresolved-x-pack-extension-security.policy");
PermissionCollection actual = InstallXPackExtensionCommand.parsePermissions(testFile, scratch);
List<Permission> permissions = Collections.list(actual.elements());
assertEquals(1, permissions.size());
assertEquals("org.fake.FakePermission fakeName", InstallXPackExtensionCommand.formatPermission(permissions.get(0)));
}
/** no guaranteed equals on these classes, we assert they contain the same set */
private void assertEquals(PermissionCollection expected, PermissionCollection actual) {
assertEquals(asSet(Collections.list(expected.elements())), asSet(Collections.list(actual.elements())));
}
}

View File

@ -0,0 +1,5 @@
grant {
// needed to cause problems
permission java.lang.RuntimePermission "getClassLoader";
permission java.lang.RuntimePermission "closeClassLoader";
};

View File

@ -0,0 +1,4 @@
grant {
// needed to waste paper
permission java.lang.RuntimePermission "queuePrintJob";
};

View File

@ -0,0 +1,4 @@
grant {
// an unresolved permission
permission org.fake.FakePermission "fakeName";
};

View File

@ -114,6 +114,7 @@ public class PutWatchRequest extends MasterNodeRequest<PutWatchRequest> {
super.readFrom(in);
id = in.readString();
source = in.readBytesReference();
active = in.readBoolean();
}
@Override
@ -121,6 +122,7 @@ public class PutWatchRequest extends MasterNodeRequest<PutWatchRequest> {
super.writeTo(out);
out.writeString(id);
out.writeBytesReference(source);
out.writeBoolean(active);
}
}

View File

@ -0,0 +1,34 @@
/*
* 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.watcher.transport.action.put;
import org.elasticsearch.common.bytes.BytesArray;
import org.elasticsearch.common.io.stream.BytesStreamOutput;
import org.elasticsearch.test.ESTestCase;
import org.elasticsearch.xpack.watcher.transport.actions.put.PutWatchRequest;
import static org.hamcrest.Matchers.is;
public class PutWatchSerializationTests extends ESTestCase {
// https://github.com/elastic/x-plugins/issues/2490
public void testPutWatchSerialization() throws Exception {
PutWatchRequest request = new PutWatchRequest();
request.setId(randomAsciiOfLength(10));
request.setActive(randomBoolean());
request.setSource(new BytesArray(randomAsciiOfLength(20)));
BytesStreamOutput streamOutput = new BytesStreamOutput();
request.writeTo(streamOutput);
PutWatchRequest readRequest = new PutWatchRequest();
readRequest.readFrom(streamOutput.bytes().streamInput());
assertThat(readRequest.isActive(), is(request.isActive()));
assertThat(readRequest.getId(), is(request.getId()));
assertThat(readRequest.getSource(), is(request.getSource()));
}
}