parent
d6eb7c05ef
commit
138e976fca
128
.github/CODEOWNERS
vendored
Normal file
128
.github/CODEOWNERS
vendored
Normal 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
|
|
@ -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 }));
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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 };
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"id": "share",
|
||||
"version": "kibana",
|
||||
"server": false,
|
||||
"server": true,
|
||||
"ui": true
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
37
src/plugins/share/server/plugin.ts
Normal file
37
src/plugins/share/server/plugin.ts
Normal 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');
|
||||
}
|
||||
}
|
32
src/plugins/share/server/routes/create_routes.ts
Normal file
32
src/plugins/share/server/routes/create_routes.ts
Normal 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 });
|
||||
}
|
64
src/plugins/share/server/routes/goto.ts
Normal file
64
src/plugins/share/server/routes/goto.ts
Normal 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),
|
||||
},
|
||||
});
|
||||
})
|
||||
);
|
||||
};
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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}"`
|
||||
);
|
||||
}
|
||||
}
|
125
src/plugins/share/server/routes/lib/short_url_lookup.test.ts
Normal file
125
src/plugins/share/server/routes/lib/short_url_lookup.test.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
});
|
84
src/plugins/share/server/routes/lib/short_url_lookup.ts
Normal file
84
src/plugins/share/server/routes/lib/short_url_lookup.ts
Normal 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;
|
||||
},
|
||||
};
|
||||
}
|
48
src/plugins/share/server/routes/shorten_url.ts
Normal file
48
src/plugins/share/server/routes/shorten_url.ts
Normal 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 } });
|
||||
})
|
||||
);
|
||||
};
|
|
@ -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);
|
||||
}
|
||||
});
|
||||
|
|
Loading…
Reference in a new issue