Unify response interface in handler and request interceptors#42442
Unify response interface in handler and request interceptors#42442mshustov merged 17 commits intoelastic:masterfrom
Conversation
Only route handler can respond with 2xx response. Interceptors may redirect or reject an incoming request.
|
Pinging @elastic/kibana-platform |
| registerRouter(router); | ||
|
|
||
| registerOnPreAuth((req, res, t) => | ||
| res.redirected(undefined, { |
There was a problem hiding this comment.
probably we can even go further and pass body and request options as one argument
return res.redirected({ headers: .... })
return res.ok({
body: {},
headers: ....
})@elastic/kibana-platform
There was a problem hiding this comment.
Makes sense to me to make this API a bit less awkward. Do you think we should do that for all of these methods for consistency?
There was a problem hiding this comment.
Do you think we should do that for all of these methods for consistency?
Yes. I can do this in a separate PR.
| .get('/') | ||
| .expect(200, 'ok'); | ||
|
|
||
| expect(callingOrder).toEqual(['first', 'second']); |
There was a problem hiding this comment.
I focused more on the integration tests to analyze the observable behavior of the system. Otherwise, our tests are tightly coupled with hapi library.
|
|
||
| private toError(kibanaResponse: KibanaResponse<ResponseError>) { | ||
| const { payload } = kibanaResponse; | ||
| return this.responseToolkit |
There was a problem hiding this comment.
had to switch to Boom error. hapi doesn't allow to respond from lifecycle event handlers. even for error responses 😞
| } catch (err) { | ||
| authLogger.error(err); | ||
| return t.rejected(wrapError(err)); | ||
| return response.unauthorized(wrapError(err)); |
There was a problem hiding this comment.
that's not 1-1 mapping. Before we passed on all ES error to the client side. we can add this logic to support rejecting with any error and status or always return something we have control over, say unauthorized/internal error. want to hear your opinion. @elastic/kibana-security @azasypkin
There was a problem hiding this comment.
As far as I remember returning 401 will force user to logout, I'd rather return internal server error from this catch as it's unexpected authentication flow.
There was a problem hiding this comment.
ok. to note: this changes API behavior. https://github.com/elastic/kibana/pull/42442/files#diff-5fa60b1877c33df27b85f0caac47988dR149
| } | ||
|
|
||
| type AuthResult = Authenticated | Rejected | Redirected; | ||
| type AuthResult = Authenticated; |
There was a problem hiding this comment.
It can be as simple as Authenticated. The current implementation hides details and allows us to introduce additional logic without affecting the interface.
| if (preAuthResult.isRedirected(result)) { | ||
| const { url, forward } = result; | ||
| if (forward) { | ||
| if (preAuthResult.isValid(result)) { |
There was a problem hiding this comment.
TODO: remove this check
💔 Build Failed |
eliperelman
left a comment
There was a problem hiding this comment.
Looks good from an implementation standpoint. Have a couple optional nits.
|
ack: will get to tomorrow |
| // Identity Provider for SSO authentication mechanisms. Authentication provider is the one who | ||
| // decides what location user should be redirected to. | ||
| return t.redirected(authenticationResult.redirectURL!); | ||
| return response.redirected(undefined, { |
There was a problem hiding this comment.
note: feels a bit weird to have a dedicated redirected method, but leverage optional generic headers for the required location header.... But functionally it works for us, so 🤷♂️
There was a problem hiding this comment.
I kinda agree. However, we did it to be able to send a payload with redirection response and to comply with the other methods signature (payload, options) => KibanaResponse
💔 Build Failed |
2cedbb4 to
8f9ecbd
Compare
💔 Build Failed |
869cf56 to
fba3b7a
Compare
💚 Build Succeeded |
| registerRouter(router); | ||
|
|
||
| registerOnPreAuth((req, res, t) => | ||
| res.redirected(undefined, { |
There was a problem hiding this comment.
Makes sense to me to make this API a bit less awkward. Do you think we should do that for all of these methods for consistency?
| } catch (err) { | ||
| authLogger.error(err); | ||
| return t.rejected(wrapError(err)); | ||
| return response.internalError(err); |
There was a problem hiding this comment.
question: since you got rid of wrapError - can we be sure that internalError will hide original error message and replace it with something like An internal server error occurred for non-Boom errors?
There was a problem hiding this comment.
don't pass error at all? we have it logged and client shouldn't have access to error details
| // Hapi typings don't support headers that are `string[]`. | ||
| error.output.headers[headerName] = headerValue as any; | ||
| } | ||
| const error = authenticationResult.error; |
There was a problem hiding this comment.
potential issue: if the change in behavior above is okay (internalError for unexpected errors), here it's a bit different, ideally we should return the same status Elasticsearch gives us during authentication, e.g. 500 or whatever ES may return in the future (since we're basically intermediate party between user and Elasticsearch in the authentication flow). Ideally 401 should only be returned when ES returns error with such status or when we can't handle authentication (notHandled). It feels like we need something like wrapError in the core/responseFactory....
There was a problem hiding this comment.
yes, that question I asked here #42442 (comment) ok, I will add customizable error handler
azasypkin
left a comment
There was a problem hiding this comment.
Changes in Spaces look good, but left a couple of comments/questions in security part.
💚 Build Succeeded |
rudolf
left a comment
There was a problem hiding this comment.
Left a couple of comments on the types, but these aren't directly linked to this PR, so might be best to address in a different PR.
| @@ -401,31 +404,22 @@ export interface LogRecord { | |||
| // Warning: (ae-forgotten-export) The symbol "OnPostAuthResult" needs to be exported by the entry point index.d.ts | |||
There was a problem hiding this comment.
OnPostAuthResult, OnPreAuthResult, RouterRoute
they are internal types of the core, plugins don't have access to them.
| }) => OnPostAuthResult; | ||
| } | ||
|
|
||
| // Warning: (ae-forgotten-export) The symbol "OnPreAuthResult" needs to be exported by the entry point index.d.ts |
| constructor(path: string); | ||
| delete<P extends ObjectType, Q extends ObjectType, B extends ObjectType>(route: RouteConfig<P, Q, B>, handler: RequestHandler<P, Q, B>): void; | ||
| get<P extends ObjectType, Q extends ObjectType, B extends ObjectType>(route: RouteConfig<P, Q, B>, handler: RequestHandler<P, Q, B>): void; | ||
| // Warning: (ae-forgotten-export) The symbol "RouterRoute" needs to be exported by the entry point index.d.ts |
| IsAuthenticated, | ||
| KibanaRequest, | ||
| KibanaRequestRoute, | ||
| LifecycleResponseFactory, |
There was a problem hiding this comment.
Shouldn't we prefix these types with Http... to stick with our convention to avoid naming conflicts and make them more discoverable? (Also applies to the other HTTP types, so would probably make sense to do it in a different PR)
There was a problem hiding this comment.
babel v7.5+ supports typescript namespaces. probably we can make another attempt to switch to them. for now, we can use manual convention.
There was a problem hiding this comment.
Well that sure is exciting.
💔 Build Failed |
|
retest |
💚 Build Succeeded |
azasypkin
left a comment
There was a problem hiding this comment.
Security and Spaces changes LGTM
💚 Build Succeeded |
…#42442) * add response factory to the interceptors * adopt x-pack code to the changes * Add a separate response factory for lifecycles. Only route handler can respond with 2xx response. Interceptors may redirect or reject an incoming request. * re-generate docs * response.internal --> response.internalError * use internalError for exceptions in authenticator * before Security plugin proxied ES error status code. now sets explicitly. * provide error via message field of error response for BWC * update docs * add customError response * restore integration test and update unit tests * update docs * support Hapi error format for BWC * add a couple of tests
…#42918) * add response factory to the interceptors * adopt x-pack code to the changes * Add a separate response factory for lifecycles. Only route handler can respond with 2xx response. Interceptors may redirect or reject an incoming request. * re-generate docs * response.internal --> response.internalError * use internalError for exceptions in authenticator * before Security plugin proxied ES error status code. now sets explicitly. * provide error via message field of error response for BWC * update docs * add customError response * restore integration test and update unit tests * update docs * support Hapi error format for BWC * add a couple of tests
💚 Build Succeeded |
Summary
Interceptor interface
The outcome of an interceptor function might be:
Each interceptor has a specific toolkit. But what they have in common is the ability to redirect and reject an incoming request.
Route interface
In the route handler function, redirection and rejection are parts of
ResponseFactoryand have completely different interface, providing ability to configure response body and headers.We want both the mechanism to have a similar interface and feature set.
This PR introduces
LifecycleResponseFactorywhich is a sub-set ofResponseFactoryand supports only request redirection/rejection.Interceptor specific outcome can be specified via toolkit passed as the last argument:
Checklist
Use
strikethroughsto remove checklist items you don't feel are applicable to this PR.[ ] This was checked for cross-browser compatibility, including a check against IE11[ ] Any text added follows EUI's writing guidelines, uses sentence case text and includes i18n support[ ] Documentation was added for features that require explanation or tutorials[ ] This was checked for keyboard-only and screenreader accessibilityFor maintainers
Dev docs
New platform plugins may want to extend Kibana capabilities with implementing a custom logic over an incoming request, before it hits a resource endpoint. For this purpose, KIbana introduced interceptors concept. Interceptors cannot change or mutate request object directly but may redirect, reject or allow a request to pass through. An interceptor may be created for different lifecycle stages of an incoming request: