-
Notifications
You must be signed in to change notification settings - Fork 8.2k
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
Add API to refresh authc headers and retry ES request when 401 is encountered #120677
Add API to refresh authc headers and retry ES request when 401 is encountered #120677
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Discussion items and remarks about the design and the proposed implementation
src/core/server/http/types.ts
Outdated
getAuthHeaders: GetAuthHeaders; | ||
setAuthHeaders: SetAuthHeaders; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As @mshustov already mentioned on the issue, now that we need to update the headers from outside of the http/login lifecycle, it would probably make sense to extract parts of the authc from the http
service. Maybe a new auth
internal service?
To start simple though, I was thinking of extracting all the Request->AuthState storages from httpServer, or at least to create a higher-level API/storage to keep/isolate them in a single place. We could KISS and keep this new AuthStorage
in the http server/service for now, and just expose it on the internal http contract to be used by elasticsearch
service.
The AuthStorage
would be responsible to keep the state of what is currently stored in 3 different objects:
kibana/src/core/server/http/http_server.ts
Lines 101 to 103 in 815ab04
private readonly authState: AuthStateStorage; | |
private readonly authRequestHeaders: AuthHeadersStorage; | |
private readonly authResponseHeaders: AuthHeadersStorage; |
export interface ElasticsearchServiceSetup { | ||
/** | ||
* TODO: doc | ||
*/ | ||
setUnauthorizedErrorHandler: (handler: UnauthorizedErrorHandler) => void; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The entrypoint for the new API, to be consumed by the security
plugin.
@elastic/kibana-security could you check the signature of the API (and of the UnauthorizedErrorHandler
types), and tell us if the API seems acceptable for your needs?
As discussed sync on slack, and explained on the issue's description, the handler currently only allow to update the authRequestHeaders
(and not authResponseHeaders
or authState
)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
TL;DR: the API looks good to me, thanks!
As I mentioned in Slack, we can safely ignore authResponseHeaders
since we use it only during Kerberos "handshake" that we don't want to perform in this case.
I also confirmed with the ES security team that during refresh the user info (== authState
, Kibana Security plugin leverages the core's authState
whenever it wants to know who the current user is and what roles they have, it's also a part of public Security plugin API that other plugins rely on) should not change for Token, SAML, OIDC and Kerberos authentication (it's cached in .security-tokens
index) which is great.
The only case when user info can potentially change during refresh is PKI authentication since instead of using refresh token (we don't have one for PKI) we exchange existing client certificate to a new token. We know that certificate is the same (otherwise we should have caught that during authentication stage), and the chance that the role mappings have changed during access token lifetime (max 60 minutes) is very low.
Even though it'd be nice to update authState
during refresh as well (for the consistency at least), considering all the above it feels safe enough to ignore it assuming it requires significant effort to support (is it correct assessment?).
updateHeaders(headers: AuthHeaders) { | ||
this[kHeaders] = { ...this[kHeaders], ...(headers as IncomingHttpHeaders) }; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When the handler returns a retry
result with updated authorization headers, I update the Client's headers (using the workaround from @delvedor #108346 (comment))
Note that I currently keep the existing headers and just spread the new ones. I wonder if we should rerun the ClusterClient.getScopedHeaders
logic and then totally replace the client's headers instead?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I wonder if we should rerun the ClusterClient.getScopedHeaders logic and then totally replace the client's headers instead?
I don't think it's safe. elasticsearch-js
can set client-specific headers. Here it sets headers during the instantiation process.
https://github.com/elastic/elasticsearch-js/blob/main/src/client.ts#L202
https://github.com/elastic/elastic-transport-js/blob/main/src/Transport.ts#L228
Maybe Transport
can support updateHeaders
API in the future? @delvedor
try { | ||
return (await super.request(params, opts)) as TransportResult<any, any>; | ||
} catch (e) { | ||
if (isUnauthorizedError(e) && unauthorizedErrorHandler) { | ||
const result = await unauthorizedErrorHandler(e); | ||
if (isRetryResult(result)) { | ||
const headers = result.authHeaders; | ||
this.updateHeaders(headers); | ||
return (await super.request(params, opts)) as TransportResult<any, any>; | ||
} | ||
} | ||
throw e; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is the logic of the retry mechanism implemented directly into the client's Transport
.
const transportClass = createTransport({ | ||
getExecutionContext: this.getExecutionContext, | ||
unauthorizedErrorHandler: internalErrorHandler, | ||
}); | ||
|
||
const scopedClient = this.rootScopedClient.child({ | ||
headers: scopedHeaders, | ||
Transport: transportClass, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Due to each client being bound to a specific request, we're forced to create a new transport class for every scoped client. However I don't think a class
is treated differently than any object in javascript, so I don't think this is a danger for memory usage or leak.
As a side note, I think in the final implementation we may want to move the logic related to the scoped client (both the part that was already present such as getScopedHeaders
and the new logic introduced in this PR) from ClusterClient
to ScopedClusterClient
for better isolation of concerns.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
so I don't think this is a danger for memory usage or leak.
The connection pool is shared among child clients, so you won't leak file descriptors.
if (!isRealRequest(request)) { | ||
return () => toolkit.notHandled(); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This part is to address
Only when request to Elasticsearch is attributed with Authorization header provided by the AuthenticationHandler (the one registered via registerAuth hook). If consumers override client's Authorization header with FakeRequest and alike we shouldn't interfere.
Only when request isn't made by the Security plugin itself (otherwise we can get into infinite recursion 🙂 ). This will be probably solved automatically by the solution for the previous point since we always override Authorization header in security plugin. But if not, maybe we'd need a special TransportRequestOptions option?
from the initial requirements expressed here: #104893 (comment)
@elastic/kibana-security Could you confirm (or infirm) that disabling the handler IIF the request used for the scoped client is not a real request (KibanaRequest
) would be sufficient?
If it's not, could you provide the logic we want to use to disable the handler depending on the request? We would ideally have this condition ONLY depends on the request (and not on the state of the request auth headers (httpServer.authRequestHeaders
) associated to it. But if required, I can adapt the code to also access the auth storage from this function.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@elastic/kibana-security Could you confirm (or infirm) that disabling the handler IIF the request used for the scoped client is not a real request (KibanaRequest) would be sufficient?
If the proposed solution works for the Kibana Security team, 👍 from me. That was not the simplest logic to implement 😅
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, I believe it should be enough, here is the only place I'm aware of where we'd want to prevent automatic retry logic and we don't use real request there (.asScoped({ headers: { ...request.headers, ...authHeaders } })
).
} | ||
return async (error) => { | ||
try { | ||
const result = await handler({ request, error }, toolkit); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure this is really an issue, but I need to mention it: we're now keeping a reference to request
within this closure/handler, meaning that we keep a reference between the ScopedClusterClient
and the associated request. Previously it wasn't the case, as the request was just used to generate the headers via getScopedHeaders
and then the reference could be GC'ed as the end of ClusterClient.asScoped
.
This means that any reference kept on the instance of the ClusterClient
that was created to instantiate the ScopedClusterClient
via asScoped
will block the GC from disposing of the request object. In theory, it shouldn't be an issue as these scoped clients are supposed to be kept only for the duration of a request (created in the request handler, meaning that it can be disposed at the end of the execution of the handler).
I wonder if we should use a WeakRef
internally, as done with WeakMap
for AuthHeadersStorage
, just to be safe?
Now, if we were to do that, that would we do if the request has been GC'ed when entering the handler? Just return notHandled
as we do if !isRealRequest(request)
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In theory, it shouldn't be an issue as these scoped clients are supposed to be kept only for the duration of a request (created in the request handler, meaning that it can be disposed at the end of the execution of the handler).
I think we designed most of our API relying on this condition.
if (isRetryResult(result)) { | ||
setAuthHeaders(request, result.authHeaders); | ||
} | ||
return result; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These 3 lines are probably the most important part. This createInternalErrorHandler
function converts the UnauthorizedErrorHandler
as registered by our API consumer (e.g security
) to the InternalUnauthorizedErrorHandler
used by the ES client.
When the client encounter a 401, it will call the internal handler, which will call the registered handler, and then, if the result is a retry
, it will update the auth headers (httpServer.authRequestHeaders
) before forwarding the result to the ES client.
The ES client, in case of retry
, will then update its headers and perform the request again.
try { | ||
const result = await handler({ request, error }, toolkit); | ||
if (isRetryResult(result)) { | ||
setAuthHeaders(request, result.authHeaders); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
setAuthHeaders
just updates the authRequestHeaders
entry for the given request with the returned headers.
I initially adapted AuthHeadersStorage
to hold an observable of the AuthHeaders
-- private authHeadersCache = new WeakMap<Request, AuthHeaders>();
++ private authHeadersCache = new WeakMap<Request, Observable<AuthHeaders>>();
to allow reactively responding to auth headers changes. This would have potentially allowed to have multiple scoped clients bound to the same request to reactively update their headers when any client is updated.
e.g (not working in current implementation)
const client1 = clusterClient.asScoped(request);
const client2 = clusterClient.asScoped(request);
// causing the 401 scenario because of expired access token
const responseReturnedAfterRetry = await client1.search({});
// client2 should ideally already be updated with the new headers and avoid to retrigger the 401
await client2.search({});
However, I couldn't find any other scenario where being able to subscribe to auth headers change would provide any value, so I reverted the change to remove the increased complexity.
WDYT? Do we have a reason/need to change the AuthHeadersStorage
implementation to hold an observable instead of only the current value of the auth headers?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Considering #108346 (comment). I'd say we can live with it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Another solution to avoid playing with the internal symbols is to keep track of the headers in your own custom transport and pass them via transport request options when needed.
eg:
class KibanaTransport extends Transport {
constructor (opts) {
super(opts)
this.kibanaHeaders = {}
}
async request (params, options = {}) {
options.headers = { ...options.headers, ...this.kibanaHeaders }
try {
return await super.request(params, options)
} catch (err) {
// update this.kibanaHeaders if needed
}
}
}
Yea, I thought about this after my initial POC, and that's probably the way to go. We'll need to remove the headers passed in the constructor to the parent's call though constructor (opts) {
const { headers, ...superOpts } = opts
super(superOpts)
this.kibanaHeaders = headers ?? {};
} but this avoid messing with internals of the |
src/core/server/http/types.ts
Outdated
@@ -399,6 +399,7 @@ export interface InternalHttpServiceSetup | |||
registerRouterAfterListening: (router: IRouter) => void; | |||
registerStaticDir: (path: string, dirPath: string) => void; | |||
getAuthHeaders: GetAuthHeaders; | |||
setAuthHeaders: SetAuthHeaders; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe it is simpler to pass a single AuthHeadersStorage
interface instead of passing its methods?
We can continue keeping the Auth service in Http for time being.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yea, just waiting for @elastic/kibana-security to confirm that only updating the request headers is sufficient (no response headers and no auth state), in which case I agree that introducing a IAuthHeadersStorage
interface and passing it down to the ES service seems the most pragmatic solution.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Replied in https://github.com/elastic/kibana/pull/120677/files#r768617303. If updating authState
in addition to authRequestHeaders
requires significant changes or just too risky, we can move forward with just updating authRequestHeaders
.
setAuthHeaders: SetAuthHeaders; | ||
}): InternalUnauthorizedErrorHandler => { | ||
// we don't want to support 401 retry for fake requests | ||
if (!isRealRequest(request)) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we keep this logic isolated in auth
service? it rather belongs there.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Well, this is impacting the ES client's behavior, as we need to totally disable the logic and not retry the 401 in case the provided request is not a 'real' one, so I don't really see a way to isolate this in the auth/http service.
We could eventually just check perform this check in ClusterClient.asScoped
, and, if it's a fake request, just not create the handler at all, but this will be be done inside the elasticsearch's service code.
Or do you have a suggestion?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I was thinking about something as simple as:
class Auth {
canBeReAuthenticated(request) {
return isRealRequest(request)
}
}
} | ||
return async (error) => { | ||
try { | ||
const result = await handler({ request, error }, toolkit); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In theory, it shouldn't be an issue as these scoped clients are supposed to be kept only for the duration of a request (created in the request handler, meaning that it can be disposed at the end of the execution of the handler).
I think we designed most of our API relying on this condition.
try { | ||
const result = await handler({ request, error }, toolkit); | ||
if (isRetryResult(result)) { | ||
setAuthHeaders(request, result.authHeaders); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Considering #108346 (comment). I'd say we can live with it.
this.allowListHeaders = ['x-opaque-id', ...this.config.requestHeadersWhitelist]; | ||
} | ||
|
||
asScoped(request: ScopeableRequest) { | ||
const scopedHeaders = this.getScopedHeaders(request); | ||
|
||
const internalErrorHandler = this.unauthorizedErrorHandler |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we create the handler lazily? We really need it only when 401
happened.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We probably can if we want to. Do you have any concern creating is eagerly? It doesn't seems like a costly operation?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's not super expensive, but asScoped
might be considered as a hot path because Security uses the scoped client(s) on every incoming request. But Kibana will need internalErrorHandler
handler very rarely only when 401
happens after auth
lifecycle.
updateHeaders(headers: AuthHeaders) { | ||
this[kHeaders] = { ...this[kHeaders], ...(headers as IncomingHttpHeaders) }; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I wonder if we should rerun the ClusterClient.getScopedHeaders logic and then totally replace the client's headers instead?
I don't think it's safe. elasticsearch-js
can set client-specific headers. Here it sets headers during the instantiation process.
https://github.com/elastic/elasticsearch-js/blob/main/src/client.ts#L202
https://github.com/elastic/elastic-transport-js/blob/main/src/Transport.ts#L228
Maybe Transport
can support updateHeaders
API in the future? @delvedor
ACK: reviewing... |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The public core API proposed in this PR should cover what Security
plugin would need to refresh access token after authentication stage, I've left a few comments with more details.
Thanks!
src/core/server/http/types.ts
Outdated
@@ -399,6 +399,7 @@ export interface InternalHttpServiceSetup | |||
registerRouterAfterListening: (router: IRouter) => void; | |||
registerStaticDir: (path: string, dirPath: string) => void; | |||
getAuthHeaders: GetAuthHeaders; | |||
setAuthHeaders: SetAuthHeaders; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Replied in https://github.com/elastic/kibana/pull/120677/files#r768617303. If updating authState
in addition to authRequestHeaders
requires significant changes or just too risky, we can move forward with just updating authRequestHeaders
.
} | ||
return async (error) => { | ||
try { | ||
const result = await handler({ request, error }, toolkit); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
question: I see that right now the request that is being passed to handler
contains auth headers (Authorization: xxx
) that were set by the Core and weren't present in the original request. Would it be possible to "unset" these headers before passing request object to the handler
?
Right now Security basically skips such requests since it assumes that it was the client who specified authentication headers explicitly. We can try to workaround this in Security plugin and unset these headers on our own (not sure if it's possible since headers
is readonly
), but I'm wondering if you have a better way of passing original "pristine" request object instead?
Update: that actually happens because we mutate request with Authorization
header after authentication stage for BWC reasons. Hmmm, I'll experiment with this a bit more to see if there anything we can do on our end.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Did you progress on your investigations?
As you said, the mutation is done here:
kibana/src/core/server/http/http_server.ts
Lines 447 to 452 in 8ffc872
if (requestHeaders) { | |
this.authRequestHeaders.set(req, requestHeaders); | |
// we mutate headers only for the backward compatibility with the legacy platform. | |
// where some plugin read directly from headers to identify whether a user is authenticated. | |
Object.assign(req.headers, requestHeaders); | |
} |
But I don't see any way to differentiate headers that have been added by this snippet from other headers, so apart from just removing this part of the code (which would likely break some stuffs relying on these headers to forge fake requests, right?) I'm not sure how we could answer your ask?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Did you progress on your investigations?
I can think of only one clumsy hack on Security side: https://github.com/elastic/kibana/pull/122155/files#diff-df4d732995701a873f86257177d6440fa562a820a0c849624cc1e54de2bbfbf1R238-R244 (Object.fromEntries(Object.entries(...))
is needed because I cannot just remove authorization
header from the headers
object since it's readonly
).
(request.headers as Record<string, unknown>) = Object.fromEntries(
Object.entries(request.headers).filter(
([headerName]) => headerName.toLowerCase() !== 'authorization'
)
);
so apart from just removing this part of the code (which would likely break some stuffs relying on these headers to forge fake requests, right?
Yeah, reporting (and probably Upgrade Assistant in some cases) still depends on this I believe.
But I don't see any way to differentiate headers that have been added by this snippet from other headers
I was thinking that you might have a copy of the auth headers returned during authentication stage so that you know which headers are original and which were added as a part of the authentication flow, or it's not the case?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I was thinking that you might have a copy of the auth headers returned during authentication stage so that you know which headers are original and which were added as a part of the authentication flow, or it's not the case?
Nope, not really.
Well, we do set the headers returned from the auth hook inside the authRequestHeaders
map, so in a way, we can identify which entries in req.headers
were injected by looking at the keys of the associated entry in authRequestHeaders
, but:
- This will be quite ugly if we do that from the ES error interception logic. We're already leaking responsibilities all over the place, and it will just increase it.
- In case of overrides (e.g a header returned from the login hook overriding a header initially present on the request), we won't be able to provide the initial value of such headers.
KibanaRequest
is immutable, and we won't be able to easily recreate one to provide it to the hook
In addition, the login
HAPI hook is performed very early, before we actually wrap HAPI's request to a KibanaRequest
, so if we want to store these original headers somewhere, we'll need to do that (also) directly on the base request. We would be forced to do something like
diff --git a/src/core/server/http/http_server.ts b/src/core/server/http/http_server.ts
--- a/src/core/server/http/http_server.ts (revision f6370f413172821ea8e06dfc0d80ff5b5a6e90bd)
+++ b/src/core/server/http/http_server.ts (date 1641814909929)
@@ -448,6 +448,7 @@
this.authRequestHeaders.set(req, requestHeaders);
// we mutate headers only for the backward compatibility with the legacy platform.
// where some plugin read directly from headers to identify whether a user is authenticated.
+ req.app.originalHeaders = { ...req.headers };
Object.assign(req.headers, requestHeaders);
}
}
and to expose these originalHeaders
as a new property of KibanaRequest
.
This is doable, but I would take any other alternative if possible to be honest.
@mshustov wdyt?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This will be quite ugly if we do that from the ES error interception logic. We're already leaking responsibilities all over the place, and it will just increase it.
Yeah, I see. It feels like there is no way to avoid an ugly workaround of some sort, it's just a matter where it will live. I'm fine with keeping it in Security if there is no elegant way to accomplish this directly in the Core.
In case of overrides (e.g a header returned from the login hook overriding a header initially present on the request), we won't be able to provide the initial value of such headers.
It's something that should never happen, at least Security will never override existing headers.
KibanaRequest is immutable, and we won't be able to easily recreate one to provide it to the hook
If we agree to keep workaround (e.g. rewriting request.headers
), is it safe to assume that request.headers
will stay writable?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
is it safe to assume that request.headers will stay writable?
KibanaRequest.headers
is defined as readonly
kibana/src/core/server/http/router/request.ts
Line 156 in 338fe1a
public readonly headers: Headers; |
and its value is frozen
kibana/src/core/server/http/router/request.ts
Line 198 in 338fe1a
this.headers = deepFreeze({ ...request.headers }); |
however as discussed sync, the request.headers
property is not technically frozen and can be overriden if forcecasted.
Pinging @elastic/kibana-core (Team:Core) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The API shape looks good to me, the WIP base integration test suite is passing with this PR as well.
As we agreed, we'll be filtering out authorization
header from the request instance before re-authentication in the Security plugin for now. Once we stop mutating requests with the headers returned at the authentication stage, we'll be able to get rid of this workaround.
Thanks!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The overall design looks good. Is it possible to add an integration test to make sure everything works as expected?
// add stored headers to the options | ||
opts.headers = { | ||
...this.headers, | ||
...optionHeaders, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: we copy opts.headers
twice.
...optionHeaders, | |
...opts.headers, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I had incredible issues in unit tests because of the way the client keep references / mutates the headers that forced me to do that to assert against the values of the used headers for each individual call. but I will try again.
); | ||
}); | ||
|
||
it('merges the headers from the constructor and from the options', async () => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
could you add a test that request
method doesn't mutate the arguments?
We talked about it with @azasypkin and agreed that it would make more sense (and would be way easier) to add such tests in the follow-up PR from the security team that will actually leverage the feature. The FTR tests are ready (and passing) in #122155 |
Will be backported as it is required to address #104893 that is a bug fix |
💚 Build Succeeded
Metrics [docs]Public APIs missing comments
History
To update your PR or re-run it, just comment with: |
💔 All backports failed
How to fixRe-run the backport manually:
Questions ?Please refer to the Backport tool documentation |
…ountered (elastic#120677) * initial POC * remove test code * update the header holding logic * add new API to plugin context * introduce the IAuthHeadersStorage interface * fix some types, mocks and tests * export types from server entrypoint * also export error type * more doc * update generated doc * Fix ES service tests * add tests for createInternalErrorHandler * fix type in cli_setup * generated doc * add tests for configureClient * add unit tests for custom transport class * fix handler propagation to initial clients * lint * address review comments (cherry picked from commit b606054) # Conflicts: # src/core/server/http/auth_headers_storage.ts
…ountered (elastic#120677) * initial POC * remove test code * update the header holding logic * add new API to plugin context * introduce the IAuthHeadersStorage interface * fix some types, mocks and tests * export types from server entrypoint * also export error type * more doc * update generated doc * Fix ES service tests * add tests for createInternalErrorHandler * fix type in cli_setup * generated doc * add tests for configureClient * add unit tests for custom transport class * fix handler propagation to initial clients * lint * address review comments (cherry picked from commit b606054) # Conflicts: # src/cli_setup/utils.ts # src/core/server/elasticsearch/client/cluster_client.ts # src/core/server/elasticsearch/client/configure_client.ts # src/core/server/elasticsearch/client/errors.ts # src/core/server/elasticsearch/elasticsearch_service.ts # src/core/server/elasticsearch/index.ts # src/core/server/http/auth_headers_storage.ts # src/core/server/index.ts # src/core/server/server.api.md
Tried to backport to |
…is encountered (#120677) (#123227) * Add API to refresh authc headers and retry ES request when 401 is encountered (#120677) * initial POC * remove test code * update the header holding logic * add new API to plugin context * introduce the IAuthHeadersStorage interface * fix some types, mocks and tests * export types from server entrypoint * also export error type * more doc * update generated doc * Fix ES service tests * add tests for createInternalErrorHandler * fix type in cli_setup * generated doc * add tests for configureClient * add unit tests for custom transport class * fix handler propagation to initial clients * lint * address review comments (cherry picked from commit b606054) # Conflicts: # src/core/server/http/auth_headers_storage.ts * try to please typescript
Summary
POC to discuss design and implementation of #108346 (and related to #104893)Fix #108346
Add a new
setUnauthorizedErrorHandler
public API toElasticsearchServiceSetup
, to allow registering a handler that will be invoked when a scoped ES client returns a 401 for any API call.The handler can return 2 results:
retry
ornotHandled
When a
retry
result is returned:authRequestHeaders
bound to the scoped client's request with the returned headersClient
's headers with the returned headers and try the request againWhen a
notHandled
result is returned:If the handler throws or rejects, we behave as if
notHandled
was returned.Usage would look like: