Migrate url shortener service (#50896) (#52732)

This commit is contained in:
Joe Reuter 2019-12-11 16:01:47 +01:00 committed by GitHub
parent d6eb7c05ef
commit 138e976fca
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
16 changed files with 630 additions and 87 deletions

128
.github/CODEOWNERS vendored Normal file
View file

@ -0,0 +1,128 @@
# GitHub CODEOWNERS definition
# Identify which groups will be pinged by changes to different parts of the codebase.
# For more info, see https://help.github.com/articles/about-codeowners/
# App
/x-pack/legacy/plugins/lens/ @elastic/kibana-app
/x-pack/legacy/plugins/graph/ @elastic/kibana-app
/src/plugins/share/ @elastic/kibana-app
/src/legacy/server/url_shortening/ @elastic/kibana-app
/src/legacy/server/sample_data/ @elastic/kibana-app
# App Architecture
/src/plugins/data/ @elastic/kibana-app-arch
/src/plugins/embeddable/ @elastic/kibana-app-arch
/src/plugins/expressions/ @elastic/kibana-app-arch
/src/plugins/kibana_react/ @elastic/kibana-app-arch
/src/plugins/kibana_utils/ @elastic/kibana-app-arch
/src/plugins/navigation/ @elastic/kibana-app-arch
/src/plugins/ui_actions/ @elastic/kibana-app-arch
/src/plugins/visualizations/ @elastic/kibana-app-arch
/x-pack/plugins/advanced_ui_actions/ @elastic/kibana-app-arch
/src/legacy/core_plugins/data/ @elastic/kibana-app-arch
/src/legacy/core_plugins/embeddable_api/ @elastic/kibana-app-arch
/src/legacy/core_plugins/interpreter/ @elastic/kibana-app-arch
/src/legacy/core_plugins/kibana_react/ @elastic/kibana-app-arch
/src/legacy/core_plugins/kibana/public/management/ @elastic/kibana-app-arch
/src/legacy/core_plugins/kibana/server/field_formats/ @elastic/kibana-app-arch
/src/legacy/core_plugins/kibana/server/routes/api/management/ @elastic/kibana-app-arch
/src/legacy/core_plugins/kibana/server/routes/api/suggestions/ @elastic/kibana-app-arch
/src/legacy/core_plugins/visualizations/ @elastic/kibana-app-arch
/src/legacy/server/index_patterns/ @elastic/kibana-app-arch
# APM
/x-pack/legacy/plugins/apm/ @elastic/apm-ui
/x-pack/test/functional/apps/apm/ @elastic/apm-ui
/src/legacy/core_plugins/apm_oss/ @elastic/apm-ui
# Beats
/x-pack/legacy/plugins/beats_management/ @elastic/beats
# Canvas
/x-pack/legacy/plugins/canvas/ @elastic/kibana-canvas
# Logs & Metrics UI
/x-pack/legacy/plugins/infra/ @elastic/logs-metrics-ui
/x-pack/legacy/plugins/integrations_manager/ @elastic/epm
# Machine Learning
/x-pack/legacy/plugins/ml/ @elastic/ml-ui
/x-pack/test/functional/apps/machine_learning/ @elastic/ml-ui
/x-pack/test/functional/services/machine_learning/ @elastic/ml-ui
/x-pack/test/functional/services/ml.ts @elastic/ml-ui
# ML team owns the transform plugin, ES team added here for visibility
# because the plugin lives in Kibana's Elasticsearch management section.
/x-pack/legacy/plugins/transform/ @elastic/ml-ui @elastic/es-ui
/x-pack/test/functional/apps/transform/ @elastic/ml-ui
/x-pack/test/functional/services/transform_ui/ @elastic/ml-ui
/x-pack/test/functional/services/transform.ts @elastic/ml-ui
# Operations
/src/dev/ @elastic/kibana-operations
/src/setup_node_env/ @elastic/kibana-operations
/src/optimize/ @elastic/kibana-operations
/packages/*eslint*/ @elastic/kibana-operations
/packages/*babel*/ @elastic/kibana-operations
/packages/kbn-dev-utils*/ @elastic/kibana-operations
/packages/kbn-es/ @elastic/kibana-operations
/packages/kbn-pm/ @elastic/kibana-operations
/packages/kbn-test/ @elastic/kibana-operations
/src/legacy/server/keystore/ @elastic/kibana-operations
/src/legacy/server/pid/ @elastic/kibana-operations
/src/legacy/server/sass/ @elastic/kibana-operations
/src/legacy/server/utils/ @elastic/kibana-operations
/src/legacy/server/warnings/ @elastic/kibana-operations
# Platform
/src/core/ @elastic/kibana-platform
/config/kibana.yml @elastic/kibana-platform
/x-pack/plugins/features/ @elastic/kibana-platform
/x-pack/plugins/licensing/ @elastic/kibana-platform
/packages/kbn-config-schema/ @elastic/kibana-platform
/src/legacy/server/config/ @elastic/kibana-platform
/src/legacy/server/csp/ @elastic/kibana-platform
/src/legacy/server/http/ @elastic/kibana-platform
/src/legacy/server/i18n/ @elastic/kibana-platform
/src/legacy/server/logging/ @elastic/kibana-platform
/src/legacy/server/saved_objects/ @elastic/kibana-platform
/src/legacy/server/status/ @elastic/kibana-platform
# Security
/x-pack/legacy/plugins/security/ @elastic/kibana-security
/x-pack/legacy/plugins/spaces/ @elastic/kibana-security
/x-pack/plugins/spaces/ @elastic/kibana-security
/x-pack/legacy/plugins/encrypted_saved_objects/ @elastic/kibana-security
/x-pack/plugins/encrypted_saved_objects/ @elastic/kibana-security
/src/legacy/server/csp/ @elastic/kibana-security
/x-pack/plugins/security/ @elastic/kibana-security
/x-pack/test/api_integration/apis/security/ @elastic/kibana-security
# Kibana Stack Services
/src/dev/i18n @elastic/kibana-stack-services
/packages/kbn-analytics/ @elastic/kibana-stack-services
/src/legacy/core_plugins/ui_metric/ @elastic/kibana-stack-services
/src/plugins/usage_collection/ @elastic/kibana-stack-services
/x-pack/legacy/plugins/telemetry @elastic/kibana-stack-services
/x-pack/legacy/plugins/alerting @elastic/kibana-stack-services
/x-pack/legacy/plugins/actions @elastic/kibana-stack-services
/x-pack/legacy/plugins/task_manager @elastic/kibana-stack-services
# Design
**/*.scss @elastic/kibana-design
# Elasticsearch UI
/src/legacy/core_plugins/console/ @elastic/es-ui
/src/plugins/es_ui_shared/ @elastic/es-ui
/x-pack/legacy/plugins/console_extensions/ @elastic/es-ui
/x-pack/legacy/plugins/cross_cluster_replication/ @elastic/es-ui
/x-pack/legacy/plugins/index_lifecycle_management/ @elastic/es-ui
/x-pack/legacy/plugins/index_management/ @elastic/es-ui
/x-pack/legacy/plugins/license_management/ @elastic/es-ui
/x-pack/legacy/plugins/remote_clusters/ @elastic/es-ui
/x-pack/legacy/plugins/rollup/ @elastic/es-ui
/x-pack/legacy/plugins/searchprofiler/ @elastic/es-ui
/x-pack/legacy/plugins/snapshot_restore/ @elastic/es-ui
/x-pack/legacy/plugins/watcher/ @elastic/es-ui
# Kibana TSVB external contractors
/src/legacy/core_plugins/metrics/ @elastic/kibana-tsvb-external

View file

@ -19,12 +19,10 @@
import { shortUrlLookupProvider } from './lib/short_url_lookup';
import { createGotoRoute } from './goto';
import { createShortenUrlRoute } from './shorten_url';
export function createRoutes(server) {
const shortUrlLookup = shortUrlLookupProvider(server);
server.route(createGotoRoute({ server, shortUrlLookup }));
server.route(createShortenUrlRoute({ shortUrlLookup }));
}

View file

@ -22,18 +22,12 @@ import { shortUrlAssertValid } from './lib/short_url_assert_valid';
export const createGotoRoute = ({ server, shortUrlLookup }) => ({
method: 'GET',
path: '/goto/{urlId}',
path: '/goto_LP/{urlId}',
handler: async function (request, h) {
try {
const url = await shortUrlLookup.getUrl(request.params.urlId, request);
shortUrlAssertValid(url);
const uiSettings = request.getUiSettingsService();
const stateStoreInSessionStorage = await uiSettings.get('state:storeInSessionStorage');
if (!stateStoreInSessionStorage) {
return h.redirect(request.getBasePath() + url);
}
const app = server.getHiddenUiAppById('stateSessionStorageRedirect');
return h.renderApp(app, {
redirectUrl: url,

View file

@ -17,7 +17,6 @@
* under the License.
*/
import crypto from 'crypto';
import { get } from 'lodash';
export function shortUrlLookupProvider(server) {
@ -34,29 +33,6 @@ export function shortUrlLookupProvider(server) {
}
return {
async generateUrlId(url, req) {
const id = crypto.createHash('md5').update(url).digest('hex');
const savedObjectsClient = req.getSavedObjectsClient();
const { isConflictError } = savedObjectsClient.errors;
try {
const doc = await savedObjectsClient.create('url', {
url,
accessCount: 0,
createDate: new Date(),
accessDate: new Date()
}, { id });
return doc.id;
} catch (error) {
if (isConflictError(error)) {
return id;
}
throw error;
}
},
async getUrl(id, req) {
const doc = await req.getSavedObjectsClient().get('url', id);
updateMetadata(doc, req);

View file

@ -48,43 +48,6 @@ describe('shortUrlLookupProvider', () => {
sandbox.restore();
});
describe('generateUrlId', () => {
it('returns the document id', async () => {
const id = await shortUrl.generateUrlId(URL, req);
expect(id).toEqual(ID);
});
it('provides correct arguments to savedObjectsClient', async () => {
await shortUrl.generateUrlId(URL, req);
sinon.assert.calledOnce(savedObjectsClient.create);
const [type, attributes, options] = savedObjectsClient.create.getCall(0).args;
expect(type).toEqual(TYPE);
expect(Object.keys(attributes).sort()).toEqual(['accessCount', 'accessDate', 'createDate', 'url']);
expect(attributes.url).toEqual(URL);
expect(options.id).toEqual(ID);
});
it('passes persists attributes', async () => {
await shortUrl.generateUrlId(URL, req);
sinon.assert.calledOnce(savedObjectsClient.create);
const [type, attributes] = savedObjectsClient.create.getCall(0).args;
expect(type).toEqual(TYPE);
expect(Object.keys(attributes).sort()).toEqual(['accessCount', 'accessDate', 'createDate', 'url']);
expect(attributes.url).toEqual(URL);
});
it('gracefully handles version conflict', async () => {
const error = savedObjectsClient.errors.decorateConflictError(new Error());
savedObjectsClient.create.throws(error);
const id = await shortUrl.generateUrlId(URL, req);
expect(id).toEqual(ID);
});
});
describe('getUrl', () => {
beforeEach(() => {
const attributes = { accessCount: 2, url: URL };

View file

@ -1,6 +1,6 @@
{
"id": "share",
"version": "kibana",
"server": false,
"server": true,
"ui": true
}

View file

@ -17,19 +17,9 @@
* under the License.
*/
import { handleShortUrlError } from './lib/short_url_error';
import { shortUrlAssertValid } from './lib/short_url_assert_valid';
import { PluginInitializerContext } from '../../../core/server';
import { SharePlugin } from './plugin';
export const createShortenUrlRoute = ({ shortUrlLookup }) => ({
method: 'POST',
path: '/api/shorten_url',
handler: async function (request) {
try {
shortUrlAssertValid(request.payload.url);
const urlId = await shortUrlLookup.generateUrlId(request.payload.url, request);
return { urlId };
} catch (err) {
throw handleShortUrlError(err);
}
}
});
export function plugin(initializerContext: PluginInitializerContext) {
return new SharePlugin(initializerContext);
}

View file

@ -0,0 +1,37 @@
/*
* 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 { CoreSetup, Plugin, PluginInitializerContext } from 'kibana/server';
import { createRoutes } from './routes/create_routes';
export class SharePlugin implements Plugin {
constructor(private readonly initializerContext: PluginInitializerContext) {}
public async setup(core: CoreSetup) {
createRoutes(core, this.initializerContext.logger.get());
}
public start() {
this.initializerContext.logger.get().debug('Starting plugin');
}
public stop() {
this.initializerContext.logger.get().debug('Stopping plugin');
}
}

View file

@ -0,0 +1,32 @@
/*
* 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 { CoreSetup, Logger } from 'kibana/server';
import { shortUrlLookupProvider } from './lib/short_url_lookup';
import { createGotoRoute } from './goto';
import { createShortenUrlRoute } from './shorten_url';
export function createRoutes({ http }: CoreSetup, logger: Logger) {
const shortUrlLookup = shortUrlLookupProvider({ logger });
const router = http.createRouter();
createGotoRoute({ router, shortUrlLookup, http });
createShortenUrlRoute({ router, shortUrlLookup });
}

View file

@ -0,0 +1,64 @@
/*
* 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 { CoreSetup, IRouter } from 'kibana/server';
import { schema } from '@kbn/config-schema';
import { shortUrlAssertValid } from './lib/short_url_assert_valid';
import { ShortUrlLookupService } from './lib/short_url_lookup';
export const createGotoRoute = ({
router,
shortUrlLookup,
http,
}: {
router: IRouter;
shortUrlLookup: ShortUrlLookupService;
http: CoreSetup['http'];
}) => {
router.get(
{
path: '/goto/{urlId}',
validate: {
params: schema.object({ urlId: schema.string() }),
},
},
router.handleLegacyErrors(async function(context, request, response) {
const url = await shortUrlLookup.getUrl(request.params.urlId, {
savedObjects: context.core.savedObjects.client,
});
shortUrlAssertValid(url);
const uiSettings = context.core.uiSettings.client;
const stateStoreInSessionStorage = await uiSettings.get('state:storeInSessionStorage');
if (!stateStoreInSessionStorage) {
return response.redirected({
headers: {
location: http.basePath.prepend(url),
},
});
}
return response.redirected({
headers: {
location: http.basePath.prepend('/goto_LP/' + request.params.urlId),
},
});
})
);
};

View file

@ -0,0 +1,63 @@
/*
* 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 { 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'],
['hostname and port', 'local.host:5601/app/kibana'],
['hostname and auth', 'user:pass@localhost.net/app/kibana'],
['path traversal', '/app/../../not-kibana'],
['deep path', '/app/kibana/foo'],
['deep path', '/app/kibana/foo/bar'],
['base path', '/base/app/kibana'],
];
invalid.forEach(([desc, url]) => {
it(`fails when url has ${desc}`, () => {
try {
shortUrlAssertValid(url);
throw new Error(`expected assertion to throw`);
} catch (err) {
if (!err || !err.isBoom) {
throw err;
}
}
});
});
const valid = [
'/app/kibana',
'/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);
});
});
});

View file

@ -0,0 +1,41 @@
/*
* 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 { parse } from 'url';
import { trim } from 'lodash';
import Boom from 'boom';
export function shortUrlAssertValid(url: string) {
const { protocol, hostname, pathname } = parse(url);
if (protocol) {
throw Boom.notAcceptable(`Short url targets cannot have a protocol, found "${protocol}"`);
}
if (hostname) {
throw Boom.notAcceptable(`Short url targets cannot have a hostname, found "${hostname}"`);
}
const pathnameParts = trim(pathname, '/').split('/');
if (pathnameParts.length !== 2) {
throw Boom.notAcceptable(
`Short url target path must be in the format "/app/{{appId}}", found "${pathname}"`
);
}
}

View file

@ -0,0 +1,125 @@
/*
* 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 { shortUrlLookupProvider, ShortUrlLookupService } from './short_url_lookup';
import { SavedObjectsClientContract, Logger } from 'kibana/server';
import { SavedObjectsClient } from '../../../../../core/server';
describe('shortUrlLookupProvider', () => {
const ID = 'bf00ad16941fc51420f91a93428b27a0';
const TYPE = 'url';
const URL = 'http://elastic.co';
let savedObjects: jest.Mocked<SavedObjectsClientContract>;
let deps: { savedObjects: SavedObjectsClientContract };
let shortUrl: ShortUrlLookupService;
beforeEach(() => {
savedObjects = ({
get: jest.fn(),
create: jest.fn(() => Promise.resolve({ id: ID })),
update: jest.fn(),
errors: SavedObjectsClient.errors,
} as unknown) as jest.Mocked<SavedObjectsClientContract>;
deps = { savedObjects };
shortUrl = shortUrlLookupProvider({ logger: ({ warn: () => {} } as unknown) as Logger });
});
describe('generateUrlId', () => {
it('returns the document id', async () => {
const id = await shortUrl.generateUrlId(URL, deps);
expect(id).toEqual(ID);
});
it('provides correct arguments to savedObjectsClient', async () => {
await shortUrl.generateUrlId(URL, { savedObjects });
expect(savedObjects.create).toHaveBeenCalledTimes(1);
const [type, attributes, options] = savedObjects.create.mock.calls[0];
expect(type).toEqual(TYPE);
expect(Object.keys(attributes).sort()).toEqual([
'accessCount',
'accessDate',
'createDate',
'url',
]);
expect(attributes.url).toEqual(URL);
expect(options!.id).toEqual(ID);
});
it('passes persists attributes', async () => {
await shortUrl.generateUrlId(URL, deps);
expect(savedObjects.create).toHaveBeenCalledTimes(1);
const [type, attributes] = savedObjects.create.mock.calls[0];
expect(type).toEqual(TYPE);
expect(Object.keys(attributes).sort()).toEqual([
'accessCount',
'accessDate',
'createDate',
'url',
]);
expect(attributes.url).toEqual(URL);
});
it('gracefully handles version conflict', async () => {
const error = savedObjects.errors.decorateConflictError(new Error());
savedObjects.create.mockImplementation(() => {
throw error;
});
const id = await shortUrl.generateUrlId(URL, deps);
expect(id).toEqual(ID);
});
});
describe('getUrl', () => {
beforeEach(() => {
const attributes = { accessCount: 2, url: URL };
savedObjects.get.mockResolvedValue({ id: ID, attributes, type: 'url', references: [] });
});
it('provides the ID to savedObjectsClient', async () => {
await shortUrl.getUrl(ID, { savedObjects });
expect(savedObjects.get).toHaveBeenCalledTimes(1);
expect(savedObjects.get).toHaveBeenCalledWith(TYPE, ID);
});
it('returns the url', async () => {
const response = await shortUrl.getUrl(ID, deps);
expect(response).toEqual(URL);
});
it('increments accessCount', async () => {
await shortUrl.getUrl(ID, { savedObjects });
expect(savedObjects.update).toHaveBeenCalledTimes(1);
const [type, id, attributes] = savedObjects.update.mock.calls[0];
expect(type).toEqual(TYPE);
expect(id).toEqual(ID);
expect(Object.keys(attributes).sort()).toEqual(['accessCount', 'accessDate']);
expect(attributes.accessCount).toEqual(3);
});
});
});

View file

@ -0,0 +1,84 @@
/*
* 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 crypto from 'crypto';
import { get } from 'lodash';
import { Logger, SavedObject, SavedObjectsClientContract } from 'kibana/server';
export interface ShortUrlLookupService {
generateUrlId(url: string, deps: { savedObjects: SavedObjectsClientContract }): Promise<string>;
getUrl(url: string, deps: { savedObjects: SavedObjectsClientContract }): Promise<string>;
}
export function shortUrlLookupProvider({ logger }: { logger: Logger }): ShortUrlLookupService {
async function updateMetadata(
doc: SavedObject,
{ savedObjects }: { savedObjects: SavedObjectsClientContract }
) {
try {
await savedObjects.update('url', doc.id, {
accessDate: new Date().valueOf(),
accessCount: get(doc, 'attributes.accessCount', 0) + 1,
});
} catch (error) {
logger.warn('Warning: Error updating url metadata');
logger.warn(error);
// swallow errors. It isn't critical if there is no update.
}
}
return {
async generateUrlId(url, { savedObjects }) {
const id = crypto
.createHash('md5')
.update(url)
.digest('hex');
const { isConflictError } = savedObjects.errors;
try {
const doc = await savedObjects.create(
'url',
{
url,
accessCount: 0,
createDate: new Date().valueOf(),
accessDate: new Date().valueOf(),
},
{ id }
);
return doc.id;
} catch (error) {
if (isConflictError(error)) {
return id;
}
throw error;
}
},
async getUrl(id, { savedObjects }) {
const doc = await savedObjects.get('url', id);
updateMetadata(doc, { savedObjects });
return doc.attributes.url;
},
};
}

View file

@ -0,0 +1,48 @@
/*
* 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 { IRouter } from 'kibana/server';
import { schema } from '@kbn/config-schema';
import { shortUrlAssertValid } from './lib/short_url_assert_valid';
import { ShortUrlLookupService } from './lib/short_url_lookup';
export const createShortenUrlRoute = ({
shortUrlLookup,
router,
}: {
shortUrlLookup: ShortUrlLookupService;
router: IRouter;
}) => {
router.post(
{
path: '/api/shorten_url',
validate: {
body: schema.object({ url: schema.string() }),
},
},
router.handleLegacyErrors(async function(context, request, response) {
shortUrlAssertValid(request.body.url);
const urlId = await shortUrlLookup.generateUrlId(request.body.url, {
savedObjects: context.core.savedObjects.client,
});
return response.ok({ body: { urlId } });
})
);
};

View file

@ -107,7 +107,7 @@ export default function featureControlsTests({ getService }: FtrProviderContext)
expect(resp.status).to.eql(302);
expect(resp.headers.location).to.eql('/app/kibana#foo/bar/baz');
} else {
expect(resp.status).to.eql(500);
expect(resp.status).to.eql(403);
expect(resp.headers.location).to.eql(undefined);
}
});