mirror of
https://github.com/elastic/elasticsearch.git
synced 2025-06-28 09:28:55 -04:00
Rethrow NoSuchFileException if encountering an invalid symlink when checking file entitlements (#124483)
This will rethrow the `NoSuchFileException` when encountering an invalid symbolic link when following links during file (read) entitlement checks. Relates to https://github.com/elastic/elasticsearch/pull/124133 (ES-11019)
This commit is contained in:
parent
cda82554aa
commit
c26d195120
10 changed files with 119 additions and 21 deletions
|
@ -64,6 +64,7 @@ import java.nio.file.FileStore;
|
||||||
import java.nio.file.FileVisitOption;
|
import java.nio.file.FileVisitOption;
|
||||||
import java.nio.file.FileVisitor;
|
import java.nio.file.FileVisitor;
|
||||||
import java.nio.file.LinkOption;
|
import java.nio.file.LinkOption;
|
||||||
|
import java.nio.file.NoSuchFileException;
|
||||||
import java.nio.file.OpenOption;
|
import java.nio.file.OpenOption;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.nio.file.WatchEvent;
|
import java.nio.file.WatchEvent;
|
||||||
|
@ -1203,7 +1204,7 @@ public interface EntitlementChecker {
|
||||||
void checkType(Class<?> callerClass, FileStore that);
|
void checkType(Class<?> callerClass, FileStore that);
|
||||||
|
|
||||||
// path
|
// path
|
||||||
void checkPathToRealPath(Class<?> callerClass, Path that, LinkOption... options);
|
void checkPathToRealPath(Class<?> callerClass, Path that, LinkOption... options) throws NoSuchFileException;
|
||||||
|
|
||||||
void checkPathRegister(Class<?> callerClass, Path that, WatchService watcher, WatchEvent.Kind<?>... events);
|
void checkPathRegister(Class<?> callerClass, Path that, WatchService watcher, WatchEvent.Kind<?>... events);
|
||||||
|
|
||||||
|
|
|
@ -63,7 +63,15 @@ public final class EntitledActions {
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Path createTempSymbolicLink() throws IOException {
|
public static Path createTempSymbolicLink() throws IOException {
|
||||||
return Files.createSymbolicLink(readDir().resolve("entitlements-link-" + random.nextLong()), readWriteDir());
|
return createTempSymbolicLink(readWriteDir());
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Path createTempSymbolicLink(Path target) throws IOException {
|
||||||
|
return Files.createSymbolicLink(readDir().resolve("entitlements-link-" + random.nextLong()), target);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Path pathToRealPath(Path path) throws IOException {
|
||||||
|
return path.toRealPath();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Path createK8sLikeMount() throws IOException {
|
public static Path createK8sLikeMount() throws IOException {
|
||||||
|
|
|
@ -24,6 +24,7 @@ dependencies {
|
||||||
compileOnly project(':server')
|
compileOnly project(':server')
|
||||||
compileOnly project(':libs:logging')
|
compileOnly project(':libs:logging')
|
||||||
compileOnly project(":libs:entitlement:qa:entitled-plugin")
|
compileOnly project(":libs:entitlement:qa:entitled-plugin")
|
||||||
|
implementation project(":libs:entitlement")
|
||||||
}
|
}
|
||||||
|
|
||||||
tasks.named("javadoc").configure {
|
tasks.named("javadoc").configure {
|
||||||
|
|
|
@ -11,6 +11,7 @@ module org.elasticsearch.entitlement.qa.test {
|
||||||
requires org.elasticsearch.server;
|
requires org.elasticsearch.server;
|
||||||
requires org.elasticsearch.base;
|
requires org.elasticsearch.base;
|
||||||
requires org.elasticsearch.logging;
|
requires org.elasticsearch.logging;
|
||||||
|
requires org.elasticsearch.entitlement;
|
||||||
requires org.elasticsearch.entitlement.qa.entitled;
|
requires org.elasticsearch.entitlement.qa.entitled;
|
||||||
|
|
||||||
// Modules we'll attempt to use in order to exercise entitlements
|
// Modules we'll attempt to use in order to exercise entitlements
|
||||||
|
|
|
@ -9,6 +9,8 @@
|
||||||
|
|
||||||
package org.elasticsearch.entitlement.qa.test;
|
package org.elasticsearch.entitlement.qa.test;
|
||||||
|
|
||||||
|
import org.elasticsearch.entitlement.runtime.api.NotEntitledException;
|
||||||
|
|
||||||
import java.lang.annotation.ElementType;
|
import java.lang.annotation.ElementType;
|
||||||
import java.lang.annotation.Retention;
|
import java.lang.annotation.Retention;
|
||||||
import java.lang.annotation.RetentionPolicy;
|
import java.lang.annotation.RetentionPolicy;
|
||||||
|
@ -27,5 +29,7 @@ public @interface EntitlementTest {
|
||||||
|
|
||||||
ExpectedAccess expectedAccess();
|
ExpectedAccess expectedAccess();
|
||||||
|
|
||||||
|
Class<? extends Exception> expectedExceptionIfDenied() default NotEntitledException.class;
|
||||||
|
|
||||||
int fromJavaVersion() default -1;
|
int fromJavaVersion() default -1;
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,12 +10,17 @@
|
||||||
package org.elasticsearch.entitlement.qa.test;
|
package org.elasticsearch.entitlement.qa.test;
|
||||||
|
|
||||||
import org.elasticsearch.entitlement.qa.entitled.EntitledActions;
|
import org.elasticsearch.entitlement.qa.entitled.EntitledActions;
|
||||||
|
import org.elasticsearch.entitlement.runtime.policy.PolicyManager;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.nio.file.FileSystems;
|
import java.nio.file.FileSystems;
|
||||||
import java.nio.file.LinkOption;
|
import java.nio.file.LinkOption;
|
||||||
|
import java.nio.file.NoSuchFileException;
|
||||||
|
import java.nio.file.Path;
|
||||||
import java.nio.file.WatchEvent;
|
import java.nio.file.WatchEvent;
|
||||||
|
import java.util.Arrays;
|
||||||
|
|
||||||
|
import static org.elasticsearch.entitlement.qa.test.EntitlementTest.ExpectedAccess.ALWAYS_DENIED;
|
||||||
import static org.elasticsearch.entitlement.qa.test.EntitlementTest.ExpectedAccess.PLUGINS;
|
import static org.elasticsearch.entitlement.qa.test.EntitlementTest.ExpectedAccess.PLUGINS;
|
||||||
|
|
||||||
@SuppressWarnings({ "unused" /* called via reflection */, "rawtypes" })
|
@SuppressWarnings({ "unused" /* called via reflection */, "rawtypes" })
|
||||||
|
@ -26,6 +31,18 @@ class PathActions {
|
||||||
FileCheckActions.readFile().toRealPath();
|
FileCheckActions.readFile().toRealPath();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@EntitlementTest(expectedAccess = ALWAYS_DENIED, expectedExceptionIfDenied = NoSuchFileException.class)
|
||||||
|
static void checkToRealPathForInvalidTarget() throws IOException {
|
||||||
|
Path invalidLink = EntitledActions.createTempSymbolicLink(FileCheckActions.readDir().resolve("invalid"));
|
||||||
|
try {
|
||||||
|
EntitledActions.pathToRealPath(invalidLink); // throws NoSuchFileException when checking entitlements due to invalid target
|
||||||
|
} catch (NoSuchFileException e) {
|
||||||
|
assert Arrays.stream(e.getStackTrace()).anyMatch(t -> t.getClassName().equals(PolicyManager.class.getName()))
|
||||||
|
: "Expected NoSuchFileException to be thrown by entitlements check";
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@EntitlementTest(expectedAccess = PLUGINS)
|
@EntitlementTest(expectedAccess = PLUGINS)
|
||||||
static void checkToRealPathWithK8sLikeMount() throws IOException, Exception {
|
static void checkToRealPathWithK8sLikeMount() throws IOException, Exception {
|
||||||
EntitledActions.createK8sLikeMount().toRealPath();
|
EntitledActions.createK8sLikeMount().toRealPath();
|
||||||
|
|
|
@ -13,6 +13,7 @@ import org.elasticsearch.client.internal.node.NodeClient;
|
||||||
import org.elasticsearch.common.Strings;
|
import org.elasticsearch.common.Strings;
|
||||||
import org.elasticsearch.core.CheckedRunnable;
|
import org.elasticsearch.core.CheckedRunnable;
|
||||||
import org.elasticsearch.core.SuppressForbidden;
|
import org.elasticsearch.core.SuppressForbidden;
|
||||||
|
import org.elasticsearch.entitlement.runtime.api.NotEntitledException;
|
||||||
import org.elasticsearch.logging.LogManager;
|
import org.elasticsearch.logging.LogManager;
|
||||||
import org.elasticsearch.logging.Logger;
|
import org.elasticsearch.logging.Logger;
|
||||||
import org.elasticsearch.rest.BaseRestHandler;
|
import org.elasticsearch.rest.BaseRestHandler;
|
||||||
|
@ -68,20 +69,25 @@ import static org.elasticsearch.rest.RestRequest.Method.GET;
|
||||||
public class RestEntitlementsCheckAction extends BaseRestHandler {
|
public class RestEntitlementsCheckAction extends BaseRestHandler {
|
||||||
private static final Logger logger = LogManager.getLogger(RestEntitlementsCheckAction.class);
|
private static final Logger logger = LogManager.getLogger(RestEntitlementsCheckAction.class);
|
||||||
|
|
||||||
record CheckAction(CheckedRunnable<Exception> action, EntitlementTest.ExpectedAccess expectedAccess, Integer fromJavaVersion) {
|
record CheckAction(
|
||||||
|
CheckedRunnable<Exception> action,
|
||||||
|
EntitlementTest.ExpectedAccess expectedAccess,
|
||||||
|
Class<? extends Exception> expectedExceptionIfDenied,
|
||||||
|
Integer fromJavaVersion
|
||||||
|
) {
|
||||||
/**
|
/**
|
||||||
* These cannot be granted to plugins, so our test plugins cannot test the "allowed" case.
|
* These cannot be granted to plugins, so our test plugins cannot test the "allowed" case.
|
||||||
*/
|
*/
|
||||||
static CheckAction deniedToPlugins(CheckedRunnable<Exception> action) {
|
static CheckAction deniedToPlugins(CheckedRunnable<Exception> action) {
|
||||||
return new CheckAction(action, SERVER_ONLY, null);
|
return new CheckAction(action, SERVER_ONLY, NotEntitledException.class, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
static CheckAction forPlugins(CheckedRunnable<Exception> action) {
|
static CheckAction forPlugins(CheckedRunnable<Exception> action) {
|
||||||
return new CheckAction(action, PLUGINS, null);
|
return new CheckAction(action, PLUGINS, NotEntitledException.class, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
static CheckAction alwaysDenied(CheckedRunnable<Exception> action) {
|
static CheckAction alwaysDenied(CheckedRunnable<Exception> action) {
|
||||||
return new CheckAction(action, ALWAYS_DENIED, null);
|
return new CheckAction(action, ALWAYS_DENIED, NotEntitledException.class, null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -128,7 +134,12 @@ public class RestEntitlementsCheckAction extends BaseRestHandler {
|
||||||
entry("responseCache_setDefault", alwaysDenied(RestEntitlementsCheckAction::setDefaultResponseCache)),
|
entry("responseCache_setDefault", alwaysDenied(RestEntitlementsCheckAction::setDefaultResponseCache)),
|
||||||
entry(
|
entry(
|
||||||
"createInetAddressResolverProvider",
|
"createInetAddressResolverProvider",
|
||||||
new CheckAction(VersionSpecificNetworkChecks::createInetAddressResolverProvider, SERVER_ONLY, 18)
|
new CheckAction(
|
||||||
|
VersionSpecificNetworkChecks::createInetAddressResolverProvider,
|
||||||
|
SERVER_ONLY,
|
||||||
|
NotEntitledException.class,
|
||||||
|
18
|
||||||
|
)
|
||||||
),
|
),
|
||||||
entry("createURLStreamHandlerProvider", alwaysDenied(RestEntitlementsCheckAction::createURLStreamHandlerProvider)),
|
entry("createURLStreamHandlerProvider", alwaysDenied(RestEntitlementsCheckAction::createURLStreamHandlerProvider)),
|
||||||
entry("createURLWithURLStreamHandler", alwaysDenied(RestEntitlementsCheckAction::createURLWithURLStreamHandler)),
|
entry("createURLWithURLStreamHandler", alwaysDenied(RestEntitlementsCheckAction::createURLWithURLStreamHandler)),
|
||||||
|
@ -237,7 +248,12 @@ public class RestEntitlementsCheckAction extends BaseRestHandler {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
Integer fromJavaVersion = testAnnotation.fromJavaVersion() == -1 ? null : testAnnotation.fromJavaVersion();
|
Integer fromJavaVersion = testAnnotation.fromJavaVersion() == -1 ? null : testAnnotation.fromJavaVersion();
|
||||||
entries.add(entry(method.getName(), new CheckAction(runnable, testAnnotation.expectedAccess(), fromJavaVersion)));
|
entries.add(
|
||||||
|
entry(
|
||||||
|
method.getName(),
|
||||||
|
new CheckAction(runnable, testAnnotation.expectedAccess(), testAnnotation.expectedExceptionIfDenied(), fromJavaVersion)
|
||||||
|
)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return entries.stream();
|
return entries.stream();
|
||||||
}
|
}
|
||||||
|
@ -437,9 +453,19 @@ public class RestEntitlementsCheckAction extends BaseRestHandler {
|
||||||
|
|
||||||
return channel -> {
|
return channel -> {
|
||||||
logger.info("Calling check action [{}]", actionName);
|
logger.info("Calling check action [{}]", actionName);
|
||||||
checkAction.action().run();
|
RestResponse response;
|
||||||
logger.debug("Check action [{}] returned", actionName);
|
try {
|
||||||
channel.sendResponse(new RestResponse(RestStatus.OK, Strings.format("Succesfully executed action [%s]", actionName)));
|
checkAction.action().run();
|
||||||
|
response = new RestResponse(RestStatus.OK, Strings.format("Succesfully executed action [%s]", actionName));
|
||||||
|
} catch (Exception e) {
|
||||||
|
var statusCode = checkAction.expectedExceptionIfDenied.isInstance(e)
|
||||||
|
? RestStatus.FORBIDDEN
|
||||||
|
: RestStatus.INTERNAL_SERVER_ERROR;
|
||||||
|
response = new RestResponse(channel, statusCode, e);
|
||||||
|
response.addHeader("expectedException", checkAction.expectedExceptionIfDenied.getName());
|
||||||
|
}
|
||||||
|
logger.debug("Check action [{}] returned status [{}]", actionName, response.status().getStatus());
|
||||||
|
channel.sendResponse(response);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -11,14 +11,17 @@ package org.elasticsearch.entitlement.qa;
|
||||||
|
|
||||||
import org.elasticsearch.client.Request;
|
import org.elasticsearch.client.Request;
|
||||||
import org.elasticsearch.client.Response;
|
import org.elasticsearch.client.Response;
|
||||||
|
import org.elasticsearch.client.ResponseException;
|
||||||
import org.elasticsearch.entitlement.qa.EntitlementsTestRule.PolicyBuilder;
|
import org.elasticsearch.entitlement.qa.EntitlementsTestRule.PolicyBuilder;
|
||||||
import org.elasticsearch.test.rest.ESRestTestCase;
|
import org.elasticsearch.test.rest.ESRestTestCase;
|
||||||
|
import org.hamcrest.Description;
|
||||||
|
import org.hamcrest.Matcher;
|
||||||
|
import org.hamcrest.TypeSafeMatcher;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
import static org.hamcrest.Matchers.containsString;
|
|
||||||
import static org.hamcrest.Matchers.equalTo;
|
import static org.hamcrest.Matchers.equalTo;
|
||||||
|
|
||||||
public abstract class AbstractEntitlementsIT extends ESRestTestCase {
|
public abstract class AbstractEntitlementsIT extends ESRestTestCase {
|
||||||
|
@ -69,8 +72,34 @@ public abstract class AbstractEntitlementsIT extends ESRestTestCase {
|
||||||
Response result = executeCheck();
|
Response result = executeCheck();
|
||||||
assertThat(result.getStatusLine().getStatusCode(), equalTo(200));
|
assertThat(result.getStatusLine().getStatusCode(), equalTo(200));
|
||||||
} else {
|
} else {
|
||||||
var exception = expectThrows(IOException.class, this::executeCheck);
|
var exception = expectThrows(ResponseException.class, this::executeCheck);
|
||||||
assertThat(exception.getMessage(), containsString("not_entitled_exception"));
|
assertThat(exception, statusCodeMatcher(403));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static Matcher<ResponseException> statusCodeMatcher(int statusCode) {
|
||||||
|
return new TypeSafeMatcher<>() {
|
||||||
|
String expectedException = null;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected boolean matchesSafely(ResponseException item) {
|
||||||
|
Response resp = item.getResponse();
|
||||||
|
expectedException = resp.getHeader("expectedException");
|
||||||
|
return resp.getStatusLine().getStatusCode() == statusCode && expectedException != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void describeTo(Description description) {
|
||||||
|
description.appendValue(statusCode).appendText(" due to ").appendText(expectedException);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void describeMismatchSafely(ResponseException item, Description description) {
|
||||||
|
description.appendText("was ")
|
||||||
|
.appendValue(item.getResponse().getStatusLine().getStatusCode())
|
||||||
|
.appendText("\n")
|
||||||
|
.appendValue(item.getMessage());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -74,6 +74,7 @@ import java.nio.file.FileStore;
|
||||||
import java.nio.file.FileVisitOption;
|
import java.nio.file.FileVisitOption;
|
||||||
import java.nio.file.FileVisitor;
|
import java.nio.file.FileVisitor;
|
||||||
import java.nio.file.LinkOption;
|
import java.nio.file.LinkOption;
|
||||||
|
import java.nio.file.NoSuchFileException;
|
||||||
import java.nio.file.OpenOption;
|
import java.nio.file.OpenOption;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.nio.file.Paths;
|
import java.nio.file.Paths;
|
||||||
|
@ -2744,7 +2745,7 @@ public class ElasticsearchEntitlementChecker implements EntitlementChecker {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void checkPathToRealPath(Class<?> callerClass, Path that, LinkOption... options) {
|
public void checkPathToRealPath(Class<?> callerClass, Path that, LinkOption... options) throws NoSuchFileException {
|
||||||
boolean followLinks = true;
|
boolean followLinks = true;
|
||||||
for (LinkOption option : options) {
|
for (LinkOption option : options) {
|
||||||
if (option == LinkOption.NOFOLLOW_LINKS) {
|
if (option == LinkOption.NOFOLLOW_LINKS) {
|
||||||
|
|
|
@ -35,6 +35,7 @@ import java.io.IOException;
|
||||||
import java.lang.StackWalker.StackFrame;
|
import java.lang.StackWalker.StackFrame;
|
||||||
import java.lang.module.ModuleFinder;
|
import java.lang.module.ModuleFinder;
|
||||||
import java.lang.module.ModuleReference;
|
import java.lang.module.ModuleReference;
|
||||||
|
import java.nio.file.NoSuchFileException;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.nio.file.Paths;
|
import java.nio.file.Paths;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
@ -326,10 +327,17 @@ public class PolicyManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
public void checkFileRead(Class<?> callerClass, Path path) {
|
public void checkFileRead(Class<?> callerClass, Path path) {
|
||||||
checkFileRead(callerClass, path, false);
|
try {
|
||||||
|
checkFileRead(callerClass, path, false);
|
||||||
|
} catch (NoSuchFileException e) {
|
||||||
|
assert false : "NoSuchFileException should only be thrown when following links";
|
||||||
|
var notEntitledException = new NotEntitledException(e.getMessage());
|
||||||
|
notEntitledException.addSuppressed(e);
|
||||||
|
throw notEntitledException;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void checkFileRead(Class<?> callerClass, Path path, boolean followLinks) {
|
public void checkFileRead(Class<?> callerClass, Path path, boolean followLinks) throws NoSuchFileException {
|
||||||
if (isPathOnDefaultFilesystem(path) == false) {
|
if (isPathOnDefaultFilesystem(path) == false) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -345,11 +353,13 @@ public class PolicyManager {
|
||||||
if (canRead && followLinks) {
|
if (canRead && followLinks) {
|
||||||
try {
|
try {
|
||||||
realPath = path.toRealPath();
|
realPath = path.toRealPath();
|
||||||
|
if (realPath.equals(path) == false) {
|
||||||
|
canRead = entitlements.fileAccess().canRead(realPath);
|
||||||
|
}
|
||||||
|
} catch (NoSuchFileException e) {
|
||||||
|
throw e; // rethrow
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
// target not found or other IO error
|
canRead = false;
|
||||||
}
|
|
||||||
if (realPath != null && realPath.equals(path) == false) {
|
|
||||||
canRead = entitlements.fileAccess().canRead(realPath);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue