Ver código fonte

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

Damien Accorsi 7 anos atrás
pai
commit
134074f376

+ 0 - 2
tracim/tracim/command/user.py Ver arquivo

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

+ 58 - 67
tracim/tracim/controllers/admin/user.py Ver arquivo

@@ -1,26 +1,14 @@
1 1
 # -*- coding: utf-8 -*-
2
-import uuid
3 2
 import random
4 3
 
5 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 7
 import tg
14 8
 from tg import predicates
15 9
 from tg import tmpl_context
16 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 12
 from tracim.controllers import TIMRestController
25 13
 from tracim.controllers.user import UserWorkspaceRestController
26 14
 
@@ -28,19 +16,20 @@ from tracim.lib import CST
28 16
 from tracim.lib import helpers as h
29 17
 from tracim.lib.base import logger
30 18
 from tracim.lib.email import get_email_manager
31
-from tracim.lib.user import UserApi
32 19
 from tracim.lib.group import GroupApi
20
+from tracim.lib.user import UserApi
33 21
 from tracim.lib.userworkspace import RoleApi
34 22
 from tracim.lib.workspace import WorkspaceApi
35 23
 
36 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 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 34
     allow_only = predicates.in_group(Group.TIM_ADMIN_GROUPNAME)
46 35
 
@@ -51,14 +40,15 @@ class UserProfileAdminRestController(TIMRestController):
51 40
     @property
52 41
     def allowed_profiles(self):
53 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 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 52
         :param args:
63 53
         :param kw:
64 54
         :return:
@@ -72,10 +62,14 @@ class UserProfileAdminRestController(TIMRestController):
72 62
         tg.tmpl_context.user = user
73 63
 
74 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 74
         return self.put(new_role)
81 75
 
@@ -87,11 +81,10 @@ class UserProfileAdminRestController(TIMRestController):
87 81
 
88 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 85
             tg.flash(_('You can\'t change your own profile'), CST.STATUS_ERROR)
92 86
             tg.redirect(self.parent_controller.url())
93 87
 
94
-
95 88
         redirect_url = self.parent_controller.url(skip_id=True)
96 89
 
97 90
         if new_profile not in self.allowed_profiles:
@@ -102,9 +95,10 @@ class UserProfileAdminRestController(TIMRestController):
102 95
         pod_manager_group = group_api.get_one(Group.TIM_MANAGER)
103 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 102
             if pod_user_group not in user.groups:
109 103
                 user.groups.append(pod_user_group)
110 104
 
@@ -120,7 +114,7 @@ class UserProfileAdminRestController(TIMRestController):
120 114
 
121 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 118
             if pod_user_group not in user.groups:
125 119
                 user.groups.append(pod_user_group)
126 120
             if pod_manager_group not in user.groups:
@@ -133,8 +127,7 @@ class UserProfileAdminRestController(TIMRestController):
133 127
 
134 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 131
             if pod_user_group not in user.groups:
139 132
                 user.groups.append(pod_user_group)
140 133
             if pod_manager_group not in user.groups:
@@ -145,7 +138,9 @@ class UserProfileAdminRestController(TIMRestController):
145 138
             flash_message = _('User {} is now an administrator').format(user.get_display_name())
146 139
 
147 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 144
             tg.flash(_('Unknown profile'), CST.STATUS_ERROR)
150 145
             tg.redirect(redirect_url)
151 146
 
@@ -163,17 +158,18 @@ class UserProfileAdminRestController(TIMRestController):
163 158
         pass
164 159
 
165 160
 
166
-
167 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 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 173
         :param args:
178 174
         :param kw:
179 175
         :return:
@@ -186,13 +182,12 @@ class UserPasswordAdminRestController(TIMRestController):
186 182
         tg.tmpl_context.user_id = user_id
187 183
         tg.tmpl_context.user = user
188 184
 
189
-
190 185
     @tg.expose('tracim.templates.admin.user_password_edit')
191 186
     def edit(self):
192 187
         current_user = tmpl_context.current_user
193 188
         api = UserApi(current_user)
194 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 192
     @tg.expose()
198 193
     def put(self, new_password1, new_password2, next_url=''):
@@ -207,12 +202,11 @@ class UserPasswordAdminRestController(TIMRestController):
207 202
             tg.flash(_('Empty password is not allowed.'), CST.STATUS_ERROR)
208 203
             tg.redirect(next_url)
209 204
 
210
-        if new_password1!=new_password2:
205
+        if new_password1 != new_password2:
211 206
             tg.flash(_('New passwords do not match.'), CST.STATUS_ERROR)
212 207
             tg.redirect(next_url)
213 208
 
214 209
         user.password = new_password1
215
-        user.update_webdav_digest_auth(new_password1)
216 210
         pm.DBSession.flush()
217 211
 
218 212
         tg.flash(_('The password has been changed'), CST.STATUS_OK)
@@ -223,7 +217,8 @@ class UserWorkspaceRestController(TIMRestController):
223 217
 
224 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 222
         :param args:
228 223
         :param kw:
229 224
         :return:
