Browse Source

merge with upstream

Guénaël Muller 6 years ago
parent
commit
55feb2a3dd

+ 0 - 11
tracim/command/__init__.py View File

@@ -75,17 +75,6 @@ class AppContextCommand(Command):
75 75
     #     transaction.commit()
76 76
 
77 77
 
78
-# TODO - G.M - 10-04-2018 - [Cleanup][tempExample] - Drop this
79
-class TestTracimCommand(AppContextCommand):
80
-
81
-    def take_app_action(
82
-            self,
83
-            parser: argparse.ArgumentParser,
84
-            app_context: AppEnvironment
85
-    ) -> None:
86
-        print('test')
87
-
88
-
89 78
 class Extender(argparse.Action):
90 79
     """
91 80
     Copied class from http://stackoverflow.com/a/12461237/801924

+ 9 - 1
tracim/exceptions.py View File

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

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

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

+ 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.

+ 77 - 2
tracim/tests/library/test_user_api.py View File

@@ -4,7 +4,10 @@ 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
 
@@ -68,7 +71,7 @@ class TestUserApi(DefaultTest):
68 71
         eq_(True, api.user_with_email_exists('bibi@bibi'))
69 72
         eq_(False, api.user_with_email_exists('unknown'))
70 73
 
71
-    def test_unit__get_one_by_email__ok__nominal_case(self):
74
+    def test_get_one_by_email(self):
72 75
         api = UserApi(
73 76
             current_user=None,
74 77
             session=self.session,
@@ -82,7 +85,7 @@ class TestUserApi(DefaultTest):
82 85
 
83 86
         eq_(uid, api.get_one_by_email('bibi@bibi').user_id)
84 87
 
85
-    def test_unit__get_one_by_email__ok__user_not_found(self):
88
+    def test_unit__get_one_by_email__err__user_does_not_exist(self):
86 89
         api = UserApi(
87 90
             current_user=None,
88 91
             session=self.session,
@@ -113,3 +116,75 @@ class TestUserApi(DefaultTest):
113 116
         api.update(u, 'titi', 'titi@titi', 'pass', do_save=True)
114 117
         one = api.get_one(u.user_id)
115 118
         eq_(u.user_id, one.user_id)
119
+
120
+    def test_unit__get_user_with_context__nominal_case(self):
121
+        user = User(
122
+            email='admin@tracim.tracim',
123
+            display_name='Admin',
124
+            is_active=True,
125
+        )
126
+        api = UserApi(
127
+            current_user=None,
128
+            session=self.session,
129
+            config=self.config,
130
+        )
131
+        new_user = api.get_user_with_context(user)
132
+        assert isinstance(new_user, UserInContext)
133
+        assert new_user.user == user
134
+        assert new_user.profile.name == 'nobody'
135
+        assert new_user.user_id == user.user_id
136
+        assert new_user.email == 'admin@tracim.tracim'
137
+        assert new_user.display_name == 'Admin'
138
+        assert new_user.is_active is True
139
+        # TODO - G.M - 03-05-2018 - [avatar][calendar] Should test this
140
+        # with true value when those param will be available.
141
+        assert new_user.avatar_url is None
142
+        assert new_user.calendar_url is None
143
+
144
+    def test_unit__get_current_user_ok__nominal_case(self):
145
+        user = User(email='admin@tracim.tracim')
146
+        api = UserApi(
147
+            current_user=user,
148
+            session=self.session,
149
+            config=self.config,
150
+        )
151
+        new_user = api.get_current_user()
152
+        assert isinstance(new_user, User)
153
+        assert user == new_user
154
+
155
+    def test_unit__get_current_user__err__user_not_exist(self):
156
+        api = UserApi(
157
+            current_user=None,
158
+            session=self.session,
159
+            config=self.config,
160
+        )
161
+        with pytest.raises(UserNotExist):
162
+            api.get_current_user()
163
+
164
+    def test_unit__authenticate_user___ok__nominal_case(self):
165
+        api = UserApi(
166
+            current_user=None,
167
+            session=self.session,
168
+            config=self.config,
169
+        )
170
+        user = api.authenticate_user('admin@admin.admin', 'admin@admin.admin')
171
+        assert isinstance(user, User)
172
+        assert user.email == 'admin@admin.admin'
173
+
174
+    def test_unit__authenticate_user___err__wrong_password(self):
175
+        api = UserApi(
176
+            current_user=None,
177
+            session=self.session,
178
+            config=self.config,
179
+        )
180
+        with pytest.raises(AuthenticationFailed):
181
+            api.authenticate_user('admin@admin.admin', 'wrong_password')
182
+
183
+    def test_unit__authenticate_user___err__wrong_user(self):
184
+        api = UserApi(
185
+            current_user=None,
186
+            session=self.session,
187
+            config=self.config,
188
+        )
189
+        with pytest.raises(AuthenticationFailed):
190
+            api.authenticate_user('unknown_user', 'wrong_password')

+ 0 - 0
tracim/tests/views/__init__.py View File


+ 0 - 67
tracim/tests/views/test_example.py View File

@@ -1,67 +0,0 @@
1
-# -*- coding: utf-8 -*-
2
-# TODO - G.M - [Cleanup][tempExample] Drop this file
3
-# import unittest
4
-# import transaction
5
-#
6
-# from pyramid import testing
7
-#
8
-#
9
-# def dummy_request(dbsession):
10
-#     return testing.DummyRequest(dbsession=dbsession)
11
-#
12
-#
13
-# class BaseTest(unittest.TestCase):
14
-#     def setUp(self):
15
-#         self.config = testing.setUp(settings={
16
-#             'sqlalchemy.url': 'sqlite:///:memory:'
17
-#         })
18
-#         self.config.include('tracim.models')
19
-#         settings = self.config.get_settings()
20
-#
21
-#         from tracim.models import (
22
-#             get_engine,
23
-#             get_session_factory,
24
-#             get_tm_session,
25
-#             )
26
-#
27
-#         self.engine = get_engine(settings)
28
-#         session_factory = get_session_factory(self.engine)
29
-#
30
-#         self.session = get_tm_session(session_factory, transaction.manager)
31
-#
32
-#     def init_database(self):
33
-#         from tracim.models.meta import DeclarativeBase
34
-#         DeclarativeBase.metadata.create_all(self.engine)
35
-#
36
-#     def tearDown(self):
37
-#         from tracim.models.meta import DeclarativeBase
38
-#
39
-#         testing.tearDown()
40
-#         transaction.abort()
41
-#         DeclarativeBase.metadata.drop_all(self.engine)
42
-#
43
-#
44
-# class TestMyViewSuccessCondition(BaseTest):
45
-#
46
-#     def setUp(self):
47
-#         super(TestMyViewSuccessCondition, self).setUp()
48
-#         self.init_database()
49
-#
50
-#         from tracim.models import MyModel
51
-#
52
-#         model = MyModel(name='one', value=55)
53
-#         self.session.add(model)
54
-#
55
-#     def test_passing_view(self):
56
-#         from tracim.views.default import my_view
57
-#         info = my_view(dummy_request(self.session))
58
-#         self.assertEqual(info['one'].name, 'one')
59
-#         self.assertEqual(info['project'], 'tracim')
60
-#
61
-#
62
-# class TestMyViewFailureCondition(BaseTest):
63
-#
64
-#     def test_failing_view(self):
65
-#         from tracim.views.default import my_view
66
-#         info = my_view(dummy_request(self.session))
67
-#         self.assertEqual(info.status_int, 500)

+ 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 - 48
tracim/views/core_api/session_controller.py View File

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