Browse Source

Merge pull request #58 from tracim/fix/better_user_lib_for_authentication

Damien Accorsi 6 years ago
parent
commit
734ed10548
No account linked to committer's email

+ 8 - 1
tracim/exceptions.py View File

@@ -85,5 +85,12 @@ class DigestAuthNotImplemented(Exception):
85 85
     pass
86 86
 
87 87
 
88
-class LoginFailed(TracimException):
88
+class AuthenticationFailed(TracimException):
89 89
     pass
90
+
91
+
92
+class WrongUserPassword(TracimException):
93
+    pass
94
+
95
+class UserNotExist(TracimException):
96
+    pass

+ 81 - 18
tracim/lib/core/user.py View File

@@ -4,30 +4,101 @@ import threading
4 4
 import transaction
5 5
 import typing as typing
6 6
 
7
+from sqlalchemy.orm import Session
8
+
9
+from tracim import CFG
7 10
 from tracim.models.auth import User
11
+from sqlalchemy.orm.exc import NoResultFound
12
+from tracim.exceptions import WrongUserPassword, UserNotExist
13
+from tracim.exceptions import AuthenticationFailed
14
+from tracim.models.context_models import UserInContext
8 15
 
9 16
 
10 17
 class UserApi(object):
11 18
 
12
-    def __init__(self, current_user: typing.Optional[User], session, config):
19
+    def __init__(
20
+            self,
21
+            current_user: typing.Optional[User],
22
+            session: Session,
23
+            config: CFG,
24
+    ) -> None:
13 25
         self._session = session
14 26
         self._user = current_user
15 27
         self._config = config
16 28
 
17
-    def get_all(self):
18
-        return self._session.query(User).order_by(User.display_name).all()
19
-
20 29
     def _base_query(self):
21 30
         return self._session.query(User)
22 31
 
23
-    def get_one(self, user_id: int):
24
-        return self._base_query().filter(User.user_id==user_id).one()
32
+    def get_user_with_context(self, user: User) -> UserInContext:
33
+        """
34
+        Return UserInContext object from User
35
+        """
36
+        user = UserInContext(
37
+            user=user,
38
+            dbsession=self._session,
39
+            config=self._config,
40
+        )
41
+        return user
42
+
43
+    # Getters
44
+
45
+    def get_one(self, user_id: int) -> User:
46
+        """
47
+        Get one user by user id
48
+        """
49
+        return self._base_query().filter(User.user_id == user_id).one()
25 50
 
26
-    def get_one_by_email(self, email: str):
27
-        return self._base_query().filter(User.email==email).one()
51
+    def get_one_by_email(self, email: str) -> User:
52
+        """
53
+        Get one user by email
54
+        :param email: Email of the user
55
+        :return: one user
56
+        """
57
+        return self._base_query().filter(User.email == email).one()
28 58
 
59
+    # FIXME - G.M - 24-04-2018 - Duplicate method with get_one.
29 60
     def get_one_by_id(self, id: int) -> User:
30
-        return self._base_query().filter(User.user_id==id).one()
61
+        return self.get_one(user_id=id)
62
+
63
+    def get_current_user(self) -> User:
64
+        """
65
+        Get current_user
66
+        """
67
+        if not self._user:
68
+            raise UserNotExist()
69
+        return self._user
70
+
71
+    def get_all(self) -> typing.Iterable[User]:
72
+        return self._session.query(User).order_by(User.display_name).all()
73
+
74
+    # Check methods
75
+
76
+    def user_with_email_exists(self, email: str) -> bool:
77
+        try:
78
+            self.get_one_by_email(email)
79
+            return True
80
+        # TODO - G.M - 09-04-2018 - Better exception
81
+        except:
82
+            return False
83
+
84
+    def authenticate_user(self, email: str, password: str) -> User:
85
+        """
86
+        Authenticate user with email and password, raise AuthenticationFailed
87
+        if uncorrect.
88
+        :param email: email of the user
89
+        :param password: cleartext password of the user
90
+        :return: User who was authenticated.
91
+        """
92
+        try:
93
+            user = self.get_one_by_email(email)
94
+            if user.validate_password(password):
95
+                return user
96
+            else:
97
+                raise WrongUserPassword()
98
+        except (WrongUserPassword, NoResultFound):
99
+            raise AuthenticationFailed()
100
+
101
+    # Actions
31 102
 
32 103
     def update(
33 104
             self,
@@ -36,7 +107,7 @@ class UserApi(object):
36 107
             email: str=None,
37 108
             do_save=True,
38 109
             timezone: str='',
39
-    ):
110
+    ) -> None:
40 111
         if name is not None:
41 112
             user.display_name = name
42 113
 
@@ -48,14 +119,6 @@ class UserApi(object):
48 119
         if do_save:
49 120
             self.save(user)
50 121
 
51
-    def user_with_email_exists(self, email: str):
52
-        try:
53
-            self.get_one_by_email(email)
54
-            return True
55
-        # TODO - G.M - 09-04-2018 - Better exception
56
-        except:
57
-            return False
58
-
59 122
     def create_user(self, email=None, groups=[], save_now=False) -> User:
60 123
         user = User()
61 124
 

+ 10 - 0
tracim/models/context_models.py View File

@@ -8,6 +8,16 @@ from tracim.models import User
8 8
 from tracim.models.auth import Profile
9 9
 
10 10
 
11
+class LoginCredentials(object):
12
+    """
13
+    Login credentials model for login
14
+    """
15
+
16
+    def __init__(self, email: str, password: str):
17
+        self.email = email
18
+        self.password = password
19
+
20
+
11 21
 class UserInContext(object):
12 22
     """
13 23
     Interface to get User data and User data related to context.

+ 89 - 15
tracim/tests/library/test_user_api.py View File

@@ -4,14 +4,17 @@ from sqlalchemy.orm.exc import NoResultFound
4 4
 
5 5
 import transaction
6 6
 
7
+from tracim.exceptions import UserNotExist, AuthenticationFailed
7 8
 from tracim.lib.core.user import UserApi
9
+from tracim.models import User
10
+from tracim.models.context_models import UserInContext
8 11
 from tracim.tests import DefaultTest
9 12
 from tracim.tests import eq_
10 13
 
11 14
 
12 15
 class TestUserApi(DefaultTest):
13 16
 
14
-    def test_create_and_update_user(self):
17
+    def test_unit__create_and_update_user__ok__nominal_case(self):
15 18
         api = UserApi(
16 19
             current_user=None,
17 20
             session=self.session,
@@ -25,7 +28,7 @@ class TestUserApi(DefaultTest):
25 28
         eq_('bob@bob', nu.email)
26 29
         eq_('bob', nu.display_name)
27 30
 
28
-    def test_user_with_email_exists(self):
31
+    def test_unit__user_with_email_exists__ok__nominal_case(self):
29 32
         api = UserApi(
30 33
             current_user=None,
31 34
             session=self.session,
@@ -38,7 +41,7 @@ class TestUserApi(DefaultTest):
38 41
         eq_(True, api.user_with_email_exists('bibi@bibi'))
39 42
         eq_(False, api.user_with_email_exists('unknown'))
40 43
 
41
-    def test_get_one_by_email(self):
44
+    def test_unit__get_one_by_email__ok__nominal_case(self):
42 45
         api = UserApi(
43 46
             current_user=None,
44 47
             session=self.session,
@@ -51,7 +54,7 @@ class TestUserApi(DefaultTest):
51 54
 
52 55
         eq_(uid, api.get_one_by_email('bibi@bibi').user_id)
53 56
 
54
-    def test_get_one_by_email_exception(self):
57
+    def test_unit__get_one_by_email__err__user_does_not_exist(self):
55 58
         api = UserApi(
56 59
             current_user=None,
57 60
             session=self.session,
@@ -60,26 +63,97 @@ class TestUserApi(DefaultTest):
60 63
         with pytest.raises(NoResultFound):
61 64
             api.get_one_by_email('unknown')
62 65
 
63
-    def test_get_all(self):
64
-        # TODO - G.M - 29-03-2018 Check why this method is not enabled
66
+    # def test_unit__get_all__ok__nominal_case(self):
67
+    #     # TODO - G.M - 29-03-2018 Check why this method is not enabled
68
+    #     api = UserApi(
69
+    #         current_user=None,
70
+    #         session=self.session,
71
+    #         config=self.config,
72
+    #     )
73
+    #     u1 = api.create_user(True)
74
+    #     u2 = api.create_user(True)
75
+    #     users = api.get_all()
76
+    #     assert 2==len(users)
77
+
78
+    def test_unit__get_one__ok__nominal_case(self):
65 79
         api = UserApi(
66 80
             current_user=None,
67 81
             session=self.session,
68 82
             config=self.config,
69 83
         )
70
-        # u1 = api.create_user(True)
71
-        # u2 = api.create_user(True)
84
+        u = api.create_user()
85
+        api.update(u, 'titi', 'titi@titi', True)
86
+        one = api.get_one(u.user_id)
87
+        eq_(u.user_id, one.user_id)
72 88
 
73
-        # users = api.get_all()
74
-        # ok_(2==len(users))
89
+    def test_unit__get_user_with_context__nominal_case(self):
90
+        user = User(
91
+            email='admin@tracim.tracim',
92
+            display_name='Admin',
93
+            is_active=True,
94
+        )
95
+        api = UserApi(
96
+            current_user=None,
97
+            session=self.session,
98
+            config=self.config,
99
+        )
100
+        new_user = api.get_user_with_context(user)
101
+        assert isinstance(new_user, UserInContext)
102
+        assert new_user.user == user
103
+        assert new_user.profile.name == 'nobody'
104
+        assert new_user.user_id == user.user_id
105
+        assert new_user.email == 'admin@tracim.tracim'
106
+        assert new_user.display_name == 'Admin'
107
+        assert new_user.is_active is True
108
+        # TODO - G.M - 03-05-2018 - [avatar][calendar] Should test this
109
+        # with true value when those param will be available.
110
+        assert new_user.avatar_url is None
111
+        assert new_user.calendar_url is None
75 112
 
76
-    def test_get_one(self):
113
+    def test_unit__get_current_user_ok__nominal_case(self):
114
+        user = User(email='admin@tracim.tracim')
115
+        api = UserApi(
116
+            current_user=user,
117
+            session=self.session,
118
+            config=self.config,
119
+        )
120
+        new_user = api.get_current_user()
121
+        assert isinstance(new_user, User)
122
+        assert user == new_user
123
+
124
+    def test_unit__get_current_user__err__user_not_exist(self):
77 125
         api = UserApi(
78 126
             current_user=None,
79 127
             session=self.session,
80 128
             config=self.config,
81 129
         )
82
-        u = api.create_user()
83
-        api.update(u, 'titi', 'titi@titi', True)
84
-        one = api.get_one(u.user_id)
85
-        eq_(u.user_id, one.user_id)
130
+        with pytest.raises(UserNotExist):
131
+            api.get_current_user()
132
+
133
+    def test_unit__authenticate_user___ok__nominal_case(self):
134
+        api = UserApi(
135
+            current_user=None,
136
+            session=self.session,
137
+            config=self.config,
138
+        )
139
+        user = api.authenticate_user('admin@admin.admin', 'admin@admin.admin')
140
+        assert isinstance(user, User)
141
+        assert user.email == 'admin@admin.admin'
142
+
143
+    def test_unit__authenticate_user___err__wrong_password(self):
144
+        api = UserApi(
145
+            current_user=None,
146
+            session=self.session,
147
+            config=self.config,
148
+        )
149
+        with pytest.raises(AuthenticationFailed):
150
+            api.authenticate_user('admin@admin.admin', 'wrong_password')
151
+
152
+    def test_unit__authenticate_user___err__wrong_user(self):
153
+        api = UserApi(
154
+            current_user=None,
155
+            session=self.session,
156
+            config=self.config,
157
+        )
158
+        with pytest.raises(AuthenticationFailed):
159
+            api.authenticate_user('unknown_user', 'wrong_password')

+ 9 - 0
tracim/views/core_api/schemas.py View File

@@ -1,5 +1,8 @@
1 1
 # coding=utf-8
2 2
 import marshmallow
3
+from marshmallow import post_load
4
+
5
+from tracim.models.context_models import LoginCredentials, UserInContext
3 6
 
4 7
 
5 8
 class ProfileSchema(marshmallow.Schema):
@@ -8,6 +11,7 @@ class ProfileSchema(marshmallow.Schema):
8 11
 
9 12
 
10 13
 class UserSchema(marshmallow.Schema):
14
+
11 15
     user_id = marshmallow.fields.Int(dump_only=True)
12 16
     email = marshmallow.fields.Email(required=True)
13 17
     display_name = marshmallow.fields.String()
@@ -29,9 +33,14 @@ class UserSchema(marshmallow.Schema):
29 33
 
30 34
 
31 35
 class BasicAuthSchema(marshmallow.Schema):
36
+
32 37
     email = marshmallow.fields.Email(required=True)
33 38
     password = marshmallow.fields.String(required=True, load_only=True)
34 39
 
40
+    @post_load
41
+    def make_login(self, data):
42
+        return LoginCredentials(**data)
43
+
35 44
 
36 45
 class LoginOutputHeaders(marshmallow.Schema):
37 46
     expire_after = marshmallow.fields.String()

+ 19 - 40
tracim/views/core_api/session_controller.py View File

@@ -1,23 +1,20 @@
1 1
 # coding=utf-8
2 2
 from pyramid.config import Configurator
3
-from sqlalchemy.orm.exc import NoResultFound
4 3
 try:  # Python 3.5+
5 4
     from http import HTTPStatus
6 5
 except ImportError:
7 6
     from http import client as HTTPStatus
8 7
 
9
-
10 8
 from tracim import TracimRequest
11 9
 from tracim.extensions import hapic
12 10
 from tracim.lib.core.user import UserApi
13
-from tracim.models.context_models import UserInContext
14 11
 from tracim.views.controllers import Controller
15 12
 from tracim.views.core_api.schemas import UserSchema
16 13
 from tracim.views.core_api.schemas import NoContentSchema
17 14
 from tracim.views.core_api.schemas import LoginOutputHeaders
18 15
 from tracim.views.core_api.schemas import BasicAuthSchema
19 16
 from tracim.exceptions import NotAuthentificated
20
-from tracim.exceptions import LoginFailed
17
+from tracim.exceptions import AuthenticationFailed
21 18
 
22 19
 
23 20
 class SessionController(Controller):
@@ -25,41 +22,26 @@ class SessionController(Controller):
25 22
     @hapic.with_api_doc()
26 23
     @hapic.input_headers(LoginOutputHeaders())
27 24
     @hapic.input_body(BasicAuthSchema())
28
-    @hapic.handle_exception(LoginFailed, http_code=HTTPStatus.BAD_REQUEST)
25
+    @hapic.handle_exception(AuthenticationFailed, HTTPStatus.BAD_REQUEST)
29 26
     # TODO - G.M - 17-04-2018 - fix output header ?
30 27
     # @hapic.output_headers()
31
-    @hapic.output_body(
32
-        NoContentSchema(),
33
-        default_http_code=HTTPStatus.NO_CONTENT
34
-    )
28
+    @hapic.output_body(NoContentSchema(), default_http_code=HTTPStatus.NO_CONTENT)  # nopep8
35 29
     def login(self, context, request: TracimRequest, hapic_data=None):
36 30
         """
37 31
         Logs user into the system
38 32
         """
39
-        email = request.json_body['email']
40
-        password = request.json_body['password']
33
+
34
+        login = hapic_data.body
41 35
         app_config = request.registry.settings['CFG']
42
-        try:
43
-            uapi = UserApi(
44
-                None,
45
-                session=request.dbsession,
46
-                config=app_config,
47
-            )
48
-            user = uapi.get_one_by_email(email)
49
-            valid_password = user.validate_password(password)
50
-            if not valid_password:
51
-                # Bad password
52
-                raise LoginFailed('Bad Credentials')
53
-        except NoResultFound:
54
-            # User does not exist
55
-            raise LoginFailed('Bad Credentials')
56
-        return
36
+        uapi = UserApi(
37
+            None,
38
+            session=request.dbsession,
39
+            config=app_config,
40
+        )
41
+        return uapi.authenticate_user(login.email, login.password)
57 42
 
58 43
     @hapic.with_api_doc()
59
-    @hapic.output_body(
60
-        NoContentSchema(),
61
-        default_http_code=HTTPStatus.NO_CONTENT
62
-    )
44
+    @hapic.output_body(NoContentSchema(), default_http_code=HTTPStatus.NO_CONTENT)  # nopep8
63 45
     def logout(self, context, request: TracimRequest, hapic_data=None):
64 46
         """
65 47
         Logs out current logged in user session
@@ -68,23 +50,20 @@ class SessionController(Controller):
68 50
         return
69 51
 
70 52
     @hapic.with_api_doc()
71
-    @hapic.handle_exception(
72
-        NotAuthentificated,
73
-        http_code=HTTPStatus.UNAUTHORIZED
74
-    )
75
-    @hapic.output_body(
76
-        UserSchema(),
77
-    )
53
+    @hapic.handle_exception(NotAuthentificated, HTTPStatus.UNAUTHORIZED)
54
+    @hapic.output_body(UserSchema(),)
78 55
     def whoami(self, context, request: TracimRequest, hapic_data=None):
79 56
         """
80 57
         Return current logged in user or 401
81 58
         """
82 59
         app_config = request.registry.settings['CFG']
83
-        return UserInContext(
84
-            user=request.current_user,
85
-            dbsession=request.dbsession,
60
+        uapi = UserApi(
61
+            request.current_user,
62
+            session=request.dbsession,
86 63
             config=app_config,
87 64
         )
65
+        user = uapi.get_current_user()  # User
66
+        return uapi.get_user_with_context(user)
88 67
 
89 68
     def bind(self, configurator: Configurator):
90 69