-
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
Restrict access to hapi Request in registerAuth #38763
Restrict access to hapi Request in registerAuth #38763
Conversation
e32deae
to
b44d3db
Compare
💔 Build Failed |
a2f41d8
to
d1849b2
Compare
this.config.requestHeadersWhitelist | ||
); | ||
|
||
return { ...headers, ...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.
if there are no authHeaders
returned from registerAuth
or registerAuth
wasn't called, we will use headers from the incoming request.
@@ -37,6 +37,7 @@ export interface KibanaRequestRoute { | |||
options: Required<RouteConfigOptions>; | |||
} | |||
|
|||
const securedHeaders = ['authorization']; |
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.
authHeaders is a map and we don't know its keys. @elastic/kibana-security we can make this value configurable when needed.
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: secured
can give a false sense of security especially when it's used in OSS or when security plugin is disabled, just sayin' 🙂 What does secured
mean here?
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.
Headers aren't exposed by getFilteredHeaders
method. We can call it filteredHeaders
but it doesn't show an intention and doesn't answer why. 🤔
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.
How about secretHeaders
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.
works for me
this.url = request.url; | ||
|
||
this[requestSymbol] = request; | ||
// prevent Symbol exposure via Object.getOwnPropertySymbols() | ||
Object.defineProperty(this, requestSymbol, { |
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 should make KibanaRequest an object literal created by a factory instead of the class to emulate private fields via closures. Also, the class exposes static methods are meant to be used only inside of the 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.
That sounds like a smart move to me, can be done in future PR.
const { req, res } = request.raw; | ||
const authHeaders = setupDeps.core.http.auth.getAuthHeaders(request); | ||
if (authHeaders) { | ||
// some plugins in Legacy relay on headers.authorization presence |
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.
@@ -103,7 +109,7 @@ export class ElasticsearchService implements CoreService<ElasticsearchServiceSet | |||
dataClient$: clients$.pipe(map(clients => clients.dataClient)), | |||
|
|||
createClient: (type: string, clientConfig: ElasticsearchClientConfig) => { | |||
return this.createClusterClient(type, clientConfig); | |||
return this.createClusterClient(type, clientConfig, deps.http.auth.getAuthHeaders); |
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.
should we extend all external cluster clients with this functionality or we should make it configurable?
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 only example I can contrive where we may want to call asScoped
on a ClusterClient
with a KibanaRequest
and not get the auth from the internal HTTP server is the proxy plugin which has it's own HTTPServer. That said, that plugin is not planning to use header/credential forwarding, but it may make the createNewServer
API behave strangely to 3rd party consumers.
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.
agree I want to extract all Kibana specific logic from HttpServer to HttpService in #38092
*/ | ||
public asScoped(req: { headers?: Headers } = {}) { | ||
public asScoped(request?: KibanaRequest | Request | FakeRequest) { |
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.
- for now, Security can use FakeRequest to perform a request to
shield.authenticate
.
also we can move all logic related to headers calculation to http_service. then cluster_client just call
return new ScopedClusterClient(
this.callAsInternalUser,
this.callAsCurrentUser,
this.getHeaders(request, this.config.requestHeadersWhitelist)
);
what do you think?
- should we use
Legacy.Request
instead ofRequest
to emphasise it is a deprecated api? - request is optional
request?
for BWC with the current code. what is the case whencluster.asScoped(undefined)
and ``cluster.asScoped({headers: undefined})` are valid?
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.
should we use Legacy.Request instead of Request to emphasise it is a deprecated api?
That makes sense to me 👍
request is optional request? for BWC with the current code. what is the case when
cluster.asScoped(undefined) and ``cluster.asScoped({headers: undefined})` are valid?
Not sure I understand this issue. Both of those cases seem like they should have the same behavior. Am I missing something?
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.
Not sure I understand this issue. Both of those cases seem like they should have the same behavior. Am I missing something?
ok, re-phrase: is it a valid case to perform a request to ES when authorization header is not set? I thought we always require authentication creating default elastic
user. don't we?
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'm not sure why request
would ever be undefined, but the headers could be empty when Security is disabled or when using OSS Kibana. I believe that case would look more like cluster.asScoped({headers: {}})
(no Authorization
header present).
💚 Build Succeeded |
💚 Build Succeeded |
Prevent exposing headers.authorization in KibanaRequest. Introduce a mechanism to associate authorization headers with an incoming request and retrieve its value to perform a request to elasticsearch cluster.
ddbdafe
to
090fdde
Compare
authenticate: adoptToHapiAuthFormat(fn, this.authState.set), | ||
authenticate: adoptToHapiAuthFormat(fn, (req, { state, headers }) => { | ||
this.authState.set(req, state); | ||
this.authHeaders.set(req, 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 used 2 different storages because they have different use cases
- the state may be exposed to any downstream plugin
- headers are meant to be used only by core when call elasticsearch cluster
private getHeaders( | ||
request?: KibanaRequest | Request | FakeRequest | ||
): Record<string, string | string[] | undefined> { | ||
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.
when migrate from Legacy to New platform, we don't need to support Request
as a valid input and can switch to
if(isKibanaRequest(request)){
const authHeaders = this.getAuthHeaders(request);
const headers = toRawRequest(request).headers;
return { ...headers, ...authHeaders };
} else {
return request.headers;
}
Pinging @elastic/kibana-platform |
Pinging @elastic/kibana-security |
💔 Build Failed |
💚 Build Succeeded |
*/ | ||
public asScoped(req: { headers?: Headers } = {}) { | ||
public asScoped(request?: KibanaRequest | Request | FakeRequest) { |
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.
should we use Legacy.Request instead of Request to emphasise it is a deprecated api?
That makes sense to me 👍
request is optional request? for BWC with the current code. what is the case when
cluster.asScoped(undefined) and ``cluster.asScoped({headers: undefined})` are valid?
Not sure I understand this issue. Both of those cases seem like they should have the same behavior. Am I missing something?
@@ -103,7 +109,7 @@ export class ElasticsearchService implements CoreService<ElasticsearchServiceSet | |||
dataClient$: clients$.pipe(map(clients => clients.dataClient)), | |||
|
|||
createClient: (type: string, clientConfig: ElasticsearchClientConfig) => { | |||
return this.createClusterClient(type, clientConfig); | |||
return this.createClusterClient(type, clientConfig, deps.http.auth.getAuthHeaders); |
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 only example I can contrive where we may want to call asScoped
on a ClusterClient
with a KibanaRequest
and not get the auth from the internal HTTP server is the proxy plugin which has it's own HTTPServer. That said, that plugin is not planning to use header/credential forwarding, but it may make the createNewServer
API behave strangely to 3rd party consumers.
@@ -37,6 +37,7 @@ export interface KibanaRequestRoute { | |||
options: Required<RouteConfigOptions>; | |||
} | |||
|
|||
const securedHeaders = ['authorization']; |
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.
How about secretHeaders
this.url = request.url; | ||
|
||
this[requestSymbol] = request; | ||
// prevent Symbol exposure via Object.getOwnPropertySymbols() | ||
Object.defineProperty(this, requestSymbol, { |
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 sounds like a smart move to me, can be done in future PR.
💔 Build Failed |
💚 Build Succeeded |
* Prevent exposing Hapi.Request to registerAuth. Prevent exposing headers.authorization in KibanaRequest. Introduce a mechanism to associate authorization headers with an incoming request and retrieve its value to perform a request to elasticsearch cluster. * fix tests * address @joshdover comments
* Prevent exposing Hapi.Request to registerAuth. Prevent exposing headers.authorization in KibanaRequest. Introduce a mechanism to associate authorization headers with an incoming request and retrieve its value to perform a request to elasticsearch cluster. * fix tests * address @joshdover comments
@restrry can you please add a Dev Doc section to this PR for API changes document? |
@rayafratkina done |
💔 Build Failed |
Summary
In New Platform only a plugin calling
registerAuth
has read-access to the authorization header.This plugin can return headers that should be used to perform a request against Elasticsearch on behalf of end-user. Those headers are stored in the core and retrieved cluster client to perform a request.
If cluster client cannot find headers returned from
registerAuth
, it will fallback to an incoming request headers.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
In the previous release, we introduced experimental
registerAuth
hook in New Platform that allows end users to define custom authentication logic.We changed API of the
AuthenticationHandler
function.In 7.2:
In 7.3
We provide
KibanaRequest
instead ofhapi
Request object and remove sessionStorage from arguments. NowsessionStorageFactory
is returned as result ofregisterAuth
call.Note: as API is experimental is still a subject for further changes.