浏览代码

Merge pull request #103 from tracim/feature/612_user_account_endpoint

inkhey 6 年前
父节点
当前提交
29456158cf
没有帐户链接到提交者的电子邮件

+ 2 - 0
tracim/__init__.py 查看文件

32
 from tracim.views.contents_api.file_controller import FileController
32
 from tracim.views.contents_api.file_controller import FileController
33
 from tracim.views.errors import ErrorSchema
33
 from tracim.views.errors import ErrorSchema
34
 from tracim.exceptions import NotAuthenticated
34
 from tracim.exceptions import NotAuthenticated
35
+from tracim.exceptions import UserNotActive
35
 from tracim.exceptions import InvalidId
36
 from tracim.exceptions import InvalidId
36
 from tracim.exceptions import InsufficientUserProfile
37
 from tracim.exceptions import InsufficientUserProfile
37
 from tracim.exceptions import InsufficientUserRoleInWorkspace
38
 from tracim.exceptions import InsufficientUserRoleInWorkspace
97
     context.handle_exception(InvalidId, HTTPStatus.BAD_REQUEST)
98
     context.handle_exception(InvalidId, HTTPStatus.BAD_REQUEST)
98
     # Auth exception
99
     # Auth exception
99
     context.handle_exception(NotAuthenticated, HTTPStatus.UNAUTHORIZED)
100
     context.handle_exception(NotAuthenticated, HTTPStatus.UNAUTHORIZED)
101
+    context.handle_exception(UserNotActive, HTTPStatus.FORBIDDEN)
100
     context.handle_exception(AuthenticationFailed, HTTPStatus.FORBIDDEN)
102
     context.handle_exception(AuthenticationFailed, HTTPStatus.FORBIDDEN)
101
     context.handle_exception(InsufficientUserRoleInWorkspace, HTTPStatus.FORBIDDEN)  # nopep8
103
     context.handle_exception(InsufficientUserRoleInWorkspace, HTTPStatus.FORBIDDEN)  # nopep8
102
     context.handle_exception(InsufficientUserProfile, HTTPStatus.FORBIDDEN)
104
     context.handle_exception(InsufficientUserProfile, HTTPStatus.FORBIDDEN)

+ 13 - 0
tracim/exceptions.py 查看文件

140
 class InvalidUserId(InvalidId):
140
 class InvalidUserId(InvalidId):
141
     pass
141
     pass
142
 
142
 
143
+
143
 class ContentNotFound(TracimException):
144
 class ContentNotFound(TracimException):
144
     pass
145
     pass
145
 
146
 
152
     pass
153
     pass
153
 
154
 
154
 
155
 
156
+class PasswordDoNotMatch(TracimException):
157
+    pass
158
+
159
+
155
 class EmptyValueNotAllowed(TracimException):
160
 class EmptyValueNotAllowed(TracimException):
156
     pass
161
     pass
157
 
162
 
164
     pass
169
     pass
165
 
170
 
166
 
171
 
172
+class UserNotActive(TracimException):
173
+    pass
174
+
175
+
176
+class NoUserSetted(TracimException):
177
+    pass
178
+
179
+
167
 class RoleDoesNotExist(TracimException):
180
 class RoleDoesNotExist(TracimException):
168
     pass
181
     pass
169
 
182
 

+ 102 - 9
tracim/lib/core/user.py 查看文件

1
 # -*- coding: utf-8 -*-
1
 # -*- coding: utf-8 -*-
2
-import threading
3
 from smtplib import SMTPException
2
 from smtplib import SMTPException
4
 
3
 
5
 import transaction
4
 import transaction
6
 import typing as typing
5
 import typing as typing
7
-
8
-from tracim.exceptions import NotificationNotSend
9
-from tracim.exceptions import EmailValidationFailed
10
-from tracim.lib.mail_notifier.notifier import get_email_manager
11
 from sqlalchemy.orm import Session
6
 from sqlalchemy.orm import Session
7
+from sqlalchemy.orm.exc import NoResultFound
12
 
8
 
13
 from tracim import CFG
9
 from tracim import CFG
14
 from tracim.models.auth import User
10
 from tracim.models.auth import User
15
 from tracim.models.auth import Group
11
 from tracim.models.auth import Group
16
-from sqlalchemy.orm.exc import NoResultFound
12
+from tracim.exceptions import NoUserSetted
13
+from tracim.exceptions import PasswordDoNotMatch
14
+from tracim.exceptions import EmailValidationFailed
17
 from tracim.exceptions import UserDoesNotExist
15
 from tracim.exceptions import UserDoesNotExist
18
 from tracim.exceptions import WrongUserPassword
16
 from tracim.exceptions import WrongUserPassword
19
 from tracim.exceptions import AuthenticationFailed
17
 from tracim.exceptions import AuthenticationFailed
18
+from tracim.exceptions import NotificationNotSend
19
+from tracim.exceptions import UserNotActive
20
 from tracim.models.context_models import UserInContext
20
 from tracim.models.context_models import UserInContext
21
+from tracim.lib.mail_notifier.notifier import get_email_manager
21
 from tracim.models.context_models import TypeUser
22
 from tracim.models.context_models import TypeUser
22
 
23
 
23
 
24
 
150
         """
151
         """
151
         try:
152
         try:
152
             user = self.get_one_by_email(email)
153
             user = self.get_one_by_email(email)
154
+            if not user.is_active:
155
+                raise UserNotActive('User "{}" is not active'.format(email))
153
             if user.validate_password(password):
156
             if user.validate_password(password):
154
                 return user
157
                 return user
155
             else:
158
             else:
158
             raise AuthenticationFailed('User "{}" authentication failed'.format(email)) from exc  # nopep8
161
             raise AuthenticationFailed('User "{}" authentication failed'.format(email)) from exc  # nopep8
159
 
162
 
160
     # Actions
163
     # Actions
164
+    def set_password(
165
+            self,
166
+            user: User,
167
+            loggedin_user_password: str,
168
+            new_password: str,
169
+            new_password2: str,
170
+            do_save: bool=True
171
+    ):
172
+        """
173
+        Set User password if loggedin user password is correct
174
+        and both new_password are the same.
175
+        :param user: User who need password changed
176
+        :param loggedin_user_password: cleartext password of logged user (not
177
+        same as user)
178
+        :param new_password: new password for user
179
+        :param new_password2: should be same as new_password
180
+        :param do_save: should we save new user password ?
181
+        :return:
182
+        """
183
+        if not self._user:
184
+            raise NoUserSetted('Current User should be set in UserApi to use this method')  # nopep8
185
+        if not self._user.validate_password(loggedin_user_password):  # nopep8
186
+            raise WrongUserPassword(
187
+                'Wrong password for authenticated user {}'. format(self._user.user_id)  # nopep8
188
+            )
189
+        if new_password != new_password2:
190
+            raise PasswordDoNotMatch('Passwords given are different')
191
+
192
+        self.update(
193
+            user=user,
194
+            password=new_password,
195
+            do_save=do_save,
196
+        )
197
+        if do_save:
198
+            # TODO - G.M - 2018-07-24 - Check why commit is needed here
199
+            transaction.commit()
200
+        return user
201
+
202
+    def set_email(
203
+            self,
204
+            user: User,
205
+            loggedin_user_password: str,
206
+            email: str,
207
+            do_save: bool = True
208
+    ):
209
+        """
210
+        Set email address of user if loggedin user password is correct
211
+        :param user: User who need email changed
212
+        :param loggedin_user_password: cleartext password of logged user (not
213
+        same as user)
214
+        :param email:
215
+        :param do_save:
216
+        :return:
217
+        """
218
+        if not self._user:
219
+            raise NoUserSetted('Current User should be set in UserApi to use this method')  # nopep8
220
+        if not self._user.validate_password(loggedin_user_password):  # nopep8
221
+            raise WrongUserPassword(
222
+                'Wrong password for authenticated user {}'. format(self._user.user_id)  # nopep8
223
+            )
224
+        self.update(
225
+            user=user,
226
+            email=email,
227
+            do_save=do_save,
228
+        )
229
+        return user
161
 
230
 
162
     def _check_email(self, email: str) -> bool:
231
     def _check_email(self, email: str) -> bool:
163
         # TODO - G.M - 2018-07-05 - find a better way to check email
232
         # TODO - G.M - 2018-07-05 - find a better way to check email
174
             name: str=None,
243
             name: str=None,
175
             email: str=None,
244
             email: str=None,
176
             password: str=None,
245
             password: str=None,
177
-            timezone: str='',
246
+            timezone: str=None,
247
+            groups: typing.Optional[typing.List[Group]]=None,
178
             do_save=True,
248
             do_save=True,
179
-    ) -> None:
249
+    ) -> User:
180
         if name is not None:
250
         if name is not None:
181
             user.display_name = name
251
             user.display_name = name
182
 
252
 
189
         if password is not None:
259
         if password is not None:
190
             user.password = password
260
             user.password = password
191
 
261
 
192
-        user.timezone = timezone
262
+        if timezone is not None:
263
+            user.timezone = timezone
264
+
265
+        if groups is not None:
266
+            # INFO - G.M - 2018-07-18 - Delete old groups
267
+            for group in user.groups:
268
+                if group not in groups:
269
+                    user.groups.remove(group)
270
+            # INFO - G.M - 2018-07-18 - add new groups
271
+            for group in groups:
272
+                if group not in user.groups:
273
+                    user.groups.append(group)
193
 
274
 
194
         if do_save:
275
         if do_save:
195
             self.save(user)
276
             self.save(user)
196
 
277
 
