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
diff --git a/requirements.txt b/requirements.txt
index b789cd5e..e9167e59 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
+
+# Comment it if you are using python >= 3.4
+enum34==0.9.23
diff --git a/settings/common.py b/settings/common.py
index 4c64750c..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",
@@ -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",
diff --git a/taiga/auth/__init__.py b/taiga/auth/__init__.py
new file mode 100644
index 00000000..8b137891
--- /dev/null
+++ b/taiga/auth/__init__.py
@@ -0,0 +1 @@
+
diff --git a/taiga/auth/api.py b/taiga/auth/api.py
new file mode 100644
index 00000000..05fb85db
--- /dev/null
+++ b/taiga/auth/api.py
@@ -0,0 +1,124 @@
+from functools import partial
+from enum import Enum
+
+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
+from rest_framework import viewsets
+from rest_framework import serializers
+
+from taiga.base.decorators import list_route
+from taiga.base import exceptions as exc
+from taiga.users.services import get_and_validate_user
+from taiga.domains.services import is_public_register_enabled_for_domain
+
+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 _public_register(self, request):
+ if not is_public_register_enabled_for_domain(request.domain):
+ raise exc.BadRequest(_("Public register is disabled for this domain."))
+
+ 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 = make_auth_response_data(request.domain, user)
+ return Response(data, status=status.HTTP_201_CREATED)
+
+ def _private_register(self, request):
+ 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:
+ data = parse_private_register_for_new_user_data(request.DATA)
+ user = private_register_for_new_user(request.domain, **data)
+
+ 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):
+ type = request.DATA.get("type", None)
+ if type == "public":
+ return self._public_register(request)
+ elif type == "private":
+ return self._private_register(request)
+ raise exc.BadRequest(_("invalid register type"))
+
+ # Login view: /api/v1/auth
+ def create(self, request, **kwargs):
+ username = request.DATA.get('username', None)
+ password = request.DATA.get('password', None)
+
+ 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/base/auth/__init__.py b/taiga/auth/backends.py
similarity index 52%
rename from taiga/base/auth/__init__.py
rename to taiga/auth/backends.py
index 5afd1ce3..ede6374d 100644
--- a/taiga/base/auth/__init__.py
+++ b/taiga/auth/backends.py
@@ -1,4 +1,20 @@
-# -*- coding: utf-8 -*-
+"""
+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
@@ -6,22 +22,21 @@ 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
+from taiga.base import exceptions as exc
class Session(BaseAuthentication):
"""
- Same as rest_framework.authentication.SessionAuthentication
- but without csrf.
+ 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):
- """
- 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)
@@ -32,11 +47,23 @@ class Session(BaseAuthentication):
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:
@@ -54,7 +81,10 @@ def get_user_for_token(token):
class Token(BaseAuthentication):
"""
- Stateless authentication system partially based on oauth.
+ 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 (.+)$")
diff --git a/taiga/base/auth/serializers.py b/taiga/auth/serializers.py
similarity index 66%
rename from taiga/base/auth/serializers.py
rename to taiga/auth/serializers.py
index 512873d8..bc020a43 100644
--- a/taiga/base/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/base/auth/tests/tests_auth.py b/taiga/auth/tests.py
similarity index 51%
rename from taiga/base/auth/tests/tests_auth.py
rename to taiga/auth/tests.py
index 4adb3f6b..de0b709b 100644
--- a/taiga/base/auth/tests/tests_auth.py
+++ b/taiga/auth/tests.py
@@ -4,26 +4,30 @@ 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 auth
-from taiga.base.users.tests import create_user, create_domain
-from taiga.projects.tests import create_project
+from taiga.base import exceptions as exc
+from taiga.users.tests import create_user, create_domain
+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.domains.models import Domain, DomainMember
-from taiga.projects.models import Membership
+from taiga.projects.tests import create_project
+from taiga.projects.tests import add_membership
class TestAuthView(viewsets.ViewSet):
- authentication_classes = (auth.Token,)
+ authentication_classes = (TokenAuthBackend,)
permission_classes = (IsAuthenticated,)
def get(self, request, *args, **kwargs):
@@ -35,8 +39,119 @@ 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):
self.user1 = create_user(1)
@@ -45,14 +160,14 @@ 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"')
-class RegisterTests(test.TestCase):
+class RegisterApiTests(test.TestCase):
fixtures = ["initial_domains.json",]
def setUp(self):
@@ -79,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",
@@ -159,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/base/auth/api.py b/taiga/base/auth/api.py
deleted file mode 100644
index b543346b..00000000
--- a/taiga/base/auth/api.py
+++ /dev/null
@@ -1,160 +0,0 @@
-# -*- coding: utf-8 -*-
-
-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 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 .serializers import (PublicRegisterSerializer,
- PrivateRegisterSerializer,
- PrivateGenericRegisterSerializer,
- PrivateRegisterExistingSerializer)
-
-
-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:
- 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)
-
- 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()
-
- 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")})
-
- else:
- serializer = PrivateRegisterSerializer(data=request.DATA)
- if not serializer.is_valid():
- raise exc.BadRequest(serializer.errors)
-
- 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)
-
- @list_route(methods=["POST"], permission_classes=[AllowAny])
- def register(self, request, **kwargs):
- type = request.DATA.get("type", None)
- if type == "public":
- return self._public_register(request)
- elif type == "private":
- return self._private_register(request)
-
- raise exc.BadRequest(_("invalid register type"))
-
- # Login view: /api/v1/auth
- def create(self, request, **kwargs):
- 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)
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/base/users/migrations/__init__.py b/taiga/base/users/migrations/__init__.py
deleted file mode 100644
index e69de29b..00000000
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..58d52b93 100644
--- a/taiga/projects/api.py
+++ b/taiga/projects/api.py
@@ -32,8 +32,8 @@ 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 . 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/base/auth/tests/__init__.py b/taiga/users/__init__.py
similarity index 100%
rename from taiga/base/auth/tests/__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/__init__.py b/taiga/users/migrations/__init__.py
similarity index 100%
rename from taiga/base/users/__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/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
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