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:
Dave Bouvier
2021-07-19 15:56:22 -04:00
parent 797b7129f6
commit a0c824942e
10 changed files with 202 additions and 86 deletions
+7
View File
@@ -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
View File
@@ -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
View File
@@ -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>
+6
View File
@@ -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);
},
+4 -6
View File
@@ -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)
+1 -1
View File
@@ -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
View File
@@ -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))
+5
View File
@@ -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