Browse Source

Merge pull request #345 from tracim/fix/307/webdav_auth_once_pwd_email_changed

Damien Accorsi 7 years ago
parent
commit
134074f376

+ 0 - 2
tracim/tracim/command/user.py View File

111
 
111
 
112
         try:
112
         try:
113
             user = User(email=login, password=password, **kwargs)
113
             user = User(email=login, password=password, **kwargs)
114
-            user.update_webdav_digest_auth(password)
115
             self._session.add(user)
114
             self._session.add(user)
116
             self._session.flush()
115
             self._session.flush()
117
 
116
 
130
     def _update_password_for_login(self, login, password):
129
     def _update_password_for_login(self, login, password):
131
         user = self._user_api.get_one_by_email(login)
130
         user = self._user_api.get_one_by_email(login)
132
         user.password = password
131
         user.password = password
133
-        user.update_webdav_digest_auth(password)
134
         self._session.flush()
132
         self._session.flush()
135
         transaction.commit()
133
         transaction.commit()
136
 
134
 

+ 58 - 67
tracim/tracim/controllers/admin/user.py View File

1
 # -*- coding: utf-8 -*-
1
 # -*- coding: utf-8 -*-
2
-import uuid
3
 import random
2
 import random
4
 
3
 
5
 import pytz
4
 import pytz
6
-from tracim import model  as pm
5
+from tracim import model as pm
7
 
6
 
8
-from sprox.tablebase import TableBase
9
-from sprox.formbase import EditableForm, AddRecordForm
10
-from sprox.fillerbase import TableFiller, EditFormFiller
11
-from tracim.config.app_cfg import CFG
12
-from tw2 import forms as tw2f
13
 import tg
7
 import tg
14
 from tg import predicates
8
 from tg import predicates
15
 from tg import tmpl_context
9
 from tg import tmpl_context
16
 from tg.i18n import ugettext as _
10
 from tg.i18n import ugettext as _
17
 
11
 
18
-from sprox.widgets import PropertyMultipleSelectField
19
-from sprox._compat import unicode_text
20
-
21
-from formencode import Schema
22
-from formencode.validators import FieldsMatch
23
-
24
 from tracim.controllers import TIMRestController
12
 from tracim.controllers import TIMRestController
25
 from tracim.controllers.user import UserWorkspaceRestController
13
 from tracim.controllers.user import UserWorkspaceRestController
26
 
14
 
28
 from tracim.lib import helpers as h
16
 from tracim.lib import helpers as h
29
 from tracim.lib.base import logger
17
 from tracim.lib.base import logger
30
 from tracim.lib.email import get_email_manager
18
 from tracim.lib.email import get_email_manager
31
-from tracim.lib.user import UserApi
32
 from tracim.lib.group import GroupApi
19
 from tracim.lib.group import GroupApi
20
+from tracim.lib.user import UserApi
33
 from tracim.lib.userworkspace import RoleApi
21
 from tracim.lib.userworkspace import RoleApi
34
 from tracim.lib.workspace import WorkspaceApi
22
 from tracim.lib.workspace import WorkspaceApi
35
 
23
 
36
 from tracim.model import DBSession
24
 from tracim.model import DBSession
37
-from tracim.model.auth import Group, User
38
-from tracim.model.serializers import Context, CTX, DictLikeClass
25
+from tracim.model.auth import Group
26
+from tracim.model.serializers import CTX
27
+from tracim.model.serializers import Context
28
+from tracim.model.serializers import DictLikeClass
29
+
39
 
30
 
40
 class UserProfileAdminRestController(TIMRestController):
31
 class UserProfileAdminRestController(TIMRestController):
41
-    """
42
-     CRUD Controller allowing to manage groups of a user
43
-    """
32
+    """CRUD Controller allowing to manage groups of a user."""
44
 
33
 
45
     allow_only = predicates.in_group(Group.TIM_ADMIN_GROUPNAME)
34
     allow_only = predicates.in_group(Group.TIM_ADMIN_GROUPNAME)
46
 
35
 
51
     @property
40
     @property
52
     def allowed_profiles(self):
41
     def allowed_profiles(self):
53
         return [
42
         return [
54
-        UserProfileAdminRestController._ALLOWED_PROFILE_USER,
55
-        UserProfileAdminRestController._ALLOWED_PROFILE_MANAGER,
56
-        UserProfileAdminRestController._ALLOWED_PROFILE_ADMIN
57
-    ]
43
+            UserProfileAdminRestController._ALLOWED_PROFILE_USER,
44
+            UserProfileAdminRestController._ALLOWED_PROFILE_MANAGER,
45
+            UserProfileAdminRestController._ALLOWED_PROFILE_ADMIN,
46
+        ]
58
 
47
 
59
     def _before(self, *args, **kw):
