Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
import org.elasticsearch.core.Nullable;

import java.io.IOException;
import java.util.Objects;
import java.util.Set;

/**
Expand Down Expand Up @@ -76,14 +77,72 @@ public enum LocalIndexResolutionResult {
/**
* Represents local (non-remote) resolution results, including expanded indices, and a {@link LocalIndexResolutionResult}.
*/
public record LocalExpressions(
Set<String> expressions,
LocalIndexResolutionResult localIndexResolutionResult,
@Nullable ElasticsearchException exception
) implements Writeable {
public LocalExpressions {
public static final class LocalExpressions implements Writeable {
private final Set<String> expressions;
private final LocalIndexResolutionResult localIndexResolutionResult;
@Nullable
private ElasticsearchException exception;

public LocalExpressions(
Set<String> expressions,
LocalIndexResolutionResult localIndexResolutionResult,
@Nullable ElasticsearchException exception
) {
assert localIndexResolutionResult != LocalIndexResolutionResult.SUCCESS || exception == null
: "If the local resolution result is SUCCESS, exception must be null";
this.expressions = expressions;
this.localIndexResolutionResult = localIndexResolutionResult;
this.exception = exception;
}

public Set<String> expressions() {
return expressions;
}

public LocalIndexResolutionResult localIndexResolutionResult() {
return localIndexResolutionResult;
}

@Nullable
public ElasticsearchException exception() {
return exception;
}

public void setException(ElasticsearchException exception) {
assert localIndexResolutionResult != LocalIndexResolutionResult.SUCCESS
: "If the local resolution result is SUCCESS, exception must be null";
Objects.requireNonNull(exception);

this.exception = exception;
}

@Override
public boolean equals(Object obj) {
if (obj == this) return true;
if (obj == null || obj.getClass() != this.getClass()) return false;
var that = (LocalExpressions) obj;
return Objects.equals(this.expressions, that.expressions)
&& Objects.equals(this.localIndexResolutionResult, that.localIndexResolutionResult)
&& Objects.equals(this.exception, that.exception);
}

@Override
public int hashCode() {
return Objects.hash(expressions, localIndexResolutionResult, exception);
}

@Override
public String toString() {
return "LocalExpressions["
+ "expressions="
+ expressions
+ ", "
+ "localIndexResolutionResult="
+ localIndexResolutionResult
+ ", "
+ "exception="
+ exception
+ ']';
}

// Singleton for the case where all expressions in a ResolvedIndexExpression instance are remote
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
import org.elasticsearch.action.ActionListener;
import org.elasticsearch.action.DelegatingActionListener;
import org.elasticsearch.action.DocWriteRequest;
import org.elasticsearch.action.IndicesRequest;
import org.elasticsearch.action.admin.indices.alias.Alias;
import org.elasticsearch.action.admin.indices.alias.TransportIndicesAliasesAction;
import org.elasticsearch.action.admin.indices.create.CreateIndexRequest;
Expand Down Expand Up @@ -108,6 +109,7 @@
import java.util.function.Consumer;
import java.util.function.Supplier;

import static org.elasticsearch.action.ResolvedIndexExpression.LocalIndexResolutionResult.CONCRETE_RESOURCE_UNAUTHORIZED;
import static org.elasticsearch.action.support.ContextPreservingActionListener.wrapPreservingContext;
import static org.elasticsearch.xpack.core.security.SecurityField.setting;
import static org.elasticsearch.xpack.core.security.authz.AuthorizationServiceField.ACTION_SCOPE_AUTHORIZATION_KEYS;
Expand Down Expand Up @@ -1022,6 +1024,19 @@ public ElasticsearchSecurityException remoteActionDenied(Authentication authenti
);
}

private void setResolvedIndexException(TransportRequest request, ElasticsearchSecurityException exception) {
if (request instanceof IndicesRequest.Replaceable replaceable) {
var indexExpressions = replaceable.getResolvedIndexExpressions();
if (indexExpressions != null) {
indexExpressions.expressions().forEach(resolved -> {
if (resolved.localExpressions().localIndexResolutionResult() == CONCRETE_RESOURCE_UNAUTHORIZED) {
resolved.localExpressions().setException(exception);
}
});
}
}
}

ElasticsearchSecurityException actionDenied(
Authentication authentication,
@Nullable AuthorizationInfo authorizationInfo,
Expand Down Expand Up @@ -1127,8 +1142,10 @@ private void handleFailure(@Nullable String context, @Nullable Exception e) {
Authentication authentication = requestInfo.getAuthentication();
String action = requestInfo.getAction();
TransportRequest request = requestInfo.getRequest();
final var denial = actionDenied(authentication, authzInfo, action, request, context, e);
setResolvedIndexException(request, denial);
auditTrailService.get().accessDenied(requestId, authentication, action, request, authzInfo);
failureConsumer.accept(actionDenied(authentication, authzInfo, action, request, context, e));
failureConsumer.accept(denial);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@
import org.elasticsearch.action.LatchedActionListener;
import org.elasticsearch.action.MockIndicesRequest;
import org.elasticsearch.action.OriginalIndices;
import org.elasticsearch.action.ResolvedIndexExpression;
import org.elasticsearch.action.ResolvedIndexExpressions;
import org.elasticsearch.action.admin.cluster.health.ClusterHealthRequest;
import org.elasticsearch.action.admin.cluster.health.TransportClusterHealthAction;
import org.elasticsearch.action.admin.indices.alias.Alias;
Expand Down Expand Up @@ -216,6 +218,8 @@
import java.util.function.Supplier;

import static java.util.Arrays.asList;
import static org.elasticsearch.action.ResolvedIndexExpression.LocalIndexResolutionResult.CONCRETE_RESOURCE_UNAUTHORIZED;
import static org.elasticsearch.action.ResolvedIndexExpression.LocalIndexResolutionResult.SUCCESS;
import static org.elasticsearch.test.ActionListenerUtils.anyActionListener;
import static org.elasticsearch.test.ActionListenerUtils.anyCollection;
import static org.elasticsearch.test.SecurityTestsUtils.assertAuthenticationException;
Expand Down Expand Up @@ -832,6 +836,11 @@ public void testUserWithNoRolesPerformsRemoteSearchWithScroll() {
public void testUserWithNoRolesCannotPerformLocalSearch() {
SearchRequest request = new SearchRequest();
request.indices("no_such_cluster:index");
request.setResolvedIndexExpressions(
new ResolvedIndexExpressions(
List.of(resolvedIndexExpression("_all", Set.of("no_such_cluster:index"), CONCRETE_RESOURCE_UNAUTHORIZED))
)
);
final Authentication authentication = createAuthentication(new User("test user"));
mockEmptyMetadata();
final String requestId = AuditUtil.getOrGenerateRequestId(threadContext);
Expand All @@ -848,6 +857,10 @@ public void testUserWithNoRolesCannotPerformLocalSearch() {
authzInfoRoles(Role.EMPTY.names())
);
verifyNoMoreInteractions(auditTrail);

final var authorizationException = request.getResolvedIndexExpressions().expressions().getFirst().localExpressions().exception();
assertThat(authorizationException, is(notNullValue()));
assertThat(authorizationException, instanceOf(ElasticsearchSecurityException.class));
}

/**
Expand All @@ -857,6 +870,14 @@ public void testUserWithNoRolesCannotPerformLocalSearch() {
public void testUserWithNoRolesCanPerformMultiClusterSearch() {
SearchRequest request = new SearchRequest();
request.indices("local_index", "wildcard_*", "other_cluster:remote_index", "*:foo?");
request.setResolvedIndexExpressions(
new ResolvedIndexExpressions(
List.of(
resolvedIndexExpression("local_index", Set.of("local_index"), CONCRETE_RESOURCE_UNAUTHORIZED),
resolvedIndexExpression("wildcard_*", Set.of("wildcard_*"), CONCRETE_RESOURCE_UNAUTHORIZED)
)
)
);
final Authentication authentication = createAuthentication(new User("test user"));
mockEmptyMetadata();
final String requestId = AuditUtil.getOrGenerateRequestId(threadContext);
Expand All @@ -873,6 +894,12 @@ public void testUserWithNoRolesCanPerformMultiClusterSearch() {
authzInfoRoles(Role.EMPTY.names())
);
verifyNoMoreInteractions(auditTrail);

request.getResolvedIndexExpressions().expressions().forEach(expression -> {
final var authorizationException = expression.localExpressions().exception();
assertThat(authorizationException, is(notNullValue()));
assertThat(authorizationException, instanceOf(ElasticsearchSecurityException.class));
});
}

public void testUserWithNoRolesCannotSql() {
Expand Down Expand Up @@ -1537,7 +1564,17 @@ public void testDenialErrorMessagesForSearchAction() {

AuditUtil.getOrGenerateRequestId(threadContext);

TransportRequest request = new SearchRequest("all-1", "read-2", "write-3", "other-4");
SearchRequest request = new SearchRequest("all-1", "read-2", "write-3", "other-4");
request.setResolvedIndexExpressions(
new ResolvedIndexExpressions(
List.of(
resolvedIndexExpression("all-1", Set.of("all-1"), SUCCESS),
resolvedIndexExpression("read-2", Set.of("read-2"), SUCCESS),
resolvedIndexExpression("write-3", Set.of("write-3"), CONCRETE_RESOURCE_UNAUTHORIZED),
resolvedIndexExpression("other-4", Set.of("other-4"), CONCRETE_RESOURCE_UNAUTHORIZED)
)
)
);

ElasticsearchSecurityException securityException = expectThrows(
ElasticsearchSecurityException.class,
Expand Down Expand Up @@ -1567,6 +1604,21 @@ public void testDenialErrorMessagesForSearchAction() {
assertThat(securityException, throwableWithMessage(not(containsString("all-1"))));
assertThat(securityException, throwableWithMessage(not(containsString("read-2"))));
assertThat(securityException, throwableWithMessage(containsString(", this action is granted by the index privileges [read,all]")));

final var expressions = request.getResolvedIndexExpressions().expressions();
var authorizationException = expressions.getFirst().localExpressions().exception();
assertThat(authorizationException, is(nullValue()));

authorizationException = expressions.get(1).localExpressions().exception();
assertThat(authorizationException, is(nullValue()));

authorizationException = expressions.get(2).localExpressions().exception();
assertThat(authorizationException, is(notNullValue()));
assertThat(authorizationException, instanceOf(ElasticsearchSecurityException.class));

authorizationException = expressions.get(3).localExpressions().exception();
assertThat(authorizationException, is(notNullValue()));
assertThat(authorizationException, instanceOf(ElasticsearchSecurityException.class));
}

public void testDenialErrorMessagesForBulkIngest() throws Exception {
Expand Down Expand Up @@ -3249,7 +3301,10 @@ public void testProxyRequestFailsOnNonProxyRequest() {
}

public void testProxyRequestAuthenticationDenied() {
final TransportRequest proxiedRequest = new SearchRequest();
final SearchRequest proxiedRequest = new SearchRequest();
proxiedRequest.setResolvedIndexExpressions(
new ResolvedIndexExpressions(List.of(resolvedIndexExpression("_all", Set.of(), CONCRETE_RESOURCE_UNAUTHORIZED)))
);
final DiscoveryNode node = DiscoveryNodeUtils.create("foo");
final TransportRequest transportRequest = TransportActionProxy.wrapRequest(node, proxiedRequest);
final String action = TransportActionProxy.getProxyAction(SearchTransportService.QUERY_ACTION_NAME);
Expand All @@ -3267,6 +3322,14 @@ public void testProxyRequestAuthenticationDenied() {
authzInfoRoles(new String[] { role.getName() })
);
verifyNoMoreInteractions(auditTrail);

final var authorizationException = proxiedRequest.getResolvedIndexExpressions()
.expressions()
.getFirst()
.localExpressions()
.exception();
assertThat(authorizationException, is(notNullValue()));
assertThat(authorizationException, instanceOf(ElasticsearchSecurityException.class));
}

public void testProxyRequestAuthenticationGrantedWithAllPrivileges() {
Expand Down Expand Up @@ -3748,4 +3811,16 @@ public String getWriteableName() {
@Override
public void writeTo(StreamOutput out) throws IOException {}
}

private static ResolvedIndexExpression resolvedIndexExpression(
String original,
Set<String> localExpressions,
ResolvedIndexExpression.LocalIndexResolutionResult localIndexResolutionResult
) {
return new ResolvedIndexExpression(
original,
new ResolvedIndexExpression.LocalExpressions(localExpressions, localIndexResolutionResult, null),
Set.of()
);
}
}