From 93993955f3f83fddf308b3e1c0e587fe41e91b24 Mon Sep 17 00:00:00 2001 From: Danilo Bargen Date: Tue, 27 May 2014 17:35:36 +0200 Subject: [PATCH 01/51] Renamed SWID measurement endpoint - Moved `/api/sessions//swid_measurement` to `/api/sessions//swid-measurement` - Moved endpoint from core app to swid app - Added optional format specifier to "add tags" endpoint --- apps/api/urls.py | 15 +++++++++---- apps/core/api_views.py | 45 ++----------------------------------- apps/swid/api_views.py | 51 +++++++++++++++++++++++++++++++++++++----- tests/test_api.py | 2 +- 4 files changed, 59 insertions(+), 54 deletions(-) diff --git a/apps/api/urls.py b/apps/api/urls.py index 232bf760..a91243fe 100644 --- a/apps/api/urls.py +++ b/apps/api/urls.py @@ -6,7 +6,7 @@ from rest_framework import routers from apps.core.api_views import IdentityViewSet, SessionViewSet -from apps.swid.api_views import EntityViewSet, TagViewSet, TagAddView +from apps.swid.api_views import EntityViewSet, TagViewSet, TagAddView, SwidMeasurementView # Create router @@ -18,11 +18,18 @@ router.register(r'swid-entities', EntityViewSet) router.register(r'swid-tags', TagViewSet) -# Generate URL configuration +# Generate basic URL configuration urlpatterns = router.urls - -# API URLs +# Register additional endpoints urlpatterns += patterns('', + # Add tags url(r'^swid/add-tags/', TagAddView.as_view(), name='swid-add-tags'), + url(r'^swid/add-tags/\.(?P[a-z0-9]+)', TagAddView.as_view(), name='swid-add-tags'), + + # Register measurement + url(r'^sessions/(?P[^/]+)/swid-measurement/', + SwidMeasurementView.as_view(), name='session-swid-measurement'), + url(r'^sessions/(?P[^/]+)/swid-measurement/\.(?P[a-z0-9]+)', + SwidMeasurementView.as_view(), name='session-swid-measurement'), ) diff --git a/apps/core/api_views.py b/apps/core/api_views.py index cd95f1d9..c8b79a0e 100644 --- a/apps/core/api_views.py +++ b/apps/core/api_views.py @@ -1,15 +1,9 @@ # -*- coding: utf-8 -*- from __future__ import print_function, division, absolute_import, unicode_literals -from rest_framework import viewsets, status -from rest_framework.decorators import action -from rest_framework.response import Response +from rest_framework import viewsets -from apps.core import models as core_models -from apps.swid import models as swid_models -from apps.swid.utils import chunked_bulk_create -from . import models -from . import serializers +from . import models, serializers class IdentityViewSet(viewsets.ReadOnlyModelViewSet): @@ -20,38 +14,3 @@ class IdentityViewSet(viewsets.ReadOnlyModelViewSet): class SessionViewSet(viewsets.ReadOnlyModelViewSet): model = models.Session serializer_class = serializers.SessionSerializer - - @action() - def swid_measurement(self, request, pk=None): - """" - Link the given software-ids with the current session. - If no corresponding tag is available for one or more software-ids, return these software-ids - with HTTP status code 412 Precondition failed. - - TODO: move this controller to separate file - - """ - software_ids = request.DATA - found_tags = [] - missing_tags = [] - - # Look for matching tags - for software_id in software_ids: - try: - tag = swid_models.Tag.objects.get(software_id=software_id) - found_tags.append(tag) - except swid_models.Tag.DoesNotExist: - missing_tags.append(software_id) - - if missing_tags: - # Some tags are missing - return Response(data=missing_tags, status=status.HTTP_412_PRECONDITION_FAILED) - else: - # All tags are available: link them with a session - try: - session = core_models.Session.objects.get(pk=pk) - except core_models.Session.DoesNotExist: - data = {'status': 'error', 'message': 'Session with id "%s" not found.' % pk} - return Response(data=data, status=status.HTTP_404_NOT_FOUND) - chunked_bulk_create(session.tag_set, found_tags, 980) - return Response(data=[], status=status.HTTP_200_OK) diff --git a/apps/swid/api_views.py b/apps/swid/api_views.py index 621914c8..62b7d906 100644 --- a/apps/swid/api_views.py +++ b/apps/swid/api_views.py @@ -6,18 +6,19 @@ from rest_framework.parsers import JSONParser from lxml.etree import XMLSyntaxError -from apps.swid import utils -from . import models -from . import serializers +from . import utils, serializers + +from .models import Entity, Tag +from apps.core.models import Session class EntityViewSet(viewsets.ReadOnlyModelViewSet): - model = models.Entity + model = Entity serializer_class = serializers.EntitySerializer class TagViewSet(viewsets.ReadOnlyModelViewSet): - model = models.Tag + model = Tag serializer_class = serializers.TagSerializer filter_fields = ('package_name', 'version', 'unique_id') @@ -33,7 +34,7 @@ class TagAddView(views.APIView): """ parser_classes = (JSONParser,) # Only JSON data is supported - def post(self, request): + def post(self, request, format=None): # Validate request data tags = request.DATA if not isinstance(tags, list): @@ -66,3 +67,41 @@ def post(self, request): 'message': 'Added {0[added]} SWID tags, replaced {0[replaced]} SWID tags.'.format(stats), } return Response(data, status=status.HTTP_200_OK) + + +class SwidMeasurementView(views.APIView): + """ + Link the given software-ids with the current session. + + If no corresponding tag is available for one or more software-ids, return + these software-ids with HTTP status code 412 Precondition failed. + + This view is defined on a session detail page. The ``pk`` argument is the + session ID. + + """ + def post(self, request, pk, format=None): + software_ids = request.DATA + found_tags = [] + missing_tags = [] + + # Look for matching tags + for software_id in software_ids: + try: + tag = Tag.objects.get(software_id=software_id) + found_tags.append(tag) + except Tag.DoesNotExist: + missing_tags.append(software_id) + + if missing_tags: + # Some tags are missing + return Response(data=missing_tags, status=status.HTTP_412_PRECONDITION_FAILED) + else: + # All tags are available: link them with a session + try: + session = Session.objects.get(pk=pk) + except Session.DoesNotExist: + data = {'status': 'error', 'message': 'Session with id "%s" not found.' % pk} + return Response(data=data, status=status.HTTP_404_NOT_FOUND) + utils.chunked_bulk_create(session.tag_set, found_tags, 980) + return Response(data=[], status=status.HTTP_200_OK) diff --git a/tests/test_api.py b/tests/test_api.py index f696fe0d..ff1cf0ea 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -122,4 +122,4 @@ def test_invalid_tag(api_client, filename): def test_invalid_xml(api_client): response = api_client.post(reverse('swid-add-tags'), [" Date: Tue, 27 May 2014 16:28:59 +0200 Subject: [PATCH 02/51] Added coveralls --- .travis.yml | 3 +++ README.rst | 8 ++++++-- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 98e5ba81..e17494fd 100644 --- a/.travis.yml +++ b/.travis.yml @@ -7,3 +7,6 @@ script: - cp config/settings.sample.ini config/settings.ini - sed -i 's/DEBUG\s*=\s*0/DEBUG = 1/' config/settings.ini - ./runtests.py +after_script: + - pip install --quiet --use-mirrors coveralls + - coveralls diff --git a/README.rst b/README.rst index cffc61f3..0215b289 100644 --- a/README.rst +++ b/README.rst @@ -2,8 +2,12 @@ strongTNC ========= .. image:: https://travis-ci.org/strongswan/strongTNC.png?branch=master - :target: https://travis-ci.org/strongswan/strongTNC - :alt: Build status + :target: https://travis-ci.org/strongswan/strongTNC + :alt: Build status + +.. image:: https://coveralls.io/repos/strongswan/strongTNC/badge.png?branch=master + :target: https://coveralls.io/r/strongswan/strongTNC + :alt: Test coverage .. image:: https://landscape.io/github/strongswan/strongTNC/master/landscape.png :target: https://landscape.io/github/strongswan/strongTNC/master From 81c3e5449fa4f143783a9bb5b0ff2dbfba7fca33 Mon Sep 17 00:00:00 2001 From: Danilo Bargen Date: Tue, 27 May 2014 16:54:41 +0200 Subject: [PATCH 03/51] Handle entities with multiple roles --- apps/policies/models.py | 1 - apps/swid/models.py | 2 ++ apps/swid/utils.py | 25 ++++++++++--------- tests/test_swid.py | 17 ++++++++++++- .../strongswan.full.swidtag.combinedrole | 11 ++++++++ 5 files changed, 42 insertions(+), 14 deletions(-) create mode 100644 tests/test_tags/strongswan.full.swidtag.combinedrole diff --git a/apps/policies/models.py b/apps/policies/models.py index 9318efe8..0a92540d 100644 --- a/apps/policies/models.py +++ b/apps/policies/models.py @@ -60,7 +60,6 @@ def create_work_item(self, enforcement, session): 'NONE', ] - # TODO create CHOICES from this, use get_types_display() types = [ 'Deny', 'Installed Packages', diff --git a/apps/swid/models.py b/apps/swid/models.py index aeede9ad..0f45790a 100644 --- a/apps/swid/models.py +++ b/apps/swid/models.py @@ -106,6 +106,8 @@ def xml_attr_to_choice(cls, value): return cls.LICENSOR elif value == 'publisher': return cls.PUBLISHER + else: + raise ValueError('Unknown role: %s' % value) class Entity(models.Model): diff --git a/apps/swid/utils.py b/apps/swid/utils.py index 90a8398e..df34773b 100644 --- a/apps/swid/utils.py +++ b/apps/swid/utils.py @@ -45,18 +45,19 @@ def start(self, tag, attrib): # Store entities regid = attrib['regid'] name = attrib['name'] - role = attrib['role'] - entity_role = EntityRole() - entity, _ = Entity.objects.get_or_create(regid=regid) - entity.name = name - role = EntityRole.xml_attr_to_choice(role) - - entity_role.role = role - self.entities.append((entity, entity_role)) - - # Use regid of first entity with tagcreator role to construct software-id - if role == EntityRole.TAGCREATOR: - self.tag.software_id = '%s_%s' % (regid, self.tag.unique_id) + roles = attrib['role'] + for role in roles.split(): + entity, _ = Entity.objects.get_or_create(regid=regid) + entity.name = name + + role_id = EntityRole.xml_attr_to_choice(role) + entity_role = EntityRole() + entity_role.role = role_id + self.entities.append((entity, entity_role)) + + # Use regid of last entity with tagcreator role to construct software-id + if role_id == EntityRole.TAGCREATOR: + self.tag.software_id = '%s_%s' % (regid, self.tag.unique_id) def close(self): """ diff --git a/tests/test_swid.py b/tests/test_swid.py index da905dc2..2e692df5 100644 --- a/tests/test_swid.py +++ b/tests/test_swid.py @@ -86,6 +86,8 @@ def test_tag_version(swidtag, filename, version): @pytest.mark.parametrize(['filename', 'tagroles'], [ ('strongswan.short.swidtag', [EntityRole.TAGCREATOR]), ('strongswan.full.swidtag', [EntityRole.TAGCREATOR, EntityRole.PUBLISHER]), + ('strongswan.full.swidtag.combinedrole', + [EntityRole.TAGCREATOR, EntityRole.PUBLISHER, EntityRole.LICENSOR]), ('cowsay.short.swidtag', [EntityRole.TAGCREATOR]), ('cowsay.full.swidtag', [EntityRole.TAGCREATOR, EntityRole.LICENSOR]), ('strongswan-tnc-imcvs.short.swidtag', [EntityRole.TAGCREATOR]), @@ -169,7 +171,7 @@ def test_invalid_tags(filename): @pytest.mark.parametrize('filename',[ - 'strongswan.full.swidtag' + 'strongswan.full.swidtag', ]) def test_entity_name_update(swidtag, filename): assert(Entity.objects.count() == 1) @@ -188,3 +190,16 @@ def test_entity_name_update(swidtag, filename): assert(Entity.objects.count() == 2) assert(Tag.objects.count() == 2) assert(not replaced) + + +@pytest.mark.parametrize('value', ['publisher', 'licensor', 'tagcreator']) +def test_valid_role(value): + try: + EntityRole.xml_attr_to_choice(value) + except ValueError: + pytest.fail('Role %s should be valid.' % value) + + +def test_invalid_role(): + with pytest.raises(ValueError): + EntityRole.xml_attr_to_choice('licensee') diff --git a/tests/test_tags/strongswan.full.swidtag.combinedrole b/tests/test_tags/strongswan.full.swidtag.combinedrole new file mode 100644 index 00000000..497b2d61 --- /dev/null +++ b/tests/test_tags/strongswan.full.swidtag.combinedrole @@ -0,0 +1,11 @@ + + + + + + + + + + + From 174e4b6442beeff073bde8aa038996e402c6fa43 Mon Sep 17 00:00:00 2001 From: Danilo Bargen Date: Tue, 27 May 2014 16:04:15 +0200 Subject: [PATCH 04/51] Improved & tested enforcement save view --- apps/policies/enforcement_views.py | 49 ++++++++----------- tests/fixtures.py | 44 +++++++++++++++++ tests/test_auth.py | 39 +-------------- tests/test_policies.py | 78 ++++++++++++++++++++++++++++++ 4 files changed, 143 insertions(+), 67 deletions(-) create mode 100644 tests/fixtures.py create mode 100644 tests/test_policies.py diff --git a/apps/policies/enforcement_views.py b/apps/policies/enforcement_views.py index 29b8e15f..4b2c11b3 100644 --- a/apps/policies/enforcement_views.py +++ b/apps/policies/enforcement_views.py @@ -3,7 +3,7 @@ import re -from django.http import HttpResponse +from django.http import HttpResponse, HttpResponseBadRequest from django.views.decorators.http import require_GET, require_POST from django.contrib import messages from django.contrib.auth.decorators import login_required, permission_required @@ -76,41 +76,33 @@ def add(request): @permission_required('auth.write_access', raise_exception=True) def save(request): """ - Insert/udpate an enforcement + Insert/update an enforcement """ - enforcementID = request.POST['enforcementId'] - if not (enforcementID == 'None' or re.match(r'^\d+$', enforcementID)): - raise ValueError - return HttpResponse(status=400) + enforcement_id = request.POST.get('enforcementId', '') + if not (enforcement_id == 'None' or re.match(r'^\d+$', enforcement_id)): + return HttpResponseBadRequest() - max_age = request.POST['max_age'] + max_age = request.POST.get('max_age', '') if not re.match(r'^\d+$', max_age): - raise ValueError - return HttpResponse(status=400) + return HttpResponseBadRequest() - policyID = request.POST['policy'] - if not re.match(r'^\d+$', policyID): - raise ValueError - return HttpResponse(status=400) + policy_id = request.POST.get('policy', '') + if not re.match(r'^\d+$', policy_id): + return HttpResponseBadRequest() - groupID = request.POST['group'] - if not re.match(r'^\d+$', groupID): - raise ValueError - return HttpResponse(status=400) + group_id = request.POST.get('group', '') + if not re.match(r'^\d+$', group_id): + return HttpResponseBadRequest() try: - policy = Policy.objects.get(pk=policyID) - group = Group.objects.get(pk=groupID) + policy = Policy.objects.get(pk=policy_id) + group = Group.objects.get(pk=group_id) except (Policy.DoesNotExist, Group.DoesNotExist): - raise ValueError - return HttpResponse(status=400) + return HttpResponseBadRequest() fail = request.POST.get('fail') if not (re.match(r'^-?\d+$', fail) and int(fail) in range(-1, len(Policy.action))): - # TODO replace lines like these with - # raise HttpResponseBadRequest() - raise ValueError - return HttpResponse(status=400) + return HttpResponseBadRequest() fail = int(fail) if fail == -1: @@ -118,18 +110,17 @@ def save(request): noresult = request.POST.get('noresult', -1) if not (re.match(r'^-?\d+$', noresult) and int(noresult) in range(-1, len(Policy.action))): - raise ValueError - return HttpResponse(status=400) + return HttpResponseBadRequest() noresult = int(noresult) if noresult == -1: noresult = None - if enforcementID == 'None': + if enforcement_id == 'None': enforcement = Enforcement.objects.create(group=group, policy=policy, max_age=max_age, fail=fail, noresult=noresult) else: - enforcement = get_object_or_404(Enforcement, pk=enforcementID) + enforcement = get_object_or_404(Enforcement, pk=enforcement_id) enforcement.group = group enforcement.policy = policy enforcement.max_age = max_age diff --git a/tests/fixtures.py b/tests/fixtures.py new file mode 100644 index 00000000..6301e4d7 --- /dev/null +++ b/tests/fixtures.py @@ -0,0 +1,44 @@ +# -*- coding: utf-8 -*- +from __future__ import print_function, division, absolute_import, unicode_literals + +import pytest + +from django.contrib.auth import get_user_model + +from apps.auth.permissions import GlobalPermission + + +@pytest.fixture +def write_access_perm(transactional_db): + """ + Provide the ``write_access`` permission. + """ + perm = GlobalPermission.objects.create(codename='write_access', + name='Has write access to data.') + return perm + + +@pytest.fixture +def strongtnc_users(transactional_db, write_access_perm): + """ + Provide two users called ``admin-user`` and ``readonly-user`` with correct + permissions. + """ + User = get_user_model() + admin_user = User.objects.create(username='admin-user') + admin_user.set_password('admin') + admin_user.user_permissions.add(write_access_perm) + admin_user.save() + readonly_user = User.objects.create(username='readonly-user') + readonly_user.set_password('readonly') + readonly_user.save() + + +@pytest.fixture +def test_user(transactional_db): + """ + Provide a user ``test`` with password ``test``. + """ + user = get_user_model().objects.create(username='test') + user.set_password('test') + user.save() diff --git a/tests/test_auth.py b/tests/test_auth.py index 9a02a870..a057facf 100644 --- a/tests/test_auth.py +++ b/tests/test_auth.py @@ -5,47 +5,10 @@ import pytest import json -from django.contrib.auth import get_user_model, login as django_login from django.core.urlresolvers import reverse from django.utils.datastructures import MultiValueDictKeyError -from apps.auth.permissions import GlobalPermission - - -@pytest.fixture -def write_access_perm(transactional_db): - """ - Provide the ``write_access`` permission. - """ - perm = GlobalPermission.objects.create(codename='write_access', - name='Has write access to data.') - return perm - - -@pytest.fixture -def strongtnc_users(transactional_db, write_access_perm): - """ - Provide two users called ``admin-user`` and ``readonly-user`` with correct - permissions. - """ - User = get_user_model() - admin_user = User.objects.create(username='admin-user') - admin_user.set_password('admin') - admin_user.user_permissions.add(write_access_perm) - admin_user.save() - readonly_user = User.objects.create(username='readonly-user') - readonly_user.set_password('readonly') - readonly_user.save() - - -@pytest.fixture -def test_user(transactional_db): - """ - Provide a user ``test`` with password ``test``. - """ - user = get_user_model().objects.create(username='test') - user.set_password('test') - user.save() +from .fixtures import * # Star import is OK here because it's just a test @pytest.mark.parametrize('username, password, success', [ diff --git a/tests/test_policies.py b/tests/test_policies.py new file mode 100644 index 00000000..8c1f8ece --- /dev/null +++ b/tests/test_policies.py @@ -0,0 +1,78 @@ +# -*- coding: utf-8 -*- +""" +Tests for `policies` app. +""" +from __future__ import print_function, division, absolute_import, unicode_literals + +from django.core.urlresolvers import reverse + +import pytest +from model_mommy import mommy + +from apps.policies.models import Policy, Enforcement +from apps.devices.models import Group + +from .fixtures import * # Star import is OK here because it's just a test + + +@pytest.fixture +def policy_testdata(transactional_db): + p = mommy.make(Policy, pk=1) + g = mommy.make(Group, pk=1) + mommy.make(Enforcement, pk=1, policy=p, group=g) + + +def test_save_enforcement_validation(strongtnc_users, client, policy_testdata): + # Log in + client.login(username='admin-user', password='admin') + url = reverse('policies:enforcement_save') + + data = {} + + invalid_ids = ['', 'new', '-1'] + def do_request(reason): + response = client.post(url, data=data) + assert response.status_code == 400, msg + + # Missing data + do_request('Missing data') + + # Invalid enforcement ID + for value in invalid_ids: + data['enforcementId'] = value + do_request('Invalid enforcementId (%s)' % value) + data['enforcementId'] = 1 + + # Invalid max_age + for value in invalid_ids: + data['max_age'] = value + do_request('Invalid max_age (%s)' % value) + data['max_age'] = 1 + + # Invalid policy + for value in invalid_ids + [2]: + data['policy'] = value + do_request('Invalid policy (%s)' % value) + data['policy'] = 1 + + # Invalid group + for value in invalid_ids + [2]: + data['group'] = value + do_request('Invalid group (%s)' % value) + data['group'] = 1 + + # Invalid fail action + for value in [-2, len(Policy.action), 'foo']: + data['fail'] = value + do_request('Invalid fail action (%s)' % value) + data['fail'] = 1 + + # Invalid noresult action + for value in [-2, len(Policy.action), 'foo']: + data['noresult'] = value + do_request('Invalid noresult action (%s)' % value) + data['noresult'] = 1 + + # Valid request + response = client.post(url, data) + assert response.status_code == 302 From eba7db64bf310bae7e6dab8081c32a01d6968692 Mon Sep 17 00:00:00 2001 From: Danilo Bargen Date: Tue, 27 May 2014 18:34:10 +0200 Subject: [PATCH 05/51] Added SQL_DEBUG setting With SQL_DEBUG enabled, all executed SQL queries get printed to stdout. --- README.rst | 7 ++++++- config/settings.py | 15 ++++++++++++++- config/settings.sample.ini | 3 +++ 3 files changed, 23 insertions(+), 2 deletions(-) diff --git a/README.rst b/README.rst index 0215b289..8d352570 100644 --- a/README.rst +++ b/README.rst @@ -82,13 +82,18 @@ Now you can start the development server. :: The web interface should be available on ``http://localhost:8000/``. +**Debugging** + If you want to use the django debug toolbar, install it via pip:: pip install django-debug-toolbar -Then start the server with the setting ``DEBUG_TOOLBAR = 1`` (in +Then start the server with the setting ``[debug] DEBUG_TOOLBAR = 1`` (in ``settings.ini``). +To print all executed SQL queries to stdout, start the server with the setting +``[debug] SQL_DEBUG = 1`` (in ``settings.ini``). + Testing ------- diff --git a/config/settings.py b/config/settings.py index 08f697c5..2bcc5e04 100644 --- a/config/settings.py +++ b/config/settings.py @@ -234,7 +234,11 @@ 'level': 'ERROR', 'filters': ['require_debug_false'], 'class': 'django.utils.log.AdminEmailHandler' - } + }, + 'console': { + 'level': 'DEBUG', + 'class': 'logging.StreamHandler', + }, }, 'loggers': { 'django.request': { @@ -244,6 +248,15 @@ }, } } +try: + if config.getboolean('debug', 'SQL_DEBUG'): + # This will cause all SQL queries to be printed + LOGGING['loggers']['django.db.backends'] = { + 'level': 'DEBUG', + 'handlers': ['console'], + } +except (NoSectionError, NoOptionError): + pass MESSAGE_TAGS = { messages.DEBUG: 'debug', diff --git a/config/settings.sample.ini b/config/settings.sample.ini index 13257259..0b901a72 100644 --- a/config/settings.sample.ini +++ b/config/settings.sample.ini @@ -5,6 +5,9 @@ DEBUG = 0 ; Whether to use the Django debugging templates when something goes wrong. ; Enable this during development, but never in production. TEMPLATE_DEBUG = 0 +; Whether to print all executed SQL queries to stdout. +; Enable this during development, but never in production. +SQL_DEBUG = 0 ; Whether to use the Django debug toolbar. If you want to use this, install ; `django-debug-toolbar` via pip. This is sometimes useful for debugging ; problems, but it also slows down page load times. From b689dd307c2ce11587d812ea2682dd429d51715b Mon Sep 17 00:00:00 2001 From: Danilo Bargen Date: Tue, 27 May 2014 19:11:31 +0200 Subject: [PATCH 06/51] Optimized database access in swid-measurement endpoint --- apps/swid/api_views.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/apps/swid/api_views.py b/apps/swid/api_views.py index 62b7d906..034c48e2 100644 --- a/apps/swid/api_views.py +++ b/apps/swid/api_views.py @@ -82,15 +82,13 @@ class SwidMeasurementView(views.APIView): """ def post(self, request, pk, format=None): software_ids = request.DATA - found_tags = [] missing_tags = [] + found_tags = Tag.objects.filter(software_id__in=software_ids) # Look for matching tags + found_software_ids = [t.software_id for t in found_tags] for software_id in software_ids: - try: - tag = Tag.objects.get(software_id=software_id) - found_tags.append(tag) - except Tag.DoesNotExist: + if software_id not in found_software_ids: missing_tags.append(software_id) if missing_tags: From b4b3d76bb2f1fd65e346aa065fe9d6dbe662a855 Mon Sep 17 00:00:00 2001 From: cfaessler Date: Wed, 28 May 2014 21:30:53 +0200 Subject: [PATCH 07/51] Moved inventory und log view js files to swid app --- apps/{front => swid}/static/js/swid-inventory.js | 0 apps/{front => swid}/static/js/swid-log.js | 0 apps/swid/templates/swid/swid_inventory.html | 7 +++++-- apps/swid/templates/swid/swid_log.html | 11 +++++++---- 4 files changed, 12 insertions(+), 6 deletions(-) rename apps/{front => swid}/static/js/swid-inventory.js (100%) rename apps/{front => swid}/static/js/swid-log.js (100%) diff --git a/apps/front/static/js/swid-inventory.js b/apps/swid/static/js/swid-inventory.js similarity index 100% rename from apps/front/static/js/swid-inventory.js rename to apps/swid/static/js/swid-inventory.js diff --git a/apps/front/static/js/swid-log.js b/apps/swid/static/js/swid-log.js similarity index 100% rename from apps/front/static/js/swid-log.js rename to apps/swid/static/js/swid-log.js diff --git a/apps/swid/templates/swid/swid_inventory.html b/apps/swid/templates/swid/swid_inventory.html index 2a740a29..6d8b76f2 100644 --- a/apps/swid/templates/swid/swid_inventory.html +++ b/apps/swid/templates/swid/swid_inventory.html @@ -125,8 +125,11 @@

{% trans 'Measured Software Tags' %}

- - {% endif %} + {% endif %} +{% endblock %} +{% block footer_js %} + {{ block.super }} + {% endblock %} diff --git a/apps/swid/templates/swid/swid_log.html b/apps/swid/templates/swid/swid_log.html index 9ae66bd0..d4293109 100644 --- a/apps/swid/templates/swid/swid_log.html +++ b/apps/swid/templates/swid/swid_log.html @@ -79,7 +79,7 @@

{% trans 'Changes summary' %}

- +
@@ -101,14 +101,17 @@

{% trans 'Change log' %}

- {% comment %} + {% comment %} currently filled via javascript - {% endcomment %} + {% endcomment %}

{% trans 'No entries' %}

- +{% endblock %} +{% block footer_js %} + {{ block.super }} + {% endblock %} From 3cab39aa0cbb8d59414345da92a3565ba446c7b9 Mon Sep 17 00:00:00 2001 From: cfaessler Date: Wed, 28 May 2014 00:07:05 +0200 Subject: [PATCH 08/51] Added sorting to reported by device table --- apps/swid/templates/swid/tags_detail.html | 4 ++-- apps/swid/views.py | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/apps/swid/templates/swid/tags_detail.html b/apps/swid/templates/swid/tags_detail.html index 5b334525..dcc71151 100644 --- a/apps/swid/templates/swid/tags_detail.html +++ b/apps/swid/templates/swid/tags_detail.html @@ -88,10 +88,10 @@

{% trans 'Reported by Devices' %}

- {% for device, session in devices.items %} + {% for device, session in reported_devices %} - {{ device.description }} + {{ device.list_repr }} {{ session.time|date:"M d H:i:s Y" }} diff --git a/apps/swid/views.py b/apps/swid/views.py index 6310337c..23f4d040 100644 --- a/apps/swid/views.py +++ b/apps/swid/views.py @@ -38,7 +38,8 @@ class SwidTagDetailView(LoginRequiredMixin, DetailView): def get_context_data(self, **kwargs): context = super(SwidTagDetailView, self).get_context_data(**kwargs) context['entityroles'] = self.object.entityrole_set.all() - context['devices'] = self.object.get_devices_with_reported_session() + context['reported_devices'] = sorted(self.object.get_devices_with_reported_session().items(), + key=lambda (device, session): device.description) return context From cb4e1a4461d959896227915ed7ea11fa059fcfb4 Mon Sep 17 00:00:00 2001 From: Jonas Furrer Date: Wed, 28 May 2014 16:53:22 +0200 Subject: [PATCH 09/51] Changed result table-headers from td to th --- apps/devices/templates/devices/session.html | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/devices/templates/devices/session.html b/apps/devices/templates/devices/session.html index 83a4c21f..6dc7718d 100644 --- a/apps/devices/templates/devices/session.html +++ b/apps/devices/templates/devices/session.html @@ -57,9 +57,9 @@

{% trans 'Results' %}

- - - + From 1ded3d0d586e30ad52ba382a5a09b1a4a0490565 Mon Sep 17 00:00:00 2001 From: Jonas Furrer Date: Wed, 28 May 2014 16:04:20 +0200 Subject: [PATCH 10/51] Prevent dates and some titles from wrapping --- .../templates/devices/device_report.html | 6 +- .../paging/device_report_sessions.html | 2 +- apps/front/static/css/base.css | 10 ++ .../packages/templates/packages/packages.html | 2 +- apps/swid/templates/swid/regid_detail.html | 8 +- apps/swid/templates/swid/swid_inventory.html | 6 +- apps/swid/templates/swid/swid_log.html | 18 +-- apps/swid/templates/swid/tags_detail.html | 116 +++++++++--------- 8 files changed, 88 insertions(+), 80 deletions(-) diff --git a/apps/devices/templates/devices/device_report.html b/apps/devices/templates/devices/device_report.html index 5399b325..f4fd0c57 100644 --- a/apps/devices/templates/devices/device_report.html +++ b/apps/devices/templates/devices/device_report.html @@ -15,9 +15,9 @@

{{ title }}

{% trans 'Device Infos' %}

-
+
{% trans "Policy" %}{% trans "Result" %}{% trans "IMV Comment" %}{% trans "Policy" %} + {% trans "Result" %} + {% trans "IMV Comment" %}
- + @@ -32,7 +32,7 @@

{% trans 'Device Infos' %}

- - - + From d2abb280966175fc3e66c20b98c9a0ef8eb16a67 Mon Sep 17 00:00:00 2001 From: Jonas Furrer Date: Wed, 28 May 2014 21:31:35 +0200 Subject: [PATCH 11/51] Provided possibility to add file via gui --- apps/filesystem/file_views.py | 39 ++++-- apps/filesystem/static/js/files.js | 20 +++ .../templates/filesystem/directories.html | 5 +- .../templates/filesystem/files.html | 118 ++++++++++++------ apps/filesystem/urls.py | 1 + apps/front/static/js/ajax-utils.js | 35 ++++++ .../policies/templates/policies/policies.html | 36 ------ tests/test_auth.py | 1 + tests/test_filesystem.py | 51 ++++++++ 9 files changed, 221 insertions(+), 85 deletions(-) create mode 100644 apps/filesystem/static/js/files.js create mode 100644 tests/test_filesystem.py diff --git a/apps/filesystem/file_views.py b/apps/filesystem/file_views.py index 2610b71b..80878eb1 100644 --- a/apps/filesystem/file_views.py +++ b/apps/filesystem/file_views.py @@ -3,14 +3,14 @@ import re -from django.http import HttpResponse +from django.http import HttpResponseBadRequest from django.views.decorators.http import require_GET, require_POST from django.contrib import messages from django.contrib.auth.decorators import login_required, permission_required from django.shortcuts import get_object_or_404, render, redirect from django.utils.translation import ugettext_lazy as _ -from .models import File, FileHash +from .models import File, FileHash, Directory from apps.policies.models import Policy, Enforcement @@ -60,18 +60,37 @@ def file(request, fileID): @permission_required('auth.write_access', raise_exception=True) def save(request): """ - Insert/update view + Insert view """ - fileID = request.POST['fileId'] - if not (fileID == 'None' or re.match(r'^\d+$', fileID)): - return HttpResponse(status=400) - - name = request.POST['name'] + name = request.POST.get('name', '') if not re.match(r'^[\S]+$', name): - return HttpResponse(status=400) + return HttpResponseBadRequest() + + dir_id = request.POST.get('dir') + if dir_id is None or not re.match(r'^\d+$', dir_id): + return HttpResponseBadRequest() + + try: + new_file = File.objects.create(name=name, directory=Directory.objects.get(pk=dir_id)) + except Directory.DoesNotExist: + return HttpResponseBadRequest() messages.success(request, _('File saved!')) - return redirect('filessystem:file_detail', file.pk) + return redirect('filesystem:file_detail', new_file.pk) + + +@require_GET +@login_required +@permission_required('auth.write_access', raise_exception=True) +def add(request): + """ + Add a file + """ + context = {} + context['add'] = True + context['title'] = _('New file') + context['file'] = File() + return render(request, 'filesystem/files.html', context) @require_POST diff --git a/apps/filesystem/static/js/files.js b/apps/filesystem/static/js/files.js new file mode 100644 index 00000000..55cd59b3 --- /dev/null +++ b/apps/filesystem/static/js/files.js @@ -0,0 +1,20 @@ +$(document).ready(function() { + setupDirectoryDropdown(); +}); + +function setupDirectoryDropdown() { + $('input#dir').select2({ + minimumInputLength: 3, + formatSelection: function (o) { + return o.directory + }, + formatResult: function (o) { + return o.directory + }, + query: function (query) { + autocompleteDelay.callback = query.callback; + autocompleteDelay.ajaxFunction = Dajaxice.apps.filesystem.directories_autocomplete; + autocompleteDelay.queryUpdate(query.term); + } + }); +} \ No newline at end of file diff --git a/apps/filesystem/templates/filesystem/directories.html b/apps/filesystem/templates/filesystem/directories.html index 4dcda129..0ce98fdb 100644 --- a/apps/filesystem/templates/filesystem/directories.html +++ b/apps/filesystem/templates/filesystem/directories.html @@ -44,7 +44,8 @@

{% trans 'Add new directory' %}

+ value="{{ directory.path }}" + {% input_editability %}> {% else %} @@ -53,8 +54,6 @@

{% trans 'Directory info:' %} {{ directory.path }}

{% endif %} - - {% if 'auth.write_access' in perms %}
{% if add %} diff --git a/apps/filesystem/templates/filesystem/files.html b/apps/filesystem/templates/filesystem/files.html index f14a6a1d..61359859 100644 --- a/apps/filesystem/templates/filesystem/files.html +++ b/apps/filesystem/templates/filesystem/files.html @@ -15,7 +15,16 @@

{{ title }}

-

{% trans "File" %}

+

{% trans "File" %} + {% if 'auth.write_access' in perms %} +
+ + + +
+ {% endif %} +


{% paged_block config_name='file_list_config' with_filter=True %} @@ -27,57 +36,89 @@

{% trans "File" %}

{% if file %}
{% csrf_token %} +
-

{% trans 'File info:' %} {{ file.directory }}/{{ file.name }}

+ {% if add %} +

{% trans 'Add new file' %}

+
+ + + +
+ +
+
+
+ + +
+ +
+
+ {% else %} +

{% trans 'File info:' %} {{ file.directory }}/{{ file.name }}

+ {% endif %}
{% if 'auth.write_access' in perms %}
- {% if file.id %} + {% if add %} +
+ +
+ {% else %} - {% endif %}
{% endif %} -
- -

File Hashes

- {% if file_hashes %} -
-
{% trans 'ID' %} {{ device.value }}
{% trans 'Last session' %}{% if last_session %} + {% if last_session %} {{ last_session|date:'M d H:i:s Y' }} {% else %} {% trans 'None' %} diff --git a/apps/devices/templates/devices/paging/device_report_sessions.html b/apps/devices/templates/devices/paging/device_report_sessions.html index f886821f..74453bac 100644 --- a/apps/devices/templates/devices/paging/device_report_sessions.html +++ b/apps/devices/templates/devices/paging/device_report_sessions.html @@ -3,7 +3,7 @@ - + diff --git a/apps/front/static/css/base.css b/apps/front/static/css/base.css index f3c5fc5a..fb614f94 100644 --- a/apps/front/static/css/base.css +++ b/apps/front/static/css/base.css @@ -140,4 +140,14 @@ pre.raw-xml { .table .siwd-new-icon { width: 20px; +} + +td.dateWidth, +th.dateWidth { + min-width: 150px; +} + +.noWrap, +.noWrap { + white-space: nowrap; } \ No newline at end of file diff --git a/apps/packages/templates/packages/packages.html b/apps/packages/templates/packages/packages.html index c7dfd15a..1499d829 100644 --- a/apps/packages/templates/packages/packages.html +++ b/apps/packages/templates/packages/packages.html @@ -67,7 +67,7 @@

{% trans "Versions:" %}

- + diff --git a/apps/swid/templates/swid/regid_detail.html b/apps/swid/templates/swid/regid_detail.html index 20c5f10e..44bf91ce 100644 --- a/apps/swid/templates/swid/regid_detail.html +++ b/apps/swid/templates/swid/regid_detail.html @@ -13,19 +13,19 @@

Regid Info

-
+
{% trans "Time" %}{% trans "Time" %} {% trans "Result" %}
{% trans 'Version' %} {% trans 'OS' %} {% trans 'Security' %}{% trans 'Registered on' %}{% trans 'Registered on' %} {% trans 'Blacklisted' %}
- + - + - + diff --git a/apps/swid/templates/swid/swid_inventory.html b/apps/swid/templates/swid/swid_inventory.html index 6d8b76f2..18c7c7c8 100644 --- a/apps/swid/templates/swid/swid_inventory.html +++ b/apps/swid/templates/swid/swid_inventory.html @@ -76,12 +76,12 @@
2. Choose from available Sessions

{% trans 'Measured Software Tags' %}

-
+
{% trans 'Entity Name' %}{% trans 'Entity Name' %} {{ object.name }}
{% trans 'Regid' %}{% trans 'Regid' %} {{ object.regid }}
{% trans 'Tag Count' %}{% trans 'Tag count' %} {{ object.tags.count }}
- @@ -114,7 +114,7 @@

{% trans 'Measured Software Tags' %}

- diff --git a/apps/swid/templates/swid/swid_log.html b/apps/swid/templates/swid/swid_log.html index d4293109..eb470d9c 100644 --- a/apps/swid/templates/swid/swid_log.html +++ b/apps/swid/templates/swid/swid_log.html @@ -54,28 +54,28 @@

{% trans 'Set date range' %}

{% trans 'Changes summary' %}

-
+
{% trans 'Session Date' %} + {% trans 'Please select session first' %} Unique ID + First reported
- + - + - + - - + + - - + +
{% trans 'Added SWID tags' %}{% trans 'Added SWID tags' %} 0
{% trans 'Removed SWID tags' %}{% trans 'Removed SWID tags' %} 0
{% trans 'Sessions in range' %}{% trans 'Sessions in range' %} 0
{% trans 'First session in range' %}{% trans 'None' %}{% trans 'First session in range' %}{% trans 'None' %}
{% trans 'Last session in range' %}{% trans 'None' %}{% trans 'Last session in range' %}{% trans 'None' %}
@@ -88,7 +88,7 @@

{% trans 'Change log' %}

class="displ-none table table-hover table-striped">
+ {% trans 'Session' %} diff --git a/apps/swid/templates/swid/tags_detail.html b/apps/swid/templates/swid/tags_detail.html index dcc71151..5b287505 100644 --- a/apps/swid/templates/swid/tags_detail.html +++ b/apps/swid/templates/swid/tags_detail.html @@ -14,66 +14,64 @@

{{ object.unique_id }}

{% if object %}

Tag Info

-
- - - - - + + + + + + + + + + +
{% trans 'Name' %} - {% if object.get_matching_packages %} - - {{ object.package_name }} - - {% else %} + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - -
{% trans 'Name' %} + {% if object.get_matching_packages %} + {{ object.package_name }} - - + + {% else %} + {{ object.package_name }} + + + {% endif %} +
{% trans 'Version' %}{{ object.version }}
{% trans 'Unique ID' %}{{ object.unique_id }}
{% trans 'Entities' %} + {% for er in entityroles %} + {{ er.entity.name }} ({{ er.get_role_display }}) + {% if not forloop.last %} +
{% endif %} -
{% trans 'Version' %}{{ object.version }}
{% trans 'Unique ID' %}{{ object.unique_id }}
{% trans 'Entities' %} - {% for er in entityroles %} - {{ er.entity.name }} ({{ er.get_role_display }}) - {% if not forloop.last %} -
- {% endif %} - {% endfor %} -
{% trans 'Regids' %} - {% for er in entityroles %} - {{ er.entity.regid }} - {% if not forloop.last %} -
- {% endif %} - {% endfor %} -
{% trans 'Software ID' %} - {{ object.software_id }} -
- - {% trans 'View raw SWID tag' %} - - + {% endfor %} +
{% trans 'Regids' %} + {% for er in entityroles %} + {{ er.entity.regid }} + {% if not forloop.last %} +
+ {% endif %} + {% endfor %} +
{% trans 'Software ID' %} + {{ object.software_id }} +
+ + {% trans 'View raw SWID tag' %} +

@@ -84,7 +82,7 @@

{% trans 'Reported by Devices' %}

{% trans 'Description' %}{% trans 'First reported' %}{% trans 'First reported' %}
- - - - - - {% if 'auth.write_access' in perms %} - - {% endif %} - - - - {% for h in file_hashes %} + {% if not add %} +
+ +

File Hashes

+ {% if file_hashes %} +
+
{% trans 'OS' %}{% trans 'Algo' %}{% trans 'Hash' %} 
+ - - - + + + {% if 'auth.write_access' in perms %} - + {% endif %} - {% endfor %} - -
{{ h.product }}{{ h.algorithm }}{{ h.hash|truncatechars:40 }}{% trans 'OS' %}{% trans 'Algo' %}{% trans 'Hash' %} - -  
- - {% else %} -

{% trans 'This file has no associated file hashes.' %}

+ + + {% for h in file_hashes %} + + {{ h.product }} + {{ h.algorithm }} + {{ h.hash|truncatechars:40 }} + {% if 'auth.write_access' in perms %} + + + + {% endif %} + + {% endfor %} + + + + {% else %} +

{% trans 'This file has no associated file hashes.' %}

+ {% endif %} {% endif %} @@ -203,3 +244,8 @@

{% trans 'Warning' %}

{% endblock %} + +{% block footer_js %} + {{ block.super }} + +{% endblock %} \ No newline at end of file diff --git a/apps/filesystem/urls.py b/apps/filesystem/urls.py index cc658ac0..475f249a 100644 --- a/apps/filesystem/urls.py +++ b/apps/filesystem/urls.py @@ -4,6 +4,7 @@ urlpatterns = patterns('', url(r'^files/?$', file_views.files, name='file_list'), url(r'^files/(?P\d+)/?$', file_views.file, name='file_detail'), + url(r'^files/add/?$', file_views.add, name='file_add'), url(r'^files/save/?$', file_views.save, name='file_save'), url(r'^files/(?P\d+)/delete/?$', file_views.delete, name='file_delete'), diff --git a/apps/front/static/js/ajax-utils.js b/apps/front/static/js/ajax-utils.js index 29dbf9be..ff036a1a 100644 --- a/apps/front/static/js/ajax-utils.js +++ b/apps/front/static/js/ajax-utils.js @@ -6,4 +6,39 @@ var ajaxSpinner = { disable: function () { this.element.spin(false); } +}; + +// delay the request, to reduce the amount of requests +// --> the request is only sent if the query does not +// change during a defined delay. +var autocompleteDelay = { + delay: 600, + query: '', + lastDelayedCall: null, + callback: null, + ajaxFunction: null, + + queryUpdate: function (newQuery) { + this.query = newQuery; + if (this.lastDelayedCall != null) { + this.cancelLastDelayedCall(); + } + this.startDelayedCall(); + }, + + startDelayedCall: function () { + this.lastDelayedCall = window.setTimeout(this.autocompleteCall, this.delay); + }, + + cancelLastDelayedCall: function () { + window.clearTimeout(this.lastDelayedCall); + }, + + autocompleteCall: function () { + // function is called by setTimeout, + // thus it's running in the context of the window object + var callback = autocompleteDelay.callback; + var query = autocompleteDelay.query; + autocompleteDelay.ajaxFunction(callback, {'search_term': query}); + } }; \ No newline at end of file diff --git a/apps/policies/templates/policies/policies.html b/apps/policies/templates/policies/policies.html index 4a9067da..fb09c3a5 100644 --- a/apps/policies/templates/policies/policies.html +++ b/apps/policies/templates/policies/policies.html @@ -325,42 +325,6 @@

{% trans 'Warning' %}

} }); }); - - // delay the request, to reduce the amount of requests - // --> the request is only sent if the query does not - // change during a defined delay. - var autocompleteDelay = { - delay: 600, - query: '', - lastDelayedCall: null, - callback: null, - ajaxFunction: null, - - queryUpdate: function (newQuery) { - this.query = newQuery; - if (this.lastDelayedCall != null) { - this.cancelLastDelayedCall(); - } - this.startDelayedCall(); - }, - - startDelayedCall: function () { - this.lastDelayedCall = window.setTimeout(this.autocompleteCall, this.delay); - }, - - cancelLastDelayedCall: function () { - window.clearTimeout(this.lastDelayedCall); - }, - - autocompleteCall: function () { - // function is called by setTimeout, - // thus it's running in the context of the window object - var callback = autocompleteDelay.callback; - var query = autocompleteDelay.query; - autocompleteDelay.ajaxFunction(callback, {'search_term': query}); - } - - }; {% endblock %} {% block footer_js %} diff --git a/tests/test_auth.py b/tests/test_auth.py index a057facf..d6d0ee49 100644 --- a/tests/test_auth.py +++ b/tests/test_auth.py @@ -60,6 +60,7 @@ def test_login_required(client, strongtnc_users, url): # Add views ('/groups/add/', 'get'), ('/devices/add/', 'get'), + ('/files/add/', 'get'), ('/directories/add/', 'get'), ('/packages/add/', 'get'), ('/products/add/', 'get'), diff --git a/tests/test_filesystem.py b/tests/test_filesystem.py new file mode 100644 index 00000000..7cc47792 --- /dev/null +++ b/tests/test_filesystem.py @@ -0,0 +1,51 @@ +# -*- coding: utf-8 -*- +""" +Tests for `policies` app. +""" +from __future__ import print_function, division, absolute_import, unicode_literals + +from django.core.urlresolvers import reverse + +import pytest +from model_mommy import mommy + +from apps.filesystem.models import File, Directory + +from .fixtures import * # Star import is OK here because it's just a test + + +@pytest.fixture +def file_testdata(transactional_db): + dir_obj = mommy.make(Directory, pk=1) + mommy.make(File, pk=1, name='the_file', directory=dir_obj) + + +def test_save_file_validation(strongtnc_users, client, file_testdata): + # Log in + client.login(username='admin-user', password='admin') + url = reverse('filesystem:file_save') + + data = {} + + def do_request(reason): + resp = client.post(url, data=data) + assert resp.status_code == 400, reason + + # Missing data + do_request('Missing data') + + # Invalid name + for value in [None, '']: + data['name'] = value + do_request('Invalid name (%s)' % value) + data['name'] = 'new_flie' + + # Invalid dir + for value in [None, '', 'new', '-1']: + data['dir'] = value + do_request('Invalid dir (%s)' % value) + data['dir'] = '1' + + # Valid request + response = client.post(url, data) + assert response.status_code == 302 From 55077432ba3e2af195483252cc41df317cd3a741 Mon Sep 17 00:00:00 2001 From: cfaessler Date: Wed, 28 May 2014 21:12:57 +0200 Subject: [PATCH 12/51] Moved swid log and inventory button to device report view --- apps/devices/templates/devices/device_report.html | 6 ++++++ apps/devices/templates/devices/devices.html | 6 +----- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/apps/devices/templates/devices/device_report.html b/apps/devices/templates/devices/device_report.html index f4fd0c57..c44aaef6 100644 --- a/apps/devices/templates/devices/device_report.html +++ b/apps/devices/templates/devices/device_report.html @@ -49,6 +49,12 @@

{% trans 'Device Infos' %}

+ diff --git a/apps/devices/templates/devices/devices.html b/apps/devices/templates/devices/devices.html index 40fd588d..af61f6dc 100644 --- a/apps/devices/templates/devices/devices.html +++ b/apps/devices/templates/devices/devices.html @@ -130,11 +130,7 @@