48
     def _before(self, *args, **kw):
60
         """
49
         """
61
-        Instantiate the current workspace in tg.tmpl_context
50
+        Instantiate the current workspace in tg.tmpl_context.
51
+
62
         :param args:
52
         :param args:
63
         :param kw:
53
         :param kw:
64
         :return:
54
         :return:
72
         tg.tmpl_context.user = user
62
         tg.tmpl_context.user = user
73
 
63
 
74
     @tg.expose()
64
     @tg.expose()
75
-    def switch(self, new_role):
65
+    def switch(self, new_role) -> None:
76
         """
66
         """
77
-        :param new_role: value should be 'tracim-user', 'tracim-manager' (allowed to create workspaces) or 'tracim-admin' (admin the whole system)
78
-        :return:
67
+        Switch to the given new role.
68
+
69
+        :param new_role: value should be:
70
+            'tracim-user',
71
+            'tracim-manager' (allowed to create workspaces) or
72
+            'tracim-admin' (admin the whole system)
79
         """
73
         """
80
         return self.put(new_role)
74
         return self.put(new_role)
81
 
75
 
87
 
81
 
88
         group_api = GroupApi(current_user)
82
         group_api = GroupApi(current_user)
89
 
83
 
90
-        if current_user.user_id==user.user_id:
84
+        if current_user.user_id == user.user_id:
91
             tg.flash(_('You can\'t change your own profile'), CST.STATUS_ERROR)
85
             tg.flash(_('You can\'t change your own profile'), CST.STATUS_ERROR)
92
             tg.redirect(self.parent_controller.url())
86
             tg.redirect(self.parent_controller.url())
93
 
87
 
94
-
95
         redirect_url = self.parent_controller.url(skip_id=True)
88
         redirect_url = self.parent_controller.url(skip_id=True)
96
 
89
 
97
         if new_profile not in self.allowed_profiles:
90
         if new_profile not in self.allowed_profiles:
102
         pod_manager_group = group_api.get_one(Group.TIM_MANAGER)
95
         pod_manager_group = group_api.get_one(Group.TIM_MANAGER)
103
         pod_admin_group = group_api.get_one(Group.TIM_ADMIN)
96
         pod_admin_group = group_api.get_one(Group.TIM_ADMIN)
104
 
97
 
105
-        flash_message = _('User updated.') # this is the default value ; should never appear
98
+        # this is the default value ; should never appear
99
+        flash_message = _('User updated.')
106
 
100
 
107
-        if new_profile==UserProfileAdminRestController._ALLOWED_PROFILE_USER:
101
+        if new_profile == UserProfileAdminRestController._ALLOWED_PROFILE_USER:
108
             if pod_user_group not in user.groups:
102
             if pod_user_group not in user.groups:
109
                 user.groups.append(pod_user_group)
103
                 user.groups.append(pod_user_group)
110
 
104
 
120
 
114
 
121
             flash_message = _('User {} is now a basic user').format(user.get_display_name())
115
             flash_message = _('User {} is now a basic user').format(user.get_display_name())
122
 
116
 
123
-        elif new_profile==UserProfileAdminRestController._ALLOWED_PROFILE_MANAGER:
117
+        elif new_profile == UserProfileAdminRestController._ALLOWED_PROFILE_MANAGER:
124
             if pod_user_group not in user.groups:
118
             if pod_user_group not in user.groups:
125
                 user.groups.append(pod_user_group)
119
                 user.groups.append(pod_user_group)
126
             if pod_manager_group not in user.groups:
120
             if pod_manager_group not in user.groups:
133
 
127
 
134
             flash_message = _('User {} can now workspaces').format(user.get_display_name())
128
             flash_message = _('User {} can now workspaces').format(user.get_display_name())
135
 
129
 
136
-
137
-        elif new_profile==UserProfileAdminRestController._ALLOWED_PROFILE_ADMIN:
130
+        elif new_profile == UserProfileAdminRestController._ALLOWED_PROFILE_ADMIN:
138
             if pod_user_group not in user.groups:
131
             if pod_user_group not in user.groups:
139
                 user.groups.append(pod_user_group)
132
                 user.groups.append(pod_user_group)
140
             if pod_manager_group not in user.groups:
133
             if pod_manager_group not in user.groups:
145
             flash_message = _('User {} is now an administrator').format(user.get_display_name())
138
             flash_message = _('User {} is now an administrator').format(user.get_display_name())
146
 
139
 
147
         else:
140
         else:
148
-            logger.error(self, 'Trying to change user {} profile with unexpected profile {}'.format(user.user_id, new_profile))
141
+            error_msg = \
142
+                'Trying to change user {} profile with unexpected profile {}'
143
+            logger.error(self, error_msg.format(user.user_id, new_profile))
149
             tg.flash(_('Unknown profile'), CST.STATUS_ERROR)
