Browse Source

User Endpoints + Tests

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

+ 5 - 0
tracim/exceptions.py View File

@@ -140,6 +140,7 @@ class InvalidWorkspaceId(InvalidId):
140 140
 class InvalidUserId(InvalidId):
141 141
     pass
142 142
 
143
+
143 144
 class ContentNotFound(TracimException):
144 145
     pass
145 146
 
@@ -152,6 +153,10 @@ class WorkspacesDoNotMatch(TracimException):
152 153
     pass
153 154
 
154 155
 
156
+class PasswordDoNotMatch(TracimException):
157
+    pass
158
+
159
+
155 160
 class EmptyValueNotAllowed(TracimException):
156 161
     pass
157 162
 

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

@@ -118,9 +118,10 @@ class UserApi(object):
118 118
             name: str=None,
119 119
             email: str=None,
120 120
             password: str=None,
121
-            timezone: str='',
121
+            timezone: str=None,
122
+            groups: typing.Optional[typing.List[Group]]=None,
122 123
             do_save=True,
123
-    ) -> None:
124
+    ) -> User:
124 125
         if name is not None:
125 126
             user.display_name = name
126 127
 
@@ -130,11 +131,24 @@ class UserApi(object):
130 131
         if password is not None:
131 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 147
         if do_save:
136 148
             self.save(user)
137 149
 
150
+        return user
151
+
138 152
     def create_user(
139 153
         self,
140 154
         email,
@@ -188,6 +202,16 @@ class UserApi(object):
188 202
 
189 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 215
     def save(self, user: User):
192 216
         self._session.flush()
193 217
 

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

@@ -34,6 +34,67 @@ class LoginCredentials(object):
34 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 98
 class WorkspaceAndContentPath(object):
38 99
     """
39 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,6 +11,11 @@ from tracim.models.contents import open_status
11 11
 from tracim.models.contents import ContentTypeLegacy as ContentType
12 12
 from tracim.models.contents import ContentStatusLegacy as ContentStatus
13 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 19
 from tracim.models.context_models import CommentCreation
15 20
 from tracim.models.context_models import TextBasedContentUpdate
16 21
 from tracim.models.context_models import SetContentStatus
@@ -53,11 +58,11 @@ class UserSchema(UserDigestSchema):
53 58
     )
54 59
     is_active = marshmallow.fields.Bool(
55 60
         example=True,
56
-         # TODO - G.M - Explains this value.
61
+        description='Is user account activated ?'
57 62
     )
58 63
     # TODO - G.M - 17-04-2018 - Restrict timezone values
59 64
     timezone = marshmallow.fields.String(
60
-        example="Paris/Europe",
65
+        example="Europe/Paris",
61 66
     )
62 67
     # TODO - G.M - 17-04-2018 - check this, relative url allowed ?
63 68
     caldav_url = marshmallow.fields.Url(
@@ -76,9 +81,78 @@ class UserSchema(UserDigestSchema):
76 81
     class Meta:
77 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 156
 class UserIdPathSchema(marshmallow.Schema):
83 157
     user_id = marshmallow.fields.Int(
84 158
         example=3,
@@ -122,6 +196,7 @@ class CommentsPathSchema(WorkspaceAndContentIdPathSchema):
122 196
         required=True,
123 197
         validate=Range(min=1, error="Value must be greater than 0"),
124 198
     )
199
+
125 200
     @post_load
126 201
     def make_path_object(self, data):
127 202
         return CommentPath(**data)
@@ -167,6 +242,7 @@ class FilterContentQuerySchema(marshmallow.Schema):
167 242
     @post_load
168 243
     def make_content_filter(self, data):
169 244
         return ContentFilter(**data)
245
+
170 246
 ###
171 247
 
172 248
 

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

@@ -1,9 +1,5 @@
1
+import transaction
1 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 4
 try:  # Python 3.5+
9 5
     from http import HTTPStatus
@@ -12,12 +8,23 @@ except ImportError:
12 8
 
13 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 18
 from tracim.lib.core.workspace import WorkspaceApi
19 19
 from tracim.views.controllers import Controller
20 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 28
 from tracim.views.core_api.schemas import WorkspaceDigestSchema
22 29
 
23 30
 USER_ENDPOINTS_TAG = 'Users'
@@ -46,12 +53,237 @@ class UserController(Controller):
46 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 249
     def bind(self, configurator: Configurator) -> None:
50 250
         """
51 251
         Create all routes and views using pyramid configurator
52 252
         for this controller
53 253
         """
54 254
 
55
-        # Applications
255
+        # user workspace
56 256
         configurator.add_route('user_workspace', '/users/{user_id}/workspaces', request_method='GET')  # nopep8
57 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')