Skip to content

Commit

Permalink
Merge pull request #794 from tonioo/change_password_api_523
Browse files Browse the repository at this point in the history
First draft of a REST API.
  • Loading branch information
tonioo committed Nov 27, 2015
2 parents d6ec507 + 0411ac2 commit 834e347
Show file tree
Hide file tree
Showing 25 changed files with 356 additions and 21 deletions.
6 changes: 3 additions & 3 deletions modoboa/admin/lib.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,21 +55,21 @@ def get_identities(user, searchquery=None, idtfilter=None, grpfilter=None):
q &= Q(is_superuser=True)
else:
q &= Q(groups__name=grpfilter)
accounts = User.objects.select_related().filter(q)
accounts = User.objects.filter(q).prefetch_related("groups")

aliases = []
if idtfilter is None or not idtfilter \
or (idtfilter in ["alias", "forward", "dlist"]):
alct = ContentType.objects.get_for_model(Alias)
ids = user.objectaccess_set.filter(content_type=alct) \
.values_list('object_id', flat=True)
q = Q(pk__in=ids)
q = Q(pk__in=ids, internal=False)
if searchquery is not None:
q &= (
Q(address__icontains=searchquery) |
Q(domain__name__icontains=searchquery)
)
aliases = Alias.objects.select_related().filter(q)
aliases = Alias.objects.select_related("domain").filter(q)
if idtfilter is not None and idtfilter:
aliases = [al for al in aliases if al.type == idtfilter]
return chain(accounts, aliases)
Expand Down
19 changes: 19 additions & 0 deletions modoboa/admin/migrations/0003_auto_20151118_1215.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('admin', '0002_migrate_from_modoboa_admin'),
]

operations = [
migrations.AlterField(
model_name='aliasrecipient',
name='address',
field=models.EmailField(max_length=254),
),
]
4 changes: 2 additions & 2 deletions modoboa/admin/tests/export.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ def assertListEqual(self, list1, list2):
def test_export_identities(self):
response = self.__export_identities()
self.assertListEqual(
"account;admin@test.com;{PLAIN}toto;;;True;DomainAdmins;admin@test.com;10;test.com\r\naccount;admin@test2.com;{PLAIN}toto;;;True;DomainAdmins;admin@test2.com;10;test2.com\r\naccount;user@test.com;{PLAIN}toto;;;True;SimpleUsers;user@test.com;10\r\naccount;user@test2.com;{PLAIN}toto;;;True;SimpleUsers;user@test2.com;10\r\nalias;alias@test.com;True;user@test.com\r\nforward;forward@test.com;True;user@external.com\r\ndlist;postmaster@test.com;True;toto@titi.com;test@truc.fr\r\n",
"account;admin@test.com;{PLAIN}toto;;;True;DomainAdmins;admin@test.com;10;test.com\r\naccount;admin@test2.com;{PLAIN}toto;;;True;DomainAdmins;admin@test2.com;10;test2.com\r\naccount;user@test.com;{PLAIN}toto;;;True;SimpleUsers;user@test.com;10\r\naccount;user@test2.com;{PLAIN}toto;;;True;SimpleUsers;user@test2.com;10\r\nalias;alias@test.com;True;user@test.com\r\nforward;forward@test.com;True;user@external.com\r\ndlist;postmaster@test.com;True;test@truc.fr;toto@titi.com\r\n",
response.content.strip()
)

Expand Down Expand Up @@ -92,5 +92,5 @@ def test_export_dlists(self):
response = self.__export_identities(idtfilter="dlist")
self.assertEqual(
response.content.strip(),
"dlist;postmaster@test.com;True;toto@titi.com;test@truc.fr"
"dlist;postmaster@test.com;True;test@truc.fr;toto@titi.com"
)
29 changes: 29 additions & 0 deletions modoboa/core/api.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
"""Modoboa core viewsets."""

from django.http import Http404

from rest_framework import status
from rest_framework.response import Response
from rest_framework.views import APIView

from . import models
from . import serializers


class UserPasswordChangeAPIView(APIView):

"""A view to update password for user instances."""

def put(self, request, pk, format=None):
"""PUT method hander."""
try:
user = models.User.objects.get(username=pk)
except models.User.DoesNotExist:
raise Http404
serializer = serializers.UserPasswordSerializer(
user, data=request.data)
if serializer.is_valid():
serializer.save()
return Response()
return Response(
serializer.errors, status=status.HTTP_400_BAD_REQUEST)
12 changes: 12 additions & 0 deletions modoboa/core/commands/templates/settings.py.tpl
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ INSTALLED_APPS = (
'django.contrib.sites',
'django.contrib.staticfiles',
'reversion',
'rest_framework.authtoken'
{% if devmode %} 'djangobower',{% endif %}
)

Expand Down Expand Up @@ -148,6 +149,17 @@ STATICFILES_DIRS = (
MEDIA_URL = '/media/'
MEDIA_ROOT = os.path.join(BASE_DIR, 'media')

# Rest framework settings

REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': (
'rest_framework.authentication.TokenAuthentication',
),
'DEFAULT_PERMISSION_CLASSES': (
'rest_framework.permissions.IsAuthenticated',
)
}

# Modoboa settings
#MODOBOA_CUSTOM_LOGO = os.path.join(MEDIA_URL, "custom_logo.png")

Expand Down
14 changes: 14 additions & 0 deletions modoboa/core/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -90,3 +90,17 @@ def save(self, commit=True):
)
user.save()
return user


class APIAccessForm(forms.Form):

"""Form to control API access."""

enable_api_access = forms.BooleanField(
label=_("Enable API access"), required=False)

def __init__(self, *args, **kwargs):
"""Initialize form."""
user = kwargs.pop("user")
super(APIAccessForm, self).__init__(*args, **kwargs)
self.fields["enable_api_access"].initial = hasattr(user, "auth_token")
1 change: 1 addition & 0 deletions modoboa/core/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,7 @@ def set_password(self, raw_value, curvalue=None):
)

def check_password(self, raw_value):
"""Compare raw_value to current password."""
match = self.password_expr.match(self.password)
if match is None:
return False
Expand Down
41 changes: 41 additions & 0 deletions modoboa/core/serializers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
"""Modoboa core serializers."""

from django.core.exceptions import ValidationError

from rest_framework import serializers
from passwords import validators

from . import models


class UserPasswordSerializer(serializers.ModelSerializer):

"""A serializer used to change a user password."""

new_password = serializers.CharField()

class Meta:
model = models.User
fields = (
"password", "new_password", )

def validate_password(self, value):
"""Check password."""
if not self.instance.check_password(value):
raise serializers.ValidationError("Password not correct")
return value

def validate_new_password(self, value):
"""Check new password."""
try:
validators.validate_length(value)
validators.complexity(value)
except ValidationError as exc:
raise serializers.ValidationError(exc.messages[0])
return value

def update(self, instance, validated_data):
"""Set new password."""
instance.set_password(validated_data["new_password"])
instance.save()
return instance
32 changes: 32 additions & 0 deletions modoboa/core/templates/core/api_access.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
{% load i18n form_tags %}

<h2>{% trans "API access" %} <small>{% trans "Control your access to Modoboa API" %}</small></h2>
<hr>
{% if user.auth_token %}
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="panel-title">{% trans "API access token" %}</h3>
</div>
<div class="panel-body">
{{ user.auth_token }}
</div>
</div>
<br>
{% endif %}
<form class="form-inline" method="POST" action="{% url 'core:user_api_access' %}"
id="api_form">
{% csrf_token %}
<div class="row">
<div class="col-sm-2">
<div class="checkbox">
<label>
{{ form.enable_api_access }}
{{ form.enable_api_access.label }}
</label>
</div>
</div>
<div class="col-sm-3">
<button type="submit" class="btn btn-primary" id="update" name="update">{% trans 'Update' %}</button>
</div>
</div>
</form>
2 changes: 1 addition & 1 deletion modoboa/core/templates/core/user_index.html
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
deflocation: "profile/",
formid: "uprefs_form",
divid: "uprefs_content",
reload_exceptions: ["profile"]
reload_exceptions: ["profile", "api"]
});
});
</script>
Expand Down
9 changes: 6 additions & 3 deletions modoboa/core/templates/core/user_profile.html
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
{% load i18n form_tags %}{% load url from future %}
{% load i18n form_tags %}
{% load url from future %}