144
             tg.flash(_('Unknown profile'), CST.STATUS_ERROR)
150
             tg.redirect(redirect_url)
145
             tg.redirect(redirect_url)
151
 
146
 
163
         pass
158
         pass
164
 
159
 
165
 
160
 
166
-
167
 class UserPasswordAdminRestController(TIMRestController):
161
 class UserPasswordAdminRestController(TIMRestController):
168
-    """
169
-     CRUD Controller allowing to manage password of a given user
170
-    """
162
+    """CRUD Controller allowing to manage password of a given user."""
171
 
163
 
172
-    allow_only = predicates.in_any_group(Group.TIM_MANAGER_GROUPNAME, Group.TIM_ADMIN_GROUPNAME)
164
+    allow_only = predicates.in_any_group(
165
+            Group.TIM_MANAGER_GROUPNAME,
166
+            Group.TIM_ADMIN_GROUPNAME,
167
+        )
173
 
168
 
174
     def _before(self, *args, **kw):
169
     def _before(self, *args, **kw):
175
         """
170
         """
176
-        Instantiate the current workspace in tg.tmpl_context
171
+        Instantiate the current workspace in tg.tmpl_context.
172
+
177
         :param args:
173
         :param args:
178
         :param kw:
174
         :param kw:
179
         :return:
175
         :return:
186
         tg.tmpl_context.user_id = user_id
182
         tg.tmpl_context.user_id = user_id
187
         tg.tmpl_context.user = user
183
         tg.tmpl_context.user = user
188
 
184
 
189
-
190
     @tg.expose('tracim.templates.admin.user_password_edit')
185
     @tg.expose('tracim.templates.admin.user_password_edit')
191
     def edit(self):
186
     def edit(self):
192
         current_user = tmpl_context.current_user
187
         current_user = tmpl_context.current_user
193
         api = UserApi(current_user)
188
         api = UserApi(current_user)
194
         dictified_user = Context(CTX.USER).toDict(tmpl_context.user, 'user')
189
         dictified_user = Context(CTX.USER).toDict(tmpl_context.user, 'user')
195
-        return DictLikeClass(result = dictified_user)
190
+        return DictLikeClass(result=dictified_user)
196
 
191
 
197
     @tg.expose()
192
     @tg.expose()
198
     def put(self, new_password1, new_password2, next_url=''):
193
     def put(self, new_password1, new_password2, next_url=''):
207
             tg.flash(_('Empty password is not allowed.'), CST.STATUS_ERROR)
202
             tg.flash(_('Empty password is not allowed.'), CST.STATUS_ERROR)
208
             tg.redirect(next_url)
203
             tg.redirect(next_url)
209
 
204
 
210
-        if new_password1!=new_password2:
205
+        if new_password1 != new_password2:
211
             tg.flash(_('New passwords do not match.'), CST.STATUS_ERROR)
206
             tg.flash(_('New passwords do not match.'), CST.STATUS_ERROR)
212
             tg.redirect(next_url)
207
             tg.redirect(next_url)
213
 
208
 
214
         user.password = new_password1
209
         user.password = new_password1
215
-        user.update_webdav_digest_auth(new_password1)
216
         pm.DBSession.flush()
210
         pm.DBSession.flush()
217
 
211
 
218
         tg.flash(_('The password has been changed'), CST.STATUS_OK)
212
         tg.flash(_('The password has been changed'), CST.STATUS_OK)
223
 
217
 
224
     def _before(self, *args, **kw):
218
     def _before(self, *args, **kw):
225
         """
219
         """
226
-        Instantiate the current workspace in tg.tmpl_context
220
+        Instantiate the current workspace in tg.tmpl_context.
221
+
227
         :param args:
222
         :param args:
228
         :param kw:
223
         :param kw:
229
         :return:
224
         :return:
266
 
261
 
267
 
262
 
268
 class UserRestController(TIMRestController):
263
 class UserRestController(TIMRestController):
269
-    """
270
-     CRUD Controller allowing to manage Users
271
-    """
272
-    allow_only = predicates.in_any_group(Group.TIM_MANAGER_GROUPNAME, Group.TIM_ADMIN_GROUPNAME)
264
+    """CRUD Controller allowing to manage Users."""
265
+
266
+    allow_only = predicates.in_any_group(
267
+            Group.TIM_MANAGER_GROUPNAME,
268
+            Group.TIM_ADMIN_GROUPNAME,
269
+        )
273
 
270
 
274
     password = UserPasswordAdminRestController()
271
     password = UserPasswordAdminRestController()
275
     profile = UserProfileAdminRestController()
272
     profile = UserProfileAdminRestController()
284
     def current_item_id_key_in_context(cls):
