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

[7.x] Add an optional authentication mode for HTTP resources (#58589) #59614

Merged
merged 1 commit into from
Mar 7, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->

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

## AuthNotHandled interface


<b>Signature:</b>

```typescript
export interface AuthNotHandled
```

## Properties

| Property | Type | Description |
| --- | --- | --- |
| [type](./kibana-plugin-server.authnothandled.type.md) | <code>AuthResultType.notHandled</code> | |

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; [AuthNotHandled](./kibana-plugin-server.authnothandled.md) &gt; [type](./kibana-plugin-server.authnothandled.type.md)

## AuthNotHandled.type property

<b>Signature:</b>

```typescript
type: AuthResultType.notHandled;
```
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->

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

## AuthRedirected interface


<b>Signature:</b>

```typescript
export interface AuthRedirected extends AuthRedirectedParams
```
## Properties
| Property | Type | Description |
| --- | --- | --- |
| [type](./kibana-plugin-server.authredirected.type.md) | <code>AuthResultType.redirected</code> | |
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; [AuthRedirected](./kibana-plugin-server.authredirected.md) &gt; [type](./kibana-plugin-server.authredirected.type.md)

## AuthRedirected.type property

<b>Signature:</b>

```typescript
type: AuthResultType.redirected;
```
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->

[Home](./index.md) &gt; [kibana-plugin-server](./kibana-plugin-server.md) &gt; [AuthRedirectedParams](./kibana-plugin-server.authredirectedparams.md) &gt; [headers](./kibana-plugin-server.authredirectedparams.headers.md)

## AuthRedirectedParams.headers property

Headers to attach for auth redirect. Must include "location" header

<b>Signature:</b>

```typescript
headers: {
location: string;
} & ResponseHeaders;
```
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->

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

## AuthRedirectedParams interface

Result of auth redirection.

<b>Signature:</b>

```typescript
export interface AuthRedirectedParams
```

## Properties

| Property | Type | Description |
| --- | --- | --- |
| [headers](./kibana-plugin-server.authredirectedparams.headers.md) | <code>{</code><br/><code> location: string;</code><br/><code> } &amp; ResponseHeaders</code> | Headers to attach for auth redirect. Must include "location" header |

Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,5 @@
<b>Signature:</b>

```typescript
export declare type AuthResult = Authenticated;
export declare type AuthResult = Authenticated | AuthNotHandled | AuthRedirected;
```
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

## AuthResultParams interface

Result of an incoming request authentication.
Result of successful authentication.

<b>Signature:</b>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,6 @@ export declare enum AuthResultType
| Member | Value | Description |
| --- | --- | --- |
| authenticated | <code>&quot;authenticated&quot;</code> | |
| notHandled | <code>&quot;notHandled&quot;</code> | |
| redirected | <code>&quot;redirected&quot;</code> | |

Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,6 @@ export interface AuthToolkit
| Property | Type | Description |
| --- | --- | --- |
| [authenticated](./kibana-plugin-server.authtoolkit.authenticated.md) | <code>(data?: AuthResultParams) =&gt; AuthResult</code> | Authentication is successful with given credentials, allow request to pass through |
| [notHandled](./kibana-plugin-server.authtoolkit.nothandled.md) | <code>() =&gt; AuthResult</code> | User has no credentials. Allows user to access a resource when authRequired: 'optional' Rejects a request when authRequired: true |
| [redirected](./kibana-plugin-server.authtoolkit.redirected.md) | <code>(headers: {</code><br/><code> location: string;</code><br/><code> } &amp; ResponseHeaders) =&gt; AuthResult</code> | Redirect user to IdP when authRequired: true Allows user to access a resource without redirection when authRequired: 'optional' |

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

[Home](./index.md) &gt; [kibana-plugin-server](./kibana-plugin-server.md) &gt; [AuthToolkit](./kibana-plugin-server.authtoolkit.md) &gt; [notHandled](./kibana-plugin-server.authtoolkit.nothandled.md)

## AuthToolkit.notHandled property

User has no credentials. Allows user to access a resource when authRequired: 'optional' Rejects a request when authRequired: true

<b>Signature:</b>

```typescript
notHandled: () => AuthResult;
```
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->

[Home](./index.md) &gt; [kibana-plugin-server](./kibana-plugin-server.md) &gt; [AuthToolkit](./kibana-plugin-server.authtoolkit.md) &gt; [redirected](./kibana-plugin-server.authtoolkit.redirected.md)

## AuthToolkit.redirected property

Redirect user to IdP when authRequired: true Allows user to access a resource without redirection when authRequired: 'optional'

<b>Signature:</b>

```typescript
redirected: (headers: {
location: string;
} & ResponseHeaders) => AuthResult;
```
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->

[Home](./index.md) &gt; [kibana-plugin-server](./kibana-plugin-server.md) &gt; [KibanaRequest](./kibana-plugin-server.kibanarequest.md) &gt; [auth](./kibana-plugin-server.kibanarequest.auth.md)

## KibanaRequest.auth property

<b>Signature:</b>

```typescript
readonly auth: {
isAuthenticated: boolean;
};
```
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ export declare class KibanaRequest<Params = unknown, Query = unknown, Body = unk

| Property | Modifiers | Type | Description |
| --- | --- | --- | --- |
| [auth](./kibana-plugin-server.kibanarequest.auth.md) | | <code>{</code><br/><code> isAuthenticated: boolean;</code><br/><code> }</code> | |
| [body](./kibana-plugin-server.kibanarequest.body.md) | | <code>Body</code> | |
| [events](./kibana-plugin-server.kibanarequest.events.md) | | <code>KibanaRequestEvents</code> | Request events [KibanaRequestEvents](./kibana-plugin-server.kibanarequestevents.md) |
| [headers](./kibana-plugin-server.kibanarequest.headers.md) | | <code>Headers</code> | Readonly copy of incoming request headers. |
Expand Down
5 changes: 4 additions & 1 deletion docs/development/core/server/kibana-plugin-server.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,10 @@ The plugin integrates with the core system via lifecycle events: `setup`<!-- -->
| [AssistanceAPIResponse](./kibana-plugin-server.assistanceapiresponse.md) | |
| [AssistantAPIClientParams](./kibana-plugin-server.assistantapiclientparams.md) | |
| [Authenticated](./kibana-plugin-server.authenticated.md) | |
| [AuthResultParams](./kibana-plugin-server.authresultparams.md) | Result of an incoming request authentication. |
| [AuthNotHandled](./kibana-plugin-server.authnothandled.md) | |
| [AuthRedirected](./kibana-plugin-server.authredirected.md) | |
| [AuthRedirectedParams](./kibana-plugin-server.authredirectedparams.md) | Result of auth redirection. |
| [AuthResultParams](./kibana-plugin-server.authresultparams.md) | Result of successful authentication. |
| [AuthToolkit](./kibana-plugin-server.authtoolkit.md) | A tool set defining an outcome of Auth interceptor for incoming request. |
| [CallAPIOptions](./kibana-plugin-server.callapioptions.md) | The set of options that defines how API call should be made and result be processed. |
| [Capabilities](./kibana-plugin-server.capabilities.md) | The read-only set of capabilities available for the current UI session. Capabilities are simple key-value pairs of (string, boolean), where the string denotes the capability ID, and the boolean is a flag indicating if the capability is enabled or disabled. |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,12 @@

## RouteConfigOptions.authRequired property

A flag shows that authentication for a route: `enabled` when true `disabled` when false
Defines authentication mode for a route: - true. A user has to have valid credentials to access a resource - false. A user can access a resource without any credentials. - 'optional'. A user can access a resource if has valid credentials or no credentials at all. Can be useful when we grant access to a resource but want to identify a user if possible.

Enabled by default.
Defaults to `true` if an auth mechanism is registered.

<b>Signature:</b>

```typescript
authRequired?: boolean;
authRequired?: boolean | 'optional';
```
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ export interface RouteConfigOptions<Method extends RouteMethod>

| Property | Type | Description |
| --- | --- | --- |
| [authRequired](./kibana-plugin-server.routeconfigoptions.authrequired.md) | <code>boolean</code> | A flag shows that authentication for a route: <code>enabled</code> when true <code>disabled</code> when false<!-- -->Enabled by default. |
| [authRequired](./kibana-plugin-server.routeconfigoptions.authrequired.md) | <code>boolean &#124; 'optional'</code> | Defines authentication mode for a route: - true. A user has to have valid credentials to access a resource - false. A user can access a resource without any credentials. - 'optional'. A user can access a resource if has valid credentials or no credentials at all. Can be useful when we grant access to a resource but want to identify a user if possible.<!-- -->Defaults to <code>true</code> if an auth mechanism is registered. |
| [body](./kibana-plugin-server.routeconfigoptions.body.md) | <code>Method extends 'get' &#124; 'options' ? undefined : RouteConfigOptionsBody</code> | Additional body options [RouteConfigOptionsBody](./kibana-plugin-server.routeconfigoptionsbody.md)<!-- -->. |
| [tags](./kibana-plugin-server.routeconfigoptions.tags.md) | <code>readonly string[]</code> | Additional metadata tag strings to attach to the route. |
| [xsrfRequired](./kibana-plugin-server.routeconfigoptions.xsrfrequired.md) | <code>Method extends 'get' ? never : boolean</code> | Defines xsrf protection requirements for a route: - true. Requires an incoming POST/PUT/DELETE request to contain <code>kbn-xsrf</code> header. - false. Disables xsrf protection.<!-- -->Set to true by default |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,7 @@ export interface CapabilitiesStart {
*/
export class CapabilitiesService {
public async start({ appIds, http }: StartDeps): Promise<CapabilitiesStart> {
const route = http.anonymousPaths.isAnonymous(window.location.pathname) ? '/defaults' : '';
const capabilities = await http.post<Capabilities>(`/api/core/capabilities${route}`, {
const capabilities = await http.post<Capabilities>('/api/core/capabilities', {
body: JSON.stringify({ applications: appIds }),
});

Expand Down
4 changes: 2 additions & 2 deletions src/core/server/capabilities/capabilities_service.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,8 @@ describe('CapabilitiesService', () => {
});

it('registers the capabilities routes', async () => {
expect(http.createRouter).toHaveBeenCalledWith('/api/core/capabilities');
expect(router.post).toHaveBeenCalledTimes(2);
expect(http.createRouter).toHaveBeenCalledWith('');
expect(router.post).toHaveBeenCalledTimes(1);
expect(router.post).toHaveBeenCalledWith(expect.any(Object), expect.any(Function));
});

Expand Down
2 changes: 1 addition & 1 deletion src/core/server/capabilities/routes/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,6 @@ import { InternalHttpServiceSetup } from '../../http';
import { registerCapabilitiesRoutes } from './resolve_capabilities';

export function registerRoutes(http: InternalHttpServiceSetup, resolver: CapabilitiesResolver) {
const router = http.createRouter('/api/core/capabilities');
const router = http.createRouter('');
registerCapabilitiesRoutes(router, resolver);
}
44 changes: 19 additions & 25 deletions src/core/server/capabilities/routes/resolve_capabilities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,30 +22,24 @@ import { IRouter } from '../../http';
import { CapabilitiesResolver } from '../resolve_capabilities';

export function registerCapabilitiesRoutes(router: IRouter, resolver: CapabilitiesResolver) {
// Capabilities are fetched on both authenticated and anonymous routes.
// However when `authRequired` is false, authentication is not performed
// and only default capabilities are returned (all disabled), even for authenticated users.
// So we need two endpoints to handle both scenarios.
[true, false].forEach(authRequired => {
router.post(
{
path: authRequired ? '' : '/defaults',
options: {
authRequired,
},
validate: {
body: schema.object({
applications: schema.arrayOf(schema.string()),
}),
},
router.post(
{
path: '/api/core/capabilities',
options: {
authRequired: 'optional',
},
async (ctx, req, res) => {
const { applications } = req.body;
const capabilities = await resolver(req, applications);
return res.ok({
body: capabilities,
});
}
);
});
validate: {
body: schema.object({
applications: schema.arrayOf(schema.string()),
}),
},
},
async (ctx, req, res) => {
const { applications } = req.body;
const capabilities = await resolver(req, applications);
return res.ok({
body: capabilities,
});
}
);
}
6 changes: 6 additions & 0 deletions src/core/server/http/http_server.mocks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import { OnPostAuthToolkit } from './lifecycle/on_post_auth';
import { OnPreAuthToolkit } from './lifecycle/on_pre_auth';

interface RequestFixtureOptions<P = any, Q = any, B = any> {
auth?: { isAuthenticated: boolean };
headers?: Record<string, string>;
params?: Record<string, any>;
body?: Record<string, any>;
Expand Down Expand Up @@ -65,11 +66,13 @@ function createKibanaRequestMock<P = any, Q = any, B = any>({
routeAuthRequired,
validation = {},
kibanaRouteState = { xsrfRequired: true },
auth = { isAuthenticated: true },
}: RequestFixtureOptions<P, Q, B> = {}) {
const queryString = stringify(query, { sort: false });

return KibanaRequest.from<P, Q, B>(
createRawRequestMock({
auth,
headers,
params,
query,
Expand Down Expand Up @@ -113,6 +116,9 @@ function createRawRequestMock(customization: DeepPartial<Request> = {}) {
{},
{
app: { xsrfRequired: true } as any,
auth: {
isAuthenticated: true,
},
headers: {},
path: '/',
route: { settings: {} },
Expand Down
24 changes: 19 additions & 5 deletions src/core/server/http/http_server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,7 @@ import { adoptToHapiAuthFormat, AuthenticationHandler } from './lifecycle/auth';
import { adoptToHapiOnPostAuthFormat, OnPostAuthHandler } from './lifecycle/on_post_auth';
import { adoptToHapiOnPreAuthFormat, OnPreAuthHandler } from './lifecycle/on_pre_auth';
import { adoptToHapiOnPreResponseFormat, OnPreResponseHandler } from './lifecycle/on_pre_response';

import { IRouter, KibanaRouteState, isSafeMethod } from './router';
import { IRouter, RouteConfigOptions, KibanaRouteState, isSafeMethod } from './router';
import {
SessionStorageCookieOptions,
createCookieSessionStorageFactory,
Expand Down Expand Up @@ -148,7 +147,7 @@ export class HttpServer {
this.log.debug(`registering route handler for [${route.path}]`);
// Hapi does not allow payload validation to be specified for 'head' or 'get' requests
const validate = isSafeMethod(route.method) ? undefined : { payload: true };
const { authRequired = true, tags, body = {} } = route.options;
const { authRequired, tags, body = {} } = route.options;
const { accepts: allow, maxBytes, output, parse } = body;

const kibanaRouteState: KibanaRouteState = {
Expand All @@ -160,8 +159,7 @@ export class HttpServer {
method: route.method,
path: route.path,
options: {
// Enforcing the comparison with true because plugins could overwrite the auth strategy by doing `options: { authRequired: authStrategy as any }`
auth: authRequired === true ? undefined : false,
auth: this.getAuthOption(authRequired),
app: kibanaRouteState,
tags: tags ? Array.from(tags) : undefined,
// TODO: This 'validate' section can be removed once the legacy platform is completely removed.
Expand Down Expand Up @@ -196,6 +194,22 @@ export class HttpServer {
this.server = undefined;
}

private getAuthOption(
authRequired: RouteConfigOptions<any>['authRequired'] = true
): undefined | false | { mode: 'required' | 'optional' } {
if (this.authRegistered === false) return undefined;

if (authRequired === true) {
return { mode: 'required' };
}
if (authRequired === 'optional') {
return { mode: 'optional' };
}
if (authRequired === false) {
return false;
}
}

private setupBasePathRewrite(config: HttpConfig, basePathService: BasePath) {
if (config.basePath === undefined || !config.rewriteBasePath) {
return;
Expand Down
2 changes: 2 additions & 0 deletions src/core/server/http/http_service.mock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,8 @@ const createOnPostAuthToolkitMock = (): jest.Mocked<OnPostAuthToolkit> => ({

const createAuthToolkitMock = (): jest.Mocked<AuthToolkit> => ({
authenticated: jest.fn(),
notHandled: jest.fn(),
redirected: jest.fn(),
});

const createOnPreResponseToolkitMock = (): jest.Mocked<OnPreResponseToolkit> => ({
Expand Down
Loading