Browse Source

User Endpoints + Tests

Guénaël Muller 6 years ago
parent
commit
69b14fb0ac

+ 5 - 0
tracim/exceptions.py View File

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
 

+ 27 - 3
tracim/lib/core/user.py View File

118
             name: str=None,
118
             name: str=None,
119
             email: str=None,
119
             email: str=None,
120
             password: str=None,
120
             password: str=None,
121
-            timezone: str='',
121
+            timezone: str=None,
122
+            groups: typing.Optional[typing.List[Group]]=None,
122
             do_save=True,
123
             do_save=True,
123
-    ) -> None:
124
+    ) -> User:
124
         if name is not None:
125
         if name is not None:
125
             user.display_name = name
126
             user.display_name = name
126
 
127
 
130
         if password is not None:
131
         if password is not None:
131
             user.password = password
132
             user.password = password
132
 
133
 
133
-        user.timezone = timezone
134
+        if timezone is not None:
135
+            user.timezone = timezone
136
+
137
+        if groups is not None:
138
+            # INFO - G.M - 2018-07-18 - Delete old groups
139
+            for group in user.groups:
140
+                if group not in groups:
141
+                    user.groups.remove(group)
142
+            # INFO - G.M - 2018-07-18 - add new groups
143
+            for group in groups:
144
+                if group not in user.groups:
145
+                    user.groups.append(group)
134
 
146
 
135
         if do_save:
147
         if do_save:
136
             self.save(user)
148
             self.save(user)
137
 
149
 
150
+        return user
151
+
138
     def create_user(
152
     def create_user(
139
         self,
153
         self,
140
         email,
154
         email,
188
 
202
 
189
         return user
203
         return user
190
 
204
 
205
+    def enable(self, user: User, do_save=False):
206
+        user.is_active = True
207
+        if do_save:
208
+            self.save(user)
209
+
210
+    def disable(self, user:User, do_save=False):
211
+        user.is_active = False
212
+        if do_save:
213
+            self.save(user)
214
+
191
     def save(self, user: User):
215
     def save(self, user: User):
192
         self._session.flush()
216
         self._session.flush()
193
 
217
 

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

34
         self.password = password
34
         self.password = password
35
 
35
 
36
 
36
 
37
+class SetEmail(object):
38
+    """
39
+    Just an email
40
+    """
41
+    def __init__(self, loggedin_user_password: str, email: str) -> None:
42
+        self.loggedin_user_password = loggedin_user_password
43
+        self.email = email
44
+
45
+
46
+class SetPassword(object):
47
+    """
48
+    Just an password
49
+    """
50
+    def __init__(self,
51
+        loggedin_user_password: str,
52
+        new_password: str,
53
+        new_password2: str
54
+    ) -> None:
55
+        self.loggedin_user_password = loggedin_user_password
56
+        self.new_password = new_password
57
+        self.new_password2 = new_password2
58
+
59
+
60
+class UserInfos(object):
61
+    """
62
+    Just some user infos
63
+    """
64
+    def __init__(self, timezone: str, public_name: str) -> None:
65
+        self.timezone = timezone
66
+        self.public_name = public_name
67
+
68
+
69
+class UserProfile(object):
70
+    """
71
+    Just some user infos
72
+    """
73
+    def __init__(self, profile: str) -> None:
74
+        self.profile = profile
75
+
76
+
77
+class UserCreation(object):
78
+    """
79
+    Just some user infos
80
+    """
81
+    def __init__(
82
+            self,
83
+            email: str,
84
+            password: str,
85
+            public_name: str,
86
+            timezone: str,
87
+            profile: str,
88
+            email_notification: str,
89
+    ) -> None:
90
+        self.email = email
91
+        self.password = password
92
+        self.public_name = public_name
93
+        self.timezone = timezone
94
+        self.profile = profile
95
+        self.email_notification = email_notification
96
+
97
+
37
 class WorkspaceAndContentPath(object):
98
 class WorkspaceAndContentPath(object):
38
     """
99
     """
39
     Paths params with workspace id and content_id model
100
     Paths params with workspace id and content_id model

File diff suppressed because it is too large
+ 1444 - 0
tracim/tests/functional/test_user.py


+ 79 - 3
tracim/views/core_api/schemas.py View File

11
 from tracim.models.contents import ContentTypeLegacy as ContentType
11
 from tracim.models.contents import ContentTypeLegacy as ContentType
12
 from tracim.models.contents import ContentStatusLegacy as ContentStatus
12
 from tracim.models.contents import ContentStatusLegacy as ContentStatus
13
 from tracim.models.context_models import ContentCreation
13
 from tracim.models.context_models import ContentCreation
14
+from tracim.models.context_models import UserCreation
15
+from tracim.models.context_models import SetEmail
16
+from tracim.models.context_models import SetPassword
17
+from tracim.models.context_models import UserInfos
18
+from tracim.models.context_models import UserProfile
14
 from tracim.models.context_models import CommentCreation
19
 from tracim.models.context_models import CommentCreation
15
 from tracim.models.context_models import TextBasedContentUpdate
20
 from tracim.models.context_models import TextBasedContentUpdate
16
 from tracim.models.context_models import SetContentStatus
21
 from tracim.models.context_models import SetContentStatus
53
     )
58
     )
54
     is_active = marshmallow.fields.Bool(
59
     is_active = marshmallow.fields.Bool(
55
         example=True,
60
         example=True,
56
-         # TODO - G.M - Explains this value.
61
+        description='Is user account activated ?'
57
     )
62
     )
58
     # TODO - G.M - 17-04-2018 - Restrict timezone values
63
     # TODO - G.M - 17-04-2018 - Restrict timezone values
59
     timezone = marshmallow.fields.String(
64
     timezone = marshmallow.fields.String(
60
-        example="Paris/Europe",
65
+        example="Europe/Paris",
61
     )
66
     )
62
     # TODO - G.M - 17-04-2018 - check this, relative url allowed ?
67
     # TODO - G.M - 17-04-2018 - check this, relative url allowed ?
63
     caldav_url = marshmallow.fields.Url(
68
     caldav_url = marshmallow.fields.Url(
76
     class Meta:
81
     class Meta:
77
         description = 'User account of Tracim'
82
         description = 'User account of Tracim'
78
 
83
 
79
-# Path Schemas
84
+
85
+class LoggedInUserPasswordSchema(marshmallow.Schema):
86
+    loggedin_user_password = marshmallow.fields.String(
87
+        required=True,
88
+    )
80
 
89
 
81
 
90
 
91
+class SetEmailSchema(LoggedInUserPasswordSchema):
92
+    email = marshmallow.fields.Email(
93
+        required=True,
94
+        example='suri.cate@algoo.fr'
95
+    )
96
+
97
+    @post_load
98
+    def create_set_email_object(self, data):
99
+        return SetEmail(**data)
100
+
101
+
102
+class SetPasswordSchema(LoggedInUserPasswordSchema):
103
+    new_password = marshmallow.fields.String(
104
+        example='8QLa$<w',
105
+        required=True
106
+    )
107
+    new_password2 = marshmallow.fields.String(
108
+        example='8QLa$<w',
109
+        required=True
110
+    )
111
+
112
+    @post_load
113
+    def create_set_password_object(self, data):
114
+        return SetPassword(**data)
115
+
116
+
117
+class UserInfosSchema(marshmallow.Schema):
118
+    timezone = marshmallow.fields.String(
119
+        example="Europe/Paris",
120
+        required=True,
121
+    )
122
+    public_name = marshmallow.fields.String(
123
+        example='Suri Cate',
124
+        required=True,
125
+    )
126
+
127
+    @post_load
128
+    def create_user_info_object(self, data):
129
+        return UserInfos(**data)
130
+
131
+
132
+class UserProfileSchema(marshmallow.Schema):
133
+    profile = marshmallow.fields.String(
134
+        attribute='profile',
135
+        validate=OneOf(Profile._NAME),
136
+        example='managers',
137
+    )
138
+    @post_load
139
+    def create_user_profile(self, data):
140
+        return UserProfile(**data)
141
+
142
+
143
+class UserCreationSchema(
144
+    SetEmailSchema,
145
+    SetPasswordSchema,
146
+    UserInfosSchema,
147
+    UserProfileSchema
148
+):
149
+    @post_load
150
+    def create_user(self, data):
151
+        return UserCreation(**data)
152
+
153
+
154
+# Path Schemas
155
+
82
 class UserIdPathSchema(marshmallow.Schema):
156
 class UserIdPathSchema(marshmallow.Schema):
83
     user_id = marshmallow.fields.Int(
157
     user_id = marshmallow.fields.Int(
84
         example=3,
158
         example=3,
122
         required=True,
196
         required=True,
123
         validate=Range(min=1, error="Value must be greater than 0"),
197
         validate=Range(min=1, error="Value must be greater than 0"),
124
     )
198
     )
199
+
125
     @post_load
200
     @post_load
126
     def make_path_object(self, data):
201
     def make_path_object(self, data):
127
         return CommentPath(**data)
202
         return CommentPath(**data)
167
     @post_load
242
     @post_load
168
     def make_content_filter(self, data):
243
     def make_content_filter(self, data):
169
         return ContentFilter(**data)
244
         return ContentFilter(**data)
245
+
170
 ###
246
 ###
171
 
247
 
172
 
248
 

+ 241 - 9
tracim/views/core_api/user_controller.py View File

1
+import transaction
1
 from pyramid.config import Configurator
2
 from pyramid.config import Configurator
2
-from sqlalchemy.orm.exc import NoResultFound
3
-
4
-from tracim.lib.utils.authorization import require_same_user_or_profile
5
-from tracim.models import Group
6
-from tracim.models.context_models import WorkspaceInContext
7
 
3
 
8
 try:  # Python 3.5+
4
 try:  # Python 3.5+
9
     from http import HTTPStatus
5
     from http import HTTPStatus
12
 
8
 
13
 from tracim import hapic, TracimRequest
9
 from tracim import hapic, TracimRequest
14
 
10
 
15
-from tracim.exceptions import NotAuthenticated
16
-from tracim.exceptions import InsufficientUserProfile
17
-from tracim.exceptions import UserDoesNotExist
11
+from tracim.exceptions import WrongUserPassword, PasswordDoNotMatch
12
+from tracim.lib.core.group import GroupApi
13
+from tracim.lib.utils.authorization import require_same_user_or_profile
14
+from tracim.lib.utils.authorization import require_profile
15
+from tracim.models import Group
16
+from tracim.models.context_models import WorkspaceInContext
17
+from tracim.lib.core.user import UserApi
18
 from tracim.lib.core.workspace import WorkspaceApi
18
 from tracim.lib.core.workspace import WorkspaceApi
19
 from tracim.views.controllers import Controller
19
 from tracim.views.controllers import Controller
20
 from tracim.views.core_api.schemas import UserIdPathSchema
20
 from tracim.views.core_api.schemas import UserIdPathSchema
21
+from tracim.views.core_api.schemas import UserSchema
22
+from tracim.views.core_api.schemas import SetEmailSchema
23
+from tracim.views.core_api.schemas import SetPasswordSchema
24
+from tracim.views.core_api.schemas import UserInfosSchema
25
+from tracim.views.core_api.schemas import NoContentSchema
26
+from tracim.views.core_api.schemas import UserCreationSchema
27
+from tracim.views.core_api.schemas import UserProfileSchema
21
 from tracim.views.core_api.schemas import WorkspaceDigestSchema
28
 from tracim.views.core_api.schemas import WorkspaceDigestSchema
22
 
29
 
23
 USER_ENDPOINTS_TAG = 'Users'
30
 USER_ENDPOINTS_TAG = 'Users'
46
             for workspace in workspaces
53
             for workspace in workspaces
47
         ]
54
         ]
