Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Validate Accept and Content-Type header for compatible API #54103

Merged
merged 61 commits into from
Apr 20, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
61 commits
Select commit Hold shift + click to select a range
a4f80ca
rest index action - 283 nofix
pgomulka Mar 5, 2020
c99b818
Index and Get and Infra
pgomulka Mar 6, 2020
c9c0e55
minor tests
pgomulka Mar 6, 2020
258542e
compile fixees
pgomulka Mar 6, 2020
548fbd9
Merge branch 'compat_rest_api' into compat/type-index-get
pgomulka Mar 9, 2020
0dbea8a
disable testing conventions
pgomulka Mar 9, 2020
4cf6bc1
assertions and todo for header fix
pgomulka Mar 10, 2020
3ac22b1
more tests and cleanup
pgomulka Mar 10, 2020
38721fe
Merge remote-tracking branch 'upstream/compat_rest_api' into compat/t…
jakelandis Mar 11, 2020
f2db19f
introduce a module to house the REST code
jakelandis Mar 11, 2020
b83b4ce
fix preCommit
jakelandis Mar 11, 2020
cf1d340
Merge pull request #18 from jakelandis/introduce_module
pgomulka Mar 11, 2020
5c4a02d
move restindex compatible handlers to rest-compatibility module. 228 …
pgomulka Mar 11, 2020
0ef7e18
moving test classes and compat related code to separate v7 module
pgomulka Mar 11, 2020
835ce56
test class rename and return 400 when compatible header not present
pgomulka Mar 13, 2020
f440231
clean up deprecation warnings and remove use of consumers
pgomulka Mar 13, 2020
84f1dde
v7 compatible actions warnings tests
pgomulka Mar 13, 2020
d106d1b
rename tests and enable them
pgomulka Mar 16, 2020
cf61bbd
rename isV7Compatible method
pgomulka Mar 16, 2020
f00f438
checkstyle
pgomulka Mar 16, 2020
ae1799f
Merge branch 'master' into compat/type-index-get
pgomulka Mar 16, 2020
a0fce89
Revert "Merge branch 'master' into compat/type-index-get"
pgomulka Mar 16, 2020
7e55744
Merge branch 'compat_rest_api' into compat/type-index-get
pgomulka Mar 16, 2020
00fe62d
Revert "Revert "Merge branch 'master' into compat/type-index-get""
pgomulka Mar 16, 2020
a6f0b9a
use locale with toLowerCase
pgomulka Mar 17, 2020
4eff534
spotless
pgomulka Mar 17, 2020
2d4161c
imports and disable integ tests as there are none
pgomulka Mar 17, 2020
6830f97
fix tests
pgomulka Mar 17, 2020
587f84d
Use content-type header with compatible api
pgomulka Mar 24, 2020
7b092f5
imports
pgomulka Mar 24, 2020
ef25920
Merge branch 'compat_rest_api' into compat/headers
pgomulka Mar 25, 2020
1c2fece
Merge branch 'compat_rest_api' into compat/headers
pgomulka Mar 30, 2020
2374c20
spotless
pgomulka Apr 1, 2020
eeae41f
fix compile test
pgomulka Apr 1, 2020
e7d9b29
checkstyle
pgomulka Apr 1, 2020
268bc1c
rename of mimetype header
pgomulka Apr 2, 2020
15f4be5
additional header test
pgomulka Apr 2, 2020
017c804
Merge branch 'compat_rest_api' into compat/headers
pgomulka Apr 3, 2020
3d975f3
initial headers validation
pgomulka Apr 6, 2020
09ef281
test cases
pgomulka Apr 6, 2020
bf09243
remove unused testcase
pgomulka Apr 6, 2020
30f1c40
precommit
pgomulka Apr 6, 2020
a013a03
headers validation
pgomulka Apr 7, 2020
6c28434
273 failing
pgomulka Apr 8, 2020
37d6426
*.*
pgomulka Apr 8, 2020
1d57fe7
precommit
pgomulka Apr 9, 2020
0662ef3
cleanup
pgomulka Apr 9, 2020
86f8bc0
cleanup
pgomulka Apr 9, 2020
b292080
Merge branch 'compat_rest_api' into compat/headers
pgomulka Apr 9, 2020
a4f472d
fix docs
pgomulka Apr 9, 2020
bb827bc
fix different mimetypes
pgomulka Apr 9, 2020
e9a4fbd
support text formats
pgomulka Apr 10, 2020
f876d46
do not validate media type correctness
pgomulka Apr 10, 2020
47fd5cb
do not allow empty accept header
pgomulka Apr 10, 2020
bcb11fd
allow empty accept header
pgomulka Apr 10, 2020
225a00b
exception propagation to user
pgomulka Apr 14, 2020
ebaa7c9
check which tests fail
pgomulka Apr 14, 2020
c96bd86
fix netty test
pgomulka Apr 15, 2020
10e8d1b
refactor test to be using hamcrest style
pgomulka Apr 15, 2020
3181daf
cleanup
pgomulka Apr 16, 2020
9bd396a
Merge branch 'compat_rest_api' into compat/headers
pgomulka Apr 16, 2020
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 @@ -117,7 +117,7 @@ public XContent xContent() {
};

