From 47c033b274988a24ac10263eafd0466e7f2f1e97 Mon Sep 17 00:00:00 2001 From: Nikolai R Kristiansen Date: Sat, 17 Jan 2015 17:53:03 +0100 Subject: [PATCH] Replace everything cpanl with django-postfix-dovecot-api --- fabfile.py | 63 +++++++++++++++++ mailadmin/api.py | 54 +++++++++------ mailadmin/permissions.py | 29 +++----- mailadmin/serializers.py | 14 ++-- mailadmin/static/app/scripts/main.js | 32 ++++----- mailadmin/static/app/templates/list.html | 8 +-- mailadmin/static/dist/scripts/main.js | 32 ++++----- mailadmin/static/dist/scripts/templates.js | 32 ++++----- mailadmin/tests.py | 39 +++-------- mailadmin/urls.py | 6 +- mailadmin/views.py | 79 ++++++---------------- mailadminsite/settings.py | 8 +-- requirements.txt | 17 ++--- 13 files changed, 209 insertions(+), 204 deletions(-) create mode 100644 fabfile.py diff --git a/fabfile.py b/fabfile.py new file mode 100644 index 0000000..c782aa1 --- /dev/null +++ b/fabfile.py @@ -0,0 +1,63 @@ +import logging +import sys + +from contextlib import contextmanager as _contextmanager +from fabric.api import run, sudo, env, cd, prefix, lcd +from fabric.api import local as lrun + +env.use_ssh_config = True +env.hosts = ['dreamcast.neuf.no'] +env.project_path = '/var/www/neuf.no/mailadmin' +env.user = 'gitdeploy' +env.activate = 'source {}/venv/bin/activate'.format(env.project_path) + + +@_contextmanager +def virtualenv(): + with cd(env.project_path): + with prefix(env.activate): + yield + + +def __git_validate_clean(): + with lcd(env.project_path): + if lrun('git diff-files', capture=True) != '': + logging.error("Your git-tree isn't clean! Commit your changes before deploying.") + sys.exit(1) + + +def test(): + lrun("python manage.py test dpdapi") + + +def commit(): + lrun("git add -p && git commit") + + +def push(): + lrun("git push") + + +def prepare_deploy(): + test() + commit() + push() + + +def gitstatus(): + # __git_validate_clean() + with virtualenv(): + # Get source + run('git branch -v') + + +def deploy(): + # __git_validate_clean() + with virtualenv(): + run('git pull') # Get source + run('pip install -r requirements.txt') # install deps in virtualenv + run('python manage.py collectstatic --noinput') # Collect static + run('python manage.py migrate') # Run DB migrations + + # Reload gunicorn + sudo('/etc/init.d/gunicorn reload', shell=False) diff --git a/mailadmin/api.py b/mailadmin/api.py index 39ace75..f3ee196 100644 --- a/mailadmin/api.py +++ b/mailadmin/api.py @@ -2,33 +2,47 @@ import requests from django.conf import settings -class CPanel(object): +class DjangoPostfixDovecotAPI(object): """ - CPanel API + django-postfix-dovecot-api + + https://git.neuf.no/edb/django-postfix-dovecot-api#tab-readme - https://documentation.cpanel.net/display/SDK/cPanel+API+2+-+Email - https://documentation.cpanel.net/display/SDK/cPanel+API+1+-+Email """ - def __init__(self, base_url=settings.CP_API_URL, username=settings.CP_API_USERNAME, password=settings.CP_API_PASSWORD): + def __init__(self, base_url=settings.DPD_API_URL, username=settings.DPD_API_USERNAME, password=settings.DPD_API_PASSWORD): self.base_url = base_url self.username = username self.password = password - def api(self, module, function, params=None, version=2): - generic = { - 'cpanel_jsonapi_user': self.username, - 'cpanel_jsonapi_module': module, - 'cpanel_jsonapi_func': function, - 'cpanel_jsonapi_apiversion': version, - } - if params is not None: - params = dict(generic.items() + params.items()) - + def _api(self, method, path, params=None, json=None): headers = {'content-type': 'application/json'} - r = requests.get( - self.base_url, + url = "{}{}".format(self.base_url, path) + + r = requests.request( + method, + url, params=params, - headers=headers, auth=requests.auth.HTTPBasicAuth(self.username, self.password), - verify=False) - return r.json() \ No newline at end of file + json=json, + headers=headers) + + return r.json() + + def create_aliases(self, aliases): + return self._api('POST', '/aliases/create_bulk/', json=aliases) + + def delete_aliases(self, aliases): + return self._api('DELETE', '/aliases/delete_bulk/', json=aliases) + + def list_aliases_regex(self, regex): + """ Limit aliases by regular expression and domain name """ + params = { + 'domain__name': settings.NEUF_EMAIL_DOMAIN_NAME, + 'source_regex': regex + } + print params + return self._api( + 'GET', + '/aliases/', + params=params + ) \ No newline at end of file diff --git a/mailadmin/permissions.py b/mailadmin/permissions.py index d3337fc..0ad7da1 100644 --- a/mailadmin/permissions.py +++ b/mailadmin/permissions.py @@ -2,33 +2,22 @@ from rest_framework import permissions from mailadmin.models import OrgUnit import re -from mailadmin.serializers import ForwardSerializer +from mailadmin.serializers import AliasSerializer -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): +class SourcePrefixOwner(permissions.BasePermission): # Ref: http://www.django-rest-framework.org/api-guide/permissions # TODO have no idea if this works def has_permission(self, request, view): + # Read permissions (GET, HEAD or OPTIONS) are allowed for all authenticated request + if request.method in permissions.SAFE_METHODS and request.user and request.user.is_authenticated(): + return True + # TODO test handling the DELETE requests if request.user.is_superuser: return True - if request.method == 'DELETE': + if request.method in ['DELETE', 'POST']: print view print view.kwargs forwarder = view.kwargs['forwarder'] # TODO you are here @@ -37,9 +26,9 @@ class DestinationPrefixOwner(permissions.BasePermission): return True fwd_data = forwarder.split("=") - fw = ForwardSerializer(data={'dest': fwd_data[0], 'forward': fwd_data[1]}) + fw = AliasSerializer(data={'dest': fwd_data[0], 'forward': fwd_data[1]}) else: - fw = ForwardSerializer(data=request.DATA) + fw = AliasSerializer(data=request.DATA) if not fw.is_valid(): return True # Skip auth if not valid data diff --git a/mailadmin/serializers.py b/mailadmin/serializers.py index 3de10b3..7422923 100644 --- a/mailadmin/serializers.py +++ b/mailadmin/serializers.py @@ -27,14 +27,14 @@ class OrgUnitSerializer(serializers.ModelSerializer): model = OrgUnit -class ForwardSerializer(serializers.Serializer): - dest = serializers.EmailField(required=True) - forward = serializers.EmailField(required=True) - _domain = settings.NEUF_EMAIL_DOMAIN +class AliasSerializer(serializers.Serializer): + source = serializers.EmailField(required=True) + destination = serializers.EmailField(required=True) + _domain_name = settings.NEUF_EMAIL_DOMAIN_NAME - def validate_dest(self, attrs, source): + def validate_destonation(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)) + if domain_part != self._domain_name: + raise serializers.ValidationError("dest '{0}' is not in domain {1}".format(attrs[source], self._domain_name)) return attrs \ No newline at end of file diff --git a/mailadmin/static/app/scripts/main.js b/mailadmin/static/app/scripts/main.js index 7fb3180..c786ff7 100644 --- a/mailadmin/static/app/scripts/main.js +++ b/mailadmin/static/app/scripts/main.js @@ -65,9 +65,7 @@ $(document).ready(function() { var csrf_token = $('meta[name=x-csrf-token]').attr('content'); var api_urls = { - fwd_list: '/api/forwards/', - fwd_delete: '/api/forward/', - fwd_create: '/api/forward/', // + forwarder + aliases: '/api/aliases/', ou_list: '/api/orgunits/', me: '/api/me/' }; @@ -150,7 +148,7 @@ }); $.post( - api_urls.fwd_create, + api_urls.aliases, new_list[dest], function() { notify('Lagt til', 'Ny liste: ' + dest + 'lagt til', 'success'); @@ -190,22 +188,24 @@ }); /* Load forwards from API and put on page */ - $.getJSON(api_urls.fwd_list, function(data) { - if( !data.cpanelresult ) { - notify("Ingen lister.", 'Fant ingen tilhørende lister', 'info'); + $.getJSON(api_urls.aliases, function(data) { + var err_msg; + if( data && data.length === 0 ) { + err_msg = 'Fant ingen tilhørende lister'; + notify("Ingen lister.", err_msg, 'info'); + forwards_container.html(err_msg); return; } - if( 'error' in data.cpanelresult ) { - notify('Oops!', 'Klarte ikke hente epostlister fra APIet: '+data.cpanelresult.error, 'error'); + if( data && 'error' in data ) { + err_msg = 'Klarte ikke hente epostlister fra APIet: '+data.error; + notify('Oops!', err_msg, 'error'); + forwards_container.html(err_msg); return; } - var forwards = data.cpanelresult.data; - var lists =_.groupBy(forwards, function(fwd) { return fwd.dest; }); - var fw_html = nunjucks.render('list.html', { - lists: lists, - api_urls: api_urls - }); + var forwards = data; + var lists =_.groupBy(forwards, function(fwd) { return fwd.source; }); + var fw_html = nunjucks.render('list.html', { lists: lists }); forwards_container.html(fw_html); @@ -248,7 +248,7 @@ var data = { type: 'DELETE', headers: {'X-CSRFToken': csrf_token}, - url: api_urls.fwd_delete, + url: api_urls.aliases, data: delete_these, dataType: 'json' }; diff --git a/mailadmin/static/app/templates/list.html b/mailadmin/static/app/templates/list.html index 9721531..ed8157c 100644 --- a/mailadmin/static/app/templates/list.html +++ b/mailadmin/static/app/templates/list.html @@ -1,11 +1,11 @@ -{% for list,members in lists %} +{% for list,aliases in lists %}
-
{{ list }} {{members|length}}
+
{{ list }} {{aliases|length}}
- {% for member in members %} - + {% for member in aliases %} + {% endfor %} \n\t\t\t"; ; } @@ -101,7 +101,7 @@ for(var t_10 in t_3) { t_1++; var t_11 = t_3[t_10]; frame.set("list", t_10); -frame.set("members", t_11); +frame.set("aliases", t_11); frame.set("loop.index", t_1 + 1); frame.set("loop.index0", t_1); frame.set("loop.revindex", t_2 - t_1); @@ -130,19 +130,19 @@ frame.set("loop.first", t_12 === 0); frame.set("loop.last", t_12 === t_13 - 1); frame.set("loop.length", t_13); output += "\n\t\t\t\t\n\t\t\t"; ; } diff --git a/mailadmin/tests.py b/mailadmin/tests.py index 05373d8..46b9349 100644 --- a/mailadmin/tests.py +++ b/mailadmin/tests.py @@ -1,5 +1,5 @@ from django.test import TestCase -from mailadmin.api import CPanel +from mailadmin.api import DjangoPostfixDovecotAPI import unittest @@ -7,41 +7,18 @@ class APITest(TestCase): @unittest.skip("Not now") def test_listforwards(self): - server = CPanel() + _api = DjangoPostfixDovecotAPI() params = dict( - domain='studentersamfundet.no', - regex='kak-*' + regex='^kak-*' ) - res = server.api('Email', 'listforwards', params) - self.assertIsInstance(res, dict) - self.assertIn('cpanelresult', res) + self.assertEqual(True, True) @unittest.skip("Not now") def test_addforward(self): - server = CPanel() - params = dict( - domain='studentersamfundet.no', - email='test.testesen', - fwdopt='fwd', - fwdemail='nikolaik@gmail.com' - ) - res = server.api('Email', 'addforward', params) - self.assertIsInstance(res, dict) - self.assertIn('cpanelresult', res) - cp_res = res['cpanelresult'] - self.assertIn('event', cp_res) - self.assertIn('result', cp_res['event']) - self.assertEqual(cp_res['event']['result'], 1) + _api = DjangoPostfixDovecotAPI() + self.assertEqual(True, True) @unittest.skip("Not now") def test_delforward(self): - server = CPanel() - 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) - self.assertIn('event', res) - self.assertIn('result', res['event']) - self.assertEqual(res['event']['result'], 1) + _api = DjangoPostfixDovecotAPI() + self.assertEqual(True, True) diff --git a/mailadmin/urls.py b/mailadmin/urls.py index 61a0a77..29d40d8 100644 --- a/mailadmin/urls.py +++ b/mailadmin/urls.py @@ -1,10 +1,9 @@ from django.conf.urls import patterns, url from rest_framework.routers import DefaultRouter -from mailadmin.views import ForwardsViewSet, MyUserViewSet, MyOrgUnitViewSet, OrgUnitViewSet, ForwardCreateView, ForwardDeleteView +from mailadmin.views import MyUserViewSet, MyOrgUnitViewSet, OrgUnitViewSet, AliasesView router = DefaultRouter() -router.register(r'forwards', ForwardsViewSet, base_name='all') router.register(r'me', MyUserViewSet, base_name='me') router.register(r'orgunits', MyOrgUnitViewSet, base_name='ous') router.register(r'orgunits/all', OrgUnitViewSet, base_name='allous') @@ -12,6 +11,5 @@ urlpatterns = router.urls urlpatterns += patterns( '', - url(r'^forward/$', ForwardCreateView.as_view(), name='forward-create'), - url(r'^forward/(?P.+)/$', ForwardDeleteView.as_view(), name='forward-delete'), + url(r'^aliases/$', AliasesView.as_view(), name='aliases'), ) diff --git a/mailadmin/views.py b/mailadmin/views.py index fe6bdb5..162c3f1 100644 --- a/mailadmin/views.py +++ b/mailadmin/views.py @@ -1,4 +1,3 @@ -from django.conf import settings from django.contrib.auth import login, logout as auth_logout from django.contrib.auth.decorators import login_required from django.contrib.auth.forms import AuthenticationForm @@ -7,15 +6,15 @@ from django.shortcuts import render, redirect from rest_framework import viewsets, views from rest_framework.permissions import IsAuthenticated from rest_framework.response import Response +from rest_framework import status -from mailadmin.api import CPanel +from mailadmin.api import DjangoPostfixDovecotAPI from mailadmin.models import OrgUnit -from mailadmin.serializers import UserSerializer, OrgUnitSerializer, ForwardSerializer -from mailadmin.permissions import DestinationPrefixOwner +from mailadmin.serializers import UserSerializer, OrgUnitSerializer, AliasSerializer +from mailadmin.permissions import SourcePrefixOwner def index(request): - form = {} if request.method == "POST": form = AuthenticationForm(data=request.POST) if form.is_valid(): @@ -41,14 +40,12 @@ def logout(request): return redirect('index') -class ForwardsViewSet(viewsets.ViewSet): - """ - Forwards from cPanel - """ +class AliasesView(views.APIView): + """ List, create and delete aliases from mxapi """ + _api = DjangoPostfixDovecotAPI() + permission_classes = (SourcePrefixOwner,) - permission_classes = [IsAuthenticated] - - def list(self, request): + def get(self, request): group_filter = request.QUERY_PARAMS.get('groups', None) if group_filter is None: my_groups = request.user.groups.all() @@ -67,62 +64,28 @@ class ForwardsViewSet(viewsets.ViewSet): my_prefixes = orgunits.values_list('prefix', flat=True) # Prepare regular expression - regexp = '|'.join(['^{0}-'.format(p) for p in my_prefixes]) + regexp = '|'.join(['^{0}-|^{0}@'.format(p) for p in my_prefixes]) if len(my_prefixes) == 0: return Response({'result': 'No results'}) # Need at least one - cp = CPanel() - params = dict( - domain=settings.NEUF_EMAIL_DOMAIN, - regex=regexp - ) - - res = cp.api('Email', 'listforwards', params) + res = self._api.list_aliases_regex(regexp) 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) + aliases = AliasSerializer(data=request.DATA, many=True) + if not aliases.is_valid(): + return Response(aliases.errors, status=status.HTTP_400_BAD_REQUEST) - # 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'] - ) - # FIXME: unreliable - # res = self._cp.api('Email', 'addforward', params) - res = {'errors': 'Not implemented'} + res = self._api.create_aliases(aliases.data) return Response(res) + def delete(self, request): + aliases = AliasSerializer(data=request.DATA, many=True) + if not aliases.is_valid(): + return Response(aliases.errors, status=status.HTTP_400_BAD_REQUEST) -class ForwardDeleteView(views.APIView): - _cp = CPanel() - permission_classes = (DestinationPrefixOwner,) - - 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']) - } - # FIXME: unreliable - # res = self._cp.api('Email', 'delforward', params, version=1) + # FIXME: not tested + # res = self._api.delete_aliases(aliases) res = {'errors': 'Not implemented'} return Response(res) diff --git a/mailadminsite/settings.py b/mailadminsite/settings.py index 73a4e33..2883c72 100644 --- a/mailadminsite/settings.py +++ b/mailadminsite/settings.py @@ -86,11 +86,11 @@ STATIC_URL = '/static/' LOGIN_REDIRECT_URL = '/lists/' -CP_API_URL = 'https://web.studentersamfundet.no:2083/json-api/cpanel' -CP_API_USERNAME = "root" -CP_API_PASSWORD = "" +DPD_API_URL= 'https://mxapi.neuf.no/api' +DPD_API_USERNAME = "" +DPD_API_PASSWORD = "" -NEUF_EMAIL_DOMAIN = 'studentersamfundet.no' +NEUF_EMAIL_DOMAIN_NAME = 'stry.no' AUTHENTICATION_BACKENDS = ( 'mailadmin.backends.LDAPEmailBackend', diff --git a/requirements.txt b/requirements.txt index fb8dfdf..e758e8d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,12 +1,13 @@ -Django==1.7.1 -django-auth-ldap==1.2.2 +Django>=1.7,<1.8 +django-auth-ldap==1.2.4 django-bootstrap-form==3.1 -django-filter==0.8 -djangorestframework==2.4.4 +django-extensions==1.4.9 +django-filter==0.9.1 +djangorestframework==3.0.2 +Fabric==1.10.1 Markdown==2.5.1 -MySQL-python==1.2.5 -ndg-httpsclient==0.3.2 +ndg-httpsclient==0.3.3 pyasn1==0.1.7 pyOpenSSL==0.14 -python-ldap==2.4.18 -requests==2.4.3 +python-ldap==2.4.19 +requests==2.5.1
{{ member.forward }}
{{ member.destination }}
diff --git a/mailadmin/static/dist/scripts/main.js b/mailadmin/static/dist/scripts/main.js index 7fb3180..c786ff7 100644 --- a/mailadmin/static/dist/scripts/main.js +++ b/mailadmin/static/dist/scripts/main.js @@ -65,9 +65,7 @@ $(document).ready(function() { var csrf_token = $('meta[name=x-csrf-token]').attr('content'); var api_urls = { - fwd_list: '/api/forwards/', - fwd_delete: '/api/forward/', - fwd_create: '/api/forward/', // + forwarder + aliases: '/api/aliases/', ou_list: '/api/orgunits/', me: '/api/me/' }; @@ -150,7 +148,7 @@ }); $.post( - api_urls.fwd_create, + api_urls.aliases, new_list[dest], function() { notify('Lagt til', 'Ny liste: ' + dest + 'lagt til', 'success'); @@ -190,22 +188,24 @@ }); /* Load forwards from API and put on page */ - $.getJSON(api_urls.fwd_list, function(data) { - if( !data.cpanelresult ) { - notify("Ingen lister.", 'Fant ingen tilhørende lister', 'info'); + $.getJSON(api_urls.aliases, function(data) { + var err_msg; + if( data && data.length === 0 ) { + err_msg = 'Fant ingen tilhørende lister'; + notify("Ingen lister.", err_msg, 'info'); + forwards_container.html(err_msg); return; } - if( 'error' in data.cpanelresult ) { - notify('Oops!', 'Klarte ikke hente epostlister fra APIet: '+data.cpanelresult.error, 'error'); + if( data && 'error' in data ) { + err_msg = 'Klarte ikke hente epostlister fra APIet: '+data.error; + notify('Oops!', err_msg, 'error'); + forwards_container.html(err_msg); return; } - var forwards = data.cpanelresult.data; - var lists =_.groupBy(forwards, function(fwd) { return fwd.dest; }); - var fw_html = nunjucks.render('list.html', { - lists: lists, - api_urls: api_urls - }); + var forwards = data; + var lists =_.groupBy(forwards, function(fwd) { return fwd.source; }); + var fw_html = nunjucks.render('list.html', { lists: lists }); forwards_container.html(fw_html); @@ -248,7 +248,7 @@ var data = { type: 'DELETE', headers: {'X-CSRFToken': csrf_token}, - url: api_urls.fwd_delete, + url: api_urls.aliases, data: delete_these, dataType: 'json' }; diff --git a/mailadmin/static/dist/scripts/templates.js b/mailadmin/static/dist/scripts/templates.js index af85147..e29dc63 100644 --- a/mailadmin/static/dist/scripts/templates.js +++ b/mailadmin/static/dist/scripts/templates.js @@ -40,7 +40,7 @@ for(t_1=0; t_1 < t_3.length; t_1++) { var t_4 = t_3[t_1][0] frame.set("list", t_3[t_1][0]); var t_5 = t_3[t_1][1] -frame.set("members", t_3[t_1][1]); +frame.set("aliases", t_3[t_1][1]); frame.set("loop.index", t_1 + 1); frame.set("loop.index0", t_1); frame.set("loop.revindex", t_2 - t_1); @@ -69,19 +69,19 @@ frame.set("loop.first", t_6 === 0); frame.set("loop.last", t_6 === t_7 - 1); frame.set("loop.length", t_7); output += "\n\t\t\t\t
"; -output += runtime.suppressValue(runtime.memberLookup((t_9),"forward", env.autoesc), env.autoesc); +output += runtime.suppressValue(runtime.memberLookup((t_9),"destination", env.autoesc), env.autoesc); output += "
"; -output += runtime.suppressValue(runtime.memberLookup((t_15),"forward", env.autoesc), env.autoesc); +output += runtime.suppressValue(runtime.memberLookup((t_15),"destination", env.autoesc), env.autoesc); output += "