Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
b92045a
Basic whitelist for allowing 'save' objects
will-moore Jan 25, 2017
39618ef
SaveView restricts PUT to can_put whitelist
will-moore Jan 26, 2017
c1b626a
Uppercase CAN_PUT, CAN_POST. Both allow P/D/S
will-moore Jan 26, 2017
c52550b
Add CAN_DELETE check into ObjectView.delete()
will-moore Jan 27, 2017
756ae7d
Return status=405 'method not supported' for disallowed methods
will-moore Jan 27, 2017
afcf581
Add test to check POST, PUT & DELETE not supported for Images
will-moore Jan 27, 2017
a1a769c
flake8 fixes
will-moore Jan 27, 2017
0303161
Remove 'Plate' from test_container_crud()
will-moore Jan 30, 2017
d1baadc
Tiny fix to test_api_images to avoid merge conflict
will-moore Feb 14, 2017
34aeb5b
Merge remote-tracking branch 'origin/develop' into api_save_whitelist
will-moore Feb 14, 2017
fd204cc
Test handling of create, update & delete for unsupported types
will-moore Feb 14, 2017
da75513
Move test_crud_unsupported to test_api_containers.py
will-moore Feb 14, 2017
17682f3
Remove comment
will-moore Feb 14, 2017
b7e936e
flake8 fix
will-moore Feb 14, 2017
ddd187a
Fix failing test_api_errors.py tests
will-moore Feb 16, 2017
4b72827
Tidy api_exceptions imports
will-moore Feb 16, 2017
102c6c9
Split test_crud_unsupported into separate tests
will-moore Feb 16, 2017
a377f3d
flake8 fix
will-moore Feb 16, 2017
242c551
flake8 fix
will-moore Feb 16, 2017
eb18edc
ApiException base class to avoid code duplication
will-moore Feb 17, 2017
4c73782
Merge remote-tracking branch 'origin/develop' into api_save_whitelist
will-moore Feb 17, 2017
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 21 additions & 11 deletions components/tools/OmeroWeb/omeroweb/api/api_exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,16 @@
"""Exceptions used by the api/views methods."""


class BadRequestError(Exception):
class ApiException(Exception):
"""A base exception class that handles message and stactrace."""

def __init__(self, message, stacktrace=None):
"""Override init to handle message and stacktrace."""
super(ApiException, self).__init__(message)
self.stacktrace = stacktrace


class BadRequestError(ApiException):
"""
An exception that will result in a response status of 400.

Expand All @@ -29,13 +38,8 @@ class BadRequestError(Exception):

status = 400

def __init__(self, message, stacktrace=None):
"""Override init to handle message and stacktrace."""
super(BadRequestError, self).__init__(message)
self.stacktrace = stacktrace


class NotFoundError(Exception):
class NotFoundError(ApiException):
"""
An exception that will result in a response status of 404.

Expand All @@ -44,10 +48,16 @@ class NotFoundError(Exception):

status = 404

def __init__(self, message, stacktrace=None):
"""Override init to handle message and stacktrace."""
super(NotFoundError, self).__init__(message)
self.stacktrace = stacktrace

class MethodNotSupportedError(ApiException):
"""
An exception that will result in a response status of 405.

Raised if user tries to DELETE, POST or PUT for an Object
where we don't support those methods.
"""

status = 405


class CreatedObject(Exception):
Expand Down
7 changes: 5 additions & 2 deletions components/tools/OmeroWeb/omeroweb/api/decorators.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,10 @@
import traceback
from django.http import JsonResponse
from functools import update_wrapper
from api_exceptions import NotFoundError, BadRequestError, CreatedObject
from api_exceptions import BadRequestError, \
CreatedObject, \
MethodNotSupportedError, \
NotFoundError


logger = logging.getLogger(__name__)
Expand Down Expand Up @@ -75,7 +78,7 @@ def handle_error(self, ex, trace):
# But we try to handle all 'expected' errors appropriately
# TODO: handle omero.ConcurrencyException

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will you do that in another PR? i.e. TODO

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes. Hard to see that we need to support ConcurrencyException since we are doing very simple get/set queries now and no stateful services.

status = 500
if isinstance(ex, NotFoundError):
if isinstance(ex, (NotFoundError, MethodNotSupportedError)):
status = ex.status
if isinstance(ex, BadRequestError):
status = ex.status
Expand Down
40 changes: 36 additions & 4 deletions components/tools/OmeroWeb/omeroweb/api/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,10 @@
from omero_marshal import get_encoder, get_decoder, OME_SCHEMA_URL
from omero import ValidationException
from omeroweb.connector import Server
from omeroweb.api.api_exceptions import BadRequestError, NotFoundError, \
CreatedObject
from api_exceptions import BadRequestError, \
CreatedObject, \
MethodNotSupportedError, \
NotFoundError
from omeroweb.api.decorators import login_required, json_response
from omeroweb.webgateway.util import getIntOrDefault

Expand Down Expand Up @@ -149,6 +151,8 @@ def add_data(self, marshalled, request, urls=None, **kwargs):
class ObjectView(ApiView):
"""Handle access to an individual Object to GET or DELETE it."""

CAN_DELETE = True

def get_opts(self, request):
"""Return a dict for use in conn.getObjects() based on request."""
return {}
Expand Down Expand Up @@ -184,6 +188,9 @@ def delete(self, request, object_id, conn=None, **kwargs):

Return 404 if not found.
"""
if not self.CAN_DELETE:
raise MethodNotSupportedError(
"Delete of %s not supported" % self.OMERO_TYPE)
try:
obj = conn.getQueryService().get(self.OMERO_TYPE, long(object_id),
conn.SERVICE_OPTS)
Expand Down Expand Up @@ -225,6 +232,8 @@ class ImageView(ObjectView):

OMERO_TYPE = 'Image'

CAN_DELETE = False

def get_opts(self, request):
"""Add support for load_pixels and load_channels."""
opts = super(ImageView, self).get_opts(request)
Expand All @@ -250,6 +259,8 @@ class PlateView(ObjectView):

OMERO_TYPE = 'Plate'

CAN_DELETE = False

# Urls to add to marshalled object. See ProjectsView for more details
urls = {
'url:wells': {'name': 'api_plate_wells',
Expand All @@ -262,6 +273,8 @@ class WellView(ObjectView):

OMERO_TYPE = 'Well'

CAN_DELETE = False

def get_opts(self, request):
"""Add support for load_images."""
opts = super(WellView, self).get_opts(request)
Expand Down Expand Up @@ -498,19 +511,36 @@ class SaveView(View):
POST to create a new Object and PUT to replace existing one.
"""

CAN_PUT = ['Project', 'Dataset', 'Screen']

CAN_POST = ['Project', 'Dataset', 'Screen']

@method_decorator(login_required(useragent='OMERO.webapi'))
@method_decorator(json_response())
def dispatch(self, *args, **kwargs):
"""Apply decorators for class methods below."""
return super(SaveView, self).dispatch(*args, **kwargs)

def get_type_name(self, marshalled):
"""Get the '@type' name from marshalled data."""
if '@type' not in marshalled:
raise BadRequestError('Need to specify @type attribute')
schema_type = marshalled['@type']
if '#' not in schema_type:
return None
return schema_type.split('#')[1]

