From c206f34644c011da514e5ecbf94f71e471c42f2c Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Wed, 16 Apr 2014 13:37:31 +0200 Subject: [PATCH 1/9] Move taiga/base/auth to taiga/auth --- taiga/{base => }/auth/__init__.py | 0 taiga/{base => }/auth/api.py | 0 taiga/{base => }/auth/serializers.py | 0 taiga/{base => }/auth/tests/__init__.py | 0 taiga/{base => }/auth/tests/tests_auth.py | 0 5 files changed, 0 insertions(+), 0 deletions(-) rename taiga/{base => }/auth/__init__.py (100%) rename taiga/{base => }/auth/api.py (100%) rename taiga/{base => }/auth/serializers.py (100%) rename taiga/{base => }/auth/tests/__init__.py (100%) rename taiga/{base => }/auth/tests/tests_auth.py (100%) diff --git a/taiga/base/auth/__init__.py b/taiga/auth/__init__.py similarity index 100% rename from taiga/base/auth/__init__.py rename to taiga/auth/__init__.py diff --git a/taiga/base/auth/api.py b/taiga/auth/api.py similarity index 100% rename from taiga/base/auth/api.py rename to taiga/auth/api.py diff --git a/taiga/base/auth/serializers.py b/taiga/auth/serializers.py similarity index 100% rename from taiga/base/auth/serializers.py rename to taiga/auth/serializers.py diff --git a/taiga/base/auth/tests/__init__.py b/taiga/auth/tests/__init__.py similarity index 100% rename from taiga/base/auth/tests/__init__.py rename to taiga/auth/tests/__init__.py diff --git a/taiga/base/auth/tests/tests_auth.py b/taiga/auth/tests/tests_auth.py similarity index 100% rename from taiga/base/auth/tests/tests_auth.py rename to taiga/auth/tests/tests_auth.py From 842d02ed1283d5cf14be4bbcbffd7cb5c5406c32 Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Wed, 16 Apr 2014 13:38:30 +0200 Subject: [PATCH 2/9] Move contents from __init__.py to backends.py with much more docstrings. --- taiga/auth/__init__.py | 74 ----------------------------- taiga/auth/backends.py | 105 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 105 insertions(+), 74 deletions(-) create mode 100644 taiga/auth/backends.py diff --git a/taiga/auth/__init__.py b/taiga/auth/__init__.py index 5afd1ce3..8b137891 100644 --- a/taiga/auth/__init__.py +++ b/taiga/auth/__init__.py @@ -1,75 +1 @@ -# -*- coding: utf-8 -*- -import base64 -import re - -from django.core import signing -from django.db.models import get_model -from rest_framework.authentication import BaseAuthentication - -import taiga.base.exceptions as exc - - -class Session(BaseAuthentication): - """ - Same as rest_framework.authentication.SessionAuthentication - but without csrf. - """ - - def authenticate(self, request): - """ - Returns a `User` if the request session currently has a logged in user. - Otherwise returns `None`. - """ - - http_request = request._request - user = getattr(http_request, 'user', None) - - if not user or not user.is_active: - return None - - return (user, None) - - -def get_token_for_user(user): - data = {"user_id": user.id} - return signing.dumps(data) - - -def get_user_for_token(token): - try: - data = signing.loads(token) - except signing.BadSignature: - raise exc.NotAuthenticated("Invalid token") - - model_cls = 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): - """ - Stateless authentication system partially based on oauth. - """ - - auth_rx = re.compile(r"^Bearer (.+)$") - - def authenticate(self, request): - if "HTTP_AUTHORIZATION" not in request.META: - return None - - token_rx_match = self.auth_rx.search(request.META["HTTP_AUTHORIZATION"]) - if not token_rx_match: - return None - - token = token_rx_match.group(1) - user = get_user_for_token(token) - return (user, token) - - def authenticate_header(self, request): - return 'Bearer realm="api"' diff --git a/taiga/auth/backends.py b/taiga/auth/backends.py new file mode 100644 index 00000000..ede6374d --- /dev/null +++ b/taiga/auth/backends.py @@ -0,0 +1,105 @@ +""" +Authentication backends for rest framework. + +This module exposes two backends: session and token. + +The first (session) is a modified version of standard +session authentication backend of restframework with +csrf token disabled. + +And the second (token) implements own version of oauth2 +like authentiacation but with selfcontained tokens. Thats +makes authentication totally stateles. + +It uses django signing framework for create new +selfcontained tokens. This trust tokes from external +fraudulent modifications. +""" + +import base64 +import re + +from django.core import signing +from django.db.models import get_model +from rest_framework.authentication import BaseAuthentication +from taiga.base import exceptions as exc + + +class Session(BaseAuthentication): + """ + Session based authentication like the standard + `rest_framework.authentication.SessionAuthentication` + but with csrf disabled (for obvious reasons because + it is for api. + + NOTE: this is only for api web interface. Is not used + for common api usage and should be disabled on production. + """ + + def authenticate(self, request): + http_request = request._request + user = getattr(http_request, 'user', None) + + if not user or not user.is_active: + return 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 = 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): + """ + Self-contained stateles authentication implementatrion + that work similar to oauth2. + It uses django signing framework for trust data stored + in the token. + """ + + auth_rx = re.compile(r"^Bearer (.+)$") + + def authenticate(self, request): + if "HTTP_AUTHORIZATION" not in request.META: + return None + + token_rx_match = self.auth_rx.search(request.META["HTTP_AUTHORIZATION"]) + if not token_rx_match: + return None + + token = token_rx_match.group(1) + user = get_user_for_token(token) + return (user, token) + + def authenticate_header(self, request): + return 'Bearer realm="api"' From 2ebdfca2530be00aa22c764057453a52130501f1 Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Wed, 16 Apr 2014 13:39:08 +0200 Subject: [PATCH 3/9] Change auth backends authentication on common settings. --- settings/common.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/settings/common.py b/settings/common.py index 4c64750c..d3da4d13 100644 --- a/settings/common.py +++ b/settings/common.py @@ -266,10 +266,10 @@ API_LIMIT_PER_PAGE = 0 REST_FRAMEWORK = { "DEFAULT_AUTHENTICATION_CLASSES": ( # Mainly used by taiga-front - "taiga.base.auth.Token", + "taiga.auth.backends.Token", # Mainly used for api debug. - "taiga.base.auth.Session", + "taiga.auth.backends.Session", ), "FILTER_BACKEND": "taiga.base.filters.FilterBackend", "EXCEPTION_HANDLER": "taiga.base.exceptions.exception_handler", From 9d41a48a463b068ea24537e383342f1a49497ea9 Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Wed, 16 Apr 2014 23:35:09 +0200 Subject: [PATCH 4/9] Move taiga/base/users to taiga/users --- settings/common.py | 2 +- taiga/{base => }/users/__init__.py | 0 taiga/{base => }/users/admin.py | 0 taiga/{base => }/users/api.py | 0 taiga/{base => }/users/fixtures/initial_user.json | 0 taiga/{base => }/users/forms.py | 0 taiga/{base => }/users/migrations/0001_initial.py | 0 ...ld_role_project__del_unique_role_slug__add_unique_role_sl.py | 0 taiga/{base => }/users/migrations/0003_roles_per_project.py | 0 .../{base => }/users/migrations/0004_remove_unassigned_roles.py | 0 taiga/{base => }/users/migrations/__init__.py | 0 taiga/{base => }/users/models.py | 0 taiga/{base => }/users/serializers.py | 0 .../users/templates/emails/password_recovery-body-html.jinja | 0 .../users/templates/emails/password_recovery-body-text.jinja | 0 .../users/templates/emails/password_recovery-subject.jinja | 0 taiga/{base => }/users/tests/__init__.py | 0 17 files changed, 1 insertion(+), 1 deletion(-) rename taiga/{base => }/users/__init__.py (100%) rename taiga/{base => }/users/admin.py (100%) rename taiga/{base => }/users/api.py (100%) rename taiga/{base => }/users/fixtures/initial_user.json (100%) rename taiga/{base => }/users/forms.py (100%) rename taiga/{base => }/users/migrations/0001_initial.py (100%) rename taiga/{base => }/users/migrations/0002_auto__add_field_role_project__del_unique_role_slug__add_unique_role_sl.py (100%) rename taiga/{base => }/users/migrations/0003_roles_per_project.py (100%) rename taiga/{base => }/users/migrations/0004_remove_unassigned_roles.py (100%) rename taiga/{base => }/users/migrations/__init__.py (100%) rename taiga/{base => }/users/models.py (100%) rename taiga/{base => }/users/serializers.py (100%) rename taiga/{base => }/users/templates/emails/password_recovery-body-html.jinja (100%) rename taiga/{base => }/users/templates/emails/password_recovery-body-text.jinja (100%) rename taiga/{base => }/users/templates/emails/password_recovery-subject.jinja (100%) rename taiga/{base => }/users/tests/__init__.py (100%) diff --git a/settings/common.py b/settings/common.py index d3da4d13..3f0018f3 100644 --- a/settings/common.py +++ b/settings/common.py @@ -157,7 +157,7 @@ INSTALLED_APPS = [ "django.contrib.admin", "django.contrib.staticfiles", - "taiga.base.users", + "taiga.users", "taiga.base.notifications", "taiga.base.searches", "taiga.base", diff --git a/taiga/base/users/__init__.py b/taiga/users/__init__.py similarity index 100% rename from taiga/base/users/__init__.py rename to taiga/users/__init__.py diff --git a/taiga/base/users/admin.py b/taiga/users/admin.py similarity index 100% rename from taiga/base/users/admin.py rename to taiga/users/admin.py diff --git a/taiga/base/users/api.py b/taiga/users/api.py similarity index 100% rename from taiga/base/users/api.py rename to taiga/users/api.py diff --git a/taiga/base/users/fixtures/initial_user.json b/taiga/users/fixtures/initial_user.json similarity index 100% rename from taiga/base/users/fixtures/initial_user.json rename to taiga/users/fixtures/initial_user.json diff --git a/taiga/base/users/forms.py b/taiga/users/forms.py similarity index 100% rename from taiga/base/users/forms.py rename to taiga/users/forms.py diff --git a/taiga/base/users/migrations/0001_initial.py b/taiga/users/migrations/0001_initial.py similarity index 100% rename from taiga/base/users/migrations/0001_initial.py rename to taiga/users/migrations/0001_initial.py diff --git a/taiga/base/users/migrations/0002_auto__add_field_role_project__del_unique_role_slug__add_unique_role_sl.py b/taiga/users/migrations/0002_auto__add_field_role_project__del_unique_role_slug__add_unique_role_sl.py similarity index 100% rename from taiga/base/users/migrations/0002_auto__add_field_role_project__del_unique_role_slug__add_unique_role_sl.py rename to taiga/users/migrations/0002_auto__add_field_role_project__del_unique_role_slug__add_unique_role_sl.py diff --git a/taiga/base/users/migrations/0003_roles_per_project.py b/taiga/users/migrations/0003_roles_per_project.py similarity index 100% rename from taiga/base/users/migrations/0003_roles_per_project.py rename to taiga/users/migrations/0003_roles_per_project.py diff --git a/taiga/base/users/migrations/0004_remove_unassigned_roles.py b/taiga/users/migrations/0004_remove_unassigned_roles.py similarity index 100% rename from taiga/base/users/migrations/0004_remove_unassigned_roles.py rename to taiga/users/migrations/0004_remove_unassigned_roles.py diff --git a/taiga/base/users/migrations/__init__.py b/taiga/users/migrations/__init__.py similarity index 100% rename from taiga/base/users/migrations/__init__.py rename to taiga/users/migrations/__init__.py diff --git a/taiga/base/users/models.py b/taiga/users/models.py similarity index 100% rename from taiga/base/users/models.py rename to taiga/users/models.py diff --git a/taiga/base/users/serializers.py b/taiga/users/serializers.py similarity index 100% rename from taiga/base/users/serializers.py rename to taiga/users/serializers.py diff --git a/taiga/base/users/templates/emails/password_recovery-body-html.jinja b/taiga/users/templates/emails/password_recovery-body-html.jinja similarity index 100% rename from taiga/base/users/templates/emails/password_recovery-body-html.jinja rename to taiga/users/templates/emails/password_recovery-body-html.jinja diff --git a/taiga/base/users/templates/emails/password_recovery-body-text.jinja b/taiga/users/templates/emails/password_recovery-body-text.jinja similarity index 100% rename from taiga/base/users/templates/emails/password_recovery-body-text.jinja rename to taiga/users/templates/emails/password_recovery-body-text.jinja diff --git a/taiga/base/users/templates/emails/password_recovery-subject.jinja b/taiga/users/templates/emails/password_recovery-subject.jinja similarity index 100% rename from taiga/base/users/templates/emails/password_recovery-subject.jinja rename to taiga/users/templates/emails/password_recovery-subject.jinja diff --git a/taiga/base/users/tests/__init__.py b/taiga/users/tests/__init__.py similarity index 100% rename from taiga/base/users/tests/__init__.py rename to taiga/users/tests/__init__.py From b7df530546ddb65de0972d86dc59fc0d2d84440d Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Thu, 17 Apr 2014 00:24:23 +0200 Subject: [PATCH 5/9] Refactorized auth controllers. Now controllers only have presentation logic. All domain logic is moved to separate transactional service functions. --- requirements.txt | 3 + taiga/auth/api.py | 210 ++++++++---------- taiga/auth/serializers.py | 12 +- taiga/auth/services.py | 179 +++++++++++++++ taiga/auth/tests/tests_auth.py | 14 +- taiga/base/exceptions.py | 29 ++- taiga/domains/serializers.py | 2 +- taiga/domains/services.py | 49 ++++ taiga/events/tests.py | 2 +- taiga/projects/admin.py | 2 +- taiga/projects/api.py | 5 +- taiga/projects/issues/tests/tests_api.py | 2 +- .../management/commands/sample_data.py | 2 +- taiga/projects/milestones/tests/tests_api.py | 2 +- taiga/projects/models.py | 7 +- taiga/projects/serializers.py | 5 +- taiga/projects/tasks/tests/tests_api.py | 2 +- taiga/projects/tests/tests_api.py | 6 +- taiga/projects/tests/tests_notifications.py | 5 +- taiga/projects/userstories/tests/tests_api.py | 2 +- .../userstories/tests/tests_services.py | 7 +- taiga/projects/wiki/tests/tests_api.py | 2 +- taiga/routers.py | 33 +-- taiga/users/services.py | 27 +++ 24 files changed, 424 insertions(+), 185 deletions(-) create mode 100644 taiga/auth/services.py create mode 100644 taiga/domains/services.py create mode 100644 taiga/users/services.py diff --git a/requirements.txt b/requirements.txt index b789cd5e..d66cdd83 100644 --- a/requirements.txt +++ b/requirements.txt @@ -16,3 +16,6 @@ django-jinja>=0.23 jinja2==2.7.1 pygments>=1.6 django-sites==0.4 + +# Uncomment it if you are using python < 3.4 +#enum34==0.9.23 diff --git a/taiga/auth/api.py b/taiga/auth/api.py index b543346b..05fb85db 100644 --- a/taiga/auth/api.py +++ b/taiga/auth/api.py @@ -1,137 +1,109 @@ -# -*- coding: utf-8 -*- +from functools import partial +from enum import Enum -from django.db.models.loading import get_model -from django.db.models import Q -from django.contrib.auth import logout, login, authenticate -from django.shortcuts import get_object_or_404 from django.utils.translation import ugettext_lazy as _ from rest_framework.response import Response from rest_framework.permissions import AllowAny -from rest_framework import status, viewsets +from rest_framework import status +from rest_framework import viewsets +from rest_framework import serializers + from taiga.base.decorators import list_route - -from taiga.domains.models import DomainMember -from taiga.domains import get_active_domain -from taiga.base.users.models import User, Role -from taiga.base.users.serializers import UserSerializer from taiga.base import exceptions as exc -from taiga.base import auth +from taiga.users.services import get_and_validate_user +from taiga.domains.services import is_public_register_enabled_for_domain -from .serializers import (PublicRegisterSerializer, - PrivateRegisterSerializer, - PrivateGenericRegisterSerializer, - PrivateRegisterExistingSerializer) +from .serializers import PublicRegisterSerializer +from .serializers import PrivateRegisterForExistingUserSerializer +from .serializers import PrivateRegisterForNewUserSerializer + +from .services import private_register_for_existing_user +from .services import private_register_for_new_user +from .services import public_register +from .services import make_auth_response_data + + +def _parse_data(data:dict, *, cls): + """ + Generic function for parse user data using + specified serializer on `cls` keyword parameter. + + Raises: RequestValidationError exception if + some errors found when data is validated. + + Returns the parsed data. + """ + + serializer = cls(data=data) + if not serializer.is_valid(): + raise exc.RequestValidationError(serializer.errors) + return serializer.data + +# Parse public register data +parse_public_register_data = partial(_parse_data, cls=PublicRegisterSerializer) + +# Parse private register data for existing user +parse_private_register_for_existing_user_data = \ + partial(_parse_data, cls=PrivateRegisterForExistingUserSerializer) + +# Parse private register data for new user +parse_private_register_for_new_user_data = \ + partial(_parse_data, cls=PrivateRegisterForNewUserSerializer) + + +class RegisterTypeEnum(Enum): + new_user = 1 + existing_user = 2 + + +def parse_register_type(userdata:dict) -> str: + """ + Parses user data and detects that register type is. + It returns RegisterTypeEnum value. + """ + # Create adhoc inner serializer for avoid parse + # manually the user data. + class _serializer(serializers.Serializer): + existing = serializers.BooleanField() + + instance = _serializer(data=userdata) + if not instance.is_valid(): + raise exc.RequestValidationError(instance.errors) + + if instance.data["existing"]: + return RegisterTypeEnum.existing_user + return RegisterTypeEnum.new_user class AuthViewSet(viewsets.ViewSet): permission_classes = (AllowAny,) - def _create_response(self, user): - serializer = UserSerializer(user) - response_data = serializer.data - - domain = get_active_domain() - response_data['is_site_owner'] = domain.user_is_owner(user) - response_data['is_site_staff'] = domain.user_is_staff(user) - response_data["auth_token"] = auth.get_token_for_user(user) - return response_data - - def _create_domain_member(self, user): - domain = get_active_domain() - - if domain.members.filter(user=user).count() == 0: - domain_member = DomainMember(domain=domain, user=user, email=user.email, - is_owner=False, is_staff=False) - domain_member.save() - - def _send_public_register_email(self, user): - context = {"user": user} - - mbuilder = MagicMailBuilder() - email = mbuilder.public_register_user(user.email, context) - email.send() - def _public_register(self, request): - if not request.domain.public_register: + if not is_public_register_enabled_for_domain(request.domain): raise exc.BadRequest(_("Public register is disabled for this domain.")) - serializer = PublicRegisterSerializer(data=request.DATA) - if not serializer.is_valid(): - raise exc.BadRequest(serializer.errors) + try: + data = parse_public_register_data(request.DATA) + user = public_register(request.domain, **data) + except exc.IntegrityError as e: + raise exc.BadRequest(e.detail) - data = serializer.data - - if User.objects.filter(Q(username=data["username"]) | Q(email=data["email"])).exists(): - raise exc.BadRequest(_("This username or email is already in use.")) - - user = User(username=data["username"], - first_name=data["first_name"], - last_name=data["last_name"], - email=data["email"]) - user.set_password(data["password"]) - user.save() - - self._create_domain_member(user) - #self._send_public_register_email(user) - - response_data = self._create_response(user) - return Response(response_data, status=status.HTTP_201_CREATED) - - def _send_private_register_email(self, user, **kwargs): - context = {"user": user} - context.update(kwargs) - - mbuilder = MagicMailBuilder() - email = mbuilder.private_register_user(user.email, context) - email.send() + data = make_auth_response_data(request.domain, user) + return Response(data, status=status.HTTP_201_CREATED) def _private_register(self, request): - base_serializer = PrivateGenericRegisterSerializer(data=request.DATA) - if not base_serializer.is_valid(): - raise exc.BadRequest(base_serializer.errors) - - membership_model = get_model("projects", "Membership") - try: - membership = membership_model.objects.get(token=base_serializer.data["token"]) - except membership_model.DoesNotExist as e: - raise exc.BadRequest(_("Invalid token")) from e - - if base_serializer.data["existing"]: - serializer = PrivateRegisterExistingSerializer(data=request.DATA) - if not serializer.is_valid(): - raise exc.BadRequest(serializer.errors) - - user = get_object_or_404(User, username=serializer.data["username"]) - if not user.check_password(serializer.data["password"]): - raise exc.BadRequest({"password": _("Incorrect password")}) + register_type = parse_register_type(request.DATA) + if register_type is RegisterTypeEnum.existing_user: + data = parse_private_register_for_existing_user_data(request.DATA) + user = private_register_for_existing_user(request.domain, **data) else: - serializer = PrivateRegisterSerializer(data=request.DATA) - if not serializer.is_valid(): - raise exc.BadRequest(serializer.errors) + data = parse_private_register_for_new_user_data(request.DATA) + user = private_register_for_new_user(request.domain, **data) - data = serializer.data - - if User.objects.filter(Q(username=data["username"]) | Q(email=data["email"])).exists(): - raise exc.BadRequest(_("This username or email is already in use.")) - - user = User(username=data["username"], - first_name=data["first_name"], - last_name=data["last_name"], - email=data["email"]) - user.set_password(data["password"]) - user.save() - - self._create_domain_member(user) - - membership.user = user - membership.save() - - #self._send_private_register_email(user, membership=membership) - - response_data = self._create_response(user) - return Response(response_data, status=status.HTTP_201_CREATED) + data = make_auth_response_data(request.domain, user) + return Response(data, status=status.HTTP_201_CREATED) @list_route(methods=["POST"], permission_classes=[AllowAny]) def register(self, request, **kwargs): @@ -140,7 +112,6 @@ class AuthViewSet(viewsets.ViewSet): return self._public_register(request) elif type == "private": return self._private_register(request) - raise exc.BadRequest(_("invalid register type")) # Login view: /api/v1/auth @@ -148,13 +119,6 @@ class AuthViewSet(viewsets.ViewSet): username = request.DATA.get('username', None) password = request.DATA.get('password', None) - try: - user = User.objects.get(username=username) - except User.DoesNotExist: - raise exc.BadRequest(_("Invalid username or password")) - - if not user.check_password(password): - raise exc.BadRequest(_("Invalid username or password")) - - response_data = self._create_response(user) - return Response(response_data, status=status.HTTP_200_OK) + user = get_and_validate_user(username=username, password=password) + data = make_auth_response_data(request.domain, user) + return Response(data, status=status.HTTP_200_OK) diff --git a/taiga/auth/serializers.py b/taiga/auth/serializers.py index 512873d8..bc020a43 100644 --- a/taiga/auth/serializers.py +++ b/taiga/auth/serializers.py @@ -14,16 +14,10 @@ class PublicRegisterSerializer(BaseRegisterSerializer): pass -class PrivateRegisterSerializer(BaseRegisterSerializer): - pass - - -class PrivateGenericRegisterSerializer(serializers.Serializer): +class PrivateRegisterForNewUserSerializer(BaseRegisterSerializer): token = serializers.CharField(max_length=255, required=True) - existing = serializers.BooleanField() - # existing = serializers.ChoiceField(choices=[("on", "on"), ("off", "off")]) - -class PrivateRegisterExistingSerializer(serializers.Serializer): +class PrivateRegisterForExistingUserSerializer(serializers.Serializer): username = serializers.CharField(max_length=200) password = serializers.CharField(min_length=4) + token = serializers.CharField(max_length=255, required=True) diff --git a/taiga/auth/services.py b/taiga/auth/services.py new file mode 100644 index 00000000..584b1aa3 --- /dev/null +++ b/taiga/auth/services.py @@ -0,0 +1,179 @@ +""" +This module contains a domain logic for authentication +process. It called services because in DDD says it. + +NOTE: Python doesn't have java limitations for "everytghing +should be contained in a class". Because of that, it +not uses clasess and uses simple functions. +""" + +from django.db.models.loading import get_model +from django.db.models import Q +from django.db import transaction as tx +from django.db import IntegrityError +from django.utils.translation import ugettext as _ + +from djmail.template_mail import MagicMailBuilder + +from taiga.base import exceptions as exc +from taiga.users.serializers import UserSerializer +from taiga.users.services import get_and_validate_user +from taiga.domains.services import (create_domain_member, + is_user_exists_on_domain) + +from .backends import get_token_for_user + + +def send_public_register_email(user) -> bool: + """ + Given a user, send public register welcome email + message to specified user. + """ + + context = {"user": user} + mbuilder = MagicMailBuilder() + email = mbuilder.public_register_user(user.email, context) + return bool(email.send()) + + +def send_private_register_email(user, **kwargs) -> bool: + """ + Given a user, send private register welcome + email message to specified user. + """ + context = {"user": user} + context.update(kwargs) + + mbuilder = MagicMailBuilder() + email = mbuilder.private_register_user(user.email, context) + return bool(email.send()) + + +def is_user_already_registred(*, username:str, email:str) -> bool: + """ + Checks if a specified user is already registred. + """ + + user_model = get_model("users", "User") + qs = user_model.objects.filter(Q(username=username) | + Q(email=email)) + return qs.exists() + + +def get_membership_by_token(token:str): + """ + Given a token, returns a membership instance + that matches with specified token. + + If not matches with any membership NotFound exception + is raised. + """ + membership_model = get_model("projects", "Membership") + qs = membership_model.objects.filter(token=token) + if len(qs) == 0: + raise exc.NotFound("Token not matches any member.") + return qs[0] + + +@tx.atomic +def public_register(domain, *, username:str, password:str, + email:str, first_name:str, last_name:str): + """ + Given a parsed parameters, try register a new user + knowing that it follows a public register flow. + + This can raise `exc.IntegrityError` exceptions in + case of conflics found. + + :returns: User + """ + + if is_user_already_registred(username=username, email=email): + raise exc.IntegrityError("User is already registred.") + + user_model = get_model("users", "User") + user = user_model(username=username, + email=email, + first_name=first_name, + last_name=last_name) + user.set_password(password) + user.save() + + if not is_user_exists_on_domain(domain, user): + create_domain_member(domain, user) + + # send_public_register_email(user) + return user + + +@tx.atomic +def private_register_for_existing_user(domain, *, token:str, username:str, password:str): + """ + Register works not only for register users, also serves for accept + inviatations for projects as existing user. + + Given a invitation token with parsed parameters, accept inviation + as existing user. + """ + + user = get_and_validate_user(username=username, password=password) + membership = get_membership_by_token(token) + + if not is_user_exists_on_domain(domain, user): + create_domain_member(domain, user) + + membership.user = user + membership.save(update_fields=["user"]) + + # send_private_register_email(user) + return user + + +@tx.atomic +def private_register_for_new_user(domain, *, token:str, username:str, email:str, + first_name:str, last_name:str, password:str): + """ + Given a inviation token, try register new user matching + the invitation token. + """ + + user_model = get_model("users", "User") + + if is_user_already_registred(username=username, email=email): + raise exc.WrongArguments(_("Username or Email is already in use.")) + + user = user_model(username=username, + email=email, + first_name=first_name, + last_name=last_name) + + user.set_password(password) + try: + user.save() + except IntegrityError: + raise exc.IntegrityError(_("Error on creating new user.")) + + if not is_user_exists_on_domain(domain, user): + create_domain_member(domain, user) + + membership = get_membership_by_token(token) + membership.user = user + membership.save(update_fields=["user"]) + + return user + + +def make_auth_response_data(domain, user) -> dict: + """ + Given a domain and user, creates data structure + using python dict containing a representation + of the logged user. + """ + serializer = UserSerializer(user) + data = dict(serializer.data) + + data['is_site_owner'] = domain.user_is_owner(user) + data['is_site_staff'] = domain.user_is_staff(user) + data["auth_token"] = get_token_for_user(user) + + return data diff --git a/taiga/auth/tests/tests_auth.py b/taiga/auth/tests/tests_auth.py index 4adb3f6b..31a751ac 100644 --- a/taiga/auth/tests/tests_auth.py +++ b/taiga/auth/tests/tests_auth.py @@ -14,16 +14,19 @@ from rest_framework.permissions import IsAuthenticated from rest_framework.response import Response from taiga import urls -from taiga.base import auth -from taiga.base.users.tests import create_user, create_domain +from taiga.users.tests import create_user, create_domain from taiga.projects.tests import create_project from taiga.domains.models import Domain, DomainMember from taiga.projects.models import Membership +from taiga.auth.backends import Token as TokenAuthBackend +from taiga.auth.backends import get_token_for_user + + class TestAuthView(viewsets.ViewSet): - authentication_classes = (auth.Token,) + authentication_classes = (TokenAuthBackend,) permission_classes = (IsAuthenticated,) def get(self, request, *args, **kwargs): @@ -37,6 +40,7 @@ urls.urlpatterns += patterns("", class TokenAuthTests(test.TestCase): fixtures = ["initial_domains.json",] + def setUp(self): self.user1 = create_user(1) @@ -45,9 +49,9 @@ class TokenAuthTests(test.TestCase): self.assertEqual(response.status_code, 401) def test_token_auth_02(self): - token = auth.get_token_for_user(self.user1) + token = get_token_for_user(self.user1) response = self.client.get(reverse("test-token-auth"), - HTTP_AUTHORIZATION="Bearer {}".format(token)) + HTTP_AUTHORIZATION="Bearer {}".format(token)) self.assertEqual(response.status_code, 200) self.assertEqual(response.content, b'"ok"') diff --git a/taiga/base/exceptions.py b/taiga/base/exceptions.py index 290c87cd..34881713 100644 --- a/taiga/base/exceptions.py +++ b/taiga/base/exceptions.py @@ -14,7 +14,7 @@ from .utils.json import to_json class BaseException(exceptions.APIException): status_code = status.HTTP_400_BAD_REQUEST - default_detail = _('Unexpected error') + default_detail = _("Unexpected error") def __init__(self, detail=None): self.detail = detail or self.default_detail @@ -26,7 +26,7 @@ class NotFound(BaseException): """ status_code = status.HTTP_404_NOT_FOUND - default_detail = _('Not found.') + default_detail = _("Not found.") class NotSupported(BaseException): @@ -39,7 +39,7 @@ class BadRequest(BaseException): Exception used on bad arguments detected on api view. """ - default_detail = _('Wrong arguments.') + default_detail = _("Wrong arguments.") class WrongArguments(BaseException): @@ -47,7 +47,11 @@ class WrongArguments(BaseException): Exception used on bad arguments detected on service. This is same as `BadRequest`. """ - default_detail = _('Wrong arguments.') + default_detail = _("Wrong arguments.") + + +class RequestValidationError(BaseException): + default_detail = _("Data validation error") class PermissionDenied(exceptions.PermissionDenied): @@ -58,6 +62,11 @@ class PermissionDenied(exceptions.PermissionDenied): pass +class IntegrityError(BaseException): + status_code = status.HTTP_400_BAD_REQUEST + default_detail = _("Integrity Error for wrong or invalid arguments") + + class PreconditionError(BaseException): """ Error raised on precondition method on viewset. @@ -108,20 +117,20 @@ def exception_handler(exc): if isinstance(exc, exceptions.APIException): headers = {} - if getattr(exc, 'auth_header', None): - headers['WWW-Authenticate'] = exc.auth_header - if getattr(exc, 'wait', None): - headers['X-Throttle-Wait-Seconds'] = '%d' % exc.wait + if getattr(exc, "auth_header", None): + headers["WWW-Authenticate"] = exc.auth_header + if getattr(exc, "wait", None): + headers["X-Throttle-Wait-Seconds"] = "%d" % exc.wait detail = format_exception(exc) return Response(detail, status=exc.status_code, headers=headers) elif isinstance(exc, Http404): - return Response({'_error_message': _('Not found')}, + return Response({"_error_message": _("Not found")}, status=status.HTTP_404_NOT_FOUND) elif isinstance(exc, DjangoPermissionDenied): - return Response({'_error_message': _('Permission denied')}, + return Response({"_error_message": _("Permission denied")}, status=status.HTTP_403_FORBIDDEN) # Note: Unhandled exceptions will raise a 500 error. diff --git a/taiga/domains/serializers.py b/taiga/domains/serializers.py index 6c307630..1537aa04 100644 --- a/taiga/domains/serializers.py +++ b/taiga/domains/serializers.py @@ -13,7 +13,7 @@ # along with this program. If not, see . from rest_framework import serializers -from taiga.base.users.serializers import UserSerializer +from taiga.users.serializers import UserSerializer from .models import Domain, DomainMember diff --git a/taiga/domains/services.py b/taiga/domains/services.py new file mode 100644 index 00000000..96a65938 --- /dev/null +++ b/taiga/domains/services.py @@ -0,0 +1,49 @@ +""" +This module contains a domain logic for domains application. +""" + +from django.db.models.loading import get_model +from django.db import transaction as tx +from django.db import IntegrityError + +from taiga.base import exceptions as exc + + +def is_user_exists_on_domain(domain, user) -> bool: + """ + Checks if user is alredy exists on domain. + """ + return domain.members.filter(user=user).exists() + + +def is_public_register_enabled_for_domain(domain) -> bool: + """ + Checks if a specified domain have public register + activated. + + The implementation is very simple but it encapsulates + request attribute access into more semantic function + call. + """ + return domain.public_register + + +@tx.atomic +def create_domain_member(domain, user): + """ + Given a domain and user, add user as member to + specified domain. + + :returns: DomainMember + """ + domain_member_model = get_model("domains", "DomainMember") + + try: + domain_member = domain_member_model(domain=domain, user=user, + email=user.email, is_owner=False, + is_staff=False) + domain_member.save() + except IntegrityError: + raise exc.IntegrityError("User is already member in a site") + + return domain_member diff --git a/taiga/events/tests.py b/taiga/events/tests.py index e21f7eab..b8c02be1 100644 --- a/taiga/events/tests.py +++ b/taiga/events/tests.py @@ -18,7 +18,7 @@ from django.http import HttpResponse from taiga.projects.tests import create_project from taiga.projects.issues.tests import create_issue -from taiga.base.users.tests import create_user +from taiga.users.tests import create_user from . import middleware as mw from . import changes as ch diff --git a/taiga/projects/admin.py b/taiga/projects/admin.py index 358ce456..d7959c48 100644 --- a/taiga/projects/admin.py +++ b/taiga/projects/admin.py @@ -18,7 +18,7 @@ from django.contrib import admin from django.contrib.contenttypes import generic from taiga.projects.milestones.admin import MilestoneInline -from taiga.base.users.admin import RoleInline +from taiga.users.admin import RoleInline from . import models import reversion diff --git a/taiga/projects/api.py b/taiga/projects/api.py index 51d35a38..0299c19e 100644 --- a/taiga/projects/api.py +++ b/taiga/projects/api.py @@ -32,8 +32,9 @@ from taiga.base import filters from taiga.base import exceptions as exc from taiga.base.decorators import list_route, detail_route from taiga.base.permissions import has_project_perm -from taiga.base.api import ModelCrudViewSet, RetrieveModelMixin -from taiga.base.users.models import Role +from taiga.base.api import ModelCrudViewSet, ModelListViewSet, RetrieveModelMixin +from taiga.users.models import Role +from taiga.projects.aggregates.tags import get_all_tags from . import serializers from . import models diff --git a/taiga/projects/issues/tests/tests_api.py b/taiga/projects/issues/tests/tests_api.py index 6127a958..b9b193f6 100644 --- a/taiga/projects/issues/tests/tests_api.py +++ b/taiga/projects/issues/tests/tests_api.py @@ -6,7 +6,7 @@ from django import test from django.core import mail from django.core.urlresolvers import reverse -from taiga.base.users.tests import create_user +from taiga.users.tests import create_user from taiga.projects.tests import create_project, add_membership from taiga.projects.milestones.tests import create_milestone from taiga.projects.issues.models import Issue diff --git a/taiga/projects/management/commands/sample_data.py b/taiga/projects/management/commands/sample_data.py index 983272f1..08a9fe22 100644 --- a/taiga/projects/management/commands/sample_data.py +++ b/taiga/projects/management/commands/sample_data.py @@ -23,7 +23,7 @@ from django.contrib.contenttypes.models import ContentType from sampledatahelper.helper import SampleDataHelper -from taiga.base.users.models import * +from taiga.users.models import * from taiga.projects.models import * from taiga.projects.milestones.models import * from taiga.projects.userstories.models import * diff --git a/taiga/projects/milestones/tests/tests_api.py b/taiga/projects/milestones/tests/tests_api.py index 5655443d..db0a5c00 100644 --- a/taiga/projects/milestones/tests/tests_api.py +++ b/taiga/projects/milestones/tests/tests_api.py @@ -6,7 +6,7 @@ from django import test from django.core import mail from django.core.urlresolvers import reverse -from taiga.base.users.tests import create_user +from taiga.users.tests import create_user from taiga.projects.tests import create_project, add_membership from taiga.projects.milestones.models import Milestone diff --git a/taiga/projects/models.py b/taiga/projects/models.py index 59b59609..1ac45bd5 100644 --- a/taiga/projects/models.py +++ b/taiga/projects/models.py @@ -32,14 +32,19 @@ from django.utils import timezone from picklefield.fields import PickledObjectField +from taiga.users.models import Role from taiga.domains.models import DomainMember from taiga.projects.userstories.models import UserStory from taiga.base.utils.slug import slugify_uniquely from taiga.base.utils.dicts import dict_sum -from taiga.base.users.models import Role from . import choices +# FIXME: this should to be on choices module (?) +VIDEOCONFERENCES_CHOICES = ( + ('appear-in', 'AppearIn'), + ('talky', 'Talky'), +) class Membership(models.Model): # This model stores all project memberships. Also diff --git a/taiga/projects/serializers.py b/taiga/projects/serializers.py index 70ed30c3..749b466d 100644 --- a/taiga/projects/serializers.py +++ b/taiga/projects/serializers.py @@ -14,15 +14,14 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . +from os import path from rest_framework import serializers from taiga.base.serializers import PickleField -from taiga.base.users.models import Role +from taiga.users.models import Role from . import models -from os import path - class AttachmentSerializer(serializers.ModelSerializer): name = serializers.SerializerMethodField("get_name") diff --git a/taiga/projects/tasks/tests/tests_api.py b/taiga/projects/tasks/tests/tests_api.py index da45a70b..f38f46e2 100644 --- a/taiga/projects/tasks/tests/tests_api.py +++ b/taiga/projects/tasks/tests/tests_api.py @@ -8,7 +8,7 @@ from django.core.urlresolvers import reverse import reversion -from taiga.base.users.tests import create_user +from taiga.users.tests import create_user from taiga.projects.tests import create_project, add_membership from taiga.projects.milestones.tests import create_milestone from taiga.projects.userstories.tests import create_userstory diff --git a/taiga/projects/tests/tests_api.py b/taiga/projects/tests/tests_api.py index f95e3c91..8ba82add 100644 --- a/taiga/projects/tests/tests_api.py +++ b/taiga/projects/tests/tests_api.py @@ -7,10 +7,12 @@ from django.core.urlresolvers import reverse from django.core import mail from django.db.models import get_model -from taiga.base.users.tests import create_user +from taiga.users.tests import create_user from taiga.projects.models import Project, Membership -from . import create_project, add_membership +from . import create_project +from . import add_membership + class ProfileTestCase(test.TestCase): fixtures = ["initial_domains.json"] diff --git a/taiga/projects/tests/tests_notifications.py b/taiga/projects/tests/tests_notifications.py index b889637a..81fe1ce3 100644 --- a/taiga/projects/tests/tests_notifications.py +++ b/taiga/projects/tests/tests_notifications.py @@ -7,12 +7,13 @@ from django.core.urlresolvers import reverse from django.core import mail from django.db.models import get_model -from taiga.base.users.tests import create_user +from taiga.users.tests import create_user from taiga.projects.models import Project, Membership from taiga.projects.issues.tests import create_issue from taiga.projects.tasks.tests import create_task -from . import create_project, add_membership +from . import create_project +from . import add_membership class AllProjectEventsNotificationsTestCase(test.TestCase): fixtures = ["initial_domains.json"] diff --git a/taiga/projects/userstories/tests/tests_api.py b/taiga/projects/userstories/tests/tests_api.py index dd933dc1..f0773019 100644 --- a/taiga/projects/userstories/tests/tests_api.py +++ b/taiga/projects/userstories/tests/tests_api.py @@ -4,7 +4,7 @@ from django import test from django.core import mail from django.core.urlresolvers import reverse -from taiga.base.users.tests import create_user +from taiga.users.tests import create_user from taiga.projects.tests import create_project, add_membership from taiga.projects.milestones.tests import create_milestone from taiga.projects.userstories.models import UserStory diff --git a/taiga/projects/userstories/tests/tests_services.py b/taiga/projects/userstories/tests/tests_services.py index 54dc416c..0d7f9a81 100644 --- a/taiga/projects/userstories/tests/tests_services.py +++ b/taiga/projects/userstories/tests/tests_services.py @@ -1,15 +1,12 @@ -# -*- coding: utf-8 -*- - import json - from django import test -from taiga.base.users.tests import create_user +from taiga.users.tests import create_user from taiga.projects.tests import create_project -from . import create_userstory from .. import services from .. import models +from . import create_userstory class UserStoriesServiceTestCase(test.TestCase): diff --git a/taiga/projects/wiki/tests/tests_api.py b/taiga/projects/wiki/tests/tests_api.py index 9e982fc8..8f39b636 100644 --- a/taiga/projects/wiki/tests/tests_api.py +++ b/taiga/projects/wiki/tests/tests_api.py @@ -6,7 +6,7 @@ from django import test from django.core import mail from django.core.urlresolvers import reverse -from taiga.base.users.tests import create_user +from taiga.users.tests import create_user from taiga.projects.tests import create_project, add_membership from taiga.projects.wiki.models import WikiPage diff --git a/taiga/routers.py b/taiga/routers.py index 3a180ae2..d9f50b27 100644 --- a/taiga/routers.py +++ b/taiga/routers.py @@ -16,32 +16,37 @@ from taiga.base import routers +from taiga.auth.api import AuthViewSet +from taiga.users.api import UsersViewSet, PermissionsViewSet +from taiga.base.searches.api import SearchViewSet +from taiga.base.resolver.api import ResolverViewSet +from taiga.projects.api import (ProjectViewSet, MembershipViewSet, InvitationViewSet, + UserStoryStatusViewSet, PointsViewSet, TaskStatusViewSet, + IssueStatusViewSet, IssueTypeViewSet, PriorityViewSet, + SeverityViewSet, ProjectAdminViewSet, RolesViewSet) #, QuestionStatusViewSet) +from taiga.domains.api import DomainViewSet, DomainMembersViewSet +from taiga.projects.milestones.api import MilestoneViewSet +from taiga.projects.userstories.api import UserStoryViewSet, UserStoryAttachmentViewSet +from taiga.projects.tasks.api import TaskViewSet, TaskAttachmentViewSet +from taiga.projects.issues.api import IssueViewSet, IssueAttachmentViewSet +#from taiga.projects.questions.api import QuestionViewSet, QuestionAttachmentViewSet +#from taiga.projects.documents.api import DocumentViewSet, DocumentAttachmentViewSet +from taiga.projects.wiki.api import WikiViewSet, WikiAttachmentViewSet + + router = routers.DefaultRouter(trailing_slash=False) - -# Users & Auth -from taiga.base.users.api import UsersViewSet -from taiga.base.users.api import PermissionsViewSet -from taiga.base.auth.api import AuthViewSet - +# taiga.users router.register(r"users", UsersViewSet, base_name="users") router.register(r"permissions", PermissionsViewSet, base_name="permissions") router.register(r"auth", AuthViewSet, base_name="auth") # Resolver & Search -from taiga.base.resolver.api import ResolverViewSet -from taiga.base.searches.api import SearchViewSet - router.register(r"resolver", ResolverViewSet, base_name="resolver") router.register(r"search", SearchViewSet, base_name="search") - # Domains -from taiga.domains.api import DomainViewSet -from taiga.domains.api import DomainMembersViewSet -from taiga.projects.api import ProjectAdminViewSet - router.register(r"sites", DomainViewSet, base_name="sites") router.register(r"site-members", DomainMembersViewSet, base_name="site-members") router.register(r"site-projects", ProjectAdminViewSet, base_name="site-projects") diff --git a/taiga/users/services.py b/taiga/users/services.py new file mode 100644 index 00000000..8c73a751 --- /dev/null +++ b/taiga/users/services.py @@ -0,0 +1,27 @@ +""" +This model contains a domain logic for users application. +""" + +from django.db.models.loading import get_model +from taiga.base import exceptions as exc + + +def get_and_validate_user(*, username:str, password:str) -> bool: + """ + Check if user with username exists and specified + password matchs well with existing user password. + + if user is valid, user is returned else, corresponding + exception is raised. + """ + + user_model = get_model("users", "User") + qs = user_model.objects.filter(username=username) + if len(qs) == 0: + raise exc.WrongArguments("Username or password does not matches user.") + + user = qs[0] + if not user.check_password(password): + raise exc.WrongArguments("Username or password does not matches user.") + + return user From dfd794036a24dcdf5131b60cff03c5ff520254aa Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Thu, 17 Apr 2014 01:34:56 +0200 Subject: [PATCH 6/9] Add tests for auth services. --- taiga/auth/{tests/tests_auth.py => tests.py} | 133 +++++++++++++++++-- taiga/auth/tests/__init__.py | 0 2 files changed, 121 insertions(+), 12 deletions(-) rename taiga/auth/{tests/tests_auth.py => tests.py} (54%) delete mode 100644 taiga/auth/tests/__init__.py diff --git a/taiga/auth/tests/tests_auth.py b/taiga/auth/tests.py similarity index 54% rename from taiga/auth/tests/tests_auth.py rename to taiga/auth/tests.py index 31a751ac..de0b709b 100644 --- a/taiga/auth/tests/tests_auth.py +++ b/taiga/auth/tests.py @@ -4,25 +4,26 @@ import uuid import json from django.core.urlresolvers import reverse -from django.conf.urls import patterns, include, url +from django.conf.urls import patterns, url from django import test from django.db.models import get_model -from rest_framework.views import APIView from rest_framework import viewsets from rest_framework.permissions import IsAuthenticated from rest_framework.response import Response from taiga import urls +from taiga.base import exceptions as exc from taiga.users.tests import create_user, create_domain -from taiga.projects.tests import create_project - -from taiga.domains.models import Domain, DomainMember -from taiga.projects.models import Membership - - +from taiga.domains.models import DomainMember +from taiga.domains.services import is_user_exists_on_domain +from taiga.domains import get_default_domain from taiga.auth.backends import Token as TokenAuthBackend from taiga.auth.backends import get_token_for_user +from taiga.auth import services + +from taiga.projects.tests import create_project +from taiga.projects.tests import add_membership class TestAuthView(viewsets.ViewSet): @@ -38,7 +39,117 @@ urls.urlpatterns += patterns("", ) -class TokenAuthTests(test.TestCase): +class AuthServicesTests(test.TestCase): + fixtures = ["initial_domains.json",] + + def setUp(self): + self.user1 = create_user(1) + self.domain = get_default_domain() + + def test_send_public_register_email(self): + """ + This test should explictly fail because these emails + at this momment does not exists. + """ + + with self.assertRaises(Exception): + services.send_public_register_email(self.user1) + + def test_send_private_register_email(self): + """ + This test should explictly fail because these emails + at this momment does not exists. + """ + + with self.assertRaises(Exception): + services.send_private_register_email(self.user1) + + def test_is_user_already_registred(self): + username = self.user1.username + email = self.user1.email + + self.assertTrue(services.is_user_already_registred(username=username, email=email)) + self.assertTrue(services.is_user_already_registred(username=username, email="foo@bar.com")) + self.assertTrue(services.is_user_already_registred(username="foo", email=email)) + self.assertFalse(services.is_user_already_registred(username="foo", email="foo@bar.com")) + + def test_get_membership_by_token(self): + with self.assertRaises(exc.NotFound): + services.get_membership_by_token("invalidtoken") + + project = create_project(1, self.user1) + membership = add_membership(project, self.user1, "back") + membership.token = "foobar" + membership.save() + + m = services.get_membership_by_token("foobar") + self.assertEqual(m.id, membership.id) + + def test_public_register(self): + with self.assertRaises(exc.IntegrityError): + services.public_register(self.domain, + username=self.user1.username, + password="secret", + email=self.user1.email, + first_name="foo", + last_name="bar") + + user = services.public_register(self.domain, + username="foousername", + password="foosecret", + email="foo@bar.ca", + first_name="Foo", + last_name="Bar") + self.assertEqual(user.username, "foousername") + self.assertTrue(user.check_password("foosecret")) + self.assertTrue(is_user_exists_on_domain(self.domain, user)) + + def test_private_register(self): + project = create_project(1, self.user1) + + membership = add_membership(project, self.user1, "back") + membership.user = None + membership.token = "foobar" + membership.save() + + # Try register with invalid token + with self.assertRaises(exc.NotFound): + services.private_register_for_existing_user(self.domain, + token="barfoo", + username=self.user1.username, + password=self.user1.username) + + # Try register with valid token and valid existing user + self.assertEqual(membership.user, None) + user = services.private_register_for_existing_user(self.domain, + token="foobar", + username=self.user1.username, + password=self.user1.username) + + membership = membership.__class__.objects.get(pk=membership.pk) + self.assertEqual(membership.user, user) + + # Try register new user + membership.user = None + membership.token = "token" + membership.save() + + user = services.private_register_for_new_user(self.domain, + token="token", + username="user2", + password="user2", + email="user2@bar.ca", + first_name="Foo", + last_name="Bar") + + + membership = membership.__class__.objects.get(pk=membership.pk) + self.assertEqual(membership.user, user) + self.assertTrue(is_user_exists_on_domain(self.domain, user)) + + + +class TokenAuthenticationBackendTests(test.TestCase): fixtures = ["initial_domains.json",] def setUp(self): @@ -56,7 +167,7 @@ class TokenAuthTests(test.TestCase): self.assertEqual(response.content, b'"ok"') -class RegisterTests(test.TestCase): +class RegisterApiTests(test.TestCase): fixtures = ["initial_domains.json",] def setUp(self): @@ -83,7 +194,6 @@ class RegisterTests(test.TestCase): self.assertEqual(DomainMember.objects.filter(domain=self.domain1).count(), 1) self.assertEqual(self.project.memberships.count(), 0) - def test_public_register_02(self): data = { "username": "pepe", @@ -163,7 +273,6 @@ class RegisterTests(test.TestCase): self.assertEqual(DomainMember.objects.filter(domain=self.domain1).count(), 0) self.assertEqual(DomainMember.objects.filter(domain=self.domain2).count(), 1) - def _create_invitation(self, email): token = str(uuid.uuid1()) membership_model = get_model("projects", "Membership") diff --git a/taiga/auth/tests/__init__.py b/taiga/auth/tests/__init__.py deleted file mode 100644 index e69de29b..00000000 From 6ecbfd39df83a39b060596681768772023bfc47e Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Thu, 17 Apr 2014 01:58:30 +0200 Subject: [PATCH 7/9] Uncomment enum34 on requirements.txt --- requirements.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/requirements.txt b/requirements.txt index d66cdd83..e9167e59 100644 --- a/requirements.txt +++ b/requirements.txt @@ -17,5 +17,5 @@ jinja2==2.7.1 pygments>=1.6 django-sites==0.4 -# Uncomment it if you are using python < 3.4 -#enum34==0.9.23 +# Comment it if you are using python >= 3.4 +enum34==0.9.23 From 71479e415c509186535cf2d8dfc3aa8add97b12d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Espino?= Date: Mon, 21 Apr 2014 18:11:50 +0200 Subject: [PATCH 8/9] removed unnecesary line --- taiga/projects/api.py | 1 - 1 file changed, 1 deletion(-) diff --git a/taiga/projects/api.py b/taiga/projects/api.py index 0299c19e..58d52b93 100644 --- a/taiga/projects/api.py +++ b/taiga/projects/api.py @@ -34,7 +34,6 @@ from taiga.base.decorators import list_route, detail_route from taiga.base.permissions import has_project_perm from taiga.base.api import ModelCrudViewSet, ModelListViewSet, RetrieveModelMixin from taiga.users.models import Role -from taiga.projects.aggregates.tags import get_all_tags from . import serializers from . import models From 53bc47272433640ad242c1bcb3d16b443d3a360c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Espino?= Date: Tue, 22 Apr 2014 10:03:07 +0200 Subject: [PATCH 9/9] Changed the path of users app on dumpdata_rol.sh script --- dumpdata_role.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dumpdata_role.sh b/dumpdata_role.sh index cddf4953..594a5d1d 100755 --- a/dumpdata_role.sh +++ b/dumpdata_role.sh @@ -1,3 +1,3 @@ #!/bin/bash -python ./manage.py dumpdata -n --indent=4 users.Role > taiga/base/users/fixtures/initial_role.json +python ./manage.py dumpdata -n --indent=4 users.Role > taiga/users/fixtures/initial_role.json