@@ -266,10 +261,12 @@ class UserWorkspaceRestController(TIMRestController):
266 261
 
267 262
 
268 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 271
     password = UserPasswordAdminRestController()
275 272
     profile = UserProfileAdminRestController()
@@ -284,7 +281,6 @@ class UserRestController(TIMRestController):
284 281
     def current_item_id_key_in_context(cls):
285 282
         return 'user_id'
286 283
 
287
-
288 284
     @tg.require(predicates.in_group(Group.TIM_MANAGER_GROUPNAME))
289 285
     @tg.expose('tracim.templates.admin.user_getall')
290 286
     def get_all(self, *args, **kw):
@@ -297,7 +293,7 @@ class UserRestController(TIMRestController):
297 293
         fake_api = Context(CTX.USERS).toDict({'current_user': current_user_content})
298 294
 
299 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 298
     @tg.require(predicates.in_group(Group.TIM_MANAGER_GROUPNAME))
303 299
     @tg.expose()
@@ -336,8 +332,6 @@ class UserRestController(TIMRestController):
336 332
             password = self.generate_password()
337 333
             user.password = password
338 334
 
339
-        user.webdav_left_digest_response_hash = '%s:/:%s' % (email, password)
340
-
341 335
         api.save(user)
342 336
 
343 337
         # Now add the user to related groups
@@ -361,14 +355,13 @@ class UserRestController(TIMRestController):
361 355
     @classmethod
362 356
     def generate_password(
363 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 361
         # character list that will be contained into the password
369 362
         char_list = []
370 363
 
371
-        for j in range(0, password_length):
364
+        for _unused in range(password_length):
372 365
             # This puts a random char from the list above inside
373 366
             # the list of chars and then merges them into a String
374 367
             char_list.append(random.choice(password_chars))
@@ -378,11 +371,11 @@ class UserRestController(TIMRestController):
378 371
     @tg.expose('tracim.templates.admin.user_getone')
379 372
     def get_one(self, user_id):
380 373
         current_user = tmpl_context.current_user
381
-        api = UserApi(current_user )
374
+        api = UserApi(current_user)
382 375
         # role_api = RoleApi(tg.tmpl_context.current_user)
383 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 380
         role_api = RoleApi(tg.tmpl_context.current_user)
388 381
         role_list = role_api.get_roles_for_select_field()
@@ -393,8 +386,7 @@ class UserRestController(TIMRestController):
393 386
                                          role_types=role_list)
394 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 391
     @tg.expose('tracim.templates.admin.user_edit')
400 392
     def edit(self, id):
@@ -422,7 +414,6 @@ class UserRestController(TIMRestController):
422 414
             tg.redirect(next_url)
423 415
         tg.redirect(self.url())
424 416
 
425
-
426 417
     @tg.require(predicates.in_group(Group.TIM_ADMIN_GROUPNAME))
427 418
     @tg.expose()
428 419
     def enable(self, id, next_url=None):
@@ -434,7 +425,7 @@ class UserRestController(TIMRestController):
434 425
         api.save(user)
435 426
 
436 427
         tg.flash(_('User {} enabled.').format(user.get_display_name()), CST.STATUS_OK)
437
-        if next_url=='user':
428
+        if next_url == 'user':
438 429
             tg.redirect(self.url(id=user.user_id))
439 430
         tg.redirect(self.url())
440 431
 
@@ -445,7 +436,7 @@ class UserRestController(TIMRestController):
445 436
         current_user = tmpl_context.current_user
446 437
         api = UserApi(current_user)
447 438
 
448
-        if current_user.user_id==id:
439
+        if current_user.user_id == id:
449 440
             tg.flash(_('You can\'t de-activate your own account'), CST.STATUS_ERROR)
450 441
         else:
451 442
             user = api.get_one(id)
@@ -453,6 +444,6 @@ class UserRestController(TIMRestController):
453 444
             api.save(user)
454 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 448
             tg.redirect(self.url(id=user.user_id))
458 449
         tg.redirect(self.url())

+ 0 - 1
tracim/tracim/controllers/user.py Ver arquivo

@@ -109,7 +109,6 @@ class UserPasswordRestController(TIMRestController):
109 109
             tg.redirect(redirect_url)
110 110
 
111 111
         current_user.password = new_password1
112
-        current_user.update_webdav_digest_auth(new_password1)
113 112
         pm.DBSession.flush()
114 113
 
115 114
         tg.flash(_('Your password has been changed'))

+ 41 - 29
tracim/tracim/lib/auth/internal.py Ver arquivo

@@ -1,56 +1,68 @@
1 1
 # -*- coding: utf-8 -*-
2
-from sqlalchemy import and_
2
+from typing import Dict
3
+
3 4
 from tg.configuration.auth import TGAuthMetadata
4 5
 
5 6
 from tracim.lib.auth.base import Auth
6 7
 from tracim.model import DBSession, User
7 8
 
8
-# TODO : temporary fix to update DB, to remove
9
-import transaction
10 9
 
11 10
 class InternalAuth(Auth):
12 11
 
13 12
     name = 'internal'
14 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 17
         super().feed_config()
22 18
         self._config['sa_auth'].user_class = User
23 19
         self._config['auth_backend'] = 'sqlalchemy'
24 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 25
 class InternalApplicationAuthMetadata(TGAuthMetadata):
26
+
29 27
     def __init__(self, sa_auth):
30 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 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 67
     def get_groups(self, identity, userid):
56 68
         return [g.group_name for g in identity['user'].groups]

+ 63 - 50
tracim/tracim/model/auth.py Ver arquivo

@@ -6,32 +6,37 @@ This is where the models used by the authentication stack are defined.
6 6
 
7 7
 It's perfectly fine to re-use this definition in the tracim application,
8 8
 though.
9
-
10 9
 """
10
+import os
11
+import time
11 12
 import uuid
12 13
 
13
-import os
14 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 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 19
 from sqlalchemy import Column
24 20
 from sqlalchemy import ForeignKey
25 21
 from sqlalchemy import Sequence
26 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 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 41
 # This is the association table for the many-to-many relationship between
37 42
 # groups and permissions.
@@ -51,6 +56,7 @@ user_group_table = Table('user_group', metadata,
51 56
         onupdate="CASCADE", ondelete="CASCADE"), primary_key=True)
52 57
 )
53 58
 
59
+
54 60
 class Group(DeclarativeBase):
55 61
 
56 62
     TIM_NOBODY = 0
@@ -84,10 +90,8 @@ class Group(DeclarativeBase):
84 90
         return DBSession.query(cls).filter_by(group_name=group_name).first()
85 91
 
86 92
 
87
-
88 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 96
     _NAME = [Group.TIM_NOBODY_GROUPNAME,
93 97
              Group.TIM_USER_GROUPNAME,
@@ -106,15 +110,14 @@ class Profile(object):
106 110
         self.label = Profile._LABEL[profile_id]
107 111
 
108 112
 
109
-
110 113
 class User(DeclarativeBase):
111 114
     """
112 115
     User definition.
113 116
 
114 117
     This is the user definition used by :mod:`repoze.who`, which requires at
115 118
     least the ``email`` column.
116
-
117 119
     """
120
+
118 121
     __tablename__ = 'users'
119 122
 
120 123
     user_id = Column(Integer, Sequence('seq__users__user_id'), autoincrement=True, primary_key=True)
@@ -151,7 +154,7 @@ class User(DeclarativeBase):
151 154
     @property
152 155
     def profile(self) -> Profile:
153 156
         profile_id = 0
154
-        if len(self.groups)>0:
157
+        if len(self.groups) > 0:
155 158
             profile_id = max(group.group_id for group in self.groups)
156 159
         return Profile(profile_id)
157 160
 
@@ -174,30 +177,37 @@ class User(DeclarativeBase):
174 177
         return DBSession.query(cls).filter_by(email=username).first()
175 178
 
176 179
     @classmethod
177
-    def _hash_password(cls, password):
180
+    def _hash_password(cls, cleartext_password: str) -> str:
178 181
         salt = sha256()
179 182
         salt.update(os.urandom(60))
180 183
         salt = salt.hexdigest()
181 184
 
182 185
         hash = sha256()
183 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 188
         hash = hash.hexdigest()
186 189
 
187
-        password = salt + hash
190
+        ciphertext_password = salt + hash
188 191
 
189 192
         # Make sure the hashed password is a unicode object at the end of the
190 193
         # process because SQLAlchemy _wants_ unicode objects for Unicode cols
191 194
         # FIXME - D.A. - 2013-11-20 - The following line has been removed since using python3. Is this normal ?!
192 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 211
         """Return the hashed version of the password."""
202 212
         return self._password
203 213
 
@@ -216,41 +226,45 @@ class User(DeclarativeBase):
216 226
 
217 227
     webdav_left_digest_response_hash = synonym('_webdav_left_digest_response_hash',
218 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 232
         self.webdav_left_digest_response_hash \
223
-            = '{username}:/:{password}'.format(
233
+            = '{username}:/:{cleartext_password}'.format(
224 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 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 246
         :return: Whether the password is valid.
238 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 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 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 268
             return self.display_name
255 269
         else:
256 270
             if remove_email_part:
@@ -269,6 +283,7 @@ class User(DeclarativeBase):
269 283
     def ensure_auth_token(self) -> None:
270 284
         """
271 285
         Create auth_token if None, regenerate auth_token if too much old.
286
+
272 287
         auth_token validity is set in
273 288
         :return:
274 289
         """
@@ -301,7 +316,6 @@ class Permission(DeclarativeBase):
301 316
 
302 317
     __tablename__ = 'permissions'
303 318
 
304
-
305 319
     permission_id = Column(Integer, Sequence('seq__permissions__permission_id'), autoincrement=True, primary_key=True)
306 320
     permission_name = Column(Unicode(63), unique=True, nullable=False)
307 321
     description = Column(Unicode(255))
@@ -314,4 +328,3 @@ class Permission(DeclarativeBase):
314 328
 
315 329
     def __unicode__(self):
316 330
         return self.permission_name
317
-