diff --git a/.eslintrc.js b/.eslintrc.js index ea3ad6ec3ccdc8..b49e073e1fd2de 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -47,6 +47,38 @@ module.exports = { rules: { 'no-restricted-imports': [2, restrictedModules], 'no-restricted-modules': [2, restrictedModules], + '@kbn/eslint/no-restricted-paths': [ + 'error', + { + basePath: __dirname, + zones: [ + { + target: [ + 'src/legacy/**/*', + 'x-pack/**/*', + '!x-pack/**/*.test.*', + 'src/plugins/**/(public|server)/**/*', + 'src/core/(public|server)/**/*', + ], + from: [ + 'src/core/public/**/*', + '!src/core/public/index*', + '!src/core/public/utils/**/*', + + 'src/core/server/**/*', + '!src/core/server/index*', + + 'src/plugins/**/public/**/*', + '!src/plugins/**/public/index*', + + 'src/plugins/**/server/**/*', + '!src/plugins/**/server/index*', + ], + allowSameFolder: true, + }, + ], + }, + ], '@kbn/eslint/module_migration': [ 'error', [ diff --git a/packages/kbn-eslint-plugin-eslint/index.js b/packages/kbn-eslint-plugin-eslint/index.js index 2840986ddf17c7..3e1fba9e5dcd49 100644 --- a/packages/kbn-eslint-plugin-eslint/index.js +++ b/packages/kbn-eslint-plugin-eslint/index.js @@ -22,6 +22,7 @@ module.exports = { 'require-license-header': require('./rules/require_license_header'), 'disallow-license-headers': require('./rules/disallow_license_headers'), 'no-default-export': require('./rules/no_default_export'), + 'no-restricted-paths': require('./rules/no_restricted_paths'), module_migration: require('./rules/module_migration'), }, }; diff --git a/packages/kbn-eslint-plugin-eslint/package.json b/packages/kbn-eslint-plugin-eslint/package.json index e2024b54a620c2..00ac1631a39507 100644 --- a/packages/kbn-eslint-plugin-eslint/package.json +++ b/packages/kbn-eslint-plugin-eslint/package.json @@ -8,6 +8,8 @@ "babel-eslint": "^10.0.1" }, "dependencies": { - "dedent": "^0.7.0" + "micromatch": "3.1.10", + "dedent": "^0.7.0", + "eslint-module-utils": "^2.3.0" } } diff --git a/packages/kbn-eslint-plugin-eslint/rules/__tests__/files/no_restricted_paths/client/a.js b/packages/kbn-eslint-plugin-eslint/rules/__tests__/files/no_restricted_paths/client/a.js new file mode 100644 index 00000000000000..d15de7d98a9e0c --- /dev/null +++ b/packages/kbn-eslint-plugin-eslint/rules/__tests__/files/no_restricted_paths/client/a.js @@ -0,0 +1 @@ +/* eslint-disable */ diff --git a/packages/kbn-eslint-plugin-eslint/rules/__tests__/files/no_restricted_paths/server/b.js b/packages/kbn-eslint-plugin-eslint/rules/__tests__/files/no_restricted_paths/server/b.js new file mode 100644 index 00000000000000..d15de7d98a9e0c --- /dev/null +++ b/packages/kbn-eslint-plugin-eslint/rules/__tests__/files/no_restricted_paths/server/b.js @@ -0,0 +1 @@ +/* eslint-disable */ diff --git a/packages/kbn-eslint-plugin-eslint/rules/__tests__/files/no_restricted_paths/server/c.js b/packages/kbn-eslint-plugin-eslint/rules/__tests__/files/no_restricted_paths/server/c.js new file mode 100644 index 00000000000000..d15de7d98a9e0c --- /dev/null +++ b/packages/kbn-eslint-plugin-eslint/rules/__tests__/files/no_restricted_paths/server/c.js @@ -0,0 +1 @@ +/* eslint-disable */ diff --git a/packages/kbn-eslint-plugin-eslint/rules/__tests__/files/no_restricted_paths/server/deep/d.js b/packages/kbn-eslint-plugin-eslint/rules/__tests__/files/no_restricted_paths/server/deep/d.js new file mode 100644 index 00000000000000..d15de7d98a9e0c --- /dev/null +++ b/packages/kbn-eslint-plugin-eslint/rules/__tests__/files/no_restricted_paths/server/deep/d.js @@ -0,0 +1 @@ +/* eslint-disable */ diff --git a/packages/kbn-eslint-plugin-eslint/rules/__tests__/no_restricted_paths.js b/packages/kbn-eslint-plugin-eslint/rules/__tests__/no_restricted_paths.js new file mode 100644 index 00000000000000..28ea3d4c055fe5 --- /dev/null +++ b/packages/kbn-eslint-plugin-eslint/rules/__tests__/no_restricted_paths.js @@ -0,0 +1,283 @@ +/* eslint-disable-line @kbn/eslint/require-license-header */ +/* + * This product uses import/no-restricted-paths which is available under a + * "MIT" license. + * + * The MIT License (MIT) + * + * Copyright (c) 2015-present, Ben Mosher + * https://github.com/benmosher/eslint-plugin-import + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +const path = require('path'); +const { RuleTester } = require('eslint'); +const rule = require('../no_restricted_paths'); + +const ruleTester = new RuleTester({ + parser: 'babel-eslint', + parserOptions: { + sourceType: 'module', + ecmaVersion: 2015, + }, +}); + +ruleTester.run('@kbn/eslint/no-restricted-paths', rule, { + valid: [ + { + code: 'import a from "../client/a.js"', + filename: path.join(__dirname, './files/no_restricted_paths/server/b.js'), + options: [ + { + basePath: __dirname, + zones: [ + { + target: 'files/no_restricted_paths/server/**/*', + from: 'files/no_restricted_paths/other/**/*', + }, + ], + }, + ], + }, + { + code: 'const a = require("../client/a.js")', + filename: path.join(__dirname, './files/no_restricted_paths/server/b.js'), + options: [ + { + basePath: __dirname, + zones: [ + { + target: 'files/no_restricted_paths/server/**/*', + from: 'files/no_restricted_paths/other/**/*', + }, + ], + }, + ], + }, + { + code: 'import b from "../server/b.js"', + filename: path.join(__dirname, './files/no_restricted_paths/client/a.js'), + options: [ + { + basePath: __dirname, + zones: [ + { + target: '**/no_restricted_paths/client/**/*', + from: '**/no_restricted_paths/other/**/*', + }, + ], + }, + ], + }, + + // irrelevant function calls + { + code: 'notrequire("../server/b.js")', + options: [ + { + basePath: __dirname, + }, + ], + }, + { + code: 'notrequire("../server/b.js")', + filename: path.join(__dirname, './files/no_restricted_paths/client/a.js'), + options: [ + { + basePath: __dirname, + zones: [ + { + target: 'files/no_restricted_paths/client/**/*', + from: 'files/no_restricted_paths/server/**/*', + }, + ], + }, + ], + }, + + // no config + { + code: 'require("../server/b.js")', + options: [ + { + basePath: __dirname, + }, + ], + }, + { + code: 'import b from "../server/b.js"', + options: [ + { + basePath: __dirname, + }, + ], + }, + + // builtin (ignore) + { + code: 'require("os")', + options: [ + { + basePath: __dirname, + }, + ], + }, + + { + code: 'const d = require("./deep/d.js")', + filename: path.join(__dirname, './files/no_restricted_paths/server/b.js'), + options: [ + { + basePath: __dirname, + zones: [ + { + allowSameFolder: true, + target: 'files/no_restricted_paths/**/*', + from: 'files/no_restricted_paths/**/*', + }, + ], + }, + ], + }, + ], + + invalid: [ + { + code: 'import b from "../server/b.js"', + filename: path.join(__dirname, './files/no_restricted_paths/client/a.js'), + options: [ + { + basePath: __dirname, + zones: [ + { + target: 'files/no_restricted_paths/client/**/*', + from: 'files/no_restricted_paths/server/**/*', + }, + ], + }, + ], + errors: [ + { + message: 'Unexpected path "../server/b.js" imported in restricted zone.', + line: 1, + column: 15, + }, + ], + }, + { + code: 'import a from "../client/a"\nimport c from "./c"', + filename: path.join(__dirname, './files/no_restricted_paths/server/b.js'), + options: [ + { + basePath: __dirname, + zones: [ + { + target: 'files/no_restricted_paths/server/**/*', + from: 'files/no_restricted_paths/client/**/*', + }, + { + target: 'files/no_restricted_paths/server/**/*', + from: 'files/no_restricted_paths/server/c.js', + }, + ], + }, + ], + errors: [ + { + message: 'Unexpected path "../client/a" imported in restricted zone.', + line: 1, + column: 15, + }, + { + message: 'Unexpected path "./c" imported in restricted zone.', + line: 2, + column: 15, + }, + ], + }, + { + code: 'const b = require("../server/b.js")', + filename: path.join(__dirname, './files/no_restricted_paths/client/a.js'), + options: [ + { + basePath: __dirname, + zones: [ + { + target: '**/no_restricted_paths/client/**/*', + from: '**/no_restricted_paths/server/**/*', + }, + ], + }, + ], + errors: [ + { + message: 'Unexpected path "../server/b.js" imported in restricted zone.', + line: 1, + column: 19, + }, + ], + }, + { + code: 'const b = require("../server/b.js")', + filename: path.join(__dirname, './files/no_restricted_paths/client/a.js'), + options: [ + { + basePath: path.join(__dirname, 'files', 'no_restricted_paths'), + zones: [ + { + target: 'client/**/*', + from: 'server/**/*', + }, + ], + }, + ], + errors: [ + { + message: 'Unexpected path "../server/b.js" imported in restricted zone.', + line: 1, + column: 19, + }, + ], + }, + + { + code: 'const d = require("./deep/d.js")', + filename: path.join(__dirname, './files/no_restricted_paths/server/b.js'), + options: [ + { + basePath: __dirname, + zones: [ + { + target: 'files/no_restricted_paths/**/*', + from: 'files/no_restricted_paths/**/*', + }, + ], + }, + ], + errors: [ + { + message: 'Unexpected path "./deep/d.js" imported in restricted zone.', + line: 1, + column: 19, + }, + ], + }, + ], +}); diff --git a/packages/kbn-eslint-plugin-eslint/rules/no_restricted_paths.js b/packages/kbn-eslint-plugin-eslint/rules/no_restricted_paths.js new file mode 100644 index 00000000000000..b9cad314a0dc71 --- /dev/null +++ b/packages/kbn-eslint-plugin-eslint/rules/no_restricted_paths.js @@ -0,0 +1,135 @@ +/* eslint-disable-line @kbn/eslint/require-license-header */ +/* + * This product uses import/no-restricted-paths which is available under a + * "MIT" license. + * + * The MIT License (MIT) + * + * Copyright (c) 2015-present, Ben Mosher + * https://github.com/benmosher/eslint-plugin-import + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +const path = require('path'); +const resolve = require('eslint-module-utils/resolve').default; +const mm = require('micromatch'); + +function isStaticRequire(node) { + return ( + node && + node.callee && + node.callee.type === 'Identifier' && + node.callee.name === 'require' && + node.arguments.length === 1 && + node.arguments[0].type === 'Literal' && + typeof node.arguments[0].value === 'string' + ); +} + +function traverseToTopFolder(src, pattern) { + while (mm([src], pattern).length > 0) { + const srcIdx = src.lastIndexOf(path.sep); + src = src.slice(0, srcIdx); + } + return src; +} + +function isSameFolderOrDescendent(src, imported, pattern) { + const srcFileFolderRoot = traverseToTopFolder(src, pattern); + const importedFileFolderRoot = traverseToTopFolder(imported, pattern); + return srcFileFolderRoot === importedFileFolderRoot; +} + +module.exports = { + meta: { + schema: [ + { + type: 'object', + properties: { + zones: { + type: 'array', + minItems: 1, + items: { + type: 'object', + properties: { + target: { + anyOf: [{ type: 'string' }, { type: 'array', items: { type: 'string' } }], + }, + from: { + anyOf: [{ type: 'string' }, { type: 'array', items: { type: 'string' } }], + }, + allowSameFolder: { type: 'boolean' }, + }, + additionalProperties: false, + }, + }, + basePath: { type: 'string' }, + }, + additionalProperties: false, + }, + ], + }, + + create(context) { + const options = context.options[0] || {}; + const zones = options.zones || []; + const basePath = options.basePath; + if (!basePath || !path.isAbsolute(basePath)) { + throw new Error('basePath option must be specified and must be absolute'); + } + + function checkForRestrictedImportPath(importPath, node) { + const absoluteImportPath = resolve(importPath, context); + if (!absoluteImportPath) return; + + const currentFilename = context.getFilename(); + for (const { target, from, allowSameFolder } of zones) { + const srcFilePath = resolve(currentFilename, context); + + const relativeSrcFile = path.relative(basePath, srcFilePath); + const relativeImportFile = path.relative(basePath, absoluteImportPath); + + if ( + !mm([relativeSrcFile], target).length || + !mm([relativeImportFile], from).length || + (allowSameFolder && isSameFolderOrDescendent(relativeSrcFile, relativeImportFile, from)) + ) + continue; + + context.report({ + node, + message: `Unexpected path "${importPath}" imported in restricted zone.`, + }); + } + } + + return { + ImportDeclaration(node) { + checkForRestrictedImportPath(node.source.value, node.source); + }, + CallExpression(node) { + if (isStaticRequire(node)) { + const [firstArgument] = node.arguments; + + checkForRestrictedImportPath(firstArgument.value, firstArgument); + } + }, + }; + }, +}; diff --git a/src/legacy/plugin_discovery/plugin_pack/package_json_at_path.js b/src/legacy/plugin_discovery/plugin_pack/package_json_at_path.js index 1d669b3bcb6642..24e442744dfd7f 100644 --- a/src/legacy/plugin_discovery/plugin_pack/package_json_at_path.js +++ b/src/legacy/plugin_discovery/plugin_pack/package_json_at_path.js @@ -22,6 +22,7 @@ import * as Rx from 'rxjs'; import { map, mergeMap, catchError } from 'rxjs/operators'; import { resolve } from 'path'; import { createInvalidPackError } from '../errors'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths import { isNewPlatformPlugin } from '../../../core/server/plugins'; import { isDirectory } from './lib'; diff --git a/src/legacy/ui/public/test_harness/test_harness.js b/src/legacy/ui/public/test_harness/test_harness.js index 36dc38b1c3b825..f1dfd533a2dc1e 100644 --- a/src/legacy/ui/public/test_harness/test_harness.js +++ b/src/legacy/ui/public/test_harness/test_harness.js @@ -26,6 +26,7 @@ import { parse as parseUrl } from 'url'; import sinon from 'sinon'; import { Notifier } from '../notify'; import { metadata } from '../metadata'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths import { UiSettingsClient } from '../../../../core/public/ui_settings'; import './test_harness.css'; diff --git a/x-pack/test/api_integration/services/es.js b/x-pack/test/api_integration/services/es.js index 7efbf3a6e83043..3d6e5b0cee1a12 100644 --- a/x-pack/test/api_integration/services/es.js +++ b/x-pack/test/api_integration/services/es.js @@ -8,6 +8,7 @@ import { format as formatUrl } from 'url'; import elasticsearch from 'elasticsearch'; import shieldPlugin from '../../../server/lib/esjs_shield_plugin'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths import { DEFAULT_API_VERSION } from '../../../../src/core/server/elasticsearch/elasticsearch_config'; export function EsProvider({ getService }) { diff --git a/yarn.lock b/yarn.lock index 848c82f95a4ab7..a51bd6b2c65cd0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8535,6 +8535,14 @@ eslint-module-utils@^2.3.0: debug "^2.6.8" pkg-dir "^2.0.0" +eslint-module-utils@^2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/eslint-module-utils/-/eslint-module-utils-2.3.0.tgz#546178dab5e046c8b562bbb50705e2456d7bda49" + integrity sha512-lmDJgeOOjk8hObTysjqH7wyMi+nsHwwvfBykwfhjR1LNdd7C2uFJBvx4OpWYpXOw4df1yE1cDEVd1yLHitk34w== + dependencies: + debug "^2.6.8" + pkg-dir "^2.0.0" + eslint-plugin-babel@^5.3.0: version "5.3.0" resolved "https://registry.yarnpkg.com/eslint-plugin-babel/-/eslint-plugin-babel-5.3.0.tgz#2e7f251ccc249326da760c1a4c948a91c32d0023" @@ -15373,6 +15381,25 @@ micro@9.1.0: mri "1.1.0" raw-body "2.3.2" +micromatch@3.1.10, micromatch@^3.0.4, micromatch@^3.1.10, micromatch@^3.1.4, micromatch@^3.1.8, micromatch@^3.1.9: + version "3.1.10" + resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-3.1.10.tgz#70859bc95c9840952f359a068a3fc49f9ecfac23" + integrity sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg== + dependencies: + arr-diff "^4.0.0" + array-unique "^0.3.2" + braces "^2.3.1" + define-property "^2.0.2" + extend-shallow "^3.0.2" + extglob "^2.0.4" + fragment-cache "^0.2.1" + kind-of "^6.0.2" + nanomatch "^1.2.9" + object.pick "^1.3.0" + regex-not "^1.0.0" + snapdragon "^0.8.1" + to-regex "^3.0.2" + micromatch@3.1.5: version "3.1.5" resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-3.1.5.tgz#d05e168c206472dfbca985bfef4f57797b4cd4ba" @@ -15411,25 +15438,6 @@ micromatch@^2.1.5, micromatch@^2.3.11: parse-glob "^3.0.4" regex-cache "^0.4.2" -micromatch@^3.0.4, micromatch@^3.1.10, micromatch@^3.1.4, micromatch@^3.1.8, micromatch@^3.1.9: - version "3.1.10" - resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-3.1.10.tgz#70859bc95c9840952f359a068a3fc49f9ecfac23" - integrity sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg== - dependencies: - arr-diff "^4.0.0" - array-unique "^0.3.2" - braces "^2.3.1" - define-property "^2.0.2" - extend-shallow "^3.0.2" - extglob "^2.0.4" - fragment-cache "^0.2.1" - kind-of "^6.0.2" - nanomatch "^1.2.9" - object.pick "^1.3.0" - regex-not "^1.0.0" - snapdragon "^0.8.1" - to-regex "^3.0.2" - miller-rabin@^4.0.0: version "4.0.1" resolved "https://registry.yarnpkg.com/miller-rabin/-/miller-rabin-4.0.1.tgz#f080351c865b0dc562a8462966daa53543c78a4d"