Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 1 addition & 1 deletion openquake/calculators/country_impact_report_builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -569,7 +569,7 @@ def _build_left_bundle(self, summary_table, body_left_style, title_style):

exposed_value_txt = (
f'The exposed value refers to the assets and population located'
f' within a {self.maximum_distance}km radius of the epicentre.')
f' within a {self.maximum_distance} km radius of the epicentre.')
left_bundle.append(
self.Paragraph(exposed_value_txt, body_left_style))
if self.no_uncertainty:
Expand Down
Empty file.
5 changes: 5 additions & 0 deletions openquake/server/authorization/apps.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
from django.apps import AppConfig


class AuthorizationConfig(AppConfig):
name = 'openquake.server.authorization'
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
from django.db import migrations


def create_permission(apps, schema_editor):
Permission = apps.get_model('auth', 'Permission')
ContentType = apps.get_model('contenttypes', 'ContentType')
content_type = ContentType.objects.get_for_model(Permission)
Permission.objects.get_or_create(
codename='can_share_own_jobs',
name='Can share own jobs',
content_type=content_type,
)


def delete_permission(apps, schema_editor):
Permission = apps.get_model('auth', 'Permission')
ContentType = apps.get_model('contenttypes', 'ContentType')
content_type = ContentType.objects.get_for_model(Permission)
Permission.objects.filter(
codename='can_share_own_jobs', content_type=content_type).delete()


class Migration(migrations.Migration):
initial = True

dependencies = [
('auth', '0012_alter_user_first_name_max_length'),
('contenttypes', '0002_remove_content_type_name'),
]

