introduce pre-/post-auth request hooks for HttpServer#36690
introduce pre-/post-auth request hooks for HttpServer#36690mshustov merged 21 commits intoelastic:masterfrom
Conversation
|
@legrego would you mind to have a look if it's sufficient to cover Security & Spaces use cases? |
There was a problem hiding this comment.
I'm not sure if it's out of scope for this PR or not, but the Spaces onPostAuth interceptor also needs to be able to render a hidden app via request takeover.
I'm not sure if the concept of "hidden applications" has been thought through for the new platform or not (@joshdover?), but we'll need equivalent functionality for both Security and Spaces
There was a problem hiding this comment.
I'm not sure if the concept of "hidden applications" has been thought through for the new
No. Although, I think it's not a blocker. All Spaces / Security logic that is a blocker for NP plugin migration should be moved to NP asap. Remaining parts still have access to API in NP and we can postpone polishing for a bit. In this case that rendering logic can be expressed as :
const spaces = await newPlatform.plugins.spaces.client.asScoped(request).getAll();
if(spaces.length === 1) ...
if(spaces.length > 0) ...There was a problem hiding this comment.
I think I agree with that, but we might need additional work to support dashboard-only-mode's interceptor) in the LP.
It's relying on "auth scopes" to determine when to render its hidden app, but auth scopes are identifiers attached to the request.auth.credentials object. Will NP's http service allow direct access to such properties in the LP once security moves to NP?
There was a problem hiding this comment.
@elastic/kibana-security Do we have a desire to keep the “auth scopes” used for dashboard-only-mode? I’m working with Mikhail to make sure the new platform has the necessary functionality to replace onRequest and onPostAuth. We want to move the authentication logic to the NP ASAP to unblock other plugins from migrating, but part of the auth logic is the concept of scopes, which are used by dashboard only mode in the legacy platform. We don’t have a solution for rendering hidden apps yet (for dashboard mode), and we don’t want to block migrating authc on that if we don’t have to.
Mikhail suggested adding a service in the NP that would allow plugins to associate arbitrary data with a particular request, which would also extend to the LP. I'm thinking we could use that to store the list of roles post-auth in NP, which dashboard-only-mode could use in the LP to determine if it should render its hidden app or not.
It's effectively the same logic, just without the concept of auth scopes. Dasbhoard-only-mode is the only consumer of the auth scopes service, and AFAIK, we don't have plans to use it elsewhere in the near future. It would be one less service to migrate to the NP, too. One of the goals in the NP is to restrict access to auth data server-side. Currently, any plugin can inspect the request headers and auth details, but Mikhail is working on abstracting that out so that plugins don't require direct access anymore.
There was a problem hiding this comment.
Removing "auth scopes" seems entirely reasonable to me, as long as we can emulate the existing behavior of dashboard-only-mode using the mechanism that you've described. I'm not a huge fan of the way that Hapi does it's auth rules, so I'd much prefer we use our own abstractions.
There was a problem hiding this comment.
We can extend core API:
- registerAuth already receive
credentialswhich we can store internally
const authenticate: AuthenticationHandler = async (req, sessionStorage, t) => {
if (req.headers.authorization) {
sessionStorage.set({ value: user, expires: Date.now() + sessionDurationMs });
return t.authenticated({ credentials: user });
} else {
return t.rejected(Boom.unauthorized());
}
};- add a method that returns passed credentials and auth status
interface Auth {
status: 'authenticated' | 'unauthenticated' | 'unknown';
credentials: unknown;
}
HttpCoreSetup {
...
getAuth: (request: KibanaRequest | Request, basePath: string) => Auth
}Oleg's comment regarding statuses #34631 (comment)
@kobelb what do you think about this api? or you have another abstraction in mind?
💔 Build Failed |
|
@elastic/kibana-security I also just realized that when we move Security to Ne platform we need to find a solution to set a cookie from Legacy. |
caef93a to
e0a607b
Compare
I'll keep that in mind when I start the authc migration. I might be able to use the |
|
@legrego signed cookie is set via |
I was thinking the security plugin could maintain a parallel implementation for LP -- essentially keeping what it has today for session management in the case of requests proxied to LP. I honestly haven't given this a ton of thought yet, so I might be wildly off base. Either way, this isn't a blocker for the first phase of spaces migration. |
💔 Build Failed |
|
Pinging @elastic/kibana-platform |
💔 Build Failed |
|
|
||
| for (const router of this.registeredRouters) { | ||
| for (const route of router.getRoutes()) { | ||
| const isAuthRequired = Boolean(this.authRegistered && route.authRequired); |
There was a problem hiding this comment.
are we fine to introduce authRequired flag as unprotected route definition?
There was a problem hiding this comment.
I think that's reasonable, as long as the default value is true
| registerOnPreAuth: HttpServiceSetup['registerOnPreAuth']; | ||
| registerAuth: HttpServiceSetup['registerAuth']; | ||
| registerOnRequest: HttpServiceSetup['registerOnRequest']; | ||
| registerOnPostAuth: HttpServiceSetup['registerOnPostAuth']; |
There was a problem hiding this comment.
@eliperelman what do you think if we allocate basePath functionality under basePath namespace in httpSetup? If you agree on this interface, I can implement for server side as a part of #35816
basePath: {
get (key) {}
set (key, value) {}
prepend(url) {}
remove (url) {}
}There was a problem hiding this comment.
I think this makes sense. I like it.
| ) => this.registerAuth(fn, cookieOptions, config.basePath), | ||
| getBasePathFor: this.getBasePathFor.bind(this, config), | ||
| setBasePathFor: this.setBasePathFor.bind(this), | ||
| auth: { |
There was a problem hiding this comment.
note: HttpServer in the current state is mix of infrastructure layer & application layer logic, we can restructure it later, as soon as finish with #36804 to avoid conflicts
💔 Build Failed |
💚 Build Succeeded |
I think I'd prefer the second option, where NP delegates to LP for rendering in this case. I'm totally fine making that a different PR |
joshdover
left a comment
There was a problem hiding this comment.
Design looks good to me, only really have nitpicky comments for ya.
| fn: AuthenticationHandler<T>, | ||
| sessionStorage: SessionStorageFactory<T> | ||
| sessionStorage: SessionStorageFactory<T>, | ||
| onSuccess: (req: Request, state: unknown) => void = noop |
There was a problem hiding this comment.
Any practical reason to widen the type of state from object to unknown here? I'd expect them to match.
There was a problem hiding this comment.
they aren't. state contain authorization information which shouldn't be cached in cookies.
that was discussed with @legrego in DM
Right now, the cookie only stores authentication information, and we delegate all authorization to Elasticsearch at “runtime”. Storing this info in a cookie would mean caching authorization details, which we don’t want to do.
we can add another generic type to specify onSuccess allowed type.
but this require some hassle to pass type declaration to https://github.com/restrry/kibana/blob/b529a1ae1c6198d0c90125bcb01309e658ae34bf/src/core/server/http/http_server.ts#L131
so I decided to handle it as a block box for core
💚 Build Succeeded |
* introduce pre-,post-auth stages * cleanup integration_tests. now contracts available in tests * auth per route is configurable * Unify lifecycle results structure * expose api to store auth state and status via http service * update tests * update docs * use full name, auth should not mutate request * move basePath functionality under namespace * regenerate docs * Revert "move basePath functionality under namespace" This reverts commit 9599d32. * Revert "regenerate docs" This reverts commit 1799d3b. * regenerate docs * updated yarn.lock no idea why * extract AuthStateStorage to a separate entity * get rid of nested ifs * describe what is the difference between hooks * re-wording
* introduce pre-,post-auth stages * cleanup integration_tests. now contracts available in tests * auth per route is configurable * Unify lifecycle results structure * expose api to store auth state and status via http service * update tests * update docs * use full name, auth should not mutate request * move basePath functionality under namespace * regenerate docs * Revert "move basePath functionality under namespace" This reverts commit 9599d32. * Revert "regenerate docs" This reverts commit 1799d3b. * regenerate docs * updated yarn.lock no idea why * extract AuthStateStorage to a separate entity * get rid of nested ifs * describe what is the difference between hooks * re-wording
…38265) * introduce pre-/post-auth request hooks for HttpServer (#36690) * introduce pre-,post-auth stages * cleanup integration_tests. now contracts available in tests * auth per route is configurable * Unify lifecycle results structure * expose api to store auth state and status via http service * update tests * update docs * use full name, auth should not mutate request * move basePath functionality under namespace * regenerate docs * Revert "move basePath functionality under namespace" This reverts commit 9599d32. * Revert "regenerate docs" This reverts commit 1799d3b. * regenerate docs * updated yarn.lock no idea why * extract AuthStateStorage to a separate entity * get rid of nested ifs * describe what is the difference between hooks * re-wording * fix typings
Summary
I'm back to the problem. After talking with the Security team we figured out that several lifecycle stages are required.
onPreAuth. Specificity: can perform request forwarding to another URL, before auth is performed and request hits a route handler. Analog in the LP -server.ext('onRequest',...). There is only one consumer as of now. But some extension points in LP can be moved here as well. one, twot.authenticatedinregisterAuthcan be retrieved later as
httpSetup.auth.get(request) => {state, status}in both New and Legacy platformsonPostAuth. Specificity: has access to auth status. Analog in the LP -server.ext('onPostAuth',...).Checklist
Use
strikethroughsto remove checklist items you don't feel are applicable to this PR.For maintainers
Dev Docs
Kibana let define custom request interceptors for incoming requests:
http.registerAuth(handler, cookieOptions) => Promise<void>To define custom authentication and/or authorization mechanism for incoming requests.
A handler should return a state to associate with the incoming request.
The state can be retrieved later via http.auth.get(..). Only one AuthenticationHandler can be registered.
http.registerOnPreAuth(handler) => voidTo define custom logic to perform for incoming requests.
Runs the handler before Auth hook performs a check that user has access to requested resources, so it's the only place when you can forward a request to another URL right on the server.
Can register any number of registerOnPostAuth, which are called in sequence (from the first registered to the last).
http.registerOnPreAuth (handler) => voidTo define custom logic to perform for incoming requests. Runs the handler after Auth hook did make sure a user has access to the requested resource.
The auth state is available at stage via http.auth.get(..)
Can register any number of registerOnPreAuth, which are called in sequence (from the first registered to the last).