mirror of
https://github.com/galaxyproject/galaxy.git
synced 2026-10-24 02:04:04 +08:00
First pass at modifying the tool HTML allow list
- Enable partial matches for owner, repository, or tool ID - Add API endpoints to GET, PUT, and DELETE list entries - Partially vuefy client components - Implement client-side interface with GET endpoint TODO: Implement interface with PUT and DELETE endpoints.
This commit is contained in:
@@ -84,6 +84,12 @@
|
||||
|
||||
<h4>Tool Management</h4>
|
||||
<ul>
|
||||
<li>
|
||||
<strong>
|
||||
<a @click.prevent="useRouter" :href="adminSanitizeAllowUrl">Manage Allowlist</a>
|
||||
</strong>
|
||||
- Manage HTML rendering for installed tools' output datasets.
|
||||
</li>
|
||||
<li v-if="isToolShedInstalled">
|
||||
<strong>
|
||||
<a @click.prevent="useRouter" :href="adminToolshedUrl">Install and Uninstall</a>
|
||||
@@ -150,6 +156,7 @@ export default {
|
||||
adminQuotasUrl: () => `${root}admin/quotas`,
|
||||
adminGroupsUrl: () => `${root}admin/groups`,
|
||||
adminRolesUrl: () => `${root}admin/roles`,
|
||||
adminSanitizeAllowUrl: () => `${root}admin/sanitize_allow`,
|
||||
adminFormsUrl: () => `${root}admin/forms`,
|
||||
adminToolshedUrl: () => `${root}admin/toolshed`,
|
||||
adminToolVersionsUrl: () => `${root}admin/tool_versions`,
|
||||
|
||||
+65
@@ -0,0 +1,65 @@
|
||||
<template>
|
||||
<div>
|
||||
<message :message="message" :status="status"></message>
|
||||
<component
|
||||
:is="currentView"
|
||||
v-if="status !== 'error'"
|
||||
v-bind="currentProps"
|
||||
>
|
||||
</component>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { getAppRoot } from "onload/loadConfig";
|
||||
import axios from "axios";
|
||||
import Message from "../Message.vue";
|
||||
import SanitizeAllowGrid from "./SanitizeAllowGrid.vue";
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
currentView: "sanitize-allow-grid",
|
||||
isLoaded: false,
|
||||
allowList: [],
|
||||
message: "",
|
||||
status: "",
|
||||
};
|
||||
},
|
||||
|
||||
components: {
|
||||
message: Message,
|
||||
"sanitize-allow-grid": SanitizeAllowGrid,
|
||||
},
|
||||
|
||||
computed: {
|
||||
currentProps() {
|
||||
let props;
|
||||
|
||||
if (this.currentView === "sanitize-allow-grid") {
|
||||
props = {
|
||||
isLoaded: this.isLoaded,
|
||||
rows: this.allowList,
|
||||
};
|
||||
}
|
||||
console.log(props);
|
||||
|
||||
return props;
|
||||
},
|
||||
},
|
||||
|
||||
created() {
|
||||
axios
|
||||
.get(`${getAppRoot()}api/sanitize_allowlist`)
|
||||
.then((response) => {
|
||||
this.isLoaded = true;
|
||||
this.allowList = response.data.data;
|
||||
this.message = response.data.message;
|
||||
this.status = response.data.status;
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(error);
|
||||
});
|
||||
},
|
||||
};
|
||||
</script>
|
||||
+48
@@ -0,0 +1,48 @@
|
||||
<template>
|
||||
<base-grid :is-loaded="isLoaded" :columns="columns" id="sanitize-allow-grid">
|
||||
<template v-slot:title> Tool Sanitization Allowlist </template>
|
||||
<template v-slot:rows>
|
||||
<template v-for="(row, index) in rows">
|
||||
<tr :key="row.tool_id" :class="[index % 2 === 0 ? 'tr' : 'odd_row']">
|
||||
<td>{{ row.tool_name }}</td>
|
||||
<td>{{ row.tool_id }}</td>
|
||||
<td>{{ row.allowed }}</td>
|
||||
<td>{{ row.toolshed }}</td>
|
||||
</tr>
|
||||
</template>
|
||||
</template>
|
||||
</base-grid>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import BaseGrid from "./BaseGrid.vue";
|
||||
|
||||
export default {
|
||||
props: {
|
||||
isLoaded: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
rows: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
columns: [
|
||||
{ text: "Tool Name", dataIndex: "tool_name" },
|
||||
{ text: "Tool ID", dataIndex: "tool_id" },
|
||||
{ text: "Allowed", dataIndex: "allowed" },
|
||||
{ text: "Tool Type", dataIndex: "tool_type" },
|
||||
],
|
||||
};
|
||||
},
|
||||
|
||||
components: {
|
||||
"base-grid": BaseGrid,
|
||||
},
|
||||
|
||||
};
|
||||
</script>
|
||||
@@ -7,6 +7,7 @@ import QueryStringParsing from "utils/query-string-parsing";
|
||||
import Router from "layout/router";
|
||||
import DataTables from "components/admin/DataTables.vue";
|
||||
import DataTypes from "components/admin/DataTypes.vue";
|
||||
import SanitizeAllow from "components/admin/SanitizeAllow.vue";
|
||||
import Jobs from "components/admin/Jobs.vue";
|
||||
import ActiveInvocations from "components/admin/ActiveInvocations.vue";
|
||||
import Landing from "components/admin/Dependencies/Landing.vue";
|
||||
@@ -40,6 +41,7 @@ export const getAdminRouter = (Galaxy, options) => {
|
||||
"(/)admin/data_types": "show_data_types",
|
||||
"(/)admin/jobs": "show_jobs",
|
||||
"(/)admin/invocations": "show_invocations",
|
||||
"(/)admin/sanitize_allow": "show_sanitize_allow",
|
||||
"(/)admin/toolbox_dependencies": "show_toolbox_dependencies",
|
||||
"(/)admin/data_manager*path": "show_data_manager",
|
||||
"(/)admin(/)reset_metadata": "show_reset_metadata",
|
||||
@@ -128,6 +130,10 @@ export const getAdminRouter = (Galaxy, options) => {
|
||||
this._display_vue_helper(ActiveInvocations);
|
||||
},
|
||||
|
||||
show_sanitize_allow: function () {
|
||||
this._display_vue_helper(SanitizeAllow);
|
||||
},
|
||||
|
||||
show_toolbox_dependencies: function () {
|
||||
this._display_vue_helper(Landing);
|
||||
},
|
||||
|
||||
@@ -957,14 +957,12 @@ class GalaxyAppConfiguration(BaseAppConfiguration, CommonConfigurationMixin):
|
||||
|
||||
def reload_sanitize_allowlist(self, explicit=True):
|
||||
self.sanitize_allowlist = []
|
||||
try:
|
||||
with open(self.sanitize_allowlist_file) as f:
|
||||
for line in f.readlines():
|
||||
if not line.startswith("#"):
|
||||
self.sanitize_allowlist.append(line.strip())
|
||||
except OSError:
|
||||
if not os.path.exists(self.sanitize_allowlist_file):
|
||||
if explicit:
|
||||
log.warning("Sanitize log file explicitly specified as '%s' but does not exist, continuing with no tools allowlisted.", self.sanitize_allowlist_file)
|
||||
else:
|
||||
with open(self.sanitize_allowlist_file) as f:
|
||||
self.sanitize_allowlist = [line.strip() for line in f.readlines() if not line.startswith('#')]
|
||||
|
||||
def ensure_tempdir(self):
|
||||
self._ensure_directory(self.new_file_path)
|
||||
|
||||
@@ -460,7 +460,7 @@ class Data(metaclass=DataMeta):
|
||||
# Sanitize anytime we respond with plain text/html content.
|
||||
# Check to see if this dataset's parent job is allowlisted
|
||||
# We cannot currently trust imported datasets for rendering.
|
||||
if not from_dataset.creating_job.imported and from_dataset.creating_job.tool_id in trans.app.config.sanitize_allowlist:
|
||||
if not from_dataset.creating_job.imported and from_dataset.creating_job.tool_id.startswith(tuple(trans.app.config.sanitize_allowlist)):
|
||||
return open(filename, mode='rb')
|
||||
|
||||
# This is returning to the browser, it needs to be encoded.
|
||||
|
||||
+66
@@ -0,0 +1,66 @@
|
||||
"""
|
||||
API operations allowing clients to retrieve and modify the HTML sanitization allow list.
|
||||
"""
|
||||
import logging
|
||||
|
||||
from galaxy.datatypes.data import Data
|
||||
from galaxy.util import asbool
|
||||
from galaxy import web
|
||||
from galaxy.webapps.base.controller import BaseAPIController
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class SanitizeAllowlistController(BaseAPIController):
|
||||
|
||||
@web.require_admin
|
||||
@web.legacy_expose_api
|
||||
def index(self, trans, **kwd):
|
||||
"""
|
||||
GET /api/sanitize_allowlist
|
||||
Return an object showing the current state of the toolbox and allow list.
|
||||
"""
|
||||
allowlist = []
|
||||
tool_name = None
|
||||
for tool_id in trans.app.config.sanitize_allowlist:
|
||||
for toolbox_id in trans.app.toolbox.tools_by_id:
|
||||
tool = trans.app.toolbox.tools_by_id[toolbox_id]
|
||||
if toolbox_id.startswith(tool_id):
|
||||
tool_name = tool.name
|
||||
break
|
||||
sanitize_tuple = dict(tool_name=tool.name, tool_id=tool_id, allowed=True, toolshed='/' in tool_id)
|
||||
allowlist.append(sanitize_tuple)
|
||||
for tool_id in sorted(trans.app.toolbox.tools_by_id):
|
||||
tool = trans.app.toolbox.tools_by_id[tool_id]
|
||||
if not tool_id.startswith(tuple(trans.app.config.sanitize_allowlist)):
|
||||
tool_status = dict(tool_name=tool.name, tool_id=tool_id, allowed=False, toolshed='/' in tool_id)
|
||||
allowlist.append(tool_status)
|
||||
return {'status': 'done', 'message': 'Tool allow list loaded.', 'data': allowlist}
|
||||
|
||||
@web.require_admin
|
||||
@web.legacy_expose_api
|
||||
def create(self, trans, tool_id, **kwd):
|
||||
"""
|
||||
POST /api/sanitize_allowlist
|
||||
Add a new tool specification to the allow list.
|
||||
"""
|
||||
if tool_id not in trans.app.config.sanitize_allowlist:
|
||||
trans.app.config.sanitize_allowlist.append(tool_id)
|
||||
self._save_allowlist(trans)
|
||||
|
||||
@web.require_admin
|
||||
@web.legacy_expose_api
|
||||
def delete(self, trans, tool_id, **kwd):
|
||||
"""
|
||||
DELETE /api/sanitize_allowlist
|
||||
Add a new tool specification to the allow list.
|
||||
"""
|
||||
if tool_id in trans.app.config.sanitize_allowlist:
|
||||
trans.app.config.sanitize_allowlist.remove(tool_id)
|
||||
self._save_allowlist(trans)
|
||||
|
||||
def _save_allowlist(self, trans):
|
||||
new_allowlist = sorted([tid for tid in trans.app.config.sanitize_allowlist])
|
||||
trans.app.config.sanitize_allowlist = new_allowlist
|
||||
with open(trans.app.config.sanitize_allowlist_file, 'wt') as f:
|
||||
f.write("\n".join(new_allowlist))
|
||||
@@ -143,6 +143,7 @@ def app_pair(global_conf, load_app_kwds=None, wsgi_preflight=True, **kwargs):
|
||||
webapp.add_client_route('/admin/forms', 'admin')
|
||||
webapp.add_client_route('/admin/groups', 'admin')
|
||||
webapp.add_client_route('/admin/repositories', 'admin')
|
||||
webapp.add_client_route('/admin/sanitize_allowlist', 'admin')
|
||||
webapp.add_client_route('/admin/tool_versions', 'admin')
|
||||
webapp.add_client_route('/admin/toolshed', 'admin')
|
||||
webapp.add_client_route('/admin/quotas', 'admin')
|
||||
@@ -429,6 +430,10 @@ def populate_api_routes(webapp, app):
|
||||
webapp.mapper.resource('tool', 'tools', path_prefix='/api')
|
||||
webapp.mapper.resource('dynamic_tools', 'dynamic_tools', path_prefix='/api')
|
||||
|
||||
webapp.mapper.connect('/api/sanitize_allowlist', action='index', controller='sanitize_allowlist', conditions=dict(method=["GET"]))
|
||||
webapp.mapper.connect('/api/sanitize_allowlist', action='create', controller='sanitize_allowlist', conditions=dict(method=["PUT"]))
|
||||
webapp.mapper.connect('/api/sanitize_allowlist', action='delete', controller='sanitize_allowlist', conditions=dict(method=["DELETE"]))
|
||||
|
||||
webapp.mapper.connect('/api/entry_points', action='index', controller="tool_entry_points")
|
||||
webapp.mapper.connect('/api/entry_points/{id:.+?}/access', action='access_entry_point', controller="tool_entry_points")
|
||||
webapp.mapper.connect('/api/entry_points/{id:.+?}', action='stop_entry_point', controller="tool_entry_points", conditions={'method': ['DELETE']})
|
||||
|
||||
@@ -1554,26 +1554,6 @@ class AdminGalaxy(controller.JSAppLauncher, AdminActions, UsesQuotaMixin, QuotaP
|
||||
unused_environments=view.unused_dependency_paths,
|
||||
viewkey=viewkey)
|
||||
|
||||
@web.expose
|
||||
@web.require_admin
|
||||
def sanitize_allowlist(self, trans, submit_allowlist=False, tools_to_allowlist=None):
|
||||
tools_to_allowlist = tools_to_allowlist or []
|
||||
if submit_allowlist:
|
||||
# write the configured sanitize_allowlist_file with new allowlist
|
||||
# and update in-memory list.
|
||||
with open(trans.app.config.sanitize_allowlist_file, 'wt') as f:
|
||||
if isinstance(tools_to_allowlist, str):
|
||||
tools_to_allowlist = [tools_to_allowlist]
|
||||
new_allowlist = sorted([tid for tid in tools_to_allowlist if tid in trans.app.toolbox.tools_by_id])
|
||||
f.write("\n".join(new_allowlist))
|
||||
trans.app.config.sanitize_allowlist = new_allowlist
|
||||
trans.app.queue_worker.send_control_task('reload_sanitize_allowlist', noop_self=True)
|
||||
# dispatch a message to reload list for other processes
|
||||
return trans.fill_template('/webapps/galaxy/admin/sanitize_allowlist.mako',
|
||||
sanitize_all=trans.app.config.sanitize_all_html,
|
||||
tools=trans.app.toolbox.tools_by_id)
|
||||
|
||||
|
||||
# ---- Utility methods -------------------------------------------------------
|
||||
|
||||
def build_select_input(name, label, options, value):
|
||||
|
||||
@@ -1,59 +0,0 @@
|
||||
<%inherit file="/base.mako"/>
|
||||
<%namespace file="/message.mako" import="render_msg" />
|
||||
|
||||
%if message:
|
||||
${render_msg( message, status )}
|
||||
%endif
|
||||
|
||||
%if not sanitize_all:
|
||||
<div><p>You currently have <strong>sanitize_all_html</strong> set to False
|
||||
in your galaxy configuration file. This prevents Galaxy from sanitizing
|
||||
tool outputs, which is an important security feature. For improved
|
||||
security, we recommend you disable the old-style blanket sanitization and
|
||||
manage it via this allowlist instead.</p></div>
|
||||
%else:
|
||||
<div><p>This interface will allow you to mark particular tools as 'trusted'
|
||||
after which Galaxy will no longer attempt to sanitize any HTML contents of
|
||||
datasets created by these tools upon display. Please be aware of the
|
||||
potential security implications of doing this -- bypassing sanitization
|
||||
using this allowlist disables Galaxy's security feature (for the indicated
|
||||
tools) that prevents Galaxy from displaying potentially malicious
|
||||
Javascript.<br/>
|
||||
Note that datasets originating from an archive import are still sanitized
|
||||
even when their creating tool is allowlisted since it isn't possible to
|
||||
validate the information supplied in the archive.</p></div>
|
||||
<form name="sanitize_allowlist" method="post" action="${h.url_for( controller='admin', action='sanitize_allowlist' )}">
|
||||
<div class="card mb-3">
|
||||
<div class="card-header">Tool Sanitization Allowlist</div>
|
||||
<div class="card-body overflow-auto">
|
||||
<table class="manage-table colored" border="0" cellspacing="0" cellpadding="0" width="100%">
|
||||
<tr>
|
||||
<th>Allowlist</th>
|
||||
<th>Name</th>
|
||||
<th>ID</th>
|
||||
</tr>
|
||||
<% ctr = 0 %>
|
||||
%for tool in tools.values():
|
||||
%if ctr % 2 == 1:
|
||||
<tr class="odd_row">
|
||||
%else:
|
||||
<tr class="tr">
|
||||
%endif
|
||||
<td>
|
||||
%if tool.id in trans.app.config.sanitize_allowlist:
|
||||
<input type="checkbox" name="tools_to_allowlist" value="${tool.id}" checked="checked"/>
|
||||
%else:
|
||||
<input type="checkbox" name="tools_to_allowlist" value="${tool.id}"/>
|
||||
%endif
|
||||
</td>
|
||||
<td>${ tool.name | h }</td>
|
||||
<td>${ tool.id | h }</td>
|
||||
</tr>
|
||||
<% ctr += 1 %>
|
||||
%endfor
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<input type="submit" name="submit_allowlist" value="Submit new allowlist"/>
|
||||
</form>
|
||||
%endif
|
||||
Reference in New Issue
Block a user