281
     def current_item_id_key_in_context(cls):
285
         return 'user_id'
282
         return 'user_id'
286
 
283
 
287
-
288
     @tg.require(predicates.in_group(Group.TIM_MANAGER_GROUPNAME))
284
     @tg.require(predicates.in_group(Group.TIM_MANAGER_GROUPNAME))
289
     @tg.expose('tracim.templates.admin.user_getall')
285
     @tg.expose('tracim.templates.admin.user_getall')
290
     def get_all(self, *args, **kw):
286
     def get_all(self, *args, **kw):
297
         fake_api = Context(CTX.USERS).toDict({'current_user': current_user_content})
293
         fake_api = Context(CTX.USERS).toDict({'current_user': current_user_content})
298
 
294
 
299
         dictified_users = Context(CTX.USERS).toDict(users, 'users', 'user_nb')
295
         dictified_users = Context(CTX.USERS).toDict(users, 'users', 'user_nb')
300
-        return DictLikeClass(result = dictified_users, fake_api=fake_api)
296
+        return DictLikeClass(result=dictified_users, fake_api=fake_api)
301
 
297
 
302
     @tg.require(predicates.in_group(Group.TIM_MANAGER_GROUPNAME))
298
     @tg.require(predicates.in_group(Group.TIM_MANAGER_GROUPNAME))
303
     @tg.expose()
299
     @tg.expose()
336
             password = self.generate_password()
332
             password = self.generate_password()
337
             user.password = password
333
             user.password = password
338
 
334
 
339
-        user.webdav_left_digest_response_hash = '%s:/:%s' % (email, password)
340
-
341
         api.save(user)
335
         api.save(user)
342
 
336
 
343
         # Now add the user to related groups
337
         # Now add the user to related groups
361
     @classmethod
355
     @classmethod
362
     def generate_password(
356
     def generate_password(
363
             cls,
357
             cls,
364
-            password_length = PASSWORD_LENGTH,
365
-            password_chars = PASSWORD_CHARACTERS
366
-            ):
367
-
358
+            password_length=PASSWORD_LENGTH,
359
+            password_chars=PASSWORD_CHARACTERS,
360
+    ):
368
         # character list that will be contained into the password
361
         # character list that will be contained into the password
369
         char_list = []
362
         char_list = []
370
 
363
 
371
-        for j in range(0, password_length):
364
+        for _unused in range(password_length):
372
             # This puts a random char from the list above inside
365
             # This puts a random char from the list above inside
373
             # the list of chars and then merges them into a String
366
             # the list of chars and then merges them into a String
374
             char_list.append(random.choice(password_chars))
367
             char_list.append(random.choice(password_chars))
378
     @tg.expose('tracim.templates.admin.user_getone')
371
     @tg.expose('tracim.templates.admin.user_getone')
379
     def get_one(self, user_id):
372
     def get_one(self, user_id):
380
         current_user = tmpl_context.current_user
373
         current_user = tmpl_context.current_user
381
-        api = UserApi(current_user )
374
+        api = UserApi(current_user)
382
         # role_api = RoleApi(tg.tmpl_context.current_user)
375
         # role_api = RoleApi(tg.tmpl_context.current_user)
383
         # user_api = UserApi(tg.tmpl_context.current_user)
376
         # user_api = UserApi(tg.tmpl_context.current_user)
384
 
377
 
385
-        user = api.get_one(user_id) # FIXME
378
+        user = api.get_one(user_id)  # FIXME
386
 
379
 
387
         role_api = RoleApi(tg.tmpl_context.current_user)
380
         role_api = RoleApi(tg.tmpl_context.current_user)
388
         role_list = role_api.get_roles_for_select_field()
381
         role_list = role_api.get_roles_for_select_field()
393
                                          role_types=role_list)
386
                                          role_types=role_list)
394
         fake_api = Context(CTX.ADMIN_USER).toDict(fake_api_content)
387
         fake_api = Context(CTX.ADMIN_USER).toDict(fake_api_content)
395
 
388
 
396
-        return DictLikeClass(result = dictified_user, fake_api=fake_api)
397
-
389
+        return DictLikeClass(result=dictified_user, fake_api=fake_api)
398
 
390
 
399
     @tg.expose('tracim.templates.admin.user_edit')
391
     @tg.expose('tracim.templates.admin.user_edit')
400
     def edit(self, id):
392
     def edit(self, id):
422
             tg.redirect(next_url)
414
             tg.redirect(next_url)
423
         tg.redirect(self.url())
415
         tg.redirect(self.url())
424
 
416
 
425
-
426
     @tg.require(predicates.in_group(Group.TIM_ADMIN_GROUPNAME))
417
     @tg.require(predicates.in_group(Group.TIM_ADMIN_GROUPNAME))
