Skip to content
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

Merged

Conversation

mshustov
Copy link
Contributor

@mshustov mshustov commented Jun 12, 2019

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 strikethroughs to remove checklist items you don't feel are applicable to this PR.

For 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:

export type AuthenticationHandler<T> = (
  request: Request,
  sessionStorage: SessionStorage<T>,
  t: AuthToolkit
) => Promise<AuthResult>;

In 7.3

export type AuthenticationHandler = (
  request: KibanaRequest,
  t: AuthToolkit
) => AuthResult | Promise<AuthResult>;

We provide KibanaRequest instead of hapi Request object and remove sessionStorage from arguments. Now sessionStorageFactory is returned as result of registerAuth call.
Note: as API is experimental is still a subject for further changes.

@mshustov mshustov force-pushed the issue-33775-restrict-access-to-hapi-request branch 3 times, most recently from e32deae to b44d3db Compare June 12, 2019 14:26
@elasticmachine
Copy link
Contributor

💔 Build Failed

@elastic elastic deleted a comment from elasticmachine Jun 12, 2019
@elastic elastic deleted a comment from elasticmachine Jun 12, 2019
@mshustov mshustov force-pushed the issue-33775-restrict-access-to-hapi-request branch 2 times, most recently from a2f41d8 to d1849b2 Compare June 13, 2019 08:03
@elastic elastic deleted a comment from elasticmachine Jun 13, 2019
this.config.requestHeadersWhitelist
);

return { ...headers, ...authHeaders };
Copy link
Contributor Author

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'];
Copy link
Contributor Author

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.

Copy link
Member

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?

Copy link
Contributor Author

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. 🤔

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about secretHeaders

Copy link
Contributor Author

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, {
Copy link
Contributor Author

@mshustov mshustov Jun 13, 2019

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.

Copy link
Contributor

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
Copy link
Contributor Author

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);
Copy link
Contributor Author

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?

Copy link
Contributor

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.

Copy link
Contributor Author

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

@elastic elastic deleted a comment from elasticmachine Jun 13, 2019
@elastic elastic deleted a comment from elasticmachine Jun 13, 2019
*/
public asScoped(req: { headers?: Headers } = {}) {
public asScoped(request?: KibanaRequest | Request | FakeRequest) {
Copy link
Contributor Author

@mshustov mshustov Jun 13, 2019

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 of Request to emphasise it is a deprecated api?
  • 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?

Copy link
Contributor

@joshdover joshdover Jun 14, 2019

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?

Copy link
Contributor Author

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 elasticuser. don't we?

Copy link
Contributor

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).

@elasticmachine
Copy link
Contributor

💚 Build Succeeded

@elasticmachine
Copy link
Contributor

💚 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.
@mshustov mshustov force-pushed the issue-33775-restrict-access-to-hapi-request branch from ddbdafe to 090fdde Compare June 14, 2019 10:12
authenticate: adoptToHapiAuthFormat(fn, this.authState.set),
authenticate: adoptToHapiAuthFormat(fn, (req, { state, headers }) => {
this.authState.set(req, state);
this.authHeaders.set(req, headers);
Copy link
Contributor Author

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)) {
Copy link
Contributor Author

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;
}

@mshustov mshustov added Feature:New Platform Team:Core Core services & architecture: plugins, logging, config, saved objects, http, ES client, i18n, etc Team:Security Team focused on: Auth, Users, Roles, Spaces, Audit Logging, and more! release_note:plugin_api_changes Contains a Plugin API changes section for the breaking plugin API changes section. review labels Jun 14, 2019
@elasticmachine
Copy link
Contributor

Pinging @elastic/kibana-platform

@elasticmachine
Copy link
Contributor

Pinging @elastic/kibana-security

@mshustov mshustov marked this pull request as ready for review June 14, 2019 10:29
@mshustov mshustov requested a review from a team as a code owner June 14, 2019 10:29
@elasticmachine
Copy link
Contributor

💔 Build Failed

@elasticmachine
Copy link
Contributor

💚 Build Succeeded

src/core/server/http/cookie_sesson_storage.test.ts Outdated Show resolved Hide resolved
src/core/server/elasticsearch/cluster_client.ts Outdated Show resolved Hide resolved
src/core/server/elasticsearch/cluster_client.ts Outdated Show resolved Hide resolved
src/core/server/elasticsearch/cluster_client.ts Outdated Show resolved Hide resolved
*/
public asScoped(req: { headers?: Headers } = {}) {
public asScoped(request?: KibanaRequest | Request | FakeRequest) {
Copy link
Contributor

@joshdover joshdover Jun 14, 2019

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?

src/core/server/elasticsearch/cluster_client.ts Outdated Show resolved Hide resolved
@@ -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);
Copy link
Contributor

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'];
Copy link
Contributor

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, {
Copy link
Contributor

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.

@mshustov mshustov requested a review from joshdover June 17, 2019 14:30
@elasticmachine
Copy link
Contributor

💔 Build Failed

@elasticmachine
Copy link
Contributor

💚 Build Succeeded

@mshustov mshustov merged commit 9e044c5 into elastic:master Jun 18, 2019
@mshustov mshustov deleted the issue-33775-restrict-access-to-hapi-request branch June 18, 2019 08:21
mshustov added a commit to mshustov/kibana that referenced this pull request Jun 18, 2019
* 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
mshustov added a commit that referenced this pull request Jun 18, 2019
* 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
@mshustov mshustov removed the review label Jun 25, 2019
@rayafratkina
Copy link
Contributor

@restrry can you please add a Dev Doc section to this PR for API changes document?

@mshustov
Copy link
Contributor Author

@rayafratkina done

@elasticmachine
Copy link
Contributor

💔 Build Failed

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Feature:New Platform release_note:plugin_api_changes Contains a Plugin API changes section for the breaking plugin API changes section. Team:Core Core services & architecture: plugins, logging, config, saved objects, http, ES client, i18n, etc Team:Security Team focused on: Auth, Users, Roles, Spaces, Audit Logging, and more! v7.3.0
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants