diff --git a/.gitignore b/.gitignore index e084584..6a9495f 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ local_settings.py venv .idea db.sqlite3 +node_modules diff --git a/mailadmin/permissions.py b/mailadmin/permissions.py new file mode 100644 index 0000000..91ff96f --- /dev/null +++ b/mailadmin/permissions.py @@ -0,0 +1,36 @@ +from rest_framework import permissions +from mailadmin.models import OrgUnit +import re + +from mailadmin.serializers import ForwardSerializer + +class IsOwnerOrReadOnly(permissions.BasePermission): + """ + Custom permission to only allow owners of an object to edit it. + """ + + def has_object_permission(self, request, view, obj): + # Read permissions are allowed to any request, + # so we'll always allow GET, HEAD or OPTIONS requests. + if request.method in permissions.SAFE_METHODS: + return True + + # Write permissions are only allowed to the owner + return obj.owner == request.user + + +class DestinationPrefixOwner(permissions.BasePermission): + def has_permission(self, request, view): + fw = ForwardSerializer(data=request.DATA) + if not fw.is_valid(): + return True # Skip auth if not valid data + + my_groups = request.user.groups.all() + # Look for mailinglist prefixes + my_prefixes = OrgUnit.objects.filter(admin_groups__in=my_groups).distinct().values_list('prefix', flat=True) + # Prepare regular expression + regexp = '|'.join(['{0}-'.format(p) for p in my_prefixes]) + if len(my_prefixes) == 0: + return False + # The destination should have at least one match with the prefix regexp + return re.search(regexp, fw.data['dest']) is not None \ No newline at end of file diff --git a/mailadmin/serializers.py b/mailadmin/serializers.py new file mode 100644 index 0000000..3de10b3 --- /dev/null +++ b/mailadmin/serializers.py @@ -0,0 +1,40 @@ +from django.conf import settings +from django.contrib.auth.models import User, Group +from rest_framework import serializers + +from mailadmin.models import OrgUnit + + +class GroupSerializer(serializers.ModelSerializer): + class Meta: + model = Group + fields = ['id', 'name'] + + +class UserSerializer(serializers.ModelSerializer): + groups = GroupSerializer(many=True) + + class Meta: + model = User + exclude = ['password', 'user_permissions'] + + +class OrgUnitSerializer(serializers.ModelSerializer): + admin_groups = GroupSerializer(many=True) + member_groups = GroupSerializer(many=True) + + class Meta: + model = OrgUnit + + +class ForwardSerializer(serializers.Serializer): + dest = serializers.EmailField(required=True) + forward = serializers.EmailField(required=True) + _domain = settings.NEUF_EMAIL_DOMAIN + + def validate_dest(self, attrs, source): + domain_part = attrs[source].split('@')[1] + if domain_part != self._domain: + raise serializers.ValidationError("dest '{0}' is not in domain {1}".format(attrs[source], self._domain)) + + return attrs \ No newline at end of file diff --git a/mailadmin/templates/base.html b/mailadmin/templates/base.html index f685cea..cb94599 100644 --- a/mailadmin/templates/base.html +++ b/mailadmin/templates/base.html @@ -1,8 +1,11 @@ - +{% load staticfiles %} + {% block "title" %}{% endblock %} + + {% block "body" %}{% endblock %} diff --git a/mailadmin/templates/home.html b/mailadmin/templates/home.html index 7254313..796b39b 100644 --- a/mailadmin/templates/home.html +++ b/mailadmin/templates/home.html @@ -1,4 +1,10 @@ {% extends "base.html" %} +{% load bootstrap %} {% block "body" %} -Hi, you should try the APIS + {% if not user.is_authenticated %} + {{ form|bootstrap }} + {% else %} +
Hei, {{ user.username }}
+ {% endif %} +
API. {% endblock %} \ No newline at end of file diff --git a/mailadmin/tests.py b/mailadmin/tests.py index d1154cd..05373d8 100644 --- a/mailadmin/tests.py +++ b/mailadmin/tests.py @@ -36,9 +36,9 @@ class APITest(TestCase): @unittest.skip("Not now") def test_delforward(self): server = CPanel() - params = dict( - forwarder='test.testesen@studentersamfundet.no=nikolaik@gmail.com', - ) + params = { + 'arg-0': 'kak-lol@studentersamfundet.no=nikolaik@gmail.com', + } res = server.api('Email', 'delforward', params, version=1) # FIXME: Does not work! self.assertIsInstance(res, dict) diff --git a/mailadmin/urls.py b/mailadmin/urls.py index d6cdc19..9a510fe 100644 --- a/mailadmin/urls.py +++ b/mailadmin/urls.py @@ -1,6 +1,16 @@ -from mailadmin.views import ForwardsViewSet +from django.conf.urls import patterns, url from rest_framework.routers import DefaultRouter +from mailadmin.views import ForwardsViewSet, MyUserViewSet, OrgUnitViewSet, ForwardCreateView, ForwardDeleteView + router = DefaultRouter() router.register(r'forwards', ForwardsViewSet, base_name='all') +router.register(r'me', MyUserViewSet, base_name='me') +router.register(r'orgunits', OrgUnitViewSet, base_name='ous') urlpatterns = router.urls + +urlpatterns += patterns( + '', + url(r'^forward/$', ForwardCreateView.as_view(), name='forward-create'), + url(r'^forward/(?P.+)/$', ForwardDeleteView.as_view(), name='forward-delete'), +) diff --git a/mailadmin/views.py b/mailadmin/views.py index 6beaa7d..7fb68c3 100644 --- a/mailadmin/views.py +++ b/mailadmin/views.py @@ -1,32 +1,120 @@ -from django.contrib.auth.models import Group +from django.conf import settings +from django.contrib.auth import login +from django.contrib.auth.forms import AuthenticationForm +from django.contrib.auth.models import User from django.shortcuts import render -from rest_framework import viewsets +from rest_framework import viewsets, views +from rest_framework.permissions import IsAuthenticated from rest_framework.response import Response from mailadmin.api import CPanel from mailadmin.models import OrgUnit +from mailadmin.serializers import UserSerializer, OrgUnitSerializer, ForwardSerializer +from mailadmin.permissions import DestinationPrefixOwner def home(request): - return render(request, "home.html") + form = {} + if request.method == "POST": + form = AuthenticationForm(data=request.POST) + if form.is_valid(): + # Log the user in. + login(request, form.get_user()) + else: + if not request.user.is_authenticated(): + form = AuthenticationForm(request) + + return render(request, "home.html", {'form': form}) class ForwardsViewSet(viewsets.ViewSet): """ - A simple ViewSet that for listing forwards + Forwards from cPanel """ + + permission_classes = [IsAuthenticated] + def list(self, request): + group_filter = request.QUERY_PARAMS.get('groups', None) + if group_filter is None: + my_groups = request.user.groups.all() + else: + # Filter groups + if ',' in group_filter: + group_filter = group_filter.split(',') + else: + group_filter = [group_filter] + my_groups = request.user.groups.filter(pk__in=group_filter) + + # Look for mailinglist prefixes + my_prefixes = OrgUnit.objects.filter(admin_groups__in=my_groups).distinct().values_list('prefix', flat=True) + # Prepare regular expression + regexp = '|'.join(['{0}-'.format(p) for p in my_prefixes]) + + if len(my_prefixes) == 0: + return Response({'result': 'No results'}) # Need at least one + cp = CPanel() - regexp = self.request.QUERY_PARAMS.get('regexp', None) - my_groups = request.user.groups.values_list('pk', flat=True) - print my_groups - ous = OrgUnit.objects.filter(admin_groups__in=my_groups).values_list('prefix', flat=True) - ous = set(ous) - print ous - #regexp = 'kak-*' params = dict( - domain='studentersamfundet.no', + domain=settings.NEUF_EMAIL_DOMAIN, regex=regexp ) + res = cp.api('Email', 'listforwards', params) - return Response(res) \ No newline at end of file + return Response(res) + + +class ForwardCreateView(views.APIView): + _cp = CPanel() + permission_classes = (DestinationPrefixOwner,) + + def post(self, request): + fw = ForwardSerializer(data=request.DATA) + if not fw.is_valid(): + return Response(fw.errors, status=400) + + # fw.data + params = dict( + domain=settings.NEUF_EMAIL_DOMAIN, + email=fw.data['dest'].replace('@{0}'.format(settings.NEUF_EMAIL_DOMAIN), ''), + fwdopt='fwd', + fwdemail=fw.data['forward'] + ) + res = self._cp.api('Email', 'addforward', params) + return Response(res) + + +class ForwardDeleteView(views.APIView): + _cp = CPanel() + + def delete(self, request, forwarder=None): + invalid_forwarder = {'error': 'Invalid forwarder: {0}'.format(forwarder)} + if '=' not in forwarder: + return Response(invalid_forwarder, status=400) + + fwd_data = forwarder.split("=") + + fw = ForwardSerializer(data={'dest': fwd_data[0], 'forward': fwd_data[1]}) + if not fw.is_valid(): + return Response(fw.errors, status=400) + + params = { + 'arg-0': '{0}={1}'.format(fw.data['dest'], fw.data['forward']) + } + res = self._cp.api('Email', 'delforward', params, version=1) + return Response(res) + + +class MyUserViewSet(viewsets.ReadOnlyModelViewSet): + queryset = User.objects.none() + serializer_class = UserSerializer + permission_classes = [IsAuthenticated] + + def get_queryset(self): + return User.objects.filter(pk=self.request.user.pk) + + +class OrgUnitViewSet(viewsets.ReadOnlyModelViewSet): + queryset = OrgUnit.objects.all() + serializer_class = OrgUnitSerializer + permission_classes = [IsAuthenticated] \ No newline at end of file diff --git a/mailadminsite/settings.py b/mailadminsite/settings.py index 7000f2d..431432f 100644 --- a/mailadminsite/settings.py +++ b/mailadminsite/settings.py @@ -37,6 +37,7 @@ INSTALLED_APPS = ( 'django.contrib.messages', 'django.contrib.staticfiles', 'rest_framework', + 'bootstrapform', 'mailadmin', ) @@ -83,10 +84,14 @@ USE_TZ = True STATIC_URL = '/static/' +LOGIN_REDIRECT_URL = '/' + CP_API_URL = 'https://web.studentersamfundet.no:2083/json-api/cpanel' CP_API_USERNAME = "root" CP_API_PASSWORD = "" +NEUF_EMAIL_DOMAIN = 'studentersamfundet.no' + AUTHENTICATION_BACKENDS = ( 'mailadmin.backends.LDAPEmailBackend', 'mailadmin.backends.LDAPUsernameBackend', @@ -147,12 +152,12 @@ AUTH_LDAP_E_USER_ATTR_MAP = AUTH_LDAP_U_USER_ATTR_MAP AUTH_LDAP_E_PROFILE_ATTR_MAP = AUTH_LDAP_U_PROFILE_ATTR_MAP AUTH_LDAP_E_ALWAYS_UPDATE_USER = AUTH_LDAP_U_ALWAYS_UPDATE_USER -# Debug logging -import logging -logger = logging.getLogger('django_auth_ldap') -logger.addHandler(logging.StreamHandler()) -logger.setLevel(logging.DEBUG) - +REST_FRAMEWORK = { + 'DEFAULT_AUTHENTICATION_CLASSES': ( + 'rest_framework.authentication.BasicAuthentication', + 'rest_framework.authentication.SessionAuthentication', + ) +} # Local settings try: diff --git a/mailadminsite/urls.py b/mailadminsite/urls.py index d60d462..2a4c36d 100644 --- a/mailadminsite/urls.py +++ b/mailadminsite/urls.py @@ -1,5 +1,6 @@ from django.conf.urls import patterns, include, url from django.contrib import admin +from django.contrib.staticfiles.urls import staticfiles_urlpatterns urlpatterns = patterns( '', @@ -8,3 +9,5 @@ urlpatterns = patterns( url(r'^admin/', include(admin.site.urls)), url(r'^api-auth/', include('rest_framework.urls', namespace='rest_framework')) ) + +urlpatterns += staticfiles_urlpatterns() \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 8d5da09..e9a9b74 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,3 +5,4 @@ django-filter==0.8 djangorestframework==2.4.4 python-ldap==2.4.18 requests==2.4.3 +django-bootstrap-form==3.1