Skip to content

Commit

Permalink
Short URLs (#107859) (#113228)
Browse files Browse the repository at this point in the history
Co-authored-by: Vadim Kibana <82822460+vadimkibana@users.noreply.github.com>
  • Loading branch information
kibanamachine and vadimkibana authored Sep 28, 2021
1 parent bd669de commit 8e6b834
Show file tree
Hide file tree
Showing 68 changed files with 2,177 additions and 788 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -319,6 +319,7 @@
"puid": "1.0.7",
"puppeteer": "^8.0.0",
"query-string": "^6.13.2",
"random-word-slugs": "^0.0.5",
"raw-loader": "^3.1.0",
"rbush": "^3.0.1",
"re-resizable": "^6.1.1",
Expand Down
16 changes: 16 additions & 0 deletions src/plugins/share/common/url_service/__tests__/setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,22 @@ export const urlServiceTestSetup = (partialDeps: Partial<UrlServiceDependencies>
getUrl: async () => {
throw new Error('not implemented');
},
shortUrls: {
get: () => ({
create: async () => {
throw new Error('Not implemented.');
},
get: async () => {
throw new Error('Not implemented.');
},
delete: async () => {
throw new Error('Not implemented.');
},
resolve: async () => {
throw new Error('Not implemented.');
},
}),
},
...partialDeps,
};
const service = new UrlService(deps);
Expand Down
1 change: 1 addition & 0 deletions src/plugins/share/common/url_service/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,4 @@

export * from './url_service';
export * from './locators';
export * from './short_urls';
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/

import type { SerializableRecord } from '@kbn/utility-types';
import type { KibanaLocation, LocatorDefinition } from '../../url_service';
import { shortUrlAssertValid } from './short_url_assert_valid';

export const LEGACY_SHORT_URL_LOCATOR_ID = 'LEGACY_SHORT_URL_LOCATOR';

export interface LegacyShortUrlLocatorParams extends SerializableRecord {
url: string;
}

export class LegacyShortUrlLocatorDefinition
implements LocatorDefinition<LegacyShortUrlLocatorParams>
{
public readonly id = LEGACY_SHORT_URL_LOCATOR_ID;

public async getLocation(params: LegacyShortUrlLocatorParams): Promise<KibanaLocation> {
const { url } = params;

shortUrlAssertValid(url);

const match = url.match(/^.*\/app\/([^\/#]+)(.+)$/);

if (!match) {
throw new Error('Unexpected URL path.');
}

const [, app, path] = match;

if (!app || !path) {
throw new Error('Could not parse URL path.');
}

return {
app,
path,
state: {},
};
}
}
2 changes: 2 additions & 0 deletions src/plugins/share/common/url_service/locators/locator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,12 +43,14 @@ export interface LocatorDependencies {
}

export class Locator<P extends SerializableRecord> implements LocatorPublic<P> {
public readonly id: string;
public readonly migrations: PersistableState<P>['migrations'];

constructor(
public readonly definition: LocatorDefinition<P>,
protected readonly deps: LocatorDependencies
) {
this.id = definition.id;
this.migrations = definition.migrations || {};
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/

import { shortUrlAssertValid } from './short_url_assert_valid';

describe('shortUrlAssertValid()', () => {
const invalid = [
['protocol', 'http://localhost:5601/app/kibana'],
['protocol', 'https://localhost:5601/app/kibana'],
['protocol', 'mailto:foo@bar.net'],
['protocol', 'javascript:alert("hi")'], // eslint-disable-line no-script-url
['hostname', 'localhost/app/kibana'], // according to spec, this is not a valid URL -- you cannot specify a hostname without a protocol
['hostname and port', 'local.host:5601/app/kibana'], // parser detects 'local.host' as the protocol
['hostname and auth', 'user:pass@localhost.net/app/kibana'], // parser detects 'user' as the protocol
['path traversal', '/app/../../not-kibana'], // fails because there are >2 path parts
['path traversal', '/../not-kibana'], // fails because first path part is not 'app'
['base path', '/base/app/kibana'], // fails because there are >2 path parts
['path with an extra leading slash', '//foo/app/kibana'], // parser detects 'foo' as the hostname
['path with an extra leading slash', '///app/kibana'], // parser detects '' as the hostname
['path without app', '/foo/kibana'], // fails because first path part is not 'app'
['path without appId', '/app/'], // fails because there is only one path part (leading and trailing slashes are trimmed)
];

invalid.forEach(([desc, url, error]) => {
it(`fails when url has ${desc as string}`, () => {
expect(() => shortUrlAssertValid(url as string)).toThrow();
});
});

const valid = [
'/app/kibana',
'/app/kibana/', // leading and trailing slashes are trimmed
'/app/monitoring#angular/route',
'/app/text#document-id',
'/app/some?with=query',
'/app/some?with=query#and-a-hash',
];

valid.forEach((url) => {
it(`allows ${url}`, () => {
shortUrlAssertValid(url);
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/

const REGEX = /^\/app\/[^/]+.+$/;

export function shortUrlAssertValid(url: string) {
if (!REGEX.test(url) || url.includes('/../')) throw new Error(`Invalid short URL: ${url}`);
}
2 changes: 2 additions & 0 deletions src/plugins/share/common/url_service/locators/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,8 @@ export interface LocatorDefinition<P extends SerializableRecord>
* Public interface of a registered locator.
*/
export interface LocatorPublic<P extends SerializableRecord> extends PersistableState<P> {
readonly id: string;

/**
* Returns a reference to a Kibana client-side location.
*
Expand Down
16 changes: 16 additions & 0 deletions src/plugins/share/common/url_service/mocks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,22 @@ export class MockUrlService extends UrlService {
getUrl: async ({ app, path }, { absolute }) => {
return `${absolute ? 'http://localhost:8888' : ''}/app/${app}${path}`;
},
shortUrls: {
get: () => ({
create: async () => {
throw new Error('Not implemented.');
},
get: async () => {
throw new Error('Not implemented.');
},
delete: async () => {
throw new Error('Not implemented.');
},
resolve: async () => {
throw new Error('Not implemented.');
},
}),
},
});
}
}
Expand Down
9 changes: 9 additions & 0 deletions src/plugins/share/common/url_service/short_urls/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/

export * from './types';
141 changes: 141 additions & 0 deletions src/plugins/share/common/url_service/short_urls/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/

import { SerializableRecord } from '@kbn/utility-types';
import { VersionedState } from 'src/plugins/kibana_utils/common';
import { LocatorPublic } from '../locators';

/**
* A factory for Short URL Service. We need this factory as the dependency
* injection is different between the server and the client. On the server,
* the Short URL Service needs a saved object client scoped to the current
* request and the current Kibana version. On the client, the Short URL Service
* needs no dependencies.
*/
export interface IShortUrlClientFactory<D> {
get(dependencies: D): IShortUrlClient;
}

/**
* CRUD-like API for short URLs.
*/
export interface IShortUrlClient {
/**
* Create a new short URL.
*
* @param locator The locator for the URL.
* @param param The parameters for the URL.
* @returns The created short URL.
*/
create<P extends SerializableRecord>(params: ShortUrlCreateParams<P>): Promise<ShortUrl<P>>;

/**
* Delete a short URL.
*
* @param slug The ID of the short URL.
*/
delete(id: string): Promise<void>;

/**
* Fetch a short URL.
*
* @param id The ID of the short URL.
*/
get(id: string): Promise<ShortUrl>;

/**
* Fetch a short URL by its slug.
*
* @param slug The slug of the short URL.
*/
resolve(slug: string): Promise<ShortUrl>;
}

/**
* New short URL creation parameters.
*/
export interface ShortUrlCreateParams<P extends SerializableRecord> {
/**
* Locator which will be used to resolve the short URL.
*/
locator: LocatorPublic<P>;

/**
* Locator parameters which will be used to resolve the short URL.
*/
params: P;

/**
* Optional, short URL slug - the part that will be used to resolve the short
* URL. This part will be visible to the user, it can have user-friendly text.
*/
slug?: string;

/**
* Whether to generate a slug automatically. If `true`, the slug will be
* a human-readable text consisting of three worlds: "<adjective>-<adjective>-<noun>".
*/
humanReadableSlug?: boolean;
}

/**
* A representation of a short URL.
*/
export interface ShortUrl<LocatorParams extends SerializableRecord = SerializableRecord> {
/**
* Serializable state of the short URL, which is stored in Kibana.
*/
readonly data: ShortUrlData<LocatorParams>;
}

/**
* A representation of a short URL's data.
*/
export interface ShortUrlData<LocatorParams extends SerializableRecord = SerializableRecord> {
/**
* Unique ID of the short URL.
*/
readonly id: string;

/**
* The slug of the short URL, the part after the `/` in the URL.
*/
readonly slug: string;

/**
* Number of times the short URL has been resolved.
*/
readonly accessCount: number;

/**
* The timestamp of the last time the short URL was resolved.
*/
readonly accessDate: number;

/**
* The timestamp when the short URL was created.
*/
readonly createDate: number;

/**
* The timestamp when the short URL was last modified.
*/
readonly locator: LocatorData<LocatorParams>;
}

/**
* Represents a serializable state of a locator. Includes locator ID, version
* and its params.
*/
export interface LocatorData<LocatorParams extends SerializableRecord = SerializableRecord>
extends VersionedState<LocatorParams> {
/**
* Locator ID.
*/
id: string;
}
12 changes: 9 additions & 3 deletions src/plugins/share/common/url_service/url_service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,19 +7,25 @@
*/

import { LocatorClient, LocatorClientDependencies } from './locators';
import { IShortUrlClientFactory } from './short_urls';

export type UrlServiceDependencies = LocatorClientDependencies;
export interface UrlServiceDependencies<D = unknown> extends LocatorClientDependencies {
shortUrls: IShortUrlClientFactory<D>;
}

/**
* Common URL Service client interface for server-side and client-side.
*/
export class UrlService {
export class UrlService<D = unknown> {
/**
* Client to work with locators.
*/
public readonly locators: LocatorClient;

constructor(protected readonly deps: UrlServiceDependencies) {
public readonly shortUrls: IShortUrlClientFactory<D>;

constructor(protected readonly deps: UrlServiceDependencies<D>) {
this.locators = new LocatorClient(deps);
this.shortUrls = deps.shortUrls;
}
}
Loading

0 comments on commit 8e6b834

Please sign in to comment.