Skip to content

Commit

Permalink
Merge 'Integration Request 5'
Browse files Browse the repository at this point in the history
- Moves the `swid_measurement` API endpoint to `swid-measurement`, and
  requires the submitted data to be contained in a JSON object (`data`
  member).  The API requires basic authentication and the user must
  either have the `is_staff=1` flag set or the `front.write_access`
  permission assigned (this is the case for the `admin-user`).

- SWID tags are only updated when imported via management command,
  not via API (only entity names are updated).

- API fields are restrictable using the `fields=` parameter.

- Default users and root user have been removed from the databases.
  They can be added with `./manage.py setpassword` and `./manage.py
  createsuperuser --database meta`, respectively.

- Adding files and package versions in the GUI are possible.

- Several other improvements regarding paging/filtering, AJAX loading,
  the SWID Inventory, timezone handling and many other things are
  included too.

Closes #12.
  • Loading branch information
tobiasbrunner committed Jun 4, 2014
2 parents 6bf13aa + 75a4b67 commit 0847cd9
Show file tree
Hide file tree
Showing 102 changed files with 3,439 additions and 1,618 deletions.
3 changes: 3 additions & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
31 changes: 28 additions & 3 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -78,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
-------
Expand All @@ -97,6 +106,22 @@ Run the tests::

./runtests.py

Run a specific test file::

./runtests.py tests/<filename>

Run only tests matching a specific pattern::

./runtests.py -k <pattern>

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
Expand Down
37 changes: 37 additions & 0 deletions apps/api/mixins.py
Original file line number Diff line number Diff line change
@@ -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)
18 changes: 14 additions & 4 deletions apps/api/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -18,11 +18,21 @@
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('',
# 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<format>[a-z0-9]+)', TagAddView.as_view(), name='swid-add-tags'),

# Register measurement
url(r'^sessions/(?P<pk>[^/]+)/swid-measurement/',
SwidMeasurementView.as_view(), name='session-swid-measurement'),
url(r'^sessions/(?P<pk>[^/]+)/swid-measurement/\.(?P<format>[a-z0-9]+)',
SwidMeasurementView.as_view(), name='session-swid-measurement'),
)
24 changes: 24 additions & 0 deletions apps/api/utils.py
Original file line number Diff line number Diff line change
@@ -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)
27 changes: 19 additions & 8 deletions apps/auth/management/commands/setpassword.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 = '[<readonly_password> <readwrite_password>]'

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.
Expand All @@ -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()

Expand Down
22 changes: 22 additions & 0 deletions apps/auth/permissions.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
"""
Expand Down Expand Up @@ -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
45 changes: 2 additions & 43 deletions apps/core/api_views.py
Original file line number Diff line number Diff line change
@@ -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):
Expand All @@ -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)
1 change: 0 additions & 1 deletion apps/core/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ class Identity(models.Model):

class Meta:
db_table = 'identities'
unique_together = [('type', 'data')]
verbose_name_plural = 'identities'
ordering = ('data',)

Expand Down
5 changes: 3 additions & 2 deletions apps/core/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()

Expand Down
24 changes: 0 additions & 24 deletions apps/devices/ajax.py

This file was deleted.

14 changes: 5 additions & 9 deletions apps/devices/device_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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)

Expand Down
Loading

0 comments on commit 0847cd9

Please sign in to comment.