{% trans "Assign Groups" %} {% endif %} -
- {% trans "View SWID inventory" %} + class="icon-file icon-white"> {% trans "Device report" %}
{% endif %} From 4c4876f7acb3fe9869aa18f13c451677f649434a Mon Sep 17 00:00:00 2001 From: Jonas Furrer Date: Wed, 28 May 2014 15:03:36 +0200 Subject: [PATCH 13/51] Inital set of hash-params should not influence browser history --- apps/front/static/js/hashquery.js | 22 ++++++---- apps/front/static/js/paging.js | 68 ++++++++++++++++--------------- 2 files changed, 49 insertions(+), 41 deletions(-) diff --git a/apps/front/static/js/hashquery.js b/apps/front/static/js/hashquery.js index 7402f977..e3f48c19 100644 --- a/apps/front/static/js/hashquery.js +++ b/apps/front/static/js/hashquery.js @@ -23,7 +23,7 @@ HashQuery.sendChanged = function (key, value) { for (var i = 0; i < listeners.length; ++i) { listeners[i](key, value); } -} +}; HashQuery.notifyAll = function () { if (HashQuery.ignoreEvents) { @@ -55,28 +55,32 @@ HashQuery.addChangedListener = function (tag, callback) { } }; -HashQuery.setHashKey = function (obj, ignoreEvents) { +HashQuery.setHashKey = function (obj, ignoreEvents, avoidBrowserHistory) { var current_obj = HashQuery.getHashQueryObject(); for (var key in obj) { current_obj[key] = obj[key]; } - HashQuery.setHashQueryObject(current_obj, ignoreEvents); -} + HashQuery.setHashQueryObject(current_obj, ignoreEvents, avoidBrowserHistory); +}; // sets a key/value javascript object as pseudo // query string after the url-hash -HashQuery.setHashQueryObject = function (obj, ignoreEvents) { +HashQuery.setHashQueryObject = function (obj, ignoreEvents, avoidBrowserHistory) { var hash = '#'; for (var key in obj) { hash += encodeURIComponent(key) + - "=" + - encodeURIComponent(obj[key]) + - "&"; + "=" + + encodeURIComponent(obj[key]) + + "&"; } HashQuery.ignoreEvents = ignoreEvents; - window.location.hash = hash.slice(0, -1); + if(avoidBrowserHistory) { + window.location.replace(hash.slice(0, -1)); + } else { + window.location.hash = hash.slice(0, -1); + } }; window.addEventListener("hashchange", HashQuery.notifyAll, false); diff --git a/apps/front/static/js/paging.js b/apps/front/static/js/paging.js index 591d261a..1cc8407e 100644 --- a/apps/front/static/js/paging.js +++ b/apps/front/static/js/paging.js @@ -39,34 +39,37 @@ var Pager = function() { this.addFilter(); } - // if url parameter were set, get them - this.grabURLParams(); + // register hashChanged events + HashQuery.addChangedListener(this.pageParam, this.grabPageParam.bind(this)); + HashQuery.addChangedListener(this.filterParam, this.grabFilterParam.bind(this)); // get initial page + this.initial = true; + this.getInitalURLParam(); this.getPage(); - $(window).on('hashchange', function() { - if(!this.loading && this.hasURLParam()) { - if(this.grabURLParams()) { - this.getPage(); - } - } - }.bind(this)); }; - this.grabURLParams = function() { - var changed = false; - var currPageIdx = parseInt(this.getURLParam(this.pageParam)); + this.grabPageParam = function(key, value) { + var currPageIdx = parseInt(value); if(!isNaN(currPageIdx)) { var idxChanged = this.currentPageIdx != currPageIdx; this.currentPageIdx = currPageIdx; } - var filter = this.getURLParam(this.filterParam); + if(idxChanged) { + this.getPage(); + } + }; + + this.grabFilterParam = function(key, value) { + var filter = value; if(filter) { var filterChanged = this.getFilterQuery() != filter; this.$filterInput.val(filter); } - return changed || idxChanged || filterChanged; + if(filterChanged) { + this.getPage(); + } }; // grab next page @@ -107,7 +110,8 @@ var Pager = function() { this.pagingCallback = function(data) { this.statsUpdate(data); this.$contentContainer.html(data.html); - this.setURLParam(this.pageParam, this.currentPageIdx); + this.setURLParam(this.pageParam, this.currentPageIdx, this.initial); + this.initial = false; this.loading = false; }; @@ -130,7 +134,6 @@ var Pager = function() { this.$nextButton.prop('disabled', true); this.$prevButton.prop('disabled', true); } - }; this.getFilterQuery = function() { @@ -145,7 +148,7 @@ var Pager = function() { this.filterQuery = function() { this.currentPageIdx = 0; this.getPage(); - this.setURLParam(this.filterParam, this.getFilterQuery()); + this.setURLParam(this.filterParam, this.getFilterQuery(), false); }; this.addFilter = function() { @@ -182,25 +185,26 @@ var Pager = function() { }; }; - this.setURLParam = function(key, value) { - var params = HashQuery.getHashQueryObject(); - params[key] = value; - HashQuery.setHashQueryObject(params); + this.setURLParam = function(hashKey, hashValue, avoidHistory) { + var hashKeyObj = {}; + hashKeyObj[hashKey] = hashValue; + HashQuery.setHashKey(hashKeyObj, false, avoidHistory); }; - this.getURLParam = function(key) { + this.getInitalURLParam = function() { var params = HashQuery.getHashQueryObject(); - var param = params[key]; - if(param) { - return param; - } - return ''; - }; - - this.hasURLParam = function() { - var params = HashQuery.getHashQueryObject() for (var key in params) { - if (hasOwnProperty.call(params, key)) return true; + if (hasOwnProperty.call(params, key)) { + if(key == this.pageParam) { + var currIdx = parseInt(params[key]); + if(!isNaN(currIdx)) { + this.currentPageIdx = currIdx; + } + } + if(key == this.filterParam) { + this.$filterInput.val(params[key]); + } + } } }; From d85b5936a34ee41cfad6c551158f9dd9beb6f83d Mon Sep 17 00:00:00 2001 From: Danilo Bargen Date: Thu, 29 May 2014 11:48:46 +0200 Subject: [PATCH 14/51] Fixed some schema problems that occur with MySQL --- apps/core/models.py | 1 - apps/swid/models.py | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/apps/core/models.py b/apps/core/models.py index 45efe742..3659e14e 100644 --- a/apps/core/models.py +++ b/apps/core/models.py @@ -16,7 +16,6 @@ class Identity(models.Model): class Meta: db_table = 'identities' - unique_together = [('type', 'data')] verbose_name_plural = 'identities' ordering = ('data',) diff --git a/apps/swid/models.py b/apps/swid/models.py index 0f45790a..ce83f51a 100644 --- a/apps/swid/models.py +++ b/apps/swid/models.py @@ -22,7 +22,7 @@ class Tag(models.Model): swid_xml = models.TextField(help_text='The full SWID tag XML') files = models.ManyToManyField('filesystem.File', blank=True, verbose_name='list of files') sessions = models.ManyToManyField('core.Session', verbose_name='list of sessions') - software_id = models.TextField(max_length=255, db_index=True, + software_id = models.CharField(max_length=767, db_index=True, help_text='The Software ID, format: {regid}_{uniqueID} ' 'e.g regid.2004-03.org.strongswan_' 'fedora_19-x86_64-strongswan-5.1.2-4.fc19') From 9fdcc2fa11b229ab106ff2594b3cf7dd2c4708cc Mon Sep 17 00:00:00 2001 From: Danilo Bargen Date: Thu, 29 May 2014 12:01:06 +0200 Subject: [PATCH 15/51] Added initial data fixtures --- apps/devices/fixtures/initial_data.json | 12 ++++++++++++ apps/filesystem/fixtures/initial_data.json | 6 ++++++ 2 files changed, 18 insertions(+) create mode 100644 apps/devices/fixtures/initial_data.json create mode 100644 apps/filesystem/fixtures/initial_data.json diff --git a/apps/devices/fixtures/initial_data.json b/apps/devices/fixtures/initial_data.json new file mode 100644 index 00000000..8a1c688b --- /dev/null +++ b/apps/devices/fixtures/initial_data.json @@ -0,0 +1,12 @@ +[ + { + "pk": 1, + "model": "devices.group", + "fields": { + "product_defaults": [], + "devices": [], + "name": "Default", + "parent": null + } + } +] diff --git a/apps/filesystem/fixtures/initial_data.json b/apps/filesystem/fixtures/initial_data.json new file mode 100644 index 00000000..fe619b01 --- /dev/null +++ b/apps/filesystem/fixtures/initial_data.json @@ -0,0 +1,6 @@ +[ + {"pk": 8192, "model": "filesystem.algorithm", "fields": {"name": "SHA384"}}, + {"pk": 16384, "model": "filesystem.algorithm", "fields": {"name": "SHA256"}}, + {"pk": 32768, "model": "filesystem.algorithm", "fields": {"name": "SHA1"}}, + {"pk": 65536, "model": "filesystem.algorithm", "fields": {"name": "SHA1-IMA"}} +] From ee590767a8f4361736a73277940f7c0cd77bb479 Mon Sep 17 00:00:00 2001 From: Danilo Bargen Date: Thu, 29 May 2014 12:36:04 +0200 Subject: [PATCH 16/51] Enabled non-interactive mode on setpassword command --- apps/auth/management/commands/setpassword.py | 27 ++++++++++++++------ 1 file changed, 19 insertions(+), 8 deletions(-) diff --git a/apps/auth/management/commands/setpassword.py b/apps/auth/management/commands/setpassword.py index 18b9c786..2d9adb50 100644 --- a/apps/auth/management/commands/setpassword.py +++ b/apps/auth/management/commands/setpassword.py @@ -6,25 +6,35 @@ Usage: ./manage.py setpassword [password] """ +import sys from getpass import getpass + from django.contrib.auth import get_user_model -from django.core.management.base import NoArgsCommand +from django.core.management.base import BaseCommand from apps.auth.permissions import GlobalPermission -class Command(NoArgsCommand): +class Command(BaseCommand): """ Required class to be recognized by manage.py. """ - help = 'Get or create admin-user and set password interactively' + help = 'Get or create admin-user and set password' + args = '[ ]' - def handle_noargs(self, **kwargs): - self.process_user('admin-user', write_access=True) - self.process_user('readonly-user') + def handle(self, *args, **kwargs): + if len(args) == 0: + readonly_pw = admin_pw = None + elif len(args) == 2: + (readonly_pw, admin_pw) = args + else: + self.stderr.write('You must either specify both paswords, or none at all.') + sys.exit(1) + self.process_user('admin-user', write_access=True, pwd=admin_pw) + self.process_user('readonly-user', pwd=readonly_pw) self.stdout.write('Passwords updated succesfully!') - def process_user(self, username, write_access=False): + def process_user(self, username, write_access=False, pwd=None): """ Get or create user, set password and set permissions. @@ -46,7 +56,8 @@ def process_user(self, username, write_access=False): self.stdout.write('--> User "%s" not found. Creating new user.' % username) # Set password - pwd = getpass('--> Please enter a new password for %s: ' % username) + if pwd is None: + pwd = getpass('--> Please enter a new password for %s: ' % username) user.set_password(pwd) user.save() From 7ee3c25db653edead4ef154d206c010bfb46762d Mon Sep 17 00:00:00 2001 From: Jonas Furrer Date: Thu, 29 May 2014 12:42:33 +0200 Subject: [PATCH 17/51] Attached event to datepicker button --- apps/swid/static/js/swid-log.js | 8 ++++++++ apps/swid/templates/swid/swid_log.html | 4 ++-- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/apps/swid/static/js/swid-log.js b/apps/swid/static/js/swid-log.js index 72766c87..9af977a4 100644 --- a/apps/swid/static/js/swid-log.js +++ b/apps/swid/static/js/swid-log.js @@ -22,6 +22,14 @@ var initDateTimePicker = function() { }); fromPicker.datepicker("setDate", '-1w'); toPicker.datepicker("setDate", new Date()); + + $("#from-btn").click(function () { + fromPicker.datepicker("show"); + }); + + $("#to-btn").click(function () { + toPicker.datepicker("show"); + }); }; var initPresetSelect = function() { diff --git a/apps/swid/templates/swid/swid_log.html b/apps/swid/templates/swid/swid_log.html index eb470d9c..5f0eb135 100644 --- a/apps/swid/templates/swid/swid_log.html +++ b/apps/swid/templates/swid/swid_log.html @@ -26,14 +26,14 @@

{% trans 'Set date range' %}

- +
- +
From 2f12465c83dbf7fe0230d8716de9f99d01a17582 Mon Sep 17 00:00:00 2001 From: Danilo Bargen Date: Thu, 29 May 2014 14:40:51 +0200 Subject: [PATCH 18/51] Added sorting to file paging --- apps/filesystem/paging.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/filesystem/paging.py b/apps/filesystem/paging.py index 9dde1f38..150d9004 100644 --- a/apps/filesystem/paging.py +++ b/apps/filesystem/paging.py @@ -12,9 +12,9 @@ def file_list_producer(from_idx, to_idx, filter_query, dynamic_params=None, static_params=None): - file_list = File.objects.all() + file_list = File.objects.all().order_by('directory', 'name') if filter_query: - file_list = File.filter(filter_query) + file_list = File.filter(filter_query, order_by=('directory', 'name')) return file_list[from_idx:to_idx] From 1793786771c241332e3a1a43fe32c8d9af4f2a74 Mon Sep 17 00:00:00 2001 From: Danilo Bargen Date: Thu, 29 May 2014 16:08:05 +0200 Subject: [PATCH 19/51] Added markdown as dependency This is used to render API endpoint documentation. --- requirements.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/requirements.txt b/requirements.txt index 4ead456b..17289a34 100644 --- a/requirements.txt +++ b/requirements.txt @@ -13,3 +13,4 @@ lxml==3.3.5 djangorestframework==2.3.13 django-filter==0.7 djangorestframework-camel-case==0.1.3 +Markdown==2.4.1 From 9076340e9c4c53dcfbd830eadb4445260f5d0328 Mon Sep 17 00:00:00 2001 From: Danilo Bargen Date: Thu, 29 May 2014 19:50:58 +0200 Subject: [PATCH 20/51] Added --no-cov option to runtests.py --- README.rst | 16 ++++++++++++++++ runtests.py | 17 ++++++++++++----- 2 files changed, 28 insertions(+), 5 deletions(-) diff --git a/README.rst b/README.rst index 8d352570..c1626e31 100644 --- a/README.rst +++ b/README.rst @@ -106,6 +106,22 @@ Run the tests:: ./runtests.py +Run a specific test file:: + + ./runtests.py tests/ + +Run only tests matching a specific pattern:: + + ./runtests.py -k + +Run only tests that failed the last time:: + + ./runtests.py --lf + +Run tests without coverage:: + + ./runtests.py --no-cov + Setup a database with test data:: $ ./manage.py shell diff --git a/runtests.py b/runtests.py index 0c31e39d..dd4db149 100755 --- a/runtests.py +++ b/runtests.py @@ -8,9 +8,15 @@ # Environment variables os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings') + measure_coverage = True + if '--no-cov' in sys.argv: + measure_coverage = False + sys.argv.remove('--no-cov') + # Start coverage tracking - cov = coverage.coverage() - cov.start() + if measure_coverage: + cov = coverage.coverage() + cov.start() # Run pytest if len(sys.argv) > 1: @@ -19,8 +25,9 @@ code = pytest.main(['tests', 'apps']) # Show coverage report - cov.stop() - cov.save() - cov.report() + if measure_coverage: + cov.stop() + cov.save() + cov.report() sys.exit(code) From 9e70f7020042f5d8c5f84dcb4614221ca101b2ca Mon Sep 17 00:00:00 2001 From: Danilo Bargen Date: Thu, 29 May 2014 20:43:04 +0200 Subject: [PATCH 21/51] Use data param for submitting a SWID measurement or tag --- apps/api/utils.py | 24 ++++++++++ apps/swid/ajax.py | 2 +- apps/swid/api_views.py | 84 +++++++++++++++++++++++--------- tests/test_api.py | 100 +++++++++++++++++++++++++++++---------- tests/test_auth.py | 2 +- tests/test_filesystem.py | 2 +- tests/test_policies.py | 2 +- 7 files changed, 164 insertions(+), 52 deletions(-) create mode 100644 apps/api/utils.py diff --git a/apps/api/utils.py b/apps/api/utils.py new file mode 100644 index 00000000..d4980363 --- /dev/null +++ b/apps/api/utils.py @@ -0,0 +1,24 @@ +# -*- coding: utf-8 -*- +""" +Helper functions used for API-related tasks. +""" +from __future__ import print_function, division, absolute_import, unicode_literals + +from rest_framework.response import Response + + +def make_message(message, status_code): + """ + Generate and return an API Response. + + Args: + message: + The message to be returned in the response. + status_code: + The HTTP status code for the response. + + Returns: + A :class:`rest_framework.response.Response` instance. + + """ + return Response({'detail': message}, status=status_code) diff --git a/apps/swid/ajax.py b/apps/swid/ajax.py index e2502942..0c2957b5 100644 --- a/apps/swid/ajax.py +++ b/apps/swid/ajax.py @@ -97,4 +97,4 @@ def session_info(request, session_id): detail = {'id': session.pk, 'time': localtime(session.time).strftime('%b %d %H:%M:%S %Y')} - return json.dumps(detail) \ No newline at end of file + return json.dumps(detail) diff --git a/apps/swid/api_views.py b/apps/swid/api_views.py index 034c48e2..cf89345a 100644 --- a/apps/swid/api_views.py +++ b/apps/swid/api_views.py @@ -10,6 +10,7 @@ from .models import Entity, Tag from apps.core.models import Session +from apps.api.utils import make_message class EntityViewSet(viewsets.ReadOnlyModelViewSet): @@ -23,26 +24,56 @@ class TagViewSet(viewsets.ReadOnlyModelViewSet): filter_fields = ('package_name', 'version', 'unique_id') +def validate_data_param(request, list_name): + """ + Validate data for API views that expect a data=[] like parameter. + + If validation fails, a ValueError is raised, with the response as exception + message. Otherwise, the list is returned. + + """ + if hasattr(request.DATA, 'getlist'): + items = request.DATA.getlist('data') + elif hasattr(request.DATA, 'get'): + items = request.DATA.get('data') + else: + response = make_message('Missing "data" parameter', status.HTTP_400_BAD_REQUEST) + raise ValueError(response) + if items is None: + response = make_message('Missing "data" parameter', status.HTTP_400_BAD_REQUEST) + raise ValueError(response) + if not items: + response = make_message('No %s submitted' % list_name, status.HTTP_400_BAD_REQUEST) + raise ValueError(response) + if not isinstance(items, list): + msg = 'The submitted "data" parameter does not contain a list' + response = make_message(msg, status.HTTP_400_BAD_REQUEST) + raise ValueError(response) + return items + + class TagAddView(views.APIView): """ Read the submitted SWID XML tags, parse them and store them into the database. - The SWID tags should be submitted in a JSON formatted list, with the - ``Content-Type`` header set to ``application/json; charset=utf-8``. + You can either send data to this endpoint in + `application/x-www-form-urlencoded` encoding + + data='tag-xml-1'&data='tag-xml-2'&data='tag-xml-3' + + ...or you can use `application/json` encoding: + + {"data": ["tag-xml-1", "tag-xml-2", "tag-xml-3"]} """ parser_classes = (JSONParser,) # Only JSON data is supported def post(self, request, format=None): - # Validate request data - tags = request.DATA - if not isinstance(tags, list): - data = { - 'status': 'error', - 'message': 'Request body must be JSON formatted list of XML tags.', - } - return Response(data, status=status.HTTP_400_BAD_REQUEST) + try: + tags = validate_data_param(request, 'SWID tags') + except ValueError as e: + return e.message # Process tags stats = {'added': 0, 'replaced': 0} @@ -50,11 +81,9 @@ def post(self, request, format=None): try: tag, replaced = utils.process_swid_tag(tag) except XMLSyntaxError: - data = {'status': 'error', 'message': 'Invalid XML'} - return Response(data, status=status.HTTP_400_BAD_REQUEST) + return make_message('Invalid XML', status.HTTP_400_BAD_REQUEST) except ValueError as e: - data = {'status': 'error', 'message': unicode(e)} - return Response(data, status=status.HTTP_400_BAD_REQUEST) + return make_message(unicode(e), status.HTTP_400_BAD_REQUEST) else: # Update stats if replaced: @@ -62,11 +91,8 @@ def post(self, request, format=None): else: stats['added'] += 1 - data = { - 'status': 'success', - 'message': 'Added {0[added]} SWID tags, replaced {0[replaced]} SWID tags.'.format(stats), - } - return Response(data, status=status.HTTP_200_OK) + msg = 'Added {0[added]} SWID tags, replaced {0[replaced]} SWID tags.'.format(stats) + return make_message(msg, status.HTTP_200_OK) class SwidMeasurementView(views.APIView): @@ -76,12 +102,24 @@ class SwidMeasurementView(views.APIView): If no corresponding tag is available for one or more software-ids, return these software-ids with HTTP status code 412 Precondition failed. - This view is defined on a session detail page. The ``pk`` argument is the + This view is defined on a session detail page. The `pk` argument is the session ID. + You can either send data to this endpoint in `application/x-www-form-urlencoded` encoding + + data='software-id-1'&data='software-id-2'&data='software-id-n' + + ...or you can use `application/json` encoding: + + {"data": ["software-id-1", "software-id-2", "software-id-n"]} + """ def post(self, request, pk, format=None): - software_ids = request.DATA + try: + software_ids = validate_data_param(request, 'software IDs') + except ValueError as e: + return e.message + missing_tags = [] found_tags = Tag.objects.filter(software_id__in=software_ids) @@ -99,7 +137,7 @@ def post(self, request, pk, format=None): try: session = Session.objects.get(pk=pk) except Session.DoesNotExist: - data = {'status': 'error', 'message': 'Session with id "%s" not found.' % pk} - return Response(data=data, status=status.HTTP_404_NOT_FOUND) + msg = 'Session with id "%s" not found' % pk + return make_message(msg, status.HTTP_404_NOT_FOUND) utils.chunked_bulk_create(session.tag_set, found_tags, 980) return Response(data=[], status=status.HTTP_200_OK) diff --git a/tests/test_api.py b/tests/test_api.py index ff1cf0ea..c44ac3a6 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -9,8 +9,9 @@ from django.utils import timezone from rest_framework.test import APIClient +from rest_framework import status -from .test_swid import swidtag +from .test_swid import swidtag # NOQA from apps.swid import utils from apps.swid.models import Tag from apps.core.models import Session @@ -18,6 +19,9 @@ @pytest.fixture def api_client(transactional_db): + """ + Return an authenticated API client. + """ user = User.objects.create_superuser(username='api-test', password='api-test', email="api-test@example.com") user.is_staff = True @@ -27,24 +31,71 @@ def api_client(transactional_db): return client -@pytest.mark.django_db +@pytest.fixture +def session(transactional_db): + """ + Generate a session object with pk=1 and no associated tags. + """ + time = timezone.now() + session = mommy.make(Session, id=1, time=time) + session.tag_set.clear() + return session + + +@pytest.mark.parametrize(['url', 'list_name'], [ + (reverse('session-swid-measurement', args=[1]), 'software IDs'), + (reverse('swid-add-tags'), 'SWID tags'), +]) +def test_data_param_validation(api_client, session, url, list_name): + + def validate(response, status_code, message): + assert response.status_code == status_code + assert response.data['detail'] == message + + def json_request(data, encode=True): + if encode: + return api_client.post(url, data, format='json') + return api_client.post(url, data, content_type='application/json') + + def form_request(data, encode=True): + if encode: + return api_client.post(url, data, format='multipart') + return api_client.post(url, data, content_type='application/x-www-form-urlencoded') + + # No data + r1 = json_request('[]', encode=False) + r2 = form_request('', encode=False) + # Uncomment the following line if + # https://github.com/tomchristie/django-rest-framework/pull/1608 gets merged + validate(r1, status.HTTP_400_BAD_REQUEST, 'Missing "data" parameter') + validate(r2, status.HTTP_400_BAD_REQUEST, 'No %s submitted' % list_name) + + # Empty data param + data = {'data': []} + r1 = json_request(data) + r2 = form_request(data) + validate(r1, status.HTTP_400_BAD_REQUEST, 'No %s submitted' % list_name) + validate(r2, status.HTTP_400_BAD_REQUEST, 'No %s submitted' % list_name) + + # Invalid data param + data = {'data': 'foo'} + r1 = json_request(data) + validate(r1, status.HTTP_400_BAD_REQUEST, 'The submitted "data" parameter does not contain a list') + + @pytest.mark.parametrize('filename', [ 'strongswan.short.swidtag', ]) -def test_swid_measurement_diff(api_client, swidtag, filename): +def test_swid_measurement_diff(api_client, session, swidtag, filename): software_ids = [ 'regid.2004-03.org.strongswan_debian_7.4-x86_64-cowsay-3.03+dfsg1-4', 'regid.2004-03.org.strongswan_debian_7.4-x86_64-strongswan-4.5.2-1.5+deb7u3' ] - - time = timezone.now() - session = mommy.make(Session, id=1, time=time) - session.tag_set.clear() + data = {'data': software_ids} # make call to get diff - response = api_client.post(reverse('session-swid-measurement', args=[session.id]), software_ids, - format='json') - assert response.status_code == 412 + response = api_client.post(reverse('session-swid-measurement', args=[session.id]), data, format='json') + assert response.status_code == status.HTTP_412_PRECONDITION_FAILED assert len(response.data) == 1 assert software_ids[1] not in response.data assert software_ids[0] in response.data @@ -55,9 +106,8 @@ def test_swid_measurement_diff(api_client, swidtag, filename): utils.process_swid_tag(file.read()) # make call again - response = api_client.post(reverse('session-swid-measurement', args=[session.id]), software_ids, - format='json') - assert response.status_code == 200 + response = api_client.post(reverse('session-swid-measurement', args=[session.id]), data, format='json') + assert response.status_code == status.HTTP_200_OK assert len(response.data) == 0 assert session.tag_set.count() == 2 @@ -67,24 +117,23 @@ def test_swid_measurement_diff(api_client, swidtag, filename): 'strongswan.short.swidtag', ]) def test_diff_on_invalid_session(api_client, swidtag, filename): - software_ids = [ - 'regid.2004-03.org.strongswan_debian_7.4-x86_64-strongswan-4.5.2-1.5+deb7u3' - ] + software_ids = ['regid.2004-03.org.strongswan_debian_7.4-x86_64-strongswan-4.5.2-1.5+deb7u3'] + data = {'data': software_ids} assert Session.objects.filter(pk=1).count() == 0 # make call to get diff - response = api_client.post(reverse('session-swid-measurement', args=[1]), software_ids, - format='json') - assert response.status_code == 404 + response = api_client.post(reverse('session-swid-measurement', args=[1]), data, format='json') + assert response.status_code == status.HTTP_404_NOT_FOUND @pytest.mark.django_db def test_add_single_tag(api_client): with open('tests/test_tags/strongswan.short.swidtag') as f: xml = f.read() - response = api_client.post(reverse('swid-add-tags'), [xml], format='json') - assert response.status_code == 200 + data = {'data': [xml]} + response = api_client.post(reverse('swid-add-tags'), data, format='json') + assert response.status_code == status.HTTP_200_OK sw_id = "regid.2004-03.org.strongswan_debian_7.4-x86_64-strongswan-4.5.2-1.5+deb7u3" assert Tag.objects.filter(software_id=sw_id).exists() @@ -99,8 +148,9 @@ def test_add_existing_tag(api_client, swidtag, filename): with open('tests/test_tags/strongswan.full.swidtag.singleentity') as f: xml = f.read() - response = api_client.post(reverse('swid-add-tags'), [xml], format='json') - assert response.status_code == 200 + data = {'data': [xml]} + response = api_client.post(reverse('swid-add-tags'), data, format='json') + assert response.status_code == status.HTTP_200_OK tag = Tag.objects.get( software_id="regid.2004-03.org.strongswan_debian_7.4-x86_64-strongswan-4.5.2-1.5+deb7u3") @@ -117,9 +167,9 @@ def test_invalid_tag(api_client, filename): with open('tests/test_tags/invalid_tags/%s' % filename) as f: xml = f.read() response = api_client.post(reverse('swid-add-tags'), [xml], format='json') - assert response.status_code == 400 # Bad request + assert response.status_code == status.HTTP_400_BAD_REQUEST def test_invalid_xml(api_client): response = api_client.post(reverse('swid-add-tags'), [" Date: Thu, 29 May 2014 10:02:43 +0200 Subject: [PATCH 22/51] Paging for dynamic tables --- apps/devices/ajax.py | 15 +- apps/front/ajax.py | 3 + apps/front/static/css/base.css | 13 +- apps/front/static/js/paging.js | 13 ++ apps/front/utils.py | 12 ++ apps/swid/ajax.py | 138 +++++++------ apps/swid/models.py | 17 +- apps/swid/paging.py | 182 +++++++++++++++++- apps/swid/static/js/swid-inventory.js | 48 +---- apps/swid/static/js/swid-log.js | 90 ++------- .../swid/paging/swid_inventory_list.html | 43 +++++ .../templates/swid/paging/swid_log_list.html | 39 ++++ apps/swid/templates/swid/swid_inventory.html | 25 +-- apps/swid/templates/swid/swid_log.html | 27 +-- tests/test_ajax.py | 97 ---------- tests/test_auth.py | 3 +- tests/test_swid.py | 150 ++++++++++++++- 17 files changed, 568 insertions(+), 347 deletions(-) create mode 100644 apps/front/utils.py create mode 100644 apps/swid/templates/swid/paging/swid_inventory_list.html create mode 100644 apps/swid/templates/swid/paging/swid_log_list.html diff --git a/apps/devices/ajax.py b/apps/devices/ajax.py index d465a829..aa922c55 100644 --- a/apps/devices/ajax.py +++ b/apps/devices/ajax.py @@ -4,10 +4,10 @@ import json from dajaxice.decorators import dajaxice_register -from django.utils.timezone import localtime from apps.core.decorators import ajax_login_required from .models import Device +from apps.front.utils import local_dtstring @dajaxice_register @@ -16,9 +16,14 @@ def sessions_for_device(request, device_id, date_from, date_to): device = Device.objects.get(pk=device_id) sessions = device.get_sessions_in_range(date_from, date_to) - data = {'sessions': [ - {'id': s.id, 'time': localtime(s.time).strftime('%b %d %H:%M:%S %Y')} - for s in sessions - ]} + data = { + 'sessions': [ + { + 'id': s.id, + 'time': local_dtstring(s.time) + } + for s in sessions + ] + } return json.dumps(data) diff --git a/apps/front/ajax.py b/apps/front/ajax.py index 9ae14fc0..50a0569d 100644 --- a/apps/front/ajax.py +++ b/apps/front/ajax.py @@ -9,6 +9,7 @@ from apps.core.decorators import ajax_login_required from . import paging as paging_functions from apps.swid.paging import regid_detail_paging, regid_list_paging, swid_list_paging +from apps.swid.paging import swid_inventory_list_paging, swid_log_list_paging from apps.filesystem.paging import dir_list_paging, file_list_paging from apps.policies.paging import policy_list_paging, enforcement_list_paging from apps.packages.paging import package_list_paging @@ -66,6 +67,8 @@ def paging(request, config_name, current_page, filter_query, pager_id, producer_ 'device_list_config': device_list_paging, 'product_list_config': product_list_paging, 'device_session_list_config': device_session_list_paging, + 'swid_inventory_list_config': swid_inventory_list_paging, + 'swid_log_list_config': swid_log_list_paging, } conf = paging_conf_dict[config_name] diff --git a/apps/front/static/css/base.css b/apps/front/static/css/base.css index fb614f94..583d96f4 100644 --- a/apps/front/static/css/base.css +++ b/apps/front/static/css/base.css @@ -130,10 +130,6 @@ pre.raw-xml { font-size: 10px; } -.displ-none { - display: none; -} - .table.statsTable td.title { width: 290px; } @@ -147,7 +143,16 @@ th.dateWidth { min-width: 150px; } +td.dateWidthFixed, +th.dateWidthFixed { + width: 150px; +} + .noWrap, .noWrap { white-space: nowrap; +} + +.actionWidth { + width: 90px; } \ No newline at end of file diff --git a/apps/front/static/js/paging.js b/apps/front/static/js/paging.js index 1cc8407e..661df528 100644 --- a/apps/front/static/js/paging.js +++ b/apps/front/static/js/paging.js @@ -48,6 +48,8 @@ var Pager = function() { this.getInitalURLParam(); this.getPage(); + // place handle to the pager object + this.$ctx.data('pager', this); }; this.grabPageParam = function(key, value) { @@ -214,6 +216,17 @@ var Pager = function() { } return true; }; + + this.setProducerArgs = function(argObj) { + this.args = argObj; + }; + + this.reset = function() { + this.currentPageIdx = 0; + if(this.filter) { + this.$filterInput.val(''); + } + }; }; // static initalizer diff --git a/apps/front/utils.py b/apps/front/utils.py new file mode 100644 index 00000000..80d6ec65 --- /dev/null +++ b/apps/front/utils.py @@ -0,0 +1,12 @@ +# -*- coding: utf-8 -*- +from __future__ import print_function, division, absolute_import, unicode_literals + +from django.conf import settings +from django.utils.timezone import localtime + + +def local_dtstring(timestamp): + """ + Return a formatted (strongTNC standard format) date string. + """ + return localtime(timestamp).strftime(settings.DEFAULT_DATETIME_FORMAT_STRING) diff --git a/apps/swid/ajax.py b/apps/swid/ajax.py index 0c2957b5..502dfb04 100644 --- a/apps/swid/ajax.py +++ b/apps/swid/ajax.py @@ -2,99 +2,115 @@ from __future__ import print_function, division, absolute_import, unicode_literals import json - -from django.conf import settings -from django.core.urlresolvers import reverse -from django.utils.timezone import localtime +from collections import Counter from dajaxice.decorators import dajaxice_register from apps.core.decorators import ajax_login_required from apps.core.models import Session +from apps.front.utils import local_dtstring from .models import Tag -from apps.devices.models import Device +from .paging import get_tag_diffs @dajaxice_register @ajax_login_required -def tags_for_session(request, session_id): +def get_tag_stats(request, session_id): + """ + Return some figures regarding installed SWID tags based on a + given session. + + Args: + session_id (int/str): + A session id, might be provided as int or string (javascript) + + Returns: + A JSON object in the following format (example data): + { + "swid-tag-count": 98, + "new-swid-tag-count": 23 + } + + """ try: session = Session.objects.get(pk=session_id) except Session.DoesNotExist: return json.dumps({}) installed_tags = Tag.get_installed_tags_with_time(session) - tags = [ - { - 'name': tag.package_name, - 'version': tag.version, - 'unique-id': tag.unique_id, - 'installed': localtime(session.time).strftime(settings.DEFAULT_DATETIME_FORMAT_STRING), - 'session-id': session.pk, - 'tag-url': reverse('swid:tag_detail', args=[tag.pk]), - } - for tag, session in installed_tags - ] - data = {'swid-tag-count': len(tags), 'swid-tags': tags} + tag_counter = Counter(session.pk for session in installed_tags.values()) + new_tags_count = tag_counter[int(session_id)] + data = {'swid-tag-count': len(installed_tags), 'new-swid-tag-count': new_tags_count} return json.dumps(data) -@dajaxice_register() -def get_tag_log(request, device_id, from_timestamp, to_timestamp): - device = Device.objects.get(pk=device_id) - sessions = device.get_sessions_in_range(from_timestamp, to_timestamp) - - sessions_with_tags = sessions.filter(tag__isnull=False).distinct().order_by('-time') - - diffs = [] - if len(sessions_with_tags) > 1: - for i, session in enumerate(sessions_with_tags, start=1): - if i < len(sessions_with_tags): - prev_session = sessions_with_tags[i] - diff = session_tag_difference(session, prev_session) - diffs.append(diff) +@dajaxice_register +@ajax_login_required +def get_tag_log_stats(request, device_id, from_timestamp, to_timestamp): + """ + Return some figures regarding SWID tags history of given device + in a given timerange. - result = [ - { - 'session_id': d['session'].pk, - 'session_date': localtime(d['session'].time).strftime( - settings.DEFAULT_DATETIME_FORMAT_STRING), - 'added_tags': [{'unique_id': t.unique_id, 'tag_id': t.pk} for t in d['added_tags']], - 'removed_tags': [{'unique_id': t.unique_id, 'tag_id': t.pk} for t in d['removed_tags']], - 'tag_count': len(d['added_tags']) + len(d['removed_tags']), - } - for d in diffs - ] - return json.dumps(result) - else: - return json.dumps([]) + Args: + device_id (int): + from_timestamp (int): + Start time of the range, in Unix time -def session_tag_difference(curr_session, prev_session): - curr_tag_ids = curr_session.tag_set.values_list('id', flat=True).order_by('id') - prev_tag_ids = prev_session.tag_set.values_list('id', flat=True).order_by('id') + to_timestamp (int): + Last time of the range, in Unix time - added_ids = list(set(curr_tag_ids) - set(prev_tag_ids)) - removed_ids = list(set(prev_tag_ids) - set(curr_tag_ids)) + Returns: + A JSON object in the following format (example data): + { + "session_count": 4, + "first_session": "Nov 17 10:22:12 2014", + "last_session": "Nov 20 10:22:12 2014", + "added_count": 99, + "removed_count": 33 + } - added_tags = Tag.objects.filter(id__in=added_ids) - removed_tags = Tag.objects.filter(id__in=removed_ids) + """ + diffs = get_tag_diffs(device_id, from_timestamp, to_timestamp) + if diffs: + added_count = 0 + removed_count = 0 + sessions = set() + for diff in diffs: + sessions.add(diff.session) + if diff.action == '+': + added_count += 1 + else: + removed_count += 1 + + s = sorted(sessions, key=lambda sess: sess.time) + first_session = s[0] + last_session = s[-1] + + result = { + 'session_count': len(sessions), + 'first_session': local_dtstring(first_session.time), + 'last_session': local_dtstring(last_session.time), + 'added_count': added_count, + 'removed_count': removed_count, + } - return { - 'session': curr_session, - 'added_tags': added_tags, - 'removed_tags': removed_tags - } + return json.dumps(result) + else: + return json.dumps({}) -@dajaxice_register() +@dajaxice_register +@ajax_login_required def session_info(request, session_id): try: session = Session.objects.get(pk=session_id) except Session.DoesNotExist: return json.dumps({}) - detail = {'id': session.pk, - 'time': localtime(session.time).strftime('%b %d %H:%M:%S %Y')} + detail = { + 'id': session.pk, + 'time': local_dtstring(session.time) + } return json.dumps(detail) diff --git a/apps/swid/models.py b/apps/swid/models.py index ce83f51a..38804a8d 100644 --- a/apps/swid/models.py +++ b/apps/swid/models.py @@ -51,18 +51,19 @@ def get_installed_tags_with_time(cls, session): The session object Returns: - A list of tuples ``(tag, time)``. The ``tag`` is a :class:`Tag` - instance, the ``time`` is the datetime object when the tag was - first measured to be installed. + A dictionary ``{tag1: session, tag2: session, ...}``. + The ``tag`` is a :class:`Tag` instance, the ``session`` is the session + when the tag was first measured to be installed. """ - device_sessions = session.device.sessions.filter(time__lte=session.time).order_by('time') - tags = {} + device_sessions = session.device.sessions.filter(time__lte=session.time).order_by('-time') + installed_tags = {t: session for t in session.tag_set.all()} for session in device_sessions.all().prefetch_related('tag_set'): for tag in session.tag_set.all(): - if tag not in tags: - tags[tag] = session - return list(tags.items()) + if tag in installed_tags: + installed_tags[tag] = session + + return installed_tags def get_devices_with_reported_session(self): devices_dict = {} diff --git a/apps/swid/paging.py b/apps/swid/paging.py index 1ec2e137..87334142 100644 --- a/apps/swid/paging.py +++ b/apps/swid/paging.py @@ -2,8 +2,14 @@ from __future__ import print_function, division, absolute_import, unicode_literals import math +from collections import OrderedDict, namedtuple + +from django.conf import settings +from django.core.urlresolvers import reverse from .models import Entity, Tag +from apps.core.models import Session +from apps.devices.models import Device from apps.front.paging import ProducerFactory # PAGING PRODUCER @@ -30,14 +36,166 @@ def entity_swid_stat_producer(page_size, filter_query, dynamic_params=None, stat return math.ceil(count / page_size) +def swid_inventory_list_producer(from_idx, to_idx, filter_query, dynamic_params, static_params=None): + if not dynamic_params: + return [] + session_id = dynamic_params.get('session_id') + installed_tags = list(get_installed_tags_dict(session_id, filter_query).items())[from_idx:to_idx] + + tags = [ + { + 'added_now': int(session_id) == session.pk, + 'tag': tag, + 'session': session, + 'tag_url': reverse('swid:tag_detail', args=[tag.pk]), + } + for tag, session in installed_tags + ] + return tags + + +def swid_inventory_stat_producer(page_size, filter_query, dynamic_params=None, static_params=None): + if not dynamic_params: + return 0 + session_id = dynamic_params.get('session_id') + installed_tags = get_installed_tags_dict(session_id, filter_query) + return math.ceil(len(installed_tags) / page_size) + + +def get_installed_tags_dict(session_id, filter_query): + """ + Return a dict of tags which are installed at the + point of the given session, furthermore the session in + which the tag was reported is provided as well. + + Args: + session_id (int): + The session to be queried + + filter_query (str): + Filter the tags (unique_id) by this string + + Returns: + A dictionary with the reported tag as key and the session in + which it was reported as value: + { + tag: session, + tag: session, + ..., + } + + """ + session = Session.objects.get(pk=session_id) + installed_tags = Tag.get_installed_tags_with_time(session) + if filter_query: + installed_tags = {t: s for t, s in installed_tags.iteritems() + if filter_query.lower() in t.unique_id.lower()} + return installed_tags + + +def swid_log_list_producer(from_idx, to_idx, filter_query, dynamic_params, static_params=None): + if not dynamic_params: + return [] + device_id = dynamic_params.get('device_id') + from_timestamp = dynamic_params.get('from_timestamp') + to_timestamp = dynamic_params.get('to_timestamp') + + diffs = get_tag_diffs(device_id, from_timestamp, to_timestamp)[from_idx:to_idx] + + result = OrderedDict() + for diff in diffs: + tag = diff.tag + tag.added = diff.action == '+' + if diff.session not in result: + result[diff.session] = [tag] + else: + result[diff.session].append(tag) + return result + + +def swid_log_stat_producer(page_size, filter_query, dynamic_params=None, static_params=None): + if not dynamic_params: + return 0 + device_id = dynamic_params.get('device_id') + from_timestamp = dynamic_params.get('from_timestamp') + to_timestamp = dynamic_params.get('to_timestamp') + diffs = get_tag_diffs(device_id, from_timestamp, to_timestamp) + return math.ceil(len(diffs) / page_size) + + +def get_tag_diffs(device_id, from_timestamp, to_timestamp): + """ + Get differences of installed SWID tags between all sessions of the + given device in the given timerange (see `session_tag_difference`). + """ + device = Device.objects.get(pk=device_id) + sessions = device.get_sessions_in_range(from_timestamp, to_timestamp) + + sessions_with_tags = sessions.filter(tag__isnull=False).distinct().order_by('-time') + + diffs = [] + # diff only possible if more than 1 session is selected + if len(sessions_with_tags) > 1: + for i, session in enumerate(sessions_with_tags, start=1): + if i < len(sessions_with_tags): + prev_session = sessions_with_tags[i] + diff = session_tag_difference(session, prev_session) + diffs.extend(diff) + + return diffs + + +def session_tag_difference(curr_session, prev_session): + """ + Calculate the difference of the installed SWID tags between + the two given sessions. + + Args: + curr_session (apps.core.models.Session): + The current session + + prev_session (apps.core.models.Session): + The session before + + Returns: + A list of named tuples, consisting of a session object, + a tag object and an action (str) which is either '+' for + added or '-' for removed: + [ + (session: session, action: '+', tag: tag), + (session: session, action: '-', tag: tag), + ..., + ] + + """ + curr_tag_ids = curr_session.tag_set.values_list('id', flat=True).order_by('id') + prev_tag_ids = prev_session.tag_set.values_list('id', flat=True).order_by('id') + + added_ids = set(curr_tag_ids) - set(prev_tag_ids) + removed_ids = set(prev_tag_ids) - set(curr_tag_ids) + + added_tags = Tag.objects.filter(id__in=added_ids) + removed_tags = Tag.objects.filter(id__in=removed_ids) + + differences = [] + DiffEntry = namedtuple('DiffEntry', ['session', 'action', 'tag']) + for tag in added_tags: + entry = DiffEntry(curr_session, '+', tag) + differences.append(entry) + + for tag in removed_tags: + entry = DiffEntry(curr_session, '-', tag) + differences.append(entry) + + return differences + + # PAGING CONFIGS regid_list_paging = { 'template_name': 'front/paging/default_list', 'list_producer': regid_producer_factory.list(), 'stat_producer': regid_producer_factory.stat(), - 'static_producer_args': None, - 'var_name': 'object_list', 'url_name': 'swid:regid_detail', 'page_size': 50, } @@ -46,8 +204,6 @@ def entity_swid_stat_producer(page_size, filter_query, dynamic_params=None, stat 'template_name': 'front/paging/regid_list_tags', 'list_producer': entity_swid_list_producer, 'stat_producer': entity_swid_stat_producer, - 'static_producer_args': None, - 'var_name': 'object_list', 'url_name': 'swid:tag_detail', 'page_size': 50, } @@ -56,8 +212,22 @@ def entity_swid_stat_producer(page_size, filter_query, dynamic_params=None, stat 'template_name': 'front/paging/default_list', 'list_producer': swid_producer_factory.list(), 'stat_producer': swid_producer_factory.stat(), - 'static_producer_args': None, - 'var_name': 'object_list', + 'url_name': 'swid:tag_detail', + 'page_size': 50, +} + +swid_inventory_list_paging = { + 'template_name': 'swid/paging/swid_inventory_list', + 'list_producer': swid_inventory_list_producer, + 'stat_producer': swid_inventory_stat_producer, + 'url_name': 'swid:tag_detail', + 'page_size': 50, +} + +swid_log_list_paging = { + 'template_name': 'swid/paging/swid_log_list', + 'list_producer': swid_log_list_producer, + 'stat_producer': swid_log_stat_producer, 'url_name': 'swid:tag_detail', 'page_size': 50, } diff --git a/apps/swid/static/js/swid-inventory.js b/apps/swid/static/js/swid-inventory.js index 292d3b06..d76a840d 100644 --- a/apps/swid/static/js/swid-inventory.js +++ b/apps/swid/static/js/swid-inventory.js @@ -2,51 +2,19 @@ var session_data = []; function AjaxTagsLoader() { this.loadTags = function (sessionID) { - ajaxSpinner.enable(); - Dajaxice.apps.swid.tags_for_session(this.fillTable, { + var pager = $('.ajax-paged').data('pager'); + pager.reset(); + pager.setProducerArgs({'session_id': sessionID}); + pager.getPage(); + + Dajaxice.apps.swid.get_tag_stats(this.updateStats, { 'session_id': sessionID }); }; - this.fillTable = function (data) { - var selectedSession = HashQuery.getHashQueryObject()['session-id']; - var table = $("#swid-tags"); - var tableBody = table.find("tbody"); - var rows = ''; - var newCount = 0; - - - tableBody.empty(); - $.each(data['swid-tags'], function (i, record) { - - // Mark tags that were added in the selected session - if (record['session-id'] == selectedSession) { - rows += ""; - rows += ""; - ++newCount; - } - else { - rows += ""; - } - rows += - "" + - record['name'] + - "" + - record['version'] + - ""+ - record['unique-id'] + - "" + - record['installed'] + - ""; - }); - + this.updateStats = function (data) { $("#swid-tag-count").text(data['swid-tag-count']); - $("#swid-newtag-count").text(newCount); - tableBody.append(rows); - table.show(); - ajaxSpinner.disable(); + $("#swid-newtag-count").text(data['new-swid-tag-count']); }; } diff --git a/apps/swid/static/js/swid-log.js b/apps/swid/static/js/swid-log.js index 9af977a4..1147a47f 100644 --- a/apps/swid/static/js/swid-log.js +++ b/apps/swid/static/js/swid-log.js @@ -1,4 +1,3 @@ - $(document).ready(function() { initDateTimePicker(); initPresetSelect(); @@ -53,88 +52,27 @@ var getTagList = function() { var fromTimestamp = Math.floor($('#from').datepicker("getDate").getTime() / 1000); var toTimestamp = Math.floor($('#to').datepicker("getDate").getTime() / 1000); - Dajaxice.apps.swid.get_tag_log(updateTable, { + var pager = $('.ajax-paged', '#logTabelContainer').data('pager'); + pager.reset(); + pager.setProducerArgs({ 'device_id': deviceId, 'from_timestamp': fromTimestamp, 'to_timestamp': toTimestamp }); -}; - -var updateTable = function (data) { - var $noEntryText = $('#noEntryText'); - var $table = $('#swid-tags'); - var $tableBody = $table.find("tbody"); - - var addedCount = 0; - var removedCount = 0; - var sessionCount = 0; - var firstSession = 'None'; - var lastSession = 'None'; - - $tableBody.empty(); - - var rows = document.createDocumentFragment(); - - if(data.length == 0){ - $table.hide(); - $noEntryText.show(); - } else { - $noEntryText.hide(); - $table.show(); - $.each(data, function (i, record) { - var hasSessionLead = false; - $.each(record['added_tags'], function (i, tag) { - var tr = $(""); - if (i == 0) { - tr = addSessionCell(tr, record); - hasSessionLead = true; - } - tr = addTagRow(tr, tag, 'ADDED'); - tr.appendTo(rows); - }); - $.each(record['removed_tags'], function (i, tag) { - var tr = $(""); - if (!hasSessionLead && i == 0) { - tr = addSessionCell(tr, record); - } - tr = addTagRow(tr, tag, 'REMOVED'); - tr.appendTo(rows); - }); - addedCount += record['added_tags'].length; - removedCount += record['removed_tags'].length; - }); - firstSession = data.shift()['session_date']; - lastSession = data.pop()['session_date']; - sessionCount = data.length; - $tableBody.append(rows); - } - updateStats(addedCount, removedCount, sessionCount, firstSession, lastSession); -}; - -var addTagRow = function(tr, tag, type) { - tr.append($("").html(type)); - var tagLink = $('', { - href: '/swid-tags/' + tag['tag_id'], - text: tag['unique_id'] - }); - tr.append($("").append(tagLink)); - return tr; -}; + pager.getPage(); -var addSessionCell = function(tr, record) { - var sessionLink = $('', { - href: '/sessions/' + record['session_id'], - text: record['session_date'] + Dajaxice.apps.swid.get_tag_log_stats(updateStats, { + 'device_id': deviceId, + 'from_timestamp': fromTimestamp, + 'to_timestamp': toTimestamp }); - tr.append($("").attr('rowspan', record['tag_count']).append(sessionLink)); - return tr; }; -var updateStats = function(addedCount, removedCount, sessionCount, firstSession, lastSession) { +var updateStats = function(data) { var $statsTable = $('.statsTable'); - $('.sessionCount', $statsTable).text(sessionCount); - $('.addedTags', $statsTable).text(addedCount); - $('.removedTags', $statsTable).text(removedCount); - $('.firstSession', $statsTable).text(firstSession); - $('.lastSession', $statsTable).text(lastSession); + $('.sessionCount', $statsTable).text(data['session_count']); + $('.addedTags', $statsTable).text(data['added_count']); + $('.removedTags', $statsTable).text(data['removed_count']); + $('.firstSession', $statsTable).text(data['first_session']); + $('.lastSession', $statsTable).text(data['last_session']); }; \ No newline at end of file diff --git a/apps/swid/templates/swid/paging/swid_inventory_list.html b/apps/swid/templates/swid/paging/swid_inventory_list.html new file mode 100644 index 00000000..b0524325 --- /dev/null +++ b/apps/swid/templates/swid/paging/swid_inventory_list.html @@ -0,0 +1,43 @@ +{% load i18n %} +{% if object_list %} + + + + + + + + + + + + + {% for obj in object_list %} + + {% if obj.added_now %} + + {% else %} + + {% endif %} + + + + + + {% endfor %} + +
+ {% trans 'Package name' %} + + {% trans 'Version' %} + + {% trans 'Unique ID' %} + + {% trans 'First reported' %} +
{{ obj.tag.package_name }}{{ obj.tag.version }}{{ obj.tag.unique_id }}{{ obj.session.time|date:"M d H:i:s Y" }}
+{% else %} +

+ There are no entries. +

+{% endif %} diff --git a/apps/swid/templates/swid/paging/swid_log_list.html b/apps/swid/templates/swid/paging/swid_log_list.html new file mode 100644 index 00000000..31a2ed59 --- /dev/null +++ b/apps/swid/templates/swid/paging/swid_log_list.html @@ -0,0 +1,39 @@ +{% load i18n %} +{% if object_list %} + + + + + + + + + + + {% for session, tags in object_list.iteritems %} + {% for tag in tags %} + + {% if forloop.first %} + + {% endif %} + + + + {% endfor %} + {% endfor %} + +
+ {% trans 'Session' %} + + {% trans 'Action' %} + + {% trans 'Tag' %} +
+ {{ session.time|date:"M d H:i:s Y" }} + {{ tag.added|yesno:"ADDED,REMOVED" }}{{ tag.unique_id }}
+{% else %} +

+ There are no entries. +

+{% endif %} diff --git a/apps/swid/templates/swid/swid_inventory.html b/apps/swid/templates/swid/swid_inventory.html index 18c7c7c8..5135b58f 100644 --- a/apps/swid/templates/swid/swid_inventory.html +++ b/apps/swid/templates/swid/swid_inventory.html @@ -2,6 +2,7 @@ {% load i18n %} {% load permissions %} +{% load paged_block %} {% block title %} SWID {% trans "Inventory for " %} {{ object.description }}{% endblock %} @@ -100,29 +101,7 @@

{% trans 'Measured Software Tags' %}

- - - - - - - - - - - - - -
- Packagename - - Version - - Unique ID - - First reported -
+ {% paged_block config_name='swid_inventory_list_config' with_filter=True %}
diff --git a/apps/swid/templates/swid/swid_log.html b/apps/swid/templates/swid/swid_log.html index 5f0eb135..0f8d7e39 100644 --- a/apps/swid/templates/swid/swid_log.html +++ b/apps/swid/templates/swid/swid_log.html @@ -2,6 +2,7 @@ {% load i18n %} {% load permissions %} +{% load paged_block %} {% block title %} SWID {% trans "log for " %} {{ object.description }}{% endblock %} @@ -82,31 +83,9 @@

{% trans 'Changes summary' %}

-
+

{% trans 'Change log' %}

- - - - - - - - - - - {% comment %} - currently filled via javascript - {% endcomment %} - -
- {% trans 'Session' %} - - {% trans 'Action' %} - - {% trans 'Tag' %} -
-

{% trans 'No entries' %}

+ {% paged_block config_name='swid_log_list_config' %}
{% endblock %} diff --git a/tests/test_ajax.py b/tests/test_ajax.py index fbb6b103..66b96dd1 100644 --- a/tests/test_ajax.py +++ b/tests/test_ajax.py @@ -8,16 +8,12 @@ from django.contrib.auth.models import User from django.utils import timezone -from django.utils.dateformat import format -from django.utils.timezone import localtime import pytest from model_mommy import mommy from apps.core.models import Session from apps.filesystem.models import File, Directory -from apps.swid.models import Tag -from apps.devices.models import Device ### Helper functions ### @@ -177,96 +173,3 @@ def test_sessions(get_sessions, from_diff_to_now, to_diff_to_now, expected): results = get_sessions(1, date_from, date_to) assert len(results) == expected, '%i instead of %i sessions found in the given time range' % \ (len(results), expected) - - -def test_tags_for_session(db, client): - """ - Test whether the ``tags_for_session`` ajax endpoint works properly. - """ - # Prepare 4 sessions and related tags - now = localtime(timezone.now()) - for i in range(1, 5): - time = now + timedelta(days=i) - session = mommy.make(Session, pk=i, time=time, device__id=1) - tag = mommy.make(Tag, package_name='name%d' % i) - tag.sessions.add(session) - - # The second session has two tags - tag = mommy.make(Tag, package_name='name5') - tag.sessions.add(2) - - # Test first session - payload = {'session_id': 1} - data = ajax_request(client, 'apps.swid.tags_for_session', payload) - assert data['swid-tag-count'] == 1 - assert len(data['swid-tags']) == 1 - assert data['swid-tags'][0]['name'] == 'name1' - assert data['swid-tags'][0]['installed'] == (now + timedelta(days=1)).strftime('%b %d %H:%M:%S %Y') - - # Test second session - payload = {'session_id': 2} - data = ajax_request(client, 'apps.swid.tags_for_session', payload) - assert data['swid-tag-count'] == 3 - assert len(data['swid-tags']) == 3 - names = sorted([t['name'] for t in data['swid-tags']]) - assert names == sorted(['name1', 'name2', 'name5']) - dates = sorted([t['installed'] for t in data['swid-tags']]) - date1 = (now + timedelta(days=1)).strftime('%b %d %H:%M:%S %Y') - date2 = (now + timedelta(days=2)).strftime('%b %d %H:%M:%S %Y') - assert dates == [date1, date2, date2] - - # Test all sessions - payload = {'session_id': 4} - data = ajax_request(client, 'apps.swid.tags_for_session', payload) - assert data['swid-tag-count'] == 5 - assert len(data['swid-tags']) == 5 - - -def test_swid_log(transactional_db, client): - now = timezone.now() - s1 = mommy.make(Session, id=1, identity__data="tester", time=now - timedelta(days=3), device__id=1) - s2 = mommy.make(Session, id=2, identity__data="tester", time=now - timedelta(days=1), device__id=1) - s3 = mommy.make(Session, id=3, identity__data="tester", time=now + timedelta(days=1), device__id=1) - mommy.make(Session, id=7, identity__data="tester", time=now + timedelta(days=2), device__id=1) - s4 = mommy.make(Session, id=4, identity__data="tester", time=now + timedelta(days=3), device__id=1) - mommy.make(Session, id=5, identity__data="tester", time=now - timedelta(days=4), device__id=1) - mommy.make(Session, id=6, identity__data="tester", time=now + timedelta(days=4), device__id=1) - - tag1 = mommy.make(Tag, id=1) - tag2 = mommy.make(Tag, id=2) - tag3 = mommy.make(Tag, id=3) - tag4 = mommy.make(Tag, id=4) - tag5 = mommy.make(Tag, id=5) - tag6 = mommy.make(Tag, id=6) - tag7 = mommy.make(Tag, id=7) - - # intital set: tag 1-4 - s1.tag_set.add(tag1, tag2, tag3, tag4) - # s2, added: tag5; - s2.tag_set.add(tag1, tag2, tag3, tag4, tag5) - # s3, removed: tag1; - s3.tag_set.add(tag2, tag3, tag4, tag5) - # s4 added: tag6, tag7; removed: tag2; - s4.tag_set.add(tag3, tag4, tag5, tag6, tag7) - - from_timestamp = format(now - timedelta(days=3), u'U') - to_timestamp = format(now + timedelta(days=4), u'U') - - payload = { - 'device_id': 1, - 'from_timestamp': int(from_timestamp), - 'to_timestamp': int(to_timestamp), - } - data = ajax_request(client, 'apps.swid.get_tag_log', payload) - - # there shoulb be three results, bc. 4 session in the range have tags - assert len(data) == 3 - # the results shoulb be in chronological order - assert data[0]['session_id'] == 4 - assert data[1]['session_id'] == 3 - assert data[2]['session_id'] == 2 - # checking if removed and added are as expected - assert data[2]['added_tags'][0]['tag_id'] == 5 - assert data[1]['removed_tags'][0]['tag_id'] == 1 - assert data[0]['removed_tags'][0]['tag_id'] == 2 - diff --git a/tests/test_auth.py b/tests/test_auth.py index 6db4a911..3929401b 100644 --- a/tests/test_auth.py +++ b/tests/test_auth.py @@ -125,7 +125,8 @@ def test_write_permission_enforced(client, strongtnc_users, url, method): @pytest.mark.parametrize('endpoint, payload', [ ('apps.filesystem.files_autocomplete', {'search_term': 'bash'}), ('apps.filesystem.directories_autocomplete', {'search_term': 'bash'}), - ('apps.swid.tags_for_session', {'session_id': 1}), + ('apps.swid.get_tag_stats', {'session_id': 1}), + ('apps.swid.get_tag_log_stats', {'device_id': 1, 'date_from': '', 'date_to': ''}), ('apps.devices.sessions_for_device', {'device_id': 1, 'date_from': '', 'date_to': ''}), ('apps.front.paging', {'template': '', 'list_producer': '', 'stat_producer': '', 'var_name': '', 'url_name': '', 'current_page': '', 'page_size': '', 'filter_query': '', 'pager_id': ''}), diff --git a/tests/test_swid.py b/tests/test_swid.py index 2e692df5..f6768507 100644 --- a/tests/test_swid.py +++ b/tests/test_swid.py @@ -1,7 +1,12 @@ # -*- coding: utf-8 -*- from __future__ import print_function, division, absolute_import, unicode_literals +from datetime import timedelta + from django.utils import timezone +from django.utils.timezone import localtime +from django.utils.dateformat import format +from django.conf import settings import pytest from model_mommy import mommy @@ -11,8 +16,8 @@ from apps.swid.models import Tag, EntityRole, Entity from apps.filesystem.models import File, Directory from apps.swid import utils -from apps.swid import views - +from apps.swid.paging import swid_inventory_list_producer, swid_log_list_producer, \ + swid_inventory_stat_producer ### FIXTURES ### @@ -203,3 +208,144 @@ def test_valid_role(value): def test_invalid_role(): with pytest.raises(ValueError): EntityRole.xml_attr_to_choice('licensee') + + +@pytest.fixture +def tags_and_sessions(transactional_db): + now = timezone.now() + s1 = mommy.make(Session, id=1, identity__data="tester", time=now - timedelta(days=3), device__id=1) + s2 = mommy.make(Session, id=2, identity__data="tester", time=now - timedelta(days=1), device__id=1) + s3 = mommy.make(Session, id=3, identity__data="tester", time=now + timedelta(days=1), device__id=1) + mommy.make(Session, id=7, identity__data="tester", time=now + timedelta(days=2), device__id=1) + s4 = mommy.make(Session, id=4, identity__data="tester", time=now + timedelta(days=3), device__id=1) + mommy.make(Session, id=5, identity__data="tester", time=now - timedelta(days=4), device__id=1) + mommy.make(Session, id=6, identity__data="tester", time=now + timedelta(days=4), device__id=1) + + tag1 = mommy.make(Tag, id=1, unique_id='tag1') + tag2 = mommy.make(Tag, id=2) + tag3 = mommy.make(Tag, id=3) + tag4 = mommy.make(Tag, id=4) + tag5 = mommy.make(Tag, id=5, unique_id='tag5') + tag6 = mommy.make(Tag, id=6) + tag7 = mommy.make(Tag, id=7) + + # intital set: tag 1-4 + s1.tag_set.add(tag1, tag2, tag3, tag4) + # s2, added: tag5; + s2.tag_set.add(tag1, tag2, tag3, tag4, tag5) + # s3, removed: tag1; + s3.tag_set.add(tag2, tag3, tag4, tag5) + # s4 added: tag6, tag7; removed: tag2; + s4.tag_set.add(tag3, tag4, tag5, tag6, tag7) + + return { + 'now': now, + 'sessions': [s1, s2, s3, s4], + 'tags': [tag1, tag2, tag3, tag4, tag5, tag6, tag6] + } + + +def test_swid_inventory_list_producer(transactional_db, tags_and_sessions): + + s1 = tags_and_sessions['sessions'][0] + + params = {'session_id': 1} + tags = swid_inventory_list_producer(0, 10, None, params) + assert len(tags) == 4 + assert tags[0]['session'] == s1 + assert tags[0]['added_now'] is True + + params = {'session_id': 2} + tags = swid_inventory_list_producer(0, 10, None, params) + assert len(tags) == 5 + assert tags[0]['added_now'] is False + + params = {'session_id': 4} + tags = swid_inventory_list_producer(0, 10, None, params) + assert len(tags) == 5 + assert tags[0]['session'] == s1 + + params = {'session_id': 4} + tags = swid_inventory_list_producer(0, 1, None, params) + assert len(tags) == 1 # test paging + + params = {'session_id': 4} + tags = swid_inventory_list_producer(0, 10, 'tag5', params) + assert len(tags) == 1 # test filter + assert tags[0]['tag'].unique_id == 'tag5' + + params = None + tags = swid_inventory_list_producer(0, 10, 'tag5', params) + assert len(tags) == 0 + assert tags == [] + + +def test_swid_inventory_stat_producer(transactional_db, tags_and_sessions): + params = {'session_id': 1} + page_count = swid_inventory_stat_producer(1, None, params) + assert page_count == 4 + + page_count = swid_inventory_stat_producer(1, 'tag1', params) + assert page_count == 1 + + page_count = swid_inventory_stat_producer(2, None, params) + assert page_count == 2 + + page_count = swid_inventory_stat_producer(3, None, params) + assert page_count == 2 + + page_count = swid_inventory_stat_producer(4, None, params) + assert page_count == 1 + + page_count = swid_inventory_stat_producer(5, None, params) + assert page_count == 1 + + +def test_swid_log(transactional_db, tags_and_sessions): + now = tags_and_sessions['now'] + + from_timestamp = format(now - timedelta(days=3), u'U') + to_timestamp = format(now + timedelta(days=4), u'U') + + params = { + 'device_id': 1, + 'from_timestamp': int(from_timestamp), + 'to_timestamp': int(to_timestamp), + } + data = swid_log_list_producer(0, 100, None, params) + + s2 = tags_and_sessions['sessions'][1] + s3 = tags_and_sessions['sessions'][2] + s4 = tags_and_sessions['sessions'][3] + + # there should be three results, bc. 4 session in the range have tags + assert len(data) == 3 + + assert len(data[s4]) == 3 + assert len(data[s3]) == 1 + assert len(data[s2]) == 1 + # checking if removed and added are as expected + assert data[s3][0].added is False + assert data[s2][0].added is True + + # test omitted params + data = swid_log_list_producer(0, 100, None, None) + assert data == [] + + +def test_get_installed_tags_with_time(transactional_db, tags_and_sessions): + s1 = tags_and_sessions['sessions'][0] # -3 days old + s2 = tags_and_sessions['sessions'][1] # -1 day old + s3 = tags_and_sessions['sessions'][2] # + 1 day + s4 = tags_and_sessions['sessions'][3] # + 3 days + + tag3 = tags_and_sessions['tags'][2] + tag5 = tags_and_sessions['tags'][4] + + installed_tags = Tag.get_installed_tags_with_time(s4) + # five tags installed in session s4 + assert len(installed_tags) == 5 + # tag3 was installed in session s1 + assert installed_tags[tag3] == s1 + # tag5 was installed in session s2 + assert installed_tags[tag5] == s2 From d40d29e91a7948d06d72dfa9a5f47a02761955c6 Mon Sep 17 00:00:00 2001 From: cfaessler Date: Thu, 29 May 2014 18:42:46 +0200 Subject: [PATCH 23/51] Extended view to allow creation and removal of versions --- apps/front/utils.py | 18 + apps/packages/models.py | 4 +- apps/packages/static/js/packages.js | 63 ++- .../packages/templates/packages/packages.html | 407 +++++++++++------- apps/packages/urls.py | 4 +- apps/packages/views.py | 80 +++- tests/test_auth.py | 3 +- tests/test_packages.py | 59 +++ tests/test_policies.py | 2 +- 9 files changed, 438 insertions(+), 202 deletions(-) create mode 100644 tests/test_packages.py diff --git a/apps/front/utils.py b/apps/front/utils.py index 80d6ec65..5673a1c3 100644 --- a/apps/front/utils.py +++ b/apps/front/utils.py @@ -10,3 +10,21 @@ def local_dtstring(timestamp): Return a formatted (strongTNC standard format) date string. """ return localtime(timestamp).strftime(settings.DEFAULT_DATETIME_FORMAT_STRING) + + +def check_not_empty(value): + """ + Return the value if it is not 'None', None or '' + Otherwise raise an HttpResponseBadRequest with status code 400 + """ + if value == 'None' or value is None or value == '': + raise ValueError('Validation failed') + else: + return value + + +def checkbox_boolean(value): + """ + Convert checkbox form data to boolean + """ + return True if value == 'on' else False diff --git a/apps/packages/models.py b/apps/packages/models.py index 632013d9..6dd11b32 100644 --- a/apps/packages/models.py +++ b/apps/packages/models.py @@ -35,8 +35,8 @@ class Version(models.Model): product = models.ForeignKey('devices.Product', db_column='product', on_delete=models.CASCADE, related_name='versions') release = models.CharField(max_length=255, db_index=True) - security = models.BooleanField(default=0) - blacklist = models.IntegerField(null=True, blank=True) + security = models.BooleanField(default=False) + blacklist = models.BooleanField(default=False) time = EpochField() class Meta: diff --git a/apps/packages/static/js/packages.js b/apps/packages/static/js/packages.js index cf7957d4..62c99278 100644 --- a/apps/packages/static/js/packages.js +++ b/apps/packages/static/js/packages.js @@ -1,10 +1,36 @@ -$(document).ready(function() { +$(document).ready(function () { initValidation(); $('#savePackageButton').on('click', savePackage); - $('.blacklistToggle').on('click', toggle); + $('#addVersion').on('click', function () { + $("#newVersionFormContainer").toggle(); + }); + $('#savePackageChanges').on('click', savePackageChanges); + $('#addVersionSave').on('click', saveNewVersion); }); +function saveNewVersion() { + var $versionForm = $("#newVersionForm"); + if ($versionForm.valid()) { + $versionForm.submit(); + } +} + + +function savePackageChanges() { + var versionFlags = []; + $("tbody", "#versions").find('tr').each(function () { + var row = $(this); + versionFlags.push({ + id: row.prop("id"), + security: row.find(".securityToggle").prop("checked"), + blacklist: row.find(".blacklistToggle").prop("checked") + }); + }); + $("#versionData").val(JSON.stringify(versionFlags)); + $("#packageform").submit(); +} + function savePackage() { var $packageForm = $("#packageform"); if ($packageForm.valid()) { @@ -12,19 +38,6 @@ function savePackage() { } } -function toggle() { - var id = $(this).data('version-id'); - $.ajax({ - url: "/versions/" + id + "/toggle", - statusCode: { - 200: function (data) { - var query = "table#versions > tbody > tr#version" + id + " > td:nth-child(5) > button"; - $(query).text(data) - } - } - }); -} - function initValidation() { $('#packageform').validate({ rules: { @@ -59,4 +72,24 @@ function initValidation() { }, ignore: ":hidden:not(.select2-offscreen)" }); + + $('#newVersionForm').validate({ + rules: { + 'version': { + required: true, + maxlength: 255 + }, + 'product': { + required: true, + regex: /^[0-9]+$/ + } + }, + highlight: function (element) { + $(element).closest('.control-group').removeClass('success').addClass('error'); + }, + success: function (element) { + element.addClass('valid').closest('.control-group').removeClass('error').addClass("invisiblevalid"); + }, + ignore: ":hidden:not(.select2-offscreen)" + }); } diff --git a/apps/packages/templates/packages/packages.html b/apps/packages/templates/packages/packages.html index 1499d829..90ad3246 100644 --- a/apps/packages/templates/packages/packages.html +++ b/apps/packages/templates/packages/packages.html @@ -13,185 +13,266 @@

{{ title }}

{% block content %}
-
-
-

{% trans "Package" %} - {% if 'auth.write_access' in perms %} - - {% endif %} -

-
-
- {% paged_block config_name="package_list_config" with_filter=True %} +
+
+

{% trans "Package" %} + {% if 'auth.write_access' in perms %} + -

+ {% endif %} +

+
+
+ {% paged_block config_name="package_list_config" with_filter=True %} +
+ -
- {% if package %} -
- {% csrf_token %} + +
+ {% if package %} + + + {% csrf_token %} + {% if add %} +

{% trans 'Add new package' %}

+
+ + +
+ + +
+
+ {% if 'auth.write_access' in perms %} +
+
{% if add %} -

{% trans 'Add new package' %}

-
- - -
- - -
-
-
- {% else %} -
-

{% trans 'Package info:' %} {{ package.name }}

+ {% endif %} + {{ package }} +
+ {% endif %} +
+ {% else %} +
+

{% trans 'Package info:' %} {{ package.name }}

+
+ -

{% trans "Versions:" %} - -

- {% if versions %} -
- - - - - - - - - - - - {% for v in versions %} - - - - - - - - {% endfor %} - -
{% trans 'Version' %}{% trans 'OS' %}{% trans 'Security' %}{% trans 'Registered on' %}{% trans 'Blacklisted' %}
{{ v.release }}{{ v.product.name }}{{ v.security|yesno:_("Yes,No") }}{{ v.time|date:'M d H:i:s Y' }} - - -
-
+ {% endif %} + + {% if not versions and package.pk %} + {% trans 'No Versions available' %} + {% endif %} + + {% if versions %} +

{% trans "Versions:" %}

+
+ + + + + + + + + {% if 'auth.write_access' in perms %} + {% endif %} - {% if 'auth.write_access' in perms %} -
+ + + + {% for v in versions %} + + + + + + + {% if 'auth.write_access' in perms %} + + {% endif %} + + {% endfor %} + +
{% trans 'Version' %}{% trans 'OS' %}{% trans 'Registered on' %}{% trans 'Security' %}{% trans 'Blacklisted' %}
{{ v.release }}{{ v.product.name }}{{ v.time|date:'M d H:i:s Y' }} + + + + + + + +
+
+ {% endif %} -
- {% if add %} - - {% endif %} - {% if package.id %} - - {% endif %} -
- {% endif %} - - {% endif %} - {% if package.pk %} - +

{% trans 'Reported by Devices' %}

@@ -99,7 +93,7 @@

{% trans 'Reported by Devices' %}

{% else %} -

No files found

+

No devices found

{% endif %}

@@ -129,11 +123,9 @@

Files


- - {% endblock %} diff --git a/django.db b/django.db index 29fd712bd224e824ab26b24cb1fc4c4680dd8ff3..ef5c6ef6860e26f15ef7a7bfcdc0b2f7851069e0 100644 GIT binary patch delta 661 zcmZoT!PIbqX@WFk!$z6a=0Zjm3WkPOCZ<+~7J3#2<^~1^n{S$Hh)6R1W#C}i%)so< ze4BY2a}Ki-(+#H0%oCX>Zfu;%RIkG9%b1sASzMT%SZ{P_nVoHDR*`R5#KXYApqg)*XjYY! zX<(3*USeTlX`EzfWKo=%TbY(oP-bqPm(B|^DI+^4+bl8J*w8ewbh6fBk~>+LqmiY&CNkxWo)Wv0FpJ}0qLInXOYzAl>P+*d@Kw{8JNYGW-_h^GPX`^Tvq?9 zk<}3%I*sXktiDaL2mu_yFQ*wNG z;b-KR0hBlK%}O_n$_+I13n?)7%(2W!i3rM%Fb=DVittK{%$>YpF8k#E`62ZY2DZ38 h*_h4)b{C--BHvw;7t9q0xkDD&9T=gxdFG;YMgYeZ#a93T delta 129 zcmV-{0Dk{~$O3@K0+1U4bg>+(Hxe*3ATlvJGc!6hG%hwWF)%qcv)MN&6cYdd00sjh z00Tz@+XJ=(Yy%<#k+Fde0|X=kO#zc0qad?JPrf`51_MR_1K|V01ET|k17!n7v4JuJ j1ReuH0h9cq8na}Npb!WG0+|2<76O^EfsF#QnWAR_pt~c3 From d4c1f67d472f5cae2bfefa49606dc57ef4945c89 Mon Sep 17 00:00:00 2001 From: Danilo Bargen Date: Fri, 30 May 2014 14:52:53 +0200 Subject: [PATCH 25/51] Implemented result highlighting in paging --- .../devices/paging/device_report_sessions.html | 3 ++- apps/front/static/css/base.css | 6 +++++- .../templates/front/paging/default_list.html | 6 ++++-- .../templates/front/paging/regid_list_tags.html | 10 ++++++---- apps/front/templatetags/text_filters.py | 15 +++++++++++++++ 5 files changed, 32 insertions(+), 8 deletions(-) create mode 100644 apps/front/templatetags/text_filters.py diff --git a/apps/devices/templates/devices/paging/device_report_sessions.html b/apps/devices/templates/devices/paging/device_report_sessions.html index 74453bac..3235d645 100644 --- a/apps/devices/templates/devices/paging/device_report_sessions.html +++ b/apps/devices/templates/devices/paging/device_report_sessions.html @@ -1,4 +1,5 @@ {% load i18n %} + {% if sessions.count %} @@ -20,4 +21,4 @@
{% else %}

{% trans 'No sessions reported for this device.' %}

-{% endif %} \ No newline at end of file +{% endif %} diff --git a/apps/front/static/css/base.css b/apps/front/static/css/base.css index 583d96f4..ab1e5dc8 100644 --- a/apps/front/static/css/base.css +++ b/apps/front/static/css/base.css @@ -155,4 +155,8 @@ th.dateWidthFixed { .actionWidth { width: 90px; -} \ No newline at end of file +} + +span.highlight { + font-weight: bold; +} diff --git a/apps/front/templates/front/paging/default_list.html b/apps/front/templates/front/paging/default_list.html index 91e77ab5..186d84aa 100644 --- a/apps/front/templates/front/paging/default_list.html +++ b/apps/front/templates/front/paging/default_list.html @@ -1,9 +1,11 @@ +{% load text_filters %} + {% if object_list %} {% for obj in object_list %} - + {% endfor %} @@ -12,4 +14,4 @@

There are no entries.

-{% endif %} \ No newline at end of file +{% endif %} diff --git a/apps/front/templates/front/paging/regid_list_tags.html b/apps/front/templates/front/paging/regid_list_tags.html index 93c659f2..0a5c7b69 100644 --- a/apps/front/templates/front/paging/regid_list_tags.html +++ b/apps/front/templates/front/paging/regid_list_tags.html @@ -1,4 +1,6 @@ {% load i18n %} +{% load text_filters %} + {% if object_list %}
{{ obj.list_repr }}{{ obj.list_repr|highlight:filter_query }}
@@ -10,15 +12,15 @@ {% for obj in object_list %} - + @@ -30,4 +32,4 @@

There are no entries.

-{% endif %} \ No newline at end of file +{% endif %} diff --git a/apps/front/templatetags/text_filters.py b/apps/front/templatetags/text_filters.py new file mode 100644 index 00000000..464abe44 --- /dev/null +++ b/apps/front/templatetags/text_filters.py @@ -0,0 +1,15 @@ +# -*- coding: utf-8 -*- +from __future__ import print_function, division, absolute_import, unicode_literals + +import re + +from django import template +from django.utils.safestring import mark_safe + +register = template.Library() + + +@register.filter +def highlight(text, word): + output = re.sub(r'(?i)(%s)' % re.escape(word), r'\1', text) + return mark_safe(output) From 28e965cfb41c4dfee0b9f8fd4102b6c9be156441 Mon Sep 17 00:00:00 2001 From: Jonas Furrer Date: Sat, 31 May 2014 09:27:47 +0200 Subject: [PATCH 26/51] Decoded filter query --- apps/front/static/js/hashquery.js | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/apps/front/static/js/hashquery.js b/apps/front/static/js/hashquery.js index e3f48c19..532099d7 100644 --- a/apps/front/static/js/hashquery.js +++ b/apps/front/static/js/hashquery.js @@ -33,12 +33,11 @@ HashQuery.notifyAll = function () { var obj = HashQuery.getHashQueryObject(); for (var key in obj) { - var key_enc = encodeURIComponent(key); - var val_enc = encodeURIComponent(obj[key]); + var val = obj[key]; - if (HashQuery.callbacks[key_enc] && HashQuery.hash[key_enc] != val_enc) { - $.each(HashQuery.callbacks[key_enc], function (idx, callback) { - callback(key_enc, val_enc); + if (HashQuery.callbacks[key] && HashQuery.hash[key] != val) { + $.each(HashQuery.callbacks[key], function (idx, callback) { + callback(key, val); }); } } @@ -84,4 +83,4 @@ HashQuery.setHashQueryObject = function (obj, ignoreEvents, avoidBrowserHistory) }; window.addEventListener("hashchange", HashQuery.notifyAll, false); -HashQuery.hash = HashQuery.getHashQueryObject(); \ No newline at end of file +HashQuery.hash = HashQuery.getHashQueryObject(); From cc15938eba41a9658812238a497b6ef3d3b8e53f Mon Sep 17 00:00:00 2001 From: Jonas Furrer Date: Fri, 30 May 2014 11:56:14 +0200 Subject: [PATCH 27/51] Added additional reverse lookup data to several views --- apps/devices/device_views.py | 2 +- apps/devices/group_views.py | 10 ++-- apps/devices/product_views.py | 7 ++- apps/devices/templates/devices/groups.html | 38 ++++++++++++-- apps/devices/templates/devices/products.html | 51 +++++++++++++++++++ apps/filesystem/file_views.py | 3 ++ .../templates/filesystem/files.html | 39 ++++++++++++++ .../packages/templates/packages/packages.html | 34 ++++++++++++- apps/packages/views.py | 4 ++ apps/policies/policy_views.py | 2 +- .../policies/templates/policies/policies.html | 25 +++++++++ 11 files changed, 204 insertions(+), 11 deletions(-) diff --git a/apps/devices/device_views.py b/apps/devices/device_views.py index 947c2262..48066825 100644 --- a/apps/devices/device_views.py +++ b/apps/devices/device_views.py @@ -13,7 +13,7 @@ from apps.core.models import Session, Result from apps.core.types import WorkItemType -from apps.policies.models import Policy +from apps.policies.models import Policy, Enforcement from .models import Device, Group, Product diff --git a/apps/devices/group_views.py b/apps/devices/group_views.py index 3ced653e..a0788d8d 100644 --- a/apps/devices/group_views.py +++ b/apps/devices/group_views.py @@ -56,11 +56,15 @@ def group(request, groupID): context['title'] = _('Group ') + context['group'].name child_groups = group.get_children() - enfocements = Enforcement.objects.filter(Q(group=group) | Q(group__in=child_groups)) + parent_groups = group.get_parents() + dependent_enforcements = Enforcement.objects.filter(Q(group=group) | Q(group__in=child_groups)) + applied_enforcements = Enforcement.objects.filter(Q(group=group) | Q(group__in=parent_groups))\ + .order_by('policy', 'group') + context['applied_enforcements'] = applied_enforcements - if len(child_groups) or enfocements.count(): + if len(child_groups) or dependent_enforcements.count(): context['has_dependencies'] = True - context['enforcements'] = enfocements + context['dependent_enforcements'] = dependent_enforcements context['child_groups'] = child_groups return render(request, 'devices/groups.html', context) diff --git a/apps/devices/product_views.py b/apps/devices/product_views.py index dc0406ff..a8e157ef 100644 --- a/apps/devices/product_views.py +++ b/apps/devices/product_views.py @@ -9,6 +9,7 @@ from django.contrib.auth.decorators import login_required, permission_required from django.shortcuts import get_object_or_404, render, redirect from django.utils.translation import ugettext_lazy as _ +from apps.policies.models import Enforcement from .models import Product, Group, Device from apps.packages.models import Version @@ -48,12 +49,16 @@ def product(request, productID): context['groups'] = groups context['title'] = _('Product ') + product.name devices = Device.objects.filter(product=product) + context['devices'] = devices versions = Version.objects.filter(product=product) if devices.count() or versions.count(): context['has_dependencies'] = True - context['devices'] = devices context['versions'] = versions + parent_groups = set([p for g in defaults for p in g.get_parents()] + list(defaults)) + enforcements = Enforcement.objects.filter(group__in=parent_groups).order_by('policy', 'group') + context['enforcements'] = enforcements + return render(request, 'devices/products.html', context) diff --git a/apps/devices/templates/devices/groups.html b/apps/devices/templates/devices/groups.html index aaf2d4ae..91f5a68f 100644 --- a/apps/devices/templates/devices/groups.html +++ b/apps/devices/templates/devices/groups.html @@ -120,6 +120,38 @@

Assign Devices

{% endif %} {% endif %} + + {% if group.pk %} +
+
+
{% trans 'Applied enforcements' %}
+ {% if applied_enforcements %} +
{{ obj.list_repr }}{{ obj.list_repr|highlight:filter_query }} {% if obj.get_matching_packages %} - {{ obj.package_name }} + {{ obj.package_name|highlight:filter_query }} {% else %} - {{ obj.package_name }} + {{ obj.package_name|highlight:filter_query }} {% endif %}
+ + + + + + + + {% for enf in applied_enforcements %} + + + {% if enf.group.pk != group.pk %} + + {% else %} + + {% endif %} + + {% endfor %} + +
{% trans 'Enforcement' %}{% trans 'Inherited' %}
{{ enf.list_repr }}{{ enf.group.list_repr }}{% trans 'Not inherited' %}
+ {% else %} +

{% trans 'This group has no applied ' %}{% trans 'enforcements' %}

+ {% endif %} + + {% endif %} + {% if group.pk %} {% endif %} - {% if enforcements %} + {% if dependent_enforcements %}
    - {% for enforcement in enforcements %} + {% for enforcement in dependent_enforcements %}
  • {{ enforcement.list_repr }}
  • diff --git a/apps/devices/templates/devices/products.html b/apps/devices/templates/devices/products.html index dd9196fc..258b0201 100644 --- a/apps/devices/templates/devices/products.html +++ b/apps/devices/templates/devices/products.html @@ -97,6 +97,57 @@

    {% trans "Assign Default Groups" %} {% endif %} {% endif %} + + {% if product.pk %} +
    +
    +
    {% trans 'Devices with this product' %}
    + {% if devices %} + + + + + + + + {% for device in devices %} + + + + {% endfor %} + +
    {% trans 'Device' %}
    {{ device.list_repr }}
    + {% else %} +

    + {% trans 'No' %} {% trans 'device' %} {% trans 'with this product found.' %} +

    + {% endif %} +
    + +
    +
    {% trans 'Applied enforcements' %}
    + {% if enforcements %} + + + + + + + + {% for enf in enforcements %} + + + + {% endfor %} + +
    {% trans 'Enforcement' %}
    {{ enf.list_repr }}
    + {% else %} +

    + {% trans 'This product has no applied ' %}{% trans 'enforcements' %} +

    + {% endif %} +
    + {% endif %}

diff --git a/apps/filesystem/file_views.py b/apps/filesystem/file_views.py index 80878eb1..f095976e 100644 --- a/apps/filesystem/file_views.py +++ b/apps/filesystem/file_views.py @@ -52,6 +52,9 @@ def file(request, fileID): context['policies'] = policies context['enforcements'] = Enforcement.objects.filter(policy__in=policies) + swid_tags = file.tag_set.all() + context['swid_tags'] = swid_tags + return render(request, 'filesystem/files.html', context) diff --git a/apps/filesystem/templates/filesystem/files.html b/apps/filesystem/templates/filesystem/files.html index 61359859..4cfdf7d4 100644 --- a/apps/filesystem/templates/filesystem/files.html +++ b/apps/filesystem/templates/filesystem/files.html @@ -124,6 +124,45 @@

File Hashes

{% endif %} + {% if file.pk %} +
+
+
{% trans 'File appears in the following SWID tags' %}
+ {% if swid_tags %} + + + + + + + + + + {% for tag in swid_tags %} + + + + + + {% endfor %} + +
{% trans 'Unique ID' %}{% trans 'Package name' %}{% trans 'Version' %}
{{ tag.list_repr }} + {% if tag.get_matching_packages %} + + {{ tag.package_name }} + + {% else %} + {{ tag.package_name }} + {% endif %} + {{ tag.version }}
+ {% else %} +

+ {% trans "This file hasn't any connected SWID tags" %} +

+ {% endif %} +
+ {% endif %} + {% if 'auth.write_access' in perms %} {% if file.pk %} diff --git a/apps/packages/templates/packages/packages.html b/apps/packages/templates/packages/packages.html index 90ad3246..bb841c8b 100644 --- a/apps/packages/templates/packages/packages.html +++ b/apps/packages/templates/packages/packages.html @@ -145,8 +145,8 @@

{% trans "Versions:" %}

{% if package.pk %} -
+
@@ -200,9 +200,39 @@

{% trans 'Add new version' %}

{% endif %} + {% endif %} - + {% if package.pk %} +
+
+
{% trans 'SWID tags with matching package name' %}
+ {% if swid_tags %} + + + + + + + + + + {% for tag in swid_tags %} + + + + + + {% endfor %} + +
{% trans 'Unique ID' %}{% trans 'Package name' %}{% trans 'Version' %}
{{ tag.list_repr }}{{ tag.package_name }}{{ tag.version }}
+ {% else %} +

+ {% trans 'No SWID tags with matching package names' %} +

+ {% endif %} +
{% endif %} + {% if package.pk %} From 43a5d3ef296494edf278131b6eb163670207ffbf Mon Sep 17 00:00:00 2001 From: cfaessler Date: Thu, 29 May 2014 15:52:47 +0200 Subject: [PATCH 28/51] Changed swid tag import to allow entity name update --- apps/swid/management/commands/importswid.py | 2 +- apps/swid/utils.py | 17 +- tests/test_api.py | 7 +- tests/test_swid.py | 150 ++++++++++++++---- .../strongswan.full.swidtag.duplicateregid | 14 ++ tests/test_tags/strongswan.full.swidtag | 2 +- .../strongswan.full.swidtag.replacement | 12 ++ 7 files changed, 169 insertions(+), 35 deletions(-) create mode 100644 tests/test_tags/invalid_tags/strongswan.full.swidtag.duplicateregid create mode 100644 tests/test_tags/strongswan.full.swidtag.replacement diff --git a/apps/swid/management/commands/importswid.py b/apps/swid/management/commands/importswid.py index a8362150..f3395fc9 100644 --- a/apps/swid/management/commands/importswid.py +++ b/apps/swid/management/commands/importswid.py @@ -36,7 +36,7 @@ def handle(self, *args, **kwargs): with open(filename, 'r') as f: for line in f: tag_xml = line.strip().decode('utf8') - tag, replaced = utils.process_swid_tag(tag_xml) + tag, replaced = utils.process_swid_tag(tag_xml, allow_tag_update=True) if replaced: self.stdout.write('Replaced {0}'.format(tag).encode(encoding, 'replace')) else: diff --git a/apps/swid/utils.py b/apps/swid/utils.py index df34773b..1fc34a87 100644 --- a/apps/swid/utils.py +++ b/apps/swid/utils.py @@ -70,7 +70,7 @@ def close(self): @transaction.atomic -def process_swid_tag(tag_xml): +def process_swid_tag(tag_xml, allow_tag_update=False): """ Parse a SWID XML tag and store the contained elements in the database. @@ -82,6 +82,8 @@ def process_swid_tag(tag_xml): Args: tag_xml (unicode): The SWID tag as an XML string. + allow_tag_update (bool): + If the tag already exists its data gets overwritten. Returns: A tuple containing the newly created Tag model instance and a flag @@ -103,9 +105,22 @@ def process_swid_tag(tag_xml): # Check whether tag already exists try: old_tag = Tag.objects.get(software_id=tag.software_id) + # Tag doesn't exist, create a new one later on except Tag.DoesNotExist: replaced = False + # Tag exists already else: + # Tag already exists but updates are not allowed + if not allow_tag_update: + replaced = False + # The tag will not be changed, but we want to make sure + # that the entities have the right name. + for entity, _ in entities: + Entity.objects.filter(pk=entity.pk).update(name=entity.name) + + # Tag needs to be reloaded after entity updates + return Tag.objects.get(pk=old_tag.pk), replaced + # Update tag with new information old_tag.package_name = tag.package_name old_tag.version = tag.version old_tag.unique_id = tag.unique_id diff --git a/tests/test_api.py b/tests/test_api.py index c44ac3a6..e584f28a 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -144,7 +144,7 @@ def test_add_single_tag(api_client): 'strongswan.full.swidtag', ]) def test_add_existing_tag(api_client, swidtag, filename): - assert len(swidtag.files.all()) == 7 + assert swidtag.files.count() == 7 with open('tests/test_tags/strongswan.full.swidtag.singleentity') as f: xml = f.read() @@ -154,9 +154,8 @@ def test_add_existing_tag(api_client, swidtag, filename): tag = Tag.objects.get( software_id="regid.2004-03.org.strongswan_debian_7.4-x86_64-strongswan-4.5.2-1.5+deb7u3") - assert tag.files.count() == 5 - assert len(tag.entityrole_set.all()) == 1 - assert len(tag.files.all()) == 5 + assert tag.files.count() == 7 + assert tag.entityrole_set.count() == 2 @pytest.mark.parametrize('filename', [ diff --git a/tests/test_swid.py b/tests/test_swid.py index f6768507..20ad9d76 100644 --- a/tests/test_swid.py +++ b/tests/test_swid.py @@ -34,7 +34,7 @@ def swidtag(request, transactional_db): filename = request.getfuncargvalue('filename') with open('tests/test_tags/%s' % filename, 'r') as f: tag_xml = f.read() - return utils.process_swid_tag(tag_xml)[0] + return utils.process_swid_tag(tag_xml, allow_tag_update=True)[0] @pytest.fixture @@ -92,7 +92,7 @@ def test_tag_version(swidtag, filename, version): ('strongswan.short.swidtag', [EntityRole.TAGCREATOR]), ('strongswan.full.swidtag', [EntityRole.TAGCREATOR, EntityRole.PUBLISHER]), ('strongswan.full.swidtag.combinedrole', - [EntityRole.TAGCREATOR, EntityRole.PUBLISHER, EntityRole.LICENSOR]), + [EntityRole.TAGCREATOR, EntityRole.PUBLISHER, EntityRole.LICENSOR]), ('cowsay.short.swidtag', [EntityRole.TAGCREATOR]), ('cowsay.full.swidtag', [EntityRole.TAGCREATOR, EntityRole.LICENSOR]), ('strongswan-tnc-imcvs.short.swidtag', [EntityRole.TAGCREATOR]), @@ -146,18 +146,6 @@ def test_tag_files(swidtag, filename, directories, files, filecount): assert swidtag.files.count() == filecount -@pytest.mark.parametrize('filename', [ - 'strongswan.full.swidtag', -]) -def test_tag_replacement(swidtag, filename): - with open('tests/test_tags/strongswan.full.swidtag.singleentity') as f: - xml = f.read() - tag, replaced = utils.process_swid_tag(xml) - assert tag.software_id == swidtag.software_id - assert replaced == True - assert len(tag.entity_set.all()) == 1 - - @pytest.mark.django_db @pytest.mark.parametrize('filename', [ 'strongswan.full.swidtag.notagcreator', @@ -175,26 +163,122 @@ def test_invalid_tags(filename): assert len(Entity.objects.all()) == 0 -@pytest.mark.parametrize('filename',[ +@pytest.mark.parametrize('value', ['publisher', 'licensor', 'tagcreator']) +def test_valid_role(value): + try: + EntityRole.xml_attr_to_choice(value) + except ValueError: + pytest.fail('Role %s should be valid.' % value) + + +def test_invalid_role(): + with pytest.raises(ValueError): + EntityRole.xml_attr_to_choice('licensee') + + +### TAG REPLACEMENT / UPDATE TESTS ### + +@pytest.mark.parametrize('filename', [ 'strongswan.full.swidtag', ]) -def test_entity_name_update(swidtag, filename): - assert(Entity.objects.count() == 1) +def test_tag_add_entities(swidtag, filename): + assert swidtag.entityrole_set.count() == 2 + old_software_id = swidtag.software_id + with open('tests/test_tags/strongswan.full.swidtag.replacement') as f: + xml = f.read() + tag, replaced = utils.process_swid_tag(xml, allow_tag_update=True) + + assert tag.software_id == old_software_id + assert replaced is True + assert tag.entity_set.count() == 4 + + +@pytest.mark.parametrize('filename', [ + 'strongswan.full.swidtag', +]) +def test_tag_replace_files(swidtag, filename): + assert swidtag.files.count() == 7 + + with open('tests/test_tags/strongswan.full.swidtag.replacement') as f: + xml = f.read() + tag, replaced = utils.process_swid_tag(xml, allow_tag_update=True) + + assert replaced is True + assert tag.files.count() == 3 + + +@pytest.mark.parametrize('filename', [ + 'invalid_tags/strongswan.full.swidtag.duplicateregid', +]) +def test_change_duplicate_regid_entity_name(swidtag, filename): + """ + Changing the name of an entity (with a role other than tagcreator) will create a new entity, since an + entity is uniquely identified by its name and regid. + + """ + + assert Entity.objects.count() == 1 + assert swidtag.entity_set.count() == 2 new_xml = swidtag.swid_xml.replace('name="strongSwan"', 'name="strongswan123"') - tag, replaced = utils.process_swid_tag(new_xml) - assert(Entity.objects.count() == 1) - assert(Tag.objects.count() == 1) - assert(replaced) + tag, replaced = utils.process_swid_tag(new_xml, allow_tag_update=True) + assert tag.entity_set.count() == 2 + assert Entity.objects.count() == 1 + assert Tag.objects.count() == 1 + assert replaced is True + + # new entities should be wired up correctly + test_entities = ['HSR', 'HSR'] + real_entities = tag.entity_set.values_list('name', flat=True) + assert sorted(test_entities) == sorted(real_entities) + +@pytest.mark.parametrize('filename', [ + 'strongswan.full.swidtag', +]) +def test_change_entity_name(swidtag, filename): + entity_to_update = Entity.objects.get(regid='regid.2004-03.org.strongswan') + old_entity_name = 'strongSwan' + new_entity_name = 'strongSwan Project' + + assert Entity.objects.count() == 2 + assert swidtag.entity_set.count() == 2 + assert entity_to_update.name == old_entity_name + + new_xml = swidtag.swid_xml.replace('name="%s"' % old_entity_name, 'name="%s"' % new_entity_name) + tag, replaced = utils.process_swid_tag(new_xml, allow_tag_update=False) + entity_to_update = Entity.objects.get(regid='regid.2004-03.org.strongswan') + + assert Entity.objects.count() == 2 + assert tag.entity_set.count() == 2 + assert entity_to_update.name == new_entity_name + + +@pytest.mark.parametrize('filename', [ + 'strongswan.full.swidtag', +]) +def test_change_tagcreator_entity_regid(swidtag, filename): + """ + Changing a tagcreator entity creates a new tag, since a tag is uniquely identified + by the regid (of the tagcreator entity) and the unique_id of the tag itself. + """ new_xml = swidtag.swid_xml.replace('name="strongSwan" regid="regid.2004-03.org.strongswan"', 'name="strongSwan" regid="regid.2005-03.org.strongswan"') - tag, replaced = utils.process_swid_tag(new_xml) + tag, replaced = utils.process_swid_tag(new_xml, allow_tag_update=True) + assert Tag.objects.count() == 2 + assert 'regid.2005-03.org.strongswan' in tag.software_id + assert replaced is False - # a new entity with a different regid should be created - # also a new tag is create because the software id has changed - assert(Entity.objects.count() == 2) - assert(Tag.objects.count() == 2) - assert(not replaced) + +@pytest.mark.parametrize('filename', [ + 'strongswan.full.swidtag', +]) +def test_remove_publisher_entity(swidtag, filename): + assert swidtag.entity_set.count() == 2 + with open('tests/test_tags/strongswan.full.swidtag.singleentity') as f: + xml = f.read() + tag, replaced = utils.process_swid_tag(xml, allow_tag_update=True) + assert replaced is True + assert tag.entity_set.count() == 1 @pytest.mark.parametrize('value', ['publisher', 'licensor', 'tagcreator']) @@ -246,7 +330,6 @@ def tags_and_sessions(transactional_db): def test_swid_inventory_list_producer(transactional_db, tags_and_sessions): - s1 = tags_and_sessions['sessions'][0] params = {'session_id': 1} @@ -349,3 +432,14 @@ def test_get_installed_tags_with_time(transactional_db, tags_and_sessions): assert installed_tags[tag3] == s1 # tag5 was installed in session s2 assert installed_tags[tag5] == s2 + + +@pytest.mark.parametrize('filename', [ + 'strongswan.full.swidtag', +]) +def test_changed_software_entity_name(swidtag, filename): + new_xml = swidtag.swid_xml.replace('SoftwareIdentity name="strongswan"', + 'SoftwareIdentity name="strongswan123"') + tag, replaced = utils.process_swid_tag(new_xml, allow_tag_update=True) + assert replaced is True + assert Tag.objects.count() == 1 diff --git a/tests/test_tags/invalid_tags/strongswan.full.swidtag.duplicateregid b/tests/test_tags/invalid_tags/strongswan.full.swidtag.duplicateregid new file mode 100644 index 00000000..d5a8bd6d --- /dev/null +++ b/tests/test_tags/invalid_tags/strongswan.full.swidtag.duplicateregid @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/tests/test_tags/strongswan.full.swidtag b/tests/test_tags/strongswan.full.swidtag index d5a8bd6d..8c6d8016 100644 --- a/tests/test_tags/strongswan.full.swidtag +++ b/tests/test_tags/strongswan.full.swidtag @@ -1,7 +1,7 @@ - + diff --git a/tests/test_tags/strongswan.full.swidtag.replacement b/tests/test_tags/strongswan.full.swidtag.replacement new file mode 100644 index 00000000..41050204 --- /dev/null +++ b/tests/test_tags/strongswan.full.swidtag.replacement @@ -0,0 +1,12 @@ + + + + + + + + + + + + From c83774707fa1b233f9bf97195fd21e81e6ea004d Mon Sep 17 00:00:00 2001 From: Danilo Bargen Date: Sat, 31 May 2014 15:46:01 +0200 Subject: [PATCH 29/51] Raise KeyError on invalid dynamic_params In the producer functions, invalid dynamic params should raise an exception. --- apps/swid/paging.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/apps/swid/paging.py b/apps/swid/paging.py index 87334142..8ad956cd 100644 --- a/apps/swid/paging.py +++ b/apps/swid/paging.py @@ -39,7 +39,7 @@ def entity_swid_stat_producer(page_size, filter_query, dynamic_params=None, stat def swid_inventory_list_producer(from_idx, to_idx, filter_query, dynamic_params, static_params=None): if not dynamic_params: return [] - session_id = dynamic_params.get('session_id') + session_id = dynamic_params['session_id'] installed_tags = list(get_installed_tags_dict(session_id, filter_query).items())[from_idx:to_idx] tags = [ @@ -57,7 +57,7 @@ def swid_inventory_list_producer(from_idx, to_idx, filter_query, dynamic_params, def swid_inventory_stat_producer(page_size, filter_query, dynamic_params=None, static_params=None): if not dynamic_params: return 0 - session_id = dynamic_params.get('session_id') + session_id = dynamic_params['session_id'] installed_tags = get_installed_tags_dict(session_id, filter_query) return math.ceil(len(installed_tags) / page_size) @@ -96,9 +96,9 @@ def get_installed_tags_dict(session_id, filter_query): def swid_log_list_producer(from_idx, to_idx, filter_query, dynamic_params, static_params=None): if not dynamic_params: return [] - device_id = dynamic_params.get('device_id') - from_timestamp = dynamic_params.get('from_timestamp') - to_timestamp = dynamic_params.get('to_timestamp') + device_id = dynamic_params['device_id'] + from_timestamp = dynamic_params['from_timestamp'] + to_timestamp = dynamic_params['to_timestamp'] diffs = get_tag_diffs(device_id, from_timestamp, to_timestamp)[from_idx:to_idx] @@ -116,9 +116,9 @@ def swid_log_list_producer(from_idx, to_idx, filter_query, dynamic_params, stati def swid_log_stat_producer(page_size, filter_query, dynamic_params=None, static_params=None): if not dynamic_params: return 0 - device_id = dynamic_params.get('device_id') - from_timestamp = dynamic_params.get('from_timestamp') - to_timestamp = dynamic_params.get('to_timestamp') + device_id = dynamic_params['device_id'] + from_timestamp = dynamic_params['from_timestamp'] + to_timestamp = dynamic_params['to_timestamp'] diffs = get_tag_diffs(device_id, from_timestamp, to_timestamp) return math.ceil(len(diffs) / page_size) From b1d57842c7ebb31c40eb253c1a0a048ff6d31e6e Mon Sep 17 00:00:00 2001 From: Danilo Bargen Date: Sat, 31 May 2014 16:32:16 +0200 Subject: [PATCH 30/51] Added error callback to all dajaxice calls --- apps/filesystem/static/js/files.js | 7 +++++-- apps/front/static/js/ajax-utils.js | 9 +++++---- apps/front/static/js/paging.js | 4 +++- apps/policies/templates/policies/policies.html | 10 ++++++++-- apps/swid/static/js/swid-log.js | 6 ++++-- 5 files changed, 25 insertions(+), 11 deletions(-) diff --git a/apps/filesystem/static/js/files.js b/apps/filesystem/static/js/files.js index 55cd59b3..51c40715 100644 --- a/apps/filesystem/static/js/files.js +++ b/apps/filesystem/static/js/files.js @@ -12,9 +12,12 @@ function setupDirectoryDropdown() { return o.directory }, query: function (query) { - autocompleteDelay.callback = query.callback; autocompleteDelay.ajaxFunction = Dajaxice.apps.filesystem.directories_autocomplete; + autocompleteDelay.callback = query.callback; + autocompleteDelay.errorCallback = function() { + alert('Error: Could not fetch directory list.'); + }; autocompleteDelay.queryUpdate(query.term); } }); -} \ No newline at end of file +} diff --git a/apps/front/static/js/ajax-utils.js b/apps/front/static/js/ajax-utils.js index ff036a1a..c6356a8f 100644 --- a/apps/front/static/js/ajax-utils.js +++ b/apps/front/static/js/ajax-utils.js @@ -16,6 +16,7 @@ var autocompleteDelay = { query: '', lastDelayedCall: null, callback: null, + errorCallback: function() {}, ajaxFunction: null, queryUpdate: function (newQuery) { @@ -37,8 +38,8 @@ var autocompleteDelay = { autocompleteCall: function () { // function is called by setTimeout, // thus it's running in the context of the window object - var callback = autocompleteDelay.callback; - var query = autocompleteDelay.query; - autocompleteDelay.ajaxFunction(callback, {'search_term': query}); + var data = {'search_term': autocompleteDelay.query}; + var config = {'error_callback': autocompleteDelay.errorCallback}; + autocompleteDelay.ajaxFunction(autocompleteDelay.callback, data, config); } -}; \ No newline at end of file +}; diff --git a/apps/front/static/js/paging.js b/apps/front/static/js/paging.js index 661df528..6e16eafe 100644 --- a/apps/front/static/js/paging.js +++ b/apps/front/static/js/paging.js @@ -99,7 +99,9 @@ var Pager = function() { var filterQuery = this.getFilterQuery(); var paramObject = this.getParamObject(filterQuery); this.loading = true; - Dajaxice.apps.front.paging(this.pagingCallback.bind(this), paramObject); + Dajaxice.apps.front.paging(this.pagingCallback.bind(this), paramObject, {'error_callback': function() { + alert('Error: Failed to fetch "' + paramObject.config_name + '" paging.'); + }}); }; this.statsUpdate = function(data) { diff --git a/apps/policies/templates/policies/policies.html b/apps/policies/templates/policies/policies.html index 68f589c5..7daf359e 100644 --- a/apps/policies/templates/policies/policies.html +++ b/apps/policies/templates/policies/policies.html @@ -324,8 +324,11 @@

{% trans 'Warning' %}

return o.file }, query: function (query) { - autocompleteDelay.callback = query.callback; autocompleteDelay.ajaxFunction = Dajaxice.apps.filesystem.files_autocomplete; + autocompleteDelay.callback = query.callback; + autocompleteDelay.errorCallback = function() { + alert('Error: Could not fetch file list.'); + }; autocompleteDelay.queryUpdate(query.term); } }); @@ -344,8 +347,11 @@

{% trans 'Warning' %}

return o.directory }, query: function (query) { - autocompleteDelay.callback = query.callback; autocompleteDelay.ajaxFunction = Dajaxice.apps.filesystem.directories_autocomplete; + autocompleteDelay.callback = query.callback; + autocompleteDelay.errorCallback = function() { + alert('Error: Could not fetch directory list.'); + }; autocompleteDelay.queryUpdate(query.term); } }); diff --git a/apps/swid/static/js/swid-log.js b/apps/swid/static/js/swid-log.js index 1147a47f..5c3f6597 100644 --- a/apps/swid/static/js/swid-log.js +++ b/apps/swid/static/js/swid-log.js @@ -65,7 +65,9 @@ var getTagList = function() { 'device_id': deviceId, 'from_timestamp': fromTimestamp, 'to_timestamp': toTimestamp - }); + }, {'error_callback': function() { + alert('Error: Could not fetch tag log stats.'); + }}); }; var updateStats = function(data) { @@ -75,4 +77,4 @@ var updateStats = function(data) { $('.removedTags', $statsTable).text(data['removed_count']); $('.firstSession', $statsTable).text(data['first_session']); $('.lastSession', $statsTable).text(data['last_session']); -}; \ No newline at end of file +}; From 14c9957c9864bcf8dffb97e1507fd245bbc1ab1e Mon Sep 17 00:00:00 2001 From: Jonas Furrer Date: Sat, 31 May 2014 09:51:26 +0200 Subject: [PATCH 31/51] Fixed most recent session in device report --- apps/devices/device_views.py | 12 ++++------- .../templates/devices/device_report.html | 11 +++++----- apps/devices/templates/devices/session.html | 20 +++++++++++-------- 3 files changed, 22 insertions(+), 21 deletions(-) diff --git a/apps/devices/device_views.py b/apps/devices/device_views.py index 48066825..7024e728 100644 --- a/apps/devices/device_views.py +++ b/apps/devices/device_views.py @@ -197,8 +197,8 @@ def report(request, deviceID): context['inherit_set'] = list(current_device.get_inherit_set()) if context['session_count'] > 0: - latest_session = Session.objects.latest('time') - context['last_session'] = latest_session.time + latest_session = Session.objects.filter(device=deviceID).latest() + context['last_session'] = latest_session context['last_user'] = latest_session.identity.data context['last_result'] = latest_session.get_recommendation_display() else: @@ -232,13 +232,9 @@ def session(request, sessionID): context = {} context['session'] = session context['title'] = _('Session details') - context['recommendation'] = Policy.action[session.recommendation] + context['recommendation'] = session.get_recommendation_display() - context['results'] = [] - for result in session.results.all(): - context['results'].append((result, Policy.action[result.recommendation])) - if result.policy.type == WorkItemType.SWIDT: - context['swid_measurement'] = result.session_id + context['results'] = session.results.all() return render(request, 'devices/session.html', context) diff --git a/apps/devices/templates/devices/device_report.html b/apps/devices/templates/devices/device_report.html index c44aaef6..3912b279 100644 --- a/apps/devices/templates/devices/device_report.html +++ b/apps/devices/templates/devices/device_report.html @@ -27,20 +27,21 @@

{% trans 'Device Infos' %}

{{ device.description }} - {% trans 'Last user' %} + {% trans 'Most recent user' %} {{ last_user }} - {% trans 'Last session' %} - {% if last_session %} - {{ last_session|date:'M d H:i:s Y' }} + {% trans 'Most recent session' %} + + {% if last_session %} + {{ last_session.time|date:'M d H:i:s Y' }} {% else %} {% trans 'None' %} {% endif %} - {% trans 'Last assessment' %} + {% trans 'Most recent assessment' %} {{ last_result }} diff --git a/apps/devices/templates/devices/session.html b/apps/devices/templates/devices/session.html index 6dc7718d..89bfdeb9 100644 --- a/apps/devices/templates/devices/session.html +++ b/apps/devices/templates/devices/session.html @@ -51,27 +51,31 @@

{% trans 'Session Info' %}

{% endif %} +
+

{% trans 'Results' %}

{% if results %} -
-

{% trans 'Results' %}

- + + {% for r in results %} - - - + + + {% endfor %}
{% trans "Policy" %} - {% trans "Result" %} - {% trans "IMV Comment" %} + {% trans "Policy" %}{% trans "Result" %}{% trans "IMV Comment" %}
{{ r.0.policy }}{{ r.1 }}{{ r.0.result }}{{ r.policy }}{{ r.get_recommendation_display }}{{ r.result }}
+ {% else %} +

+ {% trans 'No measurements in this session.' %} +

{% endif %} From 15334e5e59326d42f332eaef2e9733d531e93fd6 Mon Sep 17 00:00:00 2001 From: Danilo Bargen Date: Sun, 1 Jun 2014 17:07:42 +0200 Subject: [PATCH 32/51] Renamed `swid_log` url to `log` --- apps/devices/templates/devices/device_report.html | 2 +- apps/swid/templates/swid/swid_inventory.html | 2 +- apps/swid/urls.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/devices/templates/devices/device_report.html b/apps/devices/templates/devices/device_report.html index 3912b279..4afc5084 100644 --- a/apps/devices/templates/devices/device_report.html +++ b/apps/devices/templates/devices/device_report.html @@ -53,7 +53,7 @@

{% trans 'Device Infos' %}

diff --git a/apps/swid/templates/swid/swid_inventory.html b/apps/swid/templates/swid/swid_inventory.html index 5135b58f..b8e5d238 100644 --- a/apps/swid/templates/swid/swid_inventory.html +++ b/apps/swid/templates/swid/swid_inventory.html @@ -13,7 +13,7 @@

SWID {% trans "Inventory for " %} {{ object.description }}

{% block content %}
- {% trans 'View SWID log' %} diff --git a/apps/swid/urls.py b/apps/swid/urls.py index 7a080518..e769bc81 100644 --- a/apps/swid/urls.py +++ b/apps/swid/urls.py @@ -7,5 +7,5 @@ url(r'^swid-tags/$', views.SwidTagListView.as_view(), name='tag_list'), url(r'^swid-tags/(?P\d+)/$', views.SwidTagDetailView.as_view(), name='tag_detail'), url(r'^swid-inventory/(?P\d+)/$', views.SwidInventoryView.as_view(), name='inventory'), - url(r'^swid-log/(?P\d+)/$', views.SwidLogView.as_view(), name='swid_log'), + url(r'^swid-log/(?P\d+)/$', views.SwidLogView.as_view(), name='log'), ) From 02dffaddc41a07e9c94b95f23b4d90d3a43d4352 Mon Sep 17 00:00:00 2001 From: Danilo Bargen Date: Sun, 1 Jun 2014 17:08:00 +0200 Subject: [PATCH 33/51] Require login for swid views and statistics --- apps/front/views.py | 1 + apps/swid/views.py | 4 ++-- tests/test_auth.py | 38 ++++++++++++++++++++++++++++++++++---- 3 files changed, 37 insertions(+), 6 deletions(-) diff --git a/apps/front/views.py b/apps/front/views.py index 8527d932..5e0e85bc 100644 --- a/apps/front/views.py +++ b/apps/front/views.py @@ -24,6 +24,7 @@ def overview(request): @require_GET +@login_required def statistics(request): """ Statistics view diff --git a/apps/swid/views.py b/apps/swid/views.py index 23f4d040..e153e641 100644 --- a/apps/swid/views.py +++ b/apps/swid/views.py @@ -43,7 +43,7 @@ def get_context_data(self, **kwargs): return context -class SwidInventoryView(DetailView): +class SwidInventoryView(LoginRequiredMixin, DetailView): template_name = 'swid/swid_inventory.html' model = Device @@ -56,7 +56,7 @@ def get_context_data(self, **kwargs): return context -class SwidLogView(DetailView): +class SwidLogView(LoginRequiredMixin, DetailView): template_name = 'swid/swid_log.html' model = Device diff --git a/tests/test_auth.py b/tests/test_auth.py index 20c6291e..e5178780 100644 --- a/tests/test_auth.py +++ b/tests/test_auth.py @@ -35,12 +35,39 @@ def test_login(client, strongtnc_users, test_user, username, password, success): @pytest.mark.parametrize('url', [ # SWID views - '/regids/', - '/regids/1/', - '/swid-tags/', - '/swid-tags/1/', + '', + # '/api/', TODO uncomment as soon as login is required for all api access + reverse('devices:device_list'), + reverse('devices:device_detail', args=[1]), + reverse('devices:device_report', args=[1]), + reverse('devices:session_detail', args=[1]), + reverse('devices:group_list'), + reverse('devices:group_detail', args=[1]), + reverse('devices:product_list'), + reverse('devices:product_detail', args=[1]), + reverse('filesystem:file_list'), + reverse('filesystem:file_detail', args=[1]), + reverse('filesystem:directory_list'), + reverse('filesystem:directory_detail', args=[1]), + reverse('front:search'), + reverse('front:statistics'), + reverse('packages:package_list'), + reverse('packages:package_detail', args=[1]), + reverse('policies:policy_list'), + reverse('policies:policy_detail', args=[1]), + reverse('policies:enforcement_list'), + reverse('policies:enforcement_detail', args=[1]), + reverse('swid:regid_list'), + reverse('swid:regid_detail', args=[1]), + reverse('swid:tag_list'), + reverse('swid:tag_detail', args=[1]), + reverse('swid:inventory', args=[1]), + reverse('swid:log', args=[1]), ]) def test_login_required(client, strongtnc_users, url): + """ + Test whether login is required for all read-only views. + """ # Test as anonymous response = client.get(url) assert response.status_code == 302, 'Unauthenticated user should not have access to %s' % url @@ -98,6 +125,9 @@ def test_login_required(client, strongtnc_users, url): ('/packages/1/versions/1/remove', 'get') ]) def test_write_permission_enforced(client, strongtnc_users, url, method): + """ + Test whether login is required for views where data is written. + """ do_request = getattr(client, method) # Test as admin From 680fdc3acf7e2fe81a66065490436c477adff9cc Mon Sep 17 00:00:00 2001 From: Danilo Bargen Date: Sun, 1 Jun 2014 15:43:58 +0200 Subject: [PATCH 34/51] Added a `fields` param to all API endpoints The API now allows a new `fields` parameter that can filter returned fields per document. The fields need to be comma separated, without any spaces. - Correct: `fields=id,url,packageName` - Wrong: `fields=id&fields=url` - Wrong: `fields=id, url` Nested entities can be shown or hidden, but their fields cannot be filtered. --- apps/api/mixins.py | 37 +++++++++++++++++++++++++++++++++++ apps/core/serializers.py | 5 +++-- apps/swid/serializers.py | 7 ++++--- tests/test_api.py | 42 ++++++++++++++++++++++++++++++++++++++-- 4 files changed, 84 insertions(+), 7 deletions(-) create mode 100644 apps/api/mixins.py diff --git a/apps/api/mixins.py b/apps/api/mixins.py new file mode 100644 index 00000000..b09381c7 --- /dev/null +++ b/apps/api/mixins.py @@ -0,0 +1,37 @@ +# -*- coding: utf-8 -*- +from __future__ import print_function, division, absolute_import, unicode_literals + +try: + from djangorestframework_camel_case.parser import camel_to_underscore +except ImportError: + camel_to_underscore = lambda x: x + + +class DynamicFieldsMixin(object): + """ + A serializer mixin that takes an additional `fields` argument that controls + which fields should be displayed. + + If the djangorestframework_camel_case package is installed, the field names + are converted from camelCase to under_scores. + + Usage:: + + class MySerializer(DynamicFieldsMixin, serializers.HyperlinkedModelSerializer): + class Meta: + model = MyModel + + """ + def __init__(self, *args, **kwargs): + super(DynamicFieldsMixin, self).__init__(*args, **kwargs) + request = self.context.get('request') + if request: + fields = request.QUERY_PARAMS.get('fields') + if fields: + fields = fields.split(',') + fields = map(camel_to_underscore, fields) + # Drop any fields that are not specified in the `fields` argument. + allowed = set(fields) + existing = set(self.fields.keys()) + for field_name in existing - allowed: + self.fields.pop(field_name) diff --git a/apps/core/serializers.py b/apps/core/serializers.py index b64b66e5..0ac780b5 100644 --- a/apps/core/serializers.py +++ b/apps/core/serializers.py @@ -3,16 +3,17 @@ from rest_framework import serializers +from apps.api.mixins import DynamicFieldsMixin from . import models -class IdentitySerializer(serializers.HyperlinkedModelSerializer): +class IdentitySerializer(DynamicFieldsMixin, serializers.HyperlinkedModelSerializer): class Meta: model = models.Identity fields = ('id', 'uri', 'type', 'data') -class SessionSerializer(serializers.HyperlinkedModelSerializer): +class SessionSerializer(DynamicFieldsMixin, serializers.HyperlinkedModelSerializer): # PrimaryKey fields are only needed until endpoints exists device = serializers.PrimaryKeyRelatedField() diff --git a/apps/swid/serializers.py b/apps/swid/serializers.py index d70d7257..37691ebf 100644 --- a/apps/swid/serializers.py +++ b/apps/swid/serializers.py @@ -3,22 +3,23 @@ from rest_framework import serializers +from apps.api.mixins import DynamicFieldsMixin from . import models -class EntitySerializer(serializers.HyperlinkedModelSerializer): +class EntitySerializer(DynamicFieldsMixin, serializers.HyperlinkedModelSerializer): class Meta: model = models.Entity fields = ('id', 'uri', 'name', 'regid') -class EntityRoleSerializer(serializers.HyperlinkedModelSerializer): +class EntityRoleSerializer(DynamicFieldsMixin, serializers.HyperlinkedModelSerializer): class Meta: model = models.EntityRole fields = ('entity', 'role') -class TagSerializer(serializers.HyperlinkedModelSerializer): +class TagSerializer(DynamicFieldsMixin, serializers.HyperlinkedModelSerializer): entities = EntityRoleSerializer(source='entityrole_set', many=True) class Meta: diff --git a/tests/test_api.py b/tests/test_api.py index e584f28a..9c4b58b5 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -1,13 +1,14 @@ # -*- coding: utf-8 -*- from __future__ import print_function, division, absolute_import, unicode_literals -import pytest -from model_mommy import mommy +import json from django.contrib.auth.models import User from django.core.urlresolvers import reverse from django.utils import timezone +import pytest +from model_mommy import mommy from rest_framework.test import APIClient from rest_framework import status @@ -172,3 +173,40 @@ def test_invalid_tag(api_client, filename): def test_invalid_xml(api_client): response = api_client.post(reverse('swid-add-tags'), [" Date: Sat, 31 May 2014 16:18:19 +0200 Subject: [PATCH 35/51] Revised SWID inventory --- apps/devices/ajax.py | 29 --- apps/devices/templates/devices/session.html | 4 + apps/front/ajax.py | 2 + apps/front/static/css/base.css | 10 + apps/front/static/js/paging.js | 46 +++-- apps/front/templates/front/paged_block.html | 2 + apps/front/templatetags/paged_block.py | 4 +- apps/swid/ajax.py | 52 +++-- apps/swid/paging.py | 43 +++- apps/swid/static/js/swid-inventory.js | 189 +++++++----------- apps/swid/static/js/swid-log.js | 4 +- .../paging/swid_inventory_session_list.html | 36 ++++ apps/swid/templates/swid/swid_inventory.html | 111 +++++----- apps/swid/templates/swid/swid_log.html | 37 ++-- tests/test_ajax.py | 24 --- tests/test_auth.py | 3 +- 16 files changed, 317 insertions(+), 279 deletions(-) delete mode 100644 apps/devices/ajax.py create mode 100644 apps/swid/templates/swid/paging/swid_inventory_session_list.html diff --git a/apps/devices/ajax.py b/apps/devices/ajax.py deleted file mode 100644 index aa922c55..00000000 --- a/apps/devices/ajax.py +++ /dev/null @@ -1,29 +0,0 @@ -# -*- coding: utf-8 -*- -from __future__ import print_function, division, absolute_import, unicode_literals - -import json - -from dajaxice.decorators import dajaxice_register - -from apps.core.decorators import ajax_login_required -from .models import Device -from apps.front.utils import local_dtstring - - -@dajaxice_register -@ajax_login_required -def sessions_for_device(request, device_id, date_from, date_to): - device = Device.objects.get(pk=device_id) - sessions = device.get_sessions_in_range(date_from, date_to) - - data = { - 'sessions': [ - { - 'id': s.id, - 'time': local_dtstring(s.time) - } - for s in sessions - ] - } - - return json.dumps(data) diff --git a/apps/devices/templates/devices/session.html b/apps/devices/templates/devices/session.html index 89bfdeb9..5b8f2615 100644 --- a/apps/devices/templates/devices/session.html +++ b/apps/devices/templates/devices/session.html @@ -41,6 +41,9 @@

{% trans 'Session Info' %}

+ {% comment %} + Button is temporary disabled, the swid-inventory must be modified in order + to support this functionality. + {% endcomment %}

{% trans 'Results' %}

diff --git a/apps/front/ajax.py b/apps/front/ajax.py index 50a0569d..4df173e0 100644 --- a/apps/front/ajax.py +++ b/apps/front/ajax.py @@ -10,6 +10,7 @@ from . import paging as paging_functions from apps.swid.paging import regid_detail_paging, regid_list_paging, swid_list_paging from apps.swid.paging import swid_inventory_list_paging, swid_log_list_paging +from apps.swid.paging import swid_inventory_session_paging from apps.filesystem.paging import dir_list_paging, file_list_paging from apps.policies.paging import policy_list_paging, enforcement_list_paging from apps.packages.paging import package_list_paging @@ -69,6 +70,7 @@ def paging(request, config_name, current_page, filter_query, pager_id, producer_ 'device_session_list_config': device_session_list_paging, 'swid_inventory_list_config': swid_inventory_list_paging, 'swid_log_list_config': swid_log_list_paging, + 'swid_inventory_session_list_config': swid_inventory_session_paging, } conf = paging_conf_dict[config_name] diff --git a/apps/front/static/css/base.css b/apps/front/static/css/base.css index ab1e5dc8..622a1c0f 100644 --- a/apps/front/static/css/base.css +++ b/apps/front/static/css/base.css @@ -160,3 +160,13 @@ th.dateWidthFixed { span.highlight { font-weight: bold; } + +a.disabled { + color: #ddd; +} + +.tagCount { + min-width: 23px; + display: inline-block; + text-align: center; +} diff --git a/apps/front/static/js/paging.js b/apps/front/static/js/paging.js index 6e16eafe..4479b133 100644 --- a/apps/front/static/js/paging.js +++ b/apps/front/static/js/paging.js @@ -1,6 +1,7 @@ $(document).ready(function() { // initialize paging // static initializer see bottom of file + Pager.count = 0; Pager.init(); }); @@ -19,8 +20,11 @@ var Pager = function() { // get setup values this.config = this.$ctx.data('config'); this.filter = this.$ctx.data('filter'); + this.doInitialRequest = (this.$ctx.data('initial').toLowerCase() === "true"); + this.useURLParams = (this.$ctx.data('urlparams').toLowerCase() === "true"); this.args = this.$ctx.data('args'); this.currentPageIdx = 0; + this.afterPagingCallbacks = []; // get containers and buttons this.$buttonContainer = $('.paging-buttons', this.$ctx); @@ -39,14 +43,18 @@ var Pager = function() { this.addFilter(); } - // register hashChanged events - HashQuery.addChangedListener(this.pageParam, this.grabPageParam.bind(this)); - HashQuery.addChangedListener(this.filterParam, this.grabFilterParam.bind(this)); + if(this.useURLParams) { + // register hashChanged events + HashQuery.addChangedListener(this.pageParam, this.grabPageParam.bind(this)); + HashQuery.addChangedListener(this.filterParam, this.grabFilterParam.bind(this)); + } - // get initial page - this.initial = true; - this.getInitalURLParam(); - this.getPage(); + if(this.doInitialRequest) { + // get initial page + this.initial = true; + this.getInitalURLParam(); + this.getPage(); + } // place handle to the pager object this.$ctx.data('pager', this); @@ -117,6 +125,7 @@ var Pager = function() { this.setURLParam(this.pageParam, this.currentPageIdx, this.initial); this.initial = false; this.loading = false; + this.afterPaging(); }; this.updateStatus = function() { @@ -190,6 +199,7 @@ var Pager = function() { }; this.setURLParam = function(hashKey, hashValue, avoidHistory) { + if(!this.useURLParams) return; var hashKeyObj = {}; hashKeyObj[hashKey] = hashValue; HashQuery.setHashKey(hashKeyObj, false, avoidHistory); @@ -229,17 +239,29 @@ var Pager = function() { this.$filterInput.val(''); } }; + + this.onAfterPaging = function(callback) { + this.afterPagingCallbacks.push(callback); + }; + + this.afterPaging = function() { + $.each(this.afterPagingCallbacks, function(i, callback) { + callback(this); + }.bind(this)); + }; }; // static initalizer // creates an instance for every paged table found // on the current page Pager.init = function() { - Pager.count = 0; $('.ajax-paged').each(function() { - var p = new Pager(); - p.uid = Pager.count++; - p.$ctx = $(this); - p.init(); + var $ctx = $(this); + if(!$ctx.data('pager')) { + var p = new Pager(); + p.uid = Pager.count++; + p.$ctx = $ctx; + p.init(); + } }); }; diff --git a/apps/front/templates/front/paged_block.html b/apps/front/templates/front/paged_block.html index caacc14f..69539347 100644 --- a/apps/front/templates/front/paged_block.html +++ b/apps/front/templates/front/paged_block.html @@ -1,6 +1,8 @@
0 + + return sessions + + +def swid_inventory_session_stat_producer(page_size, filter_query, dynamic_params=None, static_params=None): + if not dynamic_params: + return 0 + sessions = get_device_sessions(dynamic_params) + return math.ceil(len(sessions) / page_size) + + +def get_device_sessions(dynamic_params): + device_id = dynamic_params.get('device_id') + from_timestamp = dynamic_params.get('from_timestamp') + to_timestamp = dynamic_params.get('to_timestamp') + + device = Device.objects.get(pk=device_id) + return device.get_sessions_in_range(from_timestamp, to_timestamp) + # PAGING CONFIGS regid_list_paging = { @@ -216,12 +247,20 @@ def session_tag_difference(curr_session, prev_session): 'page_size': 50, } +swid_inventory_session_paging = { + 'template_name': 'swid/paging/swid_inventory_session_list', + 'list_producer': swid_inventory_session_list_producer, + 'stat_producer': swid_inventory_session_stat_producer, + 'url_name': 'swid:tag_detail', + 'page_size': 10, +} + swid_inventory_list_paging = { 'template_name': 'swid/paging/swid_inventory_list', 'list_producer': swid_inventory_list_producer, 'stat_producer': swid_inventory_stat_producer, 'url_name': 'swid:tag_detail', - 'page_size': 50, + 'page_size': 10, } swid_log_list_paging = { diff --git a/apps/swid/static/js/swid-inventory.js b/apps/swid/static/js/swid-inventory.js index d76a840d..fb23bd19 100644 --- a/apps/swid/static/js/swid-inventory.js +++ b/apps/swid/static/js/swid-inventory.js @@ -1,51 +1,63 @@ -var session_data = []; function AjaxTagsLoader() { - this.loadTags = function (sessionID) { - var pager = $('.ajax-paged').data('pager'); + this.loadTags = function (sessionID, $pagerContext) { + var pager = $('.ajax-paged', $pagerContext).data('pager'); pager.reset(); pager.setProducerArgs({'session_id': sessionID}); pager.getPage(); - - Dajaxice.apps.swid.get_tag_stats(this.updateStats, { - 'session_id': sessionID - }); - }; - - this.updateStats = function (data) { - $("#swid-tag-count").text(data['swid-tag-count']); - $("#swid-newtag-count").text(data['new-swid-tag-count']); }; } function AjaxSessionsLoader() { this.deviceId = $("#device-id").val(); - this.updateSelect = function (data) { - ajaxSpinner.disable(); - session_data = data.sessions; - $("#num-of-sessions").text(data.sessions.length); - }; this.loadSessions = function () { var fromTimestamp = parseInt(HashQuery.getHashQueryObject()['from']); var toTimestamp = parseInt(HashQuery.getHashQueryObject()['to']); if (!fromTimestamp || !toTimestamp) return; - ajaxSpinner.enable(); - Dajaxice.apps.devices.sessions_for_device(this.updateSelect, { + + var pager = $('.ajax-paged').data('pager'); + pager.reset(); + pager.setProducerArgs({ 'device_id': this.deviceId, - 'date_from': fromTimestamp, - 'date_to': toTimestamp + 'from_timestamp': fromTimestamp, + 'to_timestamp': toTimestamp }); + pager.onAfterPaging( + function() { + Pager.init(); + } + ); + pager.getPage(); + Dajaxice.apps.swid.get_tag_inventory_stats(this.updateStats, { + 'device_id': this.deviceId, + 'from_timestamp': fromTimestamp, + 'to_timestamp': toTimestamp + }, + {'error_callback': function() { + alert('Error: Could not fetch tag inventory stats.'); + }}); + }; + + this.updateStats = function(data) { + $('.sessionCount').text(data.session_count); + $('.firstSession').text(data.fist_session); + $('.lastSession').text(data.last_session); }; } -function setupResetButton() { +function setupResetButton(fromDatepicker, toDatepicker, sessionsLoader) { $("#session-filter-reset").click(function () { - $("#calendar-shortcuts").prop("selectedIndex", 1); - }) + $("#calendar-shortcuts").prop("selectedIndex", 0); + fromDatepicker.datepicker("setDate", new Date()); + toDatepicker.datepicker("setDate", new Date()); + setFromDateHash(fromDatepicker); + setToDateHash(toDatepicker); + sessionsLoader.loadSessions(); + }); } -function setupRangeShortcutsDropdown(fromDatepicker, toDatepicker) { +function setupRangeShortcutsDropdown(fromDatepicker, toDatepicker, sessionsLoader) { $("#calendar-shortcuts").change(function () { fromDatepicker.datepicker("setDate", $(this).val()); toDatepicker.datepicker("setDate", new Date()); @@ -53,65 +65,41 @@ function setupRangeShortcutsDropdown(fromDatepicker, toDatepicker) { 'from': fromDatepicker.datepicker("getDate").getTime() / 1000, 'to': toDatepicker.datepicker("getDate").getTime() / 1000 }, true); - - HashQuery.sendChanged('from', fromDatepicker.datepicker("getDate")); + sessionsLoader.loadSessions(); } ); } -function setUpSelect() { - var convertResultToString = function (sessionObject) { - return sessionObject.time; - }; - - $("#num-of-sessions").text(session_data.length); - $('#sessions').select2({ - data: function () { - return { - results: session_data, - text: 'time' - } - }, - formatSelection: convertResultToString, - formatResult: convertResultToString, - placeholder: "Select a Session", - width: "element", - minimumResultsForSearch: -1, - formatNoMatches: "No Session found in the given time range" - } - ); - $("#sessions").on("select2-selecting", function (event) { - HashQuery.setHashKey({'session-id': event.val}) - }); -} - function setupDatepicker(fromDatepicker, toDatepicker) { fromDatepicker.datepicker({ - defaultDate: "-1w", - dateFormat: "dd/mm/yy", + defaultDate: new Date(), + dateFormat: "M dd. yy", changeMonth: true, numberOfMonths: 1, onSelect: function (selectedDate) { - toDatepicker.datepicker("option", "minDate", selectedDate); - var fromTimestamp = $(this).datepicker("getDate").getTime() / 1000; - HashQuery.setHashKey({'from': fromTimestamp}); + $("#calendar-shortcuts").prop("selectedIndex", 0); + toDatepicker.datepicker("option", {"minDate": selectedDate}); + setFromDateHash(fromDatepicker); } }); toDatepicker.datepicker({ - changeMonth: true, - dateFormat: "dd/mm/yy", defaultDate: new Date(), + changeMonth: true, + dateFormat: "M dd. yy", numberOfMonths: 1, onSelect: function (selectedDate) { - var toTimestamp = $(this).datepicker("getDate").getTime() / 1000; - HashQuery.setHashKey({'to': toTimestamp}); - fromDatepicker.datepicker("option", "maxDate", selectedDate); + $("#calendar-shortcuts").prop("selectedIndex", 0); + fromDatepicker.datepicker("option", {"maxDate": selectedDate}); + setToDateHash(toDatepicker); } }); - fromDatepicker.datepicker("setDate", '-1w'); - toDatepicker.datepicker("setDate", new Date()); + var today = new Date(); + fromDatepicker.datepicker("setDate", today); + fromDatepicker.datepicker("option", "maxDate", today); + toDatepicker.datepicker("setDate", today); + toDatepicker.datepicker("option", "minDate", today); $("#from-btn").click(function () { fromDatepicker.datepicker("show"); @@ -120,77 +108,52 @@ function setupDatepicker(fromDatepicker, toDatepicker) { $("#to-btn").click(function () { toDatepicker.datepicker("show"); }); +} +function setFromDateHash(fromDatepicker) { + var fromTimestamp = fromDatepicker.datepicker("getDate").getTime() / 1000; + HashQuery.setHashKey({'from': fromTimestamp}); } -function loadSingleSession(fromDatepicker, toDatepicker, sessionId) { - Dajaxice.apps.swid.session_info(function (data) { - - $("#for-session").text(data.time); - session_data = [ - {"id": data.id, "time": data.time} - ]; - $("#sessions").select2("val", data.id); - $("#num-of-sessions").text("1"); - fromDatepicker.datepicker("setDate", null); - toDatepicker.datepicker("setDate", null); - HashQuery.sendChanged('session-id', data.id); - - }, { - 'session_id': sessionId - }); +function setToDateHash(toDatepicker) { + var toTimestamp = toDatepicker.datepicker("getDate").getTime() / 1000; + HashQuery.setHashKey({'to': toTimestamp}); } $(document).ready(function () { var fromDatepicker = $("#from"); var toDatepicker = $("#to"); - var sessionDropdown = $("#sessions"); - $("#swid-tags").hide(); var sessionsLoader = new AjaxSessionsLoader(); var tagsLoader = new AjaxTagsLoader(); // setup components setupDatepicker(fromDatepicker, toDatepicker); - setUpSelect(); - setupRangeShortcutsDropdown(fromDatepicker, toDatepicker); - setupResetButton(); - - // register event listeners - HashQuery.addChangedListener('session-id', function (key, value) { - var logLink = $("#swid-log-link"); - var old_link = logLink.prop("href").split("#")[0]; - logLink.prop("href", old_link + "#session-id=" + value); - - tagsLoader.loadTags(value); - var result = $.grep(session_data, function (e) { - return e.id == value - }); - - // session-id is not available in the sessions dropdown - if (!result.length) { - var sessionId = HashQuery.getHashQueryObject()['session-id'] - loadSingleSession(fromDatepicker, toDatepicker, sessionId); - } - else { - sessionDropdown.select2("val", value); - var timeString = sessionDropdown.select2("data")["time"]; - $("#for-session").text(timeString); + setupRangeShortcutsDropdown(fromDatepicker, toDatepicker, sessionsLoader); + setupResetButton(fromDatepicker, toDatepicker, sessionsLoader); + + $('body').on('show', '#sessionAccordion', function (event) { + var $triggeredSection = $(event.target); + if(!$triggeredSection.data('loaded')) { + $triggeredSection.data('loaded', true); + var sessionId = $triggeredSection.data('sessionid'); + tagsLoader.loadTags(sessionId, $triggeredSection); } }); - HashQuery.addChangedListener('from', function (key, value) { + HashQuery.addChangedListener('from', function () { sessionsLoader.loadSessions(); }); - HashQuery.addChangedListener('to', function (key, value) { + HashQuery.addChangedListener('to', function () { sessionsLoader.loadSessions(); }); - // view was called from session view - if (HashQuery.getHashQueryObject()['session-id']) { - var sessionId = HashQuery.getHashQueryObject()['session-id'] - loadSingleSession(fromDatepicker, toDatepicker, sessionId); + var hashQueryObject = HashQuery.getHashQueryObject(); + if(hashQueryObject['to'] && hashQueryObject['from']) { + fromDatepicker.datepicker("setDate", new Date(hashQueryObject['from'] * 1000)); + toDatepicker.datepicker("setDate", new Date(hashQueryObject['to'] * 1000)); } + sessionsLoader.loadSessions(); }); diff --git a/apps/swid/static/js/swid-log.js b/apps/swid/static/js/swid-log.js index 5c3f6597..79f1f130 100644 --- a/apps/swid/static/js/swid-log.js +++ b/apps/swid/static/js/swid-log.js @@ -7,14 +7,14 @@ $(document).ready(function() { var initDateTimePicker = function() { var fromPicker = $('#from').datepicker({ - dateFormat: "dd/mm/yy", + dateFormat: "M dd. yy", onClose: function (selectedDate) { getTagList(); } }); var toPicker = $('#to').datepicker({ - dateFormat: "dd/mm/yy", + dateFormat: "M dd. yy", onClose: function (selectedDate) { getTagList(); } diff --git a/apps/swid/templates/swid/paging/swid_inventory_session_list.html b/apps/swid/templates/swid/paging/swid_inventory_session_list.html new file mode 100644 index 00000000..fe5933a1 --- /dev/null +++ b/apps/swid/templates/swid/paging/swid_inventory_session_list.html @@ -0,0 +1,36 @@ +{% load i18n %} +{% load paged_block %} + +{% if object_list %} + +{% else %} +

+ {% trans 'No sessions in the given range' %} +

+{% endif %} diff --git a/apps/swid/templates/swid/swid_inventory.html b/apps/swid/templates/swid/swid_inventory.html index b8e5d238..f34492e4 100644 --- a/apps/swid/templates/swid/swid_inventory.html +++ b/apps/swid/templates/swid/swid_inventory.html @@ -30,81 +30,68 @@

{% trans 'No sessions found!' %}

{% else %}
-

View Sessions

- -
1. Filter Session Range
+

{% trans 'Define range' %}

- -
- - -
- +
+ + +
+ + +
+ + -
- - +
+ + +
+
- - -
- + + + + + + +
- -
-
-
2. Choose from available Sessions
-
- - 0 sessions -
-
-
-
-
-

{% trans 'Measured Software Tags' %}

-
-
-
- - - - - - - - - - - - - - - - -
{% trans 'Session Date' %} - {% trans 'Please select session first' %} - -
{% trans 'Total tags for this session' %}-
{% trans 'Tags first reported in this session' %}-
-
-
-
- {% paged_block config_name='swid_inventory_list_config' with_filter=True %} +

{% trans 'Range summary' %}

+
+
+ + + + + + + + + + + + + + + +
{% trans 'Sessions in range' %}0
{% trans 'Oldest session in range' %}{% trans 'None' %}
{% trans 'Latest session in range' %}{% trans 'None' %}
+

{% trans 'Sessions' %}

+ + {% paged_block config_name="swid_inventory_session_list_config" initial_load=False %} + {% endif %} {% endblock %} diff --git a/apps/swid/templates/swid/swid_log.html b/apps/swid/templates/swid/swid_log.html index 0f8d7e39..32839c31 100644 --- a/apps/swid/templates/swid/swid_log.html +++ b/apps/swid/templates/swid/swid_log.html @@ -23,34 +23,35 @@

{% trans 'Set date range' %}

- +
+ -
- - -
+
+ + +
- + -
- - +
+ + +
+
- - -
- + + + - +
- -

{% trans 'Changes summary' %}

diff --git a/tests/test_ajax.py b/tests/test_ajax.py index 66b96dd1..275090d4 100644 --- a/tests/test_ajax.py +++ b/tests/test_ajax.py @@ -83,15 +83,6 @@ def sessions_test_data(transactional_db): mommy.make(Session, id=4, time=now - timedelta(days=3), device__id=1) -@pytest.fixture -def get_sessions(client, sessions_test_data): - def _query(device_id, date_from, date_to): - payload = {'device_id': device_id, 'date_from': date_from, 'date_to': date_to} - response_data = ajax_request(client, 'apps.devices.sessions_for_device', payload) - return response_data['sessions'] - return _query - - @pytest.fixture def get_completions(client, files_and_directories_test_data): """ @@ -158,18 +149,3 @@ def test_files_autocomplete(get_completions, search_term, expected): def test_directory_autocomplete(get_completions, search_term, expected): results = get_completions(search_term, 'apps.filesystem.directories_autocomplete', 'directory') assert sorted(results) == sorted(expected) - - -### Session Tests ### - -@pytest.mark.parametrize('from_diff_to_now, to_diff_to_now, expected', [ - (-2, +2, 2), - (-3, +3, 4) -]) -def test_sessions(get_sessions, from_diff_to_now, to_diff_to_now, expected): - now = timezone.now() - date_from = calendar.timegm((now + timedelta(days=from_diff_to_now)).utctimetuple()) - date_to = calendar.timegm((now + timedelta(days=to_diff_to_now)).utctimetuple()) - results = get_sessions(1, date_from, date_to) - assert len(results) == expected, '%i instead of %i sessions found in the given time range' % \ - (len(results), expected) diff --git a/tests/test_auth.py b/tests/test_auth.py index e5178780..9473e5ee 100644 --- a/tests/test_auth.py +++ b/tests/test_auth.py @@ -156,9 +156,8 @@ def test_write_permission_enforced(client, strongtnc_users, url, method): @pytest.mark.parametrize('endpoint, payload', [ ('apps.filesystem.files_autocomplete', {'search_term': 'bash'}), ('apps.filesystem.directories_autocomplete', {'search_term': 'bash'}), - ('apps.swid.get_tag_stats', {'session_id': 1}), + ('apps.swid.get_tag_inventory_stats', {'device_id': 1, 'date_from': '', 'date_to': ''}), ('apps.swid.get_tag_log_stats', {'device_id': 1, 'date_from': '', 'date_to': ''}), - ('apps.devices.sessions_for_device', {'device_id': 1, 'date_from': '', 'date_to': ''}), ('apps.front.paging', {'template': '', 'list_producer': '', 'stat_producer': '', 'var_name': '', 'url_name': '', 'current_page': '', 'page_size': '', 'filter_query': '', 'pager_id': ''}), ]) From c19ef9d61f32b6040954cfddf5a0b0a09951acda Mon Sep 17 00:00:00 2001 From: Danilo Bargen Date: Sun, 1 Jun 2014 17:39:52 +0200 Subject: [PATCH 36/51] Removed all users and sessions from default db --- django.db | Bin 41984 -> 41984 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/django.db b/django.db index ef5c6ef6860e26f15ef7a7bfcdc0b2f7851069e0..9b08a0bd31fbb2df7131f19b88beb279ef8afc08 100644 GIT binary patch delta 95 zcmZoT!PIbqX@WFw3j+g#CJ<`?G1Ej1bH&ZtHMcpOe_&R)Bp!-MP|Mj0xV+ZisPrn%nz5E|EIY0_s^-nTp6X(Xi)ml7Es@sRGe+kMYyhVH%E$nZLDEoHVnnx0y)s~9aI znC8SZ!>2fogBVZVOiUeJNhXsoaMTFOr3br1HQ2NJ%gow9Fdi_>j$1Dt)Rj(I+S%`D zBPqjn^TzvIY<_cR%MWVYTRTIi+$dDq1;^K1rd?(3_d6Ta zGPYG)kj>H{*m~gg&SHthC4|x}mnQp)P$2oCjH=gcox8_GAxGR264HoIb4-dy7-JX} z1pt4bz(2sB$!&Ta-M4N9q4&p~`)LkIkzSZUoeq3on zXgB&ID=`%bi@l~7)UD1y5L%67^d`%39o{e*#2J1l_ zQALnC!GYLzM^Z=hQQzVu9`!p0=l2+VEmEq8wN|aeHlSsWOt(^L@e+$o4+cIQRqMUc zY@{@-H#?l*8rFCO&G8tyUawB#KJt3wTApnW4nU;E*X(+?<8uU;dC+WjS*G8Ss@$OG zHY9Fn59VX1f)=*iR`-DMY@@-oOnW@GxrXltm5L|fF8%oZnVES29#Y^_@N@9+Xp#q^ z2(D8yO@IAeI(e~;P#Q65j7gDVh84kWwBG7B`@wG8tu3`HcFpY5C^5CX^F>R@r?Hfh zSj0#e+(N3+I5lDEsmaQj$#UGZC=JPmWH*QeNdm8(>~?DQxIFU?p{!>VcorAVPam~r z7HE1(SkJA#aW7t-19KGkEBGDw75Fizfeo+%<`RE9n*2Tyes_wJUqys0PFq3{qLx^E z6UZl>`n*`g=#m)5le)->BE@FSeQaQPa5&sxj5?7CCsE*A&rF;5*S`$W>3UK#z$hr#bTIZB@QtnI1?|E zE+@`KT~0n?3JW5G#W-jHJS71J{{&BuZ_NA!L_uBzU>49cNW{PN*|3lx6nx5VeaI1ny%D| zUYIMNjtN&a!=!PIV0|cGCh72bme#ximse%1jk9dd&Ov=MM@lJ&mCg0BHa15DW01?l zHGq`yen8f-uKt%;uYzixSGGrOngB9hCDnuoO~EQo6O2* z=jNob7>DC*;ROdN$uL$*` Date: Mon, 2 Jun 2014 01:17:35 +0200 Subject: [PATCH 37/51] Switched to custom API permission class Access to the API is granted in two cases: - User is an admin (`is_staff=True`) - User has the `auth.write_access` permission Otherwise, the user is rejected with a HTTP 401 or HTTP 403 status. --- apps/api/urls.py | 3 +++ apps/auth/permissions.py | 22 ++++++++++++++++++++ config/settings.py | 4 ++-- tests/test_api.py | 44 +++++++++++++++++++++++++++++++++++++++- 4 files changed, 70 insertions(+), 3 deletions(-) diff --git a/apps/api/urls.py b/apps/api/urls.py index a91243fe..4f6fb1d8 100644 --- a/apps/api/urls.py +++ b/apps/api/urls.py @@ -23,6 +23,9 @@ # Register additional endpoints urlpatterns += patterns('', + # Auth views + url(r'^api-auth/', include('rest_framework.urls', namespace='rest_framework')), + # Add tags url(r'^swid/add-tags/', TagAddView.as_view(), name='swid-add-tags'), url(r'^swid/add-tags/\.(?P[a-z0-9]+)', TagAddView.as_view(), name='swid-add-tags'), diff --git a/apps/auth/permissions.py b/apps/auth/permissions.py index e694c82e..773cf649 100644 --- a/apps/auth/permissions.py +++ b/apps/auth/permissions.py @@ -19,6 +19,8 @@ from django.contrib.auth.models import Permission from django.contrib.contenttypes.models import ContentType +from rest_framework import permissions, exceptions + class GlobalPermissionManager(models.Manager): """ @@ -48,3 +50,23 @@ def save(self, *args, **kwargs): # Assign the 'global_permission' content type to this permission self.content_type = ct super(GlobalPermission, self).save(*args, **kwargs) + + +class IsStaffOrHasWritePerm(permissions.BasePermission): + """ + Django Rest Framework permission class. + + It allows access to the API if it has the `is_staff` flag set, or + if it has the global `auth.write_access` permission assigned. (See + `apps/auth/management/commands/setpassword.py` to see an example on + how to assign that permission programmatically.) + + """ + def has_permission(self, request, view): + if not request.user.is_authenticated(): + return False + if request.user.is_staff: + return True + if request.user.has_perm('auth.write_access'): + return True + return False diff --git a/config/settings.py b/config/settings.py index 2bcc5e04..fc11475e 100644 --- a/config/settings.py +++ b/config/settings.py @@ -286,10 +286,10 @@ def show_debug_toolbar(request): ), 'DEFAULT_AUTHENTICATION_CLASSES': ( 'rest_framework.authentication.SessionAuthentication', - 'rest_framework.authentication.BasicAuthentication' + 'rest_framework.authentication.BasicAuthentication', ), 'DEFAULT_PERMISSION_CLASSES': ( - 'rest_framework.permissions.IsAuthenticatedOrReadOnly', + 'apps.auth.permissions.IsStaffOrHasWritePerm', ), 'DEFAULT_FILTER_BACKENDS': ( 'rest_framework.filters.DjangoFilterBackend', diff --git a/tests/test_api.py b/tests/test_api.py index 9c4b58b5..3effc0ce 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -13,6 +13,7 @@ from rest_framework import status from .test_swid import swidtag # NOQA +from apps.auth.permissions import GlobalPermission from apps.swid import utils from apps.swid.models import Tag from apps.core.models import Session @@ -23,7 +24,7 @@ def api_client(transactional_db): """ Return an authenticated API client. """ - user = User.objects.create_superuser(username='api-test', password='api-test', + user = User.objects.create_user(username='api-test', password='api-test', email="api-test@example.com") user.is_staff = True user.save() @@ -43,6 +44,47 @@ def session(transactional_db): return session +@pytest.mark.django_db +class TestApiAuth(object): + + def _do_request(self, user): + client = APIClient() + if user: + client.force_authenticate(user=user) + return client.get('/api/') + + def test_anon(self): + response = self._do_request(user=None) + # TODO: Change the following return code to HTTP 401 if + # https://github.com/tomchristie/django-rest-framework/pull/1611 gets merged. + #assert response.status_code == status.HTTP_401_UNAUTHORIZED + assert response.status_code == status.HTTP_403_FORBIDDEN + + def test_root(self): + user = User.objects.create_superuser(username='root', password='root', email='a@b.com') + response = self._do_request(user) + assert response.status_code == status.HTTP_200_OK + + def test_normal_user(self): + user = User.objects.create_user(username='user', password='1234', email='a@b.com') + response = self._do_request(user) + assert response.status_code == status.HTTP_403_FORBIDDEN + + def test_staff_user(self): + user = User.objects.create_user(username='staff', password='1234', email='a@b.com') + user.is_staff = True + user.save() + response = self._do_request(user) + assert response.status_code == status.HTTP_200_OK + + def test_write_access_user(self): + user = User.objects.create_user(username='admin', password='1234', email='a@b.com') + perm, _ = GlobalPermission.objects.get_or_create(codename='write_access') + user.user_permissions.add(perm) + response = self._do_request(user) + assert response.status_code == status.HTTP_200_OK + + @pytest.mark.parametrize(['url', 'list_name'], [ (reverse('session-swid-measurement', args=[1]), 'software IDs'), (reverse('swid-add-tags'), 'SWID tags'), From efb7611730c2475d63263ef6f61b84ddaf3a0db5 Mon Sep 17 00:00:00 2001 From: Danilo Bargen Date: Mon, 2 Jun 2014 01:34:28 +0200 Subject: [PATCH 38/51] Added CSRF_COOKIE_SECURE to settings.ini --- config/settings.py | 6 ++++++ config/settings.sample.ini | 4 ++++ 2 files changed, 10 insertions(+) diff --git a/config/settings.py b/config/settings.py index fc11475e..919026f1 100644 --- a/config/settings.py +++ b/config/settings.py @@ -47,6 +47,12 @@ except (NoSectionError, NoOptionError): ALLOWED_HOSTS = [] +# Security +try: + CSRF_COOKIE_SECURE = config.getboolean('security', 'CSRF_COOKIE_SECURE') +except (NoSectionError, NoOptionError): + CSRF_COOKIE_SECURE = False + # Database configuration DATABASES = {} try: diff --git a/config/settings.sample.ini b/config/settings.sample.ini index 0b901a72..f06de768 100644 --- a/config/settings.sample.ini +++ b/config/settings.sample.ini @@ -40,6 +40,10 @@ STATIC_ROOT = static ; List the allowed hostnames that strongTNC can serve here. ; https://docs.djangoproject.com/en/dev/ref/settings/#allowed-hosts ;ALLOWED_HOSTS = 127.0.0.1,yourdomain.com +; Enable the following to mark the CSRF cookie as "secure", which means +; browsers may ensure that the cookie is only sent under an HTTPS connection. +; This setting should be enabled in production. +CSRF_COOKIE_SECURE = 0 [localization] LANGUAGE_CODE = en-us From 61c42978c55ec4740304ae8a163b7e059043cbec Mon Sep 17 00:00:00 2001 From: Danilo Bargen Date: Mon, 2 Jun 2014 01:37:35 +0200 Subject: [PATCH 39/51] Enabled clickjacking protection --- config/settings.py | 1 + 1 file changed, 1 insertion(+) diff --git a/config/settings.py b/config/settings.py index 919026f1..fdc10bd7 100644 --- a/config/settings.py +++ b/config/settings.py @@ -179,6 +179,7 @@ 'django.middleware.common.CommonMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware', 'django.middleware.csrf.CsrfViewMiddleware', + 'django.middleware.clickjacking.XFrameOptionsMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware', 'django.contrib.messages.middleware.MessageMiddleware', ) From 9df83b66455ce6c694a5eaf972130633390d5fd0 Mon Sep 17 00:00:00 2001 From: Jonas Furrer Date: Sat, 31 May 2014 21:41:30 +0200 Subject: [PATCH 40/51] Added paging to potentially large tables --- apps/devices/paging.py | 23 ++++++++ apps/devices/product_views.py | 1 + .../templates/devices/paging/device_list.html | 21 ++++++++ apps/devices/templates/devices/products.html | 21 +------- apps/filesystem/directory_views.py | 1 + apps/filesystem/paging.py | 28 ++++++++++ .../templates/filesystem/directories.html | 25 ++------- .../filesystem/paging/file_list.html | 24 +++++++++ .../filesystem/paging/simple_file_list.html | 25 +++++++++ apps/front/ajax.py | 9 +++- apps/swid/paging.py | 48 ++++++++++++++++- .../swid/paging/swid_devices_list.html | 26 +++++++++ apps/swid/templates/swid/tags_detail.html | 53 ++----------------- apps/swid/views.py | 1 + 14 files changed, 215 insertions(+), 91 deletions(-) create mode 100644 apps/devices/templates/devices/paging/device_list.html create mode 100644 apps/filesystem/templates/filesystem/paging/file_list.html create mode 100644 apps/filesystem/templates/filesystem/paging/simple_file_list.html create mode 100644 apps/swid/templates/swid/paging/swid_devices_list.html diff --git a/apps/devices/paging.py b/apps/devices/paging.py index 7f289474..59466a75 100644 --- a/apps/devices/paging.py +++ b/apps/devices/paging.py @@ -27,6 +27,21 @@ def device_session_stat_producer(page_size, filter_query, dynamic_params=None, s return math.ceil(count / page_size) +def product_device_list_producer(from_idx, to_idx, filter_query, dynamic_params=None, static_params=None): + if not dynamic_params: + return [] + product_id = dynamic_params['product_id'] + return Device.objects.filter(product__id=product_id)[from_idx:to_idx] + + +def product_device_stat_producer(page_size, filter_query, dynamic_params=None, static_params=None): + if not dynamic_params: + return [] + product_id = dynamic_params['product_id'] + count = Device.objects.filter(product__id=product_id).count() + return math.ceil(count / page_size) + + # PAGING CONFIGS device_list_paging = { @@ -49,6 +64,14 @@ def device_session_stat_producer(page_size, filter_query, dynamic_params=None, s 'page_size': 50, } +product_devices_list_paging = { + 'template_name': 'devices/paging/device_list', + 'list_producer': product_device_list_producer, + 'stat_producer': product_device_stat_producer, + 'url_name': 'devices:device_detail', + 'page_size': 10, +} + device_session_list_paging = { 'template_name': 'devices/paging/device_report_sessions', 'list_producer': device_session_list_producer, diff --git a/apps/devices/product_views.py b/apps/devices/product_views.py index a8e157ef..229e7aec 100644 --- a/apps/devices/product_views.py +++ b/apps/devices/product_views.py @@ -48,6 +48,7 @@ def product(request, productID): groups = Group.objects.exclude(id__in=defaults.values_list('id', flat=True)) context['groups'] = groups context['title'] = _('Product ') + product.name + context['paging_args'] = {'product_id': product.pk} devices = Device.objects.filter(product=product) context['devices'] = devices versions = Version.objects.filter(product=product) diff --git a/apps/devices/templates/devices/paging/device_list.html b/apps/devices/templates/devices/paging/device_list.html new file mode 100644 index 00000000..ed3a1b44 --- /dev/null +++ b/apps/devices/templates/devices/paging/device_list.html @@ -0,0 +1,21 @@ +{% load i18n %} +{% if object_list %} + + + + + + + + {% for device in object_list %} + + + + {% endfor %} + +
{% trans 'Device' %}
{{ device.list_repr }}
+{% else %} +

+ {% trans 'No' %} {% trans 'device' %} {% trans 'with this product found.' %} +

+{% endif %} diff --git a/apps/devices/templates/devices/products.html b/apps/devices/templates/devices/products.html index 258b0201..a546ba0a 100644 --- a/apps/devices/templates/devices/products.html +++ b/apps/devices/templates/devices/products.html @@ -102,26 +102,7 @@

{% trans "Assign Default Groups" %}
{% trans 'Devices with this product' %}
- {% if devices %} - - - - - - - - {% for device in devices %} - - - - {% endfor %} - -
{% trans 'Device' %}
{{ device.list_repr }}
- {% else %} -

- {% trans 'No' %} {% trans 'device' %} {% trans 'with this product found.' %} -

- {% endif %} + {% paged_block config_name='product_devices_list_config' producer_args=paging_args %}
diff --git a/apps/filesystem/directory_views.py b/apps/filesystem/directory_views.py index b549fb9e..b498a077 100644 --- a/apps/filesystem/directory_views.py +++ b/apps/filesystem/directory_views.py @@ -46,6 +46,7 @@ def directory(request, directoryID): context['title'] = _('Directory ') + directory.path files = File.objects.filter(directory=directory).order_by('name') context['files'] = files + context['paging_params'] = {'directory_id': directory.pk} policies = Policy.objects.filter(Q(dir=directory) | Q(file__in=files)) if policies.count() or files.count(): context['has_dependencies'] = True diff --git a/apps/filesystem/paging.py b/apps/filesystem/paging.py index 150d9004..4178b6bc 100644 --- a/apps/filesystem/paging.py +++ b/apps/filesystem/paging.py @@ -29,6 +29,26 @@ def file_stat_producer(page_size, filter_query, dynamic_params=None, static_para return math.ceil(count / page_size) +def file_simple_list_producer(from_idx, to_idx, filter_query, dynamic_params=None, static_params=None): + if not dynamic_params: + return [] + directory_id = dynamic_params['directory_id'] + file_list = File.objects.filter(directory__id=int(directory_id)) + if filter_query: + file_list = file_list.filter(name__icontains=filter_query) + return file_list[from_idx:to_idx] + + +def file_simple_stat_producer(page_size, filter_query, dynamic_params=None, static_params=None): + if not dynamic_params: + return [] + directory_id = dynamic_params['directory_id'] + file_list = File.objects.filter(directory__id=directory_id) + count = file_list.filter(directory__id=directory_id).count() + if filter_query: + count = file_list.filter(name__icontains=filter_query).count() + return math.ceil(count / page_size) + # PAGING CONFIG dir_list_paging = { @@ -41,6 +61,14 @@ def file_stat_producer(page_size, filter_query, dynamic_params=None, static_para 'page_size': 50, } +dir_file_list_paging = { + 'template_name': 'filesystem/paging/simple_file_list', + 'list_producer': file_simple_list_producer, + 'stat_producer': file_simple_stat_producer, + 'url_name': 'filesystem:file_detail', + 'page_size': 45, +} + file_list_paging = { 'template_name': 'front/paging/default_list', 'list_producer': file_list_producer, diff --git a/apps/filesystem/templates/filesystem/directories.html b/apps/filesystem/templates/filesystem/directories.html index 0ce98fdb..f857fa20 100644 --- a/apps/filesystem/templates/filesystem/directories.html +++ b/apps/filesystem/templates/filesystem/directories.html @@ -69,30 +69,13 @@

{% trans 'Directory info:' %} {{ directory.path }}

{% endif %}

- {% endif %} - -
-

Files

- {% if files %} -
- - - - - - - - {% for f in files %} - - - - {% endfor %} - -
{% trans 'Name' %}
{{ f.name }}
-
{% endif %} +
+ +

Files

+ {% paged_block config_name='dir_file_list_config' producer_args=paging_params with_filter=True %} {% endif %} {% if directory.pk %}