diff --git a/client/package.json b/client/package.json index d5102c97d606..cc6ee21dc588 100644 --- a/client/package.json +++ b/client/package.json @@ -27,6 +27,7 @@ "@johmun/vue-tags-input": "^2.1.0", "@sentry/browser": "^5.20.1", "axios": "^0.21.1", + "babel-runtime": "^6.26.0", "backbone": "1.4.0", "bootstrap": "4.5.0", "bootstrap-vue": "^2.21.2", @@ -89,6 +90,7 @@ "vue-infinite-scroll": "^2.0.2", "vue-multiselect": "^2.1.0", "vue-observe-visibility": "^1.0.0", + "vue-prismjs": "^1.2.0", "vue-router": "^3.5.2", "vue-rx": "^6.2.0", "vue-scrollto": "^2.20.0", @@ -97,7 +99,8 @@ "vuex": "^3.4.0", "vuex-cache": "^3.2.0", "vuex-persist": "^3.1.3", - "vuex-persistedstate": "^4.1.0" + "vuex-persistedstate": "^4.1.0", + "xml-beautifier": "^0.5.0" }, "scripts": { "watch": "gulp && yarn run save-build-hash && yarn run webpack-watch", diff --git a/client/src/components/Tool/ToolCard.test.js b/client/src/components/Tool/ToolCard.test.js index b9e0607ba2bb..497fcb31b449 100644 --- a/client/src/components/Tool/ToolCard.test.js +++ b/client/src/components/Tool/ToolCard.test.js @@ -32,6 +32,9 @@ describe("ToolCard", () => { messageVariant: "warning", disabled: false, }, + stubs: { + ToolSourceMenuItem: { template: "
" }, + }, localVue, }); }); diff --git a/client/src/components/Tool/ToolCard.vue b/client/src/components/Tool/ToolCard.vue index 699571a9a9da..a0145300649b 100644 --- a/client/src/components/Tool/ToolCard.vue +++ b/client/src/components/Tool/ToolCard.vue @@ -26,6 +26,7 @@ Download + See in Tool Shed +
@@ -114,6 +116,7 @@ import { copyLink, downloadTool, openLink } from "./utilities"; import FormMessage from "components/Form/FormMessage"; import ToolFooter from "components/Tool/ToolFooter"; import ToolHelp from "components/Tool/ToolHelp"; +import ToolSourceMenuItem from "components/Tool/ToolSourceMenuItem"; import Webhooks from "mvc/webhooks"; import { addFavorite, removeFavorite } from "components/Tool/services"; import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome"; @@ -128,6 +131,7 @@ export default { FormMessage, ToolFooter, ToolHelp, + ToolSourceMenuItem, }, props: { id: { diff --git a/client/src/components/Tool/ToolSource.vue b/client/src/components/Tool/ToolSource.vue new file mode 100644 index 000000000000..13c7e52348ba --- /dev/null +++ b/client/src/components/Tool/ToolSource.vue @@ -0,0 +1,30 @@ + + diff --git a/client/src/components/Tool/ToolSourceMenuItem.vue b/client/src/components/Tool/ToolSourceMenuItem.vue new file mode 100644 index 000000000000..fef9964be07f --- /dev/null +++ b/client/src/components/Tool/ToolSourceMenuItem.vue @@ -0,0 +1,39 @@ + + diff --git a/client/src/components/providers/SingleQueryProvider.js b/client/src/components/providers/SingleQueryProvider.js index 76ec0cbb41d1..98b33727d57c 100644 --- a/client/src/components/providers/SingleQueryProvider.js +++ b/client/src/components/providers/SingleQueryProvider.js @@ -2,9 +2,9 @@ import hash from "object-hash"; /** * Builds a provider that gets its result from a single promise-based query function and - * caches the result of lookup for subsequent instantitations. + * caches the result of lookup for subsequent instantiations. * - * @param {Function} lookup async function that loads the result, paramters will be an object + * @param {Function} lookup async function that loads the result, parameters will be an object * whose properties are the attributes assigned to the provider component * @return {VueComponentOptions} Vue component options definition */ diff --git a/client/src/components/providers/ToolSourceProvider.js b/client/src/components/providers/ToolSourceProvider.js new file mode 100644 index 000000000000..c3861c91fedf --- /dev/null +++ b/client/src/components/providers/ToolSourceProvider.js @@ -0,0 +1,24 @@ +import axios from "axios"; +import { getAppRoot } from "onload/loadConfig"; +import { SingleQueryProvider } from "components/providers/SingleQueryProvider"; +import { rethrowSimple } from "utils/simple-error"; +import beautify from "xml-beautifier"; + +async function toolSource({ id }) { + const url = `${getAppRoot()}api/tools/${id}/raw_tool_source`; + try { + const { data, headers } = await axios.get(url); + const result = {}; + result.language = headers.language; + if (headers.language === "xml") { + result.source = beautify(data); + } else { + result.source = data; + } + return result; + } catch (e) { + rethrowSimple(e); + } +} + +export const ToolSourceProvider = SingleQueryProvider(toolSource); diff --git a/client/yarn.lock b/client/yarn.lock index 9d62093ed6dc..7673892f8047 100644 --- a/client/yarn.lock +++ b/client/yarn.lock @@ -2729,6 +2729,14 @@ babel-preset-jest@^27.2.0: babel-plugin-jest-hoist "^27.2.0" babel-preset-current-node-syntax "^1.0.0" +babel-runtime@^6.26.0: + version "6.26.0" + resolved "https://registry.yarnpkg.com/babel-runtime/-/babel-runtime-6.26.0.tgz#965c7058668e82b55d7bfe04ff2337bc8b5647fe" + integrity sha1-llxwWGaOgrVde/4E/yM3vItWR/4= + dependencies: + core-js "^2.4.0" + regenerator-runtime "^0.11.0" + babel-walk@3.0.0-canary-5: version "3.0.0-canary-5" resolved "https://registry.yarnpkg.com/babel-walk/-/babel-walk-3.0.0-canary-5.tgz#f66ecd7298357aee44955f235a6ef54219104b11" @@ -3831,6 +3839,11 @@ core-js-pure@^3.0.0: resolved "https://registry.yarnpkg.com/core-js-pure/-/core-js-pure-3.18.3.tgz#7eed77dcce1445ab68fd68715856633e2fb3b90c" integrity sha512-qfskyO/KjtbYn09bn1IPkuhHl5PlJ6IzJ9s9sraJ1EqcuGyLGKzhSM1cY0zgyL9hx42eulQLZ6WaeK5ycJCkqw== +core-js@^2.4.0: + version "2.6.12" + resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.6.12.tgz#d9333dfa7b065e347cc5682219d6f690859cc2ec" + integrity sha512-Kb2wC0fvsWfQrgk8HU5lW6U/Lcs8+9aaYcy4ZFc6DDlo4nZ7n70dEgE5rtR0oG6ufKDUnrwfWL1mXR5ljDatrQ== + core-js@^3.19.1: version "3.19.1" resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.19.1.tgz#f6f173cae23e73a7d88fa23b6e9da329276c6641" @@ -10070,7 +10083,7 @@ pretty@^2.0.0: extend-shallow "^2.0.1" js-beautify "^1.6.12" -prismjs@^1.17.1, prismjs@^1.23.0: +prismjs@^1.17.1, prismjs@^1.23.0, prismjs@^1.6.0: version "1.25.0" resolved "https://registry.yarnpkg.com/prismjs/-/prismjs-1.25.0.tgz#6f822df1bdad965734b310b315a23315cf999756" integrity sha512-WCjJHl1KEWbnkQom1+SzftbtXMKQoezOCYs5rECqMN+jP+apI7ftoflyqigqzopSO3hMhTEb0mFClA8lkolgEg== @@ -10736,6 +10749,11 @@ regenerate@^1.4.0, regenerate@^1.4.2: resolved "https://registry.yarnpkg.com/regenerate/-/regenerate-1.4.2.tgz#b9346d8827e8f5a32f7ba29637d398b69014848a" integrity sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A== +regenerator-runtime@^0.11.0: + version "0.11.1" + resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz#be05ad7f9bf7d22e056f9726cee5017fbf19e2e9" + integrity sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg== + regenerator-runtime@^0.13.4, regenerator-runtime@^0.13.7, regenerator-runtime@^0.13.9: version "0.13.9" resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.9.tgz#8925742a98ffd90814988d7566ad30ca3b263b52" @@ -10867,7 +10885,7 @@ repeat-element@^1.1.2: resolved "https://registry.yarnpkg.com/repeat-element/-/repeat-element-1.1.4.tgz#be681520847ab58c7568ac75fbfad28ed42d39e9" integrity sha512-LFiNfRcSu7KK3evMyYOuCzv3L10TW7yC1G2/+StMjK8Y6Vqd2MG7r/Qjw4ghtuCOjFvlnms/iMmLqpvW/ES/WQ== -repeat-string@^1.0.0, repeat-string@^1.6.1: +repeat-string@1.6.1, repeat-string@^1.0.0, repeat-string@^1.6.1: version "1.6.1" resolved "https://registry.yarnpkg.com/repeat-string/-/repeat-string-1.6.1.tgz#8dcae470e1c88abc2d600fff4a776286da75e637" integrity sha1-jcrkcOHIirwtYA//Sndihtp15jc= @@ -12933,6 +12951,13 @@ vue-observe-visibility@^1.0.0: resolved "https://registry.yarnpkg.com/vue-observe-visibility/-/vue-observe-visibility-1.0.0.tgz#17cf1b2caf74022f0f3c95371468ddf2b9573152" integrity sha512-s5TFh3s3h3Mhd3jaz3zGzkVHKHnc/0C/gNr30olO99+yw2hl3WBhK3ng3/f9OF+qkW4+l7GkmwfAzDAcY3lCFg== +vue-prismjs@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/vue-prismjs/-/vue-prismjs-1.2.0.tgz#b137f5ed958685ce1fd55ca6b068289f3359b8b7" + integrity sha512-1mICbMknMiKw4mfM1AU7vkFT3VX4Cq8ySyDU7IBr9tYW1fzxIPkfLoERkRMbQLBc+J74K3FIiuuZ+WfBjO3SCQ== + dependencies: + prismjs "^1.6.0" + vue-router@^3.5.2: version "3.5.2" resolved "https://registry.yarnpkg.com/vue-router/-/vue-router-3.5.2.tgz#5f55e3f251970e36c3e8d88a7cd2d67a350ade5c" @@ -13544,6 +13569,13 @@ ws@~7.4.2: resolved "https://registry.yarnpkg.com/ws/-/ws-7.4.6.tgz#5654ca8ecdeee47c33a9a4bf6d28e2be2980377c" integrity sha512-YmhHDO4MzaDLB+M9ym/mDA5z0naX8j7SIlT8f8z+I0VtzsRbekxEutHSme7NPS2qE8StCYQNUnfWdXta/Yu85A== +xml-beautifier@^0.5.0: + version "0.5.0" + resolved "https://registry.yarnpkg.com/xml-beautifier/-/xml-beautifier-0.5.0.tgz#476fd7118b972c2ec5863babca6812934d34bb56" + integrity sha512-QG/qiHeolHUd1tAtM+5zHxTzDprb8qvhmIYUYV1E9QK/jTFlrAa1Mz7QQqJPeqc3uuFAGzTOhjvbdx2hOP6bHw== + dependencies: + repeat-string "1.6.1" + xml-name-validator@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/xml-name-validator/-/xml-name-validator-3.0.0.tgz#6ae73e06de4d8c6e47f9fb181f78d648ad457c6a" diff --git a/doc/source/admin/galaxy_options.rst b/doc/source/admin/galaxy_options.rst index 105f905c112e..2347d8b882ba 100644 --- a/doc/source/admin/galaxy_options.rst +++ b/doc/source/admin/galaxy_options.rst @@ -3908,6 +3908,20 @@ :Type: bool +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +``enable_tool_source_display`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +:Description: + This option allows users to view the tool wrapper source code. + This is safe to enable if you have not hardcoded any secrets in + any of the tool wrappers installed on this Galaxy server. If you + have only installed tool wrappers from public tool sheds and + tools shipped with Galaxy there you can enable this option. +:Default: ``false`` +:Type: bool + + ~~~~~~~~~~~~~~~~~~~~~~~~~~~ ``job_metrics_config_file`` ~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/lib/galaxy/config/sample/galaxy.yml.sample b/lib/galaxy/config/sample/galaxy.yml.sample index 83fe7890909b..7d62df5f30f0 100644 --- a/lib/galaxy/config/sample/galaxy.yml.sample +++ b/lib/galaxy/config/sample/galaxy.yml.sample @@ -1912,6 +1912,13 @@ galaxy: # see dataset paths. #expose_dataset_path: false + # This option allows users to view the tool wrapper source code. This + # is safe to enable if you have not hardcoded any secrets in any of + # the tool wrappers installed on this Galaxy server. If you have only + # installed tool wrappers from public tool sheds and tools shipped + # with Galaxy there you can enable this option. + #enable_tool_source_display: false + # XML config file that contains the job metric collection # configuration. # The value of this option will be resolved with respect to diff --git a/lib/galaxy/managers/configuration.py b/lib/galaxy/managers/configuration.py index 27427d896069..0134e4498f6a 100644 --- a/lib/galaxy/managers/configuration.py +++ b/lib/galaxy/managers/configuration.py @@ -208,6 +208,7 @@ def _config_is_truthy(item, key, **context): 'upload_from_form_button': _use_config, 'release_doc_base_url': _use_config, 'expose_user_email': _use_config, + 'enable_tool_source_display': _use_config, 'user_library_import_dir_available': lambda item, key, **context: bool(item.get('user_library_import_dir')), 'welcome_directory': _use_config, } diff --git a/lib/galaxy/tool_util/parser/cwl.py b/lib/galaxy/tool_util/parser/cwl.py index 0ebb4a48261c..59f1036072ed 100644 --- a/lib/galaxy/tool_util/parser/cwl.py +++ b/lib/galaxy/tool_util/parser/cwl.py @@ -21,6 +21,8 @@ class CwlToolSource(ToolSource): + language = 'yaml' + def __init__(self, tool_file, strict_cwl_validation=True): self._cwl_tool_file = tool_file self._id, _ = os.path.splitext(os.path.basename(tool_file)) diff --git a/lib/galaxy/tool_util/parser/interface.py b/lib/galaxy/tool_util/parser/interface.py index b82154bdc960..a4f8c6ea9b56 100644 --- a/lib/galaxy/tool_util/parser/interface.py +++ b/lib/galaxy/tool_util/parser/interface.py @@ -19,6 +19,7 @@ class ToolSource(metaclass=ABCMeta): information from. """ default_is_multi_byte = False + language: str @abstractmethod def parse_id(self): diff --git a/lib/galaxy/tool_util/parser/xml.py b/lib/galaxy/tool_util/parser/xml.py index dbcf380e763d..ff16177864ae 100644 --- a/lib/galaxy/tool_util/parser/xml.py +++ b/lib/galaxy/tool_util/parser/xml.py @@ -50,6 +50,8 @@ class XmlToolSource(ToolSource): """ Responsible for parsing a tool from classic Galaxy representation. """ + language = 'xml' + def __init__(self, xml_tree, source_path=None, macro_paths=None): self.xml_tree = xml_tree self.root = xml_tree.getroot() diff --git a/lib/galaxy/tool_util/parser/yaml.py b/lib/galaxy/tool_util/parser/yaml.py index 7499cab21cbe..b0f76cf9a261 100644 --- a/lib/galaxy/tool_util/parser/yaml.py +++ b/lib/galaxy/tool_util/parser/yaml.py @@ -23,6 +23,8 @@ class YamlToolSource(ToolSource): + language = 'yaml' + def __init__(self, root_dict, source_path=None): self.root_dict = root_dict self._source_path = source_path diff --git a/lib/galaxy/webapps/galaxy/api/tools.py b/lib/galaxy/webapps/galaxy/api/tools.py index 4fbc0294f79d..f26eebe6b3f7 100644 --- a/lib/galaxy/webapps/galaxy/api/tools.py +++ b/lib/galaxy/webapps/galaxy/api/tools.py @@ -18,6 +18,7 @@ expose_api_anonymous_and_sessionless, expose_api_raw_anonymous_and_sessionless, ) +from galaxy.web.framework.decorators import expose_api_raw from galaxy.webapps.base.controller import UsesVisualizationMixin from galaxy.webapps.base.webapp import GalaxyWebTransaction from . import BaseGalaxyAPIController, depends @@ -490,6 +491,15 @@ def download(self, trans: GalaxyWebTransaction, id, **kwds): trans.response.headers["Content-Disposition"] = f'attachment; filename="{id}.tgz"' return download_file + @expose_api_raw + def raw_tool_source(self, trans: GalaxyWebTransaction, id, **kwds): + """Returns tool source. ``language`` is included in the response header.""" + if not trans.app.config.enable_tool_source_display and not trans.user_is_admin: + raise exceptions.InsufficientPermissionsException("Only administrators may display tool sources on this Galaxy server.") + tool = self._get_tool(id, user=trans.user, tool_version=kwds.get('tool_version')) + trans.response.headers['language'] = tool.tool_source.language + return tool.tool_source.to_string() + @expose_api_anonymous def fetch(self, trans: GalaxyWebTransaction, payload, **kwd): """Adapt clean API to tool-constrained API. diff --git a/lib/galaxy/webapps/galaxy/buildapp.py b/lib/galaxy/webapps/galaxy/buildapp.py index 79fba83aa888..5e5cb7006884 100644 --- a/lib/galaxy/webapps/galaxy/buildapp.py +++ b/lib/galaxy/webapps/galaxy/buildapp.py @@ -434,6 +434,7 @@ def populate_api_routes(webapp, app): webapp.mapper.connect('/api/tools/{tool_id:.+?}/convert', action='conversion', controller="tools", conditions=dict(method=["POST"])) webapp.mapper.connect('/api/tools/{id:.+?}/xrefs', action='xrefs', controller="tools") webapp.mapper.connect('/api/tools/{id:.+?}/download', action='download', controller="tools") + webapp.mapper.connect('/api/tools/{id:.+?}/raw_tool_source', action='raw_tool_source', controller="tools") webapp.mapper.connect('/api/tools/{id:.+?}/requirements', action='requirements', controller="tools") webapp.mapper.connect('/api/tools/{id:.+?}/install_dependencies', action='install_dependencies', controller="tools", conditions=dict(method=["POST"])) webapp.mapper.connect('/api/tools/{id:.+?}/dependencies', action='install_dependencies', controller="tools", conditions=dict(method=["POST"])) diff --git a/lib/galaxy/webapps/galaxy/config_schema.yml b/lib/galaxy/webapps/galaxy/config_schema.yml index 2292290f9cea..1913950b43aa 100644 --- a/lib/galaxy/webapps/galaxy/config_schema.yml +++ b/lib/galaxy/webapps/galaxy/config_schema.yml @@ -2821,6 +2821,17 @@ mapping: Details" option in the history. This option also exposes the command line to non-administrative users. Administrators can always see dataset paths. + enable_tool_source_display: + type: bool + default: false + required: false + desc: | + This option allows users to view the tool wrapper source code. This is + safe to enable if you have not hardcoded any secrets in any of the tool + wrappers installed on this Galaxy server. If you have only installed tool + wrappers from public tool sheds and tools shipped with Galaxy there you + can enable this option. + job_metrics_config_file: type: str default: job_metrics_conf.xml diff --git a/lib/galaxy_test/api/test_tools.py b/lib/galaxy_test/api/test_tools.py index 85d7b91c0210..7454db58f546 100644 --- a/lib/galaxy_test/api/test_tools.py +++ b/lib/galaxy_test/api/test_tools.py @@ -1195,6 +1195,17 @@ def test_show_dynamic_tools(self): assert found_id assert found_uuid + def test_show_tool_source_admin(self): + response = self._get("tools/cat1/raw_tool_source", admin=True) + response.raise_for_status() + assert "Concatenate datasets" in response.text + assert response.headers["language"] == 'xml' + + def test_show_tool_source_denied(self): + with self._different_user(anon=True): + response = self._get("tools/cat1/raw_tool_source") + assert response.status_code == 403 + def test_tool_deactivate(self): # Create tool. tool_response = self.dataset_populator.create_tool(MINIMAL_TOOL_NO_ID)