private static final Pattern COMPATIBLE_API_HEADER_PATTERN = Pattern.compile(
"application/(vnd.elasticsearch\\+)?([^;]+)(\\s*;\\s*compatible-with=(\\d+))?",
"(application|text)/(vnd.elasticsearch\\+)?([^;]+)(\\s*;\\s*compatible-with=(\\d+))?",
Pattern.CASE_INSENSITIVE);

/**
Expand All @@ -126,7 +126,9 @@ public XContent xContent() {
* also supports a wildcard accept for {@code application/*}. This method can be used to parse the {@code Accept} HTTP header or a
* format query string parameter. This method will return {@code null} if no match is found
*/
public static XContentType fromMediaTypeOrFormat(String mediaType) {
public static XContentType fromMediaTypeOrFormat(String mediaTypeHeaderValue) {
String mediaType = parseMediaType(mediaTypeHeaderValue);

if (mediaType == null) {
return null;
}
Expand All @@ -136,7 +138,7 @@ public static XContentType fromMediaTypeOrFormat(String mediaType) {
}
}
final String lowercaseMediaType = mediaType.toLowerCase(Locale.ROOT);
if (lowercaseMediaType.startsWith("application/*")) {
if (lowercaseMediaType.startsWith("application/*") || lowercaseMediaType.equals("*/*")) {
return JSON;
}

Expand Down Expand Up @@ -165,11 +167,12 @@ public static XContentType fromMediaType(String mediaTypeHeaderValue) {
return null;
}

//public scope needed for text formats hack
public static String parseMediaType(String mediaType) {
if (mediaType != null) {
Matcher matcher = COMPATIBLE_API_HEADER_PATTERN.matcher(mediaType);
if (matcher.find()) {
return "application/" + matcher.group(2).toLowerCase(Locale.ROOT);
return (matcher.group(1) + "/" + matcher.group(3)).toLowerCase(Locale.ROOT);
}
}

Expand All @@ -179,9 +182,9 @@ public static String parseMediaType(String mediaType) {
public static String parseVersion(String mediaType){
if(mediaType != null){
Matcher matcher = COMPATIBLE_API_HEADER_PATTERN.matcher(mediaType);
if (matcher.find() && "vnd.elasticsearch+".equalsIgnoreCase(matcher.group(1))) {
if (matcher.find() && "vnd.elasticsearch+".equalsIgnoreCase(matcher.group(2))) {

return matcher.group(4);
return matcher.group(5);
}
}
return null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,12 +37,12 @@ public class RestGetActionV7 extends RestGetAction {
private static final DeprecationLogger deprecationLogger = new DeprecationLogger(LogManager.getLogger(RestGetAction.class));
static final String TYPES_DEPRECATION_MESSAGE = "[types removal] Specifying types in "
+ "document get requests is deprecated, use the /{index}/_doc/{id} endpoint instead.";

@Override
public String getName() {
return "document_get_action_v7";
}


@Override
public List<Route> routes() {
assert Version.CURRENT.major == 8 : "REST API compatibility for version 7 is only supported on version 8";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ public boolean compatibilityRequired() {
}

public static class CompatibleCreateHandler extends RestIndexAction.CreateHandler {

@Override
public String getName() {
return "document_create_action_v7";
Expand All @@ -100,15 +101,16 @@ public boolean compatibilityRequired() {
}

public static final class CompatibleAutoIdHandler extends RestIndexAction.AutoIdHandler {
@Override
public String getName() {
return "document_create_action_auto_id_v7";
}

public CompatibleAutoIdHandler(Supplier<DiscoveryNodes> nodesInCluster) {
super(nodesInCluster);
}

@Override
public String getName() {
return "document_create_action_auto_id_v7";
}

@Override
public List<Route> routes() {
return singletonList(new Route(POST, "/{index}/{type}"));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -101,4 +101,27 @@ public void testInvalidHeaderValue() throws IOException {
assertThat(map.get("type"), equalTo("content_type_header_exception"));
assertThat(map.get("reason"), equalTo("java.lang.IllegalArgumentException: invalid Content-Type header []"));
}

public void testInvalidHeaderCombinations() throws IOException {
final Request request = new Request("GET", "/_cluster/settings");
final RequestOptions.Builder options = request.getOptions().toBuilder();
options.addHeader("Content-Type", "application/vnd.elasticsearch+json;compatible-with=7");
options.addHeader("Accept", "application/vnd.elasticsearch+json;compatible-with=8");
request.setOptions(options);
request.setJsonEntity("{\"transient\":{\"search.*\":null}}");

final ResponseException e = expectThrows(ResponseException.class, () -> client().performRequest(request));
final Response response = e.getResponse();
assertThat(response.getStatusLine().getStatusCode(), equalTo(400));
final ObjectPath objectPath = ObjectPath.createFromResponse(response);
final Map<String, Object> map = objectPath.evaluate("error");
assertThat(map.get("type"), equalTo("compatible_api_headers_combination_exception"));
assertThat(map.get("reason"), equalTo("Content-Type and Accept headers have to match when content is present. " +
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

how much details do we want on this exception?
at the moment it helped debugging tests when seeing which failed validation

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is a good level of detail.

"Accept=application/vnd.elasticsearch+json;compatible-with=8 " +
"Content-Type=application/vnd.elasticsearch+json;compatible-with=7 " +
"hasContent=true " +
"path=/_cluster/settings " +
"params={} " +
"method=GET"));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Consumer;
import java.util.stream.StreamSupport;

/**
Expand All @@ -43,7 +44,6 @@ protected AbstractCompatRestTest(@Name("yaml") ClientYamlTestCandidate testCandi
super(testCandidate);
}


private static final Logger staticLogger = LogManager.getLogger(AbstractCompatRestTest.class);

public static final String COMPAT_TESTS_PATH = "/rest-api-spec/test-compat";
Expand All @@ -66,38 +66,48 @@ public static Iterable<Object[]> createParameters() throws Exception {
});
finalTestCandidates.add(testCandidates.toArray());
}
localCandidates.keySet().forEach(lc -> finalTestCandidates.add(new Object[]{lc}));
localCandidates.keySet().forEach(lc -> finalTestCandidates.add(new Object[] { lc }));
return finalTestCandidates;
}


private static void mutateTestCandidate(ClientYamlTestCandidate testCandidate) {
testCandidate.getTestSection().getExecutableSections().stream().filter(s -> s instanceof DoSection).forEach(ds -> {
testCandidate.getSetupSection().getExecutableSections().stream().filter(s -> s instanceof DoSection).forEach(updateDoSection());
testCandidate.getTestSection().getExecutableSections().stream().filter(s -> s instanceof DoSection).forEach(updateDoSection());
}

private static Consumer<? super ExecutableSection> updateDoSection() {
return ds -> {
DoSection doSection = (DoSection) ds;
//TODO: be more selective here
// TODO: be more selective here
doSection.setIgnoreWarnings(true);

String compatibleHeader = createCompatibleHeader();
//TODO decide which one to use - Accept or Content-Type
doSection.getApiCallSection()
.addHeaders(Map.of(
CompatibleConstants.COMPATIBLE_HEADER, compatibleHeader,
"Content-Type", compatibleHeader
));
});
// for cat apis accept headers would break tests which expect txt response
if (doSection.getApiCallSection().getApi().startsWith("cat") == false) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we have tests that need Accept header to be empty - expecting text response
maybe we should extend XContentType to text
but then again, we don't expect content-type to be text, and adding text there would allow this

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we can punt on the cat api's for now, and this is sufficient for now. this code is temporary and i can address this later.

doSection.getApiCallSection()
.addHeaders(
Map.of(
CompatibleConstants.COMPATIBLE_ACCEPT_HEADER,
compatibleHeader,
CompatibleConstants.COMPATIBLE_CONTENT_TYPE_HEADER,
compatibleHeader
)
);
}

};
}

private static String createCompatibleHeader() {
return "application/vnd.elasticsearch+json;compatible-with=" + CompatibleConstants.COMPATIBLE_VERSION;
}


private static Map<ClientYamlTestCandidate, ClientYamlTestCandidate> getLocalCompatibilityTests() throws Exception {
Iterable<Object[]> candidates =
ESClientYamlSuiteTestCase.createParameters(ExecutableSection.XCONTENT_REGISTRY, COMPAT_TESTS_PATH);
Iterable<Object[]> candidates = ESClientYamlSuiteTestCase.createParameters(ExecutableSection.XCONTENT_REGISTRY, COMPAT_TESTS_PATH);
Map<ClientYamlTestCandidate, ClientYamlTestCandidate> localCompatibilityTests = new HashMap<>();
StreamSupport.stream(candidates.spliterator(), false)
.flatMap(Arrays::stream).forEach(o -> localCompatibilityTests.put((ClientYamlTestCandidate) o, (ClientYamlTestCandidate) o));
.flatMap(Arrays::stream)
.forEach(o -> localCompatibilityTests.put((ClientYamlTestCandidate) o, (ClientYamlTestCandidate) o));
return localCompatibilityTests;
}
}
4 changes: 4 additions & 0 deletions server/src/main/java/org/elasticsearch/Version.java
Original file line number Diff line number Diff line change
Expand Up @@ -342,6 +342,10 @@ public boolean isCompatible(Version version) {
return compatible;
}

public static Version minimumRestCompatibilityVersion(){
return Version.V_7_0_0;
}

@SuppressForbidden(reason = "System.out.*")
public static void main(String[] args) {
final String versionOutput = String.format(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -352,6 +352,9 @@ private void handleIncomingRequest(final HttpRequest httpRequest, final HttpChan
} catch (final RestRequest.BadParameterException e) {
badRequestCause = ExceptionsHelper.useOrSuppress(badRequestCause, e);
innerRestRequest = RestRequest.requestWithoutParameters(xContentRegistry, httpRequest, httpChannel);
} catch (final RestRequest.CompatibleApiHeadersCombinationException e){
badRequestCause = ExceptionsHelper.useOrSuppress(badRequestCause, e);
innerRestRequest = RestRequest.requestNoValidation(xContentRegistry, httpRequest, httpChannel);
}
restRequest = innerRestRequest;
}
Expand Down Expand Up @@ -384,12 +387,11 @@ private void handleIncomingRequest(final HttpRequest httpRequest, final HttpChan
}

private RestRequest requestWithoutContentTypeHeader(HttpRequest httpRequest, HttpChannel httpChannel, Exception badRequestCause) {
HttpRequest httpRequestWithoutContentType = httpRequest.removeHeader("Content-Type");
try {
return RestRequest.request(xContentRegistry, httpRequestWithoutContentType, httpChannel);
return RestRequest.requestWithoutContentType(xContentRegistry, httpRequest, httpChannel);
} catch (final RestRequest.BadParameterException e) {
badRequestCause.addSuppressed(e);
return RestRequest.requestWithoutParameters(xContentRegistry, httpRequestWithoutContentType, httpChannel);
return RestRequest.requestNoValidation(xContentRegistry, httpRequest, httpChannel);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,9 @@ public class CompatibleConstants {
/**
* TODO revisit when https://github.com/elastic/elasticsearch/issues/52370 is resolved
*/
public static final String COMPATIBLE_HEADER = "Accept";
public static final String COMPATIBLE_ACCEPT_HEADER = "Accept";
public static final String COMPATIBLE_CONTENT_TYPE_HEADER = "Content-Type";
public static final String COMPATIBLE_PARAMS_KEY = "Compatible-With";
public static final String COMPATIBLE_VERSION = "" + (Version.CURRENT.major - 1);
public static final String COMPATIBLE_VERSION = String.valueOf(Version.minimumRestCompatibilityVersion().major);

}
Loading