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

Conversation

pgomulka
Copy link
Contributor

@pgomulka pgomulka commented Mar 24, 2020

Adding validation of Accept And Content-Type headers. The idea is to verify combinations of presence and values of both headers depending if a request has a content, headers are versioned (type/vnd.elasticsearch+subtype;compatible-with) .
It also changes the way media type is parsed (previously always assuming application as a type i.e application/json) It should expect different types like text - used in SQL
Not adding a compatible headers for _cat api. These in order to return a default text do not expect Accept header (Content-type is not needed as content is not present)

See #53228 (comment) for more context

pgomulka and others added 29 commits March 5, 2020 12:44
1226 tests, 227 failures, 16ignored
23 / 2 failled in index/*
failing
CompatRestIT. test {yaml=index/70_mix_typeless_typeful/Index call that
introduces new field mappings}
CompatRestIT. test {yaml=index/70_mix_typeless_typeful/Index with
typeless API on an index that has types}
Introduce module for compat code
This reverts commit ae1799f, reversing
changes made to f00f438.
When a request to a compatible api contains a body it will require a
content-type header to also contain a compatible media-type - in a form
application/vnd.elasticsearch+json;compatible-with=7
When there is no body - it will not be required.
Accept header is always required for compatible API
@pgomulka pgomulka added the :Core/Infra/Core Core issues without another label label Mar 24, 2020
@pgomulka pgomulka changed the title WIP compatible api headers Validate Accept and Content-Type header for compatible API Apr 9, 2020
@pgomulka pgomulka marked this pull request as ready for review April 9, 2020 13:01
Copy link
Contributor

@jakelandis jakelandis left a comment

Choose a reason for hiding this comment

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

I think the compatibleWithVersion can be simplified to a return a boolean and only care about version (not media types). Also, the exception for where it is thrown is resulting in a closed connection without a proper response code.

@@ -80,6 +80,7 @@
public static final Version V_7_8_0 = new Version(7080099, org.apache.lucene.util.Version.LUCENE_8_5_0);
public static final Version V_8_0_0 = new Version(8000099, org.apache.lucene.util.Version.LUCENE_8_5_0);
public static final Version CURRENT = V_8_0_0;
public static final Version PREVIOUS = V_7_0_0;
Copy link
Contributor

Choose a reason for hiding this comment

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

This is a bit ambiguous, and probably not a necessary as a static constant.

Can we add in a method like minimumRestCompatibilityVersion ?

Copy link
Member

Choose a reason for hiding this comment

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

In doing so, can you please make the method static? It should only apply to the CURRENT version. The fact the other compatibility methods are on Version instances makes testing and modifying the rules difficult, even though in practice the CURRENT version's compatibility is all we need, and the others are just artificial for testing.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

agree, the method should be static

badRequestCause = ExceptionsHelper.useOrSuppress(badRequestCause, e);
//todo // tempting to just rethrow. removing content type is just making this less obvious
throw e;
// innerRestRequest = requestWithoutContentTypeHeader(httpRequest, httpChannel, badRequestCause);
Copy link
Contributor

Choose a reason for hiding this comment

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

can you clean up the this todo abit ? Not sure I follow.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

just throwing an exception was helping checking the logs, but indeed was breaking the channel creation and there was no response sent back..
will fix

return Version.CURRENT;
}
// both headers are versioned but set to incorrect version
throw new CompatibleApiHeadersCombinationException(
Copy link
Contributor

Choose a reason for hiding this comment

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

Throwing the exception here results in an abrupt termination of the connection (no error code or proper response). Not sure the fix, but we should add a REST test to ensure the error code.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I think that we allow RestRequest to be in an incorrect state. When creating a request and it fails due to Content-Type validation https://github.com/elastic/elasticsearch/blob/master/server/src/main/java/org/elasticsearch/rest/RestRequest.java#L93 or parameter validation https://github.com/elastic/elasticsearch/blob/master/server/src/main/java/org/elasticsearch/rest/RestRequest.java#L145
we try create again without either content-type or without parameters.

This makes it harder to create a validation of headers.. we would need to allow to have an incorrect state that would be used for channel creation in order to response back..

private boolean isRequestCompatible() {
return isHeaderCompatible(header(CompatibleConstants.COMPATIBLE_HEADER));
}
private Version compatibleWithVersion() {
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 can be greatly simplified and read easier if this returns a boolean and only concerns itself with the version (not the supported media types (that can happen later if need be)).

I think something like this below the same level of validation.

    private void addCompatibleParameter() {
        if (isRequestingCompatibility()) {
            params().put(CompatibleConstants.COMPATIBLE_PARAMS_KEY, String.valueOf(Version.CURRENT.major - 1));
            param(CompatibleConstants.COMPATIBLE_PARAMS_KEY);
        }
    }

    private boolean isRequestingCompatibility() {
        String acceptHeader = header(CompatibleConstants.COMPATIBLE_ACCEPT_HEADER);
        String aVersion = XContentType.parseVersion(acceptHeader);
        byte acceptVersion = aVersion == null ? Version.CURRENT.major : Integer.valueOf(aVersion).byteValue();
        String contentTypeHeader = header(CompatibleConstants.COMPATIBLE_CONTENT_TYPE_HEADER);
        String cVersion = XContentType.parseVersion(contentTypeHeader);
        byte contentTypeVersion = cVersion == null ? Version.CURRENT.major : Integer.valueOf(cVersion).byteValue();

        if(Version.CURRENT.major < acceptVersion || Version.CURRENT.major - acceptVersion > 1 ){
            throw new IllegalStateException("something about only 1 major version support");
        }

        if (hasContent()) {
            if(Version.CURRENT.major < contentTypeVersion || Version.CURRENT.major - contentTypeVersion > 1 ){
                throw new IllegalStateException("something about only 1 major version support");
            }

            if (contentTypeVersion != acceptVersion) {
                throw new IllegalStateException("something about needing to match");
            }
        }

        return contentTypeVersion < Version.CURRENT.major || acceptVersion < Version.CURRENT.major;
    }

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.

));
});
// 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.

@@ -121,4 +121,21 @@ public void testVersionParsing() {
assertThat(XContentType.parseVersion("APPLICATION/JSON"),
nullValue());
}

public void testVersionParsingOnText() {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

as per my comment https://github.com/elastic/elasticsearch/pull/54103/files#r409392721
maybe we should also extend parsing media types that are text? Should we unify this with SQL?
see RestSqlQueryAction at the moment sql has to use format parameter
cc @astefan

Copy link
Contributor

Choose a reason for hiding this comment

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

Can we postpone that decision for now and work and in future work ensure that text based APIs are good ?

Copy link
Contributor

@jakelandis jakelandis left a comment

Choose a reason for hiding this comment

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

The changes here represent some really good progress and the code LGMT. Lets look to getting this merged into the feature branch.

@pgomulka pgomulka merged commit 3b3da9d into elastic:compat_rest_api Apr 20, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
:Core/Infra/REST API REST infrastructure and utilities
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants