Pārlūkot izejas kodu

Merge pull request #58 from tracim/fix/better_user_lib_for_authentication

Damien Accorsi 6 gadus atpakaļ
vecāks
revīzija
734ed10548
Revīzijas autora e-pasts nav piesaistīts nevienam kontam

+ 8 - 1
tracim/exceptions.py Parādīt failu

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

+ 81 - 18
tracim/lib/core/user.py Parādīt failu

4
 import transaction
4
 import transaction
5
 import typing as typing
5
 import typing as typing
6
 
6
 
7
+from sqlalchemy.orm import Session
8
+
9
+from tracim import CFG
7
 from tracim.models.auth import User
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
 class UserApi(object):
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
         self._session = session
25
         self._session = session
14
         self._user = current_user
26
         self._user = current_user
15
         self._config = config
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
     def _base_query(self):
29
     def _base_query(self):
21
         return self._session.query(User)
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
     def get_one_by_id(self, id: int) -> User:
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
     def update(
103
     def update(
33
             self,
104
             self,
36
             email: str=None,
107
             email: str=None,
37
             do_save=True,
108
             do_save=True,
38
             timezone: str='',
109
             timezone: str='',
39
-    ):
110
+    ) -> None:
40
         if name is not None:
111
         if name is not None:
41
             user.display_name = name
112
             user.display_name = name
42
 
113
 
48
         if do_save:
119
         if do_save:
49
             self.save(user)
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
     def create_user(self, email=None, groups=[], save_now=False) -> User:
122
     def create_user(self, email=None, groups=[], save_now=False) -> User:
60
         user = User()
123
         user = User()
61
 
124
 

+ 10 - 0
tracim/models/context_models.py Parādīt failu

8
 from tracim.models.auth import Profile
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
 class UserInContext(object):
21
 class UserInContext(object):
12
     """
22
     """
13
     Interface to get User data and User data related to context.
23
     Interface to get User data and User data related to context.

+ 89 - 15
tracim/tests/library/test_user_api.py Parādīt failu

4
 
4
 
5
 import transaction
5
 import transaction
6
 
6
 
7
+from tracim.exceptions import UserNotExist, AuthenticationFailed
7
 from tracim.lib.core.user import UserApi
8
 from tracim.lib.core.user import UserApi
9
+from tracim.models import User
10
+from tracim.models.context_models import UserInContext
8
 from tracim.tests import DefaultTest
11
 from tracim.tests import DefaultTest
9
 from tracim.tests import eq_
12
 from tracim.tests import eq_
10
 
13
 
11
 
14
 