48
 
55
 
56
+    @hapic.with_api_doc(tags=[USER_ENDPOINTS_TAG])
57
+    @require_same_user_or_profile(Group.TIM_ADMIN)
58
+    @hapic.input_path(UserIdPathSchema())
59
+    @hapic.output_body(UserSchema())
60
+    def user(self, context, request: TracimRequest, hapic_data=None):
61
+        """
62
+        Get user infos.
63
+        """
64
+        app_config = request.registry.settings['CFG']
65
+        uapi = UserApi(
66
+            current_user=request.current_user,  # User
67
+            session=request.dbsession,
68
+            config=app_config,
69
+        )
70
+        return uapi.get_user_with_context(request.candidate_user)
71
+
72
+    @hapic.with_api_doc(tags=[USER_ENDPOINTS_TAG])
73
+    @hapic.handle_exception(WrongUserPassword, HTTPStatus.FORBIDDEN)
74
+    @require_same_user_or_profile(Group.TIM_ADMIN)
75
+    @hapic.input_body(SetEmailSchema())
76
+    @hapic.input_path(UserIdPathSchema())
77
+    @hapic.output_body(UserSchema())
78
+    def set_user_email(self, context, request: TracimRequest, hapic_data=None):
79
+        """
80
+        Set user Email
81
+        """
82
+        if not request.current_user.validate_password(hapic_data.body.loggedin_user_password):  # nopep8
83
+            raise WrongUserPassword(
84
+                'Wrong password for authenticated user {}'. format(request.current_user.user_id)  # nopep8
85
+            )
86
+        app_config = request.registry.settings['CFG']
87
+        uapi = UserApi(
88
+            current_user=request.current_user,  # User
89
+            session=request.dbsession,
90
+            config=app_config,
91
+        )
92
+        user = uapi.update(
93
+            request.candidate_user,
94
+            email=hapic_data.body.email,
95
+            do_save=True
96
+        )
97
+        return uapi.get_user_with_context(user)
98
+
99
+    @hapic.with_api_doc(tags=[USER_ENDPOINTS_TAG])
100
+    @hapic.handle_exception(WrongUserPassword, HTTPStatus.FORBIDDEN)
101
+    @hapic.handle_exception(PasswordDoNotMatch, HTTPStatus.BAD_REQUEST)
102
+    @require_same_user_or_profile(Group.TIM_ADMIN)
103
+    @hapic.input_body(SetPasswordSchema())
104
+    @hapic.input_path(UserIdPathSchema())
105
+    @hapic.output_body(NoContentSchema(), default_http_code=HTTPStatus.NO_CONTENT)  # nopep8
106
+    def set_user_password(self, context, request: TracimRequest, hapic_data=None):  # nopep8
107
+        """
108
+        Set user password
109
+        """
110
+        if not request.current_user.validate_password(hapic_data.body.loggedin_user_password):  # nopep8
111
+            raise WrongUserPassword(
112
+                'Wrong password for authenticated user {}'. format(request.current_user.user_id)  # nopep8
113
+            )
114
+        if hapic_data.body.new_password != hapic_data.body.new_password2:
115
+            raise PasswordDoNotMatch('Passwords given are different')
116
+        app_config = request.registry.settings['CFG']
117
+        uapi = UserApi(
118
+            current_user=request.current_user,  # User
119
+            session=request.dbsession,
120
+            config=app_config,
121
+        )
122
+        uapi.update(
123
+            request.candidate_user,
124
+            password=hapic_data.body.new_password,
125
+            do_save=True
126
+        )
127
+        uapi.save(request.candidate_user)
128
+        # TODO - G.M - 2018-07-24 - Check why commit is needed here
129
+        transaction.commit()
130
+        return
131
+
132
+    @hapic.with_api_doc(tags=[USER_ENDPOINTS_TAG])
133
+    @require_same_user_or_profile(Group.TIM_ADMIN)
134
+    @hapic.input_body(UserInfosSchema())
135
+    @hapic.input_path(UserIdPathSchema())
136
+    @hapic.output_body(UserSchema())
137
+    def set_user_infos(self, context, request: TracimRequest, hapic_data=None):
138
+        """
139
+        Set user info data
140
+        """
141
+        app_config = request.registry.settings['CFG']
142
+        uapi = UserApi(
143
+            current_user=request.current_user,  # User
144
+            session=request.dbsession,
145
+            config=app_config,
146
+        )
147
+        user = uapi.update(
148
+            request.candidate_user,
149
+            name=hapic_data.body.public_name,
150
+            timezone=hapic_data.body.timezone,
151
+            do_save=True
152
+        )
153
+        return uapi.get_user_with_context(user)
154
+
155
+    @hapic.with_api_doc(tags=[USER_ENDPOINTS_TAG])
156
+    @require_profile(Group.TIM_ADMIN)
157
+    @hapic.input_path(UserIdPathSchema())
158
+    @hapic.input_body(UserCreationSchema())
159
+    @hapic.output_body(UserSchema())
160
+    def create_user(self, context, request: TracimRequest, hapic_data=None):
161
+        """
162
+        Create new user
163
+        """
164
+        app_config = request.registry.settings['CFG']
165
+        uapi = UserApi(
166
+            current_user=request.current_user,  # User
167
+            session=request.dbsession,
168
+            config=app_config,
169
+        )
170
+        gapi = GroupApi(
171
+            current_user=request.current_user,  # User
172
+            session=request.dbsession,
173
+            config=app_config,
174
+        )
175
+        groups = [gapi.get_one_with_name(hapic_data.body.profile)]
176
+        user = uapi.create_user(
177
+            email=hapic_data.body.email,
178
+            password=hapic_data.body.password,
179
+            timezone=hapic_data.body.timezone,
180
+            name=hapic_data.body.public_name,
181
+            do_notify=hapic_data.body.email_notification,
182
+            groups=groups,
183
+            do_save=True
184
+        )
185
+        return uapi.get_user_with_context(user)
186
+
187
+    @hapic.with_api_doc(tags=[USER_ENDPOINTS_TAG])
188
+    @require_profile(Group.TIM_ADMIN)
189
+    @hapic.input_path(UserIdPathSchema())
190
+    @hapic.output_body(NoContentSchema(), default_http_code=HTTPStatus.NO_CONTENT)  # nopep8
191
+    def enable_user(self, context, request: TracimRequest, hapic_data=None):
192
+        """
193
+        enable user
194
+        """
195
+        app_config = request.registry.settings['CFG']
196
+        uapi = UserApi(
197
+            current_user=request.current_user,  # User
198
+            session=request.dbsession,
199
+            config=app_config,
200
+        )
201
+        uapi.enable(user=request.candidate_user, do_save=True)
202
+        return
203
+
204
+    @hapic.with_api_doc(tags=[USER_ENDPOINTS_TAG])
205
+    @require_profile(Group.TIM_ADMIN)
206
+    @hapic.input_path(UserIdPathSchema())
207
+    @hapic.output_body(NoContentSchema(), default_http_code=HTTPStatus.NO_CONTENT)  # nopep8
208
+    def disable_user(self, context, request: TracimRequest, hapic_data=None):
209
+        """
210
+        disable user
211
+        """
212
+        app_config = request.registry.settings['CFG']
213
+        uapi = UserApi(
214
+            current_user=request.current_user,  # User
215
+            session=request.dbsession,
216
+            config=app_config,
217
+        )
218
+        uapi.disable(user=request.candidate_user, do_save=True)
219
+        return
220
+
221
+    @hapic.with_api_doc(tags=[USER_ENDPOINTS_TAG])
222
+    @require_profile(Group.TIM_ADMIN)
223
+    @hapic.input_path(UserIdPathSchema())
224
+    @hapic.input_body(UserProfileSchema())
225
+    @hapic.output_body(NoContentSchema(), default_http_code=HTTPStatus.NO_CONTENT)  # nopep8
226
+    def set_profile(self, context, request: TracimRequest, hapic_data=None):
227
+        """
228
+        set user profile
229
+        """
230
+        app_config = request.registry.settings['CFG']
231
+        uapi = UserApi(
232
+            current_user=request.current_user,  # User
233
+            session=request.dbsession,
234
+            config=app_config,
235
+        )
236
+        gapi = GroupApi(
237
+            current_user=request.current_user,  # User
238
+            session=request.dbsession,
239
+            config=app_config,
240
+        )
241
+        groups = [gapi.get_one_with_name(hapic_data.body.profile)]
242
+        uapi.update(
243
+            user=request.candidate_user,
244
+            groups=groups,
245
+            do_save=True,
246
+        )
247
+        return
248
+
49
     def bind(self, configurator: Configurator) -> None:
249
     def bind(self, configurator: Configurator) -> None:
50
         """
250
         """
51
         Create all routes and views using pyramid configurator
251
         Create all routes and views using pyramid configurator
52
         for this controller
252
         for this controller
53
         """
253
         """
54
 
254
 
55
-        # Applications
255
+        # user workspace
56
         configurator.add_route('user_workspace', '/users/{user_id}/workspaces', request_method='GET')  # nopep8
256
         configurator.add_route('user_workspace', '/users/{user_id}/workspaces', request_method='GET')  # nopep8
57
         configurator.add_view(self.user_workspace, route_name='user_workspace')
257
         configurator.add_view(self.user_workspace, route_name='user_workspace')
258
+
259
+        # user info
260
+        configurator.add_route('user', '/users/{user_id}', request_method='GET')  # nopep8
261
+        configurator.add_view(self.user, route_name='user')
262
+
263
+        # set user email
264
+        configurator.add_route('set_user_email', '/users/{user_id}/email', request_method='PUT')  # nopep8
265
+        configurator.add_view(self.set_user_email, route_name='set_user_email')
266
+
267
+        # set user password
268
+        configurator.add_route('set_user_password', '/users/{user_id}/password', request_method='PUT')  # nopep8
269
+        configurator.add_view(self.set_user_password, route_name='set_user_password')  # nopep8
270
+
271
+        # set user_info
272
+        configurator.add_route('set_user_info', '/users/{user_id}', request_method='PUT')  # nopep8
273
+        configurator.add_view(self.set_user_infos, route_name='set_user_info')
274
+
275
+        # create user
276
+        configurator.add_route('create_user', '/users', request_method='POST')
277
+        configurator.add_view(self.create_user, route_name='create_user')
278
+
279
+        # enable user
280
+        configurator.add_route('enable_user', '/users/{user_id}/enable', request_method='PUT')  # nopep8
281
+        configurator.add_view(self.enable_user, route_name='enable_user')
282
+
283
+        # disable user
284
+        configurator.add_route('disable_user', '/users/{user_id}/disable', request_method='PUT')  # nopep8
285
+        configurator.add_view(self.disable_user, route_name='disable_user')
286
+
287
+        # set user profile
288
+        configurator.add_route('set_user_profile', '/users/{user_id}/profile', request_method='PUT')  # nopep8
289
+        configurator.add_view(self.set_profile, route_name='set_user_profile')