427
     @tg.expose()
418
     @tg.expose()
428
     def enable(self, id, next_url=None):
419
     def enable(self, id, next_url=None):
434
         api.save(user)
425
         api.save(user)
435
 
426
 
436
         tg.flash(_('User {} enabled.').format(user.get_display_name()), CST.STATUS_OK)
427
         tg.flash(_('User {} enabled.').format(user.get_display_name()), CST.STATUS_OK)
437
-        if next_url=='user':
428
+        if next_url == 'user':
438
             tg.redirect(self.url(id=user.user_id))
429
             tg.redirect(self.url(id=user.user_id))
439
         tg.redirect(self.url())
430
         tg.redirect(self.url())
440
 
431
 
445
         current_user = tmpl_context.current_user
436
         current_user = tmpl_context.current_user
446
         api = UserApi(current_user)
437
         api = UserApi(current_user)
447
 
438
 
448
-        if current_user.user_id==id:
439
+        if current_user.user_id == id:
449
             tg.flash(_('You can\'t de-activate your own account'), CST.STATUS_ERROR)
440
             tg.flash(_('You can\'t de-activate your own account'), CST.STATUS_ERROR)
450
         else:
441
         else:
451
             user = api.get_one(id)
442
             user = api.get_one(id)
453
             api.save(user)
444
             api.save(user)
454
             tg.flash(_('User {} disabled').format(user.get_display_name()), CST.STATUS_OK)
445
             tg.flash(_('User {} disabled').format(user.get_display_name()), CST.STATUS_OK)
455
 
446
 
456
-        if next_url=='user':
447
+        if next_url == 'user':
457
             tg.redirect(self.url(id=user.user_id))
448
             tg.redirect(self.url(id=user.user_id))
458
         tg.redirect(self.url())
449
         tg.redirect(self.url())

+ 0 - 1
tracim/tracim/controllers/user.py View File

109
             tg.redirect(redirect_url)
109
             tg.redirect(redirect_url)
110
 
110
 
111
         current_user.password = new_password1
111
         current_user.password = new_password1
112
-        current_user.update_webdav_digest_auth(new_password1)
113
         pm.DBSession.flush()
112
         pm.DBSession.flush()
114
 
113
 
115
         tg.flash(_('Your password has been changed'))
114
         tg.flash(_('Your password has been changed'))

+ 41 - 29
tracim/tracim/lib/auth/internal.py View File

1
 # -*- coding: utf-8 -*-
1
 # -*- coding: utf-8 -*-
2
-from sqlalchemy import and_
2
+from typing import Dict
3
+
3
 from tg.configuration.auth import TGAuthMetadata
4
 from tg.configuration.auth import TGAuthMetadata
4
 
5
 
5
 from tracim.lib.auth.base import Auth
6
 from tracim.lib.auth.base import Auth
6
 from tracim.model import DBSession, User
7
 from tracim.model import DBSession, User
7
 
8
 
8
-# TODO : temporary fix to update DB, to remove
9
-import transaction
10
 
9
 
11
 class InternalAuth(Auth):
10
 class InternalAuth(Auth):
12
 
11
 
13
     name = 'internal'
12
     name = 'internal'
14
     _internal = True
13
     _internal = True
15
 
14
 
16
-    def feed_config(self):
17
-        """
18
-        Fill config with internal (database) auth information.
19
-        :return:
20
-        """
15
+    def feed_config(self) -> None:
16
+        """Fill config with internal (database) auth information."""
21
         super().feed_config()
17
         super().feed_config()
22
         self._config['sa_auth'].user_class = User
18
         self._config['sa_auth'].user_class = User
23
         self._config['auth_backend'] = 'sqlalchemy'
19
         self._config['auth_backend'] = 'sqlalchemy'
24
         self._config['sa_auth'].dbsession = DBSession
20
         self._config['sa_auth'].dbsession = DBSession
25
-        self._config['sa_auth'].authmetadata = InternalApplicationAuthMetadata(self._config.get('sa_auth'))
21
+        self._config['sa_auth'].authmetadata = \
22
+            InternalApplicationAuthMetadata(self._config.get('sa_auth'))
26
 
23
 
27
 
24
 
28
 class InternalApplicationAuthMetadata(TGAuthMetadata):
25
 class InternalApplicationAuthMetadata(TGAuthMetadata):
26
+
29
     def __init__(self, sa_auth):
27
     def __init__(self, sa_auth):
30
         self.sa_auth = sa_auth
28
         self.sa_auth = sa_auth
31
 
29
 
32
-    def authenticate(self, environ, identity, allow_auth_token: bool=False):
33
-        user = self.sa_auth.dbsession.query(self.sa_auth.user_class).filter(and_(
34
-            self.sa_auth.user_class.is_active == True,
35
-            self.sa_auth.user_class.email == identity['login']
36
-        )).first()
37
-
38
-        if user and user.validate_password(identity['password']):
39
-            if not user.webdav_left_digest_response_hash:
40
-                user.webdav_left_digest_response_hash = '%s:/:%s' % (identity['login'], identity['password'])
41
-                DBSession.flush()
42
-                # TODO : temporary fix to update DB, to remove
43
-                transaction.commit()
44
-            return identity['login']
45
-
46
-        if user and allow_auth_token:
47
-            user.ensure_auth_token()
48
-            if user.auth_token == identity['password']:
49
-                return identity['login']
30
+    def authenticate(
31
+            self,
32
+            environ: Dict[str, str],
33
+            identity: Dict[str, str],
34
+            allow_auth_token: bool = False,
35
+    ) -> str:
36
+        """
37
+        Authenticate using given credentials.
38
+
39
+        Checks password first then auth token if allowed.
40
+        :param environ:
41
+        :param identity: The given credentials to authenticate.
42
+        :param allow_auth_token: The indicator of auth token use.
43
+        :return: The given login or an empty string if auth failed.
44
+        """
45
+        result = ''
46
+        user = self.sa_auth.dbsession \
47
+            .query(self.sa_auth.user_class) \
48
+            .filter(self.sa_auth.user_class.is_active.is_(True)) \
49
+            .filter(self.sa_auth.user_class.email == identity['login']) \
50
+            .first()
51
+        if user:
52
+            if user.validate_password(identity['password']):
53
+                result = identity['login']
54
+            if allow_auth_token:
55
+                user.ensure_auth_token()
56
+                if user.auth_token == identity['password']:
57
+                    result = identity['login']
58
+        return result
50
 
59
 
51
     def get_user(self, identity, userid):
60
     def get_user(self, identity, userid):
52
-        return self.sa_auth.dbsession.query(self.sa_auth.user_class).filter(
53
-            and_(self.sa_auth.user_class.is_active == True, self.sa_auth.user_class.email == userid)).first()
61
+        return self.sa_auth.dbsession \
62
+            .query(self.sa_auth.user_class) \
63
+            .filter(self.sa_auth.user_class.is_active.is_(True)) \
64
+            .filter(self.sa_auth.user_class.email == userid) \
65
+            .first()
54
 
66
 
55
     def get_groups(self, identity, userid):
67
     def get_groups(self, identity, userid):
56
         return [g.group_name for g in identity['user'].groups]
68
         return [g.group_name for g in identity['user'].groups]

+ 63 - 50
tracim/tracim/model/auth.py View File

6
 
6
 
7
 It's perfectly fine to re-use this definition in the tracim application,
7
 It's perfectly fine to re-use this definition in the tracim application,
8
 though.
8
 though.
9
-
10
 """
9
 """
10
+import os
11
+import time
11
 import uuid
12
 import uuid
12
 
13
 
13
-import os
14
 from datetime import datetime
14
 from datetime import datetime
15
-import time
16
-from hashlib import sha256
17
-from sqlalchemy.ext.hybrid import hybrid_property
18
-from tracim.lib.utils import lazy_ugettext as l_
19
 from hashlib import md5
15
 from hashlib import md5
20
-
21
-__all__ = ['User', 'Group', 'Permission']
16
+from hashlib import sha256
17
+from typing import TYPE_CHECKING
22
 
18
 
23
 from sqlalchemy import Column
19
 from sqlalchemy import Column
24
 from sqlalchemy import ForeignKey
20
 from sqlalchemy import ForeignKey
25
 from sqlalchemy import Sequence
21
 from sqlalchemy import Sequence
26
 from sqlalchemy import Table
22
 from sqlalchemy import Table
27
-
28
-from sqlalchemy.types import Unicode
29
-from sqlalchemy.types import Integer
30
-from sqlalchemy.types import DateTime
23
+from sqlalchemy.ext.hybrid import hybrid_property
24
+from sqlalchemy.orm import relation
25
+from sqlalchemy.orm import relationship
26
+from sqlalchemy.orm import synonym
31
 from sqlalchemy.types import Boolean
27
 from sqlalchemy.types import Boolean
32
-from sqlalchemy.orm import relation, relationship, synonym
33
-from tg import request
34
-from tracim.model import DeclarativeBase, metadata, DBSession
28
+from sqlalchemy.types import DateTime
29
+from sqlalchemy.types import Integer
30
+from sqlalchemy.types import Unicode
31
+
32
+from tracim.lib.utils import lazy_ugettext as l_
33
+from tracim.model import DBSession
34
+from tracim.model import DeclarativeBase
35
+from tracim.model import metadata
36
+if TYPE_CHECKING:
37
+    from tracim.model.data import Workspace
38
+
39
+__all__ = ['User', 'Group', 'Permission']
35
 
40
 
36
 # This is the association table for the many-to-many relationship between
41
 # This is the association table for the many-to-many relationship between
37
 # groups and permissions.
42
 # groups and permissions.
51
         onupdate="CASCADE", ondelete="CASCADE"), primary_key=True)
56
         onupdate="CASCADE", ondelete="CASCADE"), primary_key=True)
52
 )
57
 )
53
 
58
 
59
+
54
 class Group(DeclarativeBase):
60
 class Group(DeclarativeBase):
55
 
61
 
56
     TIM_NOBODY = 0
62
     TIM_NOBODY = 0
84
         return DBSession.query(cls).filter_by(group_name=group_name).first()
90
         return DBSession.query(cls).filter_by(group_name=group_name).first()
85
 
91
 
86
 
92
 
87
-
88
 class Profile(object):
93
 class Profile(object):
89
-    """ This model is the "max" group associated to a given user
90
-    """
94
+    """This model is the "max" group associated to a given user."""
91
 
95
 
92
     _NAME = [Group.TIM_NOBODY_GROUPNAME,
96
     _NAME = [Group.TIM_NOBODY_GROUPNAME,
93
              Group.TIM_USER_GROUPNAME,
97
              Group.TIM_USER_GROUPNAME,
106
         self.label = Profile._LABEL[profile_id]
110
         self.label = Profile._LABEL[profile_id]
107
 
111
 
108
 
112
 
109
-
110
 class User(DeclarativeBase):
113
 class User(DeclarativeBase):
111
     """
114
     """
112
     User definition.
115
     User definition.
113
 
116
 
114
     This is the user definition used by :mod:`repoze.who`, which requires at
117
     This is the user definition used by :mod:`repoze.who`, which requires at
115
     least the ``email`` column.
118
     least the ``email`` column.
116
-
117
     """
119
     """
120
+
118
     __tablename__ = 'users'
121
     __tablename__ = 'users'
119
 
122
 
120
     user_id = Column(Integer, Sequence('seq__users__user_id'), autoincrement=True, primary_key=True)
123
     user_id = Column(Integer, Sequence('seq__users__user_id'), autoincrement=True, primary_key=True)
151
     @property
154
     @property
152
     def profile(self) -> Profile:
155
     def profile(self) -> Profile:
153
         profile_id = 0
156
         profile_id = 0
154
-        if len(self.groups)>0:
157
+        if len(self.groups) > 0:
155
             profile_id = max(group.group_id for group in self.groups)
158
             profile_id = max(group.group_id for group in self.groups)
156
         return Profile(profile_id)
159
         return Profile(profile_id)
157
 
160
 
174
         return DBSession.query(cls).filter_by(email=username).first()
177
         return DBSession.query(cls).filter_by(email=username).first()
175
 
178
 
176
     @classmethod
179
     @classmethod
177
-    def _hash_password(cls, password):
180
+    def _hash_password(cls, cleartext_password: str) -> str:
178
         salt = sha256()
181
         salt = sha256()
179
         salt.update(os.urandom(60))
182
         salt.update(os.urandom(60))
180
         salt = salt.hexdigest()
183
         salt = salt.hexdigest()
181
 
184
 
182
         hash = sha256()
185
         hash = sha256()
183
         # Make sure password is a str because we cannot hash unicode objects
186
         # Make sure password is a str because we cannot hash unicode objects
184
-        hash.update((password + salt).encode('utf-8'))
187
+        hash.update((cleartext_password + salt).encode('utf-8'))
185
         hash = hash.hexdigest()
188
         hash = hash.hexdigest()
186
 
189
 
187
-        password = salt + hash
190
+        ciphertext_password = salt + hash
188
 
191
 
189
         # Make sure the hashed password is a unicode object at the end of the
192
         # Make sure the hashed password is a unicode object at the end of the
190
         # process because SQLAlchemy _wants_ unicode objects for Unicode cols
193
         # process because SQLAlchemy _wants_ unicode objects for Unicode cols
191
         # FIXME - D.A. - 2013-11-20 - The following line has been removed since using python3. Is this normal ?!
194
         # FIXME - D.A. - 2013-11-20 - The following line has been removed since using python3. Is this normal ?!
192
         # password = password.decode('utf-8')
195
         # password = password.decode('utf-8')
193
 
196
 
194
-        return password
197
+        return ciphertext_password
195
 
198
 
196
-    def _set_password(self, password):
197
-        """Hash ``password`` on the fly and store its hashed version."""
198
-        self._password = self._hash_password(password)
199
+    def _set_password(self, cleartext_password: str) -> None:
200
+        """
201
+        Set ciphertext password from cleartext password.
199
 
202
 
200
-    def _get_password(self):
203
+        Hash cleartext password on the fly,
204
+        Store its ciphertext version,
205
+        Update the WebDAV hash as well.
206
+        """
207
+        self._password = self._hash_password(cleartext_password)
208
+        self.update_webdav_digest_auth(cleartext_password)
209
+
210
+    def _get_password(self) -> str:
201
         """Return the hashed version of the password."""
211
         """Return the hashed version of the password."""
202
         return self._password
212
         return self._password
203
 
213
 
216
 
226
 
217
     webdav_left_digest_response_hash = synonym('_webdav_left_digest_response_hash',
227
     webdav_left_digest_response_hash = synonym('_webdav_left_digest_response_hash',
218
                                                descriptor=property(_get_hash_digest,
228
                                                descriptor=property(_get_hash_digest,
219
-                                                                    _set_hash_digest))
229
+                                                                   _set_hash_digest))
220
 
230
 
221
-    def update_webdav_digest_auth(self, password) -> None:
231
+    def update_webdav_digest_auth(self, cleartext_password: str) -> None:
222
         self.webdav_left_digest_response_hash \
232
         self.webdav_left_digest_response_hash \
223
-            = '{username}:/:{password}'.format(
233
+            = '{username}:/:{cleartext_password}'.format(
224
                 username=self.email,
234
                 username=self.email,
225
-                password=password,
235
+                cleartext_password=cleartext_password,
226
             )
236
             )
227
 
237
 
228
-
229
-    def validate_password(self, password):
238
+    def validate_password(self, cleartext_password: str) -> bool:
230
         """
239
         """
231
         Check the password against existing credentials.
240
         Check the password against existing credentials.
232
 
241
 
233
-        :param password: the password that was provided by the user to
234
-            try and authenticate. This is the clear text version that we will
235
-            need to match against the hashed one in the database.
236
-        :type password: unicode object.
242
+        :param cleartext_password: the password that was provided by the user
243
+            to try and authenticate. This is the clear text version that we
244
+            will need to match against the hashed one in the database.
245
+        :type cleartext_password: unicode object.
237
         :return: Whether the password is valid.
246
         :return: Whether the password is valid.
238
         :rtype: bool
247
         :rtype: bool
239
 
248
 
240
         """
249
         """
241
-        if not self.password:
242
-            return False
243
-        hash = sha256()
244
-        hash.update((password + self.password[:64]).encode('utf-8'))
245
-        return self.password[64:] == hash.hexdigest()
246
-
247
-    def get_display_name(self, remove_email_part=False):
250
+        result = False
251
+        if self.password:
252
+            hash = sha256()
253
+            hash.update((cleartext_password + self.password[:64]).encode('utf-8'))
254
+            result = self.password[64:] == hash.hexdigest()
255
+            if result and not self.webdav_left_digest_response_hash:
256
+                self.update_webdav_digest_auth(cleartext_password)
257
+        return result
258
+
259
+    def get_display_name(self, remove_email_part: bool=False) -> str:
248
         """
260
         """
261
+        Get a name to display from corresponding member or email.
262
+
249
         :param remove_email_part: If True and display name based on email,
263
         :param remove_email_part: If True and display name based on email,
250
-         remove @xxx.xxx part of email in returned value
264
+            remove @xxx.xxx part of email in returned value
251
         :return: display name based on user name or email.
265
         :return: display name based on user name or email.
252
         """
266
         """
253
-        if self.display_name != None and self.display_name != '':
267
+        if self.display_name:
254
             return self.display_name
268
             return self.display_name
255
         else:
269
         else:
256
             if remove_email_part:
270
             if remove_email_part:
269
     def ensure_auth_token(self) -> None:
283
     def ensure_auth_token(self) -> None:
270
         """
284
         """
271
         Create auth_token if None, regenerate auth_token if too much old.
285
         Create auth_token if None, regenerate auth_token if too much old.
286
+
272
         auth_token validity is set in
287
         auth_token validity is set in
273
         :return:
288
         :return:
274
         """
289
         """
301
 
316
 
302
     __tablename__ = 'permissions'
317
     __tablename__ = 'permissions'
303
 
318
 
304
-
305
     permission_id = Column(Integer, Sequence('seq__permissions__permission_id'), autoincrement=True, primary_key=True)
319
     permission_id = Column(Integer, Sequence('seq__permissions__permission_id'), autoincrement=True, primary_key=True)
306
     permission_name = Column(Unicode(63), unique=True, nullable=False)
320
     permission_name = Column(Unicode(63), unique=True, nullable=False)
307
     description = Column(Unicode(255))
321
     description = Column(Unicode(255))
314
 
328
 
315
     def __unicode__(self):
329
     def __unicode__(self):
316
         return self.permission_name
330
         return self.permission_name
317
-