278
+        return user
279
+
197
     def create_user(
280
     def create_user(
198
         self,
281
         self,
199
         email,
282
         email,
251
 
334
 
252
         return user
335
         return user
253
 
336
 
337
+    def enable(self, user: User, do_save=False):
338
+        user.is_active = True
339
+        if do_save:
340
+            self.save(user)
341
+
342
+    def disable(self, user:User, do_save=False):
343
+        user.is_active = False
344
+        if do_save:
345
+            self.save(user)
346
+
254
     def save(self, user: User):
347
     def save(self, user: User):
255
         self._session.flush()
348
         self._session.flush()
256
 
349
 

+ 1 - 0
tracim/lib/utils/authentification.py 查看文件

32
     user = _get_basic_auth_unsafe_user(request)
32
     user = _get_basic_auth_unsafe_user(request)
33
     if not user \
33
     if not user \
34
             or user.email != login \
34
             or user.email != login \
35
+            or not user.is_active \
35
             or not user.validate_password(cleartext_password):
36
             or not user.validate_password(cleartext_password):
36
         return None
37
         return None
37
     return []
38
     return []

+ 3 - 0
tracim/lib/utils/request.py 查看文件

3
 from sqlalchemy.orm.exc import NoResultFound
3
 from sqlalchemy.orm.exc import NoResultFound
4
 
4
 
5
 from tracim.exceptions import NotAuthenticated
5
 from tracim.exceptions import NotAuthenticated
6
+from tracim.exceptions import UserNotActive
6
 from tracim.exceptions import ContentNotFound
7
 from tracim.exceptions import ContentNotFound
7
 from tracim.exceptions import InvalidUserId
8
 from tracim.exceptions import InvalidUserId
8
 from tracim.exceptions import InvalidWorkspaceId
9
 from tracim.exceptions import InvalidWorkspaceId
321
             if not login:
322
             if not login:
322
                 raise UserNotFoundInTracimRequest('You request a current user but the context not permit to found one')  # nopep8
323
                 raise UserNotFoundInTracimRequest('You request a current user but the context not permit to found one')  # nopep8
323
             user = uapi.get_one_by_email(login)
324
             user = uapi.get_one_by_email(login)
325
+            if not user.is_active:
326
+                raise UserNotActive('User {} is not active'.format(login))
324
         except (UserDoesNotExist, UserNotFoundInTracimRequest) as exc:
327
         except (UserDoesNotExist, UserNotFoundInTracimRequest) as exc:
325
             raise NotAuthenticated('User {} not found'.format(login)) from exc
328
             raise NotAuthenticated('User {} not found'.format(login)) from exc
326
         return user
329
         return user

+ 61 - 0
tracim/models/context_models.py 查看文件

49
         self.password = password
49
         self.password = password
50
 
50
 
51
 
51
 
52
+class SetEmail(object):
53
+    """
54
+    Just an email
55
+    """
56
+    def __init__(self, loggedin_user_password: str, email: str) -> None:
57
+        self.loggedin_user_password = loggedin_user_password
58
+        self.email = email
59
+
60
+
61
+class SetPassword(object):
62
+    """
63
+    Just an password
64
+    """
65
+    def __init__(self,
66
+        loggedin_user_password: str,
67
+        new_password: str,
68
+        new_password2: str
69
+    ) -> None:
70
+        self.loggedin_user_password = loggedin_user_password
71
+        self.new_password = new_password
72
+        self.new_password2 = new_password2
73
+
74
+
75
+class UserInfos(object):
76
+    """
77
+    Just some user infos
78
+    """
79
+    def __init__(self, timezone: str, public_name: str) -> None:
80
+        self.timezone = timezone
81
+        self.public_name = public_name
82
+
83
+
84
+class UserProfile(object):
85
+    """
86
+    Just some user infos
87
+    """
88
+    def __init__(self, profile: str) -> None:
89
+        self.profile = profile
90
+
91
+
92
+class UserCreation(object):
93
+    """
94
+    Just some user infos
95
+    """
96
+    def __init__(
97
+            self,
98
+            email: str,
99
+            password: str,
100
+            public_name: str,
101
+            timezone: str,
102
+            profile: str,
103
+            email_notification: str,
104
+    ) -> None:
105
+        self.email = email
106
+        self.password = password
107
+        self.public_name = public_name
108
+        self.timezone = timezone
109
+        self.profile = profile
110
+        self.email_notification = email_notification
111
+
112
+
52
 class WorkspaceAndContentPath(object):
113
 class WorkspaceAndContentPath(object):
53
     """
114
     """
54
     Paths params with workspace id and content_id model
115
     Paths params with workspace id and content_id model

+ 82 - 0
tracim/tests/functional/test_session.py 查看文件

1
 # coding=utf-8
1
 # coding=utf-8
2
 import datetime
2
 import datetime
3
 import pytest
3
 import pytest
4
+import transaction
4
 from sqlalchemy.exc import OperationalError
5
 from sqlalchemy.exc import OperationalError
5
 
6
 
7
+from tracim import models
8
+from tracim.lib.core.group import GroupApi
9
+from tracim.lib.core.user import UserApi
10
+from tracim.models import get_tm_session
6
 from tracim.tests import FunctionalTest
11
 from tracim.tests import FunctionalTest
7
 from tracim.tests import FunctionalTestNoDB
12
 from tracim.tests import FunctionalTestNoDB
8
 
13
 
59
         assert res.json_body['caldav_url'] is None
64
         assert res.json_body['caldav_url'] is None
60
         assert res.json_body['avatar_url'] is None
65
         assert res.json_body['avatar_url'] is None
61
 
66
 
67
+    def test_api__try_login_enpoint__err_401__user_not_activated(self):
68
+        dbsession = get_tm_session(self.session_factory, transaction.manager)
69
+        admin = dbsession.query(models.User) \
70
+            .filter(models.User.email == 'admin@admin.admin') \
71
+            .one()
72
+        uapi = UserApi(
73
+            current_user=admin,
74
+            session=dbsession,
75
+            config=self.app_config,
76
+        )
77
+        gapi = GroupApi(
78
+            current_user=admin,
79
+            session=dbsession,
80
+            config=self.app_config,
81
+        )
82
+        groups = [gapi.get_one_with_name('users')]
83
+        test_user = uapi.create_user(
84
+            email='test@test.test',
85
+            password='pass',
86
+            name='bob',
87
+            groups=groups,
88
+            timezone='Europe/Paris',
89
+            do_save=True,
90
+            do_notify=False,
91
+        )
92
+        uapi.save(test_user)
93
+        uapi.disable(test_user)
94
+        transaction.commit()
95
+
96
+        params = {
97
+            'email': 'test@test.test',
98
+            'password': 'test@test.test',
99
+        }
100
+        res = self.testapp.post_json(
101
+            '/api/v2/sessions/login',
102
+            params=params,
103
+            status=403,
104
+        )
105
+
62
     def test_api__try_login_enpoint__err_403__bad_password(self):
106
     def test_api__try_login_enpoint__err_403__bad_password(self):
63
         params = {
107
         params = {
64
             'email': 'admin@admin.admin',
108
             'email': 'admin@admin.admin',
117
         assert res.json_body['caldav_url'] is None
161
         assert res.json_body['caldav_url'] is None
118
         assert res.json_body['avatar_url'] is None
162
         assert res.json_body['avatar_url'] is None
119
 
163
 
164
+    def test_api__try_whoami_enpoint__err_401__user_is_not_active(self):
165
+        dbsession = get_tm_session(self.session_factory, transaction.manager)
166
+        admin = dbsession.query(models.User) \
167
+            .filter(models.User.email == 'admin@admin.admin') \
168
+            .one()
169
+        uapi = UserApi(
170
+            current_user=admin,
171
+            session=dbsession,
172
+            config=self.app_config,
173
+        )
174
+        gapi = GroupApi(
175
+            current_user=admin,
176
+            session=dbsession,
177
+            config=self.app_config,
178
+        )
179
+        groups = [gapi.get_one_with_name('users')]
180
+        test_user = uapi.create_user(
181
+            email='test@test.test',
182
+            password='pass',
183
+            name='bob',
184
+            groups=groups,
185
+            timezone='Europe/Paris',
186
+            do_save=True,
187
+            do_notify=False,
188
+        )
189
+        uapi.save(test_user)
190
+        uapi.disable(test_user)
191
+        transaction.commit()
192
+        self.testapp.authorization = (
193
+            'Basic',
194
+            (
195
+                'test@test.test',
196
+                'pass'
197
+            )
198
+        )
199
+
200
+        res = self.testapp.get('/api/v2/sessions/whoami', status=401)
201
+
120
     def test_api__try_whoami_enpoint__err_401__unauthenticated(self):
202
     def test_api__try_whoami_enpoint__err_401__unauthenticated(self):
121
         self.testapp.authorization = (
203
         self.testapp.authorization = (
122
             'Basic',
204
             'Basic',

文件差异内容过多而无法显示
+ 1439 - 1
tracim/tests/functional/test_user.py


+ 29 - 3
tracim/tests/library/test_user_api.py 查看文件

1
 # -*- coding: utf-8 -*-
1
 # -*- coding: utf-8 -*-
2
 import pytest
2
 import pytest
3
-from sqlalchemy.orm.exc import NoResultFound
4
-
5
 import transaction
3
 import transaction
6
 
4
 
7
-from tracim.exceptions import UserDoesNotExist, AuthenticationFailed
5
+from tracim.exceptions import AuthenticationFailed
6
+from tracim.exceptions import UserDoesNotExist
7
+from tracim.exceptions import UserNotActive
8
+from tracim.lib.core.group import GroupApi
8
 from tracim.lib.core.user import UserApi
9
 from tracim.lib.core.user import UserApi
9
 from tracim.models import User
10
 from tracim.models import User
10
 from tracim.models.context_models import UserInContext
11
 from tracim.models.context_models import UserInContext
171
         assert isinstance(user, User)
172
         assert isinstance(user, User)
172
         assert user.email == 'admin@admin.admin'
173
         assert user.email == 'admin@admin.admin'
173
 
174
 
175
+    def test_unit__authenticate_user___err__user_not_active(self):
176
+        api = UserApi(
177
+            current_user=None,
178
+            session=self.session,
179
+            config=self.config,
180
+        )
181
+        gapi = GroupApi(
182
+            current_user=None,
183
+            session=self.session,
184
+            config=self.config,
185
+        )
186
+        groups = [gapi.get_one_with_name('users')]
187
+        user = api.create_user(
188
+            email='test@test.test',
189
+            password='pass',
190
+            name='bob',
191
+            groups=groups,
192
+            timezone='Europe/Paris',
193
+            do_save=True,
194
+            do_notify=False,
195
+        )
196
+        api.disable(user)
197
+        with pytest.raises(UserNotActive):
198
+            api.authenticate_user('test@test.test', 'test@test.test')
199
+
174
     def test_unit__authenticate_user___err__wrong_password(self):
200
     def test_unit__authenticate_user___err__wrong_password(self):
175
         api = UserApi(
201
         api = UserApi(
176
             current_user=None,
202
             current_user=None,

+ 78 - 3
tracim/views/core_api/schemas.py 查看文件

14
 from tracim.models.context_models import ContentIdsQuery
14
 from tracim.models.context_models import ContentIdsQuery
15
 from tracim.models.context_models import UserWorkspaceAndContentPath
15
 from tracim.models.context_models import UserWorkspaceAndContentPath
16
 from tracim.models.context_models import ContentCreation
16
 from tracim.models.context_models import ContentCreation
17
+from tracim.models.context_models import UserCreation
18
+from tracim.models.context_models import SetEmail
19
+from tracim.models.context_models import SetPassword
20
+from tracim.models.context_models import UserInfos
21
+from tracim.models.context_models import UserProfile
17
 from tracim.models.context_models import ContentPreviewSizedPath
22
 from tracim.models.context_models import ContentPreviewSizedPath
18
 from tracim.models.context_models import RevisionPreviewSizedPath
23
 from tracim.models.context_models import RevisionPreviewSizedPath
19
 from tracim.models.context_models import PageQuery
24
 from tracim.models.context_models import PageQuery
65
     )
70
     )
66
     is_active = marshmallow.fields.Bool(
71
     is_active = marshmallow.fields.Bool(
67
         example=True,
72
         example=True,
68
-         # TODO - G.M - Explains this value.
73
+        description='Is user account activated ?'
69
     )
74
     )
70
     # TODO - G.M - 17-04-2018 - Restrict timezone values
75
     # TODO - G.M - 17-04-2018 - Restrict timezone values
71
     timezone = marshmallow.fields.String(
76
     timezone = marshmallow.fields.String(
72
-        example="Paris/Europe",
77
+        example="Europe/Paris",
73
     )
78
     )
74
     # TODO - G.M - 17-04-2018 - check this, relative url allowed ?
79
     # TODO - G.M - 17-04-2018 - check this, relative url allowed ?
75
     caldav_url = marshmallow.fields.Url(
80
     caldav_url = marshmallow.fields.Url(
88
     class Meta:
93
     class Meta:
89
         description = 'User account of Tracim'
94
         description = 'User account of Tracim'
90
 
95
 
91
-# Path Schemas
96
+
97
+class LoggedInUserPasswordSchema(marshmallow.Schema):
98
+    loggedin_user_password = marshmallow.fields.String(
99
+        required=True,
100
+    )
101
+
102
+
103
+class SetEmailSchema(LoggedInUserPasswordSchema):
104
+    email = marshmallow.fields.Email(
105
+        required=True,
106
+        example='suri.cate@algoo.fr'
107
+    )
108
+
109
+    @post_load
110
+    def create_set_email_object(self, data):
111
+        return SetEmail(**data)
112
+
113
+
114
+class SetPasswordSchema(LoggedInUserPasswordSchema):
115
+    new_password = marshmallow.fields.String(
116
+        example='8QLa$<w',
117
+        required=True
118
+    )
119
+    new_password2 = marshmallow.fields.String(
120
+        example='8QLa$<w',
121
+        required=True
122
+    )
123
+
124
+    @post_load
125
+    def create_set_password_object(self, data):
126
+        return SetPassword(**data)
127
+
128
+
129
+class UserInfosSchema(marshmallow.Schema):
130
+    timezone = marshmallow.fields.String(
131
+        example="Europe/Paris",
132
+        required=True,
133
+    )
134
+    public_name = marshmallow.fields.String(
135
+        example='Suri Cate',
136
+        required=True,
137
+    )
138
+
139
+    @post_load
140
+    def create_user_info_object(self, data):
141
+        return UserInfos(**data)
142
+
143
+
144
+class UserProfileSchema(marshmallow.Schema):
145
+    profile = marshmallow.fields.String(
146
+        attribute='profile',
147
+        validate=OneOf(Profile._NAME),
148
+        example='managers',
149
+    )
150
+    @post_load
151
+    def create_user_profile(self, data):
152
+        return UserProfile(**data)
153
+
154
+
155
+class UserCreationSchema(
156
+    SetEmailSchema,
157
+    SetPasswordSchema,
158
+    UserInfosSchema,
159
+    UserProfileSchema
160
+):
161
+    @post_load
162
+    def create_user(self, data):
163
+        return UserCreation(**data)
92
 
164
 
93
 
165
 
166
+# Path Schemas
167
+
94
 class UserIdPathSchema(marshmallow.Schema):
168
 class UserIdPathSchema(marshmallow.Schema):
95
     user_id = marshmallow.fields.Int(
169
     user_id = marshmallow.fields.Int(
96
         example=3,
170
         example=3,
306
     def make_contents_ids(self, data):
380
     def make_contents_ids(self, data):
307
         return ContentIdsQuery(**data)
381
         return ContentIdsQuery(**data)
308
 
382
 
383
+
309
 ###
384
 ###
310
 
385
 
311
 
386
 

+ 236 - 12
tracim/views/core_api/user_controller.py 查看文件

1
 from pyramid.config import Configurator
1
 from pyramid.config import Configurator
2
-
3
-from tracim.lib.core.content import ContentApi
4
-from tracim.lib.utils.authorization import require_same_user_or_profile
5
-from tracim.models import Group
6
-
7
 try:  # Python 3.5+
2
 try:  # Python 3.5+
8
     from http import HTTPStatus
3
     from http import HTTPStatus
9
 except ImportError:
4
 except ImportError:
10
     from http import client as HTTPStatus
5
     from http import client as HTTPStatus
11
 
6
 
12
-from tracim import hapic, TracimRequest
13
-
7
+from tracim import hapic
8
+from tracim import TracimRequest
9
+from tracim.models import Group
10
+from tracim.lib.core.group import GroupApi
11
+from tracim.lib.core.user import UserApi
14
 from tracim.lib.core.workspace import WorkspaceApi
12
 from tracim.lib.core.workspace import WorkspaceApi
13
+from tracim.lib.core.content import ContentApi
15
 from tracim.views.controllers import Controller
14
 from tracim.views.controllers import Controller
16
-from tracim.views.core_api.schemas import UserIdPathSchema, ReadStatusSchema, \
17
-    ContentIdsQuerySchema
15
+from tracim.lib.utils.authorization import require_same_user_or_profile
16
+from tracim.lib.utils.authorization import require_profile
17
+from tracim.exceptions import WrongUserPassword
18
+from tracim.exceptions import PasswordDoNotMatch
19
+from tracim.views.core_api.schemas import UserSchema
20
+from tracim.views.core_api.schemas import SetEmailSchema
21
+from tracim.views.core_api.schemas import SetPasswordSchema
22
+from tracim.views.core_api.schemas import UserInfosSchema
23
+from tracim.views.core_api.schemas import UserCreationSchema
24
+from tracim.views.core_api.schemas import UserProfileSchema
25
+from tracim.views.core_api.schemas import UserIdPathSchema
26
+from tracim.views.core_api.schemas import ReadStatusSchema
27
+from tracim.views.core_api.schemas import ContentIdsQuerySchema
18
 from tracim.views.core_api.schemas import NoContentSchema
28
 from tracim.views.core_api.schemas import NoContentSchema
19
 from tracim.views.core_api.schemas import UserWorkspaceIdPathSchema
29
 from tracim.views.core_api.schemas import UserWorkspaceIdPathSchema
20
 from tracim.views.core_api.schemas import UserWorkspaceAndContentIdPathSchema
30
 from tracim.views.core_api.schemas import UserWorkspaceAndContentIdPathSchema
21
 from tracim.views.core_api.schemas import ContentDigestSchema
31
 from tracim.views.core_api.schemas import ContentDigestSchema
22
 from tracim.views.core_api.schemas import ActiveContentFilterQuerySchema
32
 from tracim.views.core_api.schemas import ActiveContentFilterQuerySchema
23
 from tracim.views.core_api.schemas import WorkspaceDigestSchema
33
 from tracim.views.core_api.schemas import WorkspaceDigestSchema
24
-from tracim.models.contents import ContentTypeLegacy as ContentType
25
 
34
 
26
 USER_ENDPOINTS_TAG = 'Users'
35
 USER_ENDPOINTS_TAG = 'Users'
27
 
36
 
51
 
60
 
52
     @hapic.with_api_doc(tags=[USER_ENDPOINTS_TAG])
61
     @hapic.with_api_doc(tags=[USER_ENDPOINTS_TAG])
53
     @require_same_user_or_profile(Group.TIM_ADMIN)
62
     @require_same_user_or_profile(Group.TIM_ADMIN)
63
+    @hapic.input_path(UserIdPathSchema())
64
+    @hapic.output_body(UserSchema())
65
+    def user(self, context, request: TracimRequest, hapic_data=None):
66
+        """
67
+        Get user infos.
68
+        """
69
+        app_config = request.registry.settings['CFG']
70
+        uapi = UserApi(
71
+            current_user=request.current_user,  # User
72
+            session=request.dbsession,
73
+            config=app_config,
74
+        )
75
+        return uapi.get_user_with_context(request.candidate_user)
76
+
77
+    @hapic.with_api_doc(tags=[USER_ENDPOINTS_TAG])
78
+    @hapic.handle_exception(WrongUserPassword, HTTPStatus.FORBIDDEN)
79
+    @require_same_user_or_profile(Group.TIM_ADMIN)
80
+    @hapic.input_body(SetEmailSchema())
81
+    @hapic.input_path(UserIdPathSchema())
82
+    @hapic.output_body(UserSchema())
83
+    def set_user_email(self, context, request: TracimRequest, hapic_data=None):
84
+        """
85
+        Set user Email
86
+        """
87
+        app_config = request.registry.settings['CFG']
88
+        uapi = UserApi(
89
+            current_user=request.current_user,  # User
90
+            session=request.dbsession,
91
+            config=app_config,
92
+        )
93
+        user = uapi.set_email(
94
+            request.candidate_user,
95
+            hapic_data.body.loggedin_user_password,
96
+            hapic_data.body.email,
97
+            do_save=True
98
+        )
99
+        return uapi.get_user_with_context(user)
100
+
101
+    @hapic.with_api_doc(tags=[USER_ENDPOINTS_TAG])
102
+    @hapic.handle_exception(WrongUserPassword, HTTPStatus.FORBIDDEN)
103
+    @hapic.handle_exception(PasswordDoNotMatch, HTTPStatus.BAD_REQUEST)
104
+    @require_same_user_or_profile(Group.TIM_ADMIN)
105
+    @hapic.input_body(SetPasswordSchema())
106
+    @hapic.input_path(UserIdPathSchema())
107
+    @hapic.output_body(NoContentSchema(), default_http_code=HTTPStatus.NO_CONTENT)  # nopep8
108
+    def set_user_password(self, context, request: TracimRequest, hapic_data=None):  # nopep8
109
+        """
110
+        Set user password
111
+        """
112
+        app_config = request.registry.settings['CFG']
113
+        uapi = UserApi(
114
+            current_user=request.current_user,  # User
115
+            session=request.dbsession,
116
+            config=app_config,
117
+        )
118
+        uapi.set_password(
119
+            request.candidate_user,
120
+            hapic_data.body.loggedin_user_password,
121
+            hapic_data.body.new_password,
122
+            hapic_data.body.new_password2,
123
+            do_save=True
124
+        )
125
+        return
126
+
127
+    @hapic.with_api_doc(tags=[USER_ENDPOINTS_TAG])
128
+    @require_same_user_or_profile(Group.TIM_ADMIN)
129
+    @hapic.input_body(UserInfosSchema())
130
+    @hapic.input_path(UserIdPathSchema())
131
+    @hapic.output_body(UserSchema())
132
+    def set_user_infos(self, context, request: TracimRequest, hapic_data=None):
133
+        """
134
+        Set user info data
135
+        """
136
+        app_config = request.registry.settings['CFG']
137
+        uapi = UserApi(
138
+            current_user=request.current_user,  # User
139
+            session=request.dbsession,
140
+            config=app_config,
141
+        )
142
+        user = uapi.update(
143
+            request.candidate_user,
144
+            name=hapic_data.body.public_name,
145
+            timezone=hapic_data.body.timezone,
146
+            do_save=True
147
+        )
148
+        return uapi.get_user_with_context(user)
149
+
150
+    @hapic.with_api_doc(tags=[USER_ENDPOINTS_TAG])
151
+    @require_profile(Group.TIM_ADMIN)
152
+    @hapic.input_path(UserIdPathSchema())
153
+    @hapic.input_body(UserCreationSchema())
154
+    @hapic.output_body(UserSchema())
155
+    def create_user(self, context, request: TracimRequest, hapic_data=None):
156
+        """
157
+        Create new user
158
+        """
159
+        app_config = request.registry.settings['CFG']
160
+        uapi = UserApi(
161
+            current_user=request.current_user,  # User
162
+            session=request.dbsession,
163
+            config=app_config,
164
+        )
165
+        gapi = GroupApi(
166
+            current_user=request.current_user,  # User
167
+            session=request.dbsession,
168
+            config=app_config,
169
+        )
170
+        groups = [gapi.get_one_with_name(hapic_data.body.profile)]
171
+        user = uapi.create_user(
172
+            email=hapic_data.body.email,
173
+            password=hapic_data.body.password,
174
+            timezone=hapic_data.body.timezone,
175
+            name=hapic_data.body.public_name,
176
+            do_notify=hapic_data.body.email_notification,
177
+            groups=groups,
178
+            do_save=True
179
+        )
180
+        return uapi.get_user_with_context(user)
181
+
182
+    @hapic.with_api_doc(tags=[USER_ENDPOINTS_TAG])
183
+    @require_profile(Group.TIM_ADMIN)
184
+    @hapic.input_path(UserIdPathSchema())
185
+    @hapic.output_body(NoContentSchema(), default_http_code=HTTPStatus.NO_CONTENT)  # nopep8
186
+    def enable_user(self, context, request: TracimRequest, hapic_data=None):
187
+        """
188
+        enable user
189
+        """
190
+        app_config = request.registry.settings['CFG']
191
+        uapi = UserApi(
192
+            current_user=request.current_user,  # User
193
+            session=request.dbsession,
194
+            config=app_config,
195
+        )
196
+        uapi.enable(user=request.candidate_user, do_save=True)
197
+        return
198
+
199
+    @hapic.with_api_doc(tags=[USER_ENDPOINTS_TAG])
200
+    @require_profile(Group.TIM_ADMIN)
201
+    @hapic.input_path(UserIdPathSchema())
202
+    @hapic.output_body(NoContentSchema(), default_http_code=HTTPStatus.NO_CONTENT)  # nopep8
203
+    def disable_user(self, context, request: TracimRequest, hapic_data=None):
204
+        """
205
+        disable user
206
+        """
207
+        app_config = request.registry.settings['CFG']
208
+        uapi = UserApi(
209
+            current_user=request.current_user,  # User
210
+            session=request.dbsession,
211
+            config=app_config,
212
+        )
213
+        uapi.disable(user=request.candidate_user, do_save=True)
214
+        return
215
+
216
+    @hapic.with_api_doc(tags=[USER_ENDPOINTS_TAG])
217
+    @require_profile(Group.TIM_ADMIN)
218
+    @hapic.input_path(UserIdPathSchema())
219
+    @hapic.input_body(UserProfileSchema())
220
+    @hapic.output_body(NoContentSchema(), default_http_code=HTTPStatus.NO_CONTENT)  # nopep8
221
+    def set_profile(self, context, request: TracimRequest, hapic_data=None):
222
+        """
223
+        set user profile
224
+        """
225
+        app_config = request.registry.settings['CFG']
226
+        uapi = UserApi(
227
+            current_user=request.current_user,  # User
228
+            session=request.dbsession,
229
+            config=app_config,
230
+        )
231
+        gapi = GroupApi(
232
+            current_user=request.current_user,  # User
233
+            session=request.dbsession,
234
+            config=app_config,
235
+        )
236
+        groups = [gapi.get_one_with_name(hapic_data.body.profile)]
237
+        uapi.update(
238
+            user=request.candidate_user,
239
+            groups=groups,
240
+            do_save=True,
241
+        )
242
+        return
243
+
244
+    @hapic.with_api_doc(tags=[USER_ENDPOINTS_TAG])
245
+    @require_same_user_or_profile(Group.TIM_ADMIN)
54
     @hapic.input_path(UserWorkspaceIdPathSchema())
246
     @hapic.input_path(UserWorkspaceIdPathSchema())
55
     @hapic.input_query(ActiveContentFilterQuerySchema())
247
     @hapic.input_query(ActiveContentFilterQuerySchema())
56
     @hapic.output_body(ContentDigestSchema(many=True))
248
     @hapic.output_body(ContentDigestSchema(many=True))
175
         for this controller
367
         for this controller
176
         """
368
         """
177
 
369
 
178
-        # user worskpace
370
+        # user workspace
179
         configurator.add_route('user_workspace', '/users/{user_id}/workspaces', request_method='GET')  # nopep8
371
         configurator.add_route('user_workspace', '/users/{user_id}/workspaces', request_method='GET')  # nopep8
180
         configurator.add_view(self.user_workspace, route_name='user_workspace')
372
         configurator.add_view(self.user_workspace, route_name='user_workspace')
181
 
373
 
374
+        # user info
375
+        configurator.add_route('user', '/users/{user_id}', request_method='GET')  # nopep8
376
+        configurator.add_view(self.user, route_name='user')
377
+
378
+        # set user email
379
+        configurator.add_route('set_user_email', '/users/{user_id}/email', request_method='PUT')  # nopep8
380
+        configurator.add_view(self.set_user_email, route_name='set_user_email')
381
+
382
+        # set user password
383
+        configurator.add_route('set_user_password', '/users/{user_id}/password', request_method='PUT')  # nopep8
384
+        configurator.add_view(self.set_user_password, route_name='set_user_password')  # nopep8
385
+
386
+        # set user_info
387
+        configurator.add_route('set_user_info', '/users/{user_id}', request_method='PUT')  # nopep8
388
+        configurator.add_view(self.set_user_infos, route_name='set_user_info')
389
+
390
+        # create user
391
+        configurator.add_route('create_user', '/users', request_method='POST')
392
+        configurator.add_view(self.create_user, route_name='create_user')
393
+
394
+        # enable user
395
+        configurator.add_route('enable_user', '/users/{user_id}/enable', request_method='PUT')  # nopep8
396
+        configurator.add_view(self.enable_user, route_name='enable_user')
397
+
398
+        # disable user
399
+        configurator.add_route('disable_user', '/users/{user_id}/disable', request_method='PUT')  # nopep8
400
+        configurator.add_view(self.disable_user, route_name='disable_user')
401
+
402
+        # set user profile
403
+        configurator.add_route('set_user_profile', '/users/{user_id}/profile', request_method='PUT')  # nopep8
404
+        configurator.add_view(self.set_profile, route_name='set_user_profile')
405
+
182
         # user content
406
         # user content
183
         configurator.add_route('contents_read_status', '/users/{user_id}/workspaces/{workspace_id}/contents/read_status', request_method='GET')  # nopep8
407
         configurator.add_route('contents_read_status', '/users/{user_id}/workspaces/{workspace_id}/contents/read_status', request_method='GET')  # nopep8
184
         configurator.add_view(self.contents_read_status, route_name='contents_read_status')  # nopep8
408
         configurator.add_view(self.contents_read_status, route_name='contents_read_status')  # nopep8
194
 
418
 
195
         # set workspace as read
419
         # set workspace as read
196
         configurator.add_route('read_workspace', '/users/{user_id}/workspaces/{workspace_id}/read', request_method='PUT')  # nopep8
420
         configurator.add_route('read_workspace', '/users/{user_id}/workspaces/{workspace_id}/read', request_method='PUT')  # nopep8
197
-        configurator.add_view(self.set_workspace_as_read, route_name='read_workspace')  # nopep8
421
+        configurator.add_view(self.set_workspace_as_read, route_name='read_workspace')  # nopep8