Removing cancel_token and using django.core.signing stuff
parent
4404a58b45
commit
4b859bbde9
|
@ -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
|
||||||
|
|
|
@ -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):
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
|
@ -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')}),
|
||||||
)
|
)
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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,
|
|
||||||
),
|
|
||||||
]
|
|
|
@ -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):
|
||||||
|
|
|
@ -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 %}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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.
|
||||||
|
|
||||||
|
|
|
@ -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):
|
||||||
|
|
|
@ -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")
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue