Compare commits

..

10 Commits

Author SHA1 Message Date
aaaa9fc742 Merge branch 'master' into assets_embed 2020-01-17 15:12:13 +00:00
Matthew Smith
e0c6a56263 Disable password reset as temporary fix to vulnerability (#396)
Disabled password reset and left message notifying user of problem. In response to CVE-2019-19844
2020-01-17 13:13:16 +00:00
6aba0478f0 Merge branch 'master' into assets_embed 2020-01-11 21:24:09 +00:00
4ad12ab40a FIX: Prevent basic users seeing individual asset version history
I prevented them from seeing the change stream, didn't prevent them seeing individual histories. This has to be done as otherwise it leaks financial information. If I can be arsed I'll come back to this and allow basic users to see a filtered version.
2020-01-11 21:13:50 +00:00
13205770f1 FIX: Correct template for AssetVersionHistory 2020-01-11 21:13:50 +00:00
dfb612367b Fix embeds not actually working for unauthenticated users
This is why I should have written tests...
2020-01-11 20:54:01 +00:00
4fb0a0ffe7 FIX Copy paste error ;D 2020-01-08 20:53:27 +00:00
7361605ffa FEAT: Add oembed for assets
Don't see the worth in doing supplier currently...we don't OEmbed Org/Venue etc after all...
2020-01-08 20:36:29 +00:00
6bb0c88c72 FIX: Migrations 2020-01-03 22:21:50 +00:00
82a30ca77d Miscellaneous changes to the Asset DB (#390)
* FIX #388: Prevent assets losing supplier data on edit

* FEAT: Add associated assets to supplier detail view

* FIX: Tweak supplier list to make detail view accessible

* Potential fix for #380

No idea if it works because I can't reproduce locally. S/O Reckons it should... :P

* FEAT #386: Asset search searches serial number.

Pending addition of advanced search.

* FIX: Order asset categories/statuses alphabetically

Instead of by pk because that's silly.

* FEAT: Statuses can have a CSS class defined in the admin panel

This replaces the hardcoding of colours in the asset list.

* FIX: Squash migrations

* Fixed supplier not working on all the create asset template

* Refactored away "assets" property on "Supplier" by using "related_name" instead

Co-authored-by: Matthew Smith <mattysmith22@googlemail.com>
2020-01-03 21:46:39 +00:00
15 changed files with 143 additions and 97 deletions

View File

@@ -6,6 +6,34 @@ from django.urls import reverse
from RIGS import models from RIGS import models
def get_oembed(login_url, request, oembed_view, kwargs):
context = {}
context['oembed_url'] = "{0}://{1}{2}".format(request.scheme, request.META['HTTP_HOST'], reverse(oembed_view, kwargs=kwargs))
context['login_url'] = "{0}?{1}={2}".format(login_url, REDIRECT_FIELD_NAME, request.get_full_path())
resp = render(request, 'login_redirect.html', context=context)
return resp
def has_oembed(oembed_view, login_url=None):
if not login_url:
from django.conf import settings
login_url = settings.LOGIN_URL
def _dec(view_func):
def _checklogin(request, *args, **kwargs):
if request.user.is_authenticated:
return view_func(request, *args, **kwargs)
else:
if oembed_view is not None:
return get_oembed(login_url, request, oembed_view, kwargs)
else:
return HttpResponseRedirect('%s?%s=%s' % (login_url, REDIRECT_FIELD_NAME, request.get_full_path()))
_checklogin.__doc__ = view_func.__doc__
_checklogin.__dict__ = view_func.__dict__
return _checklogin
return _dec
def user_passes_test_with_403(test_func, login_url=None, oembed_view=None): def user_passes_test_with_403(test_func, login_url=None, oembed_view=None):
""" """
Decorator for views that checks that the user passes the given test. Decorator for views that checks that the user passes the given test.
@@ -25,11 +53,7 @@ def user_passes_test_with_403(test_func, login_url=None, oembed_view=None):
return view_func(request, *args, **kwargs) return view_func(request, *args, **kwargs)
elif not request.user.is_authenticated: elif not request.user.is_authenticated:
if oembed_view is not None: if oembed_view is not None:
context = {} return get_oembed(login_url, request, oembed_view, kwargs)
context['oembed_url'] = "{0}://{1}{2}".format(request.scheme, request.META['HTTP_HOST'], reverse(oembed_view, kwargs=kwargs))
context['login_url'] = "{0}?{1}={2}".format(login_url, REDIRECT_FIELD_NAME, request.get_full_path())
resp = render(request, 'login_redirect.html', context=context)
return resp
else: else:
return HttpResponseRedirect('%s?%s=%s' % (login_url, REDIRECT_FIELD_NAME, request.get_full_path())) return HttpResponseRedirect('%s?%s=%s' % (login_url, REDIRECT_FIELD_NAME, request.get_full_path()))
else: else:

View File

@@ -0,0 +1,9 @@
{% extends 'base_rigs.html' %}
{% block title %}Password Reset Disabled{% endblock %}
{% block content %}
<h1>Password reset is disabled</h1>
<p> We are very sorry for the inconvenience, but due to a security vulnerability, password reset is currently disabled until the vulnerability can be patched.</p>
<p> If you are locked out of your account, please contact an administrator and we can manually perform a reset</p>
{% endblock %}

View File

@@ -19,7 +19,7 @@ urlpatterns = [
url('^user/login/$', views.login, name='login'), url('^user/login/$', views.login, name='login'),
url('^user/login/embed/$', xframe_options_exempt(views.login_embed), name='login_embed'), url('^user/login/embed/$', xframe_options_exempt(views.login_embed), name='login_embed'),
url(r'^user/password_reset/$', password_reset, {'password_reset_form': forms.PasswordReset}), url(r'^user/password_reset/$', views.PasswordResetDisabled.as_view()),
# People # People
url(r'^people/$', permission_required_with_403('RIGS.view_person')(views.PersonList.as_view()), url(r'^people/$', permission_required_with_403('RIGS.view_person')(views.PersonList.as_view()),

View File

@@ -392,3 +392,7 @@ class ResetApiKey(generic.RedirectView):
self.request.user.save() self.request.user.save()
return reverse_lazy('profile_detail') return reverse_lazy('profile_detail')
class PasswordResetDisabled(generic.TemplateView):
template_name = "RIGS/password_reset_disable.html"

View File

@@ -1,21 +0,0 @@
# Generated by Django 2.0.13 on 2020-01-02 19:33
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('assets', '0008_auto_20191206_2124'),
]
operations = [
migrations.AlterModelOptions(
name='assetcategory',
options={'ordering': ['name'], 'verbose_name': 'Asset Category', 'verbose_name_plural': 'Asset Categories'},
),
migrations.AlterModelOptions(
name='assetstatus',
options={'ordering': ['name'], 'verbose_name': 'Asset Status', 'verbose_name_plural': 'Asset Statuses'},
),
]

View File

@@ -1,12 +1,11 @@
# Generated by Django 2.0.13 on 2020-01-02 20:42 # Generated by Django 2.0.13 on 2020-01-03 22:15
from django.db import migrations, models from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration): class Migration(migrations.Migration):
replaces = [('assets', '0009_auto_20200102_1933'), ('assets', '0010_assetstatus_display_class'), ('assets', '0011_auto_20200102_2040')]
dependencies = [ dependencies = [
('assets', '0008_auto_20191206_2124'), ('assets', '0008_auto_20191206_2124'),
] ]
@@ -23,6 +22,11 @@ class Migration(migrations.Migration):
migrations.AddField( migrations.AddField(
model_name='assetstatus', model_name='assetstatus',
name='display_class', name='display_class',
field=models.CharField(help_text='HTML class to be appended to alter display of assets with this status, such as in the list.', max_length=80), field=models.CharField(blank=True, help_text='HTML class to be appended to alter display of assets with this status, such as in the list.', max_length=80, null=True),
),
migrations.AlterField(
model_name='asset',
name='purchased_from',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='assets', to='assets.Supplier'),
), ),
] ]

View File

@@ -1,18 +0,0 @@
# Generated by Django 2.0.13 on 2020-01-02 19:35
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('assets', '0009_auto_20200102_1933'),
]
operations = [
migrations.AddField(
model_name='assetstatus',
name='display_class',
field=models.TextField(default='', help_text='HTML class to be appended to alter display of assets with this status, such as in the list.'),
),
]

View File

@@ -1,19 +0,0 @@
# Generated by Django 2.0.13 on 2020-01-03 21:34
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('assets', '0009_auto_20200102_1933_squashed_0011_auto_20200102_2040'),
]
operations = [
migrations.AlterField(
model_name='asset',
name='purchased_from',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='assets', to='assets.Supplier'),
),
]

View File

@@ -1,18 +0,0 @@
# Generated by Django 2.0.13 on 2020-01-02 20:40
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('assets', '0010_assetstatus_display_class'),
]
operations = [
migrations.AlterField(
model_name='assetstatus',
name='display_class',
field=models.CharField(help_text='HTML class to be appended to alter display of assets with this status, such as in the list.', max_length=80),
),
]

View File

@@ -33,7 +33,7 @@ class AssetStatus(models.Model):
name = models.CharField(max_length=80) name = models.CharField(max_length=80)
should_show = models.BooleanField( should_show = models.BooleanField(
default=True, help_text="Should this be shown by default in the asset list.") default=True, help_text="Should this be shown by default in the asset list.")
display_class = models.CharField(max_length=80, help_text="HTML class to be appended to alter display of assets with this status, such as in the list.") display_class = models.CharField(max_length=80, blank=True, null=True, help_text="HTML class to be appended to alter display of assets with this status, such as in the list.")
def __str__(self): def __str__(self):
return self.name return self.name

View File

@@ -0,0 +1,45 @@
{% extends 'base_embed.html' %}
{% load static from staticfiles %}
{% block content %}
<div class="row">
<div class="col-sm-12">
<a href="/assets">
<span class="source"> TEC Asset Database</span>
</a>
</div>
<div class="col-sm-12">
<h3>
<a href="{% url 'asset_detail' object.asset_id %}">Asset: {{ object.asset_id }} | {{ object.description }} </a>
<small class="label label-default">
<strong>Category:</strong>
{{ object.category }}
</small>
&nbsp;
<small class="label label-{{ object.status.display_class|default:'default' }}">
<strong>Status:</strong>
{{ object.status }}
</small>
</h3>
<br>
{% if object.serial_number %}
<p>
<strong>Serial Number: </strong>
{{ object.serial_number }}
</p>
{% endif %}
{% if object.comments %}
<p>
<strong>Comments: </strong>
{{ object.comments|linebreaksbr }}
</p>
{% endif %}
</table>
</div>
</div>
{% endblock %}

View File

@@ -44,7 +44,7 @@
</div> </div>
</form> </form>
{% if not edit %} {% if not edit and perms.assets.view_asset %}
<div class="col-sm-12 text-right"> <div class="col-sm-12 text-right">
<div> <div>
<a href="{% url 'asset_history' object.asset_id %}" title="View Revision History"> <a href="{% url 'asset_history' object.asset_id %}" title="View Revision History">

View File

@@ -3,24 +3,34 @@ from django.urls import path
from assets import views, models from assets import views, models
from RIGS import versioning from RIGS import versioning
from PyRIGS.decorators import permission_required_with_403 from django.contrib.auth.decorators import login_required
from django.views.decorators.clickjacking import xframe_options_exempt
from PyRIGS.decorators import has_oembed, permission_required_with_403
urlpatterns = [ urlpatterns = [
path('', views.AssetList.as_view(), name='asset_index'), path('', views.AssetList.as_view(), name='asset_index'),
path('asset/list/', views.AssetList.as_view(), name='asset_list'), path('asset/list/', views.AssetList.as_view(), name='asset_list'),
path('asset/id/<str:pk>/', views.AssetDetail.as_view(), name='asset_detail'), # Lazy way to enable the oembed redirect...
path('asset/id/<str:pk>/', has_oembed(oembed_view="asset_oembed")(views.AssetDetail.as_view()), name='asset_detail'),
path('asset/create/', permission_required_with_403('assets.add_asset') path('asset/create/', permission_required_with_403('assets.add_asset')
(views.AssetCreate.as_view()), name='asset_create'), (views.AssetCreate.as_view()), name='asset_create'),
path('asset/id/<str:pk>/edit/', permission_required_with_403('assets.change_asset') path('asset/id/<str:pk>/edit/', permission_required_with_403('assets.change_asset')
(views.AssetEdit.as_view()), name='asset_update'), (views.AssetEdit.as_view()), name='asset_update'),
path('asset/id/<str:pk>/duplicate/', permission_required_with_403('assets.add_asset') path('asset/id/<str:pk>/duplicate/', permission_required_with_403('assets.add_asset')
(views.AssetDuplicate.as_view()), name='asset_duplicate'), (views.AssetDuplicate.as_view()), name='asset_duplicate'),
path('asset/id/<str:pk>/history/', views.AssetVersionHistory.as_view(), path('asset/id/<str:pk>/history/', permission_required_with_403('assets.view_asset')(views.AssetVersionHistory.as_view()),
name='asset_history', kwargs={'model': models.Asset}), name='asset_history', kwargs={'model': models.Asset}),
path('activity', permission_required_with_403('assets.view_asset') path('activity', permission_required_with_403('assets.view_asset')
(views.ActivityTable.as_view()), name='asset_activity_table'), (views.ActivityTable.as_view()), name='asset_activity_table'),
path('asset/search/', views.AssetSearch.as_view(), name='asset_search_json'), path('asset/search/', views.AssetSearch.as_view(), name='asset_search_json'),
path('asset/id/<str:pk>/embed/',
xframe_options_exempt(
login_required(login_url='/user/login/embed/')(views.AssetEmbed.as_view())),
name='asset_embed'),
path('asset/id/<str:pk>/oembed_json/',
views.AssetOembed.as_view(),
name='asset_oembed'),
path('supplier/list', views.SupplierList.as_view(), name='supplier_list'), path('supplier/list', views.SupplierList.as_view(), name='supplier_list'),
path('supplier/<int:pk>', views.SupplierDetail.as_view(), name='supplier_detail'), path('supplier/<int:pk>', views.SupplierDetail.as_view(), name='supplier_detail'),

View File

@@ -1,5 +1,6 @@
from django.contrib.auth.mixins import LoginRequiredMixin from django.contrib.auth.mixins import LoginRequiredMixin
from django.http import JsonResponse from django.http import JsonResponse
from django.http import HttpResponse
from django.views import generic from django.views import generic
from django.views.decorators.csrf import csrf_exempt from django.views.decorators.csrf import csrf_exempt
from django.utils.decorators import method_decorator from django.utils.decorators import method_decorator
@@ -9,6 +10,8 @@ from django.shortcuts import get_object_or_404
from assets import models, forms from assets import models, forms
from RIGS import versioning from RIGS import versioning
import simplejson
@method_decorator(csrf_exempt, name='dispatch') @method_decorator(csrf_exempt, name='dispatch')
class AssetList(LoginRequiredMixin, generic.ListView): class AssetList(LoginRequiredMixin, generic.ListView):
@@ -149,6 +152,28 @@ class AssetDuplicate(DuplicateMixin, AssetIDUrlMixin, AssetCreate):
return context return context
class AssetOembed(generic.View):
model = models.Asset
def get(self, request, pk=None):
embed_url = reverse('asset_embed', args=[pk])
full_url = "{0}://{1}{2}".format(request.scheme, request.META['HTTP_HOST'], embed_url)
data = {
'html': '<iframe src="{0}" frameborder="0" width="100%" height="250"></iframe>'.format(full_url),
'version': '1.0',
'type': 'rich',
'height': '250'
}
json = simplejson.JSONEncoderForHTML().encode(data)
return HttpResponse(json, content_type="application/json")
class AssetEmbed(AssetDetail):
template_name = 'asset_embed.html'
class SupplierList(generic.ListView): class SupplierList(generic.ListView):
model = models.Supplier model = models.Supplier
template_name = 'supplier_list.html' template_name = 'supplier_list.html'
@@ -213,8 +238,9 @@ class SupplierVersionHistory(versioning.VersionHistory):
template_name = "asset_version_history.html" template_name = "asset_version_history.html"
# TODO: Reduce SQL queries
class AssetVersionHistory(versioning.VersionHistory): class AssetVersionHistory(versioning.VersionHistory):
template_name = "asset_version_history.html"
def get_object(self, **kwargs): def get_object(self, **kwargs):
return get_object_or_404(models.Asset, asset_id=self.kwargs['pk']) return get_object_or_404(models.Asset, asset_id=self.kwargs['pk'])

View File

@@ -10,7 +10,7 @@
{% endblock %} {% endblock %}
{% block titleelements %} {% block titleelements %}
{% if perms.assets.view_asset %} {# % if perms.assets.view_asset % #}
<li class="dropdown"> <li class="dropdown">
<a href="#" class="dropdown-toggle" data-toggle="dropdown">Assets<b class="caret"></b></a> <a href="#" class="dropdown-toggle" data-toggle="dropdown">Assets<b class="caret"></b></a>
<ul class="dropdown-menu"> <ul class="dropdown-menu">
@@ -20,19 +20,19 @@
{% endif %} {% endif %}
</ul> </ul>
</li> </li>
{% endif %} {# % endif % #}
{% if perms.assets.view_supplier %} {# % if perms.assets.view_supplier % #}
<li class="dropdown"> <li class="dropdown">
<a href="#" class="dropdown-toggle" data-toggle="dropdown"> Suppliers<b class="caret"></b></a> <a href="#" class="dropdown-toggle" data-toggle="dropdown"> Suppliers<b class="caret"></b></a>
<ul class="dropdown-menu"> <ul class="dropdown-menu">
<li><a href="{% url 'supplier_list' %}"><span class="glyphicon glyphicon-list"></span> <li><a href="{% url 'supplier_list' %}"><span class="glyphicon glyphicon-list"></span>
List Suppliers</a></li> List Suppliers</a></li>
{% if perms.assets.add_asset %} {% if perms.assets.add_supplier %}
<li><a href="{% url 'supplier_create' %}"><span class="glyphicon glyphicon-plus"></span> Create Supplier</a></li> <li><a href="{% url 'supplier_create' %}"><span class="glyphicon glyphicon-plus"></span> Create Supplier</a></li>
{% endif %} {% endif %}
</ul> </ul>
</li> </li>
{% endif %} {# % endif % #}
{% if perms.assets.view_asset %} {% if perms.assets.view_asset %}
<li><a href="{% url 'asset_activity_table' %}">Recent Changes</a></li> <li><a href="{% url 'asset_activity_table' %}">Recent Changes</a></li>
{% endif %} {% endif %}