operations = [
migrations.RunPython(create_permission, delete_permission),
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
from django.db import migrations


def create_permission(apps, schema_editor):
Permission = apps.get_model('auth', 'Permission')
ContentType = apps.get_model('contenttypes', 'ContentType')
content_type = ContentType.objects.get_for_model(Permission)
Permission.objects.get_or_create(
codename='can_view_exposure',
name='Can view exposure',
content_type=content_type,
)


class Migration(migrations.Migration):
dependencies = [
('authorization', '0001_create_can_share_own_jobs_permission'),
]

# The legacy UserProfile migration also creates this permission; don't
# remove it when rolling back this migration.
operations = [
migrations.RunPython(create_permission, migrations.RunPython.noop),
]
Empty file.
1 change: 1 addition & 0 deletions openquake/server/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -339,6 +339,7 @@ def filter(self, record):
TEMPLATES[0]['OPTIONS']['context_processors'] = CONTEXT_PROCESSORS
# add installed_apps for cookie-consent
for app in ('django.contrib.auth', 'django.contrib.contenttypes',
'openquake.server.authorization',
'openquake.server.user_profile', 'cookie_consent',):
if app not in INSTALLED_APPS:
INSTALLED_APPS += (app,)
Expand Down
12 changes: 7 additions & 5 deletions openquake/server/templates/engine/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -93,11 +93,13 @@ <h2>List of calculations{% if application_mode != 'AELO' %}{% if server_name %}
{% endif %}
<% if (calc.get('status') == 'complete' || calc.get('status') == 'shared') { %>

{% if application_mode != 'READ_ONLY' and user_level >= 2 and lockdown %}
<% if (calc.get('status') == 'complete') { %>
<a href="#" data-calc-id="<%= calc.get('id') %>" data-calc-desc="<%= calc.get('description') %>" class="btn btn-sm btn-info calc-list-btn btn-share" style="margin: 2px 0 2px 0;">Share</a>
<% } else if (calc.get('status') == 'shared') { %>
<a href="#" data-calc-id="<%= calc.get('id') %>" data-calc-desc="<%= calc.get('description') %>" class="btn btn-sm btn-info calc-list-btn btn-unshare" style="margin: 2px 0 2px 0;">Unshare</a>
{% if application_mode != 'READ_ONLY' and lockdown %}
<% if (calc.get('can_manage_sharing')) { %>
<% if (calc.get('status') == 'complete') { %>
<a href="#" data-calc-id="<%= calc.get('id') %>" data-calc-desc="<%= calc.get('description') %>" class="btn btn-sm btn-info calc-list-btn btn-share" style="margin: 2px 0 2px 0;">Share</a>
<% } else if (calc.get('status') == 'shared') { %>
<a href="#" data-calc-id="<%= calc.get('id') %>" data-calc-desc="<%= calc.get('description') %>" class="btn btn-sm btn-info calc-list-btn btn-unshare" style="margin: 2px 0 2px 0;">Unshare</a>
<% } %>
<% } %>
{% endif %}

Expand Down
44 changes: 44 additions & 0 deletions openquake/server/tests/test_restricted_mode.py
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,50 @@ def test_level_2_can_only_share_own_jobs(self):
{'status': 'complete'}, job.calc_id)
self.remove_calc(job.calc_id)

def test_group_permission_allows_low_level_users_to_share_own_jobs(self):
from django.contrib.auth.models import Group, Permission
from django.contrib.contenttypes.models import ContentType

content_type = ContentType.objects.get(
app_label='auth', model='permission')
permission, _ = Permission.objects.get_or_create(
codename='can_share_own_jobs', name='Can share own jobs',
content_type=content_type)
group, _ = Group.objects.get_or_create(
name='test-can-share-own-jobs')
group.permissions.add(permission)
self.user1.groups.add(group)

own_dic = dict(calculation_mode='event_based',
description='test_group_share_own_job')
other_dic = dict(calculation_mode='event_based',
description='test_group_share_other_job')
[own_job] = create_jobs([own_dic], user_name=self.user1.username)
[other_job] = create_jobs([other_dic], user_name=self.user2.username)
db("UPDATE job SET ?D WHERE id=?x",
{'status': 'complete', 'is_running': 0}, own_job.calc_id)
db("UPDATE job SET ?D WHERE id=?x",
{'status': 'shared', 'is_running': 0}, other_job.calc_id)

self.c.login(username=self.user1.username, password=self.password1)
ret = self.post(f'{own_job.calc_id}/share')
self.assertEqual(ret.status_code, 200)
ret = self.post(f'{own_job.calc_id}/unshare')
self.assertEqual(ret.status_code, 200)

ret = self.post(f'{other_job.calc_id}/share')
self.assertEqual(ret.status_code, 403)
self.assertEqual(db('SELECT status FROM job WHERE id=?x',
other_job.calc_id)[0].status, 'shared')
ret = self.get('list', include_shared=1)
listed = {calc['id']: calc for calc in ret.json()}
self.assertTrue(listed[own_job.calc_id]['can_manage_sharing'])
self.assertFalse(listed[other_job.calc_id]['can_manage_sharing'])

self.remove_calc(own_job.calc_id)
self.c.login(username=self.user2.username, password=self.password2)
self.remove_calc(other_job.calc_id)

def test_superuser_can_share_other_users_jobs(self):
job_dic = dict(calculation_mode='event_based',
description='test_superuser_share_job')
Expand Down
15 changes: 12 additions & 3 deletions openquake/server/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -553,16 +553,23 @@ def calc_list(request, id=None):
utils.get_valid_users(request),
not utils.is_superuser(request), id)
response_data = []
username = psutil.Process(os.getpid()).username()
username = utils.get_username(request)
superuser = utils.is_superuser(request)
can_share_own_jobs = (
get_user_level(request) >= 2 or
request.user.has_perm('auth.can_share_own_jobs'))
process_username = psutil.Process(os.getpid()).username()
for (hc_id, owner, status, calculation_mode, is_running, desc, pid,
parent_id, size_mb, host, start_time, relevant, tags) in calc_data:
can_manage_sharing = superuser or (
owner == username and can_share_own_jobs)
if settings.DISPLAY_USER_HOST and host:
owner += '@' + host.split('.')[0]
url = urljoin(base_url, 'v1/calc/%d' % hc_id)
abortable = False
if is_running:
try:
if psutil.Process(pid).username() == username:
if psutil.Process(pid).username() == process_username:
abortable = True
except psutil.NoSuchProcess:
pass
Expand All @@ -573,6 +580,7 @@ def calc_list(request, id=None):
dict(id=hc_id, owner=owner,
calculation_mode=calculation_mode, status=status,
is_running=bool(is_running), description=desc, url=url,
can_manage_sharing=can_manage_sharing,
parent_id=parent_id, abortable=abortable, size_mb=size_mb,
start_time=start_time_str, relevant=relevant, tags=tags))

Expand Down Expand Up @@ -627,7 +635,8 @@ def calc_remove(request, calc_id):
def share_job(request, calc_id, share):
user_level = get_user_level(request)
superuser = utils.is_superuser(request)
if user_level < 2 and not superuser:
if (user_level < 2 and not superuser and
not request.user.has_perm('auth.can_share_own_jobs')):
return HttpResponseForbidden()
try:
job = logs.dbcmd('get_job', calc_id)
Expand Down