Commit 62c853d0 authored by Florian Fuchs's avatar Florian Fuchs

* added subscription list page for logged in user

* added decorator to allow login via http basic auth (to allow non-browser clients to use API views)
* added api view for list index
* several changes regarding style and navigation structure
parent f316c1bc
......@@ -36,5 +36,5 @@ setup(
package_dir = {'': 'src'},
include_package_data = True,
install_requires = ['django-social-auth>=0.6.7',
'mailmanclient']
'mailmanclient', ]
)
......@@ -18,11 +18,29 @@
"""Postorius view decorators."""
from django.contrib.auth import logout, authenticate, login
from django.core.exceptions import PermissionDenied
from postorius.models import (Domain, List, Member, MailmanUser,
MailmanApiError, Mailman404Error)
def basic_auth_login(fn):
def wrapper(*args, **kwargs):
request = args[0]
if request.user.is_authenticated():
print 'already logged in'
if not request.user.is_authenticated():
if request.META.has_key('HTTP_AUTHORIZATION'):
authmeth, auth = request.META['HTTP_AUTHORIZATION'].split(' ', 1)
if authmeth.lower() == 'basic':
auth = auth.strip().decode('base64')
username, password = auth.split(':', 1)
user = authenticate(username=username, password=password)
if user:
login(request, user)
return fn(request, **kwargs)
return wrapper
def list_owner_required(fn):
"""Check if the logged in user is the list owner of the given list.
......@@ -65,3 +83,27 @@ def list_moderator_required(fn):
user.is_list_moderator = True
return fn(*args, **kwargs)
return wrapper
def superuser_or_403(fn):
"""Make sure that the logged in user is a superuser or otherwise raise
PermissionDenied.
Assumes the request object to be the first arg."""
def wrapper(*args, **kwargs):
user = args[0].user
if not user.is_superuser:
raise PermissionDenied
return fn(*args, **kwargs)
return wrapper
def loggedin_or_403(fn):
"""Make sure that the logged in user is not anonymous or otherwise raise
PermissionDenied.
Assumes the request object to be the first arg."""
def wrapper(*args, **kwargs):
user = args[0].user
if not user.is_authenticated():
raise PermissionDenied
return fn(*args, **kwargs)
return wrapper
......@@ -92,12 +92,40 @@ Apart from these default roles, there are two others relevant in Postorius:
There are a number of decorators to protect views from unauthorized users.
- ``@user_passes_test(lambda u: u.is_superuser)``
- ``@login_required``
- ``@list_owner_required``
- ``@list_moderator_required``
- ``@user_passes_test(lambda u: u.is_superuser)`` (redirects to login form)
- ``@login_required`` (redirects to login form)
- ``@list_owner_required`` (returns 403)
- ``@list_moderator_required`` (returns 403)
- ``@superuser_or_403`` (returns 403)
- ``@loggedin_or_403`` (returns 403)
- ``@basic_auth_login``
Check out views/views.py for examples!
Check out ``views/views.py`` or ``views/api.py`` for examples!
The last one (basic_auth_login) checks the request header for HTTP Basic Auth
credentials and uses those to authenticate against Django's session-based
mechanism. It can be used in cases where a view is accessed from other clients
than the web browser.
Please make sure to put it outside the other auth decorators.
Good:
::
@basic_auth_login
@list_owner_required
def my_view_func(request):
...
Won't work, because list_owner_required will not recognize the user:
::
@list_owner_required
@basic_auth_login
def my_view_func(request):
...
Accessing the Mailman API
......
......@@ -39,6 +39,10 @@ along with Postorius. If not, see <http://www.gnu.org/licenses/>.
* added a mailmanclient shell to use as a `manage.py` command (`python manage.py mmclient`)
* use "url from future" template tag in all templates. Contributed by Richard Wackerbarth.
* added "new user" form. Contributed by George Chatzisofroniou.
* added user subscription page
* added decorator to allow login via http basic auth (to allow non-browser clients to use API views)
* added api view for list index
* several changes regarding style and navigation structure
1.0 alpha 1 -- "Space Farm"
......
......@@ -123,12 +123,34 @@ lists.</li>
</ul>
<p>There are a number of decorators to protect views from unauthorized users.</p>
<ul class="simple">
<li><tt class="docutils literal"><span class="pre">&#64;user_passes_test(lambda</span> <span class="pre">u:</span> <span class="pre">u.is_superuser)</span></tt></li>
<li><tt class="docutils literal"><span class="pre">&#64;login_required</span></tt></li>
<li><tt class="docutils literal"><span class="pre">&#64;list_owner_required</span></tt></li>
<li><tt class="docutils literal"><span class="pre">&#64;list_moderator_required</span></tt></li>
<li><tt class="docutils literal"><span class="pre">&#64;user_passes_test(lambda</span> <span class="pre">u:</span> <span class="pre">u.is_superuser)</span></tt> (redirects to login form)</li>
<li><tt class="docutils literal"><span class="pre">&#64;login_required</span></tt> (redirects to login form)</li>
<li><tt class="docutils literal"><span class="pre">&#64;list_owner_required</span></tt> (returns 403)</li>
<li><tt class="docutils literal"><span class="pre">&#64;list_moderator_required</span></tt> (returns 403)</li>
<li><tt class="docutils literal"><span class="pre">&#64;superuser_or_403</span></tt> (returns 403)</li>
<li><tt class="docutils literal"><span class="pre">&#64;loggedin_or_403</span></tt> (returns 403)</li>
<li><tt class="docutils literal"><span class="pre">&#64;basic_auth_login</span></tt></li>
</ul>
<p>Check out views/views.py for examples!</p>
<p>Check out <tt class="docutils literal"><span class="pre">views/views.py</span></tt> or <tt class="docutils literal"><span class="pre">views/api.py</span></tt> for examples!</p>
<p>The last one (basic_auth_login) checks the request header for HTTP Basic Auth
credentials and uses those to authenticate against Django&#8217;s session-based
mechanism. It can be used in cases where a view is accessed from other clients
than the web browser.</p>
<p>Please make sure to put it outside the other auth decorators.</p>
<p>Good:</p>
<div class="highlight-python"><div class="highlight"><pre><span class="nd">@basic_auth_login</span>
<span class="nd">@list_owner_required</span>
<span class="k">def</span> <span class="nf">my_view_func</span><span class="p">(</span><span class="n">request</span><span class="p">):</span>
<span class="o">...</span>
</pre></div>
</div>
<p>Won&#8217;t work, because list_owner_required will not recognize the user:</p>
<div class="highlight-python"><div class="highlight"><pre><span class="nd">@list_owner_required</span>
<span class="nd">@basic_auth_login</span>
<span class="k">def</span> <span class="nf">my_view_func</span><span class="p">(</span><span class="n">request</span><span class="p">):</span>
<span class="o">...</span>
</pre></div>
</div>
</div>
<div class="section" id="accessing-the-mailman-api">
<h2>Accessing the Mailman API<a class="headerlink" href="#accessing-the-mailman-api" title="Permalink to this headline"></a></h2>
......
......@@ -87,6 +87,10 @@ along with Postorius. If not, see &lt;<a class="reference external" href="http:/
<li>added a mailmanclient shell to use as a <cite>manage.py</cite> command (<cite>python manage.py mmclient</cite>)</li>
<li>use &#8220;url from future&#8221; template tag in all templates. Contributed by Richard Wackerbarth.</li>
<li>added &#8220;new user&#8221; form. Contributed by George Chatzisofroniou.</li>
<li>added user subscription page</li>
<li>added decorator to allow login via http basic auth (to allow non-browser clients to use API views)</li>
<li>added api view for list index</li>
<li>several changes regarding style and navigation structure</li>
</ul>
</div>
<div class="section" id="alpha-1-space-farm">
......
......@@ -92,12 +92,40 @@ Apart from these default roles, there are two others relevant in Postorius:
There are a number of decorators to protect views from unauthorized users.
- ``@user_passes_test(lambda u: u.is_superuser)``
- ``@login_required``
- ``@list_owner_required``
- ``@list_moderator_required``
- ``@user_passes_test(lambda u: u.is_superuser)`` (redirects to login form)
- ``@login_required`` (redirects to login form)
- ``@list_owner_required`` (returns 403)
- ``@list_moderator_required`` (returns 403)
- ``@superuser_or_403`` (returns 403)
- ``@loggedin_or_403`` (returns 403)
- ``@basic_auth_login``
Check out views/views.py for examples!
Check out ``views/views.py`` or ``views/api.py`` for examples!
The last one (basic_auth_login) checks the request header for HTTP Basic Auth
credentials and uses those to authenticate against Django's session-based
mechanism. It can be used in cases where a view is accessed from other clients
than the web browser.
Please make sure to put it outside the other auth decorators.
Good:
::
@basic_auth_login
@list_owner_required
def my_view_func(request):
...
Won't work, because list_owner_required will not recognize the user:
::
@list_owner_required
@basic_auth_login
def my_view_func(request):
...
Accessing the Mailman API
......
......@@ -39,6 +39,10 @@ along with Postorius. If not, see <http://www.gnu.org/licenses/>.
* added a mailmanclient shell to use as a `manage.py` command (`python manage.py mmclient`)
* use "url from future" template tag in all templates. Contributed by Richard Wackerbarth.
* added "new user" form. Contributed by George Chatzisofroniou.
* added user subscription page
* added decorator to allow login via http basic auth (to allow non-browser clients to use API views)
* added api view for list index
* several changes regarding style and navigation structure
1.0 alpha 1 -- "Space Farm"
......
......@@ -20,7 +20,7 @@ import logging
from django.conf import settings
from django.contrib.auth.models import User
from django.db.models.signals import pre_save
from django.db.models.signals import pre_delete, pre_save
from django.db import models
from django.dispatch import receiver
from django.http import Http404
......@@ -88,6 +88,7 @@ class MailmanRestManager(object):
def create(self, **kwargs):
try:
method = getattr(self.client, 'create_' + self.resource_name)
print kwargs
return method(**kwargs)
except AttributeError, e:
raise MailmanApiError(e)
......@@ -174,9 +175,3 @@ class Member(MailmanRestModel):
"""Member model class.
"""
objects = MailmanRestManager('member', 'members')
@receiver(pre_save, sender=User)
def user_create_callback(sender, **kwargs):
# inst = kwargs['instance']
pass
......@@ -110,8 +110,9 @@ h2 {
.mm_nav {
padding: 10px 0 30px 0;
margin: 20px 0;
border: solid #d8d8d8;
border-style: solid;
border-width: 1px 0;
border-color: #e8e8e8 #fff #d8d8d8 #fff;
background: -webkit-linear-gradient(top, white 0%,#EFEFEF 100%);
background: -moz-linear-gradient(top, white 0%,#EFEFEF 100%);
background: -ms-linear-gradient(top, white 0%,#EFEFEF 100%);
......
<!doctype html>
{% load i18n %}
{% block header%}{% endblock %}
......@@ -5,6 +5,7 @@
<ul class="mm_nav">
<li><a href="{% url 'user_profile' %}">{% trans "Profile" %}</a></li>
<li><a href="{% url 'user_mailmansettings' %}">{% trans "Mailman settings" %}</a></li>
<li><a href="{% url 'user_subscriptions' %}">{% trans "Subscriptions" %}</a></li>
<li><a href="{% url 'user_todos' %}">{% trans "Todos" %}</a></li>
</ul>
</div>
{% extends extend_template %}
{% load url from future %}
{% load i18n %}
{% block main %}
{% include 'postorius/menu/user_nav.html' %}
<h1>List Subscriptions <span></span></h1>
<p>You are subscribed to the following mailing lists:</p>
<table class="table table-bordered table-striped">
<thead>
<tr>
<th>{% trans 'List Name' %}</th>
<th>{% trans 'Subscription Address' %}</th>
<th>{% trans 'Role' %}</th>
<th>{% trans 'Delivery Mode' %}</th>
</tr>
</thead>
<tbody>
{% for subscription in memberships %}
<tr>
<td>{{ subscription.fqdn_listname }}</td>
<td>{{ subscription.address }}</td>
<td>{{ subscription.role }}</td>
<td>{{ subscription.delivery_mode }}</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endblock main %}
......@@ -7,7 +7,8 @@
<span class="mm_context">{% trans 'Users' %}</span>
{% if user.is_superuser %}
<ul class="mm_nav">
<li><a href="{% url 'user_index' %}">{% trans "User Index" %}</a></li>
<li><a href="{% url 'user_index' %}">{% trans "Mailman Users" %}</a></li>
<li><a href="{% url 'user_index' %}">{% trans "Web Users" %}</a></li>
<li class="mm_new_user mm_action"><a class="btn btn-mini btn-success" href="{% url 'user_new' %}">{% trans "Create User" %}</a></li>
</ul>
{% endif %}
......
......@@ -17,11 +17,13 @@
from django.contrib.auth.models import AnonymousUser, User
from django.core.exceptions import PermissionDenied
from django.test.client import RequestFactory
from django.utils import unittest
from mock import patch
from postorius.auth.decorators import (list_owner_required,
list_moderator_required)
list_moderator_required,
basic_auth_login)
from postorius.models import (Domain, List, Member, MailmanUser,
MailmanApiError, Mailman404Error)
from mailmanclient import Client
......
......@@ -32,9 +32,8 @@ urlpatterns = patterns(
url(r'^accounts/logout/$', 'user_logout', name='user_logout'),
url(r'^accounts/profile/$', 'user_profile', name='user_profile'),
url(r'^accounts/todos/$', 'user_todos', name='user_todos'),
url(r'^accounts/membership/(?:(?P<fqdn_listname>[^/]+)/)?$',
'membership_settings', kwargs={"tab": "membership"},
name='membership_settings'),
url(r'^accounts/subscriptions/$', UserSubscriptionsView.as_view(),
name='user_subscriptions'),
url(r'^accounts/mailmansettings/$',
'user_mailmansettings',
name='user_mailmansettings'),
......@@ -81,4 +80,5 @@ urlpatterns = patterns(
url(r'^users/new/$', 'user_new', name='user_new'),
url(r'^users/(?P<user_id>[^/]+)/$',
UserSummaryView.as_view(), name='user_summary'),
url(r'^api/lists/$', 'api_list_index', name='api_list_index'),
) + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
......@@ -17,3 +17,4 @@
# Postorius. If not, see <http://www.gnu.org/licenses/>.
from postorius.views.views import *
from postorius.views.api import *
# -*- coding: utf-8 -*-
# Copyright (C) 1998-2012 by the Free Software Foundation, Inc.
#
# This file is part of Postorius.
#
# Postorius is free software: you can redistribute it and/or modify it under
# the terms of the GNU General Public License as published by the Free
# Software Foundation, either version 3 of the License, or (at your option)
# any later version.
#
# Postorius is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
# more details.
#
# You should have received a copy of the GNU General Public License along with
# Postorius. If not, see <http://www.gnu.org/licenses/>.
import re
import sys
import json
import logging
import requests
from django.conf import settings
from django.contrib import messages
from django.contrib.auth import logout, authenticate, login
from django.contrib.auth.decorators import (login_required,
permission_required,
user_passes_test)
from django.contrib.auth.forms import AuthenticationForm
from django.contrib.auth.models import User
from django.core.urlresolvers import reverse
from django.http import HttpResponse, HttpResponseRedirect
from django.shortcuts import render_to_response, redirect
from django.template import Context, loader, RequestContext
from django.utils.decorators import method_decorator
from django.utils.translation import gettext as _
from urllib2 import HTTPError
from mailmanclient import Client
from postorius import utils
from postorius.models import (Domain, List, Member, MailmanUser,
MailmanApiError, Mailman404Error)
from postorius.forms import *
from postorius.auth.decorators import *
from postorius.views.generic import MailingListView, MailmanUserView
logger = logging.getLogger(__name__)
@basic_auth_login
@loggedin_or_403
def api_list_index(request):
client = Client('%s/3.0' % settings.REST_SERVER,
settings.API_USER, settings.API_PASS)
res, content = client._connection.call('lists')
return HttpResponse(json.dumps(content['entries']),
content_type="application/json")
......@@ -19,7 +19,7 @@
from django.shortcuts import render_to_response, redirect
from django.template import Context, loader, RequestContext
from django.views.generic import TemplateView
from django.views.generic import TemplateView, View
from postorius.models import (Domain, List, Member, MailmanUser,
MailmanApiError, Mailman404Error)
......@@ -61,8 +61,12 @@ class MailmanUserView(TemplateView):
return address
def _get_user(self, user_id):
user_obj = MailmanUser.objects.get_or_404(address=user_id)
try:
user_obj = MailmanUser.objects.get(address=user_id)
except Mailman404Error:
user_obj = None
# replace display_name with first address if display_name is not set
if user_obj is not None:
if user_obj.display_name == 'None' or user_obj.display_name is None:
user_obj.display_name = ''
user_obj.first_address = self._get_first_address(user_obj)
......@@ -70,11 +74,17 @@ class MailmanUserView(TemplateView):
def dispatch(self, request, *args, **kwargs):
# get the user object.
user_id = None
if 'user_id' in kwargs:
user_id = kwargs['user_id']
elif request.user.is_authenticated():
user_id = request.user.email
if user_id is not None:
try:
self.mm_user = self._get_user(kwargs['user_id'])
self.mm_user = self._get_user(user_id)
except MailmanApiError:
return utils.render_api_error(request)
# set the template
if 'template' in kwargs:
self.template = kwargs['template']
......
......@@ -19,7 +19,9 @@
import re
import sys
import json
import logging
import requests
from django.conf import settings
......@@ -28,7 +30,8 @@ from django.contrib.auth import logout, authenticate, login
from django.contrib.auth.decorators import (login_required,
permission_required,
user_passes_test)
from django.contrib.auth.forms import AuthenticationForm
from django.contrib.auth.forms import (AuthenticationForm, PasswordResetForm,
SetPasswordForm, PasswordChangeForm)
from django.contrib.auth.models import User
from django.core.urlresolvers import reverse
from django.http import HttpResponse, HttpResponseRedirect
......@@ -43,7 +46,7 @@ from postorius import utils
from postorius.models import (Domain, List, Member, MailmanUser,
MailmanApiError, Mailman404Error)
from postorius.forms import *
from postorius.auth.decorators import list_owner_required
from postorius.auth.decorators import *
from postorius.views.generic import MailingListView, MailmanUserView
......@@ -710,6 +713,39 @@ class UserSummaryView(MailmanUserView):
context_instance=RequestContext(request))
class UserSubscriptionsView(MailmanUserView):
"""Shows a summary of a user.
"""
def _get_list(self, list_id):
if getattr(self, 'lists', None) is None:
self.lists = {}
if self.lists.get(list_id) is None:
self.lists[list_id] = List.objects.get(fqdn_listname=list_id)
return self.lists[list_id]
def _get_memberships(self):
client = Client('%s/3.0' % settings.REST_SERVER,
settings.API_USER, settings.API_PASS)
memberships = []
for a in self.mm_user.addresses:
members = client._connection.call('members/find',
{'subscriber': a})
for m in members[1]['entries']:
mlist = self._get_list(m['list_id'])
memberships.append(dict(fqdn_listname=mlist.fqdn_listname,
role=m['role'],
delivery_mode=m['delivery_mode'],
address=a))
return memberships
def get(self, request):
memberships = self._get_memberships()
return render_to_response('postorius/user_subscriptions.html',
{'memberships': memberships},
context_instance=RequestContext(request))
@user_passes_test(lambda u: u.is_superuser)
def user_index(request, template='postorius/users/index.html'):
"""Show a table of all users.
......@@ -771,7 +807,7 @@ def user_login(request, template='postorius/login.html'):
context_instance=RequestContext(request))
@login_required
@login_required()
def user_profile(request, user_email=None):
if not request.user.is_authenticated():
return redirect('user_login')
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment