-
Notifications
You must be signed in to change notification settings - Fork 31
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
fix(credentials): store JMX session credentials in ThreadLocal #1388
fix(credentials): store JMX session credentials in ThreadLocal #1388
Conversation
Still a draft since it needs more testing to ensure it really works as expected. Once we're confident that it does then I will go about refactoring and cleaning it up before final code review. |
Test image available:
|
ex.
|
A quick and clear reproducer for the bug is to use session credentials and then try to start a recording on a target that requires credentials, for example Cryostat itself in the smoketest scenario. Before this PR queries like listing active recordings or populating the Events page should work, but attempting to start a recording will fail and present the auth dialog again. With this PR it succeeds and the recording is created as expected. |
src/main/java/io/cryostat/net/web/http/api/v2/graph/GraphQLPostHandler.java
Outdated
Show resolved
Hide resolved
0981b99
to
b44b7dd
Compare
Latest commit should work with GraphQL now. It still uses the same |
Test image available:
|
b44b7dd
to
bd3bab7
Compare
Test image available:
|
Most operations now seem to work, looks very nice! Just some things that probably still need fixing:
http --auth-type=basic --auth=user:pass :8181/api/v2.2/graphql query=@conf/my-query1.graphql X-JMX-Authorization:"Basic $(echo -n admin:adminpass123 | base64)" my-query1: query {
targetNodes {
name
target {
jvmId
}
}
} this results only vertx-fib-demo-1 returning a jvmId, even though we have passed in the jmx-auth header that applies to both demo-2 and demo-3. |
But I think everything else looks really good and feels good to use as well. As far as my testing goes, I think we can move forward with this idea. |
I think this is just going to stay broken, pretty much intentionally. The session credentials are supposed to only be used for the current client session, which in terms of the web-client means until you close the browser tab essentially, so activating rules based on session credentials would also imply deactivating rules when the session ends, and I don't think that's really a well-defined enough concept to try to support here. The backend only knows about request lifetimes anyway, not actual client sessions, so firing rule activations as soon as a valid credential is passed through a session credential header might seem a little haphazard IMO.
Interesting. The GraphQL fetchers/mutators are wrapped in a piece of code that causes each stage to be performed asynchronously by the worker thread pool - I suspect the fact that I'll see if I can figure a way around that. |
bf08ae9
to
cc74444
Compare
Test image available:
|
I think this is true but in fact the cause for the null IDs in the case you gave seems to be even simpler. The query is looking for the jvmId field of the ServiceRef object which is fetched from the Discovery database at the beginning of the request. The request is already in progress and this object loaded out of the database and into memory by the time we check for session credentials, let alone see that they are present, connect to the target, compute the jvmId, and update the database entry. The way around here is actually to query for the "live" JVM ID field that is nested under the mbeanMetrics query, this way the ID check is performed at the end of a query execution chain when we have a target connection open, rather than operating over stale data loaded from the database. Maybe the direct serviceRef jvmId should be removed from the GraphQL schema in favour of this live mbeanMetrics version. |
Test image available:
|
query {
targetNodes(filter: { annotations: ["PORT in (9094, 9095)"] }) {
name
target {
jvmId
annotations {
cryostat
}
}
mbeanMetrics {
jvmId
}
}
} |
I left the original ServiceRef jvmId in place because it's probably actually useful to have a way to retrieve it from the database, rather than requiring a JMX connection be opened to check it. We also already have integration tests that depend on that database query and the resulting null jvmId value for targets requiring credentials that aren't stored. This is just a caveat that should be mentioned somewhere. I guess in the GraphQL schema file makes sense? |
3a57213
to
08fed21
Compare
Test image available:
|
08fed21
to
c119043
Compare
Test image available:
|
Test image available:
|
…y required Signed-off-by: Andrew Azores <aazores@redhat.com>
…y required Signed-off-by: Andrew Azores <aazores@redhat.com>
…y required Signed-off-by: Andrew Azores <aazores@redhat.com>
…y required Signed-off-by: Andrew Azores <aazores@redhat.com>
…y required Signed-off-by: Andrew Azores <aazores@redhat.com>
cryostatio#1388)" This reverts commit 39695d0.
…y required Signed-off-by: Andrew Azores <aazores@redhat.com>
cryostatio#1388)" This reverts commit 39695d0.
…y required Signed-off-by: Andrew Azores <aazores@redhat.com>
cryostatio#1388)" This reverts commit 39695d0.
cryostatio#1388)" This reverts commit 39695d0.
…y required Signed-off-by: Andrew Azores <aazores@redhat.com>
cryostatio#1388)" This reverts commit 39695d0.
cryostatio#1388)" This reverts commit 39695d0.
cryostatio#1388)" This reverts commit 39695d0.
cryostatio#1388)" This reverts commit 39695d0.
cryostatio#1388)" This reverts commit 39695d0.
* Revert "fix(credentials): store JMX session credentials in ThreadLocal (#1388)" This reverts commit 39695d0. * compile fixes Signed-off-by: Andrew Azores <aazores@redhat.com> * fixup! Revert "fix(credentials): store JMX session credentials in ThreadLocal (#1388)" --------- Signed-off-by: Andrew Azores <aazores@redhat.com>
* Revert "fix(credentials): store JMX session credentials in ThreadLocal (#1388)" This reverts commit 39695d0. * compile fixes Signed-off-by: Andrew Azores <aazores@redhat.com> * fixup! Revert "fix(credentials): store JMX session credentials in ThreadLocal (#1388)" --------- Signed-off-by: Andrew Azores <aazores@redhat.com> (cherry picked from commit eaf801a)
* Revert "fix(credentials): store JMX session credentials in ThreadLocal (#1388)" This reverts commit 39695d0. * compile fixes Signed-off-by: Andrew Azores <aazores@redhat.com> * fixup! Revert "fix(credentials): store JMX session credentials in ThreadLocal (#1388)" --------- Signed-off-by: Andrew Azores <aazores@redhat.com> (cherry picked from commit eaf801a) Co-authored-by: Andrew Azores <aazores@redhat.com>
Welcome to Cryostat! 👋
Before contributing, make sure you have:
main
branch[chore, ci, docs, feat, fix, test]
git commit --amend --signoff
Fixes: cryostatio/cryostat-web#656
Description of the change:
This adds a
ThreadLocal
to theCredentialsManager
containing aMap<String, Credentials>
. TheString
is just thetargetId
of the current request, if any, and theCredentials
are likewise the corresponding session credentialsX-JMX-Authorization
extracted from the API request context, if any. TheCredentialsManager
, and for now (needs refactor cleanup) theConnectionDescriptor
, reference thisThreadLocal
to check for any session-provided credentials. If they are present then they are used and preferred over backend stored credentials. Once the current request is completed these credentials are cleared out of theThreadLocal Map
. This map is only expected to contain a singleton value currently and should only ever have one key corresponding to the current request'stargetId
URL parameter, but for testing purposes and request safety I used theMap
to ensure credentials are not accidentally spilling between calls.This concept is pretty standard in the "thread-per-request" model used by many webservers, where a new thread is spawned for each incoming client request, and that thread handles the entire execution lifespan, finally terminating after the response has been written. Here we use Vert.x which is reactive and processes requests using a main verticle thread and a worker pool. Most of our API handlers, in particular ones that may handle passthrough credentials, are handled by a worker in the Vert.x threadpool. Which exact thread handles the request is not predictable, but and depending on the exact execution path of that request, the thread may end up dropping the request and another thread (the same or another) may pick up the request later, if the request call stack contains something like
vertx.executeBlocking()
ortargetConnectionManager.executeConnectedAsync()
, either of which push the next unit of executable code into a work queue to be picked up by a worker thread. If the request execution is handed off between threads in this way then the session credentials held in theThreadLocal
will not be accessible and will appear to benull
to the thread that picks up the request. If, however, all of these asynchronous delegation calls happen late in the call stack, ie after the point where the original thread has saved the session credentials toThreadLocal
and then picked them back up again for the last time, then this does not matter.Motivation for the change:
This allows API requests using the old JMX passthrough authentication mechanism, the
X-JMX-Authorization
header, to work as expected again including for requests that trigger executions in the stack in other classes like theJvmIdHelper
that do not have a direct call from the API handler that can pass through theRoutingContext
/RequestParameters
object directly so that the session credentials can be extracted at the call site. As explained above, if all of the requests are structured and executed in the right way, then the session credentials can be stored in a "global"ThreadLocal
so that as the worker thread processes the request, the session credentials can be retrieved from any point in the call stack, without having to pass theRoutingContext
object through as a method call argument the whole way through the stack. For reviewers reading this, this can be thought of as somewhat analogous to a React<Context>
where pieces of state are available to descendant components anywhere below the provider in the component hierarchy. The net effect, if the above assumptions hold, is that the passthrough credentials mechanism works as expected again, without any significant refactoring of the internal interfaces to allow passing theRoutingContext
through every layer of the call stack, and without resorting to temporary storage of session credentials in the encrypted keyring database.How to manually test:
web-client
or usecurl
/httpie
and exercise as many API calls as possible, without using stored credentials. In theweb-client
this would mean deleting any previously stored credentials, going to/settings
->Advanced
and setting the credentials storage toSession (Browser Memory)
. Use browser dev tools to inspect outgoing network requests for ex. active recordings queries and ensure that theX-JMX-Authorization
credentials passthrough header is included in the request and that the request succeeds.