Removing cancel_token and using django.core.signing stuff

remotes/origin/enhancement/email-actions
Alejandro Alonso 2014-10-09 18:06:17 +02:00
parent 4404a58b45
commit 4b859bbde9
12 changed files with 84 additions and 77 deletions

View File

@ -271,6 +271,9 @@ AUTHENTICATION_BACKENDS = (
"django.contrib.auth.backends.ModelBackend", # default "django.contrib.auth.backends.ModelBackend", # default
) )
MAX_AGE_AUTH_TOKEN = None
MAX_AGE_CANCEL_ACCOUNT = 30 * 24 * 60 * 60 # 30 days in seconds
ANONYMOUS_USER_ID = -1 ANONYMOUS_USER_ID = -1
MAX_SEARCH_RESULTS = 100 MAX_SEARCH_RESULTS = 100

View File

@ -35,11 +35,10 @@ fraudulent modifications.
import base64 import base64
import re import re
from django.core import signing from django.conf import settings
from django.apps import apps
from rest_framework.authentication import BaseAuthentication from rest_framework.authentication import BaseAuthentication
from taiga.base import exceptions as exc
from .tokens import get_user_for_token
class Session(BaseAuthentication): class Session(BaseAuthentication):
""" """
@ -62,39 +61,6 @@ class Session(BaseAuthentication):
return (user, None) return (user, None)
def get_token_for_user(user):
"""
Generate a new signed token containing
a specified user.
"""
data = {"user_id": user.id}
return signing.dumps(data)
def get_user_for_token(token):
"""
Given a selfcontained token, try parse and
unsign it.
If token passes a validation, returns
a user instance corresponding with user_id stored
in the incoming token.
"""
try:
data = signing.loads(token)
except signing.BadSignature:
raise exc.NotAuthenticated("Invalid token")
model_cls = apps.get_model("users", "User")
try:
user = model_cls.objects.get(pk=data["user_id"])
except model_cls.DoesNotExist:
raise exc.NotAuthenticated("Invalid token")
else:
return user
class Token(BaseAuthentication): class Token(BaseAuthentication):
""" """
Self-contained stateles authentication implementatrion Self-contained stateles authentication implementatrion
@ -114,7 +80,10 @@ class Token(BaseAuthentication):
return None return None
token = token_rx_match.group(1) token = token_rx_match.group(1)
user = get_user_for_token(token) max_age_auth_token = getattr(settings, "MAX_AGE_AUTH_TOKEN", None)
user = get_user_for_token(token, "authentication",
max_age=max_age_auth_token)
return (user, token) return (user, token)
def authenticate_header(self, request): def authenticate_header(self, request):

View File

@ -35,7 +35,7 @@ from taiga.base import exceptions as exc
from taiga.users.serializers import UserSerializer from taiga.users.serializers import UserSerializer
from taiga.users.services import get_and_validate_user from taiga.users.services import get_and_validate_user
from .backends import get_token_for_user from .tokens import get_token_for_user
from .signals import user_registered as user_registered_signal from .signals import user_registered as user_registered_signal
def send_register_email(user) -> bool: def send_register_email(user) -> bool:
@ -43,8 +43,8 @@ def send_register_email(user) -> bool:
Given a user, send register welcome email Given a user, send register welcome email
message to specified user. message to specified user.
""" """
cancel_token = get_token_for_user(user, "cancel_account")
context = {"user": user} context = {"user": user, "cancel_token": cancel_token}
mbuilder = MagicMailBuilder() mbuilder = MagicMailBuilder()
email = mbuilder.registered_user(user.email, context) email = mbuilder.registered_user(user.email, context)
return bool(email.send()) return bool(email.send())
@ -207,5 +207,5 @@ def make_auth_response_data(user) -> dict:
""" """
serializer = UserSerializer(user) serializer = UserSerializer(user)
data = dict(serializer.data) data = dict(serializer.data)
data["auth_token"] = get_token_for_user(user) data["auth_token"] = get_token_for_user(user, "authentication")
return data return data

54
taiga/auth/tokens.py Normal file
View File

@ -0,0 +1,54 @@
# Copyright (C) 2014 Andrey Antukh <niwi@niwi.be>
# Copyright (C) 2014 Jesús Espino <jespinog@gmail.com>
# Copyright (C) 2014 David Barragán <bameda@dbarragan.com>
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program 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 Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from taiga.base import exceptions as exc
from django.apps import apps
from django.core import signing
def get_token_for_user(user, scope):
"""
Generate a new signed token containing
a specified user limited for a scope (identified as a string).
"""
data = {"user_%s_id"%(scope): user.id}
return signing.dumps(data)
def get_user_for_token(token, scope, max_age=None):
"""
Given a selfcontained token and a scope try to parse and
unsign it.
If max_age is specified it checks token expiration.
If token passes a validation, returns
a user instance corresponding with user_id stored
in the incoming token.
"""
try:
data = signing.loads(token, max_age=max_age)
except signing.BadSignature:
raise exc.NotAuthenticated("Invalid token")
model_cls = apps.get_model("users", "User")
try:
user = model_cls.objects.get(pk=data["user_%s_id"%(scope)])
except model_cls.DoesNotExist:
raise exc.NotAuthenticated("Invalid token")
else:
return user

View File

@ -48,7 +48,7 @@ class UserAdmin(DjangoUserAdmin):
fieldsets = ( fieldsets = (
(None, {'fields': ('username', 'password')}), (None, {'fields': ('username', 'password')}),
(_('Personal info'), {'fields': ('full_name', 'email', 'bio', 'photo')}), (_('Personal info'), {'fields': ('full_name', 'email', 'bio', 'photo')}),
(_('Extra info'), {'fields': ('color', 'default_language', 'default_timezone', 'token', 'colorize_tags', 'email_token', 'new_email', 'cancel_token')}), (_('Extra info'), {'fields': ('color', 'default_language', 'default_timezone', 'token', 'colorize_tags', 'email_token', 'new_email')}),
(_('Permissions'), {'fields': ('is_active', 'is_superuser',)}), (_('Permissions'), {'fields': ('is_active', 'is_superuser',)}),
(_('Important dates'), {'fields': ('last_login', 'date_joined')}), (_('Important dates'), {'fields': ('last_login', 'date_joined')}),
) )

View File

@ -22,6 +22,7 @@ from django.shortcuts import get_object_or_404
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from django.core.validators import validate_email from django.core.validators import validate_email
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.conf import settings
from easy_thumbnails.source_generators import pil_image from easy_thumbnails.source_generators import pil_image
@ -32,6 +33,7 @@ from rest_framework import status
from djmail.template_mail import MagicMailBuilder from djmail.template_mail import MagicMailBuilder
from taiga.auth.tokens import get_user_for_token
from taiga.base.decorators import list_route, detail_route from taiga.base.decorators import list_route, detail_route
from taiga.base import exceptions as exc from taiga.base import exceptions as exc
from taiga.base.api import ModelCrudViewSet from taiga.base.api import ModelCrudViewSet
@ -268,8 +270,10 @@ class UsersViewSet(ModelCrudViewSet):
raise exc.WrongArguments(_("Invalid, are you sure the token is correct?")) raise exc.WrongArguments(_("Invalid, are you sure the token is correct?"))
try: try:
user = models.User.objects.get(cancel_token=serializer.data["cancel_token"]) max_age_cancel_account = getattr(settings, "MAX_AGE_CANCEL_ACCOUNT", None)
except models.User.DoesNotExist: user = get_user_for_token(serializer.data["cancel_token"], "cancel_account",
max_age=max_age_cancel_account)
except exc.NotAuthenticated:
raise exc.WrongArguments(_("Invalid, are you sure the token is correct?")) raise exc.WrongArguments(_("Invalid, are you sure the token is correct?"))
user.cancel() user.cancel()

View File

@ -1,20 +0,0 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations
class Migration(migrations.Migration):
dependencies = [
('users', '0005_alter_user_photo'),
]
operations = [
migrations.AddField(
model_name='user',
name='cancel_token',
field=models.CharField(default=None, max_length=200, blank=True, null=True, verbose_name='email token'),
preserve_default=True,
),
]

View File

@ -31,6 +31,7 @@ from django.utils.encoding import force_bytes
from djorm_pgarray.fields import TextArrayField from djorm_pgarray.fields import TextArrayField
from taiga.auth.tokens import get_token_for_user
from taiga.base.utils.slug import slugify_uniquely from taiga.base.utils.slug import slugify_uniquely
from taiga.base.utils.iterators import split_by_n from taiga.base.utils.iterators import split_by_n
from taiga.permissions.permissions import MEMBERS_PERMISSIONS from taiga.permissions.permissions import MEMBERS_PERMISSIONS
@ -124,9 +125,6 @@ class User(AbstractBaseUser, PermissionsMixin):
github_id = models.IntegerField(null=True, blank=True, verbose_name=_("github ID")) github_id = models.IntegerField(null=True, blank=True, verbose_name=_("github ID"))
cancel_token = models.CharField(max_length=200, null=True, blank=True, default=None,
verbose_name=_("cancel account token"))
USERNAME_FIELD = 'username' USERNAME_FIELD = 'username'
REQUIRED_FIELDS = ['email'] REQUIRED_FIELDS = ['email']
@ -151,9 +149,7 @@ class User(AbstractBaseUser, PermissionsMixin):
return self.full_name or self.username or self.email return self.full_name or self.username or self.email
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
if not self.cancel_token: get_token_for_user(self, "cancel_account")
self.cancel_token = str(uuid.uuid1())
super().save(*args, **kwargs) super().save(*args, **kwargs)
def cancel(self): def cancel(self):

View File

