diff --git a/app/__init__.py b/app/__init__.py index e69de29b..35ea2850 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -0,0 +1 @@ +default_app_config = 'app.apps.MainConfig' \ No newline at end of file diff --git a/app/api/projects.py b/app/api/projects.py index 5941788f..1c143d57 100644 --- a/app/api/projects.py +++ b/app/api/projects.py @@ -1,6 +1,7 @@ from django.contrib.auth.models import User -from rest_framework import serializers, viewsets -from app import models +from rest_framework import serializers, viewsets, filters +from app import models, permissions +from guardian.shortcuts import get_objects_for_user class ProjectSerializer(serializers.HyperlinkedModelSerializer): owner = serializers.PrimaryKeyRelatedField(queryset=User.objects.all()) @@ -13,5 +14,5 @@ class ProjectViewSet(viewsets.ModelViewSet): """ Projects the current user has access to. """ + serializer_class = ProjectSerializer queryset = models.Project.objects.all() - serializer_class = ProjectSerializer \ No newline at end of file diff --git a/app/api/tests.py b/app/api/tests.py new file mode 100644 index 00000000..b9bf0cb1 --- /dev/null +++ b/app/api/tests.py @@ -0,0 +1,37 @@ +from django.test import TestCase +from rest_framework.test import APIClient +from rest_framework import status + +from app.models import Project +from django.contrib.auth.models import User + +class TestApi(TestCase): + + fixtures = ['test_users', ] + + def setUp(self): + Project( + owner=User.objects.get(username="testuser"), + name="test project" + ).save() + + def tearDown(self): + pass + + def test_project_list(self): + client = APIClient() + + # Forbidden without credentials + res = client.get('/api/projects/') + self.assertEqual(res.status_code, status.HTTP_403_FORBIDDEN) + + client.login(username="testuser", password="test1234") + res = client.get('/api/projects/') + self.assertEqual(res.status_code, status.HTTP_200_OK) + self.assertTrue(len(res.data.results) > 0) + + res = client.get('/api/projects/1/') + self.assertEqual(res.status_code, status.HTTP_200_OK) + + res = client.get('/api/projects/dasjkldas/') + self.assertEqual(res.status_code, status.HTTP_404_NOT_FOUND) diff --git a/app/apps.py b/app/apps.py index 41ec8554..1d26e888 100644 --- a/app/apps.py +++ b/app/apps.py @@ -1,6 +1,11 @@ from __future__ import unicode_literals from django.apps import AppConfig +from .boot import boot class MainConfig(AppConfig): - name = 'main' + name = 'app' + verbose_name = 'Application' + + def ready(self): + boot() \ No newline at end of file diff --git a/app/boot.py b/app/boot.py new file mode 100644 index 00000000..6fe2ce66 --- /dev/null +++ b/app/boot.py @@ -0,0 +1,24 @@ +def boot(): + from django.contrib.contenttypes.models import ContentType + from django.contrib.auth.models import Permission + from django.contrib.auth.models import User, Group + from . import signals + import logging + + logger = logging.getLogger('app.logger') + + # Check default group + default_group, created = Group.objects.get_or_create(name='Default') + if created: + logger.info("Created default group") + + # Add default permissions (view_project, change_project, delete_task, etc.) + for permission in ('_project', '_task'): + default_group.permissions.add( + *list(Permission.objects.filter(codename__endswith=permission)) + ) + + # Check super user + if User.objects.count() == 0: + User.objects.create_superuser('admin', 'admin@example.com', 'admin') + logger.info("Created superuser") diff --git a/app/fixtures/test_project.yaml b/app/fixtures/test_projects.yaml similarity index 56% rename from app/fixtures/test_project.yaml rename to app/fixtures/test_projects.yaml index b40b3d49..2f01da0f 100644 --- a/app/fixtures/test_project.yaml +++ b/app/fixtures/test_projects.yaml @@ -1,12 +1,9 @@ - model: app.project pk: 1 - fields: {owner: 2, name: Test_Project2, description: This is a test project, created_at: ! '2016-09-12 - 10:08:39.045372+00:00'} + fields: {owner: 2, name: Test_Project2, description: This is a test project, created_at: '2016-09-11T22:26:52.582858+00:00'} - model: app.project pk: 2 - fields: {owner: 1, name: Test_Project1, description: This is a test project, created_at: ! '2016-09-12 - 10:08:58.533742+00:00'} + fields: {owner: 1, name: Test_Project1, description: This is a test project, created_at: '2016-09-11T22:26:52.582858+00:00'} - model: app.project pk: 3 - fields: {owner: 1, name: Test_Project3, description: This is a test project, created_at: ! '2016-09-12 - 10:11:31.357885+00:00'} \ No newline at end of file + fields: {owner: 1, name: Test_Project3, description: This is a test project, created_at: '2016-09-11T22:26:52.582858+00:00'} \ No newline at end of file diff --git a/app/permissions.py b/app/permissions.py new file mode 100644 index 00000000..39bb0c06 --- /dev/null +++ b/app/permissions.py @@ -0,0 +1,15 @@ +from rest_framework import permissions + +class GuardianObjectPermissions(permissions.DjangoObjectPermissions): + """ + Similar to `DjangoObjectPermissions`, but adding 'view' permissions. + """ + perms_map = { + 'GET': ['%(app_label)s.view_%(model_name)s'], + 'OPTIONS': ['%(app_label)s.view_%(model_name)s'], + 'HEAD': ['%(app_label)s.view_%(model_name)s'], + 'POST': ['%(app_label)s.add_%(model_name)s'], + 'PUT': ['%(app_label)s.change_%(model_name)s'], + 'PATCH': ['%(app_label)s.change_%(model_name)s'], + 'DELETE': ['%(app_label)s.delete_%(model_name)s'], + } \ No newline at end of file diff --git a/app/signals.py b/app/signals.py new file mode 100644 index 00000000..3fd1e617 --- /dev/null +++ b/app/signals.py @@ -0,0 +1,17 @@ +from django.db.models import signals +from django.dispatch import receiver +from django.contrib.auth.models import User, Group +import logging + +logger = logging.getLogger('app.logger') + +@receiver(signals.post_save, sender=User, dispatch_uid="user_check_default_group") +def check_default_group(sender, instance, created, **kwargs): + if created: + try: + default_group = Group.objects.get(name="Default") + instance.groups.add(default_group) + instance.save() + logger.info("Added {} to default group".format(instance.username)) + except: + pass # Group "Default" is not available, probably loading fixtures at this moment... \ No newline at end of file diff --git a/app/tests.py b/app/tests.py index 37f4d828..b0d2a164 100644 --- a/app/tests.py +++ b/app/tests.py @@ -5,11 +5,19 @@ from django.contrib import messages from django.test import Client from .models import Project, Task +from .boot import boot + +import api.tests class TestApp(TestCase): fixtures = ['test_users', 'test_processingnodes', ] + @classmethod + def setUpClass(cls): + super(TestApp, cls).setUpClass() + boot() + def setUp(self): self.credentials = { 'username': 'testuser', @@ -85,6 +93,15 @@ class TestApp(TestCase): res = c.get('/processingnode/abc/') self.assertTrue(res.status_code == 404) + def test_default_group(self): + # It exists + self.assertTrue(Group.objects.filter(name='Default').count() == 1) + + # Verify that all new users are assigned to default group + u = User.objects.create_user(username="default_user") + u.refresh_from_db() + self.assertTrue(u.groups.filter(name='Default').count() == 1) + def test_projects(self): # Get a normal user user = User.objects.get(pk=2) diff --git a/start.sh b/start.sh index 11091875..cd72d1f6 100644 --- a/start.sh +++ b/start.sh @@ -3,9 +3,4 @@ echo Running migrations python manage.py makemigrations python manage.py migrate -echo Creating default superuser... - -# This will fail if the user is a duplicate -echo "from django.contrib.auth.models import User; User.objects.create_superuser('admin', 'admin@example.com', 'admin')" | python manage.py shell - python manage.py runserver 0.0.0.0:8000 \ No newline at end of file diff --git a/webodm/settings.py b/webodm/settings.py index 499f9028..53cebbfa 100644 --- a/webodm/settings.py +++ b/webodm/settings.py @@ -137,6 +137,44 @@ STATICFILES_DIRS = [ ] +# Logging +LOGGING = { + 'version': 1, + 'disable_existing_loggers': False, + 'formatters': { + 'verbose': { + 'format': '%(levelname)s %(asctime)s %(module)s %(process)d %(thread)d %(message)s' + }, + 'simple': { + 'format': '%(levelname)s %(message)s' + }, + }, + 'filters': { + 'require_debug_true': { + '()': 'django.utils.log.RequireDebugTrue', + }, + }, + 'handlers': { + 'console': { + 'level': 'INFO', + 'filters': ['require_debug_true'], + 'class': 'logging.StreamHandler', + 'formatter': 'simple' + } + }, + 'loggers': { + 'django': { + 'handlers': ['console'], + 'propagate': True, + }, + 'app.logger': { + 'handlers': ['console'], + 'level': 'INFO', + } + } +} + + # Auth LOGIN_REDIRECT_URL = '/dashboard/' LOGIN_URL = '/login/' @@ -154,7 +192,10 @@ MESSAGE_TAGS = { # Use Django's standard django.contrib.auth permissions (no anonymous usage) REST_FRAMEWORK = { 'DEFAULT_PERMISSION_CLASSES': [ - 'rest_framework.permissions.DjangoModelPermissions', + 'app.permissions.GuardianObjectPermissions', + ], + 'DEFAULT_FILTER_BACKENDS': [ + 'rest_framework.filters.DjangoObjectPermissionsFilter', ], 'PAGE_SIZE': 10, }