Parcourir la source

merge with upstream

Guénaël Muller il y a 7 ans
Parent
révision
55feb2a3dd

+ 0 - 11
tracim/command/__init__.py Voir le fichier

75
     #     transaction.commit()
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
 class Extender(argparse.Action):
78
 class Extender(argparse.Action):
90
     """
79
     """
91
     Copied class from http://stackoverflow.com/a/12461237/801924
80
     Copied class from http://stackoverflow.com/a/12461237/801924

+ 9 - 1
tracim/exceptions.py Voir le fichier

85
     pass
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
     pass
97
     pass
90
 
98
 
91
 
99
 

+ 81 - 18
tracim/lib/core/user.py Voir le fichier

7
 
7
 
8
 from tracim.exceptions import NotificationNotSend
8
 from tracim.exceptions import NotificationNotSend
9
 from tracim.lib.mail_notifier.notifier import get_email_manager
9
 from tracim.lib.mail_notifier.notifier import get_email_manager
10
+from sqlalchemy.orm import Session
11
+
12
+from tracim import CFG
10
 from tracim.models.auth import User
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
 class UserApi(object):
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
         self._session = session
28
         self._session = session
17
         self._user = current_user
29
         self._user = current_user
18
         self._config = config
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
     def _base_query(self):
32
     def _base_query(self):
24
         return self._session.query(User)
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
     def get_one_by_id(self, id: int) -> User:
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
     def update(
106
     def update(
36
             self,
107
             self,
40
             password: str=None,
111
             password: str=None,
41
             timezone: str='',
112
             timezone: str='',
42
             do_save=True,
113
             do_save=True,
43
-    ):
114
+    ) -> None:
44
         if name is not None:
115
         if name is not None:
45
             user.display_name = name
116
             user.display_name = name
46
 
117
 
55
         if do_save:
126
         if do_save:
56
             self.save(user)
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
     def create_user(
129
     def create_user(
67
         self,
130
         self,
68
         email,
131
         email,

+ 10 - 0
tracim/models/context_models.py Voir le fichier

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.

+ 77 - 2
tracim/tests/library/test_user_api.py Voir le fichier

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
 
68
         eq_(True, api.user_with_email_exists('bibi@bibi'))
71
         eq_(True, api.user_with_email_exists('bibi@bibi'))
69
         eq_(False, api.user_with_email_exists('unknown'))
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
         api = UserApi(
75
         api = UserApi(
73
             current_user=None,
76
             current_user=None,
74
             session=self.session,
77
             session=self.session,
82
 
85
 
83
         eq_(uid, api.get_one_by_email('bibi@bibi').user_id)
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
         api = UserApi(
89
         api = UserApi(
87
             current_user=None,
90
             current_user=None,
88
             session=self.session,
91
             session=self.session,
113
         api.update(u, 'titi', 'titi@titi', 'pass', do_save=True)
116
         api.update(u, 'titi', 'titi@titi', 'pass', do_save=True)
114
         one = api.get_one(u.user_id)
117
         one = api.get_one(u.user_id)
115
         eq_(u.user_id, one.user_id)
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 Voir le fichier


+ 0 - 67
tracim/tests/views/test_example.py Voir le fichier

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 Voir le fichier

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 - 48
tracim/views/core_api/session_controller.py Voir le fichier

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