@ -7,8 +7,7 @@
<p>Welcome to Taiga, an Open Source, Agile Project Management Tool</p> <p>Welcome to Taiga, an Open Source, Agile Project Management Tool</p>
<p>You, or someone you know has invited you to Taiga. You may remove your account from this service by clicking here:</p> <p>You, or someone you know has invited you to Taiga. You may remove your account from this service by clicking here:</p>
{{ resolve_front_url('cancel-account', cancel_token) }}
{{ resolve_front_url('cancel-account', user.cancel_token) }}
<p>We built Taiga because we wanted the project management tool that sits open on our computers all day long, to serve as a continued reminder of why we love to collaborate, code and design. We built it to be beautiful, elegant, simple to use and fun - without forsaking flexibility and power. We named it Taiga after the forest biome, because like its namesake, our tool is at the center of an ecosystem - your team, your projects. A great project management tool also helps you see the forest for the trees. We couldn't think of a more appropriate name for what we've done.</p> <p>We built Taiga because we wanted the project management tool that sits open on our computers all day long, to serve as a continued reminder of why we love to collaborate, code and design. We built it to be beautiful, elegant, simple to use and fun - without forsaking flexibility and power. We named it Taiga after the forest biome, because like its namesake, our tool is at the center of an ecosystem - your team, your projects. A great project management tool also helps you see the forest for the trees. We couldn't think of a more appropriate name for what we've done.</p>
@ -23,5 +22,3 @@
The Taiga development team. The Taiga development team.
</p> </p>
{% endblock %} {% endblock %}

View File

@ -2,7 +2,7 @@ Welcome to Taiga, an Open Source, Agile Project Management Tool
You, or someone you know has invited you to Taiga. You may remove your account from this service by clicking here: You, or someone you know has invited you to Taiga. You may remove your account from this service by clicking here:
{{ resolve_front_url('cancel-account', user.cancel_token) }} {{ resolve_front_url('cancel-account', cancel_token) }}
We built Taiga because we wanted the project management tool that sits open on our computers all day long, to serve as a continued reminder of why we love to collaborate, code and design. We built it to be beautiful, elegant, simple to use and fun - without forsaking flexibility and power. We named it Taiga after the forest biome, because like its namesake, our tool is at the center of an ecosystem - your team, your projects. A great project management tool also helps you see the forest for the trees. We couldn't think of a more appropriate name for what we've done. We built Taiga because we wanted the project management tool that sits open on our computers all day long, to serve as a continued reminder of why we love to collaborate, code and design. We built it to be beautiful, elegant, simple to use and fun - without forsaking flexibility and power. We named it Taiga after the forest biome, because like its namesake, our tool is at the center of an ecosystem - your team, your projects. A great project management tool also helps you see the forest for the trees. We couldn't think of a more appropriate name for what we've done.

View File

@ -27,6 +27,7 @@ from .. import factories
from taiga.base.connectors import github from taiga.base.connectors import github
from taiga.front import resolve as resolve_front_url from taiga.front import resolve as resolve_front_url
from taiga.users import models from taiga.users import models
from taiga.auth.tokens import get_token_for_user
pytestmark = pytest.mark.django_db pytestmark = pytest.mark.django_db
@ -89,7 +90,8 @@ def test_response_200_in_public_registration(client, settings):
assert len(mail.outbox) == 1 assert len(mail.outbox) == 1
assert mail.outbox[0].subject == "You've been Taigatized!" assert mail.outbox[0].subject == "You've been Taigatized!"
user = models.User.objects.get(username="mmcfly") user = models.User.objects.get(username="mmcfly")
cancel_url = resolve_front_url("cancel-account", user.cancel_token) cancel_token = get_token_for_user(user, "cancel_account")
cancel_url = resolve_front_url("cancel-account", cancel_token)
assert mail.outbox[0].body.index(cancel_url) > 0 assert mail.outbox[0].body.index(cancel_url) > 0
def test_response_200_in_registration_with_github_account(client, settings): def test_response_200_in_registration_with_github_account(client, settings):

View File

@ -6,6 +6,7 @@ from django.core.urlresolvers import reverse
from .. import factories as f from .. import factories as f
from taiga.users import models from taiga.users import models
from taiga.auth.tokens import get_token_for_user
pytestmark = pytest.mark.django_db pytestmark = pytest.mark.django_db
@ -130,7 +131,8 @@ def test_api_user_delete(client):
def test_api_user_cancel_valid_token(client): def test_api_user_cancel_valid_token(client):
user = f.UserFactory.create() user = f.UserFactory.create()
url = reverse('users-cancel') url = reverse('users-cancel')
data = {"cancel_token": user.cancel_token} cancel_token = get_token_for_user(user, "cancel_account")
data = {"cancel_token": cancel_token}
client.login(user) client.login(user)
response = client.post(url, json.dumps(data), content_type="application/json") response = client.post(url, json.dumps(data), content_type="application/json")