Skip to content

Commit cf84571

Browse files
authored
Update DefaultAuthenticationFailureHandler to handle 403s (elastic#138930)
1. Catch 403s are return them when certain metadata conditions are fulfilled. Deny 403s in response otherwise.
1 parent d8b6b9c commit cf84571

File tree

2 files changed

+106
-1
lines changed

2 files changed

+106
-1
lines changed

x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/DefaultAuthenticationFailureHandler.java

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,11 @@
2929
* response headers like 'WWW-Authenticate'
3030
*/
3131
public class DefaultAuthenticationFailureHandler implements AuthenticationFailureHandler {
32+
/**
33+
* Metadata key to denote exceptions that are allowed to return with 403 status
34+
*/
35+
public static final String ACCESS_DENIED_METADATA_KEY = "es.security.access_denied";
36+
3237
private volatile Map<String, List<String>> defaultFailureResponseHeaders;
3338

3439
/**
@@ -113,6 +118,11 @@ public ElasticsearchSecurityException exceptionProcessingRequest(HttpPreRequest
113118
if (e instanceof ElasticsearchAuthenticationProcessingError) {
114119
return (ElasticsearchAuthenticationProcessingError) e;
115120
}
121+
if (e instanceof ElasticsearchSecurityException ese) {
122+
if (ese.status() == RestStatus.FORBIDDEN && ese.getMetadata(ACCESS_DENIED_METADATA_KEY) != null) {
123+
return ese;
124+
}
125+
}
116126
return createAuthenticationError("error attempting to authenticate request", e, (Object[]) null);
117127
}
118128

@@ -128,6 +138,11 @@ public ElasticsearchSecurityException exceptionProcessingRequest(
128138
if (e instanceof ElasticsearchAuthenticationProcessingError) {
129139
return (ElasticsearchAuthenticationProcessingError) e;
130140
}
141+
if (e instanceof ElasticsearchSecurityException ese) {
142+
if (ese.status() == RestStatus.FORBIDDEN && ese.getMetadata(ACCESS_DENIED_METADATA_KEY) != null) {
143+
return ese;
144+
}
145+
}
131146
return createAuthenticationError("error attempting to authenticate request", e, (Object[]) null);
132147
}
133148

@@ -168,7 +183,7 @@ private ElasticsearchSecurityException createAuthenticationError(final String me
168183
final ElasticsearchSecurityException ese;
169184
final boolean containsNegotiateWithToken;
170185
if (t instanceof ElasticsearchSecurityException) {
171-
assert ((ElasticsearchSecurityException) t).status() == RestStatus.UNAUTHORIZED;
186+
assert ((ElasticsearchSecurityException) t).status() == RestStatus.UNAUTHORIZED : "rest status must be 401 UNAUTHORIZED";
172187
ese = (ElasticsearchSecurityException) t;
173188
if (ese.getBodyHeader("WWW-Authenticate") != null && ese.getBodyHeader("WWW-Authenticate").isEmpty() == false) {
174189
/**

x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authc/DefaultAuthenticationFailureHandlerTests.java

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
import org.elasticsearch.http.HttpPreRequest;
1414
import org.elasticsearch.rest.RestStatus;
1515
import org.elasticsearch.test.ESTestCase;
16+
import org.elasticsearch.transport.TransportRequest;
1617
import org.elasticsearch.xpack.core.XPackField;
1718

1819
import java.util.Arrays;
@@ -21,7 +22,9 @@
2122
import java.util.List;
2223
import java.util.Map;
2324

25+
import static org.elasticsearch.xpack.core.security.authc.DefaultAuthenticationFailureHandler.ACCESS_DENIED_METADATA_KEY;
2426
import static org.hamcrest.Matchers.contains;
27+
import static org.hamcrest.Matchers.containsString;
2528
import static org.hamcrest.Matchers.equalTo;
2629
import static org.hamcrest.Matchers.is;
2730
import static org.hamcrest.Matchers.notNullValue;
@@ -112,6 +115,7 @@ public void testExceptionProcessingRequest() {
112115
} else {
113116
expectThrows(
114117
AssertionError.class,
118+
containsString("rest status must be 401 UNAUTHORIZED"),
115119
() -> failureHandler.exceptionProcessingRequest(
116120
mock(HttpPreRequest.class),
117121
cause,
@@ -156,6 +160,92 @@ public void testSortsWWWAuthenticateHeaderValues() {
156160
assertWWWAuthenticateWithSchemes(ese, negotiateAuthScheme, bearerAuthScheme, apiKeyAuthScheme, basicAuthScheme);
157161
}
158162

163+
public void testExceptionProcessingRequestWith403SecurityException() {
164+
final DefaultAuthenticationFailureHandler failureHandler = new DefaultAuthenticationFailureHandler(Collections.emptyMap());
165+
final ThreadContext threadContext = new ThreadContext(Settings.builder().build());
166+
167+
// Test 1: 403 exception with es.security.access_denied metadata should be returned as-is
168+
{
169+
ElasticsearchSecurityException forbiddenWithMetadata = new ElasticsearchSecurityException(
170+
"failed to authorize user for project [test-project]",
171+
RestStatus.FORBIDDEN,
172+
null,
173+
(Object[]) null
174+
);
175+
forbiddenWithMetadata.addMetadata(ACCESS_DENIED_METADATA_KEY, "true");
176+
177+
ElasticsearchSecurityException result = failureHandler.exceptionProcessingRequest(
178+
mock(HttpPreRequest.class),
179+
forbiddenWithMetadata,
180+
threadContext
181+
);
182+
assertThat(result, is(sameInstance(forbiddenWithMetadata)));
183+
assertThat(result.status(), equalTo(RestStatus.FORBIDDEN));
184+
185+
result = failureHandler.exceptionProcessingRequest(
186+
mock(TransportRequest.class),
187+
"index:foo/bar",
188+
forbiddenWithMetadata,
189+
threadContext
190+
);
191+
assertThat(result, is(sameInstance(forbiddenWithMetadata)));
192+
assertThat(result.status(), equalTo(RestStatus.FORBIDDEN));
193+
}
194+
195+
// Test 2: 403 exception without metadata should fail assertion
196+
{
197+
ElasticsearchSecurityException forbiddenWithoutMetadata = new ElasticsearchSecurityException(
198+
"some forbidden error",
199+
RestStatus.FORBIDDEN,
200+
null,
201+
(Object[]) null
202+
);
203+
204+
expectThrows(
205+
AssertionError.class,
206+
containsString("rest status must be 401 UNAUTHORIZED"),
207+
() -> failureHandler.exceptionProcessingRequest(mock(HttpPreRequest.class), forbiddenWithoutMetadata, threadContext)
208+
);
209+
expectThrows(
210+
AssertionError.class,
211+
containsString("rest status must be 401 UNAUTHORIZED"),
212+
() -> failureHandler.exceptionProcessingRequest(
213+
mock(TransportRequest.class),
214+
"index:foo/bar",
215+
forbiddenWithoutMetadata,
216+
threadContext
217+
)
218+
);
219+
}
220+
221+
// Test 3: 403 exception with different metadata should fail assertion
222+
{
223+
ElasticsearchSecurityException forbiddenWithDifferentMetadata = new ElasticsearchSecurityException(
224+
"some other forbidden error",
225+
RestStatus.FORBIDDEN,
226+
null,
227+
(Object[]) null
228+
);
229+
forbiddenWithDifferentMetadata.addMetadata("es.some.other.metadata", "value");
230+
231+
expectThrows(
232+
AssertionError.class,
233+
containsString("rest status must be 401 UNAUTHORIZED"),
234+
() -> failureHandler.exceptionProcessingRequest(mock(HttpPreRequest.class), forbiddenWithDifferentMetadata, threadContext)
235+
);
236+
expectThrows(
237+
AssertionError.class,
238+
containsString("rest status must be 401 UNAUTHORIZED"),
239+
() -> failureHandler.exceptionProcessingRequest(
240+
mock(TransportRequest.class),
241+
"index:foo/bar",
242+
forbiddenWithDifferentMetadata,
243+
threadContext
244+
)
245+
);
246+
}
247+
}
248+
159249
private void assertWWWAuthenticateWithSchemes(final ElasticsearchSecurityException ese, final String... schemes) {
160250
assertThat(ese.getBodyHeader("WWW-Authenticate").size(), is(schemes.length));
161251
assertThat(ese.getBodyHeader("WWW-Authenticate"), contains(schemes));

0 commit comments

Comments
 (0)