Skip to content
20 changes: 20 additions & 0 deletions openquake/server/tests/test_impact_mode.py
Original file line number Diff line number Diff line change
Expand Up @@ -265,6 +265,11 @@ def impact_run_then_remove(
losses_by_site = numpy.load(BytesIO(content))
pandas.DataFrame.from_dict(
{item: losses_by_site[item] for item in losses_by_site})
ret = self.get('%s/extract/losses_by_location' % job_id)
content = self.get_response_content(ret)
losses_by_location = numpy.load(BytesIO(content))
pandas.DataFrame.from_dict(
{item: losses_by_location[item] for item in losses_by_location})

# check that users can download hidden outputs only if their level is at
# least 2 or if they have the can_view_exposure permission
Expand All @@ -274,6 +279,9 @@ def impact_run_then_remove(
results = json.loads(ret.content.decode('utf8'))
exposure_urls = [res['url'] for res in results if res['type'] == 'exposure']
self.assertEqual(len(exposure_urls), 0)
# ...and without can_view_exposure they can't extract the assetcol
ret = self.c.get(f'/v1/calc/{job_id}/extract/assetcol')
self.assertEqual(ret.status_code, 403)

# level 1 users with the can_view_exposure permission can see the exposure
self.user1.groups.add(self.users_who_can_view_exposure)
Expand All @@ -283,6 +291,9 @@ def impact_run_then_remove(
download_exposure_url = download_url
ret = self.c.get(download_url)
self.assertEqual(ret.status_code, 200)
# ...and with can_view_exposure they can extract the assetcol
ret = self.c.get(f'/v1/calc/{job_id}/extract/assetcol')
self.assertEqual(ret.status_code, 200)

# level 2 users without the show_exposure group can see the exposure
self.user1.groups.remove(self.users_who_can_view_exposure)
Expand All @@ -298,6 +309,9 @@ def impact_run_then_remove(
[exposure_url] = [res['url'] for res in results if res['type'] == 'exposure']
ret = self.c.get(exposure_url)
self.assertEqual(ret.status_code, 200)
# ...and even without can_view_exposure they can extract the assetcol
ret = self.c.get(f'/v1/calc/{job_id}/extract/assetcol')
self.assertEqual(ret.status_code, 200)

# level 0 users without the can_view_exposure permission can't see the exposure
self.user1.profile.level = 0
Expand All @@ -311,6 +325,9 @@ def impact_run_then_remove(
results = json.loads(ret.content.decode('utf8'))
exposure_urls = [res['url'] for res in results if res['type'] == 'exposure']
self.assertEqual(len(exposure_urls), 0)
# ...and without can_view_exposure they can't extract the assetcol
ret = self.c.get(f'/v1/calc/{job_id}/extract/assetcol')
self.assertEqual(ret.status_code, 403)

# level 1 users without the can_view_exposure permission can't see the exposure
self.user1.profile.level = 1
Expand All @@ -324,6 +341,9 @@ def impact_run_then_remove(
results = json.loads(ret.content.decode('utf8'))
exposure_urls = [res['url'] for res in results if res['type'] == 'exposure']
self.assertEqual(len(exposure_urls), 0)
# ...and without can_view_exposure they can't extract the assetcol
ret = self.c.get(f'/v1/calc/{job_id}/extract/assetcol')
self.assertEqual(ret.status_code, 403)

ret = self.post('%s/remove' % job_id)
if ret.status_code != 200:
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from django.db import migrations


def create_exposure_permission(apps, schema_editor):
def create_can_view_exposure_permission(apps, schema_editor):
Permission = apps.get_model('auth', 'Permission')
ContentType = apps.get_model('contenttypes', 'ContentType')

Expand All @@ -16,7 +16,7 @@ def create_exposure_permission(apps, schema_editor):
)


def delete_exposure_permission(apps, schema_editor):
def delete_can_view_exposure_permission(apps, schema_editor):
Permission = apps.get_model('auth', 'Permission')
Permission.objects.filter(codename='can_view_exposure').delete()

Expand All @@ -27,5 +27,6 @@ class Migration(migrations.Migration):
]

operations = [
migrations.RunPython(create_exposure_permission, delete_exposure_permission),
migrations.RunPython(create_can_view_exposure_permission,
delete_can_view_exposure_permission),
]
27 changes: 27 additions & 0 deletions openquake/server/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,12 @@
}

HIDDEN_OUTPUTS = ['exposure', 'job']
EXTRACTABLE_RESOURCES = ['aggrisk_tags', 'mmi_tags', 'losses_by_site',
'losses_by_asset', 'losses_by_location']
# NOTE: the 'exposure' output internally corresponds to the 'assetcol' in the
# datastore, and the can_view_exposure permission gives access both to the
# 'exposure' output and to the 'assetcol' item in the datastore
MAP_RESOURCE_OUTPUT = {'assetcol': 'exposure'}

# disable check on the export_dir, since the WebUI exports in a tmpdir
oqvalidation.OqParam.is_valid_export_dir = lambda self: True
Expand Down Expand Up @@ -1532,6 +1538,8 @@ def extract(request, calc_id, what):
return HttpResponseNotFound()
if not utils.user_has_permission(request, job.user_name, job.status):
return HttpResponseForbidden()
if not can_extract(request, what):
return HttpResponseForbidden()
path = request.get_full_path()
n = len(request.path_info)
query_string = unquote_plus(path[n:])
Expand Down Expand Up @@ -2010,6 +2018,23 @@ def download_aggrisk(request, calc_id):
return response


def can_extract(request, resource):
try:
user = request.user
except AttributeError:
# without authentication
return True
if (resource in EXTRACTABLE_RESOURCES
or user.level >= 2
or user.has_perm(f'auth.can_view_{resource}')):
return True
if resource in MAP_RESOURCE_OUTPUT:
corresponding_output = MAP_RESOURCE_OUTPUT[resource]
if user.has_perm(f'auth.can_view_{corresponding_output}'):
return True
return False


@cross_domain_ajax
@require_http_methods(['GET'])
def extract_html_table(request, calc_id, name):
Expand All @@ -2018,6 +2043,8 @@ def extract_html_table(request, calc_id, name):
return HttpResponseNotFound()
if not utils.user_has_permission(request, job.user_name, job.status):
return HttpResponseForbidden()
if not can_extract(request, name):
return HttpResponseForbidden()
try:
with datastore.read(job.ds_calc_dir + '.hdf5') as ds:
table = _extract(ds, name)
Expand Down