def put(self, request, conn=None, **kwargs):
"""
PUT handles saving of existing objects.

Therefore '@id' should be set.
"""
object_json = json.loads(request.body)
obj_type = self.get_type_name(object_json)
if obj_type not in self.CAN_PUT:
raise MethodNotSupportedError(
"Update of %s not supported" % obj_type)
if '@id' not in object_json:
raise BadRequestError(
"No '@id' attribute. Use POST to create new objects")
Expand All @@ -523,6 +553,10 @@ def post(self, request, conn=None, **kwargs):
Therefore '@id' should not be set.
"""
object_json = json.loads(request.body)
obj_type = self.get_type_name(object_json)
if obj_type not in self.CAN_POST:
raise MethodNotSupportedError(
"Creation of %s not supported" % obj_type)
if '@id' in object_json:
raise BadRequestError(
"Object has '@id' attribute. Use PUT to update objects")
Expand All @@ -535,8 +569,6 @@ def _save_object(self, request, conn, object_json, **kwargs):
# Try to get group from request, OR from details below...
group = getIntOrDefault(request, 'group', None)
decoder = None
if '@type' not in object_json:
raise BadRequestError('Need to specify @type attribute')
objType = object_json['@type']
decoder = get_decoder(objType)
# If we are passed incomplete object, or decoder couldn't be found...
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
WellI, \
WellSampleI
from omero.rtypes import rstring, rint
from omero_marshal import OME_SCHEMA_URL


def build_url(client, url_name, url_kwargs):
Expand Down Expand Up @@ -178,8 +179,41 @@ def user_screens(self, user1):
screens.sort(cmp_name_insensitive)
return screens

@pytest.mark.parametrize("dtype", ['Plate', 'Image', 'Well',
'Channel', 'foo'])
@pytest.mark.parametrize("method", [(_csrf_post_json, 'Creation'),
(_csrf_put_json, 'Update')])
def test_create_update_unsupported(self, user1, dtype, method):
"""Test create and update are rejected for unsupported types."""
conn = get_connection(user1)
user_name = conn.getUser().getName()
django_client = self.new_django_client(user_name, user_name)
version = settings.API_VERSIONS[-1]
save_url = reverse('api_save', kwargs={'api_version': version})
payload = {'Name': 'test',
'@type': OME_SCHEMA_URL + '#%s' % dtype}
# Test PUT/POST
rsp = method[0](django_client, save_url, payload,
status_code=405)
assert rsp['message'] == '%s of %s not supported' % (method[1], dtype)

@pytest.mark.parametrize("dtype", ['Plate', 'Image', 'Well'])
def test_delete_unsupported(self, user1, dtype):
"""Test delete is rejected for unsupported types."""
conn = get_connection(user1)
user_name = conn.getUser().getName()
django_client = self.new_django_client(user_name, user_name)
version = settings.API_VERSIONS[-1]
# Delete (fake url - image doesn't need to exist for test)
url_name = 'api_%s' % dtype.lower()
delete_url = reverse(url_name, kwargs={'api_version': version,
'object_id': 1})
rsp = _csrf_delete_response_json(django_client, delete_url, {},
status_code=405)
assert rsp['message'] == 'Delete of %s not supported' % dtype

@pytest.mark.parametrize("dtype", ['Project', 'Dataset',
'Screen', 'Plate'])
'Screen'])
def test_container_crud(self, dtype):
"""
Test create, read, update and delete of Containers.
Expand Down
22 changes: 12 additions & 10 deletions components/tools/OmeroWeb/test/integration/test_api_errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
from django.conf import settings
import pytest
from test_api_projects import get_connection
from omero.model import ProjectI
from omero.model import ProjectI, TagAnnotationI
from omero.rtypes import rstring
from omero_marshal import get_encoder, get_decoder, OME_SCHEMA_URL
from omero import ValidationException
Expand Down Expand Up @@ -59,6 +59,7 @@ def test_save_post_no_id(self):
version = settings.API_VERSIONS[-1]
save_url = reverse('api_save', kwargs={'api_version': version})
payload = {'Name': 'test_save_post_no_id',
'@type': '%s#Project' % OME_SCHEMA_URL,
'@id': 1}
rsp = _csrf_post_json(django_client, save_url, payload,
status_code=400)
Expand All @@ -70,7 +71,8 @@ def test_save_put_id(self):
django_client = self.django_root_client
version = settings.API_VERSIONS[-1]
save_url = reverse('api_save', kwargs={'api_version': version})
payload = {'Name': 'test_save_put_id'}
payload = {'Name': 'test_save_put_id',
'@type': '%s#Project' % OME_SCHEMA_URL}
rsp = _csrf_put_json(django_client, save_url, payload,
status_code=400)
assert (rsp['message'] ==
Expand All @@ -81,7 +83,7 @@ def test_marshal_type(self):
django_client = self.django_root_client
version = settings.API_VERSIONS[-1]
save_url = reverse('api_save', kwargs={'api_version': version})
objType = 'SomeInvalid#Type'
objType = 'SomeInvalidSchema#Project'
payload = {'Name': 'test_marshal_type',
'@type': objType}
rsp = _csrf_post_json(django_client, save_url, payload,
Expand Down Expand Up @@ -142,16 +144,16 @@ def test_validation_exception(self, user_A):
save_url += '?group=' + str(group)

# Create Tag
tag = {'Value': 'test_tag',
'@type': OME_SCHEMA_URL + '#TagAnnotation'}
rsp = _csrf_post_json(django_client, save_url, tag,
status_code=201)
tag_rsp = rsp['data']
tag = TagAnnotationI()
tag.textValue = rstring('test_tag')
tag = conn.getUpdateService().saveAndReturnObject(tag)
tag_json = {'Value': 'test_tag',
'@id': tag.id.val,
'@type': OME_SCHEMA_URL + '#TagAnnotation'}
# Add Tag twice to Project to get Validation Exception
del tag_rsp['omero:details']
payload = {'Name': 'test_validation',
'@type': OME_SCHEMA_URL + '#Project',
'Annotations': [tag_rsp, tag_rsp]}
'Annotations': [tag_json, tag_json]}
rsp = _csrf_post_json(django_client, save_url, payload,
status_code=400)
# NB: message contains whole stack trace
Expand Down