12
 class TestUserApi(DefaultTest):
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
         api = UserApi(
18
         api = UserApi(
16
             current_user=None,
19
             current_user=None,
17
             session=self.session,
20
             session=self.session,
25
         eq_('bob@bob', nu.email)
28
         eq_('bob@bob', nu.email)
26
         eq_('bob', nu.display_name)
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
         api = UserApi(
32
         api = UserApi(
30
             current_user=None,
33
             current_user=None,
31
             session=self.session,
34
             session=self.session,
38
         eq_(True, api.user_with_email_exists('bibi@bibi'))
41
         eq_(True, api.user_with_email_exists('bibi@bibi'))
39
         eq_(False, api.user_with_email_exists('unknown'))
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
         api = UserApi(
45
         api = UserApi(
43
             current_user=None,
46
             current_user=None,
44
             session=self.session,
47
             session=self.session,
51
 
54
 
52
         eq_(uid, api.get_one_by_email('bibi@bibi').user_id)
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
         api = UserApi(
58
         api = UserApi(
56
             current_user=None,
59
             current_user=None,
57
             session=self.session,
60
             session=self.session,
60
         with pytest.raises(NoResultFound):
63
         with pytest.raises(NoResultFound):
61
             api.get_one_by_email('unknown')
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
         api = UserApi(
79
         api = UserApi(
66
             current_user=None,
80
             current_user=None,
67
             session=self.session,
81
             session=self.session,
68
             config=self.config,
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
         api = UserApi(
125
         api = UserApi(
78
             current_user=None,
126
             current_user=None,
79
             session=self.session,
127
             session=self.session,
80
             config=self.config,
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 Parādīt failu

1
 # coding=utf-8
1
 # coding=utf-8
2
 import marshmallow
2
 import marshmallow
3
+from marshmallow import post_load
4
+
5
+from tracim.models.context_models import LoginCredentials, UserInContext
3
 
6
 
4
 
7
 
5
 class ProfileSchema(marshmallow.Schema):
8
 class ProfileSchema(marshmallow.Schema):
8
 
11
 
9
 
12
 
10
 class UserSchema(marshmallow.Schema):
13
 class UserSchema(marshmallow.Schema):
14
+
11
     user_id = marshmallow.fields.Int(dump_only=True)
15
     user_id = marshmallow.fields.Int(dump_only=True)
12
     email = marshmallow.fields.Email(required=True)
16
     email = marshmallow.fields.Email(required=True)
13
     display_name = marshmallow.fields.String()
17
     display_name = marshmallow.fields.String()
29
 
33
 
30
 
34
 
31
 class BasicAuthSchema(marshmallow.Schema):
35
 class BasicAuthSchema(marshmallow.Schema):
36
+
32
     email = marshmallow.fields.Email(required=True)
37
     email = marshmallow.fields.Email(required=True)
33
     password = marshmallow.fields.String(required=True, load_only=True)
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
 class LoginOutputHeaders(marshmallow.Schema):
45
 class LoginOutputHeaders(marshmallow.Schema):
37
     expire_after = marshmallow.fields.String()
46
     expire_after = marshmallow.fields.String()

+ 19 - 40
tracim/views/core_api/session_controller.py Parādīt failu

1
 # coding=utf-8
1
 # coding=utf-8
2
 from pyramid.config import Configurator
2
 from pyramid.config import Configurator
3
-from sqlalchemy.orm.exc import NoResultFound
4
 try:  # Python 3.5+
3
 try:  # Python 3.5+
5
     from http import HTTPStatus
4
     from http import HTTPStatus
6
 except ImportError:
5
 except ImportError:
7
     from http import client as HTTPStatus
6
     from http import client as HTTPStatus
8
 
7
 
9
-
10
 from tracim import TracimRequest
8
 from tracim import TracimRequest
11
 from tracim.extensions import hapic
9
 from tracim.extensions import hapic
12
 from tracim.lib.core.user import UserApi
10
 from tracim.lib.core.user import UserApi
13
-from tracim.models.context_models import UserInContext
14
 from tracim.views.controllers import Controller
11
 from tracim.views.controllers import Controller
15
 from tracim.views.core_api.schemas import UserSchema
12
 from tracim.views.core_api.schemas import UserSchema
16
 from tracim.views.core_api.schemas import NoContentSchema
13
 from tracim.views.core_api.schemas import NoContentSchema
17
 from tracim.views.core_api.schemas import LoginOutputHeaders
14
 from tracim.views.core_api.schemas import LoginOutputHeaders
18
 from tracim.views.core_api.schemas import BasicAuthSchema
15
 from tracim.views.core_api.schemas import BasicAuthSchema
19
 from tracim.exceptions import NotAuthentificated
16
 from tracim.exceptions import NotAuthentificated
20
-from tracim.exceptions import LoginFailed
17
+from tracim.exceptions import AuthenticationFailed
21
 
18
 
22
 
19
 
23
 class SessionController(Controller):
20
 class SessionController(Controller):
25
     @hapic.with_api_doc()
22
     @hapic.with_api_doc()
26
     @hapic.input_headers(LoginOutputHeaders())
23
     @hapic.input_headers(LoginOutputHeaders())
27
     @hapic.input_body(BasicAuthSchema())
24
     @hapic.input_body(BasicAuthSchema())
28
-    @hapic.handle_exception(LoginFailed, http_code=HTTPStatus.BAD_REQUEST)
25
+    @hapic.handle_exception(AuthenticationFailed, HTTPStatus.BAD_REQUEST)
29
     # TODO - G.M - 17-04-2018 - fix output header ?
26
     # TODO - G.M - 17-04-2018 - fix output header ?
30
     # @hapic.output_headers()
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
     def login(self, context, request: TracimRequest, hapic_data=None):
29
     def login(self, context, request: TracimRequest, hapic_data=None):
36
         """
30
         """
37
         Logs user into the system
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
         app_config = request.registry.settings['CFG']
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
     @hapic.with_api_doc()
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
     def logout(self, context, request: TracimRequest, hapic_data=None):
45
     def logout(self, context, request: TracimRequest, hapic_data=None):
64
         """
46
         """
65
         Logs out current logged in user session
47
         Logs out current logged in user session
68
         return
50
         return
69
 
51
 
70
     @hapic.with_api_doc()
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
     def whoami(self, context, request: TracimRequest, hapic_data=None):
55
     def whoami(self, context, request: TracimRequest, hapic_data=None):
79
         """
56
         """
80
         Return current logged in user or 401
57
         Return current logged in user or 401
81
         """
58
         """
82
         app_config = request.registry.settings['CFG']
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
             config=app_config,
63
             config=app_config,
87
         )
64
         )
65
+        user = uapi.get_current_user()  # User
66
+        return uapi.get_user_with_context(user)
88
 
67
 
89
     def bind(self, configurator: Configurator):
68
     def bind(self, configurator: Configurator):
90
 
69