Skip to content

Fix NPE in RequestContextSubscriber #7235

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

Merged
merged 1 commit into from
Aug 30, 2019

Conversation

robotmrv
Copy link
Contributor

@robotmrv robotmrv commented Aug 7, 2019

RequestContextSubscriber could cause NPE if Mono/Flux.subscribe
was invoked outside of Web Context.
In addition it replaced source Context with its own without respect
to old data.
Now Request Context Data is Propagated within holder class and
it is added to existing reactor Context

Fixes gh-7228

@pivotal-issuemaster
Copy link

@robotmrv Please sign the Contributor License Agreement!

Click here to manually synchronize the status of this Pull Request.

See the FAQ for frequently asked questions.

@spring-projects-issues spring-projects-issues added the status: waiting-for-triage An issue we've not yet triaged label Aug 7, 2019
@pivotal-issuemaster
Copy link

@robotmrv Thank you for signing the Contributor License Agreement!

Copy link
Member

@rwinch rwinch left a comment

Choose a reason for hiding this comment

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

Thanks for the PR! I added feedback inline

@robotmrv
Copy link
Contributor Author

I have doubts about ServletOAuth2AuthorizedClientExchangeFilterFunction.createRequestContextSubscriber()
It creates RequestContextSubscriber on every subscribe()/block() even outside of Security or WebContext and propagates empty DataHolder.
Maybe it would be better to create RequestContextSubscriber only in case if (authentication != null || requestAttributes != null) ?

@rwinch
Copy link
Member

rwinch commented Aug 12, 2019

@robotmrv - It looks like something needs to change to me as well. The ThreadLocal should only be invoked inside of defaultRequest which is guaranteed to be invoked on the original Servlet container Thread. I'm going to let @jgrandja take a look at this since he worked on some of the cleanup in this class.

@jgrandja jgrandja added in: oauth2 An issue in OAuth2 modules (oauth2-core, oauth2-client, oauth2-resource-server, oauth2-jose) type: bug A general bug and removed status: waiting-for-triage An issue we've not yet triaged labels Aug 20, 2019
Copy link
Contributor

@jgrandja jgrandja left a comment

Choose a reason for hiding this comment

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

Thank you for these improvements @robotmrv! I've left some comments inline


// gh-7228
@Test
public void publisherWhenHooksInitAndSubscribedOutsideOfWebContextAndOutsideOfSecurityContextAndWithParentContextThenShouldNotReplaceParentContextAndShouldNotAddRequestContext() throws Exception {
Copy link
Contributor

Choose a reason for hiding this comment

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

I'm not sure about this test and the next one. It's quite low level and not directly testing the API of ServletOAuth2AuthorizedClientExchangeFilterFunction. Most tests are directly testing filter() and defaultRequest(), whereas these 2 tests are directly using the Reactor API's.

The preference would be to change these tests so they are aligned with the existing filter() tests.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The main issue was that current instrumentation affects all application
So this tests check if it is possible to performe operations on Publishers without web/sucurity context with initialized reactor instrumentation (which is ServletOAuth2AuthorizedClientExchangeFilterFunction functionality)

Copy link
Contributor

Choose a reason for hiding this comment

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

Both of these tests do not directly test the API of ServletOAuth2AuthorizedClientExchangeFilterFunction but instead are testing internal implementation details. In order to simplify the tests, please make RequestContextSubscriber package-private along with the constructor. This will allow you to test the constructed state of RequestContextSubscriber using currentContext(). Please remove these 2 tests after you have added the new ones. Thanks.

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 this PR fixes scenario without web/security context (see #7235 (comment) ) and in this scenario ExchangeFilterFunction API of ServletOAuth2AuthorizedClientExchangeFilterFunction is not used
but ServletOAuth2AuthorizedClientExchangeFilterFunction#afterPropertiesSet is used and it is part of API it is makes sense to test these scenarios without usage of ExchangeFilterFunction part of API.

Other scenarios with direct usage of ExchangeFilterFunction API are covered by existing tests.
I will make RequestContextSubscriber package-private but test will not change very much as they could not use ExchangeFilterFunction API by scenario.

These test could be moved to a new test class but I do not see point in this as they still test ServletOAuth2AuthorizedClientExchangeFilterFunction#afterPropertiesSet API.

@robotmrv
Copy link
Contributor Author

@jgrandja I've fixed codestyle according to convention standard and removed publisherWhenHooksInitAndSubscribedOutsideOfWebContextThenShouldNotThrowNpe()
Regarding other tests:

  • They check reactor Hook which is part of the ServletOAuth2AuthorizedClientExchangeFilterFunction
  • RequestContextSubscriber is low level implementation which should be checked
  • these tests cover scenarios from RequestContextSubscriber could put null value in Reactor Context #7228 affect of global reactor hook on whole application - it should not affect unrelated functionality (e.g. reactive DB init on application startup etc.)
  • I do not know how to test it better
    so I leave them as is
    If you still have concerns about them please let me know.

Copy link
Contributor

@jgrandja jgrandja left a comment

Choose a reason for hiding this comment

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

Thanks for the updates @robotmrv. I've left a couple comments inline

@@ -484,6 +494,9 @@ private ClientRequest bearer(ClientRequest request, OAuth2AuthorizedClient autho
response = requestAttributes.getResponse();
}
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication == null && request == null && response == null) {
return delegate;
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 misleading since it doesn't create and return a RequestContextSubscriber instance, but instead returns the "current" Subscriber. I think we should return null for these cases and ensure Hooks.onLastOperator is NOT called in afterPropertiesSet(). Please add a test for this as well.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Hooks.onLastOperator() - applies global lift function on subscribe()/block() and it should be better installed once per application life circle.
As Lift function is invoked only at the moment of subscribe() lazily (not at the moment of application context / Filter initialization) it is not possible to unsure is there web/security context or not.

Not sure we are on the same page with case I try to fix
One of the cases:
I want to initialize application with some data. I have my reactive service API to do it.
I make ApplicationRunner or just Application listener which invokes my reactive API on startup and call subscribe()/block().
So If my API implementation uses reactor context (e.g. reactive transactions, or just some custom implementation) with initialized hook from filter and without current fix I will get NPE, because RequestContextSubscriber will populate reactor context with null values (as there is no Web and Security Context) and this is forbidden.

Another scenario - my reactive API is invoked form RabbitMQ listener and there is no web context - as the result NPE.

So what PR fixes:

  1. it ensures that no null values are propagated to the reactor context.
  2. As reactor context recommended to be as small as possible (see javadocs) this fix reduces number of used entries from 4 to 1/0.
  3. RequestContextSubscriber replaced original context so data from other hooks could be lost. This fix ensures that RequestContextSubscriber will not replace all parent context but adds values to it.
  4. In described case RequestContextDataHolder will be created with null values and will be useless as it is not required at all for the operation (application initialization), so it is make sense not to populate reactor context with useless data because of p.2 and not to create additional Subscriber to reduce allocation and invocation stack size on each emit or currentContext()

Lift function can't return null as later we will get NPE so delegate subscriber looks fine to me as it means NOOP lift function.

@jgrandja, do you still want to return null from lift function and create RequestContextSubscriber in any case with empty RequestContextDataHolder?

@@ -146,9 +153,10 @@ public void setup() {
}

@After
public void cleanup() {
public void cleanup() throws Exception {
Copy link
Contributor

Choose a reason for hiding this comment

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

Please revert the 2 changes in cleanup() as I believe it was added as a result of the removed test.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

No.
It removes reactor Hook from the filter, i.e. makes cleanup of the global state.
So it should be better here.


// gh-7228
@Test
public void publisherWhenHooksInitAndSubscribedOutsideOfWebContextAndOutsideOfSecurityContextAndWithParentContextThenShouldNotReplaceParentContextAndShouldNotAddRequestContext() throws Exception {
Copy link
Contributor

Choose a reason for hiding this comment

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

Both of these tests do not directly test the API of ServletOAuth2AuthorizedClientExchangeFilterFunction but instead are testing internal implementation details. In order to simplify the tests, please make RequestContextSubscriber package-private along with the constructor. This will allow you to test the constructed state of RequestContextSubscriber using currentContext(). Please remove these 2 tests after you have added the new ones. Thanks.

@robotmrv
Copy link
Contributor Author

@jgrandja ,
I've updated tests, please check

Copy link
Contributor

@jgrandja jgrandja left a comment

Choose a reason for hiding this comment

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

Thanks for the updates @robotmrv! I left a couple of other minor requests.

@@ -635,6 +641,69 @@ public void filterWhenRequestAttributesNotSetAndHooksInitHooksResetThenDefaultsN
assertThat(getBody(request)).isEmpty();
}

// gh-7228
@Test
public void afterPropertiesSetWhenHooksInitAndPublisherSubscribedOutsideOfWebContextAndOutsideOfSecurityContextThenSubscriptionShouldNotThrowException() throws Exception {
Copy link
Contributor

Choose a reason for hiding this comment

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

Let's shorten this name to afterPropertiesSetWhenHooksInitAndOutsideWebSecurityContextThenShouldNotThrowException

}

@Test
public void createRequestContextSubscriberIfNecessaryWhenOutsideOfWebContextAndOutsideOfSecurityContextThenShouldReturnOriginalSubscriber() throws Exception {
Copy link
Contributor

Choose a reason for hiding this comment

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

Please rename to createRequestContextSubscriberIfNecessaryWhenOutsideWebSecurityContextThenReturnOriginalSubscriber


// gh-7228
@Test
public void requestSubscriberWhenHasRequestAndHasResponseThenShouldAddToParentContextRequestContextDataHolder() throws Exception {
Copy link
Contributor

Choose a reason for hiding this comment

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

Please rename to createRequestContextSubscriberWhenRequestResponseProvidedThenCreateWithParentContext


// gh-7228
@Test
public void requestSubscriberWhenHasAuthenticationThenShouldAddToParentContextRequestContextDataHolder() throws Exception {
Copy link
Contributor

Choose a reason for hiding this comment

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

Please rename to createRequestContextSubscriberWhenAuthenticationProvidedThenCreateWithParentContext

public void requestSubscriberWhenHasAuthenticationThenShouldAddToParentContextRequestContextDataHolder() throws Exception {
testRequestContextSubscriber(null, null, this.authentication);
}

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 also add a test where RequestContextSubscriber constructor uses parentContext

			if (parentContext.hasKey(REQUEST_CONTEXT_DATA_HOLDER)) {
				context = parentContext;

``

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@jgrandja
I've added test and renamed existing as you suggested.
please check

Copy link
Contributor

@jgrandja jgrandja left a comment

Choose a reason for hiding this comment

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

Thanks @robotmrv. I left one minor request. After you apply the update can you please rebase off master and squash commits. Thank you.

testRequestContextSubscriber(null, null, this.authentication);
}

@Test
public void createRequestContextSubscriberWhenParentContextHasDataHolderThenShouldReuseParentContext() throws Exception {
RequestContextDataHolder testValue = mock(RequestContextDataHolder.class);
Copy link
Contributor

Choose a reason for hiding this comment

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

Instead of a mock, we can just set an empty RequestContextDataHolder instance

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@jgrandja done

RequestContextSubscriber could cause NPE if Mono/Flux.subscribe()
was invoked outside of Web Context.
In addition it replaced source Context with its own without respect
to old data.
Now Request Context Data is Propagated within holder class and
it is added to existing reactor Context if Holder is not empty.

Fixes spring-projectsgh-7228
@jgrandja jgrandja added this to the 5.2.0.RC1 milestone Aug 30, 2019
@jgrandja jgrandja merged commit ffc43e0 into spring-projects:master Aug 30, 2019
@jgrandja
Copy link
Contributor

Thank you for all the updates @robotmrv. This is now in master.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
in: oauth2 An issue in OAuth2 modules (oauth2-core, oauth2-client, oauth2-resource-server, oauth2-jose) type: bug A general bug
Projects
None yet
Development

Successfully merging this pull request may close these issues.

RequestContextSubscriber could put null value in Reactor Context
5 participants