<h2>{% trans "Profile" %} <small>{% trans "Update your personal information" %}</small></h2>
<hr>
<form class="form-horizontal" method="POST" action="{% url 'core:user_profile' %}"
id="uprefs_form">{% csrf_token %}
id="uprefs_form">
{% csrf_token %}
{% render_form form %}
<div class="form-actions">
<button type="submit" class="btn btn-primary col-lg-offset-8 col-sm-offset-7 col-lg-1 col-sm-2" id="update" name="update">{% trans 'Update' %}</button>
<button type="submit" class="btn btn-primary btn-lg col-sm-offset-4 col-sm-4" id="update" name="update">{% trans 'Update' %}</button>
</div>
</form>
7 changes: 7 additions & 0 deletions modoboa/core/templatetags/core_tags.py
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,13 @@ def uprefs_menu(selection, user):
"url": "preferences/",
"label": _("Preferences")},
]
if user.is_superuser:
entries.append({
"name": "api",
"class": "ajaxnav",
"url": "api/",
"label": _("API"),
})
entries += events.raiseQueryEvent("UserMenuDisplay", "uprefs_menu", user)
entries = sorted(entries, key=lambda e: e["label"])
return render_to_string('common/menu.html', {
Expand Down
66 changes: 65 additions & 1 deletion modoboa/core/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@

from django.core.urlresolvers import reverse

from modoboa.lib.tests import ModoTestCase
from modoboa.lib.tests import ModoTestCase, ModoAPITestCase
from . import factories
from . import models


class AuthenticationTestCase(ModoTestCase):
Expand Down Expand Up @@ -80,3 +81,66 @@ def test_update_password(self):
self.assertTrue(
self.clt.login(username="user@test.com", password="Toto1234")
)


class APIAccessFormTestCase(ModoTestCase):

"""Check form access."""

@classmethod
def setUpTestData(cls):
"""Create test data."""
super(APIAccessFormTestCase, cls).setUpTestData()
cls.account = factories.UserFactory(
username="user@test.com", groups=('SimpleUsers',)
)

def test_form_access(self):
"""Check access restrictions."""
url = reverse("core:user_api_access")
self.ajax_get(url)
self.clt.logout()
self.clt.login(username="user@test.com", password="toto")
response = self.clt.get(url, HTTP_X_REQUESTED_WITH="XMLHttpRequest")
self.assertEqual(response.status_code, 278)

def test_form(self):
"""Check that token is created/removed."""
url = reverse("core:user_api_access")
self.ajax_post(url, {"enable_api_access": True})
user = models.User.objects.get(username="admin")
self.assertTrue(hasattr(user, "auth_token"))
self.ajax_post(url, {"enable_api_access": False})
user = models.User.objects.get(username="admin")
self.assertFalse(hasattr(user, "auth_token"))


class APITestCase(ModoAPITestCase):

"""Check API."""

@classmethod
def setUpTestData(cls):
"""Create test data."""
super(APITestCase, cls).setUpTestData()
cls.account = factories.UserFactory(
username="user@test.com", groups=('SimpleUsers', )
)

def test_change_password(self):
"""Check the change password service."""
url = reverse(
"external_api:user_password_change", args=["user@test.com"])
response = self.client.put(
url, {"password": "toto", "new_password": "pass"},
format="json")
# must fail because password is too weak
self.assertEqual(response.status_code, 400)

response = self.client.put(
url, {"password": "toto", "new_password": "Toto1234"},
format="json")
self.assertEqual(response.status_code, 200)
self.assertTrue(
models.User.objects.get(
pk=self.account.pk).check_password("Toto1234"))
2 changes: 2 additions & 0 deletions modoboa/core/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,4 +29,6 @@
name="user_preferences"),
url(r'^user/profile/$', 'modoboa.core.views.user.profile',
name="user_profile"),
url(r'^user/api/$', 'modoboa.core.views.user.api_access',
name="user_api_access"),
)
14 changes: 14 additions & 0 deletions modoboa/core/urls_api.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
"""Core API urls."""

from django.conf.urls import patterns, url

from . import api


urlpatterns = patterns(
"",
url("^users/(?P<pk>.+)/password/$",
api.UserPasswordChangeAPIView.as_view(),
name="user_password_change"),

)

0 comments on commit 834e347

Please sign in to comment.