Update KibanaRequest to use the new WHATWG URL API (#80713)

This commit is contained in:
Thomas Watson 2020-10-29 14:35:48 +01:00 committed by GitHub
parent 275c30a926
commit 1407f713e5
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
46 changed files with 262 additions and 113 deletions

View file

@ -30,9 +30,9 @@ export declare class KibanaRequest<Params = unknown, Query = unknown, Body = unk
| [isSystemRequest](./kibana-plugin-core-server.kibanarequest.issystemrequest.md) | | <code>boolean</code> | Whether or not the request is a "system request" rather than an application-level request. Can be set on the client using the <code>HttpFetchOptions#asSystemRequest</code> option. |
| [params](./kibana-plugin-core-server.kibanarequest.params.md) | | <code>Params</code> | |
| [query](./kibana-plugin-core-server.kibanarequest.query.md) | | <code>Query</code> | |
| [rewrittenUrl](./kibana-plugin-core-server.kibanarequest.rewrittenurl.md) | | <code>Url</code> | URL rewritten in onPreRouting request interceptor. |
| [rewrittenUrl](./kibana-plugin-core-server.kibanarequest.rewrittenurl.md) | | <code>URL</code> | URL rewritten in onPreRouting request interceptor. |
| [route](./kibana-plugin-core-server.kibanarequest.route.md) | | <code>RecursiveReadonly&lt;KibanaRequestRoute&lt;Method&gt;&gt;</code> | matched route details |
| [socket](./kibana-plugin-core-server.kibanarequest.socket.md) | | <code>IKibanaSocket</code> | [IKibanaSocket](./kibana-plugin-core-server.ikibanasocket.md) |
| [url](./kibana-plugin-core-server.kibanarequest.url.md) | | <code>Url</code> | a WHATWG URL standard object. |
| [url](./kibana-plugin-core-server.kibanarequest.url.md) | | <code>URL</code> | a WHATWG URL standard object. |
| [uuid](./kibana-plugin-core-server.kibanarequest.uuid.md) | | <code>string</code> | A UUID to identify this request. |

View file

@ -9,5 +9,5 @@ URL rewritten in onPreRouting request interceptor.
<b>Signature:</b>
```typescript
readonly rewrittenUrl?: Url;
readonly rewrittenUrl?: URL;
```

View file

@ -9,5 +9,5 @@ a WHATWG URL standard object.
<b>Signature:</b>
```typescript
readonly url: Url;
readonly url: URL;
```

View file

@ -16,6 +16,7 @@
* specific language governing permissions and limitations
* under the License.
*/
import { parse as parseUrl } from 'url';
import { Request } from 'hapi';
import { merge } from 'lodash';
import { Socket } from 'net';
@ -72,6 +73,7 @@ function createKibanaRequestMock<P = any, Q = any, B = any>({
auth = { isAuthenticated: true },
}: RequestFixtureOptions<P, Q, B> = {}) {
const queryString = stringify(query, { sort: false });
const url = parseUrl(`${path}${queryString ? `?${queryString}` : ''}`);
return KibanaRequest.from<P, Q, B>(
createRawRequestMock({
@ -83,12 +85,7 @@ function createKibanaRequestMock<P = any, Q = any, B = any>({
payload: body,
path,
method,
url: {
path,
pathname: path,
query: queryString,
search: queryString ? `?${queryString}` : queryString,
},
url,
route: {
settings: { tags: routeTags, auth: routeAuthRequired, app: kibanaRouteOptions },
},
@ -121,6 +118,11 @@ interface DeepPartialArray<T> extends Array<DeepPartial<T>> {}
type DeepPartialObject<T> = { [P in keyof T]+?: DeepPartial<T[P]> };
function createRawRequestMock(customization: DeepPartial<Request> = {}) {
const pathname = customization.url?.pathname || '/';
const path = `${pathname}${customization.url?.search || ''}`;
const url = Object.assign({ pathname, path, href: path }, customization.url);
// @ts-expect-error _core isn't supposed to be accessed - remove once we upgrade to hapi v18
return merge(
{},
{
@ -129,17 +131,21 @@ function createRawRequestMock(customization: DeepPartial<Request> = {}) {
isAuthenticated: true,
},
headers: {},
path: '/',
path,
route: { settings: {} },
url: {
href: '/',
},
url,
raw: {
req: {
url: '/',
url: path,
socket: {},
},
},
// TODO: Remove once we upgrade to hapi v18
_core: {
info: {
uri: 'http://localhost',
},
},
},
customization
) as Request;

View file

@ -271,7 +271,7 @@ export class HttpServer {
}
this.registerOnPreRouting((request, response, toolkit) => {
const oldUrl = request.url.href!;
const oldUrl = request.url.pathname + request.url.search;
const newURL = basePathService.remove(oldUrl);
const shouldRedirect = newURL !== oldUrl;
if (shouldRedirect) {

View file

@ -124,7 +124,13 @@ describe('OnPreRouting', () => {
const router = createRouter('/');
router.get({ path: '/login', validate: false }, (context, req, res) => {
return res.ok({ body: { rewrittenUrl: req.rewrittenUrl?.path } });
return res.ok({
body: {
rewrittenUrl: req.rewrittenUrl
? `${req.rewrittenUrl.pathname}${req.rewrittenUrl.search}`
: undefined,
},
});
});
registerOnPreRouting((req, res, t) => t.rewriteUrl('/login'));
@ -143,7 +149,13 @@ describe('OnPreRouting', () => {
const router = createRouter('/');
router.get({ path: '/reroute-2', validate: false }, (context, req, res) => {
return res.ok({ body: { rewrittenUrl: req.rewrittenUrl?.path } });
return res.ok({
body: {
rewrittenUrl: req.rewrittenUrl
? `${req.rewrittenUrl.pathname}${req.rewrittenUrl.search}`
: undefined,
},
});
});
registerOnPreRouting((req, res, t) => t.rewriteUrl('/reroute-1'));
@ -163,7 +175,13 @@ describe('OnPreRouting', () => {
const router = createRouter('/');
router.get({ path: '/login', validate: false }, (context, req, res) => {
return res.ok({ body: { rewrittenUrl: req.rewrittenUrl?.path } });
return res.ok({
body: {
rewrittenUrl: req.rewrittenUrl
? `${req.rewrittenUrl.pathname}${req.rewrittenUrl.search}`
: undefined,
},
});
});
registerOnPreRouting((req, res, t) => t.next());

View file

@ -17,6 +17,7 @@
* under the License.
*/
import { URL } from 'url';
import { Lifecycle, Request, ResponseToolkit as HapiResponseToolkit } from 'hapi';
import { Logger } from '../../logging';
import {
@ -110,10 +111,30 @@ export function adoptToHapiOnRequest(fn: OnPreRoutingHandler, log: Logger) {
if (preRoutingResult.isRewriteUrl(result)) {
const appState = request.app as KibanaRequestState;
appState.rewrittenUrl = appState.rewrittenUrl ?? request.url;
appState.rewrittenUrl =
// @ts-expect-error request._core isn't supposed to be accessed - remove once we upgrade to hapi v18
appState.rewrittenUrl ?? new URL(request.url.href!, request._core.info.uri);
const { url } = result;
request.setUrl(url);
// TODO: Remove once we upgrade to Node.js 12!
//
// Warning: The following for-loop took 10 days to write, and is a hack
// to force V8 to make a copy of the string in memory.
//
// The reason why we need this is because of what appears to be a bug
// in V8 that caused some URL paths to not be routed correctly once
// `request.setUrl` was called with the path.
//
// The details can be seen in this discussion on Twitter:
// https://twitter.com/wa7son/status/1319992632366518277
let urlCopy = '';
for (let i = 0; i < url.length; i++) {
urlCopy += url[i];
}
request.setUrl(urlCopy);
// We should update raw request as well since it can be proxied to the old platform
request.raw.req.url = url;
return responseToolkit.continue;

View file

@ -17,7 +17,7 @@
* under the License.
*/
import { Url } from 'url';
import { URL } from 'url';
import uuid from 'uuid';
import { Request, RouteOptionsApp, ApplicationState } from 'hapi';
import { Observable, fromEvent, merge } from 'rxjs';
@ -45,7 +45,7 @@ export interface KibanaRouteOptions extends RouteOptionsApp {
export interface KibanaRequestState extends ApplicationState {
requestId: string;
requestUuid: string;
rewrittenUrl?: Url;
rewrittenUrl?: URL;
}
/**
@ -163,7 +163,7 @@ export class KibanaRequest<
*/
public readonly uuid: string;
/** a WHATWG URL standard object. */
public readonly url: Url;
public readonly url: URL;
/** matched route details */
public readonly route: RecursiveReadonly<KibanaRequestRoute<Method>>;
/**
@ -190,7 +190,7 @@ export class KibanaRequest<
/**
* URL rewritten in onPreRouting request interceptor.
*/
public readonly rewrittenUrl?: Url;
public readonly rewrittenUrl?: URL;
/** @internal */
protected readonly [requestSymbol]: Request;
@ -212,7 +212,8 @@ export class KibanaRequest<
this.uuid = appState?.requestUuid ?? uuid.v4();
this.rewrittenUrl = appState?.rewrittenUrl;
this.url = request.url;
// @ts-expect-error request._core isn't supposed to be accessed - remove once we upgrade to hapi v18
this.url = new URL(request.url.href!, request._core.info.uri);
this.headers = deepFreeze({ ...request.headers });
this.isSystemRequest =
request.headers['kbn-system-request'] === 'true' ||
@ -304,8 +305,8 @@ export class KibanaRequest<
if (authOptions === false) return false;
throw new Error(
`unexpected authentication options: ${JSON.stringify(authOptions)} for route: ${
this.url.href
}`
this.url.pathname
}${this.url.search}`
);
}
}

View file

@ -162,7 +162,7 @@ import { Type } from '@kbn/config-schema';
import { TypeOf } from '@kbn/config-schema';
import { UpdateDocumentByQueryParams } from 'elasticsearch';
import { UpdateDocumentParams } from 'elasticsearch';
import { Url } from 'url';
import { URL } from 'url';
// @public
export interface AppCategory {
@ -1007,11 +1007,11 @@ export class KibanaRequest<Params = unknown, Query = unknown, Body = unknown, Me
readonly params: Params;
// (undocumented)
readonly query: Query;
readonly rewrittenUrl?: Url;
readonly rewrittenUrl?: URL;
readonly route: RecursiveReadonly<KibanaRequestRoute<Method>>;
// (undocumented)
readonly socket: IKibanaSocket;
readonly url: Url;
readonly url: URL;
readonly uuid: string;
}

View file

@ -145,6 +145,12 @@ test('executes the task by calling the executor with proper parameters', async (
url: '/',
},
},
// TODO: Remove once we upgrade to hapi v18
_core: {
info: {
uri: 'http://localhost',
},
},
},
});
});
@ -271,6 +277,12 @@ test('uses API key when provided', async () => {
url: '/',
},
},
// TODO: Remove once we upgrade to hapi v18
_core: {
info: {
uri: 'http://localhost',
},
},
},
});
});
@ -310,6 +322,12 @@ test(`doesn't use API key when not provided`, async () => {
url: '/',
},
},
// TODO: Remove once we upgrade to hapi v18
_core: {
info: {
uri: 'http://localhost',
},
},
},
});
});

View file

@ -102,6 +102,12 @@ export class TaskRunnerFactory {
url: '/',
},
},
// TODO: Remove once we upgrade to hapi v18
_core: {
info: {
uri: 'http://localhost',
},
},
} as unknown) as KibanaRequest;
let executorResult: ActionTypeExecutorResult<unknown>;

View file

@ -60,6 +60,12 @@ const fakeRequest = ({
url: '/',
},
},
// TODO: Remove once we upgrade to hapi v18
_core: {
info: {
uri: 'http://localhost',
},
},
getSavedObjectsClient: () => savedObjectsClient,
} as unknown) as Request;

View file

@ -149,6 +149,12 @@ describe('Alerting Plugin', () => {
url: '/',
},
},
// TODO: Remove once we upgrade to hapi v18
_core: {
info: {
uri: 'http://localhost',
},
},
getSavedObjectsClient: jest.fn(),
} as unknown) as KibanaRequest;
await startContract.getAlertsClientWithRequest(fakeRequest);

View file

@ -364,6 +364,12 @@ describe('Task Runner', () => {
url: '/',
},
},
// TODO: Remove once we upgrade to hapi v18
_core: {
info: {
uri: 'http://localhost',
},
},
});
expect(actionsClient.enqueueExecution).toHaveBeenCalledTimes(1);
expect(actionsClient.enqueueExecution.mock.calls[0]).toMatchInlineSnapshot(`
@ -662,6 +668,12 @@ describe('Task Runner', () => {
url: '/',
},
},
// TODO: Remove once we upgrade to hapi v18
_core: {
info: {
uri: 'http://localhost',
},
},
});
});
@ -694,6 +706,12 @@ describe('Task Runner', () => {
url: '/',
},
},
// TODO: Remove once we upgrade to hapi v18
_core: {
info: {
uri: 'http://localhost',
},
},
});
});

View file

@ -101,6 +101,12 @@ export class TaskRunner {
url: '/',
},
},
// TODO: Remove once we upgrade to hapi v18
_core: {
info: {
uri: 'http://localhost',
},
},
} as unknown) as KibanaRequest;
}

View file

@ -4,6 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { parse as parseUrl } from 'url';
import {
OnPostAuthHandler,
OnPostAuthToolkit,
@ -45,7 +46,7 @@ describe('DashboardOnlyModeRequestInterceptor', () => {
test('should not redirects for not app/* requests', async () => {
const request = ({
url: {
path: 'api/test',
pathname: 'api/test',
},
} as unknown) as KibanaRequest;
@ -57,7 +58,7 @@ describe('DashboardOnlyModeRequestInterceptor', () => {
test('should not redirects not authenticated users', async () => {
const request = ({
url: {
path: '/app/home',
pathname: '/app/home',
},
} as unknown) as KibanaRequest;
@ -70,10 +71,9 @@ describe('DashboardOnlyModeRequestInterceptor', () => {
function testRedirectToDashboardModeApp(url: string) {
describe(`requests to url:"${url}"`, () => {
test('redirects to the dashboard_mode app instead', async () => {
const { pathname, search, hash } = parseUrl(url);
const request = ({
url: {
path: url,
},
url: { pathname, search, hash },
credentials: {
roles: [DASHBOARD_ONLY_MODE_ROLE],
},

View file

@ -22,7 +22,7 @@ export const setupDashboardModeRequestInterceptor = ({
getUiSettingsClient,
}: DashboardModeRequestInterceptorDependencies) =>
(async (request, response, toolkit) => {
const path = request.url.path || '';
const path = request.url.pathname;
const isAppRequest = path.startsWith('/app/');
if (!isAppRequest) {

View file

@ -21,7 +21,6 @@ describe('callEnterpriseSearchConfigAPI', () => {
accessCheckTimeoutWarning: 100,
};
const mockRequest = {
url: { path: '/app/kibana' },
headers: { authorization: '==someAuth' },
};
const mockDependencies = {

View file

@ -322,6 +322,12 @@ function FakeRequest(): KibanaRequest {
url: '/',
},
},
// TODO: Remove once we upgrade to hapi v18
_core: {
info: {
uri: 'http://localhost',
},
},
getSavedObjectsClient: () => savedObjectGetter,
} as unknown) as KibanaRequest;
}

View file

@ -56,6 +56,12 @@ function fakeRequest(): KibanaRequest {
url: '/',
},
},
// TODO: Remove once we upgrade to hapi v18
_core: {
info: {
uri: 'http://localhost',
},
},
getSavedObjectsClient: () => savedObjectsClient,
} as unknown) as KibanaRequest;
}

View file

@ -93,6 +93,12 @@ function fakeRequest(): KibanaRequest {
url: '/',
},
},
// TODO: Remove once we upgrade to hapi v18
_core: {
info: {
uri: 'http://localhost',
},
},
getSavedObjectsClient: () => savedObjectsClient,
} as unknown) as KibanaRequest;
}

View file

@ -23,6 +23,12 @@ const fakeRequest = ({
url: '/',
},
},
// TODO: Remove once we upgrade to hapi v18
_core: {
info: {
uri: 'http://localhost',
},
},
} as unknown) as KibanaRequest;
export async function agentPolicyUpdateEventHandler(

View file

@ -23,6 +23,12 @@ function getInternalUserSOClient() {
url: '/',
},
},
// TODO: Remove once we upgrade to hapi v18
_core: {
info: {
uri: 'http://localhost',
},
},
} as unknown) as KibanaRequest;
return appContextService.getInternalUserSOClient(fakeRequest);

View file

@ -58,6 +58,12 @@ function getInternalUserSOClient() {
url: '/',
},
},
// TODO: Remove once we upgrade to hapi v18
_core: {
info: {
uri: 'http://localhost',
},
},
} as unknown) as KibanaRequest;
return appContextService.getInternalUserSOClient(fakeRequest);

View file

@ -456,16 +456,10 @@ export function dataFrameAnalyticsRoutes({ router, mlLicense }: RouteInitializat
},
mlLicense.fullLicenseAPIGuard(async ({ client, request, response }) => {
try {
const options: { id: string; force?: boolean | undefined } = {
const { body } = await client.asInternalUser.ml.stopDataFrameAnalytics({
id: request.params.analyticsId,
};
// @ts-expect-error TODO: update types
if (request.url?.query?.force !== undefined) {
// @ts-expect-error TODO: update types
options.force = request.url.query.force;
}
const { body } = await client.asInternalUser.ml.stopDataFrameAnalytics(options);
force: request.query.force,
});
return response.ok({
body,
});

View file

@ -210,11 +210,18 @@ export class ReportingCore {
}
public getFakeRequest(baseRequest: object, spaceId: string | undefined, logger = this.logger) {
// @ts-expect-error _core isn't supposed to be accessed - remove once we upgrade to hapi v18
const fakeRequest = KibanaRequest.from({
path: '/',
route: { settings: {} },
url: { href: '/' },
raw: { req: { url: '/' } },
// TODO: Remove once we upgrade to hapi v18
_core: {
info: {
uri: 'http://localhost',
},
},
...baseRequest,
} as Hapi.Request);

View file

@ -27,7 +27,7 @@ const getMockContext = () =>
const getMockRequest = () =>
({
url: { port: '5601', query: '', path: '/foo' },
url: { port: '5601', search: '', pathname: '/foo' },
route: { path: '/foo', options: {} },
} as KibanaRequest);

View file

@ -4,6 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { URL } from 'url';
import {
EventOutcome,
SavedObjectAction,
@ -192,11 +193,11 @@ describe('#httpRequestEvent', () => {
},
"message": "User is requesting [/path] endpoint",
"url": Object {
"domain": undefined,
"domain": "localhost",
"path": "/path",
"port": undefined,
"query": undefined,
"scheme": undefined,
"scheme": "http:",
},
}
`);
@ -211,12 +212,7 @@ describe('#httpRequestEvent', () => {
kibanaRequestState: {
requestId: '123',
requestUuid: '123e4567-e89b-12d3-a456-426614174000',
rewrittenUrl: {
path: '/original/path',
pathname: '/original/path',
query: 'query=param',
search: '?query=param',
},
rewrittenUrl: new URL('http://localhost/original/path?query=param'),
},
}),
})
@ -234,11 +230,11 @@ describe('#httpRequestEvent', () => {
},
"message": "User is requesting [/original/path] endpoint",
"url": Object {
"domain": undefined,
"domain": "localhost",
"path": "/original/path",
"port": undefined,
"query": "query=param",
"scheme": undefined,
"scheme": "http:",
},
}
`);

View file

@ -105,10 +105,10 @@ export interface HttpRequestParams {
}
export function httpRequestEvent({ request }: HttpRequestParams): AuditEvent {
const { pathname, search } = request.rewrittenUrl ?? request.url;
const url = request.rewrittenUrl ?? request.url;
return {
message: `User is requesting [${pathname}] endpoint`,
message: `User is requesting [${url.pathname}] endpoint`,
event: {
action: 'http_request',
category: EventCategory.WEB,
@ -120,11 +120,11 @@ export function httpRequestEvent({ request }: HttpRequestParams): AuditEvent {
},
},
url: {
domain: request.url.hostname,
path: pathname,
port: request.url.port ? parseInt(request.url.port, 10) : undefined,
query: search?.slice(1) || undefined,
scheme: request.url.protocol,
domain: url.hostname,
path: url.pathname,
port: url.port ? parseInt(url.port, 10) : undefined,
query: url.search ? url.search.slice(1) : undefined,
scheme: url.protocol,
},
};
}

View file

@ -333,7 +333,7 @@ export class Authenticator {
this.logger.debug('Redirecting request to Login Selector.');
return AuthenticationResult.redirectTo(
`${this.options.basePath.serverBasePath}/login?next=${encodeURIComponent(
`${this.options.basePath.get(request)}${request.url.path}`
`${this.options.basePath.get(request)}${request.url.pathname}${request.url.search}`
)}`
);
}
@ -728,7 +728,7 @@ export class Authenticator {
preAccessRedirectURL = `${preAccessRedirectURL}?next=${encodeURIComponent(
authenticationResult.redirectURL ||
redirectURL ||
`${this.options.basePath.get(request)}${request.url.path}`
`${this.options.basePath.get(request)}${request.url.pathname}${request.url.search}`
)}`;
} else if (redirectURL && !authenticationResult.redirectURL) {
preAccessRedirectURL = redirectURL;

View file

@ -101,13 +101,13 @@ describe('BasicAuthenticationProvider', () => {
await expect(
provider.authenticate(
httpServerMock.createKibanaRequest({
path: '/s/foo/some-path # that needs to be encoded',
path: '/s/foo/some path that needs to be encoded',
}),
null
)
).resolves.toEqual(
AuthenticationResult.redirectTo(
'/mock-server-basepath/login?next=%2Fmock-server-basepath%2Fs%2Ffoo%2Fsome-path%20%23%20that%20needs%20to%20be%20encoded'
'/mock-server-basepath/login?next=%2Fmock-server-basepath%2Fs%2Ffoo%2Fsome%2520path%2520that%2520needs%2520to%2520be%2520encoded'
)
);
});

View file

@ -90,7 +90,9 @@ export class BasicAuthenticationProvider extends BaseAuthenticationProvider {
* @param [state] Optional state object associated with the provider.
*/
public async authenticate(request: KibanaRequest, state?: ProviderState | null) {
this.logger.debug(`Trying to authenticate user request to ${request.url.path}.`);
this.logger.debug(
`Trying to authenticate user request to ${request.url.pathname}${request.url.search}.`
);
if (HTTPAuthorizationHeader.parseFromRequest(request) != null) {
this.logger.debug('Cannot authenticate requests with `Authorization` header.');
@ -106,7 +108,9 @@ export class BasicAuthenticationProvider extends BaseAuthenticationProvider {
this.logger.debug('Redirecting request to Login page.');
const basePath = this.options.basePath.get(request);
return AuthenticationResult.redirectTo(
`${basePath}/login?next=${encodeURIComponent(`${basePath}${request.url.path}`)}`
`${basePath}/login?next=${encodeURIComponent(
`${basePath}${request.url.pathname}${request.url.search}`
)}`
);
}
@ -119,7 +123,7 @@ export class BasicAuthenticationProvider extends BaseAuthenticationProvider {
* @param [state] Optional state object associated with the provider.
*/
public async logout(request: KibanaRequest, state?: ProviderState | null) {
this.logger.debug(`Trying to log user out via ${request.url.path}.`);
this.logger.debug(`Trying to log user out via ${request.url.pathname}${request.url.search}.`);
// Having a `null` state means that provider was specifically called to do a logout, but when
// session isn't defined then provider is just being probed whether or not it can perform logout.

View file

@ -56,7 +56,9 @@ export class HTTPAuthenticationProvider extends BaseAuthenticationProvider {
* @param request Request instance.
*/
public async authenticate(request: KibanaRequest) {
this.logger.debug(`Trying to authenticate user request to ${request.url.path}.`);
this.logger.debug(
`Trying to authenticate user request to ${request.url.pathname}${request.url.search}.`
);
const authorizationHeader = HTTPAuthorizationHeader.parseFromRequest(request);
if (authorizationHeader == null) {
@ -72,12 +74,12 @@ export class HTTPAuthenticationProvider extends BaseAuthenticationProvider {
try {
const user = await this.getUser(request);
this.logger.debug(
`Request to ${request.url.path} has been authenticated via authorization header with "${authorizationHeader.scheme}" scheme.`
`Request to ${request.url.pathname}${request.url.search} has been authenticated via authorization header with "${authorizationHeader.scheme}" scheme.`
);
return AuthenticationResult.succeeded(user);
} catch (err) {
this.logger.debug(
`Failed to authenticate request to ${request.url.path} via authorization header with "${authorizationHeader.scheme}" scheme: ${err.message}`
`Failed to authenticate request to ${request.url.pathname}${request.url.search} via authorization header with "${authorizationHeader.scheme}" scheme: ${err.message}`
);
return AuthenticationResult.failed(err);
}

View file

@ -65,7 +65,9 @@ export class KerberosAuthenticationProvider extends BaseAuthenticationProvider {
* @param [state] Optional state object associated with the provider.
*/
public async authenticate(request: KibanaRequest, state?: ProviderState | null) {
this.logger.debug(`Trying to authenticate user request to ${request.url.path}.`);
this.logger.debug(
`Trying to authenticate user request to ${request.url.pathname}${request.url.search}.`
);
const authorizationHeader = HTTPAuthorizationHeader.parseFromRequest(request);
if (authorizationHeader && authorizationHeader.scheme.toLowerCase() !== 'negotiate') {
@ -100,7 +102,7 @@ export class KerberosAuthenticationProvider extends BaseAuthenticationProvider {
* @param state State value previously stored by the provider.
*/
public async logout(request: KibanaRequest, state?: ProviderState | null) {
this.logger.debug(`Trying to log user out via ${request.url.path}.`);
this.logger.debug(`Trying to log user out via ${request.url.pathname}${request.url.search}.`);
// Having a `null` state means that provider was specifically called to do a logout, but when
// session isn't defined then provider is just being probed whether or not it can perform logout.

View file

@ -166,7 +166,9 @@ export class OIDCAuthenticationProvider extends BaseAuthenticationProvider {
* @param [state] Optional state object associated with the provider.
*/
public async authenticate(request: KibanaRequest, state?: ProviderState | null) {
this.logger.debug(`Trying to authenticate user request to ${request.url.path}.`);
this.logger.debug(
`Trying to authenticate user request to ${request.url.pathname}${request.url.search}.`
);
if (HTTPAuthorizationHeader.parseFromRequest(request) != null) {
this.logger.debug('Cannot authenticate requests with `Authorization` header.');
@ -418,7 +420,7 @@ export class OIDCAuthenticationProvider extends BaseAuthenticationProvider {
* @param state State value previously stored by the provider.
*/
public async logout(request: KibanaRequest, state?: ProviderState | null) {
this.logger.debug(`Trying to log user out via ${request.url.path}.`);
this.logger.debug(`Trying to log user out via ${request.url.pathname}${request.url.search}.`);
// Having a `null` state means that provider was specifically called to do a logout, but when
// session isn't defined then provider is just being probed whether or not it can perform logout.
@ -477,7 +479,7 @@ export class OIDCAuthenticationProvider extends BaseAuthenticationProvider {
`${
this.options.basePath.serverBasePath
}/internal/security/capture-url?next=${encodeURIComponent(
`${this.options.basePath.get(request)}${request.url.path}`
`${this.options.basePath.get(request)}${request.url.pathname}${request.url.search}`
)}&providerType=${encodeURIComponent(this.type)}&providerName=${encodeURIComponent(
this.options.name
)}`,

View file

@ -61,7 +61,9 @@ export class PKIAuthenticationProvider extends BaseAuthenticationProvider {
* @param [state] Optional state object associated with the provider.
*/
public async authenticate(request: KibanaRequest, state?: ProviderState | null) {
this.logger.debug(`Trying to authenticate user request to ${request.url.path}.`);
this.logger.debug(
`Trying to authenticate user request to ${request.url.pathname}${request.url.search}.`
);
if (HTTPAuthorizationHeader.parseFromRequest(request) != null) {
this.logger.debug('Cannot authenticate requests with `Authorization` header.');
@ -105,7 +107,7 @@ export class PKIAuthenticationProvider extends BaseAuthenticationProvider {
* @param state State value previously stored by the provider.
*/
public async logout(request: KibanaRequest, state?: ProviderState | null) {
this.logger.debug(`Trying to log user out via ${request.url.path}.`);
this.logger.debug(`Trying to log user out via ${request.url.pathname}${request.url.search}.`);
// Having a `null` state means that provider was specifically called to do a logout, but when
// session isn't defined then provider is just being probed whether or not it can perform logout.

View file

@ -193,7 +193,9 @@ export class SAMLAuthenticationProvider extends BaseAuthenticationProvider {
* @param [state] Optional state object associated with the provider.
*/
public async authenticate(request: KibanaRequest, state?: ProviderState | null) {
this.logger.debug(`Trying to authenticate user request to ${request.url.path}.`);
this.logger.debug(
`Trying to authenticate user request to ${request.url.pathname}${request.url.search}`
);
if (HTTPAuthorizationHeader.parseFromRequest(request) != null) {
this.logger.debug('Cannot authenticate requests with `Authorization` header.');
@ -232,7 +234,7 @@ export class SAMLAuthenticationProvider extends BaseAuthenticationProvider {
* @param state State value previously stored by the provider.
*/
public async logout(request: KibanaRequest, state?: ProviderState | null) {
this.logger.debug(`Trying to log user out via ${request.url.path}.`);
this.logger.debug(`Trying to log user out via ${request.url.pathname}${request.url.search}.`);
// Normally when there is no active session in Kibana, `logout` method shouldn't do anything
// and user will eventually be redirected to the home page to log in. But when SAML SLO is
@ -631,7 +633,7 @@ export class SAMLAuthenticationProvider extends BaseAuthenticationProvider {
`${
this.options.basePath.serverBasePath
}/internal/security/capture-url?next=${encodeURIComponent(
`${this.options.basePath.get(request)}${request.url.path}`
`${this.options.basePath.get(request)}${request.url.pathname}${request.url.search}`
)}&providerType=${encodeURIComponent(this.type)}&providerName=${encodeURIComponent(
this.options.name
)}`,

View file

@ -173,13 +173,13 @@ describe('TokenAuthenticationProvider', () => {
await expect(
provider.authenticate(
httpServerMock.createKibanaRequest({
path: '/s/foo/some-path # that needs to be encoded',
path: '/s/foo/some path that needs to be encoded',
}),
null
)
).resolves.toEqual(
AuthenticationResult.redirectTo(
'/mock-server-basepath/login?next=%2Fmock-server-basepath%2Fs%2Ffoo%2Fsome-path%20%23%20that%20needs%20to%20be%20encoded'
'/mock-server-basepath/login?next=%2Fmock-server-basepath%2Fs%2Ffoo%2Fsome%2520path%2520that%2520needs%2520to%2520be%2520encoded'
)
);
});

View file

@ -92,7 +92,9 @@ export class TokenAuthenticationProvider extends BaseAuthenticationProvider {
* @param [state] Optional state object associated with the provider.
*/
public async authenticate(request: KibanaRequest, state?: ProviderState | null) {
this.logger.debug(`Trying to authenticate user request to ${request.url.path}.`);
this.logger.debug(
`Trying to authenticate user request to ${request.url.pathname}${request.url.search}.`
);
if (HTTPAuthorizationHeader.parseFromRequest(request) != null) {
this.logger.debug('Cannot authenticate requests with `Authorization` header.');
@ -126,7 +128,7 @@ export class TokenAuthenticationProvider extends BaseAuthenticationProvider {
* @param state State value previously stored by the provider.
*/
public async logout(request: KibanaRequest, state?: ProviderState | null) {
this.logger.debug(`Trying to log user out via ${request.url.path}.`);
this.logger.debug(`Trying to log user out via ${request.url.pathname}${request.url.search}.`);
// Having a `null` state means that provider was specifically called to do a logout, but when
// session isn't defined then provider is just being probed whether or not it can perform logout.
@ -241,7 +243,9 @@ export class TokenAuthenticationProvider extends BaseAuthenticationProvider {
* @param request Request instance.
*/
private getLoginPageURL(request: KibanaRequest) {
const nextURL = encodeURIComponent(`${this.options.basePath.get(request)}${request.url.path}`);
const nextURL = encodeURIComponent(
`${this.options.basePath.get(request)}${request.url.pathname}${request.url.search}`
);
return `${this.options.basePath.get(request)}/login?next=${nextURL}`;
}
}

View file

@ -33,11 +33,13 @@ export function initAPIAuthorization(
// we've actually authorized the request
if (checkPrivilegesResponse.hasAllRequested) {
logger.debug(`User authorized for "${request.url.path}"`);
logger.debug(`User authorized for "${request.url.pathname}${request.url.search}"`);
return toolkit.next();
}
logger.warn(`User not authorized for "${request.url.path}": responding with 403`);
logger.warn(
`User not authorized for "${request.url.pathname}${request.url.search}": responding with 403`
);
return response.forbidden();
});
}

View file

@ -168,7 +168,7 @@ export class AuthorizationService {
http.registerOnPreResponse((request, preResponse, toolkit) => {
if (preResponse.statusCode === 403 && canRedirectRequest(request)) {
const basePath = http.basePath.get(request);
const next = `${basePath}${request.url.path}`;
const next = `${basePath}${request.url.pathname}${request.url.search}`;
const regularBundlePath = `${basePath}/${buildNumber}/bundles`;
const logoutUrl = http.basePath.prepend(

View file

@ -135,7 +135,7 @@ export function defineOIDCRoutes({
loginAttempt = {
type: OIDCLogin.LoginWithAuthorizationCodeFlow,
// We pass the path only as we can't be sure of the full URL and Elasticsearch doesn't need it anyway.
authenticationResponseURI: request.url.path!,
authenticationResponseURI: request.url.pathname + request.url.search,
};
} else if (request.query.iss) {
logger.warn(

View file

@ -100,7 +100,7 @@ describe('Login view routes', () => {
auth: { isAuthenticated: true },
});
(request as any).url = new URL(
`${request.url.path}${request.url.search}`,
`${request.url.pathname}${request.url.search}`,
'https://kibana.co'
);
license.getFeatures.mockReturnValue({ showLogin: true } as any);
@ -114,7 +114,7 @@ describe('Login view routes', () => {
// Redirect if `showLogin` is `false` even if user is not authenticated.
request = httpServerMock.createKibanaRequest({ query, auth: { isAuthenticated: false } });
(request as any).url = new URL(
`${request.url.path}${request.url.search}`,
`${request.url.pathname}${request.url.search}`,
'https://kibana.co'
);
license.getFeatures.mockReturnValue({ showLogin: false } as any);

View file

@ -28,7 +28,7 @@ export function initSpacesOnPostAuthRequestInterceptor({
http.registerOnPostAuth(async (request, response, toolkit) => {
const serverBasePath = http.basePath.serverBasePath;
const path = request.url.pathname!;
const path = request.url.pathname;
const spaceId = spacesService.getSpaceId(request);

View file

@ -9,8 +9,6 @@ import {
LifecycleResponseFactory,
CoreSetup,
} from 'src/core/server';
import { format } from 'url';
import { modifyUrl } from '../utils/url';
import { getSpaceIdFromPath } from '../../../common';
export interface OnRequestInterceptorDeps {
@ -34,16 +32,9 @@ export function initSpacesOnRequestInterceptor({ http }: OnRequestInterceptorDep
http.basePath.set(request, reqBasePath);
const newLocation = (path && path.substr(reqBasePath.length)) || '/';
const newPathname = path.substr(reqBasePath.length) || '/';
const newUrl = modifyUrl(format(request.url), (parts) => {
return {
...parts,
pathname: newLocation,
};
});
return toolkit.rewriteUrl(newUrl);
return toolkit.rewriteUrl(`${newPathname}${request.url.search}`);
}
return toolkit.next();

View file

@ -58,7 +58,7 @@ const createService = async (serverBasePath: string = '') => {
serverBasePath,
} as HttpServiceSetup['basePath'];
httpSetup.basePath.get = jest.fn().mockImplementation((request: KibanaRequest) => {
const { spaceId } = getSpaceIdFromPath(request.url.path);
const { spaceId } = getSpaceIdFromPath(request.url.pathname);
if (spaceId !== DEFAULT_SPACE_ID) {
return `/s/${spaceId}`;
@ -83,7 +83,7 @@ describe('SpacesService', () => {
const spacesServiceSetup = await createService();
const request: KibanaRequest = {
url: { path: '/app/kibana' },
url: { pathname: '/app/kibana' },
} as KibanaRequest;
expect(spacesServiceSetup.getSpaceId(request)).toEqual(DEFAULT_SPACE_ID);
@ -93,7 +93,7 @@ describe('SpacesService', () => {
const spacesServiceSetup = await createService();
const request: KibanaRequest = {
url: { path: '/s/foo/app/kibana' },
url: { pathname: '/s/foo/app/kibana' },
} as KibanaRequest;
expect(spacesServiceSetup.getSpaceId(request)).toEqual('foo');
@ -140,7 +140,7 @@ describe('SpacesService', () => {
const spacesServiceSetup = await createService();
const request: KibanaRequest = {
url: { path: '/app/kibana' },
url: { pathname: '/app/kibana' },
} as KibanaRequest;
expect(spacesServiceSetup.isInDefaultSpace(request)).toEqual(true);
@ -150,7 +150,7 @@ describe('SpacesService', () => {
const spacesServiceSetup = await createService();
const request: KibanaRequest = {
url: { path: '/s/foo/app/kibana' },
url: { pathname: '/s/foo/app/kibana' },
} as KibanaRequest;
expect(spacesServiceSetup.isInDefaultSpace(request)).toEqual(false);