Skip to content

Commit

Permalink
Scottx611x/persistent visualization urls (#2638)
Browse files Browse the repository at this point in the history
* Add VisualizationToolProxy so that we can check user perms. & provide persistant VisTool urls

* Add `user_has_access_to_tool()`

* Fix reference

* Add test coverage

* Fix typo

* Rename VizualizationProxy to AutoRelaunchProxy

* Simplify logic

* Less wordy error messages

* Fix test

* Just assert that we can launch Igv;
Towards #2238 and refinery-platform/visualization-tools#22
  • Loading branch information
scottx611x authored Mar 1, 2018
1 parent 6d5f995 commit 6972de6
Show file tree
Hide file tree
Showing 4 changed files with 120 additions and 71 deletions.
86 changes: 48 additions & 38 deletions refinery/tool_manager/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,8 +61,6 @@
ToolFactory)
from factory_boy.utils import create_dataset_with_necessary_models
from file_store.models import FileStoreItem, FileType
from selenium_testing.utils import (MAX_WAIT, SeleniumTestBaseGeneric,
wait_until_class_visible)
from tool_manager.management.commands.load_tools import \
Command as LoadToolsCommand
from tool_manager.tasks import django_docker_cleanup
Expand All @@ -72,7 +70,7 @@
VisualizationToolError, WorkflowTool)
from .utils import (FileTypeValidationError, create_tool,
create_tool_definition, get_workflows,
validate_tool_annotation,
user_has_access_to_tool, validate_tool_annotation,
validate_tool_launch_configuration,
validate_workflow_step_annotation)
from .views import ToolDefinitionsViewSet, ToolsViewSet
Expand Down Expand Up @@ -2874,7 +2872,7 @@ def test_relaunch_failure_insufficient_user_perms(self):
uuid=self.tool.uuid
)
self.assertEqual(get_response.status_code, 403)
self.assertIn("not have sufficient permissions",
self.assertIn("User does not have permission",
get_response.content)

def test_relaunch_failure_tool_already_running(self):
Expand Down Expand Up @@ -2979,6 +2977,38 @@ def test_get_with_invalid_tool_type_request_param(self):
self._make_tools_get_request(tool_type="coffee")
self.assertEqual(self.get_response.data, [])

def _test_launch_vis_container(self, user_has_permission=True):
self.create_tool(ToolDefinition.VISUALIZATION)
self.assertFalse(self.tool.is_running())

if user_has_permission:
assign_perm('core.read_dataset', self.user, self.tool.dataset)

# Need to set_password() to be able to login. Otherwise
# user.password is the hash representation which is not what the
# login() expects
temp_password = "password"
self.user.set_password(temp_password)
self.user.save()
self.client.login(username=self.user.username,
password=temp_password)

with mock.patch.object(VisualizationTool, "launch") as launch_mock:
get_response = self.client.get(
"{}/".format(self.tool.get_relative_container_url())
)
if user_has_permission:
self.assertTrue(launch_mock.called)
else:
self.assertEqual(get_response.status_code, 403)
self.assertFalse(launch_mock.called)

def test_vis_tool_url_after_container_removed_relaunches(self):
self._test_launch_vis_container()

def test_vis_tool_url_user_without_permission(self):
self._test_launch_vis_container(user_has_permission=False)


class WorkflowToolLaunchTests(ToolManagerTestBase):
tasks_mock = "analysis_manager.tasks"
Expand Down Expand Up @@ -3443,26 +3473,19 @@ def test_workflow_tool_analysis_name(self):
)


class VisualizationToolLaunchTests(ToolManagerTestBase, # TODO: Cypress
SeleniumTestBaseGeneric):
class VisualizationToolLaunchTests(ToolManagerTestBase):
def setUp(self):
# super() will only ever resolve a single class type for a given method
ToolManagerTestBase.setUp(self)
SeleniumTestBaseGeneric.setUp(self)
super(VisualizationToolLaunchTests, self).setUp()

self.sample_igv_file_url = "http://www.example.com/sample.seg"

self.sample_igv_file = urljoin(
self.live_server_url,
"/tool_manager/test_data/sample.seg"
)
mock.patch.object(
LoadToolsCommand,
"_get_available_visualization_tool_registry_names",
).start()

def tearDown(self):
# super() will only ever resolve a single class type for a given method
ToolManagerTestBase.tearDown(self)
SeleniumTestBaseGeneric.tearDown(self)
super(VisualizationToolLaunchTests, self).tearDown()

# Explicitly call delete() to purge any containers we spun up
Tool.objects.all().delete()
Expand Down Expand Up @@ -3502,7 +3525,7 @@ def _start_visualization(
"dataset_uuid": self.dataset.uuid,
"tool_definition_uuid": self.td.uuid,
Tool.FILE_RELATIONSHIPS: "[{}]".format(
self.make_node(source=self.sample_igv_file)
self.make_node(source=self.sample_igv_file_url)
),
ToolDefinition.PARAMETERS: {
self.mock_parameter.uuid: self.mock_parameter.default_value
Expand Down Expand Up @@ -3533,28 +3556,9 @@ def _start_visualization(
assertions(last_tool)

def test_IGV(self):
def assertions(tool):
# Check to see if IGV shows what we want
igv_url = urljoin(
self.live_server_url,
tool.get_relative_container_url()
)

self.browser.get(igv_url)
time.sleep(15)

wait_until_class_visible(self.browser, "igv-track-label", MAX_WAIT)
self.assertIn(
"sample.seg",
self.browser.find_elements_by_class_name(
"igv-track-label"
)[0].text
)

self._start_visualization(
'igv.json',
self.sample_igv_file,
assertions
self.sample_igv_file_url
)

def test_HiGlass(self):
Expand Down Expand Up @@ -3614,7 +3618,7 @@ def assertions(tool):

self._start_visualization(
'igv.json',
self.sample_igv_file,
self.sample_igv_file_url,
assertions
)

Expand Down Expand Up @@ -3839,6 +3843,12 @@ def test_get_workflows_with_connection_error(self):
context.exception.message
)

def test_user_has_access_to_tool(self):
self.create_tool(ToolDefinition.VISUALIZATION)
assign_perm('core.read_dataset', self.user, self.tool.dataset)
self.assertTrue(user_has_access_to_tool(self.user, self.tool))
self.assertFalse(user_has_access_to_tool(self.user2, self.tool))


class ParameterTests(TestCase):
def test_cast_param_value_to_proper_type_bool(self):
Expand Down
31 changes: 2 additions & 29 deletions refinery/tool_manager/urls.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
from django.conf import settings
from django.conf.urls import include, url

from django_docker_engine.proxy import Proxy
from rest_framework.routers import DefaultRouter

from .views import ToolDefinitionsViewSet, ToolsViewSet
from .views import AutoRelaunchProxy, ToolDefinitionsViewSet, ToolsViewSet

# DRF url routing
tool_manager_router = DefaultRouter()
Expand All @@ -15,33 +14,7 @@
base_name="tooldefinition"
)

url_patterns = Proxy(
settings.DJANGO_DOCKER_ENGINE_DATA_DIR,
please_wait_title='Please wait...',
please_wait_body_html='''
<style>
body {{
font-family: "Source Sans Pro",Helvetica,Arial,sans-serif;
font-size: 40pt;
text-align: center;
}}
div {{
position: absolute;
top: 50%;
left: 50%;
margin-right: -50%;
transform: translate(-50%, -50%)
}}
</style>
<div>
<img src="{0}/logo.svg" width='150'>
<p>Please wait...</p>
<img src="{0}/spinner.gif">
</div>
'''.format(settings.STATIC_URL + 'images') # noqa
).url_patterns()
django_docker_engine_url = url(
r'^{}/'.format(settings.DJANGO_DOCKER_ENGINE_BASE_URL),
include(url_patterns)
include(AutoRelaunchProxy().url_patterns())
)
4 changes: 4 additions & 0 deletions refinery/tool_manager/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -565,3 +565,7 @@ def create_expanded_workflow_graph(galaxy_workflow_dict):
)
graph[parent_node_id][current_node_id]['input_id'] = edge_input_id
return graph


def user_has_access_to_tool(user, tool):
return user.has_perm('core.read_dataset', tool.dataset)
70 changes: 66 additions & 4 deletions refinery/tool_manager/views.py
Original file line number Diff line number Diff line change
@@ -1,18 +1,22 @@
import logging

from django.conf import settings
from django.db import transaction
from django.http import HttpResponseBadRequest, HttpResponseForbidden

from django_docker_engine.proxy import Proxy
from rest_framework import status
from rest_framework.decorators import detail_route
from rest_framework.generics import get_object_or_404
from rest_framework.response import Response
from rest_framework.viewsets import ModelViewSet

from core.models import DataSet

from .models import Tool, ToolDefinition, VisualizationTool, WorkflowTool
from .serializers import ToolDefinitionSerializer, ToolSerializer
from .utils import create_tool, validate_tool_launch_configuration
from .utils import (create_tool, user_has_access_to_tool,
validate_tool_launch_configuration)

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -150,10 +154,11 @@ def relaunch(self, request, *args, **kwargs):
.format(tool_uuid, e)
)

if not request.user.has_perm('core.read_dataset', tool.dataset):
if not user_has_access_to_tool(request.user, tool):
return HttpResponseForbidden(
"Requesting User does not have sufficient permissions to "
"relaunch Tool with uuid: {}".format(tool_uuid)
"User does not have permission to view Tool: {}".format(
tool_uuid
)
)

if tool.is_running():
Expand All @@ -164,3 +169,60 @@ def relaunch(self, request, *args, **kwargs):
except Exception as e:
logger.error(e)
return HttpResponseBadRequest(e)


class AutoRelaunchProxy(Proxy, object):
"""
Wrapper around Django-Docker-Engine Proxy to allow for VisualizationTools
that had been launched previously to have persisting urls even after
their containers hve been destroyed, rather than relying on users to
manually relaunch (although that remains an option).
"""
def __init__(self):
super(AutoRelaunchProxy, self).__init__(
settings.DJANGO_DOCKER_ENGINE_DATA_DIR,
please_wait_title='Please wait...',
please_wait_body_html='''
<style>
body {{
font-family: "Source Sans Pro",Helvetica,Arial,sans-serif;
font-size: 40pt;
text-align: center;
}}
div {{
position: absolute;
top: 50%;
left: 50%;
margin-right: -50%;
transform: translate(-50%, -50%)
}}
</style>
<div>
<img src="{0}/logo.svg" width='150'>
<p>Please wait...</p>
<img src="{0}/spinner.gif">
</div>
'''.format(settings.STATIC_URL + 'images')
)

def _proxy_view(self, request, container_name, url):
visualization_tool = get_object_or_404(
VisualizationTool,
container_name=container_name
)
if not user_has_access_to_tool(request.user, visualization_tool):
return HttpResponseForbidden(
"User does not have permission to view Tool: {}".format(
visualization_tool.uuid
)
)

if not visualization_tool.is_running():
visualization_tool.launch()

return super(AutoRelaunchProxy, self)._proxy_view(
request,
container_name,
url
)

0 comments on commit 6972de6

Please sign in to comment.