-
Notifications
You must be signed in to change notification settings - Fork 8.3k
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
Unify response interface in handler and request interceptors #42442
Unify response interface in handler and request interceptors #42442
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
had to switch to Boom error. hapi doesn't allow to respond from lifecycle event handlers. even for error responses 😞
@@ -88,7 +88,7 @@ export async function setupAuthentication({ | |||
authenticationResult = await authenticator.authenticate(request); | |||
} catch (err) { | |||
authLogger.error(err); | |||
return t.rejected(wrapError(err)); | |||
return response.unauthorized(wrapError(err)); |
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.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
TODO: remove this check
💔 Build Failed |
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.
Looks good from an implementation standpoint. Have a couple optional nits.
ack: will get to tomorrow |
@@ -105,28 +105,25 @@ export async function setupAuthentication({ | |||
// authentication (username and password) or arbitrary external page managed by 3rd party | |||
// 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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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?
// 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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yes, that question I asked here #42442 (comment) ok, I will add customizable error handler
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.
Changes in Spaces look good, but left a couple of comments/questions in security part.
💚 Build Succeeded |
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.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Shouldn't we export this?
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.
OnPostAuthResult
, OnPreAuthResult
, RouterRoute
they are internal types of the core, plugins don't have access to them.
redirected: (url: string) => OnPostAuthResult; | ||
rejected: (error: Error, options?: { | ||
statusCode?: number; | ||
}) => OnPostAuthResult; | ||
} | ||
|
||
// Warning: (ae-forgotten-export) The symbol "OnPreAuthResult" needs to be exported by the entry point index.d.ts |
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.
Missing export?
@@ -535,16 +529,14 @@ export class Router { | |||
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 |
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.
missing export?
@@ -74,6 +74,7 @@ export { | |||
IsAuthenticated, | |||
KibanaRequest, | |||
KibanaRequestRoute, | |||
LifecycleResponseFactory, |
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.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Well that sure is exciting.
💔 Build Failed |
retest |
💚 Build Succeeded |
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.
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
ResponseFactory
and 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
LifecycleResponseFactory
which is a sub-set ofResponseFactory
and 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: