Skip to content

Commit

Permalink
Expose elasticsearch error wrapper (#40242)
Browse files Browse the repository at this point in the history
* expose elasticsearch error wrapper

* generate docs

* Update src/core/server/elasticsearch/errors.ts

Co-Authored-By: Rudolf Meijering <skaapgif@gmail.com>

* address Oleg comments
  • Loading branch information
mshustov committed Jul 4, 2019
1 parent 7ad2c94 commit 8a97392
Show file tree
Hide file tree
Showing 12 changed files with 292 additions and 9 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->

[Home](./index.md) &gt; [kibana-plugin-server](./kibana-plugin-server.md) &gt; [ElasticsearchError](./kibana-plugin-server.elasticsearcherror.md) &gt; [\[code\]](./kibana-plugin-server.elasticsearcherror.[code].md)

## ElasticsearchError.\[code\] property

<b>Signature:</b>

```typescript
[code]?: string;
```
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->

[Home](./index.md) &gt; [kibana-plugin-server](./kibana-plugin-server.md) &gt; [ElasticsearchError](./kibana-plugin-server.elasticsearcherror.md)

## ElasticsearchError interface

<b>Signature:</b>

```typescript
export interface ElasticsearchError extends Boom
```
## Properties
| Property | Type | Description |
| --- | --- | --- |
| [\[code\]](./kibana-plugin-server.elasticsearcherror.[code].md) | <code>string</code> | |
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->

[Home](./index.md) &gt; [kibana-plugin-server](./kibana-plugin-server.md) &gt; [ElasticsearchErrorHelpers](./kibana-plugin-server.elasticsearcherrorhelpers.md) &gt; [decorateNotAuthorizedError](./kibana-plugin-server.elasticsearcherrorhelpers.decoratenotauthorizederror.md)

## ElasticsearchErrorHelpers.decorateNotAuthorizedError() method

<b>Signature:</b>

```typescript
static decorateNotAuthorizedError(error: Error, reason?: string): ElasticsearchError;
```

## Parameters

| Parameter | Type | Description |
| --- | --- | --- |
| error | <code>Error</code> | |
| reason | <code>string</code> | |

<b>Returns:</b>

`ElasticsearchError`

Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->

[Home](./index.md) &gt; [kibana-plugin-server](./kibana-plugin-server.md) &gt; [ElasticsearchErrorHelpers](./kibana-plugin-server.elasticsearcherrorhelpers.md) &gt; [isNotAuthorizedError](./kibana-plugin-server.elasticsearcherrorhelpers.isnotauthorizederror.md)

## ElasticsearchErrorHelpers.isNotAuthorizedError() method

<b>Signature:</b>

```typescript
static isNotAuthorizedError(error: any): error is ElasticsearchError;
```

## Parameters

| Parameter | Type | Description |
| --- | --- | --- |
| error | <code>any</code> | |

<b>Returns:</b>

`error is ElasticsearchError`

Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->

[Home](./index.md) &gt; [kibana-plugin-server](./kibana-plugin-server.md) &gt; [ElasticsearchErrorHelpers](./kibana-plugin-server.elasticsearcherrorhelpers.md)

## ElasticsearchErrorHelpers class

Helpers provided to simplify future migration away from Boom as internal Error.

<b>Signature:</b>

```typescript
export declare class ElasticsearchErrorHelpers
```

## Methods

| Method | Modifiers | Description |
| --- | --- | --- |
| [decorateNotAuthorizedError(error, reason)](./kibana-plugin-server.elasticsearcherrorhelpers.decoratenotauthorizederror.md) | <code>static</code> | |
| [isNotAuthorizedError(error)](./kibana-plugin-server.elasticsearcherrorhelpers.isnotauthorizederror.md) | <code>static</code> | |

## Example

Handle errors

```js
try {
await client.callWithRequest(request, '...');
} catch (err) {
if (ElasticsearchErrorHelpers.isNotAuthorizedError(err)) {
const authHeader = err.output.headers['WWW-Authenticate'];
}

```
2 changes: 2 additions & 0 deletions docs/development/core/server/kibana-plugin-server.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ The plugin integrates with the core system via lifecycle events: `setup`<!-- -->
| Class | Description |
| --- | --- |
| [ClusterClient](./kibana-plugin-server.clusterclient.md) | Represents an Elasticsearch cluster API client and allows to call API on behalf of the internal Kibana user and the actual user that is derived from the request headers (via <code>asScoped(...)</code>). |
| [ElasticsearchErrorHelpers](./kibana-plugin-server.elasticsearcherrorhelpers.md) | Helpers provided to simplify future migration away from Boom as internal Error. |
| [KibanaRequest](./kibana-plugin-server.kibanarequest.md) | Kibana specific abstraction for an incoming request. |
| [Router](./kibana-plugin-server.router.md) | |
| [SavedObjectsErrorHelpers](./kibana-plugin-server.savedobjectserrorhelpers.md) | |
Expand All @@ -30,6 +31,7 @@ The plugin integrates with the core system via lifecycle events: `setup`<!-- -->
| [CoreSetup](./kibana-plugin-server.coresetup.md) | Context passed to the plugins <code>setup</code> method. |
| [CoreStart](./kibana-plugin-server.corestart.md) | Context passed to the plugins <code>start</code> method. |
| [DiscoveredPlugin](./kibana-plugin-server.discoveredplugin.md) | Small container object used to expose information about discovered plugins that may or may not have been started. |
| [ElasticsearchError](./kibana-plugin-server.elasticsearcherror.md) | |
| [ElasticsearchServiceSetup](./kibana-plugin-server.elasticsearchservicesetup.md) | |
| [FakeRequest](./kibana-plugin-server.fakerequest.md) | Fake request object created manually by Kibana plugins. |
| [HttpServiceSetup](./kibana-plugin-server.httpservicesetup.md) | |
Expand Down
11 changes: 2 additions & 9 deletions src/core/server/elasticsearch/cluster_client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,11 @@
* specific language governing permissions and limitations
* under the License.
*/

import Boom from 'boom';
import { Client } from 'elasticsearch';
import { get } from 'lodash';
import { Request } from 'hapi';

import { ElasticsearchErrorHelpers } from './errors';
import { GetAuthHeaders, isRealRequest } from '../http';
import { filterHeaders, KibanaRequest, ensureRawRequest } from '../http/router';
import { Logger } from '../logging';
Expand Down Expand Up @@ -97,13 +96,7 @@ async function callAPI(
throw err;
}

const boomError = Boom.boomify(err, { statusCode: err.statusCode });
const wwwAuthHeader: string = get(err, 'body.error.header[WWW-Authenticate]');

boomError.output.headers['WWW-Authenticate'] =
wwwAuthHeader || 'Basic realm="Authorization Required"';

throw boomError;
throw ElasticsearchErrorHelpers.decorateNotAuthorizedError(err);
}
}

Expand Down
70 changes: 70 additions & 0 deletions src/core/server/elasticsearch/errors.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/

import Boom from 'boom';

import { ElasticsearchErrorHelpers } from './errors';

describe('ElasticsearchErrorHelpers', () => {
describe('NotAuthorized error', () => {
describe('decorateNotAuthorizedError', () => {
it('returns original object', () => {
const error = new Error();
expect(ElasticsearchErrorHelpers.decorateNotAuthorizedError(error)).toBe(error);
});

it('makes the error identifiable as a NotAuthorized error', () => {
const error = new Error();
expect(ElasticsearchErrorHelpers.isNotAuthorizedError(error)).toBe(false);
ElasticsearchErrorHelpers.decorateNotAuthorizedError(error);
expect(ElasticsearchErrorHelpers.isNotAuthorizedError(error)).toBe(true);
});

it('adds boom properties', () => {
const error = ElasticsearchErrorHelpers.decorateNotAuthorizedError(new Error());
expect(typeof error.output).toBe('object');
expect(error.output.statusCode).toBe(401);
});

it('preserves boom properties of input', () => {
const error = Boom.notFound();
ElasticsearchErrorHelpers.decorateNotAuthorizedError(error);
expect(error.output.statusCode).toBe(404);
});

describe('error.output', () => {
it('defaults to message of error', () => {
const error = ElasticsearchErrorHelpers.decorateNotAuthorizedError(new Error('foobar'));
expect(error.output.payload).toHaveProperty('message', 'foobar');
});
it('prefixes message with passed reason', () => {
const error = ElasticsearchErrorHelpers.decorateNotAuthorizedError(
new Error('foobar'),
'biz'
);
expect(error.output.payload).toHaveProperty('message', 'biz: foobar');
});
it('sets statusCode to 401', () => {
const error = ElasticsearchErrorHelpers.decorateNotAuthorizedError(new Error('foo'));
expect(error.output).toHaveProperty('statusCode', 401);
});
});
});
});
});
90 changes: 90 additions & 0 deletions src/core/server/elasticsearch/errors.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/

import Boom from 'boom';
import { get } from 'lodash';

const code = Symbol('ElasticsearchError');

enum ErrorCode {
NOT_AUTHORIZED = 'Elasticsearch/notAuthorized',
}

export interface ElasticsearchError extends Boom {
[code]?: string;
}

function isElasticsearchError(error: any): error is ElasticsearchError {
return Boolean(error && error[code]);
}

function decorate(
error: Error,
errorCode: ErrorCode,
statusCode: number,
message?: string
): ElasticsearchError {
if (isElasticsearchError(error)) {
return error;
}

const boom = Boom.boomify(error, {
statusCode,
message,
// keep status and messages if Boom error object already has them
override: false,
}) as ElasticsearchError;

boom[code] = errorCode;

return boom;
}

/**
* Helpers for working with errors returned from the Elasticsearch service.Since the internal data of
* errors are subject to change, consumers of the Elasticsearch service should always use these helpers
* to classify errors instead of checking error internals such as `body.error.header[WWW-Authenticate]`
* @public
*
* @example
* Handle errors
* ```js
* try {
* await client.asScoped(request).callAsCurrentUser(...);
* } catch (err) {
* if (ElasticsearchErrorHelpers.isNotAuthorizedError(err)) {
* const authHeader = err.output.headers['WWW-Authenticate'];
* }
* ```
*/
export class ElasticsearchErrorHelpers {
public static isNotAuthorizedError(error: any): error is ElasticsearchError {
return isElasticsearchError(error) && error[code] === ErrorCode.NOT_AUTHORIZED;
}

public static decorateNotAuthorizedError(error: Error, reason?: string) {
const decoratedError = decorate(error, ErrorCode.NOT_AUTHORIZED, 401, reason);
const wwwAuthHeader = get<string>(error, 'body.error.header[WWW-Authenticate]');

decoratedError.output.headers['WWW-Authenticate'] =
wwwAuthHeader || 'Basic realm="Authorization Required"';

return decoratedError;
}
}
1 change: 1 addition & 0 deletions src/core/server/elasticsearch/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,4 @@ export { CallAPIOptions, ClusterClient, FakeRequest, LegacyRequest } from './clu
export { ScopedClusterClient, Headers, APICaller } from './scoped_cluster_client';
export { ElasticsearchClientConfig } from './elasticsearch_client_config';
export { config } from './elasticsearch_config';
export { ElasticsearchError, ElasticsearchErrorHelpers } from './errors';
2 changes: 2 additions & 0 deletions src/core/server/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@ export {
Headers,
ScopedClusterClient,
ElasticsearchClientConfig,
ElasticsearchError,
ElasticsearchErrorHelpers,
APICaller,
FakeRequest,
LegacyRequest,
Expand Down
16 changes: 16 additions & 0 deletions src/core/server/server.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,22 @@ export type ElasticsearchClientConfig = Pick<ConfigOptions, 'keepAlive' | 'log'
ssl?: Partial<ElasticsearchConfig['ssl']>;
};

// Warning: (ae-missing-release-tag) "ElasticsearchError" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
export interface ElasticsearchError extends Boom {
// (undocumented)
[code]?: string;
}

// @public
export class ElasticsearchErrorHelpers {
// (undocumented)
static decorateNotAuthorizedError(error: Error, reason?: string): ElasticsearchError;
// (undocumented)
static isNotAuthorizedError(error: any): error is ElasticsearchError;
}

// @public (undocumented)
export interface ElasticsearchServiceSetup {
// (undocumented)
Expand Down

0 comments on commit 8a97392

Please sign in to comment.